学习笔记之Qt从入门到精通(一)

整理日期: 2010年4月9日

曾经研究过的技术,现在编辑分享出来,很长,需要分三个链接来发布,大家耐心点看吧。

经常有人问哪里有学习Qt 的资料,Qt 的教程,怎么才能入门等等,或者抱怨说中文的信息太少。其实网上有很多关于Qt 的学习资料,今天在这里总结一下,希望各位想学习Qt 的同学,各取所需,早日从入门到精通!
关于本教程:
本教程是基于您已经了解C++的基础之上,毕竟Qt 是一个C++库。否则您必须先学习c++后再来看本教程,C++入门比较好的书籍是《C++程序设计》(谭浩强)。
某些人可能觉得《c++ primer》好,但我认为这本书不适合c++入门,所以还是推荐一下谭老的这本通俗易懂的书吧。
教程由3 部分组成,第一部分为新手上路,是Qt 的官方教程。其实把它放在第一部分并不合适,因为对于一个初学Qt 的人来说一上来就将最好是从一个HelloWorld 程序开始学习比较好。所以推荐初学者从第二部分Qt 学习之路开始阅读。
第三部分为Qt 深入编程,对Qt 很熟悉的读者可以深入研究研究。教程中的所有代码必须联网才能下载,给您造成的不便之处本人感到非常抱歉。

Part 1: 新手上路

Qt 官方学习教程

地址簿教程

本教程介绍了使用 Qt 跨平台框架的 GUI 编程。
在这里插入图片描述
在学习过程中,我们将了解部分 Qt 基本技术,如
 Widget 和布局管理器
 容器类
 信号和槽
 输入和输出设备
如果您完全不了解 Qt,请阅读如何学习 Qt(如果您还未阅读)。
教程的源代码位于 Qt 的 examples/tutorials/addressbook 目录下。
教程章节:

  1. 设计用户界面
  2. 添加地址
  3. 浏览地址簿条目
  4. 编辑和删除地址
  5. 添加查找功能
  6. 加载和保存
  7. 附加功能
    虽然这个小型应用程序看起来并不象一个成熟的现代 GUI 应用程序,但它使用
    多种用于更复杂应用程序的基本技术。在您完成学习之后,我们建议您查看一下
    应用程序示例,它提供带有菜单、工具栏、状态栏等项目的小型 GUI 应用程序。
地址簿 1 — 设计用户界面

文件:
 tutorials/addressbook/part1/addressbook.cpp
 tutorials/addressbook/part1/addressbook.h
 tutorials/addressbook/part1/main.cpp
 tutorials/addressbook/part1/part1.pro
本教程的第一部分讲述了用于地址簿应用程序的基本图形用户界面 (GUI) 的设
计。
创建 GUI 程序的第一步就是设计用户界面。在本章中,我们的目标是设置应用
基本地址簿应用程序所需的标签和输入字段。下图为期望输出的屏幕截图。
在这里插入图片描述
我们需要使用两个 QLabel 对象:nameLabel 和 addressLabel,以及两个输入
字段:QLineEdit 对象 nameLine 和 QTextEdit 对象 addressText,这样用户
才能输入联系人的姓名和地址。使用的 widget 及其位置如下图所示。
在这里插入图片描述
要应用地址簿需使用三个文件:
 addressbook.h — AddressBook 类的定义文件,
 addressbook.cpp — AddressBook 类的执行文件,以及
 main.cpp — 包含 main() 函数并带有 AddressBook 实例的文件。

Qt 编程 — 使用子类

在编写 Qt 程序时,我们通常使用 Qt 对象子类来添加功能。这是创建定制
widget 或标准 widget 集合的基本概念之一。使用子类扩展或改变 widget 的
操作具有以下优势:
 我们可以编写虚函数或纯虚函数应用,以得到我们确切所需的功能,并在需要时再
使用基本的类应用。
 这样我们就可以在类中封装部分用户界面,应用程序的其他部分也就无需了解用户
界面中单独使用的 widget。
 可使用子类在同一应用程序或库中创建多个定制 widget,这样子类的代码可在其他
项目重复使用。
由于 Qt 未提供特定的地址簿 widget,我们在标准的 Qt widget 类中使用子
类,然后添加功能。我们在本教程中创建的 AddressBook 类在需要使用基本地
址簿 widget 的情况下可重复使用。
定义 AddressBook 类
addressbook.h 文件用于定义 AddressBook 类。
我们从定义 AddressBook 为 QWidget 子类和声明构造器开始入手。我们还使用
Q_OBJECT 宏表明该类使用国际化功能与 Qt 信号和槽功能,即使在本阶段不会
用到所有这些功能。

class AddressBook : public QWidget
{
Q_OBJECT
public:
AddressBook(QWidget *parent = 0);
private:
QLineEdit *nameLine;
QTextEdit *addressText;
};

该类包含了 nameLine 和 addressText 的声明、上文提到的 QLineEdit 和
QTextEdit 的私有实例。在以后章节中,您会看到储存在 nameLine 和
addressText 中的数据在地址簿的许多功能中都会用到。
我们不必包含要使用的 QLabel 对象的声明,这是因为在创建这些对象后我们不
必对其进行引用。在下一部分中,我们会说明 Qt 记录对象所属关系的方式。
Q_OBJECT 宏本身应用了部分更高级的 Qt 功能。 我们暂时把 Q_OBJECT 宏理解
为使用 tr() 和 connect() 函数的快捷方式,这种理解对我们的学习更有用。
我们现已完成 addressbook.h 文件,接下来我们来执行对应的
addressbook.cpp 文件。

应用 AddressBook 类

AddressBook 的构造器接收 QWidget 参数 parent。按惯例,我们将参数传递给
基本类的构造器。这种父项可有一个或多个子项的所属概念对 Qt 中的 widget
分组十分有用。例如,如果删除父项,也会删除其所有子项。

