QT5-入门指南-全-
QT5 入门指南(全)
原文:
zh.annas-archive.org/md5/f2977f74ca22e03049f2ca6fe3ef50c5译者:飞龙
前言
在计算领域有许多流行词汇,其中大多数都与各种软件技术和概念有关。浏览器已成为获取信息和消费各种数据的首选方式。但仍然有一个空白,只能由必须安装和运行在操作系统上的独立应用程序来填补。浏览器本身作为一个应用程序,不能通过浏览器访问,这也证明了这一论断。
VLC、Adobe Photoshop、Google Earth 和 QGIS 等应用程序是直接在操作系统上运行的应用程序的一些例子。有趣的是,这些知名的软件品牌是用 Qt 构建的。
Qt(发音为“cute”)是一个跨平台的应用程序框架和控件工具包,用于创建在多种不同的硬件和操作系统上运行的图形用户界面应用程序。上述应用程序就是使用这个相同的工具包编写的。
本书的主要目的是向读者介绍 Qt。通过使用简单易懂的例子,它将引导用户从一个概念过渡到下一个,而不太关注理论。本书的篇幅要求我们在材料展示上要简洁。结合所提供的丰富例子,我们希望缩短理解和使用 Qt 的学习路径。
这本书面向谁
任何想要开始开发图形用户界面应用程序的人都会发现这本书很有用。为了理解这本书,不需要对其他工具包有先前的接触。然而,拥有这些技能将会很有用。
然而,本书假设您对 C++的使用有实际的知识。如果您能在开发算法和使用面向对象编程中表达自己的思想,您会发现内容很容易消化。
拥有 Qt 知识的专家或中级人员应寻求更多详细的外部材料。这本书不是参考指南,而应仅用作入门材料。
为了最大限度地利用这本书
每章的开始将包含一些理论,这应该有助于巩固您的理解。之后,一系列的例子被用来解释概念,并帮助读者更好地掌握主题。
本书也避免了继续使用前一章的例子。每个章节的例子都很短,不需要读者了解前一章的内容。这样,您可以挑选任何感兴趣的章节并开始学习。
已经提供了适当的链接来在 Windows 上设置环境。Linux 和 macOS 平台在本书中也得到了直接的支持。
下载示例代码文件
您可以从 www.packt.com 的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packt.com/support 并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在 www.packt.com 登录或注册。
-
选择支持选项卡。
-
点击代码下载与勘误。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
文件下载完成后,请确保使用最新版本的以下软件解压缩或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Getting-Started-with-Qt-5。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包可供下载,请访问 github.com/PacktPublishing/。查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载: www.packtpub.com/sites/default/files/downloads/9781789956030_ColorImages.pdf。
约定使用
本书使用了多种文本约定。
CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“要将密码设置为connection参数,发出代码片段,db_conn.setPassword("")。”
代码块应如下设置:
QSqlDatabase db_conn =
QSqlDatabase::addDatabase("QMYSQL", "contact_db");
db_conn.setHostName("127.0.0.1");
db_conn.setDatabaseName("contact_db");
db_conn.setUserName("root");
db_conn.setPassword("");
db_conn.setPort(3306);
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
任何命令行输入或输出都应如下所示:
% mkdir helloWorld
% ./run_executable
粗体: 表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中如下所示。以下是一个示例:“它在一个标签中显示文本 Hello world !。”
警告或重要说明如下所示。
小技巧如下所示。
联系我们
我们读者的反馈总是受欢迎的。
一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书籍标题,并通过 customercare@packtpub.com 邮箱联系我们。
勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并附上材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦你阅读并使用了这本书,为何不在你购买它的网站上留下评论呢?潜在读者可以查看并使用你的客观意见来做出购买决定,Packt 公司可以了解你对我们的产品有何看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问 packt.com。
第一章:介绍 Qt 5
Qt 为开发者提供了一个出色的工具箱,可以轻松地创建令人惊叹且实用的应用程序,而无需承受太多压力,您很快就会发现这一点。在本章中,我们将介绍 Qt 并描述如何在机器上设置它。到本章结束时,您应该能够做到以下内容:
-
安装 Qt
-
使用 Qt 编写一个简单的程序
-
编译并运行一个 Qt 程序
目标保持简单直接。那么,让我们开始吧!
在 Linux 上安装 Qt
Ubuntu 操作系统使安装 Qt 5 变得相对容易。输入以下命令来设置您的环境:
sudo apt-get install qt5-default
安装后,Qt 程序将从命令行编译和运行。在 第六章 “连接 Qt 与数据库”中,我们将展示如何使用 Qt 连接到数据库。输入以下命令以确保安装了 Qt 运行所需的库。我们将连接到的是 MySQL 数据库:
sudo apt-get install libqt5sql5-mysql
在 macOS 上安装 Qt
在 Mac 上安装 Qt 有多种方法。要开始安装 Qt 5 到您的 Mac,您需要在您的机器上安装 Xcode。在终端中输入以下命令:
xcode-select --install
如果您得到以下输出,那么您就可以进行下一步了:
xcode-select: error: command line tools are already installed, use "Software Update" to install updates
HomeBrew 是一种软件包管理工具,它允许您轻松安装随 macOS 不附带安装的 Unix 工具。
如果您的机器上还没有安装,您可以在终端中输入以下命令进行安装:
/user/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
之后,您应该在终端中输入另一组命令来安装 Qt:
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/fdfc724dd532345f5c6cdf47dc43e99654e6a5fd/Formula/qt5.rb
brew install ./qt5.rb
在接下来的几章中,我们将使用 MySql 数据库。要配置 Qt 5 与 MySql,请输入以下命令:
brew install ./qt5 --with-mysql
此命令可能需要一段时间才能完成,如果一切顺利,您就可以编写 Qt 程序了。
Windows 上的安装
对于使用 Windows 的读者,安装过程仍然简单,尽管稍微不那么直接。我们可以先访问 download.qt.io。
选择 official_releases/,然后 online_installers/,并选择下载 qt-unified-windows-x86-online.exe。
运行程序并选择创建账户。点击通过选择安装文件夹,并且不要忘记在选择需要安装的组件时选择 MinGW 5.3.0 32 位选项作为编译器。
本书中的大多数命令都应该在这个 IDE 中运行。
什么是 Qt?
现在我们已经设置了开发环境,让我们来构建一个“Hello World”示例。然而,首先让我们先简要地了解一下。
Qt 是一个用于创建 图形用户界面(GUI)以及跨平台应用程序的工具包。GUI 应用程序是使用鼠标向计算机发出命令以执行程序的应用程序。虽然 Qt 在某些情况下可以不使用鼠标操作,但这正是其用途所在。
在编写 GUI 应用程序时,试图在多个操作系统上实现相同的外观、感觉和功能是一项很大的挑战。Qt 通过提供一种只需编写一次代码并确保它在大多数操作系统上运行而无需进行太多或任何更改的方法,完全消除了这一障碍。
Qt 使用了一些模块。这些模块将相关的功能组合在一起。以下列出了一些模块及其功能:
-
QtCore:正如其名所示,这些模块包含 Qt 框架的核心和重要类。这包括容器、事件和线程管理等功能。 -
QtWidgets和QtGui:此模块包含用于调用控件的类。控件是构成图形界面大部分组件的元素。这包括按钮、文本框和标签。 -
QtWebkit:此模块使得在 Qt 应用程序中使用网页和应用成为可能。 -
QtNetwork:此模块提供连接到并通信网络资源的类。 -
QtXML:为了解析 XML 文档,此模块包含有用的类。 -
QtSQL:此模块具有丰富的类和驱动程序,允许连接到数据库,包括 MySQL、PostgreSQL 和 SQLite。
Qt 中的“Hello World”
在本节中,我们将组合一个非常简单的“Hello World”程序。程序将在窗口中显示一个简单的按钮。在新建的名为hello_world的文件夹中创建一个名为hello.cpp的文件。打开文件并插入以下代码:
#include <QApplication>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QLabel label("Hello world !");
label.show();
return app.exec();
}
这看起来像是一个普通的 C++程序,除了使用了不熟悉的类。
就像任何常规程序一样,int main()函数是应用程序的入口点。
创建了一个QApplication类的实例,名为app,并将传递给main()函数的参数。app对象是必需的,因为它触发了事件循环,该循环会一直运行,直到我们关闭应用程序。没有QApplication对象,实际上无法创建 Qt GUI 应用程序。
然而,可以在不创建QApplication实例的情况下使用 Qt 的某些功能。
此外,QApplication的构造函数要求我们向其传递argc和argv。
我们实例化了一个QLabel类的对象,名为label。我们将"Hello World!"字符串传递给其构造函数。QLabel代表我们所说的控件,这是一个用来描述屏幕上视觉元素的术语。标签用于显示文本。
默认情况下,创建的控件是隐藏的。要显示它们,必须调用show()函数。
要启动事件循环,需要执行app.exec()这一行代码。这会将应用程序的控制权交给 Qt。
return关键字将一个整数返回给操作系统,表示应用程序关闭或退出时的状态。
要编译和运行我们的程序,导航到存储hello.cpp的文件夹。在终端中输入以下命令:
% qmake -project
这将创建hello_world.pro文件。hello_world这个名字是hello.cpp所在的文件夹的名字。生成的文件将根据你存储hello.cpp文件的路径而变化。
使用你选择的任何文本编辑器打开hello_world.pro文件。以下几行需要一些解释:
TEMPLATE = app
这里,app的值意味着项目的最终输出将是一个应用程序。或者,它可能是一个库或子目录:
TARGET = hello_world
这里,hello_world的名字是应用程序或(库)的名称,它将被执行:
SOURCES += hello.cpp
由于hello.cpp是我们项目中的唯一源文件,它被添加到SOURCES变量中。
我们需要生成一个Makefile,它将详细说明编译我们的 hello world 程序所需的步骤。这个自动生成的Makefile的好处是它消除了我们了解在不同操作系统上编译程序的各种细微差别所需的必要性。
在同一项目目录下,执行以下命令:
% qmake
这将在目录中生成一个Makefile。
现在,执行以下命令来编译程序:
% make
当运行make命令时,会产生以下错误(以及更多信息)作为输出:
#include <QApplication>
^~~~~~~~~~~~
之前我们提到,各种组件和类被打包到模块中。QApplication正在我们的应用程序中使用,但正确的模块尚未包含。在编译过程中,这种遗漏会导致错误。
为了解决这个问题,打开hello_world.pro文件,并在该行之后插入以下几行:
INCLUDEPATH += .
QT += widgets
这将添加QtWidget模块,以及QtCore模块,到编译程序中。添加了正确的模块后,再次在命令行上运行make命令:
% make
在同一文件夹中会生成一个hello_world文件。按照以下方式从命令行运行此文件:
% ./hello_world
在 macOS 上,可执行文件的完整路径将从以下命令行路径指定:
./hello_world.app/Contents/MacOS/hello_world
这应该会产生以下输出:

好的,这就是我们的第一个 GUI 程序。它在一个标签中显示 Hello world !。要关闭应用程序,请点击窗口的关闭按钮。
让我们添加一些Qt 样式表(QSS)来给我们的标签添加一点效果!
按照以下方式修改hello.cpp文件:
#include <QApplication>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QLabel label("Hello world !");
label.setStyleSheet("QLabel:hover { color: rgb(60, 179, 113)}");
label.show();
return app.exec();
}
这里的唯一变化是label.setStyleSheet("QLabel:hover { color: rgb(60, 179, 113)}");。
一个 QSS 规则作为参数传递给label对象的setStyleSheet方法。该规则设置我们应用程序中的每个标签,当光标悬停在其上时显示绿色。
运行以下命令重新编译应用程序并运行它:
% make
% ./hello_world
程序应该看起来像以下截图。当鼠标放在标签上时,标签变为绿色:

摘要
本章为了解 Qt 及其用途奠定了基础。概述了在 macOS 和 Linux 上安装 Qt 的步骤。编写并编译了一个简单的“Hello World”应用程序,所有操作均通过命令行完成,无需任何集成开发环境(IDE)。这意味着我们还了解了导致最终程序的各种步骤。
最后,将“Hello World”应用程序修改为使用 QSS,以展示可以对小部件进行哪些其他操作。
在第二章“创建小部件和布局”中,我们将探索 Qt 中的更多小部件以及如何组织和分组它们。
第二章:创建窗口小部件和布局
在本章中,我们将探讨窗口小部件是什么以及可用于创建 GUI 的各种类型。对于你将要编写的绝大多数 GUI 应用程序,Qt 都提供了足够的窗口小部件来实现它。与窗口小部件一起使用的布局类帮助我们安排和定位窗口小部件,以使其更具吸引力。
到本章结束时,你应该了解以下内容:
-
理解并知道如何使用窗口小部件
-
了解需要用于布局的类
窗口小部件
窗口小部件是我们构建用户界面的图形组件。一个熟悉的例子是文本框。这是在 GUI 应用程序表单中用来捕获我们的电子邮件地址或姓氏和名字的组件。
在 Qt 中关于窗口小部件有几个关键点需要注意:
-
信息通过事件传递给窗口小部件。对于文本框,一个事件示例可能是用户在文本框内点击或当文本框光标闪烁时按下
return键。 -
每个窗口小部件都可以有一个父窗口小部件或子窗口小部件。
-
没有父窗口小部件的窗口小部件在调用
show()函数时将变成一个窗口。这样的窗口小部件将被包含在一个带有关闭、最大化、最小化按钮的窗口中。 -
子窗口小部件在其父窗口小部件内显示。
Qt 通过大量使用继承来组织其类,因此掌握这一点非常重要。考虑以下图表:

在层次结构的顶部是 QObject。许多类都继承自 QObject 类。QObject 类还包含信号和槽以及事件管理的机制,等等。
此外,具有共同行为的窗口小部件被分组在一起。QCheckBox、QPushButton 和 QRadioButton 都是同一类型的按钮,因此它们继承自 QAbstractButton,该类包含所有按钮共享的属性和函数。这一原则也适用于 QAbstractScrollArea 及其子类,QGraphicsView 和 QTextEdit。
为了将我们刚刚学到的一些知识付诸实践,让我们创建一个只有一个窗口小部件的简单 Qt 程序。
这个 Qt 应用程序只显示一个按钮。打开一个文本文件,并给它起一个你想要的名称,后缀为 .cpp。
大多数示例将需要你创建一个新的文件夹,源代码将存储在那里。这将允许你轻松地将程序作为项目编译。
插入以下代码行。创建一个新的文件夹,并将 .cpp 文件移动到其中:
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton myButton(QIcon("filesaveas.png"),"Push Me");
myButton.setToolTip("Click this to turn back the hands of time");
myButton.show();
return app.exec();
}
本应用程序的目的是展示一个没有父对象窗口小部件在执行时如何成为主窗口。将要创建的按钮将包括一个图标和工具提示。
首先,这个应用程序看起来与我们写在第一章结尾的部分相似,即介绍 Qt 5。在这个应用程序中,声明了一个名为myButton的按钮。将QIcon的一个实例作为QPushButton默认构造函数的第一个参数。这读取名为filesaveas.png的文件(目前应该与 GitHub 上的源代码文件在同一文件夹中)。将文本"Push Me"作为第二个参数传递。这段文本将显示在按钮上。
下一个行,myButton.setToolTip("Click this to turn back the hands of time");,用于设置按钮的工具提示。工具提示是一段文本或消息,当你在小部件上悬停鼠标光标时显示。它通常包含比小部件可能显示的额外或解释性信息。
最后,我们在myButton对象上调用show()函数来显示它并在屏幕上绘制。在这个应用程序中,我们只有一个小部件,QPushButton。这个小部件的父级是什么?好吧,如果没有指定,父级默认为NULL,这告诉 Qt 该小部件没有父级。由于这个原因,显示此类小部件时,它将被包含在一个窗口中。
保存文件并运行以下命令以编译你的应用程序。将目录更改到包含.cpp文件的新文件夹:
应在终端或命令行中运行的命令以%符号开头,它代表终端上的提示符。根据你的终端设置,这可能会略有不同,但命令是%符号后面的所有字符。
% qmake -project
从.pro文件的名字来看,它告诉我们.cpp文件所在的文件夹名为qbutton。因此,当你发出前面的命令时,这个名称应该更改为.cpp文件所在的文件夹名。
现在,请记住在qbutton.pro文件中在INCLUDEPATH += .下面添加以下行:
QT += widgets
继续以下命令:
% qmake
% make
根据问题,从命令行运行应用程序:
% ./qbutton
你应该获得以下截图:

前一个截图显示了程序首次运行时你会看到的内容:

当我们将光标放在按钮上时,会显示在代码中指定的工具提示,如前一个截图所示。
按钮还显示了那些你想在按钮上添加图像以增强 UI 直观性的情况。
以下是一些值得注意的观察结果:
-
在
QPushButton类中找不到setToolTip()函数。相反,它是属于QWidget类的一组函数之一。 -
这突出了通过继承方式类获得的有用性。
-
存储工具提示值的
QWiget类的属性或成员是toolTip。
为了结束关于控件的这一部分,让我们自定义一个QLabel并显示它。这次,QLabel的字体将被更改,并且将显示比通常更长的文本。
在新创建的文件夹中创建一个名为qlabel_long_text.cpp的文件,并插入以下代码:
#include <QApplication>
#include <QString>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QString message = "'What do you know about this business?' the King said to Alice.\n'Nothing,' said Alice.\n'Nothing whatever?' persisted the King.\n'Nothing whatever,' said Alice.";
QLabel label(message);
label.setFont(QFont("Comic Sans MS", 18));
label.setAlignment(Qt::AlignCenter);
label.show();
return app.exec();
}
我们 Qt 程序的结构并没有发生太大的变化。前三行包含include指令,添加了我们将要使用的类的头文件。
如往常一样,将main()函数的参数传递给app()。message变量是一个QString对象,它包含一个长字符串。QString是在处理字符串时使用的类。它具有许多在 C++字符串中不可用的功能。
创建了一个QLabel实例,命名为label,并将message传递给它。为了改变标签字符串的显示样式,我们向setFont函数传递了一个QFont实例。我们选择了字体样式Comic Sans MS,字号为18,传递给QFont的构造函数。
为了使所有文本居中,我们在label对象上调用setAlignment函数,并传递Qt::AlignCenter常量。
最后,我们通过在label对象上调用show函数来显示控件。
如往常一样,我们将在命令行上输入以下代码来编译和运行此程序:
% qmake -project
% qmake
% ./qlabel_long_text
记得在.pro文件中添加QT += widgets。
程序的输出如下。所有文本行都居中显示:

