文章目录
Address Book Example
地址簿示例展示了如何使用代理模型来显示来自单个模型的数据的不同视图。
本例提供了一个地址簿,允许按字母顺序将联系人分组为9组:ABC、DEF、GHI,…,VW,…XYZ。这是通过在同一个模型上使用多个视图来实现的,每个视图都是使用QSortFilterProxyModel类的一个实例进行筛选的。
概述
地址本包含5个类:MainWindow, addreswidget, TableModel, NewAddressTab和AddDialog。MainWindow类使用addreswidget作为其中心小部件,并提供文件和工具菜单。
AddressWidget类是一个QTabWidget子类,用于操作示例中显示的10个选项卡:9个字母组选项卡和一个NewAddressTab实例。NewAddressTab类是QWidget的一个子类,它只在地址簿为空时使用,提示用户添加一些联系人。
AddressWidget还与TableModel的实例进行交互,以添加、编辑和删除地址簿中的条目。
TableModel是QAbstractTableModel的子类,它提供了标准的模型/视图API来访问数据。它包含一个添加的联系人列表。然而,这些数据在单个选项卡中并不都是可见的。相反,QTableView被用来提供同一数据的9个不同视图,根据字母表组。
QSortFilterProxyModel是负责过滤每组联系人的联系人的类。每个代理模型都使用QRegExp来过滤不属于相应字母组的联系人。AddDialog类用于从用户处获取地址簿的信息。这个QDialog子类由NewAddressTab实例化以添加联系人,并由AddressWidget实例化以添加和编辑联系人。
main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
#ifdef Q_OS_ANDROID
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QApplication app(argc, argv);
MainWindow mw;
mw.show();
return app.exec();
}
MainWindow类
MainWindow类扩展了QMainWindow并实现了操作地址簿所需的菜单和操作。使用这个类主要是为了提供菜单。
MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "addresswidget.h"
#include <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;
};
#endif // MAINWINDOW_H
MainWindow类使用一个AddressWidget 作为其中心小部件,并提供文件菜单的打开、关闭和退出操作,以及工具菜单的添加条目…、编辑条目…并删除入口操作。
MainWindow.cpp
#include "mainwindow.h"
#include <QAction>
#include <QFileDialog>
#include <QMenuBar>
// 构造函数 实例化addreswidget,将其设置为中心小部件并调用createMenus()函数
MainWindow::MainWindow()
: QMainWindow(),
addressWidget(new AddressWidget)
{
setCentralWidget(addressWidget);
createMenus();
setWindowTitle(tr("Address Book"));
}
// createMenus()函数设置文件和工具菜单,将操作连接到各自的插槽。编辑条目…默认情况下,删除条目操作是禁用的,因为这些操作不能在空的地址簿上执行。只有添加了一个或多个联系人时才启用。
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);
QAction *saveAct = new QAction(tr("&Save As..."), this);
fileMenu->addAction(saveAct);
connect(saveAct, &QAction::triggered, this, &MainWindow::saveFile);
fileMenu->addSeparator();
QAction *exitAct = new QAction(tr("E&xit"), this);
fileMenu->addAction(exitAct);
connect(exitAct, &QAction::triggered, this, &QWidget::close);
QMenu *toolMenu = menuBar()->addMenu(tr("&Tools")); // 工具菜单
QAction *addAct = new QAction(tr("&Add Entry..."), this);
toolMenu->addAction(addAct);
connect(addAct, &QAction::triggered,addressWidget, &AddressWidget::showAddEntryDialog);
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);
}
// 打开文件
void MainWindow::openFile()
{
QString fileName = QFileDialog::getOpenFileName(this);
if (!fileName.isEmpty())
addressWidget->readFromFile(fileName);
}
// 保存文件
void MainWindow::saveFile()
{
QString fileName = QFileDialog::getSaveFileName(this);
if (!fileName.isEmpty())
addressWidget->writeToFile(fileName);
}
// 更新动作状态 操作不能在空的地址簿上执行
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);
}
}
AddressWidget类
从技术上讲,AddressWidget类是本例中涉及的主要类,因为它提供了添加、编辑和删除联系人、将联系人保存到文件并从文件加载它们的函数。
AddressWidget扩展了QTabWidget,以便容纳10个选项卡(NewAddressTab和9个字母组选项卡),并操作table、TableModel对象、proxyModel、QSortFilterProxyModel对象(我们用来过滤条目)和tableView对象(QTableView对象)。
AddressWidget.h
#ifndef ADDRESSWIDGET_H
#define ADDRESSWIDGET_H
#include "newaddresstab.h"
#include "tablemodel.h"
#include <QItemSelection>
#include <QTabWidget>
QT_BEGIN_NAMESPACE
class QSortFilterProxyModel;
class QItemSelectionModel;
QT_END_NAMESPACE
class AddressWidget : public QTabWidget
{
Q_OBJECT
public:
AddressWidget(QWidget *parent = nullptr);
void readFromFile(const QString &fileName);
void writeToFile(const QString &fileName);
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();
TableModel *table;
NewAddressTab *newAddressTab;
};
#endif // ADDRESSWIDGET_H
AddressWidget.cpp
#include "addresswidget.h"
#include "adddialog.h"
#include <QtWidgets>
// 构造函数
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();
}
// 显示AddDialog
void AddressWidget::showAddEntryDialog()
{
AddDialog aDialog;
if (aDialog.exec())
addEntry(aDialog.name(), aDialog.address());
}
// 添加条目
void AddressWidget::addEntry(const QString &name, const QString &address)
{
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));
}
}
// 编辑条目
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;
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);
}
}
}
// 删除条目
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"));
}
// 设置选项页
void AddressWidget::setupTabs()
{
const auto groups = { "ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ" };
for (const QString &str : groups) {
const auto regExp = QRegularExpression(QString("^[%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); // 添加选项页
}
}
// 从文件读取
void AddressWidget::readFromFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
file.errorString());
return;
}
QVector<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: qAsConst(contacts))
addEntry(contact.name, contact.address);
}
}
// 写入文件
void AddressWidget::writeToFile(const QString &fileName)
{
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();
}
TableModel 类
TableModel类通过子类化QAbstractTableModel提供了访问联系人列表中的数据的标准API。为此必须实现的基本函数有:rowCount()、columnCount()、data()、headerData()。要使TableModel可编辑,它必须提供实现insertRows()、removeRows()、setData()和flags()函数。
TableModel.h
#ifndef TABLEMODEL_H
#define TABLEMODEL_H
#include <QAbstractTableModel>
#include <QVector>
// 联系人结构体
struct Contact
{
QString name;
QString address;
// 相等操作符重载
bool operator==(const Contact &other) const
{
return name == other.name && address == other.address;
}
};
// 提供Contact的流式输入
inline QDataStream &operator<<(QDataStream &stream, const Contact &contact)
{
return stream << contact.name << contact.address;
}
// 提供Contact的流式输出
inline QDataStream &operator>>(QDataStream &stream, Contact &contact)
{
return stream >> contact.name >> contact.address;
}
// 继承QAbstractTableModel
class TableModel : public QAbstractTableModel
{
Q_OBJECT
public:
TableModel(QObject *parent = nullptr);
TableModel(const QVector<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 QVector<Contact> &getContacts() const;
private:
QVector<Contact> contacts;
};
#endif // TABLEMODEL_H
TableModel.cpp
#include "tablemodel.h"
// 使用了两个构造函数,一个默认构造函数使用TableModel自己的QVector<contact>,
// 另一个为了方便,将QVector<contact>作为参数。
TableModel::TableModel(QObject *parent)
: QAbstractTableModel(parent)
{
}
TableModel::TableModel(const QVector<Contact> &contacts, QObject *parent)
: QAbstractTableModel(parent),
contacts(contacts)
{
}
// 重写 rowCount
int TableModel::rowCount(const QModelIndex &parent) const
{ // rowCount()的值将根据添加到地址簿的联系人数量而变化
return parent.isValid() ? 0 : contacts.size();
}
// 重写 columnCount
int TableModel::columnCount(const QModelIndex &parent) const
{ //columnCount()的值总是2,因为我们只需要名称和地址列的空间。
return parent.isValid() ? 0 : 2;
}
// 重写data
// data()函数根据提供的模型索引的内容返回一个名称或地址。
// 存储在模型索引中的行号用于引用联系人列表中的一个项目。选择是由QItemSelectionModel处理的,我们将使用addreswidget对其进行解释。
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();
}
// 重写头部数据
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()两次,因为每行有两列。发出datachchanged()信号非常重要,因为它告诉所有连接的视图更新它们的显示。
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;
}
// 设置Qt:: itemeditable标志是因为我们想要允许TableModel被编辑。虽然在这个例子中没有使用QTableView对象的编辑特性,但在这里启用了它们,以便可以在其他程序中重用模型。
Qt::ItemFlags TableModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::ItemIsEnabled;
return QAbstractTableModel::flags(index) | Qt::ItemIsEditable;
}
// 获取成员变量 contacts
const QVector<Contact> &TableModel::getContacts() const
{
return contacts;
}
NewAddressTab类
NewAddressTab类提供了一个信息性选项卡,告诉用户地址簿是空的。它根据地址簿的内容出现和消失,正如addreswidget的实现中提到的那样。
NewAddressTab.h
NewAddressTab类扩展了QWidget,并包含一个QLabel和QPushButton。
#ifndef NEWADDRESSTAB_H
#define NEWADDRESSTAB_H
#include <QWidget>
QT_BEGIN_NAMESPACE
class QLabel;
class QPushButton;
class QVBoxLayout;
QT_END_NAMESPACE
class NewAddressTab : public QWidget
{
Q_OBJECT
public:
NewAddressTab(QWidget *parent = nullptr);
public slots:
void addEntry();
signals:
void sendDetails(const QString &name, const QString &address);
};
#endif
NewAddressTab.cpp
#include "newaddresstab.h"
#include "adddialog.h"
#include <QtWidgets>
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);
}
void NewAddressTab::addEntry()
{
AddDialog aDialog;
if (aDialog.exec())
emit sendDetails(aDialog.name(), aDialog.address());
}
AddDialog 类
AddDialog类扩展了QDialog,并为用户提供了QLineEdit和QTextEdit来将数据输入到地址簿。
AddDialog .h
#ifndef ADDDIALOG_H
#define ADDDIALOG_H
#include <QtWidgets>
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;
};
#endif // ADDDIALOG_H
AddDialog .cpp
#include "adddialog.h"
// 构造函数
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);
}