AddressBook::AddressBook(QWidget *parent)
: QWidget(parent)
{
QLabel *nameLabel = new QLabel(tr("Name:"));
nameLine = new QLineEdit;
QLabel *addressLabel = new QLabel(tr("Address:"));
addressText = new QTextEdit;

在该构造器中,我们声明并通过实例来表示两个局部 QLabel 对象 nameLabel
和 addressLabel,以及 nameLine 和 addressText。如果字符串已进行转换,
则 tr() 函数返回已转换的字符串,否则返回字符串本身。我们可以将此函数理
解 标识来标记要进行转换 QString 对象。在以后
章节和 Qt Examples 中,您会看到只要使用了可转换的字符串就是使用该函数。
使用 Qt 编程时,了解布局是如何起作用的会对您很有帮助。Qt 提供三个主要
布局类 QHBoxLayout、QVBoxLayout 和 QGridLayout 来处理 widget 的位置。
在这里插入图片描述
我们使用 QGridLayout 以结构化的方式放置标签和输入字段。QGridLayout 将
可用空间分为网格,并将 widget 放置在指定了行列号的单元格中。上面的图表
显示了布局单元格和 widget 的位置。我们通过以下代码指定这种排列方式:

QGridLayout *mainLayout = new QGridLayout;
mainLayout->addWidget(nameLabel, 0, 0);
mainLayout->addWidget(nameLine, 0, 1);
mainLayout->addWidget(addressLabel, 1, 0, Qt::AlignTop);
mainLayout->addWidget(addressText, 1, 1);

请注意,addressLabel 是使用作为附加参数的 Qt::AlignTop 来排放位置。这
可确保其不会纵向放置在单元格 (1,0) 中央。有关 Qt 布局的基本简介,请参
见布局类文档。
要在 widget 上安装布局对象,必须调用 widget 的 setLayout() 函数:

setLayout(mainLayout);
setWindowTitle(tr("Simple Address Book"));
}

最后,我们将 widget 标题设置为“简单地址簿”。

运行应用程序

main() 函数使用单独的文件 main.cpp。在该函数中,我们实例化了
QApplication 对象 app。QApplication 负责管理多种应用范围的资源(如默认
字体和光标),以及运行事件循环。因此,在每个使用 Qt 的 GUI 应用程序中
都会有一个 QApplication 对象。

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
AddressBook *addressBook = new AddressBook;
addressBook->show();
return app.exec();
}

我们使用 new 关键字在堆中构造一个新的 AddressBook widget,然后调用
show() 函数对其进行显示。不过,该 widget 只有在应用程序事件循环开始时
才会显示。我们通过调用应用程序的 exec() 函数开始事件循环。该函数返回的
结果作为 main() 函数的返回值。

地址簿 2 — 添加地址

文件:
 tutorials/addressbook/part2/addressbook.cpp
 tutorials/addressbook/part2/addressbook.h
 tutorials/addressbook/part2/main.cpp
 tutorials/addressbook/part2/part2.pro
创建基本地址簿应用程序的下一步是添加少许用户互动操作。
在这里插入图片描述
我们将提供一个按钮,用户可点击该按钮来添加新联系人。此外,还需要对数据
结构进行限定,以便有序地储存这些联系人。

定义 AddressBook 类

由于已经设置了标签和输入字段,我们只需添加按钮就可完成添加联系人这一步
骤。也就是说,在 addressbook.h 文件中已经声明了三个 QPushButton 对象和
三个对应的公共槽。

public slots:
void addContact();
void submitContact();
void cancel();

槽是对特殊信号进行响应的函数。我们将在应用 AddressBook 类时进一步详细
说明这一概念。如需有关 Qt 信号和槽概念的简介,请参见信号和槽文档。
三个 QPushButton 对象分别是 addButton、submitButton 和 cancelButton,
已与要在上一章中说明的 nameLine 和 addressText 一同包含在私有变量声明
中。

private:
QPushButton *addButton;
QPushButton *submitButton;
QPushButton *cancelButton;
QLineEdit *nameLine;
QTextEdit *addressText;

我们需要一个容器来储存地址簿联系人,这样才能搜索和显示联系人。QMap 对
象 contacts 就可实现此功能,因为其带有一个键-值对:联系人姓名作为键,
而联系人地址作为值。

QMap<QString, QString> contacts;
QString oldName;
QString oldAddress;
};

我们还会声明两个私有 QString 对象:oldName 和 oldAddress。这些对象用来
保留在用户点击添加时最后显示的联系人姓名和地址。这样,当用户点击取消时,
我们就可以返回至上一个联系人的详细信息。

应用 AddressBook 类

在 AddressBook 构造器中,我们将 nameLine 和 addressText 设置为只读,这
样就可仅显示而不必编辑现有联系人的详细信息。

...
nameLine->setReadOnly(true);
...
addressText->setReadOnly(true);

然后,我们实例化以下按钮:addButton、submitButton 和 cancelButton。

addButton = new QPushButton(tr("&Add"));
addButton->show();
submitButton = new QPushButton(tr("&Submit"));
submitButton->hide();
cancelButton = new QPushButton(tr("&Cancel"));
cancelButton->hide();

显示 addButton 是通过调用 show() 函数实现的,而隐藏 submitButton 和
cancelButton 则需调用 hide()。这两个按钮仅当用户点击添加时才会显示,而
此操作是通过在下文中说明的 addContact() 函数处理的。

connect(addButton, SIGNAL(clicked()), this, SLOT(addContact()));
connect(submitButton, SIGNAL(clicked()), this, SLOT(submitContact()));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancel()));

我们将按钮的 clicked() 信号与其相应的槽关联。下面的图表说明了此过程。
在这里插入图片描述
接下来,我们将按钮整齐的排列在地址簿 widget 的右侧,使用 QVBoxLayout 将
其进行纵向排列。

QVBoxLayout *buttonLayout1 = new QVBoxLayout;
buttonLayout1->addWidget(addButton, Qt::AlignTop);
buttonLayout1->addWidget(submitButton);
buttonLayout1->addWidget(cancelButton);
buttonLayout1->addStretch();