再次强调,label应用程序中唯一的控件成为主窗口,因为它没有与之关联的父对象。其次,控件成为窗口,因为调用了label上的show()方法。
布局
到目前为止,我们创建的应用程序只有一个控件作为主要组件,并且由此扩展,也是一个窗口。然而,GUI 应用程序通常由多个控件组成,这些控件共同向用户传达一个过程。我们可以利用多个控件的一种方法是通过使用布局作为画布,我们将控件插入其中。
考虑以下类继承关系图:

在布局控件时考虑使用的类是很重要的。通常,从QLayout抽象类继承的顶级类是QObject。此外,QLayout通过从QLayoutItem继承实现了多重继承。这里的具体类有QBoxLayout、QFormLayout、QGridLayout和QStackedLayout。QHBoxLayout和QVBoxLayout通过添加布局内控件可能排列的方向来进一步细化QBoxLayout类。
以下表格简要描述了主要布局的功能:
| 布局类 | 描述 |
|---|---|
QFormLayout |
QFormLayout 类 (doc.qt.io/qt-5/qformlayout.html) 管理输入小部件及其关联的标签。 |
QGridLayout |
QGridLayout 类 (doc.qt.io/qt-5/qgridlayout.html) 以网格形式排列小部件。 |
QStackedLayout |
QStackedLayout 类 (doc.qt.io/qt-5/qstackedlayout.html) 提供了一个小部件堆栈,一次只显示一个小部件。 |
QVBoxLayout |
QVBoxLayout 类 (doc.qt.io/qt-5/qvboxlayout.html) 将小部件垂直排列。 |
QHBoxLayout |
QHBoxLayout 类 (doc.qt.io/qt-5/qhboxlayout.html) 将小部件水平排列。 |
我们需要布局小部件有两个主要原因:
-
为了允许我们显示多个小部件。
-
为了将我们界面中的许多小部件以良好且直观的方式呈现,以便使 UI 有用。并非所有 GUI 都允许用户很好地完成工作。糟糕的布局可能会让系统用户感到困惑,并使他们难以正确使用。
让我们创建一个简单的程序来展示如何使用一些布局类。
QGridLayout
QGridLayout 通过指定将被多个小部件填满的行数和列数来排列小部件。类似于表格的网格结构,它有行和列,小部件被插入到行和列相交的单元格中。
创建一个新的文件夹,并使用任何编辑器创建一个名为 main.cpp 的文件:
#include <QApplication>
#include <QPushButton>
#include <QGridLayout>
#include <QLineEdit>
#include <QDateTimeEdit>
#include <QSpinBox>
#include <QComboBox>
#include <QLabel>
#include <QStringList>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget *window = new QWidget;
QLabel *nameLabel = new QLabel("Open Happiness");
QLineEdit *firstNameLineEdit= new QLineEdit;
QLineEdit *lastNameLineEdit= new QLineEdit;
QSpinBox *ageSpinBox = new QSpinBox;
ageSpinBox->setRange(1, 100);
QComboBox *employmentStatusComboBox= new QComboBox;
QStringList employmentStatus = {"Unemployed", "Employed", "NA"};
employmentStatusComboBox->addItems(employmentStatus);
QGridLayout *layout = new QGridLayout;
layout->addWidget(nameLabel, 0, 0);
layout->addWidget(firstNameLineEdit, 0, 1);
layout->addWidget(lastNameLineEdit, 0, 2);
layout->addWidget(ageSpinBox, 1, 0);
layout->addWidget(employmentStatusComboBox, 1, 1,1,2);
window->setLayout(layout);
window->show();
return app.exec();
}
程序的目的是展示如何使用布局对象。为了填充布局,还将讨论其他小部件。
在前面的代码中,*window 是 QWidget 的一个实例。目前,请保留此对象以查看我们将如何将其转换为窗口。
我们将要插入到布局中的小部件将在之后创建,即 name、firstnameLineEdit 和 lastNameLineEdit。
一些开发者喜欢通过在其名称后附加他们实例化的类的名称来命名变量。这里也使用了驼峰命名法。
QLineEdit 是创建文本框的基本类。QSpinbox 是一个允许在给定范围内选择值的控件。在这种情况下,ageSpinBox->setRange(1, 100) 设置了可能的值范围在 1 到 100 之间。
接下来,我们实例化 QComboBox 类以创建一个具有由存储在 QStringList 中的字符串列表指定的下拉值的控件。然后,通过调用其 addItems() 方法将字符串列表 employmentStatus 传递给 employmentStatusComboBox。这些将成为当控件被点击时显示的选项。
由于我们想要以网格布局排列小部件,因此我们创建了一个QGridLayout对象,*layout。为了将小部件添加到布局中,我们调用addWIdget()方法,并且每次都指定小部件,以及两个(2)数字,这些数字指定了小部件要插入的行和列:
layout->addWidget(nameLabel, 0, 0);
layout->addWidget(firstNameLineEdit, 0, 1);
layout->addWidget(lastNameLineEdit, 0, 2);
layout->addWidget(ageSpinBox, 1, 0);
layout->addWidget(employmentStatusComboBox, 1, 1,1,2);
首先被插入到布局对象中的小部件是标签,nameLabel。它占据了网格的第一行和第一列。第一行由第二个参数0表示,而第一列由0表示。这对应于选择网格的第一个单元格以保留nameLabel。
被添加到布局中的第二个小部件是firstNameLineEdit。这个小部件将被插入到第一行,标记为0,以及第二列,标记为1。紧邻此小部件的是添加的lastNameLineEdit小部件,它也位于同一行,0。
ageSpinBox小部件将被固定在第二行,标记为1,以及第一列,标记为0。
employmentStatusComboBox小部件被添加到layout对象中,并通过指定最后一个(1, 2)参数进一步扩展:
window->setLayout(layout);
window->show();
window对象没有布局。为了设置小部件的布局,调用setLayout并传入包含其他小部件的布局对象。
由于window(基本上是一个小部件)没有父对象,因此当我们对其调用show()方法时,它将成为一个窗口。此外,通过addWidget()方法添加到布局对象的所有小部件都是layout对象的子对象。
通过在命令行中发出创建项目和编译的命令来运行项目。
在成功编译后,你应该会看到以下内容:

注意下拉小部件如何扩展以填充第三列。小部件的放置符合我们调用addWidget()时设置的布局。
在下一节中,我们将查看一个有用的布局类,称为QFormLayout。
QFormLayout
对于那些只需要在两列布局中将多个小部件放在一起的情况,QFormLayout非常有用。你可以选择使用QGridLayout构建表单,但对于表单的展示,QFormLayout最为合适。
以以下代码为例。它展示了一个表单,其中第一列包含标签,第二列包含用于接收用户输入的实际控件:
#include <QApplication>
#include <QFormLayout>
#include <QPushButton>
#include <QLineEdit>
#include <QSpinBox>
#include <QComboBox>
#include <QStringList>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget *window = new QWidget;
QLineEdit *firstNameLineEdit= new QLineEdit;
QLineEdit *lastNameLineEdit= new QLineEdit;
QSpinBox *ageSpingBox = new QSpinBox;
QComboBox *employmentStatusComboBox= new QComboBox;
QStringList employmentStatus = {"Unemployed", "Employed", "NA"};
ageSpingBox->setRange(1, 100);
employmentStatusComboBox->addItems(employmentStatus);
QFormLayout *personalInfoformLayout = new QFormLayout;
personalInfoformLayout->addRow("First Name:", firstNameLineEdit);
personalInfoformLayout->addRow("Last Name:", lastNameLineEdit );
personalInfoformLayout->addRow("Age", ageSpingBox);
personalInfoformLayout->addRow("Employment Status",
employmentStatusComboBox);
window->setLayout(personalInfoformLayout);
window->show();
return app.exec();
}
到现在为止,代码应该看起来很熟悉了。我们实例化了在表单中想要显示的各种小部件的对象。然后,创建布局:
QFormLayout *personalInfoformLayout = new QFormLayout;
创建了一个QFormLayout实例。任何我们想要将小部件添加到布局中,*personalInformformLayout,我们都会调用addRow()方法,传递一个表示标签的字符串,最后是我们要与标签对齐的小部件:
personalInfoformLayout->addRow("First Name:", firstNameLineEdit);
"First Name: " 是标签,而这里的部件是 firstNameLineEdit。
其他部件以这种方式添加到布局中:
window->setLayout(personalInfoformLayout);
然后,将 personalInfoformLayout 传递给 QWidget 实例的 setLayout() 方法。这意味着应用程序窗口 window 的布局是 personalInfoformLayout。
记住,QWidget 实例 window 将成为应用程序的主窗口,因为它的 show() 方法被调用。
QForm 通过提供一种简单的方式来添加行到我们的布局中,消除了指定列和行的需要,每次我们这样做时,我们都可以指定要显示的标签和部件。
当你编译并运行项目时,你应该看到以下输出:

上述截图显示了部件在这些布局中的对齐方式。表单以问答方式呈现。标签通常位于左侧,而接收用户输入的部件位于右侧。
带方向的布局
有一些布局在添加部件时提供了增长方向。有些情况下,我们希望将布局中的所有部件水平或垂直对齐。
QHBoxLayout 和 QVBoxLayout 类提供了这种功能。
QVBoxLayout
在 QVBoxLayout 布局中,部件是垂直对齐的,并且从上到下打包在布局中。
考虑以下图示:

对于 QVBoxLayout,箭头指示部件添加到布局中的增长方向。第一个部件 widget 1 将占据布局的顶部,而 addWidget() 的最后一次调用将使 widget 5 占据布局的底部。
要说明如何使用 QVBoxLayout,考虑以下程序:
#include <QApplication>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QLineEdit>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget *window = new QWidget;
QLabel *label1 = new QLabel("Username");
QLabel *label2 = new QLabel("Password");
QLineEdit *usernameLineEdit = new QLineEdit;
usernameLineEdit->setPlaceholderText("Enter your username");
QLineEdit *passwordLineEdit = new QLineEdit;
passwordLineEdit->setEchoMode(QLineEdit::Password);
passwordLineEdit->setPlaceholderText("Enter your password");
QPushButton *button1 = new QPushButton("&Login");
QPushButton *button2 = new QPushButton("&Register");
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(label1);
layout->addWidget(usernameLineEdit);
layout->addWidget(label2);
layout->addWidget(passwordLineEdit);
layout->addWidget(button1);
layout->addWidget(button2);
window->setLayout(layout);
window->show();
return app.exec();
}
在之前的例子中,我们说明了创建 QWidget 实例的原因。创建了两个带有字符串 "Username" 和 "Password" 的标签。还创建了一个用于接收用户名和密码输入的文本框,QLineEdit 实例。在 passwordLineEdit 对象上,通过传递常量 QLineEdit::Password 给 setEchoMode() 方法来隐藏该文本框的输入,并用点替换以防止输入的字符可读。
通过 setPlaceholderText() 方法设置 passwordLineEdit 中的占位文本。占位文本提供了关于文本框用途的更多信息。
同样创建了两个按钮,button1 和 button2。创建了一个 QVBoxLayout 的实例。为了向布局中添加小部件,调用 addWidget() 方法并传递特定的部件。传递给 addWidget 的第一个部件将在显示时出现在顶部。同样,最后添加的部件将显示在底部,在这个例子中是 button2。
通过将 layout 传递给 setLayout() 方法来设置 window 小部件实例的布局。
最后,在窗口上调用 show() 方法。编译项目并运行它以查看输出:

在前面的屏幕截图中,我们可以看到首先添加到布局中的小部件是标签 label1,而带有文本“注册”的 button2 是最后一个占据底部的小部件。
QHBoxLayout
QHBoxLayout 布局类在用法上与 QVBoxLayout 非常相似。通过调用 addWidget() 方法将小部件添加到布局中。
考虑以下图表:

图表中的箭头显示了小部件随着添加到 QHBoxLayout 而增长的方向。首先添加到这个布局中的是 小部件 1,而 小部件 3 是最后添加到布局中的小部件。
一个允许用户输入 URL 的小应用程序使用了这种布局类型:
#include <QApplication>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLineEdit>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget *window = new QWidget;
QLineEdit *urlLineEdit= new QLineEdit;
QPushButton *exportButton = new QPushButton("Export");
urlLineEdit->setPlaceholderText("Enter Url to export. Eg, http://yourdomain.com/items");
urlLineEdit->setFixedWidth(400);
QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(urlLineEdit);
layout->addWidget(exportButton);
window->setLayout(layout);
window->show();
return app.exec();
}
创建了一个文本框或 QLineEdit 和按钮。在 QLineEdit 实例 urlLineEdit 上设置了占位符。为了使占位符可见,我们通过将 setFixedWidth 设置为 400 来拉伸 urlLineEdit。
创建了一个 QHBoxLayout 实例并将其传递给 layout 指针。通过 addWidget() 方法将两个小部件 urlLineEdit 和 exportButton 添加到 layout 中。
布局被设置在 window 上,并调用了窗口的 show() 方法。
编译应用程序并运行它。你应该看到以下输出:

请参阅第一章,介绍 Qt 5,以编译应用程序。为了简化编译过程,请记住创建一个新的文件夹并将 .cpp 文件添加到其中。像往常一样,需要将 .pro 文件更改为包含小部件模块。
因为按钮是在文本框之后添加到布局中的,所以它相应地显示,紧邻文本框。如果另一个小部件被添加到布局中,它也会出现在按钮 exportButton 之后。
摘要
在本章中,我们探讨了在创建 GUI 应用程序时非常有用的许多小部件。编译过程保持不变。我们还学习了如何使用布局来展示和排列多个小部件。
到目前为止,我们的应用程序没有任何功能。当点击 QPushButton 实例时,它们以及其他由动作驱动的其他小部件都不会执行任何操作。
在下一章中,我们将学习如何使我们的应用程序能够响应用户操作,从而使它们变得有用。
第三章:使用信号和槽
到目前为止,我们已经学习了如何创建应用程序并显示各种类型的窗口部件。如果 GUI 应用程序只由这些组成,那么事情就结束了。但是,为了使我们的应用程序可用,我们还需要做更多的事情。在本章中,我们将着手以下内容:
-
理解信号和槽背后的概念
-
学习连接信号和槽的不同方法
GUI 工具包通常提供一种方法来响应应用程序内部发生的事情。不会留下任何偶然。应用程序内部发生的每一个动作都会被注册并记录下来。例如,当你移动一个窗口或调整其大小时,该动作会被注册,并且如果已经编写了足够的代码,它将作为对窗口移动或调整大小的反应来执行。对于发生的每一个动作,可能会有许多结果发生。本质上,我们想要回答的问题是:当特定的动作或事件发生时,我们该怎么办?我们如何处理它?
实现对已发生动作的响应能力的一种方法是通过使用称为观察者模式的设计模式。
在观察者模式设计中,一个可观察对象将其状态变化通知给正在观察它的其他对象。例如,任何时间一个对象(A)想要通知另一个对象(B)的状态变化,它首先必须识别该对象(B)并将自己注册为应该接收此类状态变化通知的对象之一。在未来某个时刻,当对象(B)的状态发生变化时,对象(B)将遍历它保持的想要了解状态变化的对象列表,此时将包括对象(A):

从前面的图中可以看出,主题圆圈被称为可观察对象,而边界框中的圆圈是观察者。它们正在被通知主题的状态变化,因为其计数变量从 1 增加到 5。
我们的应用程序中可能发生的一些事件或动作,我们可能会感兴趣并希望对其做出反应,包括以下内容:
-
窗口正在调整大小
-
按钮被点击
-
按下回车键
-
窗口部件正在被拖动
-
鼠标悬停在窗口部件上
对于按钮来说,对鼠标点击的典型响应可能是启动下载过程或发送电子邮件。
信号和槽
在 Qt 中,这种动作-响应方案由信号和槽处理。本节将包含一些定义,然后我们将通过一个示例进行进一步解释。
信号是一个传递的消息,用于传达对象状态已发生变化。这个信号可能携带有关发生变化的额外信息。例如,当一个窗口被调整大小时,信号通常会携带新状态(或大小)的坐标。有时,信号可能不携带额外信息,例如按钮点击。
槽是对象的一个特定函数,每当发出某个信号时都会被调用。由于槽是函数,它们将包含执行动作的代码行,例如关闭窗口、禁用按钮和发送电子邮件,仅举几例。
信号和槽必须被连接(在代码中)。如果不编写代码来连接信号和槽,它们将作为独立的实体存在。
大多数 Qt 中的部件都自带一系列的信号和槽。然而,你也可以编写自己的信号和槽。
那么,信号和槽看起来是什么样子?
考虑以下代码列表:
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton *quitButton = new QPushButton("Quit");
QObject::connect(quitButton, SIGNAL(clicked()),
&app, SLOT(quit()));
quitButton->show();
return app.exec();
}
如同往常,我们将使用以下步骤来编译项目:
-
创建一个具有适当名称的新文件夹
-
创建一个名为
main.cpp的.cpp文件 -
在终端中输入以下命令:
% qmake -project
% qmake
% make
% ./executable_file
一定要编辑.pro文件,在编译时包含widget模块。
编译并运行应用。
创建一个QPushButton的实例,命名为quitButton。这里的quitButton实例是可观察对象。任何时候点击这个按钮,都会发出clicked()信号。这里的clicked()信号是QPushButton类的一个方法,它已经被标记为信号。
app对象的quit()方法被调用,这终止了event循环。
为了指定当quitButton被点击时应该发生什么,我们传递app并说明app对象上的quit()方法应该被调用。这四个参数通过QObject类的静态函数connect()连接起来。
通用格式是(objectA,信号(methodOnObjectA()),objectB,槽(methodOnObjectB()))。
第二个和最后一个参数是表示信号和槽的方法的签名。第一个和第三个参数是指针,应该包含对象的地址。由于quitButton已经是一个指针,我们只需按原样传递它。另一方面,&app会返回app的地址。
现在,点击按钮,应用将会关闭:

