通讯录
通讯录示例展示了如何使用代理模型在不同的视图中展示来自单个模型的数据。
本示例提供了一个通讯录,允许联系人以字母顺序分为9组:ABC、DEF、GHI、…、VW、…、XYZ。这是通过使用同一模型的多重视图来实现的,每个视图都使用一个QSortFilterProxyModel实例进行过滤。
概述
通讯录包含5个类:MainWindow、AddressWidget、TableModel、NewAddressTab和AddDialog。MainWindow类使用AddressWidget作为其中心小部件,并提供了文件和工具菜单。
AddressWidget类是一个QTabWidget子类,用于操作示例中显示的10个标签:9个字母分组标签和一个NewAddressTab实例。NewAddressTab类是一个QWidget子类,仅在通讯录为空时使用,提示用户添加一些联系人。《AddressWidget》还与一个TableModel实例交互,以向通讯录添加、编辑和删除条目。
TableModel是一个QAbstractTableModel的子类,提供了标准模型/视图API来访问数据。它保存添加的联系人列表。然而,这些数据并不是在一个单独的标签页中可见。相反,使用了QTableView提供9种不同的数据视图,根据字母组来排序。
QSortFilterProxyModel负责为每个联系人组过滤联系人。每个代理模型使用一个QRegularExpression来过滤出不属于相应字母组的联系人。《AddDialog》类用于从用户处获取通讯录的信息。这个QDialog子类由《NewAddressTab》实例化以添加联系人,并由《AddressWidget》实例化以添加和编辑联系人。
我们首先看一下TableModel的实现。
TableModel 类定义
TableModel类通过子类化QAbstractTableModel提供标准的API来访问其联系人列表中的数据。必须实现的基数函数包括:rowCount()、columnCount()、data()、headerData()。为了使TableModel可编辑,它必须提供insertRows()、removeRows()、setData()和flags()函数的实现。
struct Contact { QString name; QString address; bool operator==(const Contact &other) const { return name == other.name && address == other.address; } }; 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; } class TableModel : public QAbstractTableModel { Q_OBJECT public: TableModel(QObject *parent = nullptr); TableModel(const QList<Contact> &contacts, QObject *parent = nullptr); int rowCount(const QModelIndex &parent) const override; int columnCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex()) override; bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex()) override; const QList<Contact> &getContacts() const; private: QList<Contact> contacts; };
使用了两个构造函数,一个是默认构造函数,它使用 TableModel
自身的 QList<Contact>
,另一个是接收一个 QList<Contact>
参数的构造函数,以方便起见。
TableModel 类实现
我们按照头文件中定义的方式实现了这两个构造函数。第二个构造函数使用参数值初始化模型中的联系人列表。
TableModel::TableModel(QObject *parent) : QAbstractTableModel(parent) { } TableModel::TableModel(const QList<Contact> &contacts, QObject *parent) : QAbstractTableModel(parent), contacts(contacts) { }
函数 rowCount()
和 columnCount()
必须返回模型的大小。而 rowCount()
的值将根据添加到通讯录的联系人数量而变化,而 columnCount()
的值始终为 2,因为我们只需要为 姓名 和 地址 栏提供服务。
int TableModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : contacts.size(); } int TableModel::columnCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : 2; }
函数 data()
根据提供的模型索引内容返回 姓名 或 地址。模型索引中存储的行号用于引用联系人列表中的项目。选择由 QItemSelectionModel 处理,这将在 AddressWidget
中进行说明。
QVariant TableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() >= contacts.size() || index.row() < 0) return QVariant(); if (role == Qt::DisplayRole) { const auto &contact = contacts.at(index.row()); switch (index.column()) { case 0: return contact.name; case 1: return contact.address; default: break; } } return QVariant(); }
函数 headerData()
显示表格的标题,姓名 和 地址。如果您需要在通讯录中有编号的条目,您可以使用垂直标题,我们在本例中隐藏了它(请参阅 AddressWidget
实现)。
QVariant TableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); if (orientation == Qt::Horizontal) { switch (section) { case 0: return tr("Name"); case 1: return tr("Address"); default: break; } } return QVariant(); }
在添加新数据之前会调用函数 insertRows()
,否则数据将不会显示。通过调用 beginInsertRows()
和 endInsertRows()
函数,确保所有连接的视图都意识到这些变化。
bool TableModel::insertRows(int position, int rows, const QModelIndex &index) { Q_UNUSED(index); beginInsertRows(QModelIndex(), position, position + rows - 1); for (int row = 0; row < rows; ++row) contacts.insert(position, { QString(), QString() }); endInsertRows(); return true; }
通过调用函数 removeRows()
来删除数据。同样,再次调用 beginRemoveRows() 和 endRemoveRows() 以确保所有连接的视图都意识到这些变化。
bool TableModel::removeRows(int position, int rows, const QModelIndex &index) { Q_UNUSED(index); beginRemoveRows(QModelIndex(), position, position + rows - 1); for (int row = 0; row < rows; ++row) contacts.removeAt(position); endRemoveRows(); return true; }
函数 setData()
是将数据插入到表格中的函数,它逐个插入项,而不是按行插入。这意味着为了填充通讯录中的行,必须调用两次 setData()
,因为每行有 2 列。重要的是发出信号 dataChanged(),这样告诉所有连接的视图更新它们显示的内容。
bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.isValid() && role == Qt::EditRole) { const int row = index.row(); auto contact = contacts.value(row); switch (index.column()) { case 0: contact.name = value.toString(); break; case 1: contact.address = value.toString(); break; default: return false; } contacts.replace(row, contact); emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); return true; } return false; }
函数 flags()
返回给定索引的项目标志。
Qt::ItemFlags TableModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::ItemIsEnabled; return QAbstractTableModel::flags(index) | Qt::ItemIsEditable; }
我们设置了 Qt::ItemIsEditable 标志,因为我们希望允许编辑 TableModel
。尽管在本文例中我们没有使用 QTableView 对象的编辑功能,但我们这里启用它们,以便我们可以重用该模型在其他程序中。
TableModel
中的最后一个函数 getContacts()
返回包含通讯录中所有联系人的 QList<Contact> 对象。我们稍后会使用此函数获取检查现有条目的联系人列表、将联系人写入文件以及将它们读回。有关详细信息,请参阅 AddressWidget
。
const QList<Contact> &TableModel::getContacts() const { return contacts; }
AddressWidget 类定义
AddressWidget
类在技术上是本示例中涉及的主要类,因为它提供了添加、编辑和移除联系人的函数,保存联系人到文件以及从文件中加载联系人。
class AddressWidget : public QTabWidget { Q_OBJECT public: AddressWidget(QWidget *parent = nullptr); void readFromFile(); void writeToFile(); public slots: void showAddEntryDialog(); void addEntry(const QString &name, const QString &address); void editEntry(); void removeEntry(); signals: void selectionChanged (const QItemSelection &selected); private: void setupTabs(); inline static QString fileName = QStandardPaths::standardLocations(QStandardPaths::TempLocation).value(0) + QStringLiteral("/addressbook.dat"); TableModel *table; NewAddressTab *newAddressTab; };
AddressWidget
扩展了 QTabWidget,以容纳 10 个选项卡(《新建地址选项卡》和 9 个字母组合选项卡)并操作 table
、TableModel
对象、proxyModel
、我们用于过滤条目的 QSortFilterProxyModel 对象以及 tableView
、QTableView 对象。
地址小部件类实现
AddressWidget
构造函数接受一个父小部件并实例化NewAddressTab
、TableModel
和QSortFilterProxyModel。用于指示地址簿为空的NewAddressTab
对象被添加,其余9个标签页使用setupTabs()
设置。
AddressWidget::AddressWidget(QWidget *parent) : QTabWidget(parent), table(new TableModel(this)), newAddressTab(new NewAddressTab(this)) { connect(newAddressTab, &NewAddressTab::sendDetails, this, &AddressWidget::addEntry); addTab(newAddressTab, tr("Address Book")); setupTabs(); }
setupTabs()
函数用于设置AddressWidget
中的9个字母分组标签页、表格视图和代理模型。每个代理模型依次设置,根据相关字母组使用不区分大小写的QRegularExpression对象过滤联系人姓名。表格视图也使用相应代理模型的sort()函数按升序排序。
每个表格视图的selectionMode设置为QAbstractItemView::SingleSelection,而selectionBehavior设置为QAbstractItemView::SelectRows,允许用户同时选中一行中的所有条目。每个QTableView对象都自动提供一个QItemSelectionModel,以跟踪选中的索引。
void AddressWidget::setupTabs() { using namespace Qt::StringLiterals; 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(table); proxyModel->setFilterRegularExpression(regExp); proxyModel->setFilterKeyColumn(0); QTableView *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类提供了一个selectionChanged信号,该信号连接到AddressWidget
的selectionChanged()
信号。我们还连接QTabWidget::currentChanged()信号到lambda表达式,该表达式发出AddressWidget
的selectionChanged()
。这些连接是必要的,以便启用MainWindow
工具菜单中的编辑条目...和删除条目操作。这进一步在MainWindow
的实现中解释。
将地址簿中的每个表格视图添加到具有相关标签的QTabWidget中,这些标签来自组QStringList。
我们提供了两个addEntry()
函数:一个用于接受用户输入,另一个用于执行将新条目添加到地址簿的实际任务。我们将添加条目的责任分为两部分,以便使newAddressTab
能够插入数据而不需要弹出对话框。
第一个addEntry()
函数是一个槽,连接到MainWindow
的添加条目...操作。此函数创建一个AddDialog
对象,然后调用第二个addEntry()
函数以将联系人实际添加到table
。
void AddressWidget::showAddEntryDialog() { AddDialog aDialog; if (aDialog.exec()) addEntry(aDialog.name(), aDialog.address()); }
第二个addEntry()
函数执行基本验证以防止地址簿中有重复条目。如前所述,这是为什么我们要求使用获取器方法getContacts()
的原因之一。
void AddressWidget::addEntry(const QString &name, const QString &address) { if (!name.front().isLetter()) { QMessageBox::information(this, tr("Invalid name"), tr("The name must start with a letter.")); } else if (!table->getContacts().contains({ name, address })) { table->insertRows(0, 1, QModelIndex()); QModelIndex index = table->index(0, 0, QModelIndex()); table->setData(index, name, Qt::EditRole); index = table->index(0, 1, QModelIndex()); table->setData(index, address, Qt::EditRole); removeTab(indexOf(newAddressTab)); } else { QMessageBox::information(this, tr("Duplicate Name"), tr("The name \"%1\" already exists.").arg(name)); } }
如果模型尚未包含具有相同名称的条目,则调用setData()
将名称和地址插入第一个和第二个列。否则,我们显示一个QMessageBox来告知用户。
注意:newAddressTab
在添加联系人后就会删除,因为地址簿不再为空。
编辑条目是一种更新联系人地址的方式,因为示例不允许用户更改现有联系人的名字。
首先,我们使用QTabWidget::currentWidget() 获取活动选项卡的 QTableView 对象。然后,我们从 tableView
中提取 selectionModel
以获取选中索引。
void AddressWidget::editEntry() { QTableView *temp = static_cast<QTableView*>(currentWidget()); QSortFilterProxyModel *proxy = static_cast<QSortFilterProxyModel*>(temp->model()); QItemSelectionModel *selectionModel = temp->selectionModel(); const QModelIndexList indexes = selectionModel->selectedRows(); QString name; QString address; int row = -1; for (const QModelIndex &index : indexes) { row = proxy->mapToSource(index).row(); QModelIndex nameIndex = table->index(row, 0, QModelIndex()); QVariant varName = table->data(nameIndex, Qt::DisplayRole); name = varName.toString(); QModelIndex addressIndex = table->index(row, 1, QModelIndex()); QVariant varAddr = table->data(addressIndex, Qt::DisplayRole); address = varAddr.toString(); }
接下来,我们提取用户打算编辑的行中的数据。这些数据在一个具有不同窗口标题的 AddDialog
实例中显示。只有当在 aDialog
中修改了数据时,才会更新 table
。
AddDialog aDialog; aDialog.setWindowTitle(tr("Edit a Contact")); aDialog.editAddress(name, address); if (aDialog.exec()) { const QString newAddress = aDialog.address(); if (newAddress != address) { const QModelIndex index = table->index(row, 1, QModelIndex()); table->setData(index, newAddress, Qt::EditRole); } } }
使用 removeEntry()
函数删除条目。通过 QItemSelectionModel 对象 selectionModel
访问选中的行来删除它。只有当用户删除地址簿中的所有联系人时,才会将 newAddressTab
重新添加到 AddressWidget
中。
void AddressWidget::removeEntry() { QTableView *temp = static_cast<QTableView*>(currentWidget()); QSortFilterProxyModel *proxy = static_cast<QSortFilterProxyModel*>(temp->model()); QItemSelectionModel *selectionModel = temp->selectionModel(); const QModelIndexList indexes = selectionModel->selectedRows(); for (QModelIndex index : indexes) { int row = proxy->mapToSource(index).row(); table->removeRows(row, 1, QModelIndex()); } if (table->rowCount(QModelIndex()) == 0) insertTab(0, newAddressTab, tr("Address Book")); }
使用 writeToFile()
函数保存包含地址簿中所有联系人的文件。该文件以自定义的 .dat
格式保存。使用 QDataStream 将联系人列表的内容写入到 file
中。如果无法打开文件,将显示一个包含相关错误消息的 QMessageBox。
void AddressWidget::writeToFile() { QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::information(this, tr("Unable to open file"), file.errorString()); return; } QDataStream out(&file); out << table->getContacts(); }
使用 readFromFile()
函数加载一个文件,该文件包含之前使用 writeToFile()
保存的地址簿中所有联系人的详细信息。使用 QDataStream 将 .dat
文件的内容读取到联系人列表中,并使用 addEntry()
将每个条目添加进去。
void AddressWidget::readFromFile() { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::information(this, tr("Unable to open file"), file.errorString()); return; } QList<Contact> contacts; QDataStream in(&file); in >> contacts; if (contacts.isEmpty()) { QMessageBox::information(this, tr("No contacts in file"), tr("The file you are attempting to open contains no contacts.")); } else { for (const auto &contact: std::as_const(contacts)) addEntry(contact.name, contact.address); } }
NewAddressTab 类定义
NewAddressTab
类提供了一个信息标签,向用户说明地址簿为空。根据 AddressWidget
的实现,它根据地址簿的内容出现和消失。
NewAddressTab
类扩展了 QWidget 并包含一个 QLabel 和一个 QPushButton。
class NewAddressTab : public QWidget { Q_OBJECT public: NewAddressTab(QWidget *parent = nullptr); public slots: void addEntry(); signals: void sendDetails(const QString &name, const QString &address); };
NewAddressTab 类实现
构造函数实例化了 addButton
、descriptionLabel
并将 addButton
的信号连接到位 addEntry()
槽。
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::addEntry); auto mainLayout = new QVBoxLayout; mainLayout->addWidget(descriptionLabel); mainLayout->addWidget(addButton, 0, Qt::AlignCenter); setLayout(mainLayout); }
addEntry()
函数类似于 AddressWidget
的 addEntry()
,因为这两个函数都实例化了 AddDialog
对象。从对话框中提取数据并通过发出 sendDetails()
信号将其送到 AddressWidget
的 addEntry()
槽。
void NewAddressTab::addEntry() { AddDialog aDialog; if (aDialog.exec()) emit sendDetails(aDialog.name(), aDialog.address()); }
AddDialog 类定义
AddDialog
类扩展了 QDialog并为用户提供了一个 QLineEdit 和一个 QTextEdit 用于向地址簿输入数据。
class AddDialog : public QDialog { Q_OBJECT public: AddDialog(QWidget *parent = nullptr); QString name() const; QString address() const; void editAddress(const QString &name, const QString &address); private: QLineEdit *nameText; QTextEdit *addressText; };
AddDialog 类实现
AddDialog
的构造函数设置了用户界面,创建了必要的控件并将它们放在布局中。
AddDialog::AddDialog(QWidget *parent) : QDialog(parent), nameText(new QLineEdit), addressText(new QTextEdit) { auto nameLabel = new QLabel(tr("Name")); auto addressLabel = new QLabel(tr("Address")); auto okButton = new QPushButton(tr("OK")); auto cancelButton = new QPushButton(tr("Cancel")); auto gLayout = new QGridLayout; gLayout->setColumnStretch(1, 2); gLayout->addWidget(nameLabel, 0, 0); gLayout->addWidget(nameText, 0, 1); gLayout->addWidget(addressLabel, 1, 0, Qt::AlignLeft|Qt::AlignTop); gLayout->addWidget(addressText, 1, 1, Qt::AlignLeft); auto buttonLayout = new QHBoxLayout; buttonLayout->addWidget(okButton); buttonLayout->addWidget(cancelButton); gLayout->addLayout(buttonLayout, 2, 1, Qt::AlignRight); auto mainLayout = new QVBoxLayout; mainLayout->addLayout(gLayout); setLayout(mainLayout); connect(okButton, &QAbstractButton::clicked, this, &QDialog::accept); connect(cancelButton, &QAbstractButton::clicked, this, &QDialog::reject); setWindowTitle(tr("Add a Contact")); } QString AddDialog::name() const { return nameText->text(); } QString AddDialog::address() const { return addressText->toPlainText(); } void AddDialog::editAddress(const QString &name, const QString &address) { nameText->setReadOnly(true); nameText->setText(name); addressText->setPlainText(address); }
为了使对话框具有所需的行为,我们将 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
作为它的中央部件,提供带有 Open、Close 和 Exit 操作的文件菜单,以及带有 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 操作都被禁用。只有在添加了一个或多个联系人后,这些操作才会被启用。
void MainWindow::createMenus() { QMenu *fileMenu = menuBar()->addMenu(tr("&File")); QAction *openAct = new QAction(tr("&Open..."), this); 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() { addressWidget->readFromFile(); }
saveFile()
函数保存一个自定义的 addressbook.dat
文件,该文件将包含通讯录联系人。该函数是连接到 File 菜单中的 saveAct
的槽。
void MainWindow::saveFile() { addressWidget->writeToFile(); }
updateActions()
函数根据通讯录的内容启用或禁用 Edit Entry... 和 Remove Entry。如果通讯录为空,则这些操作将会被禁用;否则,它们会被启用。该函数是一个连接到 AddressWidget
的 selectionChanged()
信号的槽。
void MainWindow::updateActions(const QItemSelection &selection) { QModelIndexList indexes = selection.indexes(); if (!indexes.isEmpty()) { removeAct->setEnabled(true); editAct->setEnabled(true); } else { removeAct->setEnabled(false); editAct->setEnabled(false); } }
main() 函数
通讯录的主函数创建了 QApplication 并在运行事件循环之前打开了一个 MainWindow
。
int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow mw; mw.show(); return app.exec(); }
© 2024 The Qt Company Ltd. 本文档中的文档贡献属于各自所有者的版权。此处提供的文档是根据自由软件基金会的 GNU 自由文档许可第 1.3 版 许可的。Qt 及相关徽标是 The Qt Company Ltd. 在芬兰和其他国家/地区的商标。所有其他商标均为各自所有者的财产。