addStretch() 函数用来确保按钮并不是采用均匀间隔排列的,而是更靠近
widget 的顶部。下图显示了是否使用 addStretch() 的差别。
在这里插入图片描述
然后,我们使用 addLayout() 将 buttonLayout1 增加至 mainLayout。 这样我
们就有了嵌套布局,因为 buttonLayout1 现在是 mainLayout 的子项。

QGridLayout *mainLayout = new QGridLayout;
mainLayout->addWidget(nameLabel, 0, 0);
mainLayout->addWidget(nameLine, 0, 1);
mainLayout->addWidget(addressLabel, 1, 0, Qt::AlignTop);
mainLayout->addWidget(addressText, 1, 1);
mainLayout->addLayout(buttonLayout1, 1, 2);

布局坐标显示如下:
在这里插入图片描述
在 addContact() 函数中,我们使用 oldName 和 oldAddress 储存最后显示的
联系人详细信息。然后,我们清空这些输入字段并关闭只读模式。输入焦点设置
在 nameLine,显示 submitButton 和 cancelButton。

void AddressBook::addContact()
{
oldName = nameLine->text();
oldAddress = addressText->toPlainText();
nameLine->clear();
addressText->clear();
nameLine->setReadOnly(false);
nameLine->setFocus(Qt::OtherFocusReason);
addressText->setReadOnly(false);
addButton->setEnabled(false);
submitButton->show();
cancelButton->show();
}

submitContact() 函数可分为三个部分:
我们从 nameLine 和 addressText 提取联系人的详细信息,然后将其储存在
QString 对象中。我们还要验证确保用户没有在输入字段为空时点击提交,否则,
会显示 QMessageBox 提示用户输入姓名和地址。

void AddressBook::submitContact()
{
    QString name = nameLine->text();
   QString address = addressText->toPlainText();

   if (name == "" || address == "") {
      QMessageBox::information(this, tr("Empty Field"),
      tr("Please enter a name and address."));
      return;
   }

我们接着继续检查是否联系人已存在。如果不存在,将联系人添加至 contacts,
然后显示 QMessageBox 提示用户已添加联系人。

   if (!contacts.contains(name)) {
      contacts.insert(name, address);
      QMessageBox::information(this, tr("Add Successful"),
      tr("\"%1\" has been added to your address book.").arg(name));
   } else {
      QMessageBox::information(this, tr("Add Unsuccessful"),
      tr("Sorry, \"%1\" is already in your address book.").arg(name));
      return;
   }

如果联系人已存在,还是会显示 QMessageBox 以提示用户,以免添加重
复的联系人。由于 contacts 对象是基于姓名地址的键-值对,因此要确
保键唯一。
在处理了上述两种情况后,使用以下代码将按钮恢复为正常状态:

    if (contacts.isEmpty()) {
       nameLine->clear();
       addressText->clear();
    }

    nameLine->setReadOnly(true);
    addressText->setReadOnly(true);
    addButton->setEnabled(true);
    submitButton->hide();
    cancelButton->hide();
} // void AddressBook::submitContact() End.

下面的屏幕截图显示了用于向用户显示提示信息的 QMessageBox 对象。
在这里插入图片描述
cancel() 函数恢复上次显示的联系人详细信息,并启用 addButton,还会隐藏
submitButton 和 cancelButton。

void AddressBook::cancel()
{
nameLine->setText(oldName);
nameLine->setReadOnly(true);
addressText->setText(oldAddress);
addressText->setReadOnly(true);
addButton->setEnabled(true);
submitButton->hide();
cancelButton->hide();
}

添加联系人的总体思想就是提高用户操作的灵活性,可在任何时候点击提交或取
消。下面的流程图详细说明了此概念:
在这里插入图片描述

地址簿 3 — 浏览地址簿条目

文件:
 tutorials/addressbook/part3/addressbook.cpp
 tutorials/addressbook/part3/addressbook.h
 tutorials/addressbook/part3/main.cpp
 tutorials/addressbook/part3/part3.pro
构建地址簿应用程序现已进展过半。我们需要增加一些函数,以便浏览联系人。
但首先要决定采用何种数据结构方式来储存这些联系人。
在第二章中,我们使用了 QMap 键-值对,即联系人姓名作为键,而联系人地址
作为值。这种方式很适合我们的实例。不过,要浏览和显示每个条目,还需要进
行一些改进。
我们改进 QMap 的方式是,将数据结构替换为类似循环链接的列表,其中所有元
素都是相互关联的,包括第一个元素和最后一个元素。下图图解说明了该数据结
构。
在这里插入图片描述

定义 AddressBook 类

要给地址簿应用程序增加浏览功能,我们需要为 AddressBook 类再增加两个函
数:next() 和 previous()。将这两个函数添加到 addressbook.h 文件中:

void next();
void previous();

我们还需要使用其他两个 QPushButton 对象,因此将 nextButton 和
previousButton 声明为私有变量:

QPushButton *nextButton;
QPushButton *previousButton;
应用 AddressBook 类

在 addressbook.cpp 的 AddressBook 构造器中,我们实例化 nextButton 和
previousButton,并且这两项默认为禁用。这是因为仅当地址簿中有多个联系人
时才会启用浏览功能。

nextButton = new QPushButton(tr("&Next"));
nextButton->setEnabled(false);
previousButton = new QPushButton(tr("&Previous"));
previousButton->setEnabled(false);

然后,我们将这两个按钮与其相应的槽关联:

connect(nextButton, SIGNAL(clicked()), this, SLOT(next()));
connect(previousButton, SIGNAL(clicked()), this, SLOT(previous()));

下图即为预期的图形用户界面。请注意,该用户界面已很接近应用程序最终的样子。
在这里插入图片描述
我们按照 next() 和 previous() 函数的基本规范,将 nextButton 放置在右
侧,而 previousButton 放置在左侧。为了使布局更加直观,我们使用
QHBoxLayout 将 widget 并排放置:

QHBoxLayout *buttonLayout2 = new QHBoxLayout;
buttonLayout2->addWidget(previousButton);
buttonLayout2->addWidget(nextButton);

然后,将 QHBoxLayout 对象 buttonLayout2 增加至 mainLayout。

mainLayout->addLayout(buttonLayout2, 3, 1);

下图显示了 widget 在 mainLayout 中的坐标位置。
在这里插入图片描述
在 addContact() 函数中,我们必须禁用这几个按钮,这样用户就不会在增加联
系人时尝试进行浏览。

nextButton->setEnabled(false);
previousButton->setEnabled(false);

此外,在 submitContact() 函数中,我们启用了浏览按钮 nextButton 和
previousButton,这取决于 contacts 的多少。如上文所述,浏览功能仅在地址
簿中有多个联系人时才会启用。以下代码行说明了如何实现此功能:

int number = contacts.size();
nextButton->setEnabled(number > 1);
previousButton->setEnabled(number > 1);

我们还在 cancel() 函数中加入这几行代码。
记得我们曾使用 QMap 对象 contacts 模拟了一个循环链接的列表。因此,在
next() 函数中,我们获取 contacts 的迭代器,然后执行以下操作:
 如果迭代器未达到 contacts 结尾,就会增加一。
 如果迭代器已达到 contacts 的结尾,就移至 contacts 的起始位置。这给人感
觉 QMap 就像是一个循环链接的列表。

void AddressBook::next()
{
QString name = nameLine->text();
QMap<QString, QString>::iterator i = contacts.find(name);
if (i != contacts.end())
i++;
if (i == contacts.end())
i = contacts.begin();
nameLine->setText(i.key());
addressText->setText(i.value());
}

一旦在 contacts 中循环至正确的对象,就会通过 nameLine 和 addressText
显示对象的内容。
同样,在 previous() 函数中,我们获取 contacts 的迭代器,然后执行以下操
作:
 如果迭代器达到 contacts 的结尾,就清除显示内容,然后返回。
 如果迭代器在 contacts 的起始位置,就将其移至结尾。
 然后,将迭代器减一。

void AddressBook::previous()
{
QString name = nameLine->text();
QMap<QString, QString>::iterator i = contacts.find(name);
if (i == contacts.end()){
nameLine->clear();
addressText->clear();
return;
}
if (i == contacts.begin())
i = contacts.end();
i--;
nameLine->setText(i.key());
addressText->setText(i.value());
}

接着,重新显示 contacts 中当前对象的内容。

地址簿 4 — 编辑和删除地址

文件:
 tutorials/addressbook/part4/addressbook.cpp
 tutorials/addressbook/part4/addressbook.h
 tutorials/addressbook/part4/main.cpp
 tutorials/addressbook/part4/part4.pro
在本章中,我们将了解如何修改储存在地址簿应用程序中的联系人的内容。
在这里插入图片描述
现有的地址簿不仅可以井井有条地储存联系人,还可进行浏览。再添加上编辑和
删除功能,以便在需要时更改联系人的详细信息,这样更易于使用。不过,还需
使用 enum 类型进行一些改进。在前几章中,我们使用以下两种模式:AddingMode
和 NavigationMode。但是,他们并未定义为 enum。我们是采用手动方式启用和
禁用相应的按钮,这就导致有多行重复的代码。
在本章中,我们定义带有以下三种不同值的 Mode enum 类型:
 NavigationMode、
 AddingMode 和
 EditingMode。

定义 AddressBook 类

addressbook.h 文件已更新为包含 Mode enum 类型:
enum Mode { NavigationMode, AddingMode, EditingMode };
我们还要向当前的公有槽列表增加两个新槽:editContact() 和

removeContact()。
void editContact();
void removeContact();

为了在模式间切换,我们引入了 updateInterface() 函数来控制所有
QPushButton 对象的启用和禁用。要实现上文提及的编辑和删除功能,我们还要
增加两个新按钮:editButton 和 removeButton。

void updateInterface(Mode mode);
...
QPushButton *editButton;
QPushButton *removeButton;
...
Mode currentMode;

最后,我们声明 currentMode 来跟踪 enum 的当前模式。

应用 AddressBook 类

我们现在必须应用地址簿应用程序的模式更改功能。editButton 和
removeButton 已实例化并默认为禁用,这是因为地址簿启动时在内存中没有联
系人。

editButton = new QPushButton(tr("&Edit"));
editButton->setEnabled(false);
removeButton = new QPushButton(tr("&Remove"));
removeButton->setEnabled(false);

这些按钮会与其相应的槽 editContact() 和 removeContact() 关联,然后我们
将其添加至 buttonLayout1。

connect(editButton, SIGNAL(clicked()), this, SLOT(editContact()));
connect(removeButton, SIGNAL(clicked()), this, SLOT(removeContact()));
...
buttonLayout1->addWidget(editButton);
buttonLayout1->addWidget(removeButton);

在将模式切换到 EditingMode 之前,editContact() 函数使用 oldName 和
oldAddress 储存联系人旧的详细信息。 在该模式下,submitButton 和
cancelButton 均已启用,这样用户就可以更改联系人的详细信息并可点击任何一个按钮。

void AddressBook::editContact()
{
oldName = nameLine->text();
oldAddress = addressText->toPlainText();
updateInterface(EditingMode);
}

submitContact() 函数已被 if-else 语句分为两部分。我们查看 currentMode
是否在 AddingMode 模式下。如果是,我们继续添加操作。