当这个应用运行时,你应该看到以下内容。
我们刚刚所展示的例子相当原始。让我们编写一个应用,其中一个部件状态的变化传递给另一个部件。信号不仅会连接到一个槽,还会携带数据:
#include <QApplication>
#include <QVBoxLayout>
#include <QLabel>
#include <QDial>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget *window = new QWidget;
QVBoxLayout *layout = new QVBoxLayout;
QLabel *volumeLabel = new QLabel("0");
QDial *volumeDial= new QDial;
layout->addWidget(volumeDial);
layout->addWidget(volumeLabel);
QObject::connect(volumeDial, SIGNAL(valueChanged(int)), volumeLabel,
SLOT(setNum(int)));
window->setLayout(layout);
window->show();
return app.exec();
}
这是一个说明如何在信号和槽之间传递数据的另一个简单程序。创建了一个QVBoxLayout实例,命名为layout。创建了一个QLabel实例,命名为volumeLabel,它将用于显示发生的变化。它被初始化为字符串0。接下来,创建了一个QDial实例,命名为QDial *volumeDial = new QDial。QDial小部件是一个类似旋钮的控件,带有最小和最大数值范围。借助鼠标,可以旋转旋钮,就像你会上调扬声器或收音机的音量一样。
这两个小部件volumeLabel和volumeDial随后使用addWidget()方法添加到布局中。
每当我们改变QDial的旋钮时,都会发出一个名为valueChanged(int)的信号。volumeLabel对象的名为setNum(int)的槽是一个接受int值的函数。
注意以下代码中信号与槽之间的连接是如何建立的:
QObject::connect(volumeDial, SIGNAL(valueChanged(int)), volumeLabel, SLOT(setNum(int)));
这实际上建立了一个连接,该连接读取“每当 QDial 改变其值时,调用 volumeLabel 对象的 setNum() 方法,并传递一个 int 值。”QDial中可能发生许多状态变化。该连接进一步明确指出,我们只对旋钮(QDial)移动时发出的已更改的值感兴趣,这反过来又通过valueChanged(int)信号发出其当前值。
为了进行干燥运行程序,让我们假设QDial的范围代表从0到100的广播音量范围。如果QDial的旋钮改变到范围的一半,将发出valueChanged(50)信号。现在,值 50 将被传递给setNum(50)函数。这将用于设置标签的文本,在我们的例子中是volumeLabel,以显示 50。
编译应用程序并运行它。第一次运行时将显示以下输出:

正如你所见,QDial的初始状态为零。下面的标签也显示了这一点。移动旋钮,你会看到标签的值会相应地改变。以下截图显示了旋钮移动到范围一半后的应用程序状态:

移动旋钮并观察标签如何相应地改变。这一切都是通过信号和槽机制实现的。
信号和槽配置
不仅可以将一个信号连接到一个槽,还可以将一个信号连接到多个槽。这涉及到重复QObject::connect()调用,并在每个实例中指定当特定信号发出时应调用的槽。
单个信号,多个槽
在本节中,我们将关注如何将单个信号连接到多个槽。
检查以下程序:
#include <QApplication>
#include <QVBoxLayout>
#include <QLabel>
#include <QDial>
#include <QLCDNumber>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget *window = new QWidget;
QVBoxLayout *layout = new QVBoxLayout;
QLabel *volumeLabel = new QLabel("0");
QDial *volumeDial= new QDial;
QLCDNumber *volumeLCD = new QLCDNumber;
volumeLCD->setPalette(Qt::red);
volumeLabel->setAlignment(Qt::AlignHCenter);
volumeDial->setNotchesVisible(true);
volumeDial->setMinimum(0);
volumeDial->setMaximum(100);
layout->addWidget(volumeDial);
layout->addWidget(volumeLabel);
layout->addWidget(volumeLCD);
QObject::connect(volumeDial, SIGNAL(valueChanged(int)), volumeLabel,
SLOT(setNum(int)));
QObject::connect(volumeDial, SIGNAL(valueChanged(int)), volumeLCD ,
SLOT(display(int)));
window->setLayout(layout);
window->show();
return app.exec();
}
我们想说明一个信号如何连接到两个不同的槽,或者更确切地说,连接到多个槽。将要发出信号的部件是QDial的一个实例,即volumeDial。创建了一个QLCDNumber的实例,即volumeLCD。此小部件以类似 LCD 的数字形式显示信息。注意volumeLabel是QLabel的一个实例。这两个小部件将提供两个槽。
为了使volumeLCD的文本突出,我们使用volumeLCD->setPalette(Qt::red);将显示器的颜色设置为红色。
layout是一个QVBoxLayout的实例,这意味着添加到该布局中的小部件将从上到下流动。每个添加到布局中的小部件都将围绕中间对齐,因为我们已经在volumeLabel上设置了setAlignment(Qt::AlignHCenter);:
volumeDial->setNotchesVisible(true);
volumeDial->setMinimum(0);
volumeDial->setMaximum(100);
当调用setNotchesVisible(true)方法时,volumeDial上的刻度可见。setNotchesVisible()方法的默认参数是false,这使得刻度(刻度)不可见。我们通过调用setMinimum(0)和setMaximum(100)来设置QDial实例的范围。
使用addWidget()方法调用相应地添加了三个小部件:
layout->addWidget(volumeDial);
layout->addWidget(volumeLabel);
layout->addWidget(volumeLCD);
现在,volumeDial发出信号valueChanged(int),我们将其连接到volumeLabel的setNum(int)槽。当volumeDial的旋钮改变时,当前值将被发送到volumeLabel进行显示:
QObject::connect(volumeDial, SIGNAL(valueChanged(int)), volumeLabel, SLOT(setNum(int)));
QObject::connect(volumeDial, SIGNAL(valueChanged(int)), volumeLCD , SLOT(display(int)));
与此相同的信号,volumeDial的valueChanged(int),也连接到了volumeLCD的display(int)槽。
这两个连接的总效果是,当volumeDial发生变化时,volumeLabel和volumeLCD都会更新为volumeDial的当前值。所有这些都在同一时间发生,应用程序没有阻塞,这都要归功于信号和槽的高效设计。
编译并运行项目。程序的典型输出如下:

在前面的屏幕截图中,当QDial小部件(即看起来像圆形的对象)移动到 32 时,volumeLabel和volumeLCD都进行了更新。当你移动旋钮时,volumeLabel和volumeLCD将通过信号接收更新,并相应地更新自己。
单槽,多信号
在下一个示例中,我们将连接来自不同小部件的两个信号到一个单独的槽。让我们按照以下方式修改我们之前的程序:
#include <QApplication>
#include <QVBoxLayout>
#include <QLabel>
#include <QDial>
#include <QSlider>
#include <QLCDNumber>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget *window = new QWidget;
QVBoxLayout *layout = new QVBoxLayout;
QDial *volumeDial= new QDial;
QSlider *lengthSlider = new QSlider(Qt::Horizontal);
QLCDNumber *volumeLCD = new QLCDNumber;
volumeLCD->setPalette(Qt::red);
lengthSlider->setTickPosition(QSlider::TicksAbove);
lengthSlider->setTickInterval(10);
lengthSlider->setSingleStep(1);
lengthSlider->setMinimum(0);
lengthSlider->setMaximum(100);
volumeDial->setNotchesVisible(true);
volumeDial->setMinimum(0);
volumeDial->setMaximum(100);
layout->addWidget(volumeDial);
layout->addWidget(lengthSlider);
layout->addWidget(volumeLCD);
QObject::connect(volumeDial, SIGNAL(valueChanged(int)), volumeLCD ,
SLOT(display(int)));
QObject::connect(lengthSlider, SIGNAL(valueChanged(int)), volumeLCD
, SLOT(display(int)));
window->setLayout(layout);
window->show();
return app.exec();
}
在include语句中,我们添加了#include <QSlider>这一行,以添加QSlider类,这是一个可以设置在给定范围内的值的部件:
QApplication app(argc, argv);
QWidget *window = new QWidget;
QVBoxLayout *layout = new QVBoxLayout;
QDial *volumeDial= new QDial;
QSlider *lengthSlider = new QSlider(Qt::Horizontal);
QLCDNumber *volumeLCD = new QLCDNumber;
volumeLCD->setPalette(Qt::red);
实例化了QSlider小部件,并传递了Qt::Horizontal,这是一个常数,它改变了小部件的方向,使其水平显示。其他一切都是与之前示例中看到的一样。实例化了窗口和布局,以及QDial和QSlider对象:
lengthSlider->setTickPosition(QSlider::TicksAbove);
lengthSlider->setTickInterval(10);
lengthSlider->setSingleStep(1);
lengthSlider->setMinimum(0);
在本例中,第一个应该发出信号的部件是volumeDial对象。但现在,QSlider实例也发出一个信号,允许我们在QSlider状态改变时获取其状态。
为了在QSlider上显示刻度,我们调用setTickPosition()方法并传递常量QSlider::TicksAbove。这将显示在滑块顶部的刻度,非常类似于直边上的刻度显示。
setMinimum()和setMaximum()变量用于设置QSlider实例的值范围。这里的范围是0到100。
lengthSlider对象上的setTickInterval(10)方法用于设置刻度之间的间隔。
QVBoxLayout对象layout通过以下行将lengthSlider部件对象添加到它将容纳的部件列表中,layout->addWidget(lengthSlider);:
QObject::connect(volumeDial, SIGNAL(valueChanged(int)), volumeLCD , SLOT(display(int)));
QObject::connect(lengthSlider, SIGNAL(valueChanged(int)), volumeLCD , SLOT(display(int)));
有两个对静态方法connect()的调用。第一个调用将在volumeDial的valueChanged(int)信号与volumeLCD的display(int)槽之间建立连接。结果,每当QDial对象改变时,值将被传递到display(int)槽进行显示。
从不同的对象,我们将lengthSlider的valueChanged(int)信号连接到volumeLCD对象的相同槽display()。
程序的其余部分与往常一样。
按照我们为前一个示例所做的那样,从命令行编译并运行程序。
第一次运行应用程序时,输出应类似于以下内容:

QDial和QSlider都处于零的位置。现在,我们将QDial移动到 48。看看QLCDNumber是如何相应更新的:

通过我们设置的信号和槽,QSlider也可以更新相同的部件,即volumeLCD。当我们移动QSlider时,我们会看到volumeLCD会立即根据其值更新:

如所示,QSlider已经移动到其范围的末端,并且值已经传递到volumeLCD。
摘要
在本章中,我们探讨了 Qt 中信号和槽的核心概念。在创建我们的第一个应用程序后,我们研究了信号和槽可以连接的各种方式。
我们看到了如何将一个部件的信号连接到多个槽。这是一种典型的设置信号和槽的方式,尤其是在部件状态的变化需要通知许多其他部件时。
为了展示信号和槽可以如何灵活地配置,我们还查看了一个例子,其中多个信号连接到部件的一个槽。这种安排在可以使用不同的部件在部件上实现相同效果时很有用。
在第四章,实现窗口和对话框,我们将改变编写应用程序的风格,并学习如何制作完整的窗口应用程序。
第四章:实现窗口和对话框
在上一章中,我们学习了如何通过使用信号和槽来触发和响应应用程序内部发生的行为来使我们的应用程序动画化。到目前为止,我们一直专注于只包含在一个文件中的示例,并且没有明确描述一个完整的工作应用程序。要做到这一点,我们需要改变我们编写应用程序的方式,并采用一些新的约定。
在本章中,我们将使用 Qt 中的窗口,因此到本章结束时,你应该能够做到以下事情:
-
理解如何派生和创建自定义窗口应用程序
-
向窗口添加菜单栏
-
向窗口添加工具栏
-
使用各种对话框(框)向用户传达信息
创建自定义窗口
要创建一个窗口(应用程序),我们通常在一个QWidget实例上调用show()方法,这将使该小部件包含在一个自己的窗口中,以及在其中显示的子小部件。
对于这样一个简单的应用程序的回顾如下:
#include <QApplication>
#include <QMainWindow>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QMainWindow mainWindow;
mainWindow.show();
return a.exec();
}
mainWindow在这里是QMainWindow的一个实例,它继承自QWidget。因此,通过调用show()方法,会出现一个窗口。如果你将QMainWindow替换为QLabel,这仍然有效。
但这种编写应用程序的方式并不是最好的。相反,从现在开始,我们将定义我们自己的自定义小部件,在其中定义子小部件,并在信号和插槽之间建立连接。
现在,让我们通过派生QMainWindow来重写前面的应用程序。我们选择派生QMainWindow是因为我们需要展示菜单和工具栏。
我们首先创建一个新的文件夹并定义一个头文件。我们这里的头文件名为mainwindow.h,但你可以随意命名,并记得添加.h后缀。这个文件基本上应该包含以下内容:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QLabel>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
};
#endif
我们在我们的头文件中包含了 Qt 类QMainWindow和QLabel。然后,我们派生自QMainWindow并称它为MainWindow。这个新类的构造函数声明如下:
public:
MainWindow();
整个类定义被包裹在一个#ifndef ... #endif指令中,这个指令告诉预处理器如果它意外地在一个文件中多次包含其内容,则忽略其内容。
可以使用非标准的但广泛使用的预处理器指令#pragma once。
注意Q_OBJECT宏。这是使信号和槽机制成为可能的原因。记住,C++语言不知道用于设置信号和槽的关键字。通过包含这个宏,它就成为了 C++语法的组成部分。
我们到目前为止定义的只是头文件。主程序的主体必须存在于某个其他的.cpp文件中。为了便于识别,我们称之为mainwindow.cpp。在同一个文件夹中创建这个文件,并添加以下代码行:
#include "mainwindow.h"
MainWindow::MainWindow()
{
setWindowTitle("Main Window");
resize(400, 700);
QLabel *mainLabel = new QLabel("Main Widget");
setCentralWidget(mainLabel);
mainLabel->setAlignment(Qt::AlignCenter);
}
我们包含了之前定义的头文件。定义了我们的子类小部件 MainWindow 的默认构造函数。
注意我们是如何调用设置窗口标题的方法的。setWindowTitle() 被调用,并且由于它是一个从 QWindow 继承的方法,可以在构造函数内部访问。因此,不需要使用 this 关键字。窗口的大小是通过调用 resize() 方法并传递两个整数来指定的,这两个整数用作窗口的尺寸。
创建了一个 QLabel 的实例,mainLabel。通过调用 mainLabel->setAlignment(Qt::AlignCenter) 将标签内的文本对齐到中心。
调用 setCentralWidget() 是很重要的,因为它将任何从 QWidget 继承的类定位在窗口的内部。在这里,mainLabel 被传递给 setCentralWidget,这将使其成为窗口内唯一显示的小部件。
考虑以下图中 QMainWindow 的结构:

在每个窗口的顶部是 菜单栏。文件、编辑和帮助菜单等元素都放在那里。下面是 工具栏。工具栏中包含 停靠小部件,它们是可以折叠的面板。现在,窗口中的主要控制必须放在 中央小部件 位置。由于 UI 由多个小部件组成,因此创建一个包含子小部件的小部件将是有益的。这个父小部件是你将放入 中央小部件 区域的部件。为此,我们调用 setCentralWidget() 并传递父小部件。在窗口底部是 状态栏。
要运行应用程序,我们需要创建我们自定义窗口类的实例。在头文件和 .cpp 文件所在的同一文件夹中创建一个名为 main.cpp 的文件。将以下代码行添加到 main.cpp 中:
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow mainwindow;
mainwindow.show();
return app.exec();
}
我们包含了头文件 mainwindow.h,其中包含了我们自定义类 MainWindow 的声明。没有这个头文件,编译器将不知道在哪里找到 MainWindow 类的定义。
创建了一个 MainWindow 的实例,并调用了它的 show() 方法。我们仍然需要在 mainwindow 上调用 show() 方法。MainWindow 是 QMainWindow 的子类,并且表现得就像任何其他小部件一样。此外,正如我们已经知道的,要使小部件出现,必须在该小部件上调用 show() 方法。
要运行程序,通过命令行进入文件夹,并执行以下命令:
% qmake -project
将 QT += widgets 添加到生成的 .pro 文件中。现在继续执行下一组命令:
% qmake
% make
再次检查 .pro 文件。在文件的底部,我们有以下几行:
HEADERS += mainwindow.h
SOURCES += main.cpp mainwindow.cpp
标头文件会自动收集并添加到HEADERS中。同样,.cpp文件会被收集并添加到SOURCES中。始终记得在出现编译错误时检查此文件,以确保已添加所有必需的文件。
要运行程序,请发出以下命令:
% ./classSimpleWindow
对于在 macOS 上工作的人来说,为了运行可执行文件,你需要发出的正确命令如下:
% ./classSimpleWindow.app/Contents/MacOS/classSimpleWindow
运行中的应用程序应如下所示:

菜单栏
大多数应用程序都包含一组可点击的菜单项,这些菜单项会显示另一组动作列表,从而向用户展示更多功能。其中最受欢迎的是文件、编辑和帮助菜单。
在 Qt 中,菜单栏占据窗口的顶部。我们将创建一个简短的程序来使用菜单栏。
在新创建的文件夹中必须创建三个文件。这些如下所示:
-
main.cpp -
mainwindow.h -
mainwindow.cpp
在内容方面,main.cpp文件将保持不变。因此,从上一节复制main.cpp文件。让我们检查mainwindow.h文件:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QApplication>
#include <QAction>
#include <QtGui>
#include <QAction>
#include <QMenuBar>
#include <QMenu>
#include <Qt>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
private slots:
private:
// Menus
QMenu *fileMenu;
QMenu *helpMenu;
// Actions
QAction *quitAction;
QAction *aboutAction;
QAction *saveAction;
QAction *cancelAction;
QAction *openAction;
QAction *newAction;
QAction *aboutQtAction;
};
#endif
再次,头文件被包含在一个ifndef指令中,以防止由于多次包含此文件而可能发生的错误。
要在窗口内创建一个菜单,你需要QMenu的实例。每个菜单,如文件菜单,都将有子菜单或项目,这些项目构成了菜单。文件菜单通常有打开、新建和关闭子菜单。
菜单栏的典型图像如下,包括文件、编辑和帮助菜单。文件菜单下的文件菜单项包括新建...、打开...、保存、另存为...和退出:

我们的应用程序将只有两个菜单,即fileMenu和helpMenu。其他QAction实例是单个菜单项:quitAction、saveAction、cancelAction和newAction。
菜单和子菜单项都被定义为头文件中的类的成员。此外,这种声明将允许用户修改它们的行为,并在将它们连接到套接字时轻松访问它们。
现在,让我们切换到mainwindow.cpp。将以下代码复制到mainwindow.cpp中:
#include "mainwindow.h"
MainWindow::MainWindow()
{
setWindowTitle("SRM System");
setFixedSize(500, 500);
QPixmap newIcon("new.png");
QPixmap openIcon("open.png");
QPixmap closeIcon("close.png");
// Setup File Menu
fileMenu = menuBar()->addMenu("&File");
quitAction = new QAction(closeIcon, "Quit", this);
quitAction->setShortcuts(QKeySequence::Quit);
newAction = new QAction(newIcon, "&New", this);
newAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_C));
openAction = new QAction(openIcon, "&New", this);
openAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_O));
fileMenu->addAction(newAction);
fileMenu->addAction(openAction);
fileMenu->addSeparator();
fileMenu->addAction(quitAction);
helpMenu = menuBar()->addMenu("Help");
aboutAction = new QAction("About", this);
aboutAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_H));
helpMenu->addAction(aboutAction);
// Setup Signals and Slots
connect(quitAction, &QAction::triggered, this, &QApplication::quit);
}
在文件的开始处包含头文件mainwindow.h,以便在程序中使用类声明和 Qt 类。
在我们自定义类MainWindow的默认构造函数中,我们首先通过调用setWindowTitle()并给出一个合适的名称来设置窗口的名称。然后通过调用setFixedSize()确定窗口的大小。以下代码块展示了这一点:
QPixmap newIcon("new.png");
QPixmap openIcon("open.png");
QPixmap closeIcon("close.png");
菜单项可以显示带有图像的旁边。要将图像或图标与菜单项QAction关联,首先需要在一个QPixmap实例中捕获该图像。newIcon、openIcon和closeIcon变量中捕获了三个这样的图像。这些将在代码的下方使用。
让我们按照以下方式设置fileMenu:
fileMenu = menuBar()->addMenu("&File");
quitAction = new QAction(closeIcon, "Quit", this);
quitAction->setShortcuts(QKeySequence::Quit);
要将菜单添加到窗口中,需要调用menuBar()。这将返回一个QMenu实例,我们调用该对象的addMenu方法,指定要添加的菜单名称。在这里,我们调用我们的第一个菜单,文件。文件中的 F 前面的"&"符号将使得按下键盘上的Alt + F成为可能。
quitAction传递了一个QAction()实例。closeIcon是我们想要与这个子菜单关联的图像。"Quit"是显示名称,this关键字使quitAction成为MainWindow的子小部件。
通过调用setShortcuts()将子菜单的快捷键与quitAction关联。通过使用QKeySequence::Quit,我们掩盖了对平台特定键序列的需求。
newAction和openAction在其创建中遵循相同的逻辑。
现在我们已经有了fileMenu中的菜单和quitAction、newAction以及openActions中的菜单项,我们需要将它们链接在一起:
fileMenu->addAction(newAction);
fileMenu->addAction(openAction);
fileMenu->addSeparator();
fileMenu->addAction(quitAction);
要添加子菜单项,我们在QMenu实例fileMenu上调用addAction()方法,并传递所需的QAction实例。addSeparator()用于在我们的菜单项列表中插入一个视觉标记。它也返回一个QAction实例,但在此刻我们对此对象不感兴趣。
在应用程序中添加了一个新的菜单及其唯一的子菜单项:
helpMenu = menuBar()->addMenu("Help");
aboutAction = new QAction("About", this);
aboutAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_H));
helpMenu->addAction(aboutAction);
QAction封装了一个可以插入到小部件中的通用动作概念。在这里,我们使用QAction将动作插入到我们的菜单中。
这些QAction实例发出triggered信号,可以将该信号连接到套接字以使应用程序发生变化,如下所示:
connect(quitAction, &QAction::triggered, this, &QApplication::quit);
在类定义中将信号连接到槽时,只需调用connect()方法并传递参数,就像通常做的那样。第一个参数是将要发出我们感兴趣信号的对象。&QAction::triggered是指定触发信号的一种方式。这等同于写SIGNAL(triggered())。this关键字指的是将来将要创建的MainWindow对象。退出槽由&QApplication::quit指定。
连接的信号和槽将创建一种情况,当打开文件菜单并点击关闭按钮时,应用程序将关闭。
运行此示例所需的最后一个文件是main.cpp文件。应该将之前创建的main.cpp文件复制到这个项目中。
编译并运行项目。典型的输出应该如下所示:

在 Mac 上,按Command + Q键组合,这将关闭应用程序。在 Linux 和 Windows 上,Alt + F4应该做同样的事情。这是以下代码行实现的:
quitAction->setShortcuts(QKeySequence::Quit);
这行代码通过依赖于 Qt 的QKeySequence::Quit来模糊不同操作系统之间的差异。
点击文件菜单并选择新建:

没有发生任何事情。这是因为我们没有定义当用户点击该操作时应发生什么。另一方面,最后一个菜单项“退出”根据我们声明的套接字和槽关闭应用程序。
此外,请注意每个菜单项前面都有一个适当的图标或图像。
访问 Packt 网站以获取本书的图像。
工具栏
在菜单栏下方是一个通常被称为工具栏的面板。它包含一组控件,可以是小部件或QAction的实例,就像我们在创建菜单栏时看到的那样。这也意味着你可以选择用小部件替换QAction,例如一个普通的QPushButton或QComboBox。
工具栏可以固定在窗口顶部(菜单栏下方),可以将其固定在那里或使其在停靠小部件周围浮动。
再次强调,我们需要创建一个新的项目或修改本章前一部分的项目。我们将创建的文件是main.cpp、mainwindow.h和mainwindow.cpp。
main.cpp文件保持不变,如下所示。我们仅实例化我们的自定义类,并在其上调用show():
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); //
MainWindow mainwindow;
mainwindow.show();
return app.exec();
}
mainwindow.h文件将基本上包含将持有我们工具栏中的操作的QAction成员:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QApplication>
#include <QAction>
#include <QPushButton>
#include <QAction>
#include <QMenuBar>
#include <QMenu>
#include <QtGui>
#include <Qt>
#include <QToolBar>
#include <QTableView>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
private slots:
private:
// Menus
QMenu *fileMenu;
QMenu *helpMenu;
// Actions
QAction *quitAction;
QAction *aboutAction;
QAction *saveAction;
QAction *cancelAction;
QAction *openAction;
QAction *newAction;
QAction *aboutQtAction;
QToolBar *toolbar;
QAction *newToolBarAction;
QAction *openToolBarAction;
QAction *closeToolBarAction;
};
#endif
此头文件看起来与之前相同。唯一的不同之处在于QToolbar实例*toolbar和将在工具栏中显示的QAction对象。这些是newToolBarAction、openToolBarAction和closeToolBarAction。在菜单中使用的QAction实例与工具栏中使用的相同。
注意没有声明槽。
mainwindow.cpp文件将包含以下内容:
#include "mainwindow.h"
MainWindow::MainWindow()
{
setWindowTitle("Form in Window");
setFixedSize(500, 500);
QPixmap newIcon("new.png");
QPixmap openIcon("open.png");
QPixmap closeIcon("close.png");
// Setup File Menu
fileMenu = menuBar()->addMenu("&File");
quitAction = new QAction(closeIcon, "Quit", this);
quitAction->setShortcuts(QKeySequence::Quit);
newAction = new QAction(newIcon, "&New", this);
newAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_C));
openAction = new QAction(openIcon, "&New", this);
openAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_O));
fileMenu->addAction(newAction);
fileMenu->addAction(openAction);
fileMenu->addSeparator();
fileMenu->addAction(quitAction);
helpMenu = menuBar()->addMenu("Help");
aboutAction = new QAction("About", this);
aboutAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_H));
helpMenu->addAction(aboutAction);
// Setup Tool bar menu
toolbar = addToolBar("main toolbar");
// toolbar->setMovable( false );
newToolBarAction = toolbar->addAction(QIcon(newIcon), "New File");
openToolBarAction = toolbar->addAction(QIcon(openIcon), "Open File");
toolbar->addSeparator();
closeToolBarAction = toolbar->addAction(QIcon(closeIcon), "Quit Application");
// Setup Signals and Slots
connect(quitAction, &QAction::triggered, this, &QApplication::quit);
connect(closeToolBarAction, &QAction::triggered, this, &QApplication::quit);
}
用于菜单栏的相同图标集也将用于工具栏。
要获取用于进一步操作的 Windows 工具栏的实例,请调用addTooBar()方法,它将返回一个QToolBar的实例。该方法接受任何用作窗口标题的文本。它还将工具栏添加到窗口中。
到目前为止的工具栏可以在窗口内移动。要将它固定在窗口顶部,请在QToolBar实例toolbar上调用toolbar->setMovable(false);函数:
newToolBarAction = toolbar->addAction(QIcon(newIcon), "New File");
openToolBarAction = toolbar->addAction(QIcon(openIcon), "Open File");
toolbar->addSeparator();
closeToolBarAction = toolbar->addAction(QIcon(closeIcon), "Quit Application");
创建了两个QAction对象,并将它们传递给newToolBarAction和openToolBarAction对象。我们传递了QIcon对象,它将成为QAction上的图像,以及一个用作工具提示的名称或文本。通过调用addSeparator()方法,在工具栏中添加了一个分隔符。最后一个控件closeToolBarAction包含要在工具栏上显示的图像。
将closeToolBarAction的触发信号链接到窗口的退出槽,我们进行以下操作:
connect(closeToolBarAction, &QAction::triggered, this, &QApplication::quit);
要编译此项目作为复习,请运行以下命令:
% qmake -project
将QT += widgets添加到生成的.pro文件中,并确保文件底部列出所有三个文件:
按顺序发出以下命令以构建项目:
% qmake
% make
% ./name_of_executable
如果一切顺利,您将看到以下内容:

上一张截图显示了文件和帮助菜单下方的工具栏。三个图标显示了代表新建、打开和关闭操作的三个QAction对象。只有最后一个按钮(关闭应用程序)的操作是有效的。这是因为我们只为closeToolBarAction和QAction对象定义了一个信号-槽连接。
通过将鼠标悬停在工具栏菜单项上,会出现一些文本。这个消息被称为工具提示。如前图所示,打开文件消息是从以下行的最后一个参数派生出来的:
openToolBarAction = toolbar->addAction(QIcon(openIcon), "Open File");
如前所述,工具栏可以在窗口内移动,如下所示:

如您所见,通过点击工具栏左侧的三个垂直点并移动它,您可以将工具栏从顶部移至左侧、右侧或底部。要显示此类功能,请发出以下命令:
toolbar->setMovable(false);
这将使工具栏固定在顶部,使其不能移动。
添加其他小部件
到目前为止,我们只向窗口添加了菜单栏和工具栏。为了添加可能使我们的应用程序更有用的其他小部件,我们必须向头文件中添加更多成员。在本节中,我们将创建一个简单的应用程序,将个人详细信息追加到可显示的列表中。
将会收到一个表单,其中包含多个联系人的详细信息。这些详细信息随后将被添加到窗口上的一个列表中。随着联系人的增加,列表也会增长。我们将基于前一个部分的代码并在此基础上继续构建。
如常,您创建一个包含三个文件的新文件夹,分别是main.cpp、mainwindow.cpp和mainwindow.h。main.cpp文件将保持与之前章节相同。
mainwindow.h文件应包含以下代码行:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QApplication>
#include <QLabel>
#include <QLineEdit>
#include <QDate>
#include <QDateEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QPushButton>
#include <QMessageBox>
#include <QAction>
#include <QMenuBar>
#include <QMenu>
#include <QtGui>
#include <Qt>
#include <QToolBar>
#include <QTableView>
#include <QHeaderView>
该文件导入了将在自定义类内部声明成员的类。整个文件被#ifndef指令包裹,以便头文件可以被多次包含而不会产生错误。
将以下代码行添加到相同的头文件mainwindow.h中:
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
private slots:
void saveButtonClicked();
然后我们声明我们类的默认构造函数。
在我们的应用程序中只有一个槽,它将被用来将多个小部件的内容移动到列表中。
通过添加以下代码行继续代码列表,这些代码将添加类的成员并定义一些helper方法的原型:
private:
// Widgets
QWidget *mainWidget;
QVBoxLayout *centralWidgetLayout;
QGridLayout *formLayout;
QHBoxLayout *buttonsLayout;
QLabel *nameLabel;
QLabel *dateOfBirthLabel;
QLabel *phoneNumberLabel;
QPushButton *savePushButton;
QPushButton *newPushButton;
QLineEdit *nameLineEdit;
QDateEdit *dateOfBirthEdit;
QLineEdit *phoneNumberLineEdit;
QTableView *appTable;
QStandardItemModel *model;
// Menus
QMenu *fileMenu;
QMenu *helpMenu;
// Actions
QAction *quitAction;
QAction *aboutAction;
QAction *saveAction;
QAction *cancelAction;
QAction *openAction;
QAction *newAction;
QAction *aboutQtAction;
QAction *newToolBarAction;
QAction *openToolBarAction;
QAction *closeToolBarAction;
QAction *clearToolBarAction;
// Toolbar
QToolBar *toolbar;
// Icons
QPixmap newIcon;
QPixmap openIcon;
QPixmap closeIcon;
QPixmap clearIcon;
// init methods
void clearFields();
void createIcons();
void createMenuBar();
void createToolBar();
void setupSignalsAndSlot();
void setupCoreWidgets();
};
#endif
成员包括布局和其他小部件类,以及我们的菜单、工具栏及其关联的QAction对象。
如您所见,代码是从前一个部分借用的,除了添加的控件。
私有方法createIcons()、createMenuBar()、createToolBar()、setupSignalsAndSlot()和setupCoreWidgets()将被用来重构应该存在于我们的默认构造函数中的代码。clearFields()方法将被用来清除多个小部件中的数据。
在mainwindow.cpp文件中,我们将使用以下代码行定义我们的类:
#include "mainwindow.h"
#include "mainwindow.h"
MainWindow::MainWindow()
{
setWindowTitle("Form in Window");
setFixedSize(500, 500);
createIcons();
setupCoreWidgets();
createMenuBar();
createToolBar();
centralWidgetLayout->addLayout(formLayout);
centralWidgetLayout->addWidget(appTable);
centralWidgetLayout->addLayout(buttonsLayout);
mainWidget->setLayout(centralWidgetLayout);
setCentralWidget(mainWidget);
setupSignalsAndSlots();
}
在这里,默认构造函数已经进行了大量的重构。代码的构建块已经被移动到函数中,以帮助使代码可读。
现在,我们只设置了应用程序窗口的标题和大小。接下来,我们将调用一个方法来创建各种小部件将使用的图标。通过调用setupCoreWidgets()方法,我们再次进行函数调用以设置核心小部件。通过调用createMenuBar()和createToolBar()方法,创建了菜单和工具栏。
布局对象centralWidgetLayout是我们应用程序的主要布局。我们首先添加formLayout对象,然后是appTable对象。正如你所见,可以将布局插入到另一个布局中。最后,我们插入包含我们的按钮的buttonsLayout对象。
将mainWidget对象的布局设置为centralWidgetLayout。然后,将此mainWidget对象设置为应该占据窗口中心的主体小部件,正如本章第一图所示。
所有信号和槽都将设置在setupSignalsAndSlot()方法中。
将以下代码行添加到定义createIcons()方法的mainwindow.cpp文件中:
void MainWindow::createIcons() {
newIcon = QPixmap("new.png");
openIcon = QPixmap("open.png");
closeIcon = QPixmap("close.png");
clearIcon = QPixmap("clear.png");
}
createIcons()方法将QPixmap实例传递给在mainwindow.h中声明的成员。
setupCoreWidgets()的定义如下,位于mainwindow.cpp中:
void MainWindow::setupCoreWidgets() {
mainWidget = new QWidget();
centralWidgetLayout = new QVBoxLayout();
formLayout = new QGridLayout();
buttonsLayout = new QHBoxLayout();
nameLabel = new QLabel("Name:");
dateOfBirthLabel= new QLabel("Date Of Birth:");
phoneNumberLabel = new QLabel("Phone Number");
savePushButton = new QPushButton("Save");
newPushButton = new QPushButton("Clear All");
nameLineEdit = new QLineEdit();
dateOfBirthEdit = new QDateEdit(QDate::currentDate());
phoneNumberLineEdit = new QLineEdit();
// TableView
appTable = new QTableView();
model = new QStandardItemModel(1, 3, this);
appTable->setContextMenuPolicy(Qt::CustomContextMenu);
appTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); /** Note **/
model->setHorizontalHeaderItem(0, new QStandardItem(QString("Name")));
model->setHorizontalHeaderItem(1, new QStandardItem(QString("Date of Birth")));
model->setHorizontalHeaderItem(2, new QStandardItem(QString("Phone Number"))); appTable->setModel(model)
QStandardItem *firstItem = new QStandardItem(QString("G. Shone"));
QDate dateOfBirth(1980, 1, 1);
QStandardItem *secondItem = new QStandardItem(dateOfBirth.toString());
QStandardItem *thirdItem = new QStandardItem(QString("05443394858"));
model->setItem(0,0,firstItem);
model->setItem(0,1,secondItem);
model->setItem(0,2,thirdItem);
formLayout->addWidget(nameLabel, 0, 0);
formLayout->addWidget(nameLineEdit, 0, 1);
formLayout->addWidget(dateOfBirthLabel, 1, 0);
formLayout->addWidget(dateOfBirthEdit, 1, 1);
formLayout->addWidget(phoneNumberLabel, 2, 0);
formLayout->addWidget(phoneNumberLineEdit, 2, 1);
buttonsLayout->addStretch();
buttonsLayout->addWidget(savePushButton);
buttonsLayout->addWidget(newPushButton);
}
在这里,我们只是实例化在应用程序中使用的对象。这里没有什么不同寻常的。nameLineEdit和phoneNumberLineEdit将被用来收集即将保存的联系人姓名和电话号码。dateOfBirthEdit是一种特殊的文本框,允许你指定日期。savePushButton和newPushButton是按钮,将被用来触发联系人的保存和列表的清除。
标签和行编辑控件将被用于formLayout对象,这是一个QGridLayout实例。QGridLayout允许通过列和行指定小部件。
要保存联系人,这意味着我们将将其保存到可以显示项目列表的小部件中。Qt 有多个此类小部件。这些包括QListView、QTableView和QTreeView。
当使用QListView显示信息时,它通常如下所示:

QTableView将使用列和行在单元格中显示数据或信息,如下所示:

为了显示层次信息,QTreeView 也被使用,如下面的截图所示:

将 QTableView 的一个实例传递给 appTable。我们需要为我们的 QTableView 实例提供一个模型。该模型将保存将在表格中显示的数据。当数据被添加或从模型中删除时,其相应的视图将自动更新以显示发生的变化。这里的模型是 QStandardItemModel 的一个实例。QStandardItemModel(1, 3, this) 这行代码将创建一个具有一行三列的实例。this 关键字用于使模型成为 MainWindow 对象的子对象:
appTable->setContextMenuPolicy(Qt::CustomContextMenu);
这行代码用于帮助我们定义当我们在表格上打开上下文菜单时应发生的自定义操作:
appTable->horizontalHeader()->setSectionResizeMode(
QHeaderView::Stretch); /** Note **/
前面的这一行很重要,它使得我们的表格标题可以完全拉伸。这是省略该行时的结果(如红色方框所示区域):

理想情况下,我们希望我们的表格具有以下标题,以便看起来像这样:

要设置表格的标题,我们可以使用以下代码行:
model->setHorizontalHeaderItem(0, new QStandardItem(QString("Name")));
显示联系人的表格需要标题。模型对象上的 setHorizontalHeaderItem() 方法使用第一个参数来指示新 QStandardItem(QString()) 应该插入的位置。由于我们的表格使用三列,因此标题行重复三次,分别是姓名、出生日期和电话号码:
appTable->setModel(model);
QStandardItem *firstItem = new QStandardItem(QString("G. Shone"));
QDate dateOfBirth(1980, 1, 1);
QStandardItem *secondItem = new QStandardItem(dateOfBirth.toString());
QStandardItem *thirdItem = new QStandardItem(QString("05443394858"));
model->setItem(0,0,firstItem);
model->setItem(0,1,secondItem);
model->setItem(0,2,thirdItem);
我们通过在 appTable 上调用 setModel() 并将 model 作为参数传递,使 model 成为我们的 QTableView 的模型。
为了填充我们的模型,从而更新其视图 QTableView,我们将创建 QStandardItem 的实例。我们的表格中的每个单元格都必须封装在这个类中。dateOfBirth 是 QDate 类型,因此我们在其上调用 toString() 并将其传递给 new QStandardItem()。通过指定行和列,firstItem 被插入到我们的模型中,如 model->setItem(0, 0, firstItem); 这行代码所示。
这也适用于第二个和第三个 QStandardItem 对象。
现在,让我们填充我们的 formLayout 对象。这是一个 QGridLayout 类型的对象。要向布局中插入小部件,请使用以下代码行:
formLayout->addWidget(nameLabel, 0, 0);
formLayout->addWidget(nameLineEdit, 0, 1);
formLayout->addWidget(dateOfBirthLabel, 1, 0);
formLayout->addWidget(dateOfBirthEdit, 1, 1);
formLayout->addWidget(phoneNumberLabel, 2, 0);
formLayout->addWidget(phoneNumberLineEdit, 2, 1);
我们通过调用 addWidget() 并提供小部件及其应填充的行和列来向布局添加小部件。0, 0 将填充第一个单元格,0, 1 将填充第一行的第二个单元格,而 1, 0 将填充第二行的第一个单元格。
以下代码向 buttonsLayout 的 QHBoxLayout 实例添加按钮:
buttonsLayout->addStretch();
buttonsLayout->addWidget(savePushButton);
buttonsLayout->addWidget(newPushButton);
要将 savePushButton 和 newPushButton 推送到右侧,我们首先通过在调用 addWidget() 添加小部件之前调用 addStretch() 来添加一个拉伸,以填充空余空间。
在我们来到应用程序的菜单之前,请添加以下代码。为了将菜单和工具栏添加到我们的应用程序中,请将createMenuBar()和createToolBar()的定义添加到mainwindow.cpp文件中:
void MainWindow::createMenuBar() {
// Setup File Menu
fileMenu = menuBar()->addMenu("&File");
quitAction = new QAction(closeIcon, "Quit", this);
quitAction->setShortcuts(QKeySequence::Quit);
newAction = new QAction(newIcon, "&New", this);
newAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_C));
openAction = new QAction(openIcon, "&New", this);
openAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_O));
fileMenu->addAction(newAction);
fileMenu->addAction(openAction);
fileMenu->addSeparator();
fileMenu->addAction(quitAction);
helpMenu = menuBar()->addMenu("Help");
aboutAction = new QAction("About", this);
aboutAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_H));
helpMenu->addAction(aboutAction);
}
void MainWindow::createToolBar() {
// Setup Tool bar menu
toolbar = addToolBar("main toolbar");
// toolbar->setMovable( false );
newToolBarAction = toolbar->addAction(QIcon(newIcon), "New File");
openToolBarAction = toolbar->addAction(QIcon(openIcon), "Open File");
toolbar->addSeparator();
clearToolBarAction = toolbar->addAction(QIcon(clearIcon), "Clear All");
closeToolBarAction = toolbar->addAction(QIcon(closeIcon), "Quit Application");
}
前面的代码是添加工具栏和菜单到我们窗口的熟悉代码。代码的最后几行定义了setupSignalsAndSlots()方法:
void MainWindow::setupSignalsAndSlots() {
// Setup Signals and Slots
connect(quitAction, &QAction::triggered, this, &QApplication::quit);
connect(closeToolBarAction, &QAction::triggered, this, &QApplication::quit);
connect(savePushButton, SIGNAL(clicked()), this, SLOT(saveButtonClicked()));
}
在前面的代码中,我们将quitAction的触发信号连接到QApplication的退出槽。closeToolBarAction的触发信号也被连接到相同的槽,以实现关闭应用程序的效果。
savePushButton的clicked()信号被连接到槽saveButtonClicked()。因为它是在我们的类中定义的,所以在第三个参数中使用this关键字。
确保输入到表单中的信息被保存的确切操作是由充当槽的saveButtonClicked()函数定义的。
要定义我们的槽,请将以下代码添加到mainwindow.cpp中:
void MainWindow::saveButtonClicked()
{
QStandardItem *name = new QStandardItem(nameLineEdit->text());
QStandardItem *dob = new QStandardItem(dateOfBirthEdit->date().toString());
QStandardItem *phoneNumber = new QStandardItem(phoneNumberLineEdit->text());
model->appendRow({ name, dob, phoneNumber});
clearFields();
}
当saveButtonClicked()被调用时,我们将从控件nameLinedEdit、dateOfBirthEdit和phoneNumberLineEdit中提取值。通过在模型对象上调用appendRow(),我们将它们追加到模型中。我们可以访问模型对象,因为它是我们类定义中的一个成员指针变量。
将新联系人信息添加到列表后,所有字段都会通过调用clearFields()方法被清除并重置。
为了清除字段,我们调用clearFields(),该函数在mainwindow.cpp中定义如下:
void MainWindow::clearFields()
{
nameLineEdit->clear();
phoneNumberLineEdit->setText("");
QDate dateOfBirth(1980, 1, 1);
dateOfBirthEdit->setDate(dateOfBirth);
}
通过调用clear()方法,nameLineEdit对象被重置为空字符串。此方法也充当槽。将QLineEdit对象设置为空字符串的另一种方法是调用setText("")来设置文本:
因为QDateEdit接受日期,所以我们必须创建一个date实例并将其传递给dateOfBirthEdit的setDate()。
编译并运行项目。你应该会看到以下输出:

添加新联系人,请填写表格并点击保存按钮:

在点击保存按钮后,你应该会看到以下内容:

添加对话框
有时候,应用程序需要通知用户一个动作或接收输入以进行进一步处理。通常,会出现另一个窗口,通常是小型窗口,其中包含此类信息或说明。在 Qt 中,QMessageBox为我们提供了使用QInputDialog发出警报和接收输入的功能。
如下表所述,有不同的消息:

为了向用户传达最近完成的任务,可以创建QMessage实例的以下代码列表作为示例:
QMessageBox::information(this, tr("RMS System"), tr("Record saved successfully!"),QMessageBox::Ok|QMessageBox::Default,
QMessageBox::NoButton, QMessageBox::NoButton);
前面的代码列表将产生如下输出:

这个 QMessageBox 实例正在被用来向用户传达操作成功。
QMessageBox 实例的图标和按钮数量是可以配置的。
让我们完成正在编写的联系应用,以展示如何使用 QMessageBox 和 QInputDialog。
选择基于上一节中的示例进行构建,或者创建一个包含我们迄今为止一直在使用的三个主要文件的新文件夹,即 main.cpp、mainwindow.cpp 和 mainwindow.h。
mainwindow.h 文件应包含以下内容:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QApplication>
#include <QLabel>
#include <QLineEdit>
#include <QDate>
#include <QDateEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QPushButton>
#include <QMessageBox>
#include <QAction>
#include <QMenuBar>
#include <QMenu>
#include <QtGui>
#include <Qt>
#include <QToolBar>
#include <QTableView>
#include <QHeaderView>
#include <QInputDialog>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
private slots:
void saveButtonClicked();
void aboutDialog();
void clearAllRecords();
void deleteSavedRecord();
private:
// Widgets
QWidget *mainWidget;
QVBoxLayout *centralWidgetLayout;
QGridLayout *formLayout;
QHBoxLayout *buttonsLayout;
QLabel *nameLabel;
QLabel *dateOfBirthLabel;
QLabel *phoneNumberLabel;
QPushButton *savePushButton;
QPushButton *clearPushButton;
QLineEdit *nameLineEdit;
QDateEdit *dateOfBirthEdit;
QLineEdit *phoneNumberLineEdit;
QTableView *appTable;
QStandardItemModel *model;
// Menus
QMenu *fileMenu;
QMenu *helpMenu;
// Actions
QAction *quitAction;
QAction *aboutAction;
QAction *saveAction;
QAction *cancelAction;
QAction *openAction;
QAction *newAction;
QAction *aboutQtAction;
QAction *newToolBarAction;
QAction *openToolBarAction;
QAction *closeToolBarAction;
QAction *clearToolBarAction;
QAction *deleteOneEntryToolBarAction;
// Icons
QPixmap newIcon;
QPixmap openIcon;
QPixmap closeIcon;
QPixmap clearIcon;
QPixmap deleteIcon;
// Toolbar
QToolBar *toolbar;
void clearFields();
void createIcons();
void createMenuBar();
void createToolBar();
void setupSignalsAndSlots();
void setupCoreWidgets();
};
#endif
唯一值得注意的是槽的数量有所增加。saveButtonClicked() 槽将被重新实现,以弹出一个消息告知用户保存操作成功。aboutDialog() 槽将用于显示关于信息。这通常是一个传达有关程序信息的窗口,通常包含版权、帮助和联系方式。
clearAllRecords() 槽将调用一个询问消息框,提示用户即将执行破坏性操作。deleteSavedRecord() 将使用 QInputDialog 接受用户输入,以确定从我们的联系人列表中删除哪一行。
QAction *aboutQtAction 将用于调用显示关于页面或消息的插槽。我们还将添加一个工具栏操作 QAction *deleteOneEntryToolBarAction,它将用于调用一个接收用户输入的对话框。观察这三个输入,QPixmap deleteIcon、QPixmap clearIcon 和 QPixmap deleteIcon,随着我们向窗口添加更多操作,同样还有 QPushButton*clearPushButton,它正在替换上一个示例中的 newPushButton。
关于头文件的其他内容保持不变。额外导入的两个类是 QMessageBox 和 QInputDialog 类。
在 mainwindow.cpp 文件中,我们定义 MainWindow 类的默认构造函数如下:
#include "mainwindow.h"
MainWindow::MainWindow()
{
setWindowTitle("RMS System");
setFixedSize(500, 500);
setWindowIcon(QIcon("window_logo.png"));
createIcons();
setupCoreWidgets();
createMenuBar();
createToolBar();
centralWidgetLayout->addLayout(formLayout);
centralWidgetLayout->addWidget(appTable);
//centralWidgetLayout->addStretch();
centralWidgetLayout->addLayout(buttonsLayout);
mainWidget->setLayout(centralWidgetLayout);
setCentralWidget(mainWidget);
setupSignalsAndSlots();
}
这次,我们希望给整个应用程序添加一个图标,当它在运行时会在任务栏或托盘上显示。为此,我们调用 setWindowIcon() 方法,并传入 QIcon("window_logo.png") 实例。
window_logo.png 文件包含在项目中,以及其他用作此书 Packt 网站附件的图像文件。
在之前的示例中,一切保持不变。设置应用程序各个部分的方法已经略有修改。
setupSignalsAndSlots() 方法使用以下代码实现:
void MainWindow::setupSignalsAndSlots() {
// Setup Signals and Slots
connect(quitAction, &QAction::triggered, this, &QApplication::quit);
connect(aboutAction, SIGNAL(triggered()), this, SLOT(aboutDialog()));
connect(clearToolBarAction, SIGNAL(triggered()), this, SLOT(clearAllRecords()));
connect(closeToolBarAction, &QAction::triggered, this, &QApplication::quit);
connect(deleteOneEntryToolBarAction, SIGNAL(triggered()), this, SLOT(deleteSavedRecord()));
connect(savePushButton, SIGNAL(clicked()), this, SLOT(saveButtonClicked()));
connect(clearPushButton, SIGNAL(clicked()), this, SLOT(clearAllRecords()));
}
aboutAction 的 triggered() 信号连接到 aboutDialog() 插槽。此方法会弹出一个对话框,用于显示包含有关应用程序的一些信息和应用程序标志(我们通过调用 setWindowIcon() 定义)的窗口:
void MainWindow::aboutDialog()
{
QMessageBox::about(this, "About RMS System","RMS System 2.0" "<p>Copyright © 2005 Inc." "This is a simple application to demonstrate the use of windows," "tool bars, menus and dialog boxes");
}
静态方法QMessageBox::about()以this作为其第一个参数被调用。窗口的标题是第二个参数,第三个参数是一个描述应用程序的字符串。
在运行时,点击帮助菜单,然后点击关于。你应该看到以下输出:

在setupSignalsAndSlots()方法中建立的第三个信号-槽连接如下:
connect(clearToolBarAction, SIGNAL(triggered()), this, SLOT(clearAllRecords()));
在clearAllRecords()槽中,我们将首先使用提示来询问用户,他们是否确定要删除模型中的所有项目。这可以通过以下代码实现:
int status = QMessageBox::question( this, tr("Delete Records ?"), tr("You are about to delete all saved records "
"<p>Are you sure you want to delete all records "), QMessageBox::No|QMessageBox::Default, QMessageBox::No|QMessageBox::Escape, QMessageBox::NoButton);
if (status == QMessageBox::Yes)
return model->clear();
QMessageBox::question用于弹出一个对话框来询问用户问题。它有两个主要按钮,是和否。QMessageBox::No|QMessageBox::Default将“否”选项设置为默认选择。QMessageBox::No|QMessageBox::Escape使得 Esc 键具有与点击“否”选项相同的效果。
用户选择的任何选项都将存储为int类型的status变量。然后,它将与QMessageBox::Yes常量进行比较。这种方式询问用户是或否的问题信息不足,尤其是在用户点击“是”时将执行破坏性操作的情况下。我们将在clearAllRecords()中使用定义的替代形式:
void MainWindow::clearAllRecords()
{
*/
int status = QMessageBox::question( this, tr("Delete all Records ?"), tr("This operation will delete all saved records. " "<p>Do you want to remove all saved records ? "
), tr("Yes, Delete all records"), tr("No !"), QString(), 1, 1);
if (status == 0) {
int rowCount = model->rowCount();
model->removeRows(0, rowCount);
}
}
如同往常,父对象由this指向。第二个参数是对话框的标题,接下来是问题的字符串。我们将第一个选项描述得详细一些,通过传递“是,删除所有记录”。用户阅读后,将知道点击按钮会有什么效果。No !参数将显示在代表问题另一个答案的按钮上。传递QString()是为了不显示第三个按钮。当点击第一个按钮时,status将返回0。当点击第二个按钮或选项时,将返回1。通过指定1,我们使"No !"按钮成为对话框的默认按钮。我们再次选择1,因为最后一个参数指定当按下 Esc 键时应该选择"No !"按钮。
如果用户点击“是,删除所有记录”按钮,则status将存储0。在if语句的主体中,我们获取我们的模型对象的行数。调用removeRows并指定从第一个(由0表示)到rowCount的所有条目都应该被删除。然而,如果用户点击“No !”按钮,应用程序将不执行任何操作,因为我们没有在if语句中指定这一点。
当点击“清除所有”按钮时,对话框窗口应如下显示:

saveButtonClicked()槽也被修改,向用户显示一个简单的消息,表明操作已成功,如下代码块所示:
void MainWindow::saveButtonClicked()
{
QStandardItem *name = new QStandardItem(nameLineEdit->text());
QStandardItem *dob = new QStandardItem(dateOfBirthEdit->date().toString());
QStandardItem *phoneNumber = new QStandardItem(phoneNumberLineEdit->text());
model->appendRow({ name, dob, phoneNumber});
clearFields();
QMessageBox::information(this, tr("RMS System"), tr("Record saved successfully!"),
QMessageBox::Ok|QMessageBox::Default,
QMessageBox::NoButton, QMessageBox::NoButton);
}
最后两个参数是常数,用于防止按钮在消息框中显示。
为了允许应用程序从表中删除某些行,使用deleteSaveRecords()方法来弹出一个基于输入的对话框,该对话框接收我们想要删除的行的rowId:
void MainWindow::deleteSavedRecord()
{
bool ok;
int rowId = QInputDialog::getInt(this, tr("Select Row to delete"), tr("Please enter Row ID of record (Eg. 1)"),
1, 1, model->rowCount(), 1, &ok );
if (ok)
{
model->removeRow(rowId-1);
}
}
this关键字指的是父对象。静态方法QInputDialog::getInt()的第二个参数用作对话框窗口的标题。请求被捕获在第二个参数中。这里的第三个参数用于指定输入字段的默认数值。1和model->rowCount()是应接受的最低和最高值。
倒数第二个参数1是最低值和最高值之间的增量步长。True或False将被存储在&ok中。当用户点击“确定”时,True将被存储在&ok中,基于这一点,if语句将调用模型对象的removeRow。用户输入的任何值都将传递给rowId。我们传递rowId-1以获取模型中行的实际索引。
通过执行以下命令建立与此槽的连接:
connect(deleteOneEntryToolBarAction, SIGNAL(triggered()), this,
SLOT(deleteSavedRecord()));
deleteOneEntryToolBarAction是工具栏上的倒数第二个操作。
以下截图是用户点击此操作时将出现的界面:

设置工具栏的方法如下所示:
void MainWindow::createToolBar() {
// Setup Tool bar menu
toolbar = addToolBar("main toolbar");
// toolbar->setMovable( false );
newToolBarAction = toolbar->addAction(QIcon(newIcon), "New File");
openToolBarAction = toolbar->addAction(QIcon(openIcon), "Open File");
toolbar->addSeparator();
clearToolBarAction = toolbar->addAction(QIcon(clearIcon), "Clear All");
deleteOneEntryToolBarAction = toolbar->addAction(QIcon(deleteIcon), "Delete a record");
closeToolBarAction = toolbar->addAction(QIcon(closeIcon), "Quit Application");
}
所有其他方法都是从上一节借用的,可以从本书附带的源代码中获得。
总结一下,编译并运行项目后你应该看到以下内容:

记住,我们已经在模型对象中有一个条目的原因是因为我们在setupCoreWidgets()方法中创建了这样一个条目。
填写姓名、出生日期和电话号码字段,然后点击保存。这将向窗口中的表格添加一行。一个对话框消息将告诉你操作是否成功。
要在表中删除一行,选择所需的行并点击回收站图标,然后确认你是否真的想要删除条目。
摘要
在本章中,我们看到了如何创建菜单、工具栏,以及如何使用对话框接收进一步输入并向用户显示信息。
在第五章“管理事件、自定义信号和槽”中,我们将探讨事件的使用以及更多关于信号和槽的内容。
第五章:管理事件、自定义信号和槽
本章介绍了事件的概念。为了保持工作状态,消息从窗口系统传递到应用程序,并在应用程序内部传递。这些消息可能包含在某个目的地交付时可能有用的数据。这里讨论的消息在 Qt 中被称为事件。
在本章中,我们将涵盖以下主题:
-
事件
-
事件处理程序
-
拖放
-
自定义信号
事件
在 Qt 中,所有发生的事件都被封装在继承自 QEvent 抽象类的对象中。一个发生事件的例子是窗口被调整大小或移动。应用程序状态的改变将被注意到,并将创建一个适当的 QEvent 对象来表示它。
应用程序的事件循环将此对象传递给继承自 QObject 的某些对象。这个 QEvent 对象将通过调用一个将被调用的方法来处理。
有不同类型的事件。当鼠标被点击时,将创建一个 QMouseEvent 对象来表示这个事件。该对象将包含额外的信息,例如被点击的具体鼠标按钮以及事件发生的位置。
事件处理程序
所有 QObjects 都有一个 event() 方法,它接收事件。对于 QWidgets,此方法将事件对象传递给更具体的事件处理程序。可以通过子类化感兴趣的控件并重新实现该事件处理程序来重新定义事件处理程序应该做什么。
让我们创建一个应用程序,我们将重新实现一个事件处理程序。
创建一个包含 main.cpp、mainwindow.cpp 和 mainwindow.h 文件的文件夹。mainwindow.h 文件应包含以下代码:
#include <QMainWindow>
#include <QMoveEvent>
#include <QMainWindow>
class MainWindow: public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
protected:
void moveEvent(QMoveEvent *event);
};
在前面的代码中,我们只对 QMainWindow 进行了子类化。声明了一个默认构造函数,并重写或重新实现了我们想要重写的事件处理程序,即 moveEvent(QMoveEvent *event) 处理程序。
当窗口被移动时,QMainWindow 对象的 event() 方法将被调用。事件将被进一步封装在 QMoveEvent 对象中,并转发给 moveEvent() 事件处理程序。由于我们感兴趣的是在窗口移动时改变窗口的行为,我们定义了自己的 moveEvent()。
将以下代码行添加到 mainwindow.cpp 中:
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent) : QMainWindow (parent){
setWindowTitle("Locate Window with timer");
}
void MainWindow::moveEvent(QMoveEvent *event) {
int xCord = event->pos().x();
int yCord = event->pos().y();
QString text = QString::number(xCord) + ", " + QString::number(yCord);
statusBar()->showMessage(text);
}
在默认构造函数中,设置了窗口的标题。事件对象携带窗口当前所在位置的坐标。然后调用 event->pos().x() 获取 x 坐标,同样通过调用 event->pos().y() 获取 y 坐标。
我们将 yCord 和 xCord 转换为文本,并存储在 text 中。要访问窗口的状态栏,调用 statusBar() 并将 text 传递给从 statusBar() 调用返回的状态栏对象的 showMessage() 方法。
main.cpp 文件将包含以下代码,如往常一样:
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[]){
QApplication app(argc, argv);
MainWindow window;
window.resize(300, 300);
window.show();
return app.exec();
}
编译并运行应用程序。注意当移动应用程序窗口时状态栏的变化。
这里有两个屏幕截图显示了当窗口移动时,位于窗口底部的状态栏如何变化。
窗口的第一个状态显示在下述屏幕截图:

当窗口被移动时,稍后显示了如下所示的输出:

注意窗口的底部以及其变化。持续移动窗口并观察状态栏的变化。
让我们再写一个例子来提高我们对 Qt 事件的理解。
除了由窗口系统生成的事件外,Qt 还会生成其他事件。以下示例将说明如何让 Qt 在特定间隔发送基于定时器的应用程序事件。
如同往常,我们将从通常创建的三个主要文件开始,即main.cpp、mainwindow.cpp和mainwindow.h。项目基于之前的示例。
在mainwindow.h文件中,插入以下代码行:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QMoveEvent>
#include <QMainWindow>
#include <QStatusBar>
#include <QLabel>
class MainWindow: public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
protected:
void moveEvent(QMoveEvent *event);
void timerEvent(QTimerEvent *event);
private:
QLabel *currentDateTimeLabel;
};
#endif
为了接收定时器事件,我们将实现自己的timerEvent方法,这将作为定时器到期时发出的事件的目的地。这就是添加 void timerEvent(QTimerEvent *event)签名的基本原理。QLabel currentDateTimeLabel实例将用于显示日期和时间。
在mainwindow.cpp文件中,默认构造函数由以下代码定义:
#include <QDateTime>
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent) : QMainWindow (parent){
setWindowTitle("Locate Window");
currentDateTimeLabel = new QLabel("Current Date and Time");
currentDateTimeLabel->setAlignment(Qt::AlignCenter);
setCentralWidget(currentDateTimeLabel);
startTimer(1000);
}
窗口的标题已设置。创建了一个QLabel实例,并通过调用setAlignment确保其内容居中。然后currentDateTimeLabel传递给setCentralWidget()方法。startTimer(1000)方法启动一个定时器,并且每秒触发一个QTimerEvent对象,表示为1000。
对于每一秒,我们现在需要通过重新实现timerEvent()方法来定义应该发生什么。
将以下代码添加到mainwindow.cpp:
void MainWindow::timerEvent(QTimerEvent *event){
Q_UNUSED(event);
QString dateTime = QDateTime::currentDateTime().toString();
currentDateTimeLabel->setText(dateTime);
}
每秒,timerEvent()将被调用并传递一个QTimerEvent实例。Q_UNUSED(event)用于防止编译器抱怨event()没有被以任何方式使用。当前日期和时间的字符串表示形式传递给dateTime并设置为currentDateTimeLabel实例变量的文本。
main.cpp文件与之前相同。作为参考,以下代码再次呈现,如下所示:
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[]){
QApplication app(argc, argv);
MainWindow window;
window.resize(300, 300);
window.show();
return app.exec();
}
编译并运行应用程序,如下所示:

应用程序最初将显示文本、当前日期和时间,但一秒后应更改并显示更新的时间。每过去一秒,文本也会更新。
拖放
在本节中,我们将组合一个简单的应用程序,它可以处理从外部源到应用程序中的拖放操作。
该应用程序是一个小型文本编辑器。当文本文件被拖放到文本区域时,它将打开并将该文本文件的內容插入到文本区域中。窗口的状态将显示文本区域的字符数,这是一个 QTextEdit 的实例。
此示例应用程序还说明了关于事件的一个非常重要的观点。要自定义小部件,必须通过重写其事件处理程序来改变该小部件的现有行为。在尝试自定义小部件时(除了事件外),不考虑信号和槽。
要开始此项目,请执行以下步骤:
-
创建一个您选择的名称的新文件夹
-
创建
main.cpp、mainwindow.cpp、mainwindow.h、dragTextEdit.h和dragTextEdit.cpp文件
dragTextEdit.h 和 dragTextEdit.cpp 文件将包含我们自定义小部件的定义。mainwindow.cpp 和 mainwindow.h 文件将用于构建应用程序。
让我们从自定义的 QTextEdit 小部件开始。将以下代码行插入到 dragTextEdit.h:
#ifndef TEXTEDIT_H
#define TEXTEDIT_H
#include <QMoveEvent>
#include <QMouseEvent>
#include <QDebug>
#include <QDateTime>
#include <QTextEdit>
#include <QMimeData>
#include <QMimeDatabase>
#include <QMimeType>
class DragTextEdit: public QTextEdit
{
Q_OBJECT
public:
explicit DragTextEdit(QWidget *parent = nullptr);
protected:
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override;
void dropEvent(QDropEvent *event) override;
};
#endif
DragTextEdit 自定义小部件继承自 QTextEdit。声明了默认构造函数。为了接受拖放事件,我们需要重写以下方法以确保适当的行为,如下面的代码所示:
protected:
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override;
void dropEvent(QDropEvent *event) override
现在已经创建了头文件,打开 dragTextEdit.cpp 文件并添加默认构造函数的定义,如下面的代码所示:
#include "dragTextEdit.h"
DragTextEdit::DragTextEdit(QWidget *parent) : QTextEdit(parent)
{
setAcceptDrops(true);
}
#include 指令导入头文件,之后定义默认构造函数。为了使我们的小部件能够接受拖放事件,我们需要通过调用 setAcceptDrops(true) 方法来声明这一点。
我们现在必须添加我们想要重写的函数的定义。将以下行添加到 dragTextEdit.cpp:
void DragTextEdit::dragMoveEvent(QDragMoveEvent *event)
{
event->acceptProposedAction();
}
void DragTextEdit::dragLeaveEvent(QDragLeaveEvent *event)
{
event->accept();
}
void DragTextEdit::dragEnterEvent(QDragEnterEvent *event)
{ event->acceptProposedAction();
}
这些事件处理程序处理将要进行拖放操作时涉及的主要步骤。在 dragEnterEvent() 和 dragMoveEvent() 方法中,会调用事件对象上的 acceptProposedAction() 方法。这些事件在拖动模式下的光标位于调用 setAcceptDrops() 方法的窗口边界时被调用。如果您拒绝调用 acceptProposedAction() 方法,拖放行为可能会出现异常。
当光标在感兴趣的小部件内时,会调用 dragMoveEvent() 事件处理程序。但为了定义拖放事件发生时会发生什么,我们需要定义 dropEvent() 处理程序。
将以下代码添加到 dragTextEdit.cpp:
void DragTextEdit::dropEvent(QDropEvent *event)
{
const QMimeData *mimeData = event->mimeData();
if (mimeData->hasText()) {
QTextStream out(stdout);
QFile file(mimeData->urls().at(0).path());
file.open(QFile::ReadOnly | QFile::Text);
QString contents = file.readAll();
setText(contents);
event->acceptProposedAction();
}
else{
event->ignore();
}
}
通过调用event->mimeData()从事件对象中获取文件的 mime 数据。如果它包含文本数据,我们将提取文件的正文并调用QTextEdit的setText()方法。这将用该文本填充DragTextEdit实例。请注意,我们继续调用event->acceptProposedAction()来告诉 Qt 我们已经处理了这个事件。另一方面,如果调用event->ignore(),它被视为不受欢迎的事件或操作,因此被传播到父小部件。
这完成了自定义QTextEdit的实现。现在我们需要创建mainwindow.h和mainwindow.cpp,它们将构建主应用程序窗口并使用DragTextEdit。
创建mainwindow.h文件并插入以下代码:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QLabel>
#include <QMoveEvent>
#include <QMouseEvent>
#include <QVBoxLayout>
#include <QDebug>
#include <QDateTime>
#include <QMainWindow>
#include <QStatusBar>
#include "dragTextEdit.h"
class MainWindow: public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
private slots:
void updateStatusBar();
private:
DragTextEdit *slateDragTextEdit;
};
#endif
导入QMainWindow、QLabel类和其他常用类,以及dragTextEdit.h头文件,这允许包含我们的自定义类。声明一个将在任何文本被添加到或从DragTextEdit小部件中删除时被调用的槽。最后,创建一个DragTextEdit实例。
创建并打开mainwindow.cpp文件,并插入以下代码:
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent) : QMainWindow (parent)
{
QWidget *mainWidget = new QWidget;
QVBoxLayout *layout = new QVBoxLayout;
slateDragTextEdit = new DragTextEdit();
layout->addWidget(slateDragTextEdit);
mainWidget->setLayout(layout);
setCentralWidget(mainWidget);
statusBar()->showMessage(QString::number(0));
connect(slateDragTextEdit, SIGNAL(textChanged()), this, SLOT(updateStatusBar()));
}
void MainWindow::updateStatusBar()
{ int charCount = slateDragTextEdit->toPlainText().count();
statusBar()->showMessage(QString::number(charCount));
}
在构造函数中,创建QWidget和QVBoxLayout对象以容纳主小部件和布局。然后通过调用setCentralWdiget()将此小部件插入,如下面的代码所示:
slateDragTextEdit = new DragTextEdit();
layout->addWidget(slateDragTextEdit);
创建一个DragTextEdit自定义类的实例,并将其传递给slateDragTextEdit。此小部件被添加到我们的主布局中,如下面的代码所示:
statusBar()->showMessage(QString::number(0));
窗口的状态栏设置为0。
每当slateDragTextEdit发出textChanged()信号时,都会调用updateStatusBar()槽。在这个槽中,将从slateDragTextEdit中提取并计算字符。因此,当字符被添加到或从slateDragTextEdit中删除时,状态栏将被更新。
main.cpp文件将只包含以下几行代码来实例化窗口并显示它:
#include <QApplication>
#include <Qt>
#include "mainwindow.h"
int main(int argc, char *argv[]){
QApplication app(argc, argv);
MainWindow window;
window.setWindowTitle("Drag Text Edit");
window.show();
return app.exec();
}
项目结束时,你应该在你的文件夹中有五个(5)个文件。要在命令行中编译项目,请在文件夹内执行以下命令:
% qmake -project
不要忘记将QT += widgets添加到生成的.pro文件中。.pro文件应包含头文件和程序文件。它应该如下所示:
# Input
HEADERS += dragTextEdit.h mainwindow.h
SOURCES += dragTextEdit.cpp main.cpp mainwindow.cpp
继续执行以下命令:
% qmake
% make
% ./program_executable
运行中的程序将如下截图所示:

由于程序执行时没有字符,状态栏将显示 0,如前述截图所示。
在文本区域中输入一些输入,并找出每次按键时状态栏是如何更新的,如下面的截图所示:

本节中的示例说明了文本区域如何接受应用程序外部的项目。将任何文本(.txt)文件或包含文本的任何文件拖放到文本区域,看看其内容是如何用于填充文本框的,如下面的截图所示:

从前面的截图可以看出,包含文本的sometext.txt文件的内容将被粘贴到文本区域中,如下面的截图所示:

通过移除对acceptProposedAction()和accept()的调用进行实验,看看拖放是如何变化的。
本章的最后部分将涉及自定义信号的制作。
自定义信号
在前面的章节中,我们看到了如何使用槽和创建自定义槽来在信号发出时实现一些功能。现在,在本节中,我们将探讨如何创建可以发出并连接到其他槽的自定义信号。
要创建自定义信号,需要声明一个方法签名并使用Q_OBJECT宏将其标记为信号。声明时,信号没有返回类型,但可以接受参数。
让我们通过一个项目来实践一下。像往常一样,应该创建一个包含三个(3)个文件的新文件夹,即main.cpp、mainwindow.cpp和mainwindow.h。
在此示例中,我们将重写mousePressEvent并发出一个自定义信号,该信号将连接到一个槽以在窗口上执行多个更新。
在mainwindow.h文件中,插入以下代码行:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QMoveEvent>
#include <QMouseEvent>
#include <QVBoxLayout>
#include <QDebug>
#include <QDateTime>
#include <QStatusBar>
#include <QLabel>
class MainWindow: public QMainWindow
{
Q_OBJECT
public slots:
void updateMousePosition(QPoint pos);
signals:
void mouseMoved(QPoint pos);
public:
MainWindow(QWidget *parent = 0);
protected:
void mousePressEvent(QMouseEvent *event);
private:
QLabel *mousePosition;
QWidget *windowCentralWidget;
};
#endif
此处自定义信号使用以下行声明:
signals:
void mouseMoved(QPoint pos);
当此信号发出时,它将传递一个QPoint实例作为参数。如果我们不想我们的信号传递任何参数,它将用void mouseMoved()编写。
自定义信号不应返回任何内容。当我们在mousePressEvent()处理程序中重新实现时,将发出信号。
void updateMousePosition(QPoint pos),槽将连接到自定义信号。其定义可在mainwindow.cpp中找到。
成员指针mousePosition将在鼠标点击时显示鼠标的坐标。
在mainwindow.cpp文件中,我们将定义三个(3)方法。这些是默认构造函数、槽updateMousePosition()和mousePressEvent()重写方法,如下面的代码所示:
#include "mainwindow.h"
void MainWindow::mousePressEvent(QMouseEvent *event){
emit mouseMoved(event->pos());
}
include语句必须位于文件的顶部。在此override方法中,我们通过调用event->pos()来获取鼠标按下事件生成的坐标。
通过调用x()和y()分别获得x和y坐标。
emit mouseMoved(event->pos())行用于发出在头文件中声明的信号。此外,event->pos()将返回一个QPoint对象,该对象符合信号的签名。
下面的截图显示了在mainwindow.cpp文件中定义的槽:
void MainWindow::updateMousePosition(QPoint point){
int xCord = point.x();
int yCord = point.y();
QString text = QString::number(xCord) + ", " + QString::number(yCord);
mousePosition->setText(text);
statusBar()->showMessage(text);
}
插槽接收 QPoint 实例作为参数。通过调用 point.x() 和 point.y() 分别获取其 x 和 y 坐标。使用 QString 实例 text 将两个值 xCord 和 yCord 连接到一个更长的字符串中。
将使用 QLabel 实例 mousePosition 通过调用其 setText() 方法来显示此坐标。同样,可以通过调用 statusBar()->showMessage(text) 来设置窗口的状态栏。
要将自定义信号连接到我们的插槽进行管道连接,我们需要定义默认构造函数。将以下行添加到 mainwindow.cpp 中:
MainWindow::MainWindow(QWidget *parent) : QMainWindow (parent){
windowCentralWidget = new QWidget();
mousePosition = new QLabel("Mouse Position");
QVBoxLayout *innerLayout = new QVBoxLayout();
innerLayout->addWidget(mousePosition);
windowCentralWidget->setLayout(innerLayout);
setCentralWidget(windowCentralWidget);
statusBar()->showMessage("Ready");
connect(this, SIGNAL(mouseMoved(QPoint)), this, SLOT(updateMousePosition(QPoint)));
}
如我们之前所做的那样,windowCentralWidget 被用作我们应用程序中的主小部件。向其布局 innerLayout 中添加了 QLabel,并将状态栏的初始值设置为 "Ready"。
将 mouseMoved(QPoint) 信号连接到 updateMousePosition(QPoint) 插槽。
在 main.cpp 文件中,我们将实例化我们的窗口并启动主事件循环,如下面的代码所示:
#include <QApplication>
#include <Qt>
#include "mainwindow.h"
int main(int argc, char *argv[]){
QApplication app(argc, argv);
MainWindow window;
window.resize(300, 300);
window.setWindowTitle("Hover Events");
window.show();
return app.exec();
}
按照以下截图编译并运行可执行文件:

状态栏显示 "Ready",而组成窗口主小部件的 QLabel 显示 "鼠标位置"。现在,点击窗口内的任何部分,并查看状态栏和标签更改以显示鼠标点击处的坐标。
以下截图为例:

光标的位置是 145,157,其中 145 位于 x 轴上,157 位于 y 轴上。当光标移动时,此值不会改变。然而,当鼠标点击时,将发出 mouseMoved() 信号,并带有坐标以更新屏幕。
摘要
本章进一步阐述了如何在 Qt 中使用事件。我们了解了在不同情况下使用事件而不是信号-槽机制的不同情况。最初的例子涉及如何覆盖和实现自定义事件处理程序。我们实现的事件捕获了窗口的位置,并在示例应用程序中重新定义了每秒应发生的事情。
在拖放操作中,借助事件,我们还实现了一个简单的放下事件,创建了一个简单的文本编辑器来接受在文本区域中放下的文件。最后,本章说明了如何创建在事件发生时发出的自定义信号。
在 第六章,将 Qt 与数据库连接,我们将关注在构建 Qt 应用程序时存储数据和检索数据的各种方法。
第六章:将 Qt 与数据库连接
近年来,大多数应用程序都会集成一些数据库来存储信息,以便进行进一步处理和未来使用。
Qt 附带了一些模块和类,使得连接到数据库变得轻而易举。本章将使用 MySql 数据库来演示示例,但相同的原理也适用于其他数据库。
到本章结束时,你应该能够执行以下操作:
-
连接到数据库并读取
-
通过小部件显示和编辑数据库条目
QtSql
QtSql 模块配备了访问数据库的类和驱动程序。要继续前进,你应该在系统上安装 Qt 时进行了必要的配置,以启用数据库访问。
对于使用 Homebrew 的 macOS 用户,请记住按照第一章,“Qt 5 简介”中先前描述的命令执行。
对于 Linux 用户,必须在编译时安装模块并启用正确的标志,以便 QtSql 模块能够工作,但大多数情况下,第一章,“Qt 5 简介”中的说明应该足够。
QtSql 模块由以下层组成:
-
用户界面层
-
SQL API 层
-
驱动层

