配套视频讲解:https://www.bilibili.com/video/BV1uf4y1U7aa/
地址簿示例
地址簿示例展示了如何使用代理模式,基于同一个模型数据展示不同的视图。
这个例子提供了一个地址簿,支持将联系人按人名的头字母划分为9组: ABC, DEF, GHI, … , VW, …, XYZ。该功能通过QSortFilterProxyModel 类对模型进行过滤,生成9个代理模型对象,分别将过滤后的数据交给9个视图对象进行显示。对象图如下所示:
概述
本示例包含五个类: MainWindow, AddressWidget, TableModel, NewAddressTab 和 AddDialog。MainWindow 类使用AddressWidget 作为 central widget 并提供了 File 和 Tools 菜单.
AddressWidget 是 QTabWidget 的子类,用于实现10个tab:9个字母分组,和1个NewAddressTab ( QWidget 的子类当模型中数据为空时会显示 )。AddressWidget 还负责与TableModel实例的交互工作,包含增加、编辑、删除地址簿中的条目。
TableModel 是 QAbstractTableModel 的子类,提供了标准的 model/view API 操作模型中的数据。数据为一个联系人列表,每一个条目包含一个结构体{联系人名称,地址}。但是,这个数据并没有展现在一个单独的tab中,而且是通过字母分组,展示到了9个tab中,每个tab包含一个QTableView 对象用户显示过滤后的数据。
QSortFilterProxyModel用户过滤联系人数据。每一个代理模型使用QRegExp(正则表达式)过滤掉不属于该组的条目。是QDialog的子类,用于从用户输入获取地址簿的信息。 在NewAddressTab 和 AddressWidget 中可以被调用生成,进行数据输入。
实现步骤
- 生成工程
启动Creator4.11.1
文件->新建文件或项目
创建一个Qt Widgets Application项目
后面按要求一路点下去,给项目起一个自己的名字,例如:myAddressBook。
在details步骤里,取消Generate form选项,UI的内容我都将采用代码实现。入下图所示:
Kits步骤里,一定要选择一个已有的编译器,入下图所示。多选几个也可以。例如,在我机器里,没有安装MSVC2017,如果只选择了该选项,在编译的时候将无法编程生成源文件。这里我选择了QT内置的WinGW编译器
完成后,将会看到一些默认的源文件,点击运行试一试效果:
-新建类
按上面概述的描述,先新建下面四个类的源文件(MainWindow已经有了):
AddressWidget, TableModel, NewAddressTab 和 AddDialog。
右键工程名称,选择Add New…,创建相应的C++ Class文件具体如下所示:
这里为了方便,所有的基类都选择QWidget。基类不是QWidget的后面可以在代码里进行修改。
重复上面操作,构出所需的所有类。如果现在不想一起创建,也可以后面再补充。
-源码分析
构造一个软件,我们应该从内往外,先创建TableModel,AddDialog,NewAddressTab,这些部分好比是构造外层的必需品; 然后再完成AddressWidget和MainWindow。
但为了测试、学习方便。我们可以先把MainWindow的框架搭建起来,有些目前用不到的函数可以先写入出头文件,然后注释掉,后面再补充完善。
从外往里的构建方式,往往会造成软件结构的混乱,在现实编程中应该尽量采用从内往外的方式。也就是先准备好源部件,再进行组装。最后按下开关,见证奇迹(当然,如何对模块进行测试又是一门大学问)。但从内往外的编程方式作为教学方法会不够直观,所以这里我们还是采用从外往里的构建过程。
有些程序员,项目初期往往令老板和客户抓狂。因为前期,甚至中后期都看不到什么效果,只能听程序员汇报完成了某一个模块,感觉都是准备工作,后面还遥遥无期。但最终的成品总是调理更清晰,bug也会比较少,总工期更短。
MainWindow
我们先来分析一下主窗口的源码,具体见注释。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "addresswidget.h"
#include <QMainWindow>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
//注意这里没有析构函数,因为例子里没有,这里我也就删掉了。
//代码里采用了QT5的信号槽语法,下面这一行可以删掉
private slots:
// 这部分内容需要构建完addressWidget以后会用到,先注释掉。
// void updateActions(const QItemSelection &selection);
void openFile();
void saveFile();
private:
void createMenus();
//注意这里是一个指针,在使用前,需要new一个对象出来才可以。否则会出现运行错误
AddressWidget *addressWidget;
//下面两个个action需要作为类的私有数据,是因为菜单项生成后,
//后面还行需要控制菜单项是否可用(setEnabled)。所以不能像其他菜单项一样,new完以后完全交给父窗口维护。
QAction *editAct;
QAction *removeAct;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include <QtWidgets>
MainWindow::MainWindow(QWidget *parent)
//addressWidget原本只是个空指针,必须先赋值。虽然很简单,但容易漏掉。
: QMainWindow(parent), addressWidget(new AddressWidget)
{
setCentralWidget(addressWidget);
createMenus();
setWindowTitle(tr("Address Book"));
}
//createMenus的代码比较简单,就不详细写注释了。
//此时我们的AddressWidget类并没有完成,所以注释掉了需要AddressWidget的部分。
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);
}
//openFile,saveFile是将文件内容导入内存中,以及将内存数据保持到文件中
//具体内容交给了addressWidget去完成。
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);
}
//updateActions函数会再addressWidget有条目被选中是被调用。默认disable的编辑和删除菜单项将会enable
//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所需的TableModel和NewAddressTab还没有完成。因此,我们这里只是先把界面的中需要出现的10个tab显示出来。
#ifndef ADDRESSWIDGET_H
#define ADDRESSWIDGET_H
#include "newaddresstab.h"
#include "tablemodel.h"
#include <QItemSelection>
#include <QTabWidget>
//注意,这里基类是QTabWidget
class AddressWidget : public QTabWidget
{
Q_OBJECT
public:
AddressWidget(QWidget *parent = nullptr);
private:
//为tabwidget添加tabs。后面我还将在这里设置model和view。
void setupTabs();
TableModel *table;
NewAddressTab *newAddressTab;
};
#endif // ADDRESSWIDGET_H
#include "addresswidget.h"
#include "adddialog.h"
#include <QtWidgets>
AddressWidget::AddressWidget(QWidget *parent) : QTabWidget(parent),
table(new TableModel(this)),
newAddressTab(new NewAddressTab(this))
{ //添加一个tab,改tab只有在地址簿数据为空的时候才会出现。
addTab(newAddressTab, tr("Address Book"));
setupTabs();
}
//只是简单的添加了9个字母组分类的tabs。
void AddressWidget::setupTabs()
{
const auto groups = { "ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ" };
for (const QString &str : groups) {
QTableView *tableView = new QTableView;
addTab(tableView, str);
}
}
现在来看看运行结果吧:
AddressWidget 是与用户交互的主要部分,也是本示例中最关键的部分,目前我们只完成了一小部分。下面我们将先完成所需的“元器件”( TableModel , NewAddressTab ),再回来补充完善剩下的部分。
NewAddressTab
NewAddressTab 的实现比较简单,只是在界面中显示一段提示信息,加上一个Add按钮。Add按钮的功能实现需要AddDialog的实现,这部分功能我们将后面完善。
#ifndef NEWADDRESSTAB_H
#define NEWADDRESSTAB_H
#include <QWidget>
class NewAddressTab : public QWidget
{
Q_OBJECT
public:
explicit NewAddressTab(QWidget *parent = nullptr);
void addEntry();
signals:
//sendDetails这里只是一个singal,不需要在cpp文件中实现。只需要通过信号槽机制,把参数传出去就行。
void sendDetails(const QString &name, const QString &address);
};
#endif // NEWADDRESSTAB_H
#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());
}
TableModel
总算到了TableModel,TableModel本该是最早构建的内容。没有model,就没有数据,也就View不出任何东西。
QT的model/view机制,使得model中的数据发生变化时,views会自动更新。TableModel管理的是整个电话簿的数据,在AddressWidget,添加tabs的过程中我们将为9个tab创建9个代理model,与9个view一一对应。具体参见上面的对象图。
#ifndef TABLEMODEL_H
#define TABLEMODEL_H
#include <QAbstractTableModel>
#include <QVector>
//! [0]
//结构Contact包含名字和地址
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;
}
//注意这里基类要写成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;
//源码中有ItemFlags,但本例中没有用到可以删掉。
// 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
#include "tablemodel.h"
TableModel::TableModel(QObject *parent)
: QAbstractTableModel(parent)
{
}
//行数,如果不存在父级索引,表示没有数据返回0。否则返回条目的数量。
//contacts是一个vector容器,通过size()函数可以获得容器中的条目个数。
int TableModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : contacts.size();
}
//列数,与行数类似。列数只可能为0,或2(名称,地址)
int TableModel::columnCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : 2;
}
//返回model中的某一个数据,并不是一个条目,而是表格里的一个项。
//我们的model是基于QAbstractTableModel,数据的索引类似二维数组的下标。
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();
}
//插入数据(起点,行数,父级索引)
bool TableModel::insertRows(int position, int rows, const QModelIndex &index)
{
//没有实质性的作用,用来避免编译器警告,对于tree来说,index需要使用,我们这里是table,index用不上。
Q_UNUSED(index);
//必须吧contacts的操作,包含在begin和end中,这样对应的views才会自动更新。
beginInsertRows(QModelIndex(), position, position + rows - 1);
//插入时,name和address都为空字符串。需要配合setData一起使用。
for (int row = 0; row < rows; ++row)
contacts.insert(position, { QString(), QString() });
endInsertRows();
return true;
}
//与插入类似
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;
}
//注意这里的数据角色需要时Qt::EditRole
//通过索引找到需要修改的项,通过value修改它。
//使用方式举例:setData(index, name, Qt::EditRole);
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);
//需要通知数据的使用者,数据发生了变化。views才会更新数据。dataChanged是基类的函数。
//topLeft, bottomRight两个index划出需要更新的范围。
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
return false;
}
const QVector<Contact> &TableModel::getContacts() const
{
return contacts;
}
AddDialog
AddDialog是一个零时窗口,用户修改或添加条目。
#ifndef ADDDIALOG_H
#define ADDDIALOG_H
#include <QDialog>
class QLineEdit;
class 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;
};
#endif // ADDDIALOG_H
#include "adddialog.h"
#include <QtWidgets>
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);
}
NewAddressTab(2)
现在回到NewAddressTab,补充完善addEntry()函数(取消我们之前加上的注释即可)。
点击Add之后我们将可以看到我们的AddDialog窗口,输入信息,点击OK后,名称和地址将会通过信号槽机制发送出去。真正执行插入操作的是AddressWidget::addEntry
AddressWidget(2)
现在构建AddressWidget所需的源部件都已经准备完善。可以来看看完整的代码了。
#ifndef ADDRESSWIDGET_H
#define ADDRESSWIDGET_H
#include "newaddresstab.h"
#include "tablemodel.h"
#include <QItemSelection>
#include <QTabWidget>
//注意,这里基类是QTabWidget
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:
//为tabwidget添加tabs。后面我还将在这里设置model和view。
void setupTabs();
TableModel *table;
NewAddressTab *newAddressTab;
};
#endif // ADDRESSWIDGET_H
#include "addresswidget.h"
#include "adddialog.h"
#include <QtWidgets>
AddressWidget::AddressWidget(QWidget *parent)
: QTabWidget(parent),
table(new TableModel(this)),
newAddressTab(new NewAddressTab(this))
{
//newAddressTab中点击add按钮,将会出发AddressWidget::addEntry
connect(newAddressTab, &NewAddressTab::sendDetails,
this, &AddressWidget::addEntry);
addTab(newAddressTab, tr("Address Book"));
setupTabs();
}
void AddressWidget::showAddEntryDialog()
{
AddDialog aDialog;
//和newAddressTab中点击add按钮效果一样。都是将AddDialog的信息,发送给AddressWidget::addEntry
if (aDialog.exec())
addEntry(aDialog.name(), aDialog.address());
}
void AddressWidget::addEntry(const QString &name, const QString &address)
{
if (!table->getContacts().contains({ name, address })) {
//index的行总是0,也就是从表头插入
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);
//此时数据条目不为空,newAddressTab没必要存在了。
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());
//我们已经在setuptabs里为view设置了model,所以我们知道view的类型为代理model
QSortFilterProxyModel *proxy = static_cast<QSortFilterProxyModel*>(temp->model());
//QTableView内置QItemSelectionModel,记录被选中的indexes。
QItemSelectionModel *selectionModel = temp->selectionModel();
const QModelIndexList indexes = selectionModel->selectedRows();
QString name;
QString address;
int row = -1;
//从model中提取数据
//这里只能单选,所以可以不用for。自己使用const QModelIndex &index =indexes.first();效果一样
for (const QModelIndex &index : indexes) {
//通过mapToSource,可以找到真正的原始模型中的数据
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
AddDialog aDialog;
aDialog.setWindowTitle(tr("Edit a Contact"));
aDialog.editAddress(name, address);
//修改值,保存回model。只能修改address
if (aDialog.exec()) {
const QString newAddress = aDialog.address();
if (newAddress != address) {
const QModelIndex index = table->index(row, 1, QModelIndex());
//setData中会调用 emit dataChanged,通知views更新数据。
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) {
//正则表达式,以str任意一个开头的单词
const auto regExp = QRegularExpression(QString("^[%1].*").arg(str),
QRegularExpression::CaseInsensitiveOption);
auto proxyModel = new QSortFilterProxyModel(this);
proxyModel->setSourceModel(table);
proxyModel->setFilterRegularExpression(regExp);
proxyModel->setFilterKeyColumn(0);
//上面是models,下面是views
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);
//当QTabWidget当前选项发生变化时,需要selectionChanged通知菜单(编辑,删除)
//MainWindow::updateActions会被执行。修改菜单项的状态。
connect(tableView->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &AddressWidget::selectionChanged);
//当选中切的tab发送变化时,tabview会清除选中状态。但我们的菜单并不知道,编辑,删除仍然可用。
//这时候进行编辑,删除会找不到index而造成程序的crash。所以需要使用emit selectionChanged
connect(this, &QTabWidget::currentChanged, this, [this, tableView](int tabIndex) {
if (widget(tabIndex) == tableView)
emit selectionChanged(tableView->selectionModel()->selection());
});
addTab(tableView, str);
}
}
//文件的读写依赖于TableModel中<<和>>符号的重载。
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();
}
MainWindow(2)
最后再完善一下MainWindow,就可以大功告成了!!!
这里面的内容,都是对AddressWidget的调用,看代码就很清楚了,不需要太多解释。
#include "mainwindow.h"
#include <QtWidgets>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), addressWidget(new AddressWidget)
{
setCentralWidget(addressWidget);
createMenus();
setWindowTitle(tr("Address Book"));
}
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);
}
}