void AddressBook::submitContact()
{
...
if (currentMode == AddingMode) {
if (!contacts.contains(name)) {
contacts.insert(name, address);
QMessageBox::information(this, tr("Add Successful"),
tr("\"%1\" has been added to your address book.").arg(name));
} else {
QMessageBox::information(this, tr("Add Unsuccessful"),
tr("Sorry, \"%1\" is already in your address book.").arg(name));
return;
}

否则,我们查看 currentMode 是否在 EditingMode 模式下。如果是,我们比较
oldName 和 name。如果姓名已更改,我们从 contacts 中删除旧的联系人并插
入已更新的联系人。

} else if (currentMode == EditingMode) {
if (oldName != name) {
if (!contacts.contains(name)) {
QMessageBox::information(this, tr("Edit Successful"),
tr("\"%1\" has been edited in your address book.").arg(oldName));
contacts.remove(oldName);
contacts.insert(name, address);
} else {
QMessageBox::information(this, tr("Edit Unsuccessful"),
tr("Sorry, \"%1\" is already in your address book.").arg(name));
return;
}
} else if (oldAddress != address) {
QMessageBox::information(this, tr("Edit Successful"),
tr("\"%1\" has been edited in your address book.").arg(name));
contacts[name] = address;
}
}
updateInterface(NavigationMode);
}

如果仅更改了地址(例如 oldAddress 与 address 不同),我们就更新联系人
的地址。最后,我们将 currentMode 设置为 NavigationMode。这一步至关重要,
因为它会重新启用所有已禁用的按钮。
要从地址簿中删除联系人,我们采用 removeContact() 函数。该函数查看
contacts 中是否包含该联系人。

void AddressBook::removeContact()
{
QString name = nameLine->text();
QString address = addressText->toPlainText();
if (contacts.contains(name)) {
int button = QMessageBox::question(this,
tr("Confirm Remove"),
tr("Are you sure you want to remove \"%1\"?").arg(name),
QMessageBox::Yes | QMessageBox::No);
if (button == QMessageBox::Yes) {
previous();
contacts.remove(name);
QMessageBox::information(this, tr("Remove Successful"),
tr("\"%1\" has been removed from your address book.").arg(name));
}
}
updateInterface(NavigationMode);
}

如果有,我们显示 QMessageBox,确认用户的删除操作。一旦用户确认操作,我们调用 previous() 确保用户界面显示其他联系人,然后我们使用 QMap 的remove() 函数删除已已确认的联系人。出于好意,我们会显示 QMessageBox 提示用户。在该函数中使用两种信息框显示如下:
在这里插入图片描述

更新用户界面

我们在上文提到 updateInterface() 函数,它可根据当前的模式启用和禁用按
钮。该函数会根据传递给它的 mode 参数更新当前的模式,在校验值之前将参数
分配给 currentMode。
这样,每个按钮就根据当前的模式进行启用或禁用。AddingMode 和 EditingMode
的代码显示如下:

void AddressBook::updateInterface(Mode mode)
{
currentMode = mode;
switch (currentMode) {
case AddingMode:
case EditingMode:
nameLine->setReadOnly(false);
nameLine->setFocus(Qt::OtherFocusReason);
addressText->setReadOnly(false);
addButton->setEnabled(false);
editButton->setEnabled(false);
removeButton->setEnabled(false);
nextButton->setEnabled(false);
previousButton->setEnabled(false);
submitButton->show();
cancelButton->show();
break;

不过对于 NavigationMode,我们在 QPushButton::setEnabled() 函数的参数中加入了条件。这样可确保 editButton 和 removeButton 在地址簿中至少有一个联系人的情况下启用,而 nextButton 和 previousButton 仅在地址簿中有多个联系人时才启用。

case NavigationMode:
if (contacts.isEmpty()) {
nameLine->clear();
addressText->clear();
}
nameLine->setReadOnly(true);
addressText->setReadOnly(true);
addButton->setEnabled(true);
int number = contacts.size();
editButton->setEnabled(number >= 1);
removeButton->setEnabled(number >= 1);
nextButton->setEnabled(number > 1);
previousButton->setEnabled(number >1 );
submitButton->hide();
cancelButton->hide();
break;
}
}

通过在同一函数中设置模式和更新用户界面,我们可以避免用户界面与应用程序
内部状态不同步的可能性。

地址簿 5 — 添加查找功能

文件:
 tutorials/addressbook/part5/addressbook.cpp
 tutorials/addressbook/part5/addressbook.h
 tutorials/addressbook/part5/finddialog.cpp
 tutorials/addressbook/part5/finddialog.h
 tutorials/addressbook/part5/main.cpp
 tutorials/addressbook/part5/part5.pro
在本章中,我们将了解如何在地址簿应用程序中定位联系人和地址。
在这里插入图片描述
随着我们不断为地址簿应用程序添加联系人,使用下一个和上一个按钮浏览联系人就会变得很繁琐。在这种情况下,使用查找函数查找联系人就会更加有效。上面的屏幕截图显示了查找按钮及其在按钮面板上的位置。
当用户点击查找按钮时,有必要显示一个对话框,用户可在其中输入联系人的姓名。Qt 提供了 QDialog(我们会在本章中将其用作子类),可使用 FindDialog
类。

定义 FindDialog 类

在这里插入图片描述
要使用 QDialog 的子类,我们首先要在 finddialog.h 文件中声明 QDialog 的
头信息。此外,我们还使用向前 (forward) 声明来声明 QLineEdit 和
QPushButton,这样我们就能在对话框类中使用这些 widget。
因为在 AddressBook 类中,FindDialog 类包含了 Q_OBJECT 宏,并且其构造器
已定义为接收父级 QWidget,即使对话框以单独的窗口方式打开。

#include <QDialog>
class QLineEdit;
class QPushButton;
class FindDialog : public QDialog
{
Q_OBJECT
public:
FindDialog(QWidget *parent = 0);
QString getFindText();
public slots:
void findClicked();
private:
QPushButton *findButton;
QLineEdit *lineEdit;
QString findText;
};

我们定义了公有函数 getFindText(),供实例化 FindDialog 的类使用,这样这
些类可以获取用户输入的文本。公有槽 findClicked() 定义为在用户点击查找
按钮时处理搜索字符串。
最后,我们定义私有变量 findButton、lineEdit 和 findText,分别对应查找
按钮、用户输入搜索字符串的行编辑框和储存搜索字符串供稍后使用的内部字符
串。

应用 FindDialog 类

在 FindDialog 的构造器中,我们设置私有变量 lineEdit、findButton 和
findText。使用 QHBoxLayout 放置 widget。