每个级别都使用了类,如前图所示。
建立连接
我们需要为编写我们的应用程序奠定基础,在这种情况下,我们需要有一个运行中的 MySql 实例。XAMPP 是一个很好的候选者,可以快速访问一个工作数据库。
XAMPP 是由 Apache Friends 开发的一个免费开源、跨平台的 Web 服务器解决方案堆栈包,主要由 Apache HTTP 服务器、MariaDB(或 MySql)数据库以及用于 PHP 和 Perl 编程语言脚本的解释器组成。从www.apachefriends.org/download.html下载最新版本。
让我们通过以下语句创建以下表的数据库:
use contact_db;
CREATE TABLE IF NOT EXISTS contacts (
id INT AUTO_INCREMENT,
last_name VARCHAR(255) NOT NULL,
first_name VARCHAR(255) NOT NULL,
phone_number VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=INNODB;
数据库名为contact_db,假设你已经在安装的 MySql 实例中创建了它。
SQL 语句创建了一个名为contacts的表,其中包含一个自增的id字段,以及存储字符的last_name、first_name和phone_number字段。
现在,创建一个新的文件夹,并添加一个名为main.cpp的文件。插入以下代码行:
#include <QApplication>
#include <QtSql>
#include <QDebug>
/*
use contact_db;
CREATE TABLE IF NOT EXISTS contacts (
id INT AUTO_INCREMENT,
last_name VARCHAR(255) NOT NULL,
first_name VARCHAR(255) NOT NULL,
phone_number VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=INNODB;
*/
int main(int argc, char *argv[]) {
// Setup db connection
QSqlDatabase db_conn =
QSqlDatabase::addDatabase("QMYSQL", "contact_db");
db_conn.setHostName("127.0.0.1");
db_conn.setDatabaseName("contact_db");
db_conn.setUserName("root");
db_conn.setPassword("");
db_conn.setPort(3306);
// Error checks
if (!db_conn.open()) {
qDebug() << db_conn.lastError();
return 1;
} else {
qDebug() << "Database connection established !";
}
}
要建立数据库连接,我们需要包含QtSql。QDebug提供了一个输出流,我们可以在开发过程中将有用的(调试)信息写入文件、设备或标准输出。
在前面的代码中,数据库表的结构已被注释掉,但作为提醒,以防你没有创建它。
要打开到数据库的连接,将调用QSqlDatabase::addDatabase()。QMYSQL参数是驱动程序类型,contact_db是连接名称。一个程序可以与同一数据库有多个连接。此外,addDatabase()调用将返回一个QSqlDatabase实例,本质上就是数据库的连接。
此连接db_conn随后使用使连接工作的参数进行初始化。主机名、我们想要连接的特定数据库、用户名、密码和端口号都设置在数据库连接对象db_conn上:
db_conn.setHostName("127.0.0.1");
db_conn.setDatabaseName("contact_db");
db_conn.setUserName("root");
db_conn.setPassword("");
db_conn.setPort(3306);
根据不同的情况,您可能需要指定比这些参数更多的信息才能访问数据库,但大多数情况下,这应该可以工作。此外,请注意,密码是一个空字符串。这只是为了说明目的。您必须根据您的数据库更改密码。
为了建立连接,我们需要在连接对象上调用open():
// Error checks
if (!db_conn.open()) {
qDebug() << db_conn.lastError();
return 1;
} else {
qDebug() << "Database connection established !";
}
调用open()将返回一个布尔值以确定数据库连接是否成功。!db_conn.open()测试返回值是否为False。
注意我们编译和运行此程序的方式。
在您位于main.cpp文件所在的文件夹中时,在命令行上执行以下操作:
% qmake -project
打开生成的.pro文件,并添加以下行:
QT += widgets sql
我们打算在本章中使用小部件,因此它已被列为第一个要包含的模块。同样,我们也包括 SQL 模块。继续以下命令:
% qmake
% make
% ./program_executable
如果您收到Database connection established!的响应,那么这意味着您的程序能够顺利地连接到数据库。另一方面,您可能会收到一个错误,该错误将描述连接无法建立的原因。当您遇到错误时,请通过以下列表确保您处于正确的路径:
-
确保数据库服务正在运行
-
确保您尝试连接的数据库实际上存在
-
确保由模式给出的表存在
-
确保数据库的用户名和密码存在
-
确保 Qt 已编译了 MySql 模块
现在,让我们更新程序,以便我们可以展示如何在 Qt 中发出各种 SQL 语句。
列出记录
为了对数据库执行查询语句,我们将使用QSqlQuery类。这些语句包括数据更改语句,如INSERT、SELECT和UPDATE。也可以发出数据定义语句,如CREATE TABLE。
考虑以下代码片段以列出联系人表中的所有条目:
QSqlQuery statement("SELECT * FROM contacts", db_conn);
QSqlRecord record = statement.record();
while (statement.next()){
QString firstName = statement.value(record.indexOf("first_name")).toString();
QString lastName = statement.value(record.indexOf("last_name")).toString();
QString phoneNumber = statement.value(record.indexOf("phone_number")).toString();
qDebug() << firstName << " - " << lastName << " - " << phoneNumber;
}
查询语句和数据库连接作为参数传递给QSqlQuery语句的一个实例。QSqlRecord用于封装数据库行或视图。我们将使用其实例record来获取行中列的索引。statement.record()返回当前查询的字段信息。
如果statement中有任何与查询匹配的行,statement.next()将允许我们遍历返回的行。我们可以调用previous()、first()和last()来使我们能够前后移动返回的行或数据。
对于每行通过调用statement.next()返回并访问的行,使用statement对象根据代码statement.value(0).toString()获取其对应的数据。这应该返回第一列转换为字符串以存储在firstName中。而不是这种方法,我们可以使用record来获取我们感兴趣的列的索引。因此,为了提取姓名列,我们编写statement.value(record.indexOf("first_name")).toString()。
qDebug()调用有助于打印出firstName、lastName和phoneNumber中的数据,类似于我们使用cout所做的那样。
INSERT操作
要执行数据库操作并将数据存储到数据库中,有几种方式可以发出INSERT语句。
考虑 Qt 中INSERT操作的一种形式:
// Insert new contacts
QSqlQuery insert_statement(db_conn);
insert_statement.prepare("INSERT INTO contacts (last_name, first_name, phone_number)"
"VALUES (?, ?, ?)");
insert_statement.addBindValue("Sidle");
insert_statement.addBindValue("Sara");
insert_statement.addBindValue("+14495849555");
insert_statement.exec();
QSqlQuery对象insert_statement通过传递数据库连接来实例化。接下来,将INSERT语句字符串传递给prepare()方法的调用。注意,我们如何使用三个(3)?, ?, ?(问号)来使我们的语句不完整。这些问号将被用作占位符。为了填充这些占位符,将调用addBindValue()方法。insert_statement.addBindValue("Sidle")这一行将用于填充contacts表中的last_name列中的数据。对addBindValue("Sara")的第二次调用将用于填充第二个占位符。
要执行语句,必须调用insert_statement.exec()。整体效果是在表中插入一条新记录。
要改变插入数据顺序,我们可以使用insert_statement.bindValue()函数。INSERT语句有三个(3)位置占位符,编号从0到2。我们可以首先填充最后一个占位符,如下所示:
insert_statement.prepare("INSERT INTO contacts (last_name, first_name, phone_number)"
"VALUES (?, ?, ?)");
insert_statement.bindValue(2, "+144758849555");
insert_statement.bindValue(1, "Brass");
insert_statement.bindValue(0, "Jim");
insert_statement.exec();
电话号码列的占位符首先通过指定bind(2, "+144758849555")来填充,其中2是(phone_number)占位符的索引。
使用占位符的位置作为替代方案的一种方法是命名它们。考虑以下INSERT语句:
insert_statement.prepare("INSERT INTO contacts (last_name, first_name, phone_number)"
"VALUES (:last_name, :first_name, :phone_number)");
insert_statement.bindValue(":last_name", "Brown");
insert_statement.bindValue(":first_name", "Warrick");
insert_statement.bindValue(":phone_number", "+7494588594");
insert_statement.exec();
在完成 SQL 语句时,而不是使用占位符的位置索引,使用命名占位符来引用VALUES部分中的数据。这样,占位符的名称与相应的值一起传递到每个bindValue()调用。
要持久化数据,必须调用insert_statement.exec()函数。
DELETE操作
DELETE操作是可以在表上执行的其他操作之一。为此,我们需要传递数据库连接的引用,并将DELETE语句传递给QSqlQuery的exec()方法。
考虑以下代码片段:
// Delete a record
QSqlQuery delete_statement(db_conn);
delete_statement.exec("DELETE FROM contacts WHERE first_name = 'Warrick'");
qDebug() << "Number of rows affected: " << delete_statement.numRowsAffected();
numRowsAffected() 是一个用于确定影响了多少记录的方法。这个方法的一个好处是它有助于确定我们的查询是否改变了数据库。如果它返回 -1,则意味着查询的操作产生了不确定的结果。
更新操作
UPDATE 操作遵循与 DELETE 操作相同的逻辑。考虑以下代码行:
// Update a record
QSqlQuery update_statement(db_conn);
update_statement.exec("UPDATE contacts SET first_name='Jude' WHERE id=1 ");
qDebug() << "Number of rows affected: " << update_statement.numRowsAffected();
此处的语句将 ID 为 1 的记录的 first_name 设置为 'Jude'。update_statement.numRowsAffected() 将返回空值,尤其是在表中的第一条记录 id=1 缺失的情况下。请特别注意这一点。
以下概述了展示主要操作的完整程序:
#include <QApplication>
#include <QtSql>
#include <QDebug>
int main(int argc, char *argv[]) {
// Setup db connection
QSqlDatabase db_conn =
QSqlDatabase::addDatabase("QMYSQL", "contact_db");
db_conn.setHostName("127.0.0.1");
db_conn.setDatabaseName("contact_db");
db_conn.setUserName("root");
db_conn.setPassword("");
db_conn.setPort(3306);
// Error checks
if (!db_conn.open()) {
qDebug() << db_conn.lastError();
return 1;
} else {
qDebug() << "Database connection established !";
}
// Create table
QString table_definition = "use contact_db;\n"
" CREATE TABLE IF NOT EXISTS contacts (\n"
" id INT AUTO_INCREMENT,\n"
" last_name VARCHAR(255) NOT NULL,\n"
" first_name VARCHAR(255) NOT NULL,\n"
" phone_number VARCHAR(255) NOT NULL,\n"
" PRIMARY KEY (id)\n"
") ENGINE=INNODB;";
QSqlQuery table_creator(table_definition, db_conn);
// Issue SELECT statement
QSqlQuery statement("SELECT * FROM contacts", db_conn);
QSqlRecord record = statement.record();
while (statement.next()){
QString firstName =
statement.value(record.indexOf("first_name")).toString();
QString lastName =
statement.value(record.indexOf("last_name")).toString();
QString phoneNumber =
statement.value(record.indexOf("phone_number")).toString();
qDebug() << firstName << " - " << lastName << " - " <<
phoneNumber;
}
// Insert new contacts
QSqlQuery insert_statement(db_conn);
insert_statement.prepare("INSERT INTO contacts (last_name,
first_name, phone_number)"
"VALUES (?, ?, ?)");
insert_statement.addBindValue("Sidle");
insert_statement.addBindValue("Sara");
insert_statement.addBindValue("+14495849555");
insert_statement.exec();
//QSqlQuery insert_statement(db_conn);
insert_statement.prepare("INSERT INTO contacts (last_name,
first_name, phone_number)"
"VALUES (?, ?, ?)");
insert_statement.bindValue(2, "+144758849555");
insert_statement.bindValue(1, "Brass");
insert_statement.bindValue(0, "Jim");
insert_statement.exec();
insert_statement.prepare("INSERT INTO contacts (last_name,
first_name, phone_number)"
"VALUES (:last_name, :first_name,
:phone_number)");
insert_statement.bindValue(":last_name", "Brown");
insert_statement.bindValue(":first_name", "Warrick");
insert_statement.bindValue(":phone_number", "+7494588594");
insert_statement.exec();
// Delete a record
QSqlQuery delete_statement(db_conn);
delete_statement.exec("DELETE FROM contacts WHERE first_name =
'Warrick'");
qDebug() << "Number of rows affected: " <<
delete_statement.numRowsAffected();
// Update a record
QSqlQuery update_statement(db_conn);
update_statement.exec("UPDATE contacts SET first_name='Jude' WHERE
id=1 ");
qDebug() << "Number of rows affected: " <<
update_statement.numRowsAffected();
}
特别重要的是数据库表的创建方式。从前面的代码列表中,QString 实例 table_definition 保存了我们即将创建的表的结构。当 table_definition 和数据库连接传递给 QSqlQuery 的一个实例时,表就创建了。这就是创建表的全部过程。
编译并运行程序。
记得编辑 .pro 文件以包含 sql 模块。
从命令行运行程序的典型输出如下所示:
./dbBasics.app/Contents/MacOS/dbBasics
Database connection established !
"Jude" - "Sidle" - "+14495849555"
"Brass" - "Jim" - "+144758849555"
Number of rows affected: 1
Number of rows affected: 0
使用数据模型进行数据库访问
有两个类可以用于访问数据库。这些是 QSqlTableModel 和 QSqlQueryModel 类。QSqlQueryModel 类仅向数据库提供只读模型。QSqlTableModel 提供对数据库的读写模型访问。
在应用程序开发中,你面临着一个挑战,那就是如何展示数据,以及如何维护数据与展示(视图)之间的关系,以便数据的变化反映在视图中。
在 PHP 语言的早期,数据、展示和业务逻辑都混杂在一个或多个脚本中。这使得调试和最终的代码维护变得噩梦般。这种困境有时也会出现在语言和框架设计中。
模型-视图-控制器(MVC)方法试图解决这个问题。它认识到软件的一个关键部分是数据。通过认识到这一点,它将数据抽象为所谓的模型。模型基本上是软件中数据的表示。这些数据可以是字符串或整数的列表。它也可以是父文件夹下的文件夹和文件。数据也可以是从对数据库的查询返回的行列表。
获得的数据需要显示或呈现给用户。通过这些组件传输数据的组件称为视图。例如,显示学生名单列表的 HTML 页面可以称为视图。在 Qt 中,有许多小部件可以用于在模型中显示数据。以下是一些典型的数据展示视图:

这些视图类针对显示信息进行了优化,因此当它们与模型关联时,模型的变化将导致视图自动更新。视图维护自己的状态,并在模型发生变化时得到通知。
例如,当在 QListView 中显示姓名列表时,对模型调用 remove() 将同时从模型列表中删除项目,并通过减少显示的项目数量来更新视图。
与直接编写代码来更新视图不同,视图类会代表我们这样做。让我们创建一个示例项目,该项目将使用模型从数据库中访问数据:
创建一个新的文件夹,并在其中创建一个名为 main.cpp 的文件。将以下代码行复制到 main.cpp 文件中:
#include <QtSql>
#include <QDebug>
/*
int main(int argc, char *argv[])
{
// Setup db connection
QSqlDatabase db_conn =
QSqlDatabase::addDatabase("QMYSQL", "contact_db");
db_conn.setHostName("127.0.0.1");
db_conn.setDatabaseName("contact_db");
db_conn.setUserName("root");
db_conn.setPassword("");
db_conn.setPort(3306);
// Error checks
if (!db_conn.open()) {
qDebug() << db_conn.lastError(); return 1;
}
// Use Database model
QSqlTableModel *contactsTableModel = new QSqlTableModel(0, db_conn);
contactsTableModel->setTable("contacts");
contactsTableModel->select();
for (int i = 0; i < contactsTableModel->rowCount(); ++i) {
QSqlRecord record = contactsTableModel->record(i);
QString id = record.value("id").toString();
QString last_name = record.value("last_name").toString();
QString first_name = record.value("first_name").toString();
QString phone_number = record.value("phone_number").toString();
qDebug() << id << " : " << first_name << " : " << last_name << " : " << phone_number;
}
// Insert Row
int row = contactsTableModel->rowCount();
contactsTableModel->insertRows(row, 1);
contactsTableModel->setData(contactsTableModel->index(row, 1), "Stokes");
contactsTableModel->setData(contactsTableModel->index(row, 2), "Nick");
contactsTableModel->setData(contactsTableModel->index(row, 3), "+443569948");
contactsTableModel->submitAll();
// Custom filter
qDebug() << "\nCustom filter: \n";
contactsTableModel->setFilter("id=12 AND last_name like'Stokes'");
contactsTableModel->select();
for (int i = 0; i < contactsTableModel->rowCount(); ++i) {
QSqlRecord record = contactsTableModel->record(i);
QString id = record.value("id").toString();
QString last_name = record.value("last_name").toString();
QString first_name = record.value("first_name").toString();
QString phone_number = record.value("phone_number").toString();
qDebug() << id << " : " << first_name << " : " << last_name << " : " << phone_number;
}
}
该程序的目的在于连接到数据库,列出特定表中的行,并对它发出一个 SELECT 语句。
在建立数据库连接后,我们使用以下行创建 QSqlTableModel 实例:QSqlTableModel *contactsTableModel = new QSqlTableModel(0, db_conn);。这个实例接收一个指向父对象的指针和数据库连接。这个 QSqlTableModel 模型也允许编辑表中的行。
要选择我们想要操作的数据库中的表,在 contactsTableModel 上调用 setTable() 方法。将 contacts 字符串传递为表名。
要用表中的信息填充 contactsTableModel 模型,发出一个 select() 调用。现在使用循环遍历模型中的数据:
for (int i = 0; i < contactsTableModel->rowCount(); ++i) {
QSqlRecord record = contactsTableModel->record(i);
QString id = record.value("id").toString();
QString last_name = record.value("last_name").toString();
QString first_name = record.value("first_name").toString();
QString phone_number = record.value("phone_number").toString();
qDebug() << id << " : " << first_name << " : " << last_name << " : " << phone_number;
}
使用索引获取表中的每一行。这里的索引 0 指的是模型中的第一个项目。这个索引与表中的 主键 无关。它是一种简单地引用表中行的方法。
rowCount() 方法很有用,因为它有助于了解与最新的 SELECT 语句关联的总行数。
要获取表中的每一行,循环中的索引 i 被传递给 contactsTableModel->record(i)。QSqlRecord 实例将持有对表中行的引用,该行是通过调用 record(i) 返回的。
对于每一行,通过传递列名给 value 获取交叉列中存储的值。因此,record.value("id") 将返回存储在联系人表 id 列中的值。toString() 将输出作为字符串返回。相同的调用用于获取表中每一行(QSqlRecord record)的 last_name、first_name 和 phone_number 的值。
然后使用 qDebug() 语句输出每一行的所有值。
由于 QSqlTableModel 允许编辑表,以下语句插入了一个包含数据的新的行:
// Insert Row
int row = contactsTableModel->rowCount();
contactsTableModel->insertRows(row, 1);
contactsTableModel->setData(contactsTableModel->index(row, 1), "Stokes");
contactsTableModel->setData(contactsTableModel->index(row, 2), "Nick");
contactsTableModel->setData(contactsTableModel->index(row, 3), "+443569948");
contactsTableModel->submitAll();
通过调用rowCount()获取表中的总项数。要向表中插入单行,请调用insertRows(row, 1)。这里的单行由位置row处的1表示。
在列1中,新行的last_name列在调用setData()后获得值"Stokes"。contactsTableModel->index(row,1)代表插入"Stokes"的索引位置。
为了持久化数据,将发出对submitAll()的调用。这将把内存中任何悬而未决的更改写入数据库。
注意,此时模型已成为访问数据库中数据的接口。我们也不需要知道应用程序与不同类型的数据库交互时,语句映射到的特定查询。这是一个巨大的优势。
如果此模型与视图相关联,则新插入的行将自动填充到屏幕上,而无需执行此类操作的任何代码。
为了细化选择语句,使用setFilter()方法:
// Custom filter
qDebug() << "\nCustom filter: \n";
contactsTableModel->setFilter("id=12 AND last_name like 'Stokes'");
contactsTableModel->select();
SQL 语句的WHERE子句部分是传递给setFilter()的。在这种情况下,WHERE子句是从表中选择id等于12且last_name字段为'Stokes'的行。
要应用过滤器,请在contactsTableModel上调用select()方法。然后循环用于遍历结果。
编译并运行项目:
% qmake -project
请确保在.pro文件中包含以下行:
QT += sql widgets
编译并运行项目:
% qmake
% make
% ./executable_file
显示模型
在上一节中,我们看到了如何使用模型作为抽象来访问数据库。现在,我们将尝试将其与用于显示的模型相链接。使用上一节中的代码列表,修改main.cpp如下所示:
#include <QApplication>
#include <QtSql>
#include <QVBoxLayout>
#include <QPushButton>
#include <QDebug>
#include <Qt>
#include <QTableView>
#include <QHeaderView>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// Setup db connection
QSqlDatabase db_conn =
QSqlDatabase::addDatabase("QMYSQL", "contact_db");
db_conn.setHostName("127.0.0.1");
db_conn.setDatabaseName("contact_db");
db_conn.setUserName("root");
db_conn.setPassword("");
db_conn.setPort(3306);
// Error checks
if (!db_conn.open()) {
qDebug() << db_conn.lastError(); return 1;
}
由于我们想要显示模型,因此已包含小部件类。数据库连接保持不变。
现在,将以下代码行添加到main.cpp中:
enum {
ID = 0,
LastName = 1,
FirstName = 2,
PhoneNumber = 3,
};
QSqlTableModel *contactsTableModel = new QSqlTableModel(0, db_conn);
contactsTableModel->setTable("contacts");
contactsTableModel->select();
contactsTableModel->setHeaderData(ID, Qt::Horizontal, QObject::tr("ID"));
contactsTableModel->setHeaderData(LastName, Qt::Horizontal, QObject::tr("Last Name"));
contactsTableModel->setHeaderData(FirstName, Qt::Horizontal, QObject::tr("First Name"));
contactsTableModel->setHeaderData(PhoneNumber, Qt::Horizontal, QObject::tr("Phone Number"));
contactsTableModel->setEditStrategy(
QSqlTableModel::OnManualSubmit);
与使用如0、1等魔法数字相比,枚举提供了一些上下文,用于常数0、1等。
使用连接对象db_conn创建QSqlTableModel的实例。选择数据库表contacts进行操作。当模型被显示时,使用标题来标记列。为此,我们传递枚举值和列应显示的名称。例如,调用setHeaderData(FirstName, Qt::Horizontal, QObject::tr("First Name"))将第一列FirstName(其实际值为 0)设置为水平显示"First Name"。
我们说过,模型-视图概念有一个额外的优点,即对视图所做的更改可以反映在数据库中,而无需编写额外的代码:
contactsTableModel->setEditStrategy(
QSqlTableModel::OnManualSubmit);
前面的行规定,不应将视图显示的数据更改传播到数据库。相反,应有一个独立的过程触发视图与数据库中数据的同步。与手动进行同步过程相比,替换已被注释掉的代码:
//contactsTableModel->setEditStrategy(
// QSqlTableModel::OnRowChange);
setEditStrategy(QSqlTableModel::OnRowChange) 的意思是,通过视图对数据进行更改时,当行中的数据发生变化时,这些更改将反映在数据库中。我们将在运行完成的程序时看到更多关于这一点的内容。
由于我们已经创建了模型,现在是时候添加视图了。将以下代码行添加到 main.cpp 中:
//contactsTableModel->setEditStrategy(
// QSqlTableModel::OnRowChange);
// continue from here ...
QTableView *contactsTableView = new QTableView();
contactsTableView->setModel(contactsTableModel);
contactsTableView->setSelectionMode(QAbstractItemView::SingleSelection);
contactsTableView->setSelectionBehavior(QAbstractItemView::SelectRows);
QHeaderView *header = contactsTableView->horizontalHeader();
header->setStretchLastSection(true);
为了显示数据库表中的条目,这里使用了视图类 QTableView。QTableView 类特别之处在于它是一个实现了模型和视图一体化的类。这意味着在内部,这个类有一个内部模型,可以插入数据以供显示。就我们的目的而言,我们将替换这个模型。
QTableView 以表格形式呈现数据,具有行和列。我们选择使用这个视图,因为它类似于关系数据库中数据的组织方式。
实例化 QTableView 后,我们将模型设置为 contactsTableModel,这是我们通过调用 setModel() 方法创建的模型。
当调用 setSelectionMode() 方法时,表格中项目的选择被限制为单个项目。如果我们想允许在表格中进行多选,则应将 QAbstractItemView::MultiSelection 常量传递给 setSelectionMode()。在这种情况下,选择是通过点击并拖动鼠标到表格中你感兴趣的项目来进行的。
为了指定可以选择的内容,将 QAbstractItemView::SelectRows 常量传递给 setSelectionBehavior()。这个常量允许只选择整个行。
当 QTableView 渲染时,在小部件右侧有未使用的空间。
以下截图展示了这个问题:

考虑一下标记为“空空间”的区域在界面中呈现的巨大缺口。
为了使最后一列扩展以填充包含的小部件,我们需要获取 QTableView 的标题对象实例,并将所需的属性 setStretchLastSection() 设置为 true,如下面的代码所示:
QHeaderView *header = contactsTableView->horizontalHeader();
header->setStretchLastSection(true);
在这一点上,我们需要为应用程序构建一个简单的窗口和布局。将以下行添加到 main.cpp 中:
QWidget window;
QVBoxLayout *layout = new QVBoxLayout();
QPushButton *saveToDbPushButton = new QPushButton("Save Changes");
layout->addWidget(contactsTableView);
layout->addWidget(saveToDbPushButton);
QVBoxLayout 实例将作为应用程序窗口的主要布局。对表格条目的更改不会持久化到数据库。我们有意这样做,以便使用按钮手动将更改写入数据库。因此,创建了一个 QPushButton 实例。将表格和按钮添加到布局对象中。
main.cpp 的最后一行代码如下:
QObject::connect(saveToDbPushButton, SIGNAL(clicked()), contactsTableModel, SLOT(submitAll()));
window.setLayout(layout);
window.show();
return app.exec();
}
saveToDbPushButton 对象的 clicked() 信号连接到了模型 contactsTableModel 的 submitAll() 插槽。在应用程序中修改表格中的条目后,点击推送按钮会将更改写入数据库。
其余的代码与以往一样。
要编译应用程序,执行以下命令:
% qmake -project
确保在 .pro 文件中的 QT 变量具有以下行:
QT += widgets sql
继续执行以下命令:
% qmake
% make
% ./name_of_executable
假设联系人表不为空,应用程序的输出将填充表格中的列表:

注意最后一列已经扩展到了窗口的边缘。从前面的屏幕截图可以看到已经持久化到数据库中的数据。双击任何单元格并编辑其内容。点击“保存更改”按钮。当你访问数据库时,你会看到应用程序中的更改已经反映在应用中。
摘要
本章说明了在开发 Qt 应用程序时如何连接到数据库。我们学习了如何使用模型作为操作数据库中数据的抽象。最后,借助 Model-View 类展示了数据库表中的信息。这些类使得提取数据以供显示变得容易,同时允许在视图中做出的更改传播到数据库。


浙公网安备 33010602011771号