概要
- 主要涉及到的类有QSortFilterProxyModel、QTableView、QAbstractTableModel。
- QAbstractTableModel提供了标准的模型API来访问数据。QTableView视图用于显示QAbstractTableModel里的数据,而QSortFilterProxyModel相当于QAbstractTableModel的代理,对QAbstractTableModel里的数据进行管理。
代码分析
main.cpp
#include "mainwindow.h"
#include <QApplication>
//! [0]
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
//创建一个QMainWindow窗口mw
MainWindow mw;
//显示mw窗口
mw.show();
return app.exec();
}
MainWindow.h
主窗口类包括一个菜单栏,一个中央小部件(addressWidget)和一些操作。文件菜单包括打开、保存和退出操作,工具菜单包括添加、编辑和删除地址操作。当选中一个条目时,就可以使用编辑和删除功能。主窗口类还包括打开和保存文件的槽函数,以及更新操作状态的槽函数。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "addresswidget.h"
#include <QMainWindow>
//! [0]
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
private slots:
void updateActions(const QItemSelection &selection);
void openFile();
void saveFile();
private:
void createMenus();
AddressWidget *addressWidget;
QMenu *fileMenu;
QMenu *toolMenu;
QAction *openAct;
QAction *saveAct;
QAction *exitAct;
QAction *addAct;
QAction *editAct;
QAction *removeAct;
};
//! [0]
#endif // MAINWINDOW_H
MainWindow.cpp
#include "mainwindow.h"
#include <QAction>
#include <QFileDialog>
#include <QMenuBar>
//! [0]
MainWindow::MainWindow()
{
//创建了一个AddressWidget对象,并将其设置为主窗口的中央部件
addressWidget = new AddressWidget;
setCentralWidget(addressWidget);
//自定义的创建菜单函数
createMenus();
//设置窗口标题
setWindowTitle(tr("Address Book"));
}
//! [0]
//! [1a]
void MainWindow::createMenus()
{
//创建一个名为fileMenu的菜单,并将其添加到菜单栏中
fileMenu = menuBar()->addMenu(tr("&File"));
//创建一个名为openAct的动作,并将其添加到fileMenu菜单中。当该动作被触发时,将调用MainWindow类的openFile()函数
openAct = new QAction(tr("&Open..."), this);
fileMenu->addAction(openAct);
connect(openAct, &QAction::triggered, this, &MainWindow::openFile);
//! [1a]
//创建一个名为saveAct的动作,并将其添加到fileMenu菜单中。当该动作被发时,将调用MainWindow类的saveFile()函数
saveAct = new QAction(tr("&Save As..."), this);
fileMenu->addAction(saveAct);
connect(saveAct, &QAction::triggered, this, &MainWindow::saveFile);
//在fileMenu菜单中添加一个分隔线
fileMenu->addSeparator();
//创建一个名为exitAct的动作,并将其添加到fileMenu菜单中。当该动作被触发时,将关闭应用程序
exitAct = new QAction(tr("E&xit"), this);
fileMenu->addAction(exitAct);
connect(exitAct, &QAction::triggered, this, &QWidget::close);
//创建一个名为toolMenu的菜单,并将其添加到菜单栏中
toolMenu = menuBar()->addMenu(tr("&Tools"));
//创建一个名为addAct的动作,并将其添加到toolMenu菜单中。当该动作被触发时,将调用AddressWidget类的showAddEntryDialog()函数
addAct = new QAction(tr("&Add Entry..."), this);
toolMenu->addAction(addAct);
connect(addAct, &QAction::triggered, addressWidget, &AddressWidget::showAddEntryDialog);
//! [1b]
//创建一个名为editAct的动作,并将其添加到toolMenu菜单中。初始时该动作不可用。当该动作被触发时,将调用AddressWidget类的editEntry()函数
editAct = new QAction(tr("&Edit Entry..."), this);
editAct->setEnabled(false);
toolMenu->addAction(editAct);
connect(editAct, &QAction::triggered, addressWidget, &AddressWidget::editEntry);
//在toolMenu菜单中添加一个分隔线
toolMenu->addSeparator();
//创建一个名为removeAct的动作,并将其添加到toolMenu菜单中。初始时该动作不可用。当该动作被触发时,将调用AddressWidget类的removeEntry()函数
removeAct = new QAction(tr("&Remove Entry"), this);
removeAct->setEnabled(false);
toolMenu->addAction(removeAct);
connect(removeAct, &QAction::triggered, addressWidget, &AddressWidget::removeEntry);
//将AddressWidget类的selectionChanged信号连接到MainWindow类的updateActions()槽函数。当AddressWidget类的选择的item发生变化时,将更新动作的可用性状态
connect(addressWidget, &AddressWidget::selectionChanged,
this, &MainWindow::updateActions);
}
//! [1b]
//! [2]
void MainWindow::openFile()
{
//使用QFileDialog类打开一个文件对话框,以便选择要打开的文件
QString fileName = QFileDialog::getOpenFileName(this);
if (!fileName.isEmpty())
//传递给AddressWidget类的readFromFile函数,以便从文件中读取地址簿数据
addressWidget->readFromFile(fileName);
}
//! [2]
//! [3]
void MainWindow::saveFile()
{
//使用QFileDialog类打开一个文件对话框,以便选择要保存的文件
QString fileName = QFileDialog::getSaveFileName(this);
if (!fileName.isEmpty())
//传递给AddressWidget类的writeToFile函数,以便将地址簿数据写入文件中
addressWidget->writeToFile(fileName);
}
//! [3]
//! [4]
//接受一个QItemSelection对象作为参数
void MainWindow::updateActions(const QItemSelection &selection)
{
QModelIndexList indexes = selection.indexes();
//根据选择的项更新编辑和删除操作的状态。如果至少选择了一个item,这些操作将会被启用。否则,它们将会被禁用
if (!indexes.isEmpty()) {
removeAct->setEnabled(true);
editAct->setEnabled(true);
} else {
removeAct->setEnabled(false);
editAct->setEnabled(false);
}
}
//! [4]
addresswidget.h
AddressWidget的类,它继承自QTabWidget类。AddressWidget类包含了一些用于管理地址簿数据的函数
#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
//! [0]
class AddressWidget : public QTabWidget
{
Q_OBJECT
public:
AddressWidget(QWidget *parent = 0);
void readFromFile(const QString &fileName);
void writeToFile(const QString &fileName);
public slots:
void showAddEntryDialog();
void addEntry(QString name, QString address);
void editEntry();
void removeEntry();
signals:
void selectionChanged (const QItemSelection &selected);
private:
void setupTabs();
TableModel *table;
NewAddressTab *newAddressTab;
QSortFilterProxyModel *proxyModel;
};
addresswidget.cpp
#include "adddialog.h"
#include "addresswidget.h"
#include <QtWidgets>
//! [0]
AddressWidget::AddressWidget(QWidget *parent)
: QTabWidget(parent)
{
//只定义了一个名为table的tablemodel,多个tableview共用一个tablemodel,只是tableview只显示想显示的内容
table = new TableModel(this);
newAddressTab = new NewAddressTab(this);
connect(newAddressTab, &NewAddressTab::sendDetails,
this, &AddressWidget::addEntry);
//将newAddressTab添加到AddressWidget对象中
addTab(newAddressTab, "Address Book");
//调用setupTabs函数来设置地址簿的标签页
setupTabs();
}
//! [0]
//! [2]
void AddressWidget::showAddEntryDialog()
{
AddDialog aDialog;
//点击“OK”按钮时调用addEntry函数
if (aDialog.exec()) {
QString name = aDialog.nameText->text();
QString address = aDialog.addressText->toPlainText();
addEntry(name, address);
}
}
//! [2]
//! [3]
//添加表格数据
void AddressWidget::addEntry(QString name, 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));
}
}
//! [3]
//! [4a]
//编辑表格数据
void AddressWidget::editEntry()
{
//获取当前活动的tab页,并将其强制转为QTableView类型的指针
QTableView *temp = static_cast<QTableView*>(currentWidget());
//获取temp的model,并将其强制转换为QSortFilterProxyModel类型的指针proxy,因为tableview直接setModel的是QSortFilterProxyModel,它是一个代理模型,用于对原始数据模型进行排序、过滤等操作
QSortFilterProxyModel *proxy = static_cast<QSortFilterProxyModel*>(temp->model());
//获取当前QTableView的选择模型,并将其存储在QItemSelectionModel类型的指针中。选择模型用于跟踪表格中的选择
QItemSelectionModel *selectionModel = temp->selectionModel();
//获取当前选择的行的索引
QModelIndexList indexes = selectionModel->selectedRows();
QString name;
QString address;
int row = -1;
//遍历选择的行,并从模型中获取行号、姓名和地址
foreach (QModelIndex index, indexes) {
//表格使用了QSortFilterProxyModel,则使用proxy->mapToSource获取当前选中的联系人信息的行号,这里需要把index从proxy转到对应的源table的index
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();
}
//! [4a]
//! [4b]
AddDialog aDialog;
aDialog.setWindowTitle(tr("Edit a Contact"));
aDialog.nameText->setReadOnly(true);
aDialog.nameText->setText(name);
aDialog.addressText->setText(address);
//显示`aDialog`并等待用户完成编辑。如果用户单击了对话框上的“OK”按钮,则获取`addressText`的当前文本,并将其与`address`的值进行比较。如果它们不相等,则将新的地址数据设置到表格中。否则,不做任何事情
if (aDialog.exec()) {
QString newAddress = aDialog.addressText->toPlainText();
if (newAddress != address) {
QModelIndex index = table->index(row, 1, QModelIndex());
table->setData(index, newAddress, Qt::EditRole);
}
}
}
//! [4b]
//! [5]
//移除表格数据
void AddressWidget::removeEntry()
{
//获取当前的 QTableView 对象,即当前的地址簿表格视图
QTableView *temp = static_cast<QTableView*>(currentWidget());
QSortFilterProxyModel *proxy = static_cast<QSortFilterProxyModel*>(temp->model());
QItemSelectionModel *selectionModel = temp->selectionModel();
//获取当前选择的items的索引列表
QModelIndexList indexes = selectionModel->selectedRows();
//遍历索引列表,获取每个条目的原始行号,并在数据模型中删除对应的行
foreach (QModelIndex index, indexes) {
//这里需要把index从proxy转到对应的源table的index
int row = proxy->mapToSource(index).row();
table->removeRows(row, 1, QModelIndex());
}
if (table->rowCount(QModelIndex()) == 0) {
insertTab(0, newAddressTab, "Address Book");
}
}
//! [5]
//! [1]
//初始化表格
void AddressWidget::setupTabs()
{
QStringList groups;
groups << "ABC" << "DEF" << "GHI" << "JKL" << "MNO" << "PQR" << "STU" << "VW" << "XYZ";
for (int i = 0; i < groups.size(); ++i) {
QString str = groups.at(i);
QString regExp = QString("^[%1].*").arg(str);
//创建一个 QSortFilterProxyModel 对象,用于过滤和排序地址簿数据
proxyModel = new QSortFilterProxyModel(this);
//设置过滤的model为table
proxyModel->setSourceModel(table);
//用正则来过滤model的数据
proxyModel->setFilterRegExp(QRegExp(regExp, Qt::CaseInsensitive));
proxyModel->setFilterKeyColumn(0);
QTableView *tableView = new QTableView;
tableView->setModel(proxyModel);
//设置 QTableView 的选择行为、水平表头、垂直表头、编辑触发方式、选择模式和排序方式
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](int tabIndex) {
auto *tableView = qobject_cast<QTableView *>(widget(tabIndex));
if (tableView)
emit selectionChanged(tableView->selectionModel()->selection());
});
//当前类是一个QTabWidget,把一个个tableView作为tab页添加
addTab(tableView, str);
}
}
//! [1]
//! [7]
//从指定的文件中读取联系人信息,并将其添加到AddressWidget中
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;
}
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: qAsConst(contacts))
addEntry(contact.name, contact.address);
}
}
//! [7]
//! [6]
//将通讯录中的联系人信息写入到文件中
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();
}
newaddresstab.h
用于添加联系人姓名和地址的临时界面,添加成功后便从addresswidget中移除这个tab页
#ifndef NEWADDRESSTAB_H
#define NEWADDRESSTAB_H
#include <QWidget>
QT_BEGIN_NAMESPACE
class QLabel;
class QPushButton;
class QVBoxLayout;
QT_END_NAMESPACE
//! [0]
class NewAddressTab : public QWidget
{
Q_OBJECT
public:
NewAddressTab(QWidget *parent = 0);
public slots:
void addEntry();
signals:
void sendDetails(QString name, QString address);
private:
QLabel *descriptionLabel;
QPushButton *addButton;
QVBoxLayout *mainLayout;
};
//! [0]
#endif
newaddresstab.cpp
#include "adddialog.h"
#include "newaddresstab.h"
#include <QtWidgets>
//! [0]
NewAddressTab::NewAddressTab(QWidget *parent)
{
Q_UNUSED(parent);
descriptionLabel = new QLabel(tr("There are currently no contacts in your address book. "
"\nClick Add to add new contacts."));
addButton = new QPushButton(tr("Add"));
connect(addButton, &QAbstractButton::clicked, this, &NewAddressTab::addEntry);
mainLayout = new QVBoxLayout;
mainLayout->addWidget(descriptionLabel);
mainLayout->addWidget(addButton, 0, Qt::AlignCenter);
setLayout(mainLayout);
}
//! [0]
//! [1]
//从对话框中获取联系人的姓名和地址,并通过`sendDetails()`信号将其发送出去
void NewAddressTab::addEntry()
{
AddDialog aDialog;
if (aDialog.exec()) {
QString name = aDialog.nameText->text();
QString address = aDialog.addressText->toPlainText();
emit sendDetails(name, address);
}
}
adddialog.h
添加联系人时弹出的对话框
#ifndef ADDDIALOG_H
#define ADDDIALOG_H
#include <QDialog>
QT_BEGIN_NAMESPACE
class QLabel;
class QPushButton;
class QTextEdit;
class QLineEdit;
QT_END_NAMESPACE
//! [0]
class AddDialog : public QDialog
{
Q_OBJECT
public:
AddDialog(QWidget *parent = 0);
QLineEdit *nameText;
QTextEdit *addressText;
private:
QLabel *nameLabel;
QLabel *addressLabel;
QPushButton *okButton;
QPushButton *cancelButton;
};
//! [0]
#endif // ADDDIALOG_H
adddialog.cpp
#include "adddialog.h"
#include <QtWidgets>
//! [0]
AddDialog::AddDialog(QWidget *parent)
: QDialog(parent)
{
nameLabel = new QLabel("Name");
addressLabel = new QLabel("Address");
okButton = new QPushButton("OK");
cancelButton = new QPushButton("Cancel");
nameText = new QLineEdit;
addressText = new QTextEdit;
QGridLayout *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);
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(okButton);
buttonLayout->addWidget(cancelButton);
gLayout->addLayout(buttonLayout, 2, 1, Qt::AlignRight);
QVBoxLayout *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"));
}
tablemodel.h
#ifndef TABLEMODEL_H
#define TABLEMODEL_H
#include <QAbstractTableModel>
#include <QList>
//! [0]
//tablemodel的基本数据结构,比较简单
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;
}
//tablemodel继承QAbstractTableModel并重写许多函数,数据只有一个QList<Contact> contacts
class TableModel : public QAbstractTableModel
{
Q_OBJECT
public:
TableModel(QObject *parent = 0);
TableModel(QList<Contact> contacts, QObject *parent = 0);
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;
QList<Contact> getContacts() const;
private:
QList<Contact> contacts;
};
//! [0]
#endif // TABLEMODEL_H
tablemodel.cpp
#include "tablemodel.h"
//! [0]
TableModel::TableModel(QObject *parent)
: QAbstractTableModel(parent)
{
}
TableModel::TableModel(QList<Contact> contacts, QObject *parent)
: QAbstractTableModel(parent)
, contacts(contacts)
{
}
//! [0]
//! [1]
//返回数据的大小
int TableModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return contacts.size();
}
//返回列数
int TableModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 2;
}
//! [1]
//! [2]
//返回各类数据
QVariant TableModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.row() >= contacts.size() || index.row() < 0)
return QVariant();
//Qt::DisplayRole一般返回界面显示的内容
if (role == Qt::DisplayRole) {
const auto &contact = contacts.at(index.row());
if (index.column() == 0)
return contact.name;
else if (index.column() == 1)
return contact.address;
}
return QVariant();
}
//! [2]
//! [3]
//返回表格模型的头数据
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:
return QVariant();
}
}
return QVariant();
}
//! [3]
//! [4]
//插入数据
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;
}
//! [4]
//! [5]
//移除数据
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;
}
//! [5]
//! [6]
//设置各角色的数据
bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (index.isValid() && role == Qt::EditRole) {
int row = index.row();
auto contact = contacts.value(row);
if (index.column() == 0)
contact.name = value.toString();
else if (index.column() == 1)
contact.address = value.toString();
else
return false;
contacts.replace(row, contact);
emit dataChanged(index, index, {role});
return true;
}
return false;
}
//! [6]
//! [7]
//返回tablemodel的一些属性
Qt::ItemFlags TableModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::ItemIsEnabled;
return QAbstractTableModel::flags(index) | Qt::ItemIsEditable;
}
//! [7]
//! [8]
QList<Contact> TableModel::getContacts() const
{
return contacts;
}
//! [8]
总结
AddressWidget类是一个QTabWidget子类,用于操作示例中显示的10个选项卡:9个字母组选项卡和NewAddressTab的一个实例。NewAddressTab类是QWidget的一个子类,它只在地址簿为空时使用,提示用户添加一些联系人。AddressWidget还与TableModel的实例交互,向地址簿中添加、编辑和删除条目。
TableModel是QAbstractTableModel的子类,它提供了标准的模型/视图API来访问数据。它包含已添加的联系人列表。然而,这些数据并不都在一个选项卡中可见。相反,QTableView用于根据字母组提供相同数据的9个不同视图。
QSortFilterProxyModel是负责为每组联系人筛选联系人的类。每个代理模型都使用QRegExp来过滤不属于对应字母组的联系人。AddDialog类用于从用户那里获取地址簿的信息。这个QDialog子类由NewAddressTab实例化以添加联系人,由addresswget实例化以添加和编辑联系人。