アドレス帳
アドレス帳の例では、プロキシモデルを使用して、単一のモデルから取得したデータを異なるビューで表示する方法を示しています。

この例では、連絡先をアルファベット順に「ABC」、「DEF」、「GHI」、「...」、「VW」、「...」、「XYZ」の 9 つのグループに分類できるアドレス帳を提供しています。これは、同じモデルに対して複数のビューを使用し、それぞれをQSortFilterProxyModel クラスのインスタンスを用いてフィルタリングすることで実現されています。
概要
アドレス帳には、MainWindow 、AddressWidget 、NewAddressTab 、AddDialog の4つのクラスが含まれています。MainWindow クラスは、AddressWidget を中央ウィジェットとして使用し、File およびTools メニューを提供します。

AddressWidget クラスは、QTabWidget のサブクラスであり、例で表示される10個のタブ(9つのアルファベットグループタブとNewAddressTab のインスタンス)を操作するために使用されます。NewAddressTab クラスはQWidget のサブクラスであり、アドレス帳が空の場合にのみ使用され、ユーザーに連絡先を追加するよう促します。AddressWidget また、TableModel のインスタンスと連携して、アドレス帳へのエントリの追加、編集、削除を行います。
struct Contact は連絡先を保存し、QDataStream を通じて比較およびシリアライズ機能を提供します:
struct Contact
{
private:
Q_GADGET
Q_PROPERTY(QString name MEMBER name)
Q_PROPERTY(QString address MEMBER address)
public:
QString name;
QString address;
friend bool comparesEqual(const Contact &lhs, const Contact &rhs) noexcept
{
return lhs.name == rhs.name && lhs.address == rhs.address;
}
friend Qt::strong_ordering compareThreeWay(const Contact &lhs, const Contact &rhs) noexcept
{
int cmp = lhs.name.compare(rhs.name);
if (cmp == 0)
cmp = lhs.address.compare(rhs.address);
return Qt::compareThreeWay(cmp, 0);
}
Q_DECLARE_STRONGLY_ORDERED(Contact)
};
inline QDataStream &operator<<(QDataStream &stream, const Contact &contact)
{
return stream << contact.name << contact.address;
}
inline QDataStream &operator>>(QDataStream &stream, Contact &contact)
{
return stream >> contact.name >> contact.address;
}TableModel はQAbstractTableModel のサブクラスであり、データにアクセスするための標準的なモデル/ビューAPIを提供します。追加された連絡先のリストを保持しています。ただし、このデータは1つのタブにすべて表示されるわけではありません。代わりに、QTableView が使用され、アルファベットグループに応じて、同じデータに対して9つの異なるビューを提供します。
QSortFilterProxyModel は、各連絡先グループごとに連絡先をフィルタリングする役割を担うクラスです。各プロキシモデルは、QRegularExpression を使用して、対応するアルファベットグループに属さない連絡先を除外します。AddDialog クラスは、アドレス帳に関する情報をユーザーから取得するために使用されます。このQDialog のサブクラスは、連絡先を追加するためにNewAddressTab によって、また連絡先の追加および編集のためにAddressWidget によってインスタンス化されます。
AddressWidget クラスの定義
AddressWidget クラスは、連絡先の追加、編集、削除、ファイルへの保存、およびファイルからの読み込みを行う機能を提供するため、技術的にはこの例において中心となるクラスです。
class AddressWidget : public QTabWidget
{
Q_OBJECT
public:
explicit AddressWidget(QWidget *parent = nullptr);
bool readFromFile();
bool writeToFile();
static QString fileName();
public slots:
void showAddEntryDialog();
void addEntry(const Contact &contact);
void editEntry();
void removeEntry();
signals:
void selectionChanged (const QItemSelection &selected);
private:
void setupTabs();
NewAddressTab *newAddressTab;
QList<Contact> contacts;
using Adapter = decltype(QRangeModelAdapter(std::ref(contacts)));
Adapter adapter;
};AddressWidget は、10個のタブ(NewAddressTab および9つのアルファベットグループタブ)を保持するためにQTabWidget を継承しており、また、table (TableModel オブジェクト)、proxyModel (エントリのフィルタリングに使用するQSortFilterProxyModel オブジェクト)、およびtableView (QTableView オブジェクト)を操作します。
これは、QList に連絡先を格納し、QRangeModelAdapter を使用して操作を行います。
QRangeModel::RowOptions の特化形を通じて、struct Contact の行ヘッダー値を提供します:
template <>
struct QRangeModel::RowOptions<Contact>
{
inline static QVariant headerData(int section, int role);
};
QVariant QRangeModel::RowOptions<Contact>::headerData(int section, int role)
{
if (role == Qt::DisplayRole) {
switch (section) {
case 0:
return AddressWidget::tr("Name");
case 1:
return AddressWidget::tr("Address");
default:
break;
}
}
return {};
}AddressWidget クラスの実装
AddressWidget のコンストラクタは親ウィジェットを受け取り、NewAddressTab 、TableModel 、およびQSortFilterProxyModel をインスタンス化します。アドレス帳が空であることを示すために使用されるNewAddressTab オブジェクトが追加され、残りの9つのタブはsetupTabs() を使用して設定されます。
AddressWidget::AddressWidget(QWidget *parent)
: QTabWidget(parent),
newAddressTab(new NewAddressTab(this)),
adapter(std::ref(contacts))
{
connect(newAddressTab, &NewAddressTab::triggered, this, &AddressWidget::showAddEntryDialog);
addTab(newAddressTab, tr("Address Book"));
setupTabs();
}setupTabs() 関数を使用して、AddressWidget 内の9つのアルファベットグループタブ、テーブルビュー、およびプロキシモデルを設定します。各プロキシモデルは、大文字小文字を区別しないQRegularExpression オブジェクトを使用して、関連するアルファベットグループに従って連絡先名をフィルタリングするように設定されます。また、テーブルビューは、対応するプロキシモデルのsort()関数を使用して昇順でソートされます。
各テーブルビューのselectionMode はQAbstractItemView::SingleSelection に設定され、selectionBehavior はQAbstractItemView::SelectRows に設定されており、これによりユーザーは1行内のすべての項目を一度に選択できるようになっています。各QTableView オブジェクトには、選択されたインデックスを追跡するQItemSelectionModel が自動的に割り当てられます。
void AddressWidget::setupTabs()
{
const auto groups = { "ABC"_L1, "DEF"_L1, "GHI"_L1, "JKL"_L1, "MNO"_L1, "PQR"_L1,
"STU"_L1, "VW"_L1, "XYZ"_L1 };
for (QLatin1StringView str : groups) {
const auto regExp = QRegularExpression(QLatin1StringView("^[%1].*").arg(str),
QRegularExpression::CaseInsensitiveOption);
auto *proxyModel = new QSortFilterProxyModel(this);
proxyModel->setSourceModel(adapter.model());
proxyModel->setFilterRegularExpression(regExp);
proxyModel->setFilterKeyColumn(0);
auto *tableView = new QTableView;
tableView->setModel(proxyModel);
tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
tableView->horizontalHeader()->setStretchLastSection(true);
tableView->verticalHeader()->hide();
tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
tableView->setSelectionMode(QAbstractItemView::SingleSelection);
tableView->setSortingEnabled(true);
connect(tableView->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &AddressWidget::selectionChanged);
connect(this, &QTabWidget::currentChanged, this, [this, tableView](int tabIndex) {
if (widget(tabIndex) == tableView)
emit selectionChanged(tableView->selectionModel()->selection());
});
addTab(tableView, str);
}
}QItemSelectionModel クラスは、AddressWidget のselectionChanged() シグナルに接続されたselectionChanged シグナルを提供します。また、QTabWidget::currentChanged()シグナルを、AddressWidget のselectionChanged() も発火させるラムダ式に接続します。これらの接続は、MainWindow の「ツール」メニューにあるEdit Entry... およびRemove Entry アクションを有効にするために必要です。これについては、MainWindow の実装でさらに詳しく説明されています。
アドレス帳の各テーブルビューは、グループのQStringList から取得した関連するラベルとともに、QTabWidget のタブとして追加されます。