FindDialog::FindDialog(QWidget *parent)
: QDialog(parent)
{
QLabel *findLabel = new QLabel(tr("Enter the name of a contact:"));
lineEdit = new QLineEdit;
findButton = new QPushButton(tr("&Find"));
findText = "";
QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(findLabel);
layout->addWidget(lineEdit);
layout->addWidget(findButton);
setLayout(layout);
setWindowTitle(tr("Find a Contact"));
connect(findButton, SIGNAL(clicked()), this, SLOT(findClicked()));
connect(findButton, SIGNAL(clicked()), this, SLOT(accept()));
}

我们设定布局和窗口标题,并将信号与其各自的槽关联。请注意,findButton 的
clicked() 信号已与 findClicked() 和 accept() 关联。QDialog 提供的
accept() 槽会隐藏对话框并将结果代码设置为 Accepted。我们使用该函数有助
于 AddressBook 的 findContact() 函数知晓 FindDialog 对象关闭的时间。我
们在讨论 findContact() 函数时将对该函数做进一步说明。
在这里插入图片描述
在 findClicked() 中,我们验证 lineEdit 以确保用户没有在尚未输入联系人
姓名时就点击查找按钮。然后,我们将 findText 设置为从 lineEdit 提取的搜
索字符串。之后,我们清空 lineEdit 的内容并隐藏对话框。

void FindDialog::findClicked()
{
QString text = lineEdit->text();
if (text.isEmpty()) {
QMessageBox::information(this, tr("Empty Field"),
tr("Please enter a name."));
return;
} else {
findText = text;
lineEdit->clear();
hide();
}
}

findText 变量已有公有 getter 函数 getFindText() 与其相关联。既然我们仅
在构造器和 findClicked() 函数中直接设定了 findText, 我们就不在创建
getFindText() 的同时再创建 setter 函数。由于 getFindText() 是公有的,
实例化和使用 FindDialog 的类可始终读取用户已输入并确认的搜索字符串。

QString FindDialog::getFindText()
{
return findText;
}
定义 AddressBook 类

要确保我们可使用 AddressBook 类中的 FindDialog,我们要在 addressbook.h
文件中包含 finddialog.h。

#include "finddialog.h"

至此,所有地址簿功能都有了 QPushButton 和对应的槽。同样,Find 功能有

findButton 和 findContact()。
findButton 声明为私有变量,而 findContact() 函数声明为公有槽。
void findContact();
...
QPushButton *findButton;

最后,我们声明私有变量 dialog,用于引用 FindDialog 的实例。

FindDialog *dialog;

在实例化对话框后,我们可能会对其进行多次使用。使用私有变量可在类中不同
位置对其进行多次引用。

应用 AddressBook 类

在 AddressBook 类的构造器中,实例化私有对象 findButton 和 findDialog:

findButton = new QPushButton(tr("&Find"));
findButton->setEnabled(false);
...
dialog = new FindDialog;

接下来,将 findButton 的 clicked() 信号与 findContact() 关联。
connect(findButton, SIGNAL(clicked()), this, SLOT(findContact()));
现在唯一要完成的就是 findContact() 函数的编码:

void AddressBook::findContact()
{
dialog->show();
if (dialog->exec() == QDialog::Accepted) {
QString contactName = dialog->getFindText();
if (contacts.contains(contactName)) {
nameLine->setText(contactName);
addressText->setText(contacts.value(contactName));
} else {
QMessageBox::information(this, tr("Contact Not Found"),
tr("Sorry, \"%1\" is not in your address book.").arg(contactName));
return;
}
}
updateInterface(NavigationMode);
}

我们从显示 FindDialog 的实例 dialog 开始入手。这时用户开始输入联系人姓
名进行查找。用户点击对话框的 findButton 后,对话框会隐藏,并且结果代码
设置为 QDialog::Accepted.这样就确保了 if 语句始终为真。
然后,我们就开始使用 FindDialog 的 getFindText() 函数提取搜索字符串,
这个字符串也就是本例中的 contactName。如果地址簿中有联系人,就立即显示
该联系人。否则,显示如下所示的 QMessageBox 表明搜索失败。
在这里插入图片描述

地址簿 6 — 加载和保存

文件:
 tutorials/addressbook/part6/addressbook.cpp
 tutorials/addressbook/part6/addressbook.h
 tutorials/addressbook/part6/finddialog.cpp
 tutorials/addressbook/part6/finddialog.h
 tutorials/addressbook/part6/main.cpp
 tutorials/addressbook/part6/part6.pro
本章描述了用于编写地址簿应用程序的加载和保存程序所使用的 Qt 文件处理
功能。
在这里插入图片描述
虽然浏览和搜索联系人是非常实用的功能,但只有在可以保存现有联系人并可以
在以后加载的前提下地址簿才真正完全可用。Qt 提供大量用于输入和输出的类,
但我们只选择两个易于合并使用的类:QFile 和 QDataStream。
QFile 对象表示磁盘上可读取和写入的文件。QFile 是代表多种不同设备且应用
更广的 QIODevice 类的子类。
QDataStream 对象用于按顺序排列二进制数据,以便储存在 QIODevice 中并供
以后检索。读取或写入 QIODevice 就如同打开数据流,然后读取或写入一样简
单,只是参数为不同的设备。

定义 AddressBook 类

我们声明两个公有槽 saveToFile() 和 loadFromFile(),以及两个

QPushButton 对象 loadButton 和 saveButton。
void saveToFile();
void loadFromFile();
...
QPushButton *loadButton;
QPushButton *saveButton;
应用 AddressBook 类

在构造器中,我们实例化 loadButton 和 saveButton。理想情况下,将按钮标
签设置为“从文件加载联系人”和“将联系人保存至文件”会更方便用户使用。
不过,由于其他按钮的大小限制,我们将标签设置为加载…和保存…。幸运的
是,Qt 提供了使用 setToolTip() 来设置工具提示的简单方式,我们可通过如
下方式将其用于按钮:

