提要:Qt的这个示例主要讲的是使用代理模型,实现在不同的视图上面显示单个数据模型的数据 这个示例提供了一个地址簿,将联系人按照名称字母{"ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"}分成9个组。这是通过在同一个模型上使用多个视图实现的,每个视图都使用QSortFilterProxyModel类的一个实例进行过滤。地址簿包含5个类:MainWindow、AddressWidget、TableModel、NewAddressTab和AddDialog。MainWindow类使用AddressWidget作为其中心小部件,并提供文件和工具菜单。(与官方示例不同的地方是:MainWindow,使用AddressBook类继承了一下)
源码地址:https://gitee.com/mao_zg/AddressBook
官方结构图:
结构图
自己实现的结构图: 连接线我使用了依赖关系来连接
结构图
AddressWidget类是一个QTabWidget子类,用于操作示例中显示的10个选项卡:9个字母组选项卡和一个NewAddressTab实例。NewAddressTab类是QWidget的一个子类,它只在地址簿为空时使用,提示用户添加一些联系人。AddressWidget还与TableModel的实例进行交互,以添加、编辑和删除地址簿中的条目。
TableModel是QAbstractTableModel的子类,它提供了访问数据的标准模型/视图API。它包含一个添加联系人列表。但是,这些数据在单个选项卡中并不都是可见的。相反,根据字母表组,QTableView被用来提供相同数据的9种不同视图。
QSortFilterProxyModel是负责过滤每个联系人组的联系人的类。每个代理模型使用一个QRegExp来过滤不属于相应字母组的联系人。AddDialog类用于从用户获取地址簿的信息。这个QDialog子类由NewAddressTab实例化以添加联系人,并由AddressWidget实例化以添加和编辑联系人。
在官方示例的基础之上,把MainWindow使用AddressBook继承了一下。
实现的话,按照从底层到上层的方式实现,那么先实现TableModel。 TableModel类通过子类化QAbstractTableModel来提供标准API来访问联系人列表中的数据。为此必须实现的基本函数有:rowCount()、columnCount()、data()、headerData()。要使TableModel可编辑,它必须提供实现insertRows()、removeRows()、setData()和flags()函数。
1、TableModel的定义
Contact是数据模型所使用和管理的数据
代码语言:javascript复制//记录地址簿数据
struct Contact
{
QString strName;
QString strAddress;
//重载等于操作符
bool operator==(const Contact& oContact) const
{
return strName == oContact.strName && strAddress == oContact.strAddress;
}
};
接下来是一段重载QDataStream的IO操作,这两个重载是为了实现读取、存储文件功能。
代码语言:javascript复制//输出
inline QDataStream& operator<<(QDataStream& stream,const Contact& oContact)
{
return stream << oContact.strName << oContact.strAddress;
}
//输入
inline QDataStream& operator>>(QDataStream& stream, Contact& oContact)
{
return stream >> oContact.strName >> oContact.strAddress;
}
我这里新增了一个枚举变量的定义,为了标识表格的列,避免代码中出现魔鬼数字,以及支撑后期列的扩展变化。
代码语言:javascript复制enum class AddressBookColumn
{
name = 0,
address
};
接下来是类的定义: 这里使用了两个构造函数,一个是使用TableModel自己的默认构造函数,另一个是使用QVector<Contact>作为参数的构造函数,这是为了方便起见。TableModel中的最后一个函数getContacts()返回QVector<Contact>对象,该对象保存通讯录中的所有联系人。
代码语言:javascript复制class TableModel : public QAbstractTableModel
{
Q_OBJECT
public:
TableModel(QObject* parent = nullptr);
TableModel(const QVector<Contact>& contacts, QObject* parent = nullptr);
~TableModel();
virtual int rowCount(const QModelIndex& parent) const override;
virtual int columnCount(const QModelIndex& parent) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
virtual QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
virtual bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;
virtual bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;
const QVector<Contact>& getContacts() const { return m_oContacts; };
private:
QVector<Contact> m_oContacts;
};
2、TableModel的实现
实现头文件中定义的两个构造函数。第二个构造函数使用参数值初始化模型中的联系人列表。 由于本示例的列是固定的两列,所以这里增加了一个常量来定义列的个数,后期增加列的话直接修改该常量即可
代码语言:javascript复制static const int c_nColumnCnt = 2;
TableModel::TableModel(QObject * parent /*= nullptr*/)
: QAbstractTableModel(parent)
{
}
TableModel::TableModel(const QVector<Contact> & contacts, QObject * parent /*= nullptr*/)
:QAbstractTableModel(parent), m_oContacts(contacts)
{
}
官方原话:rowCount()和columnCount()函数返回模型的维数。然而,rowCount()的值将根据添加到地址簿的联系人数量而变化,columnCount()的值总是2,因为我们只需要名称和地址列的空间。 官方示例的实现代码:
官方代码
我的写法:
代码语言:javascript复制int TableModel::rowCount(const QModelIndex& parent) const
{
//行数会根据数据量而变化
return m_oContacts.size();
}
int TableModel::columnCount(const QModelIndex& parent) const
{
//官方示例这里给了数值2,不符合代码规范,这里定义一个常量,未来扩展列数,比如添加一个邮编列,只需要
//修改常量的值就好
return c_nColumnCnt;
}
没有必要写成官方那样复杂,行数就是数据量,而列数又是一个固定值。所以直接返回即可。
data()函数根据提供的模型索引的内容返回名称或地址。模型索引中存储的行号用于引用联系人列表中的项。
代码语言:javascript复制QVariant TableModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
{
return {};
}
if (Qt::DisplayRole == role)
{
//预防越界访问
if (index.row() > rowCount(index) ||
index.row() < 0)
{
return {};
}
const auto& oContact = m_oContacts.at(index.row());
switch ((AddressBookColumn)index.column())
{
case AddressBookColumn::name:
return oContact.strName;
case AddressBookColumn::address:
return oContact.strAddress;
default:
break;
}
}
return QVariant();
}
headerData()函数的作用是:显示表的标题,“Name”和“Address”。
代码语言:javascript复制QVariant TableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (Qt::DisplayRole != role)
{
return {};
}
if (Qt::Horizontal == orientation)
{
switch ((AddressBookColumn)section)
{
case AddressBookColumn::name:
return tr("Name");
case AddressBookColumn::address:
return tr("Address");
default:
break;
}
}
return QVariant();
}
insertRows()函数的作用是:在添加新数据之前调用insertRows()函数,否则数据将不会显示。调用beginInsertRows()和endInsertRows()函数以确保所有连接的视图都知道这些更改。该函数是提供给添加联系人的功能使用的,在插入数据之前,先在表格内添加一行,然后容器添加一条空记录。
代码语言:javascript复制bool TableModel::insertRows(int row, int count, const QModelIndex& parent)
{
Q_UNUSED(parent);
beginInsertRows(parent, row, row count - 1);
for (int i = 0; i < count; i)
{
m_oContacts.insert(row, { QString(), QString() });
}
endInsertRows();
return true;
}
调用removeRows()函数来删除数据。再次调用beginRemoveRows()和endRemoveRows(),以确保所有连接的视图都知道这些更改。 写的时候需要注意一下,begin、end在插入删除上函数较为类似,不要写反了。
代码语言:javascript复制bool TableModel::removeRows(int row, int count, const QModelIndex& parent)
{
Q_UNUSED(parent);
beginRemoveRows(parent, row, row count - 1);
for (int i = 0; i < count; i)
{
m_oContacts.removeAt(row);
}
endRemoveRows();
return true;
}
setData()函数的作用是:向表中逐项而不是逐行插入数据。这意味着要填充地址本中的一行,必须调用两次setData(),因为每一行有两列。 发出dataChanged()信号很重要,因为它告诉所有连接的视图更新它们的显示。 同时需要关注一下返回值,如果返回值写的有问题,数据刷新就会存在问题。 insertRows()是在容器内插入了一行空行,那么setData()函数就是给当前新插入的一行空行写入数据。
代码语言:javascript复制bool TableModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (index.isValid() && Qt::EditRole == role)
{
const auto& row = index.row();
auto oContact = m_oContacts.value(row);
switch (AddressBookColumn(index.column()))
{
case AddressBookColumn::name:
oContact.strName = value.toString();
break;
case AddressBookColumn::address:
oContact.strAddress = value.toString();
break;
default:
return false;
}
m_oContacts.replace(row, oContact);
emit dataChanged(index, index, { Qt::DisplayRole, Qt::EditRole });
return true;
}
return false;
}
flags()函数的作用是:返回给定索引的项标志 设置Qt::ItemIsEditable标志,因为希望允许编辑TableModel。虽然在本例中没有使用QTableView对象的编辑特性,但是在这里启用了它们,这样就可以在其他程序中重用这个模型。
代码语言:javascript复制Qt::ItemFlags TableModel::flags(const QModelIndex& index) const
{
if (!index.isValid())
{
return Qt::ItemIsEnabled;
}
return QAbstractTableModel::flags(index) | Qt::ItemIsEnabled;
}
3、AddressWidget的定义
AddressWidget类在技术上是本例中涉及的主要类,因为它提供了添加、编辑和删除联系人、将联系人保存到文件中以及从文件中加载联系人的功能
代码语言:javascript复制class AddressWidget : public QTabWidget
{
Q_OBJECT
public:
AddressWidget(QWidget* parent = nullptr);
~AddressWidget();
void readFromFile(const QString& strFile);
void writeToFile(const QString& strFile);
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* m_pTableModel = nullptr;
NewAddressTab* m_pNewAddressTab = nullptr;
};
4、AddressWidget的实现
AddressWidget构造函数接受一个父小部件并实例化NewAddressTab、TableModel和QSortFilterProxyModel。添加NewAddressTab对象(用于指示地址簿为空),其余9个选项卡使用setupTabs()设置。 注意:NewAddressTab在这之前没有定义
代码语言:javascript复制AddressWidget::AddressWidget(QWidget * parent /*= nullptr*/)
:QTabWidget(parent),m_pTableModel(new TableModel(this)),
m_pNewAddressTab(new NewAddressTab(this))
{
connect(m_pNewAddressTab, &NewAddressTab::sendDetails, this, &AddressWidget::addEntry);
addTab(m_pNewAddressTab, tr("Address Book"));
setupTabs();
}
这里就先跳转到NewAddressTab的定义与实现,因为AddressWidget依赖它。
4.1、NewAddressTab定义
NewAddressTab类提供一个提供信息的选项卡,告诉用户地址簿是空的。它根据地址簿的内容是否为空来控制显示和消失。 界面效果如图:
NewAddressTab
NewAddressTab类扩展了QWidget并包含QLabel和QPushButton。
代码语言:javascript复制class NewAddressTab : public QWidget
{
Q_OBJECT
public:
NewAddressTab(QWidget* parent = nullptr);
~NewAddressTab();
public slots:
void addEntry();
signals:
void sendDetails(const QString& name, const QString& address);
};
从代码上面可以看到有一个sendDetails的信号,这个信号就是添加联系人所发出的信号,主要用来通知视图刷新数据以及存储新增数据。
4.2、NewAddressTab实现
构造函数实例化addButton、descriptionLabel并将addButton的信号连接到addEntry()槽。 addEntry()函数与AddressWidget的addEntry()类似,因为这两个函数都实例化了一个AddDialog对象。通过发出sendDetails()信号,提取对话框中的数据并将其发送到AddressWidget的addEntry()槽。
这个AddDialog就是实现添加数据的对话框,在NewAddressTab、AddressWidget中都有调用。
image.png
代码语言:javascript复制NewAddressTab::NewAddressTab(QWidget * parent /*= nullptr*/)
:QWidget(parent)
{
auto pDescriptionLabel = new QLabel(tr("There are currently no contacts in your address book. "
"nClick Add to add new contacts."), this);
auto pAddBtn = new QPushButton(tr("Add"), this);
auto pMainLayout = new QVBoxLayout(this);
pMainLayout->addWidget(pDescriptionLabel);
pMainLayout->addWidget(pAddBtn, 0, Qt::AlignCenter);
setLayout(pMainLayout);
connect(pAddBtn, &QPushButton::clicked, this, &NewAddressTab::addEntry);
}
NewAddressTab::~NewAddressTab()
{
}
void NewAddressTab::addEntry()
{
AddDialog oDialog;
if (oDialog.exec() == QDialog::Accepted)
{
sendDetails(oDialog.name(), oDialog.address());
}
}
啊,这里又出现了一个AddDialog,这个在之前也没有定义过,那么我们还需要定义它,不然无法通过编译不是吗?
4.3、AddDialog定义
AddDialog类扩展了QDialog,并为用户提供QLineEdit和QTextEdit,以便将联系人数据(姓名、地址)输入地址簿。 实现后的界面如下图:
AddDialog
代码语言:javascript复制class AddDialog : public QDialog
{
Q_OBJECT
public:
AddDialog(QWidget* parent = nullptr);
~AddDialog();
QString name() const;
QString address() const;
void editAddress(const QString& strName, const QString& strAddress);
private:
QLineEdit* m_pNameEdit = nullptr;
QTextEdit* m_pAddressEdit = nullptr;
};
4.4、AddDialog实现
AddDialog的构造函数设置用户界面,创建必要的小部件并将它们放置到布局中。 大家注意QGridLayout,这个网格布局,对齐方式比较常用,各个控件之间的间隔、对齐调整起来较为费时。 界面布局这里使用了网格、垂直、水平三种布局方式,在做界面设计的时候,这三种布局是非常常用的。而且布局除了可以添加QWidget之外也可以添加其他Layout setWindowTitle()该函数是用来设置窗体标题的,我们这里给了一个常量,标题可以设置成参数传递进来,这样可以做成一个可定制窗体
代码语言:javascript复制AddDialog::AddDialog(QWidget * parent /*= nullptr*/)
:QDialog(parent), m_pNameEdit(new QLineEdit(this)), m_pAddressEdit(new QTextEdit(this))
{
auto pNameLab = new QLabel("name", this);
auto pAddressLab = new QLabel("address", this);
auto pOkBtn = new QPushButton("OK", this);
auto pCancelBtn = new QPushButton("Cancel", this);
auto pLayout = new QGridLayout(this);
pLayout->setColumnStretch(1, 2);
pLayout->addWidget(pNameLab, 0, 0);
pLayout->addWidget(m_pNameEdit, 0, 1);
pLayout->addWidget(pAddressLab, 1, 0, Qt::AlignLeft | Qt::AlignTop); //左对齐、顶部对齐
pLayout->addWidget(m_pAddressEdit, 1, 1, Qt::AlignLeft);
auto pBtnLayout = new QHBoxLayout(this);
pBtnLayout->addWidget(pOkBtn);
pBtnLayout->addWidget(pCancelBtn);
pLayout->addLayout(pBtnLayout, 2, 1, Qt::AlignRight); //右对齐
auto pMainLayout = new QVBoxLayout(this);
pMainLayout->addLayout(pLayout);
setLayout(pMainLayout);
connect(pOkBtn, &QAbstractButton::clicked, this, &QDialog::accept);
connect(pCancelBtn, &QAbstractButton::clicked, this, &QDialog::reject);
setWindowTitle(tr("Add a Contact"));
}
提供两个接口函数,以获取界面输入,封装自身属性。
代码语言:javascript复制QString AddDialog::name() const
{
if (nullptr == m_pNameEdit)
{
return {};
}
return m_pNameEdit->text();
}
QString AddDialog::address() const
{
if (nullptr == m_pAddressEdit)
{
return {};
}
return m_pAddressEdit->toPlainText();
}
editAddress这个函数是提供给添加使用的,当地址簿中已经存在联系人数据的时候,编辑、修改已有数据,这些数据需要显示在界面中同时Name项无法进行编辑,要把它设置为只读。
代码语言:javascript复制void AddDialog::editAddress(const QString& strName, const QString& strAddress)
{
if (nullptr != m_pNameEdit)
{
m_pNameEdit->setReadOnly(true);
m_pNameEdit->setText(strName);
}
if (nullptr != m_pAddressEdit)
{
m_pAddressEdit->setPlainText(strAddress);
}
}
OK,绕了这么久,现在可以回到AddressWidget的实现了。 setupTabs()函数用于在AddressWidget中设置9个字母组选项卡、表视图和代理模型。每个代理模型依次设置为使用不区分大小写的QRegExp对象根据相关字母表组过滤联系人名称。表视图也使用相应的代理模型的sort()函数按升序排序。每个表视图的selectionMode被设置为QAbstractItemView::SingleSelection(只能单选), selectionBehavior被设置为QAbstractItemView::SelectRows(按行选择),允许用户同时选择一行中的所有项。每个QTableView对象都会自动给出一个QItemSelectionModel来跟踪所选的索引。
代码语言:javascript复制void AddressWidget::setupTabs()
{
const auto oGroup = { "ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ" };
for (const auto& itemTab : oGroup)
{
const auto regExp = QRegularExpression(QString("^[%1].*").arg(itemTab), QRegularExpression::CaseInsensitiveOption);
auto pProxyModel = new QSortFilterProxyModel(this);
pProxyModel->setSourceModel(m_pTableModel);
pProxyModel->setFilterRegularExpression(regExp);
pProxyModel->setFilterKeyColumn((int)AddressBookColumn::name);
QTableView* pTab = new QTableView(this);
pTab->setModel(pProxyModel);
pTab->setSelectionBehavior(QAbstractItemView::SelectRows); //设置选择模式 按行选择
pTab->horizontalHeader()->setStretchLastSection(true); //最后一个选项是否占用剩余所有空间
pTab->verticalHeader()->hide(); //隐藏垂直标头
pTab->setEditTriggers(QAbstractItemView::NoEditTriggers); //设置编辑框不可编辑
pTab->setSelectionMode(QAbstractItemView::SingleSelection);
pTab->setSortingEnabled(true); // Enabled生效就立即执行排序
connect(pTab->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &AddressWidget::selectionChanged);
connect(this, &QTabWidget::currentChanged, this, [this, pTab](int nTabIndex) {
if (widget(nTabIndex) == pTab)
{
emit selectionChanged(pTab->selectionModel()->selection());
}
});
addTab(pTab, itemTab);
}
}
QItemSelectionModel类提供一个selectionChanged信号,该信号连接到AddressWidget的selectionChanged()信号。 我们还将QTabWidget::currentChanged()信号连接到发出AddressWidget的selectionChanged()的lambda表达式。 这两个信号是给菜单中的Edit Entry、Remove Entry两个Action使用的,这两个Action会根据选择的变化而进行刷新可用状态,当没有选择数据的时候,这两个Action是灰显不可用的状态,反之就是可用状态。
地址簿中的每个表视图都作为附签添加到QTabWidget,并带有相关的标签,这些标签是从组的QStringList中获得的。
image.png
我们提供了两个addEntry()函数:一个用于接受用户输入,另一个用于执行向地址簿添加新条目的实际任务。我们将添加条目的职责分为两部分
,以允许newAddressTab插入数据,而不必弹出一个对话框。
第一个addEntry()函数是一个槽,函数名为:showAddEntryDialog,它连接到主窗口的
"Add Entry" Action。该函数创建一个AddDialog对象,然后调用第二个addEntry()函数来实际将联系人添加到表中。
代码语言:javascript复制void AddressWidget::showAddEntryDialog()
{
AddDialog oDialog;
if (oDialog.exec() == QDialog::Accepted)
{
addEntry(oDialog.name(), oDialog.address());
}
}
基本验证在第二个addEntry()函数中完成,以防止地址簿中的重复条目。正如在TableModel中提到的,这是我们需要getter方法getContacts()的部分原因。
代码语言:javascript复制void AddressWidget::addEntry(const QString& name, const QString& address)
{
if (!m_pTableModel->getContacts().contains({name, address}))
{
m_pTableModel->insertRows(0, 1, QModelIndex());
QModelIndex index = m_pTableModel->index(0, 0, QModelIndex());
m_pTableModel->setData(index, name, Qt::EditRole);
index = m_pTableModel->index(0, 1, QModelIndex());
m_pTableModel->setData(index, address, Qt::EditRole);
removeTab(indexOf(m_pNewAddressTab)); //当添加了一条地址后,添加地址的tab就被移除
}
else
{
QMessageBox::information(this, tr("Duplicate Name"),
tr("The name "%1" already exists.").arg(name));
}
}
如果模型还没有包含具有相同名称的条目,则调用setData()将名称和地址插入第一列和第二列。否则,我们将显示一个QMessageBox来通知用户。 注意:一旦添加了联系人,newAddressTab将被删除,因为地址簿不再为空。
editEntry只是更新联系人地址的一种方式,因为示例不允许用户更改现有联系人的名称。 首先,我们使用QTabWidget::currentWidget()获取活动选项卡的QTableView对象。然后我们从tableView中提取selectionModel来获取被选中的索引。
代码语言:javascript复制void AddressWidget::editEntry()
{
QTableView* pTempView = static_cast<QTableView*>(currentWidget());
if (nullptr == pTempView)
{
return;
}
QSortFilterProxyModel* pSortProxyModel = static_cast<QSortFilterProxyModel*>(pTempView->model());
if (nullptr == pSortProxyModel)
{
return;
}
QItemSelectionModel* pSelectModel = pTempView->selectionModel();
const QModelIndexList oIndexList = pSelectModel->selectedRows();
QString strName = "";
QString strAddress = "";
int nRow = -1;
for (const auto& oIndex : oIndexList)
{
nRow = pSortProxyModel->mapToSource(oIndex).row();
QModelIndex oNameIndex = m_pTableModel->index(nRow, 0, {});
QVariant name = m_pTableModel->data(oNameIndex, Qt::DisplayRole);
strName = name.toString();
QModelIndex oAddressIndex = m_pTableModel->index(nRow, 1, {});
QVariant address = m_pTableModel->data(oAddressIndex, Qt::DisplayRole);
strAddress = address.toString();
}
AddDialog oDialog;
oDialog.setWindowTitle(tr("Edit a Contact"));
oDialog.editAddress(strName, strAddress); //上文中说到的AddDialog中的editAddress函数,就是在这里调用的
if (oDialog.exec() == QDialog::Accepted)
{
const QString strNewAddress = oDialog.address();
if (strNewAddress != strAddress)
{
const QModelIndex oIndex = m_pTableModel->index(nRow, 1, {});
m_pTableModel->setData(oIndex, strNewAddress, Qt::EditRole);
}
}
}
实现效果如下图:
image.png
使用removeEntry()函数删除条目。通过QItemSelectionModel对象selectionModel访问被选中的行,从而删除它。只有当用户删除了地址簿中的所有联系人时,才会将newAddressTab重新添加到AddressWidget。
代码语言:javascript复制void AddressWidget::removeEntry()
{
QTableView* pTempView = static_cast<QTableView*>(currentWidget());
if (nullptr == pTempView)
{
return;
}
QSortFilterProxyModel* pSortProxyModel = static_cast<QSortFilterProxyModel*>(pTempView->model());
if (nullptr == pSortProxyModel)
{
return;
}
QItemSelectionModel* pSelectModel = pTempView->selectionModel();
const QModelIndexList oIndexList = pSelectModel->selectedRows();
for (const auto& oIndex : oIndexList)
{
int nRow = pSortProxyModel->mapToSource(oIndex).row();
m_pTableModel->removeRows(nRow, 1, {});
}
if (m_pTableModel->rowCount({}) == 0)
{
insertTab(0, m_pNewAddressTab, tr("Address Book"));
}
}
writeToFile()函数的作用是:保存一个包含通讯录中所有联系人的文件。文件以自定义的.dat格式保存。联系人列表的内容使用QDataStream写入文件。如果文件无法打开,则会显示一个QMessageBox,并显示相关的错误消息。 readFromFile()函数的作用是:加载一个包含通讯录中所有联系人的文件,该通讯录以前是使用writeToFile()保存的。QDataStream用于将.dat文件的内容读入联系人列表,每个联系人都是使用addEntry()添加的。这里就用到了开始的时候定义的QDataStream重载输入、输入操作符。
代码语言:javascript复制void AddressWidget::readFromFile(const QString& strFile)
{
QFile file(strFile);
if (!file.open(QIODevice::ReadOnly))
{
QMessageBox::information(this, tr("Unable to open file"),
file.errorString());
return;
}
QVector<Contact> oContacts;
QDataStream oStream(&file);
oStream >> oContacts;
if (oContacts.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(oContacts)) //qAsConst == std::as_const()
{
addEntry(contact.strName, contact.strAddress);
}
}
}
void AddressWidget::writeToFile(const QString& strFile)
{
QFile file(strFile);
if (!file.open(QIODevice::WriteOnly))
{
QMessageBox::information(this, tr("Unable to open file"), file.errorString());
return;
}
QDataStream oStream(&file);
oStream << m_pTableModel->getContacts();
}
5、addressBook定义
主窗体主要实现了,把AddressWidget窗体作为主窗体的中心界面,然后创建两个菜单,File、Tools,分别有Open、Save As、Add Entry、Edit Entry、Remove Entry等Action
代码语言:javascript复制class addressBook : public QMainWindow
{
Q_OBJECT
public:
addressBook(QWidget *parent = Q_NULLPTR);
private slots:
void updateActions(const QItemSelection& oSelection);
void openFile();
void saveFile();
private:
void createMenus();
private:
Ui::addressBookClass ui;
AddressWidget* m_pAddWidget = nullptr;
QAction* m_pEditAction = nullptr;
QAction* m_pRemoveAction = nullptr;
};
6、addressBook实现
addressBook的构造函数实例化AddressWidget,将其设置为其中心小部件,并调用createMenus()函数。
代码语言:javascript复制addressBook::addressBook(QWidget *parent)
: QMainWindow(parent), m_pAddWidget(new AddressWidget(this))
{
setCentralWidget(m_pAddWidget);
setWindowTitle(tr("Address Book"));
createMenus();
/*ui.setupUi(this);*/
}
createMenus()函数设置File、Open菜单,将操作连接到它们各自的槽。两个编辑条目Edit Entry和Remove Entry操作在默认情况下是禁用的,因为这样的操作不能在一个空的地址簿上执行。只有在添加一个或多个联系人时才启用它们。
代码语言:javascript复制void addressBook::createMenus()
{
//添加文件菜单以及Action
QMenu* pFileMenu = menuBar()->addMenu(tr("File"));
QAction* pOpenAct = new QAction(tr("&Open..."), this);
pFileMenu->addAction(pOpenAct);
connect(pOpenAct, &QAction::triggered, this, &addressBook::openFile);
QAction* pSaveAct = new QAction(tr("&Save As..."), this);
pFileMenu->addAction(pSaveAct);
connect(pSaveAct, &QAction::triggered, this, &addressBook::saveFile);
pFileMenu->addSeparator(); //此函数添加一个分隔符
QAction* pExitAct = new QAction(tr("E&xit"), this);
pFileMenu->addAction(pExitAct);
connect(pExitAct, &QAction::triggered, this, &QWidget::close);
//添加工具菜单以及Action
QMenu* pToolsMenu = menuBar()->addMenu(tr("&Tools"));
QAction* pAddAct = new QAction(tr("&Add Entry..."), this);
pToolsMenu->addAction(pAddAct);
connect(pAddAct, &QAction::triggered, m_pAddWidget, &AddressWidget::showAddEntryDialog);
m_pEditAction = new QAction(tr("&Edit Entry..."), this);
pToolsMenu->addAction(m_pEditAction);
connect(m_pEditAction, &QAction::triggered, m_pAddWidget, &AddressWidget::editEntry);
pToolsMenu->addSeparator();
m_pRemoveAction = new QAction(tr("&Remove Entry..."), this);
pToolsMenu->addAction(m_pRemoveAction);
connect(m_pRemoveAction, &QAction::triggered, m_pAddWidget, &AddressWidget::removeEntry);
connect(m_pAddWidget, &AddressWidget::selectionChanged, this, &addressBook::updateActions);
}
除了将所有动作的信号连接到它们各自的插槽之外,我们还将AddressWidget的selectionChanged()信号连接到它的updateActions()插槽。 把Add Entry Action的响应信号,绑定到了AddressWidget的showAddEntryDialog槽上面。
updateActions()函数的作用是:根据地址簿的内容决定禁用启用Edit Entry和Remove Entry。如果地址簿为空,则禁用这些操作;否则,它们是启用的。这个函数是一个插槽连接到AddressWidget的selectionChanged()信号。
代码语言:javascript复制void addressBook::updateActions(const QItemSelection& oSelection)
{
QModelIndexList oIndexs = oSelection.indexes();
if (!oIndexs.isEmpty())
{
m_pEditAction->setEnabled(true);
m_pRemoveAction->setEnabled(true);
}
else
{
m_pEditAction->setEnabled(false);
m_pRemoveAction->setEnabled(false);
}
}
那么最后就是打开和保存文件的Action实现了 打开的功能就是用来打开保存功能存储的文件,保存就是把地址簿中的联系人数据存储为文件,数据是二进制流数据。
代码语言:javascript复制void addressBook::openFile()
{
QString strFile = QFileDialog::getOpenFileName(this);
if (!strFile.isEmpty())
{
m_pAddWidget->readFromFile(strFile);
}
}
void addressBook::saveFile()
{
QString strFile = QFileDialog::getSaveFileName(this);
if (!strFile.isEmpty())
{
m_pAddWidget->writeToFile(strFile);
}
}