addEntry() には2つの関数を用意しています。1つはユーザー入力を受け付けるためのもの、もう1つはアドレス帳に新しいエントリを追加する実際の処理を行うものです。エントリ追加の処理を2つの部分に分けることで、newAddressTab がダイアログを表示することなくデータを挿入できるようにしています。
最初のaddEntry() 関数は、MainWindow のAdd Entry... アクションに接続されたスロットです。この関数はAddDialog オブジェクトを作成し、次に2つ目のaddEntry() 関数を呼び出して、実際に連絡先をtable に追加します。
void AddressWidget::showAddEntryDialog()
{
AddDialog aDialog(this);
if (aDialog.exec() == QDialog::Accepted)
addEntry(aDialog.contact());
}アドレス帳への重複登録を防ぐため、2番目のaddEntry() 関数で基本的な検証が行われます。TableModel で述べたように、これがgetContacts() というゲッターメソッドが必要とされる理由の一部です。
void AddressWidget::addEntry(const Contact &contact)
{
if (contacts.contains(contact)) {
QMessageBox::information(this, tr("Duplicate Name"),
tr("The name \"%1\" already exists.").arg(contact.name));
return;
}
adapter.insertRow(adapter.rowCount(), contact);
removeTab(indexOf(newAddressTab));
const QChar firstChar = contact.name.at(0).toUpper();
for (int t = 0, tabCount = count(); t < tabCount; ++t) {
if (tabText(t).contains(firstChar)) {
setCurrentIndex(t);
break;
}
}
}モデルに同じ名前のエントリがまだ存在しない場合、setData() を呼び出して、名前と住所をそれぞれ1列目と2列目に挿入します。そうでない場合は、QMessageBox を表示してユーザーに通知します。
注: 連絡先が追加されると、アドレス帳は空ではなくなるため、newAddressTab は 削除されます。
エントリの編集は、連絡先の住所のみを更新する手段です。この例では、ユーザーが既存の連絡先の名前を変更することは許可されていません。
まず、QTabWidget::currentWidget() を使用して、アクティブなタブのQTableView オブジェクトを取得します。次に、tableView からselectionModel を抽出し、選択されたインデックスを取得します。
void AddressWidget::editEntry()
{
auto *tableView = static_cast<QTableView *>(currentWidget());
auto *proxy = static_cast<QSortFilterProxyModel *>(tableView->model());
QItemSelectionModel *selectionModel = tableView->selectionModel();
const QModelIndexList indexes = selectionModel->selectedRows();
if (indexes.isEmpty())
return;
const int row = proxy->mapToSource(indexes.constFirst()).row();
const Contact contact = contacts.at(row);次に、ユーザーが編集しようとしている行からデータを抽出します。このデータは、異なるウィンドウタイトルを持つAddDialog のインスタンスに表示されます。table は、aDialog 内のデータに変更があった場合にのみ更新されます。
AddDialog aDialog(this);
aDialog.setWindowTitle(tr("Edit a Contact"));
aDialog.editAddress(contact);
if (aDialog.exec() == QDialog::Accepted) {
const Contact newContact = aDialog.contact();
if (newContact != contact)
adapter[row] = newContact;
}
}
エントリの削除には、removeEntry() 関数を使用します。選択された行は、QItemSelectionModel オブジェクトであるselectionModel を介してアクセスすることで削除されます。newAddressTab は、ユーザーがアドレス帳内の連絡先をすべて削除した場合にのみ、AddressWidget に再追加されます。
void AddressWidget::removeEntry()
{
auto *tableView = static_cast<QTableView *>(currentWidget());
auto *proxy = static_cast<QSortFilterProxyModel *>(tableView->model());
QItemSelectionModel *selectionModel = tableView->selectionModel();
const QModelIndexList indexes = selectionModel->selectedRows();
if (indexes.isEmpty())
return;
const int row = proxy->mapToSource(indexes.constFirst()).row();
adapter.removeRow(row);
if (adapter.rowCount() == 0)
insertTab(0, newAddressTab, tr("Address Book"));
}writeToFile() 関数は、アドレス帳内のすべての連絡先を含むファイルを保存するために使用されます。このファイルは、.dat の独自形式で保存されます。連絡先リストの内容は、QDataStream を使用してfile に書き込まれます。ファイルを開くことができない場合は、QMessageBox が表示され、関連するエラーメッセージが表示されます。
bool AddressWidget::writeToFile()
{
QFile file(fileName());
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
tr("Cannot write to %1: %2").arg(QDir::toNativeSeparators(fileName()),
file.errorString()));
return false;
}
auto sortedContacts = contacts;
std::sort(sortedContacts.begin(), sortedContacts.end());
QDataStream out(&file);
out << sortedContacts;
return true;
}readFromFile() 関数は、以前にwriteToFile() を使用して保存された、アドレス帳内のすべての連絡先を含むファイルを読み込みます。QDataStream は、.dat ファイルの内容を連絡先リストに読み込むために使用され、各連絡先はaddEntry() を使用して追加されます。
bool AddressWidget::readFromFile()
{
QFile file(fileName());
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
tr("Cannot open %1: %2").arg(QDir::toNativeSeparators(fileName()),
file.errorString()));
return false;
}
QList<Contact> contactsIn;
QDataStream in(&file);
in >> contactsIn;
if (contactsIn.isEmpty()) {
QMessageBox::information(this, tr("No contacts in file"),
tr("The file you are attempting to open contains no contacts."));
}
for (const auto &contact : std::as_const(contactsIn))
addEntry(contact);
return true;
}NewAddressTab クラスの定義
NewAddressTab クラスは、アドレス帳が空であることをユーザーに知らせる情報タブを提供します。これは、AddressWidget の実装で述べられているように、アドレス帳の内容に応じて表示・非表示になります。