loadButton->setToolTip(tr("Load contacts from a file"));
...
saveButton->setToolTip(tr("Save contacts to a file"));

虽然此处没有显示,但与其他应用的功能一样,我们在右侧的布局面板
button1Layout 上添加按钮,然后将按钮的 clicked() 信号与其相应的槽关联。
至于保存功能,我们首先使用 QFileDialog::getSaveFileName() 获取
fileName。 这是 QFileDialog 提供的一个便捷功能,可弹出样式文件对话框并
允许用户输入文件名或选择现有的 .abk 文件。.abk 文件是保存联系人时创建
的地址簿扩展名。

void AddressBook::saveToFile()
{
QString fileName = QFileDialog::getSaveFileName(this,
tr("Save Address Book"), "",
tr("Address Book (*.abk);;All Files (*)"));

弹出的文件对话框屏幕截图显示如下:
在这里插入图片描述
如果 fileName 不为空,我们就使用 fileName 创建 QFile 对象 file。 QFile
与 QDataStream 一同使用,这是因为QFile 是 QIODevice。
接下来,我们尝试以 WriteOnly 模式打开文件。如果未能打开,会显示
QMessageBox 提示用户。

if (fileName.isEmpty())
return;
else {
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
file.errorString());
return;
}

否则,会用实例表示 QDataStream 对象 out,以写入打开的文件。QDataStream
要求读写操作需使用相同版本的数据流。在将数据按顺序写入 file 之前,将使
用的版本设置为采用 Qt 4.5 的版本就可确保版本相同。

QDataStream out(&file);
out.setVersion(QDataStream::Qt_4_5);
out << contacts;
}
}

至于加载功能,我们也是使用 QFileDialog::getOpenFileName() 获取
fileName。该函数与 QFileDialog::getSaveFileName() 相对应,也是弹出样式
文件对话框并允许用户输入文件名或选择现有的 .abk 文件加载到地址簿中。

void AddressBook::loadFromFile()
{
QString fileName = QFileDialog::getOpenFileName(this,
tr("Open Address Book"), "",
tr("Address Book (*.abk);;All Files (*)"));

例如,在 Windows 上,该函数弹出本地文件对话框,如以下屏幕截图所示。
在这里插入图片描述
如果 fileName 不为空,还是使用 QFile 对象 file,然后尝试在 ReadOnly 模
式下打开文件。与 saveToFile() 的应用方式类似,如果尝试失败,会显示
QMessageBox 提示用户。

if (fileName.isEmpty())
return;
else {
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
file.errorString());
return;
}
QDataStream in(&file);
in.setVersion(QDataStream::Qt_4_5);
contacts.empty(); // empty existing contacts
in >> contacts;

否则,会用实例表示 QDataStream 对象 in,按上文所述设置其版本,然后将按
顺序排列的数据读入 contacts 数据结构。请注意,在将数据读入之前清空
contacts 可简化文件读取过程。更高级的方法是将联系人读取至临时 QMap 对
象,然后仅复制 contacts 中不存在的联系人。

if (contacts.isEmpty()) {
QMessageBox::information(this, tr("No contacts in file"),
tr("The file you are attempting to open contains no contacts."));
} else {
QMap<QString, QString>::iterator i = contacts.begin();
nameLine->setText(i.key());
addressText->setText(i.value());
}
}
updateInterface(NavigationMode);
}

要显示从文件中读取的联系人,必须要先验证获取的数据,以确保读取的文件实
际包含地址簿联系人。如果为真,显示第一个联系人,否则显示 QMessageBox 提
示出现问题。最后,我们更新界面以相应地启用和禁用按钮。

地址簿 7 — 附加功能

文件:
 tutorials/addressbook/part7/addressbook.cpp
 tutorials/addressbook/part7/addressbook.h
 tutorials/addressbook/part7/finddialog.cpp
 tutorials/addressbook/part7/finddialog.h
 tutorials/addressbook/part7/main.cpp
 tutorials/addressbook/part7/part7.pro
本章讲述了部分可使地址簿应用程序日常使用更加便捷的附加功能。
在这里插入图片描述
虽然地址簿应用程序其自身功能已经很实用,但是如果可和其他应用程序互换联
系人数据就会更加有益。vCard 格式是一种流行的文件格式,就可用于此目的。
在本章中,我们会扩展地址簿客户端,可将联系人导出到 vCard .vcf 文件中。

定义 AddressBook 类

我们在 addressbook.h 文件的 AddressBook 类中添加 QPushButton 对象
exportButton 以及对应的公有槽 exportAsVCard()。

void exportAsVCard();
...
QPushButton *exportButton;
应用 AddressBook 类

在 AddressBook 构造器中,我们将 exportButton 的 clicked() 信号连接至
exportAsVCard()。我们还会将该按钮添加至 buttonLayout1,它是负责右侧按
钮面板的布局类。
在 exportAsVCard() 函数中,我们从提取 name 中联系人姓名开始入手。我们
声明 firstName、lastName 和 nameList。接下来,我们查找 name 中第一处空
白的索引。如果有空白,就将联系人的姓名分隔为 firstName 和 lastName。然
后,将空白替换为下划线 ("_")。或者,如果没有空白,就认定联系人只有名字。

void AddressBook::exportAsVCard()
{
QString name = nameLine->text();
QString address = addressText->toPlainText();
QString firstName;
QString lastName;
QStringList nameList;
int index = name.indexOf(" ");
if (index != -1) {
nameList = name.split(QRegExp("\\s+"), QString::SkipEmptyParts);
firstName = nameList.first();
lastName = nameList.last();
} else {
firstName = name;
lastName = "";
}
QString fileName = QFileDialog::getSaveFileName(this,
tr("Export Contact"), "",
tr("vCard Files (*.vcf);;All Files (*)"));
if (fileName.isEmpty())
return;
QFile file(fileName);

至于 saveToFile() 函数,会打开文件对话框,让用户选择文件的位置。通过选
择的文件名称,我们创建要写入的 QFile 实例。
我们尝试以 WriteOnly 模式打开文件。如果操作失败,会显示 QMessageBox 提
示用户出现问题并返回。否则,将文件作为参数传递给 QTextStream 对象 out。
与 QDataStream 类似,QTextStream 类提供了读取纯文本和将其写入到文件的
功能。因此,所生成的 .vcf 文件可以在文本编辑器中打开进行编辑。

if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
file.errorString());
return;
}
QTextStream out(&file);