NewAddressTab クラスはQWidget を継承しており、QLabel およびQPushButton を含んでいます。
class NewAddressTab : public QWidget
{
Q_OBJECT
public:
explicit NewAddressTab(QWidget *parent = nullptr);
signals:
void triggered();
};NewAddressTab クラスの実装
コンストラクタは、addButton 、descriptionLabel をインスタンス化し、addButton のシグナルをtriggered() シグナルに接続します。
NewAddressTab::NewAddressTab(QWidget *parent)
: QWidget(parent)
{
auto *descriptionLabel = new QLabel(tr("There are currently no contacts in your address book. "
"\nClick Add to add new contacts."));
auto *addButton = new QPushButton(tr("Add"));
connect(addButton, &QAbstractButton::clicked, this, &NewAddressTab::triggered);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(descriptionLabel, 0, Qt::AlignCenter);
mainLayout->addWidget(addButton, 0, Qt::AlignCenter);
}
AddDialogクラスの定義
AddDialog クラスはQDialog を継承しており、アドレス帳にデータを入力するためのQLineEdit とQTextEdit をユーザーに提供します。
class AddDialog : public QDialog
{
Q_OBJECT
public:
explicit AddDialog(QWidget *parent = nullptr);
Contact contact() const;
void setContact(const Contact &c);
void editAddress(const Contact &c);
private slots:
void updateEnabled();
private:
QDialogButtonBox *buttonBox;
QLineEdit *nameText;
QPlainTextEdit *addressText;
};
AddDialog クラスの実装
AddDialog のコンストラクタは、ユーザーインターフェースを設定し、必要なウィジェットを作成してレイアウトに配置します。
AddDialog::AddDialog(QWidget *parent)
: QDialog(parent),
buttonBox(new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, this)),
nameText(new QLineEdit),
addressText(new QPlainTextEdit)
{
auto *formLayout = new QFormLayout;
formLayout->addRow(tr("Name"), nameText);
formLayout->addRow(tr("Address"), addressText);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(formLayout);
mainLayout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(nameText, &QLineEdit::textChanged, this, &AddDialog::updateEnabled);
connect(addressText, &QPlainTextEdit::textChanged, this, &AddDialog::updateEnabled);
setWindowTitle(tr("Add a Contact"));
updateEnabled();
}
void AddDialog::updateEnabled()
{
Contact c = contact();
const bool valid = !c.name.isEmpty() && c.name.front().isLetter() && !c.address.isEmpty();
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid);
}
Contact AddDialog::contact() const
{
return { nameText->text().trimmed(), addressText->toPlainText().trimmed() };
}
void AddDialog::setContact(const Contact &c)
{
nameText->setText(c.name);
addressText->setPlainText(c.address);
updateEnabled();
}
void AddDialog::editAddress(const Contact &c)
{
nameText->setReadOnly(true);
setContact(c);
}ダイアログに所望の動作を持たせるため、OK およびCancel ボタンを、ダイアログのaccept()およびreject()スロットに接続します。このダイアログは名前と住所情報のコンテナとしてのみ機能するため、それ以外の関数を実装する必要はありません。
MainWindowクラスの定義
MainWindow クラスはQMainWindow を継承し、アドレス帳を操作するために必要なメニューやアクションを実装しています。
![]() | ![]() |
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
private slots:
void updateActions(const QItemSelection &selection);
void openFile();
void saveFile();
private:
void createMenus();
AddressWidget *addressWidget;
QAction *editAct;
QAction *removeAct;
};MainWindow クラスは、AddressWidget を中央のウィジェットとして使用し、[File]メニューにOpen 、Close 、Exit の各アクションを提供するとともに、[Tools ]メニューにAdd Entry... 、Edit Entry... 、Remove Entry の各アクションを提供します。
MainWindow クラスの実装
MainWindow のコンストラクタは、AddressWidgetをインスタンス化し、それを中心ウィジェットとして設定し、createMenus() 関数を呼び出します。
MainWindow::MainWindow()
: QMainWindow(),
addressWidget(new AddressWidget)
{
setCentralWidget(addressWidget);
createMenus();
setWindowTitle(tr("Address Book"));
}createMenus() 関数は、File およびTools メニューを設定し、アクションをそれぞれのスロットに接続します。Edit Entry... およびRemove Entry の両アクションは、空のアドレス帳では実行できないため、デフォルトでは無効になっています。これらは、1つ以上の連絡先が追加された場合にのみ有効になります。
void MainWindow::createMenus()
{
QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
auto *openAct = new QAction(QIcon::fromTheme(QIcon::ThemeIcon::DocumentOpen),
tr("&Open..."), this);
openAct->setShortcut(QKeySequence(QKeySequence::Open));
fileMenu->addAction(openAct);
connect(openAct, &QAction::triggered, this, &MainWindow::openFile);
...
editAct = new QAction(tr("&Edit Entry..."), this);
editAct->setEnabled(false);
toolMenu->addAction(editAct);
connect(editAct, &QAction::triggered, addressWidget, &AddressWidget::editEntry);
toolMenu->addSeparator();
removeAct = new QAction(tr("&Remove Entry"), this);
removeAct->setEnabled(false);
toolMenu->addAction(removeAct);
connect(removeAct, &QAction::triggered, addressWidget, &AddressWidget::removeEntry);
connect(addressWidget, &AddressWidget::selectionChanged,
this, &MainWindow::updateActions);
}すべてのアクションのシグナルをそれぞれのスロットに接続するほか、AddressWidget のselectionChanged() シグナルをupdateActions() スロットに接続します。
openFile() 関数は、アドレス帳の連絡先を含むカスタムaddressbook.dat ファイルを開きます。この関数は、File メニュー内のopenAct に接続されたスロットです。
void MainWindow::openFile()
{
if (addressWidget->readFromFile())
statusBar()->showMessage(tr("Read %1").arg(QDir::toNativeSeparators(AddressWidget::fileName())));
}「saveFile() 」機能は、アドレス帳の連絡先を含むカスタムaddressbook.dat ファイルを保存します。この機能は、File メニュー内の「saveAct 」に接続されたスロットです。
void MainWindow::saveFile()
{
if (addressWidget->writeToFile())
statusBar()->showMessage(tr("Wrote %1").arg(QDir::toNativeSeparators(AddressWidget::fileName())));
}updateActions() 関数は、アドレス帳の内容に応じて、Edit Entry... およびRemove Entry を有効または無効にします。アドレス帳が空の場合、これらのアクションは無効になり、それ以外の場合は有効になります。この関数は、AddressWidget のselectionChanged() シグナルに接続されたスロットです。
void MainWindow::updateActions(const QItemSelection &selection)
{
const QModelIndexList indexes = selection.indexes();
removeAct->setEnabled(!indexes.isEmpty());
editAct->setEnabled(!indexes.isEmpty());
}main() 関数
アドレス帳のメイン関数は、イベントループを実行する前に、QApplication をインスタンス化し、MainWindow を開きます。
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow mw;
const auto availableGeometry = mw.screen()->availableGeometry();
mw.resize(availableGeometry.width() / 3, availableGeometry.height() / 3);
mw.show();
return QApplication::exec();
}© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.