然后,我们写出依次带有 BEGIN:VCARD 和 VERSION:2.1 标记的 vCard 文件。
联系人的姓名使用 N: 标记写入。至于写入 vCard “File as”属性的 FN: 标
记,我们必须要查看是否联系人带有姓。如果联系人有姓,就使用 nameList 中
的详细信息填入该标记。否则,仅写入 firstName。

out << "BEGIN:VCARD" << "\n";
out << "VERSION:2.1" << "\n";
out << "N:" << lastName << ";" << firstName << "\n";
if (!nameList.isEmpty())
out << "FN:" << nameList.join(" ") << "\n";
else
out << "FN:" << firstName << "\n";

我们继续写入联系人的地址。地址中的分号使用 “” 进行转义,新行使用分号
进行替换,而逗号使用空白进行替换。最后,我们依次写入 ADR;HOME:;、address
和 END:VCARD 标记。

address.replace(";", "\\;", Qt::CaseInsensitive);
address.replace("\n", ";", Qt::CaseInsensitive);
address.replace(",", " ", Qt::CaseInsensitive);
out << "ADR;HOME:;" << address << "\n";
out << "END:VCARD" << "\n";
QMessageBox::information(this, tr("Export Successful"),
tr("\"%1\" has been exported as a vCard.").arg(name));
}

最后,会显示 QMessageBox 提示用户已成功导出 vCard。
vCard 是 Internet Mail Consortium 的商标。

Widgets 教程

Widget 是使用 Qt 编写的图形用户界面 (GUI) 应用程序的基本生成块。每个
GUI 组件,如按钮、标签或文本编辑器,都是一个 widget ,并可以放置在现
有的用户界面中或作为单独的窗口显示。每种类型的组件都是由 QWidget 的
特殊子类提供的,而 QWidget 自身又是 QObject 的子类。

简介

Widget 是使用 Qt 编写的图形用户界面 (GUI) 应用程序的基本生成块。每个
GUI 组件,如按钮、标签或文本编辑器,都是一个 widget ,并可以放置在现有的用户界面中或作为单独的窗口显示。每种类型的组件都是由 QWidget 的特殊子类提供的,而 QWidget 自身又是 QObject 的子类。
QWidget 不是一个抽象类;它可用作其他 widget 的容器,并很容易作为子类使用来创建定制 widget。它经常用来创建放置其他 widget 的窗口。
至于 QObject,可使用父对象创建 widget 以表明其所属关系,这可确保删除不再使用的对象。使用 widget,这些父子关系就有了更多的意义:每个子类都显示在其父级所拥有的屏幕区域内。也就是说,当删除窗口时,其包含的所有widget 也都自动删除。

创建窗口

如果 widget 未使用父级进行创建,则在显示时视为窗口或顶层 widget。由于
顶层 widget 没有父级对象类来确保在其不再使用时就删除,因此需要开发人员在应用程序中对其进行跟踪。
在下例中,我们使用 QWidget 创建和显示具有默认大小的窗口:

QWidget *window = new QWidget();
window->resize(320, 240);
window->show();

在这里插入图片描述
我们可以通过将 window 作为父级传递给其构造器来向窗口添加子 widget。在这种情况下,我们向窗口添加按钮并将其放置在特定位置:

QPushButton(tr("Press me"), window);
button->move(100, 100);
button->show();

在这里插入图片描述
该按钮现在为窗口的子项,并在删除窗口时一同删除。请注意,隐藏或关闭窗口不会自动删除该按钮。

使用布局

通常,子 widget 是通过使用布局对象在窗口中进行排列,而不是通过指定位置和大小进行排列。在此处,我们构造要并排排列的标签和行编辑框 widget。

QLabel *label = new QLabel(tr("Name:"));
QLineEdit *lineEdit = new QLineEdit();
QHBoxLayout *layout = new
QHBoxLayout();
layout->addWidget(label);
layout->addWidget(lineEdit);
window->setLayout(layout);

在这里插入图片描述
我们构造的布局对象管理通过 addWidget() 函数提供的 widget 的位置和大
小。布局本身是通过调用 setLayout() 提供给窗口的。布局仅可通过其对所管
理的 widget(和其他布局)的效果才可显示。
在上文示例中,每个 widget 的所属关系并不明显。由于我们未使用父级对象构造 widget 和布局,我们会看到一个空窗口和两个包含了标签与行编辑框的窗口。不过,如果我们告知布局来管理标签和行编辑框,并在窗口中设置布局,两个 widget 与布局本身就都会成为窗口的子项。
由于 widget 可包含其他 widget,布局可用来提供按不同层次分组的 widget。
这里,我们要在显示查询结果的表视图上方、窗口顶部的行编辑框旁,显示一个标签。

QLabel *queryLabel = new QLabel(tr("Query:"));
QLineEdit *queryEdit = new QLineEdit();
QTableView *resultView = new QTableView();
QHBoxLayout *queryLayout = new
QHBoxLayout();
queryLayout->addWidget(queryLabel);
queryLayout->addWidget(queryEdit);
QVBoxLayout *mainLayout = new
QVBoxLayout();
mainLayout->addLayout(queryLayout);
mainLayout->addWidget(resultView);
window->setLayout(mainLayout);

在这里插入图片描述
除了 QHBoxLayout 和 QVBoxLayout,Qt 还提供了 QGridLayout 和
QFormLayout 类来协助实现更复杂的用户界面。

待续。。。学习笔记之Qt从入门到精通(二)
参考链接:https://blog.csdn.net/weixin_41486034/article/details/106379306

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值