QT6-C---GUI-编程秘籍-全-
QT6 C++ GUI 编程秘籍(全)
原文:
zh.annas-archive.org/md5/abba4d8c7c68545e3294fde74215b6c4译者:飞龙
前言
随着开发多目标和屏幕 GUI 的需求不断增长,提高应用程序的视觉质量变得重要,以便在竞争中脱颖而出。凭借其跨平台能力和最新的 UI 范式,Qt 使得为你的应用程序构建直观、交互式和用户友好的用户界面成为可能。
《Qt 6 C++ GUI 编程食谱,第三版》教你如何使用最新版本的 QT6 和 C++开发功能强大且吸引人的用户界面。这本书将帮助你学习各种主题,如 GUI 定制和动画、图形渲染和实现 Google Maps。你还将探索异步编程、使用信号和槽的事件处理、网络编程以及优化应用程序的各个方面。
到本书结束时,你将自信地设计和定制满足客户期望的 GUI 应用程序,并了解解决常见问题的最佳实践方案。
本书面向对象
这本中级水平的书是为那些想使用 Qt 6 开发软件的人设计的。如果你想提高你软件应用程序的视觉质量和内容展示,这本书适合你。需要具备 C++编程经验。
本书涵盖内容
第一章,使用 Qt Designer 进行外观和感觉定制,展示了如何使用 Qt Creator 和 Qt Design Studio 来设计你的程序的用户界面。
第二章,事件处理 - 信号和槽,涵盖了与 Qt 6 提供的信号和槽机制相关的主题,该机制允许你轻松处理程序的事件回调。
第三章,使用 Qt 和 QML 的状态和动画,解释了如何通过启用状态机框架和动画框架来动画化你的用户界面小部件。
第四章,QPainter 和 2D 图形,介绍了如何使用 Qt 内置类在屏幕上绘制矢量形状和位图图像。
第五章,OpenGL 实现,演示了如何通过将 OpenGL 集成到你的 Qt 项目中,在你的程序中渲染 3D 图形。
第六章,从 Qt5 迁移到 Qt6,介绍了如何将你的 Qt 5 项目迁移到 Qt 6,并讨论了两个版本之间的差异。
第七章,使用网络和管理大型文档,展示了如何设置 FTP 文件服务器,然后创建一个程序帮助你将文件传输到和从服务器。
第八章,线程基础 - 异步编程,介绍了如何在 Qt 6 应用程序中创建多线程进程,并同时运行它们以处理繁重的计算。
第九章,使用 Qt 6 构建触摸屏应用程序,解释了如何创建在触摸屏设备上运行的程序。
第十章,轻松解析 JSON,展示了如何处理 JSON 格式的数据,并使用 Google 地理编码 API 与之结合创建一个简单的地址查找器。
第十一章,转换库,介绍了如何使用 Qt 内置类以及第三方程序在不同变量类型、图像格式和视频格式之间进行转换。
第十二章,使用 SQL 驱动器和 Qt 访问数据库,解释了如何使用 Qt 将您的程序连接到 SQL 数据库。
第十三章,使用 Qt WebEngine 开发 Web 应用程序,介绍了如何使用 Qt 提供的网页渲染引擎开发能够利用网络技术的程序。
第十四章,性能优化,向您展示了如何优化您的 Qt 6 应用程序并加快其处理速度。
为了充分利用本书
您将需要以下软件/硬件来尝试本书中的学习内容:
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Qt Creator 12.0.2 | Windows、macOS 或 Linux |
| Qt 设计工作室 | Windows、macOS 或 Linux |
| SQLiteStudio | Windows、macOS 或 Linux |
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件 github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在 github.com/PacktPublishing/ 获取。查看它们吧!
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“一个名为 on_pushButton_clicked() 的槽函数现在将出现在 mainwindow.h 和 mainwindow.cpp 中。”
代码块应如下设置:
import QtQuick
import QtQuick.Window
Window {
visible: true
width: 640
title: qsTr("Hello World")
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
width: 128;
height: 128;
x: -128;
y: parent.height / 2;
任何命令行输入或输出都应如下编写:
find_package(Qt6 REQUIRED COMPONENTS Network)
target_link_libraries(mytarget PRIVATE Qt6::Network)
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“在项目窗口下选择应用程序(Qt),然后选择Qt 小部件应用程序。”
小贴士或重要笔记
看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata 并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packtpub.com 并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问 authors.packtpub.com。
分享您的想法
一旦您阅读了 Qt 6 C++ GUI Programming Cookbook,我们很乐意听到您的想法!请 点击此处直接进入此书的亚马逊评论页面 并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买此书!
您喜欢在旅途中阅读,但无法随身携带印刷书籍吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在,随着每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日访问权限
按照以下简单步骤获取优惠:
- 扫描二维码或访问以下链接

packt.link/free-ebook/978-1-80512-263-0
-
提交您的购买证明
-
就这些!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件。
第一章:使用 Qt Designer 进行外观和感觉定制
Qt 6 允许我们通过大多数人熟悉的方法轻松设计程序的用户界面。Qt 不仅为我们提供了一个强大的用户界面工具包,称为 Qt Designer,它使我们能够在不写任何代码的情况下设计用户界面,而且还允许高级用户通过一种简单的脚本语言,称为 Qt 样式表,来自定义他们的用户界面组件。
在本章中,我们将介绍以下菜谱:
-
使用 Qt Designer 与样式表结合使用
-
定制基本样式表
-
使用样式表创建登录屏幕
-
在样式表中使用资源
-
定制属性和子控件
-
在 Qt 模型语言(QML)中进行样式化
-
将 QML 对象指针暴露给 C++
技术要求
本章的技术要求包括拥有 Qt 6.1.1 MinGW 64-bit 和 Qt Creator 12.0.2。本章使用的代码可以从本书的 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter01。
使用 Qt Designer 与样式表结合使用
在本例中,我们将学习如何通过使用样式表和资源来改变我们程序的外观和感觉,使其看起来更加专业。Qt 允许您使用一种名为 Qt 样式表 的样式表语言来装饰您的 图形用户界面(GUI),这与网页设计师用来装饰他们网站的 层叠样式表(CSS)非常相似。
如何做到...
让我们开始学习如何创建一个新项目,并熟悉 Qt Designer:
-
打开 Qt Creator 并创建一个新项目。如果您是第一次使用 Qt Creator,您可以点击大按钮,上面写着 创建项目…,或者简单地转到 文件 | 新建项目…。
-
从项目窗口中选择应用程序(Qt)并选择Qt 小部件应用程序。
-
点击底部的选择...按钮。会出现一个窗口,要求您输入项目的名称和位置。
-
点击 下一步 几次,然后点击 完成 按钮来创建项目。我们现在将保持默认设置。一旦项目创建完成,您将首先看到左侧窗口中带有大量大图标的面板,这被称为模式选择面板;我们将在 剖析 Qt Designer 菜谱中更详细地讨论这一点。
-
您将在侧边栏面板上看到所有源文件列表,该面板位于模式选择面板旁边。您可以选择要编辑的文件。在这种情况下,这是
mainwindow.ui,因为我们即将开始设计程序的 UI。 -
双击
mainwindow.ui文件;您将看到一个完全不同的界面突然出现。Qt Creator 帮助您从脚本编辑器切换到 UI 编辑器(Qt Designer),因为它检测到您要打开的文件上的.ui扩展名。 -
您还会注意到模式选择面板上高亮的按钮已从编辑更改为设计。您可以通过点击模式选择面板上半部分的按钮之一切换回脚本编辑器或切换到任何其他工具。
-
让我们回到 Qt Designer,查看
mainwindow.ui文件。这是我们的程序的主窗口(如文件名所示),默认情况下它是空的,没有任何小部件。您可以通过按下模式选择面板底部的运行按钮(绿色箭头按钮)来尝试编译并运行程序;一旦编译完成,您将看到一个空窗口弹出。 -
让我们在程序的 UI 中添加一个按钮,通过在小部件框区域(位于按钮类别下)点击推按钮项并将其拖动到表单编辑器中的主窗口中来实现。保持推按钮选中状态;您将在窗口右侧的属性编辑器区域看到该按钮的所有属性。向下滚动到中间,寻找一个名为styleSheet的属性。这就是您将应用样式到您的控件的地方,这些样式可能或可能不是从其子控件或孙控件递归继承的,具体取决于您如何设置样式表。或者,您可以在表单编辑器中的 UI 中的任何控件上右键单击,并从弹出菜单中选择更改样式表...。
-
您可以点击styleSheet属性的输入字段来直接编写样式表代码,或者点击输入字段旁边的…按钮来打开编辑样式表窗口,该窗口有更大的空间来编写较长的样式表代码。在窗口顶部,您可以找到几个按钮,例如添加资源、添加渐变、添加颜色和添加字体,如果您记不住属性名称,这些按钮可以帮助您开始编码。让我们尝试使用编辑样式表窗口进行一些简单的样式设置。
-
点击添加颜色并选择一种颜色。
-
从颜色选择器窗口中随机选择一种颜色——比如说,纯红色。然后,点击确定。
-
在编辑样式表窗口的文本字段中已经添加了一行代码,在我的情况下如下所示:
color: rgb(255, 0, 0); -
点击确定按钮;您的推按钮上的文本应变为红色。
它是如何工作的...
在我们开始学习如何设计自己的 UI 之前,让我们花点时间熟悉 Qt Designer 的界面:

图 1.1 – Qt Designer 界面的概述
上述截图的解释如下:
-
菜单栏:菜单栏包含特定于应用程序的菜单,提供对基本功能的轻松访问,例如创建新项目、保存文件、撤销、重做、复制和粘贴。它还允许您访问 Qt Creator 附带的开发工具,例如编译器、调试器和性能分析器。
-
部件箱:在这里,您可以找到 Qt Designer 提供的所有不同类型的部件。您可以通过从部件箱区域中选择一个部件并将其拖动到表单编辑器中来将部件添加到程序的 UI 中。
-
模式选择器:模式选择器是一个侧面板,其中放置了快捷按钮,以便轻松访问不同的工具。您可以通过点击模式选择器面板上的编辑或设计按钮快速在脚本编辑器和表单编辑器之间切换,这对于多任务处理非常有用。您还可以以相同的方式和速度轻松导航到调试器和性能分析工具。
-
构建快捷键:构建快捷键位于模式选择器面板的底部。您可以通过按下这里的快捷按钮轻松构建、运行和调试项目。
-
表单编辑器:表单编辑器是您编辑程序 UI 的地方。您可以通过从部件箱区域选择一个部件并将其拖动到表单编辑器中来向程序添加不同的部件。
-
表单工具栏:从这里,您可以快速选择要编辑的不同表单。点击部件箱区域顶部的下拉框并选择您想要用 Qt Designer 打开的文件。在下拉框旁边是按钮,用于在表单编辑器的不同模式之间切换,以及按钮用于更改 UI 布局。
-
.ui文件。所有部件都根据它们在层次结构中的父子关系排列。您可以从对象检查器区域选择一个部件,在属性****编辑器区域显示其属性。 -
属性编辑器:属性编辑器区域将显示您从对象检查器区域或表单编辑器窗口中选择的部件的所有属性。
-
动作编辑器和信号与槽编辑器:此窗口包含两个编辑器:动作编辑器和信号与槽编辑器。您可以通过窗口下方的标签访问这两个编辑器。动作编辑器是您创建可以添加到程序 UI 菜单栏或工具栏中的动作的地方。
-
输出面板:输出面板由几个不同的窗口组成,显示与脚本编译和调试相关的信息和输出消息。您可以通过按下带有数字的按钮在不同输出面板之间切换,例如1 问题、2 搜索结果或3 应用程序输出。
更多...
在本示例中,我们讨论了如何通过 C++编码将样式表应用到 Qt 小部件上。虽然这种方法效果不错,但大多数情况下,负责设计程序 UI 的人不是程序员,而是一个专注于设计用户友好 UI 的 UI 设计师。在这种情况下,最好让 UI 设计师使用不同的工具来设计程序的布局和样式表,而不是与代码纠缠。Qt 提供了一个名为Qt Creator的一站式编辑器。
Qt Creator 由几个不同的工具组成,例如脚本编辑器、编译器、调试器、性能分析器和 UI 编辑器。UI 编辑器,也称为Qt Designer,是设计师在没有编写任何代码的情况下设计程序 UI 的完美工具。这是因为 Qt Designer 采用了所见即所得的方法,通过提供最终结果的准确视觉表示,这意味着你用 Qt Designer 设计的任何内容在程序编译和运行时都将具有相同的视觉表现。
Qt 样式表与 CSS 之间的相似之处如下:
-
这就是典型的 CSS 代码的样子:
h1 { color: red; background-color: white;} -
这就是 Qt 样式表的样子,它与前面的 CSS 几乎相同:
QLineEdit { color: red; background-color: white;}
如你所见,它们都包含一个选择器和声明块。每个声明包含一个属性和一个值,由冒号分隔。在 Qt 中,可以通过在 C++代码中调用QObject::setStyleSheet()函数将样式表应用到单个小部件上。
例如,考虑以下内容:
myPushButton->setStyleSheet("color : blue");
上述代码将myPushButton变量名的按钮文本变为蓝色。你同样可以通过在 Qt Designer 的样式表属性字段中编写声明来实现相同的结果。我们将在自定义基本样式表示例中进一步讨论 Qt Designer。
Qt 样式表还支持 CSS2 标准中定义的所有不同类型的选择器,包括usernameEdit对象名称,我们可以通过使用 ID 选择器来引用它:
QLineEdit#usernameEdit { background-color: blue }
注意
要了解 CSS2 中所有可用的选择器(Qt 样式表也支持这些选择器),请参阅此文档:www.w3.org/TR/REC-CSS2/selector.html。
自定义基本样式表
在上一个示例中,你学习了如何使用 Qt Designer 将样式表应用到小部件上。让我们疯狂一点,进一步探索,创建一些其他类型的小部件,并将它们的样式属性更改为一些奇怪的东西,以学习为目的。
然而,这一次,我们不会逐个将样式应用到每个小部件上;相反,我们将学习如何将样式表应用到主窗口,并让它向下继承到所有其他小部件,这样样式表就更容易在长期内进行管理和维护。
如何做到这一点...
在下面的示例中,我们将对画布上的不同类型的小部件进行格式化,并在样式表中添加一些代码以改变其外观:
-
通过选择
PushButton并单击样式表属性旁边的箭头按钮来从PushButton移除样式表。此按钮将属性还原为其默认值,在这种情况下是空样式表。 -
通过从小部件框区域逐个拖动它们到表单编辑器中,向 UI 添加更多小部件。我已经添加了一个行编辑器、组合框、水平滑块、单选按钮和一个复选框。
-
为了简化,通过在对象检查器区域选择它们,右键单击并选择移除来从你的 UI 中删除菜单栏、主工具栏和状态栏。现在,你的 UI 应该看起来类似于这样:

图 1.2 – 将一些小部件拖放到表单编辑器中
-
从表单编辑器或对象检查器区域选择主窗口,然后右键单击并选择更改样式表...以打开编辑样式表窗口。将以下内容插入到样式表中:
border: 2px solid gray; border-radius: 10px; padding: 0 8px; background: yellow; -
你将看到一个奇特的 UI,所有内容都被黄色覆盖,并且有粗边框。这是因为前面的样式表没有选择器,这意味着样式将应用于主窗口层次结构下的子小部件。为了改变这一点,让我们尝试不同的方法:
QPushButton { border: 2px solid gray; border-radius: 10px; padding: 0 8px; background: yellow; } -
这次,只有PushButton将获得前面代码中描述的样式,所有其他小部件将恢复到默认样式。你可以尝试向你的 UI 添加更多按钮;它们都将看起来相同:

图 1.3 – 将按钮改为黄色
-
这是因为我们明确告诉选择器将样式应用于具有
QPushButton类的所有小部件。我们也可以通过在样式表中提及其名称来仅将样式应用于一个按钮,如下面的代码所示:QPushButton#pushButton_3 { border: 2px solid gray; border-radius: 10px; padding: 0 8px; background: yellow; } -
一旦你理解了这种方法,我们就可以将以下代码添加到样式表中:
QPushButton { color: red; border: 0px; padding: 0 8px; background: white; } QPushButton#pushButton_2 { border: 1px solid red; border-radius: 10px; } -
这段代码会改变所有按钮的样式,以及
pushButton_2按钮的一些属性。我们保持pushButton_3的样式表不变。现在,按钮将看起来像这样:

图 1.4 – 为每个按钮应用不同的样式
-
第一组样式表将所有
QPushButton类型的小部件更改为无边框的白色矩形按钮,文本为红色。第二组样式表仅更改名为pushButton_2的特定QPushButton小部件的边框。请注意,pushButton_2的背景颜色和文本颜色仍然分别是白色和红色,因为我们没有在第二组样式表中覆盖它们,因此它将返回到第一组样式表中描述的样式,因为它适用于所有QPushButton小部件。第三个按钮的文本也变为红色,因为我们没有在第三组样式表中描述颜色属性。 -
创建另一组使用通用选择器的样式表,可以使用以下代码:
* { background: qradialgradient(cx: 0.3, cy: -0.4, fx: 0.3, fy: -0.4, radius: 1.35, stop: 0 #fff, stop: 1 #888); color: rgb(255, 255, 255); border: 1px solid #ffffff; } -
通用选择器将影响所有小部件,无论它们的类型如何。因此,前面的样式表将应用一个漂亮的渐变颜色到所有小部件的背景,并将它们的文本设置为白色,并带有白色的一像素实线轮廓。我们不需要写出颜色的名称(即白色),我们可以使用
rgb函数(rgb(255, 255, 255))或十六进制代码(#ffffff)来描述颜色值。 -
与之前一样,前面的样式表不会影响按钮,因为我们已经为它们提供了自己的样式,这将覆盖通用选择器中描述的通用样式。只需记住,在 Qt 中,当有多个样式影响小部件时,最终将使用更具体的样式。这就是 UI 现在的样子:

图 1.5 – 将渐变背景应用到所有其他小部件
它是如何工作的……
如果你曾经参与过使用 HTML 和 CSS 的 Web 开发,Qt 的样式表与 CSS 的工作方式相同。样式表提供了定义来描述小部件的展示——每个小部件组中每个元素的色彩是什么,边框应该有多厚,等等。如果你将小部件的名称指定给样式表,它将更改你提供的名称的特定PushButton小部件的样式。其他小部件将不受影响,并保持默认样式。
要更改小部件的名称,从表编辑器或对象检查器区域选择小部件,并在属性窗口中更改objectName属性。如果你之前使用 ID 选择器更改小部件的样式,更改其对象名称将破坏样式表并丢失样式。要解决这个问题,只需在样式表中更改对象名称即可。
使用样式表创建登录屏幕
接下来,我们将学习如何将之前学到的所有知识结合起来,为虚构的操作系统创建一个假图形登录界面。除了样式表之外,你还需要掌握如何使用 Qt Designer 中的布局系统整齐地排列小部件。
如何做到这一点...
让我们按照以下步骤开始:
- 在开始做任何事情之前,我们需要设计图形登录界面的布局。规划对于制作优秀的软件非常重要。以下是我制作的示例布局设计,以展示我如何想象登录界面的外观。只要能清楚地传达信息,这样的简单线条草图就足够了:

图 1.6 – 描述登录界面的简单草图
-
再次回到 Qt Designer。
-
我们将首先在顶部面板放置小部件,然后在下面放置标志和登录表单。
-
选择主窗口,将其宽度和高度分别从
400和300改为800和600——我们需要更大的空间来放置所有小部件。 -
从显示小部件类别中点击并拖动一个标签到小部件框区域的表单编辑器。
-
更改
currentDateTime并更改其文本属性以显示当前日期和时间——例如,周三,25-10-20233:14 PM。 -
点击并拖动
PushButton到restartButton和shutdownButton下。 -
选择主窗口,并点击表单工具栏上的小图标按钮,当鼠标悬停时它显示为垂直布局。你会看到小部件会自动排列在主窗口上,但这还不是我们想要的。
-
在布局类别下,点击并拖动一个水平布局小部件到主窗口。
-
点击并拖动两个按钮和文本标签到水平布局中。你会看到三个小部件被排列成一行,但在垂直方向上,它们位于屏幕的中间。水平排列几乎正确,但垂直位置不正确。
-
从间隔类别中点击并拖动一个垂直间隔小部件,并将其放置在我们在第 9 步中创建的水平布局小部件下方(在红色矩形轮廓下)。所有小部件都会被间隔推到顶部。
-
在文本标签和两个按钮之间放置一个水平间隔小部件,以保持它们之间的距离。这将确保文本标签始终保持在左侧,而按钮则对齐到右侧。
-
将两个按钮的
55 x 55设置为相同。将按钮的文本属性设置为空,因为我们将会使用图标而不是文本。我们将在使用样式表中的资源菜谱中学习如何在按钮小部件中放置图标。 -
你的 UI 应该看起来像这样:

图 1.7 – 使用水平间距将文本和按钮分开
接下来,我们将添加标志。按照以下步骤操作:
-
在顶部面板和水平间距小部件之间添加一个水平布局小部件,用作标志的容器。
-
在添加水平布局小部件后,您会发现布局的高度(几乎为零高度)太窄,以至于您无法向其中添加任何小部件。这是因为布局是空的,并且被其下的垂直间距推到零高度。为了解决这个问题,我们可以将其垂直间距(无论是layoutTopMargin还是layoutBottomMargin)临时设置得更大,直到向布局中添加小部件。
-
添加一个
logo。我们将在使用样式表中的资源配方中了解更多关于如何将图像插入到标签中,以便将其用作标志。现在,只需将150x 150清空即可。 -
如果您还没有这样做,请将布局的垂直间距设置回零。
-
标志现在看起来将不可见,因此我们将放置一个临时样式表来使其可见,直到我们在使用样式表中的资源配方中添加图像。样式表非常简单:
border: 1px solid;您的 UI 应该看起来类似于这个:

图 1.8 – 将占位符标志放置在中间
现在,让我们创建登录表单:
-
添加一个
100),以便您可以更轻松地向其中添加小部件。 -
添加一个
20),以便我们可以将其中的小部件放置进去。 -
右键单击
QWidget对象,它将自动继承小部件类的所有属性,这意味着我们现在可以调整其大小以满足我们的需求。 -
将我们刚刚从布局转换过来的
QWidget对象重命名为loginForm,并将其大小改为350 x 200。 -
由于我们已经在水平布局中放置了
loginForm小部件,我们可以将其layoutTopMargin属性设置回零。 -
将您为标志添加的相同样式表添加到
loginForm小部件中,使其临时可见。然而,这次,我们需要在前面添加一个 ID 选择器,以便它只会将样式应用于loginForm而不是其子小部件:#loginForm { border: 1px solid; }您的 UI 应该看起来类似于这个:

图 1.9 – 构建登录表单的框架
我们还没有完成登录表单。现在我们已经创建了登录表单的容器,是时候将更多小部件放入表单中:
-
在登录表单容器中放置两个水平布局。我们需要两个布局:一个用于用户名字段,另一个用于密码字段。
-
将
Username:和其下方的Password:添加到loginForm中。将两个行编辑分别重命名为username和password。 -
在密码布局下方添加一个按钮,并将其
Login改为loginButton。 -
您可以在
5之后添加一个Login按钮,以稍微分隔它们。 -
选择
loginForm容器并将其所有边距设置为35。这是为了通过在其所有边上添加一些空间来使登录表单看起来更好。 -
将
Username、Password和loginButton小部件的值设置为25,这样它们看起来就不会那么拥挤。你的 UI 应该看起来像这样:

图 1.10 – 向登录表单添加小部件
注意
或者,你可以使用网格布局来保持Username和Password字段的大小一致。
我们还没有完成!正如你所见,由于它们下面的垂直间距小部件,登录表单和标志都粘附在主窗口的顶部。标志和登录表单应该放置在主窗口的中心而不是顶部。要解决这个问题,请按照以下步骤操作:
-
在顶部面板和标志布局之间添加另一个垂直间距小部件。这将抵消底部的间距,以平衡对齐。
-
如果你认为标志与登录表单的距离太近,你可以添加一个
10。 -
右键单击顶部面板的布局并选择
topPanel。布局必须转换为QWidget,因为我们不能将样式表应用到布局上。这是因为布局除了边距之外没有其他属性。 -
主窗口的边缘有一点边距 – 我们不希望这样。要删除边距,请从对象检查器窗口中选择centralWidget对象,该窗口位于MainWindow面板下方,并将所有边距值设置为零。
-
通过单击运行按钮(带有绿色箭头图标)来运行项目,看看你的程序看起来像什么。如果一切顺利,你应该看到如下所示的内容:

图 1.11 – 我们完成了布局 – 至少目前是这样
-
现在,让我们使用样式表来装饰 UI!由于所有重要的小部件都已经赋予了对象名称,因此从主窗口应用样式表到它们会更容易,因为我们只需将样式表写入主窗口,并让它们从层次结构树中继承下来。
-
在对象检查器区域中右键单击MainWindow,然后选择更改样式表...。
-
将以下代码添加到样式表中:
#centralWidget { background: rgba(32, 80, 96, 100); } -
主窗口的背景将改变颜色。我们将在使用样式表中的资源配方中学习如何使用图像作为背景。所以,颜色只是临时的。
-
在 Qt 中,如果你想将样式应用到主窗口本身,你必须将其应用到其centralWidget小部件上,而不是主窗口,因为窗口只是一个容器。
-
在顶部面板上添加一个漂亮的渐变色:
#topPanel { background-color: qlineargradient(spread:reflect, x1:0.5, y1:0, x2:0, y2:0, stop:0 rgba(91, 204, 233, 100), stop:1 rgba(32, 80, 96, 100)); } -
将黑色应用到登录表单,并使其看起来半透明。我们还将通过设置
border-radius属性使登录表单容器的角落略微圆滑:#loginForm { background: rgba(0, 0, 0, 80); border-radius: 8px; } -
将样式应用到小部件的一般类型:
QLabel { color: white; } QLineEdit { border-radius: 3px; } -
之前的样式表将所有标签的文本颜色更改为白色;这包括小部件上的文本,因为内部 Qt 使用与具有文本的小部件相同的标签类型。此外,我们还使行编辑小部件的角落略微圆滑。
-
将样式表应用于我们 UI 上的所有按钮:
QPushButton { color: white; background-color: #27a9e3; border-width: 0px; border-radius: 3px; } -
之前样式表将所有按钮的文本颜色改为白色,然后将背景颜色设置为蓝色,并使其角落略微圆滑。
-
为了更进一步,我们将使用
hover关键字使按钮的颜色在鼠标悬停时改变:QPushButton:hover { background-color: #66c011; } -
当我们将鼠标悬停在按钮上时,之前的样式表将按钮的背景颜色更改为绿色。我们将在自定义属性和 子控件 菜单中进一步讨论这一点。
-
您可以进一步调整小部件的大小和边距,使它们看起来更好。请记住,通过移除在 步骤 6 中直接应用于登录表单的样式表来移除登录表单的边框线。
-
您的登录屏幕应该看起来像这样:

图 1.12 – 将颜色和样式应用于小部件
它是如何工作的...
此示例更多地关注 Qt 的布局系统。Qt 的布局系统允许我们的应用程序 GUI 通过排列每个小部件的子对象自动在给定空间内排列自己。我们在此菜谱中使用的空格项有助于将布局中包含的小部件推向外部,从而在空格项的宽度上创建间距。
要在布局中间定位小部件,我们必须在布局中放入两个空格项:一个在小部件的左侧,一个在小部件的右侧。然后,两个空格会将小部件推向布局的中间。
在样式表中使用资源
Qt 为我们提供了一个平台无关的资源系统,允许我们将任何类型的文件存储在我们的程序的可执行文件中以供以后使用。我们可以在可执行文件中存储的文件类型没有限制——图像、音频、视频、HTML、XML、文本文件、二进制文件等等都是允许的。
资源系统对于将资源文件(如图标和翻译文件)嵌入可执行文件以便应用程序随时访问非常有用。为了实现这一点,我们必须在.qrc文件中告诉 Qt 我们想要添加到其资源系统中的文件;Qt 将在构建过程中处理其余部分。
如何做到这一点...
要将新的 .qrc 文件添加到我们的项目中,请转到 resources) 并点击 .qrc 文件现在将被 Qt Creator 创建并自动打开。您不需要直接以 XML 格式编辑 .qrc 文件,因为 Qt Creator 为您提供了用户界面来管理您的资源。
要将图像和图标添加到你的项目中,你需要确保图像和图标被放置在你的项目目录中。当.qrc文件在 Qt Creator 中打开时,点击添加按钮,然后点击添加前缀按钮。前缀用于对资源进行分类,以便在项目中有大量资源时能更好地管理:
-
将你刚刚创建的前缀重命名为
/icons。 -
通过点击添加,然后点击添加前缀来创建另一个前缀。
-
将新前缀重命名为
/images。 -
选择
/icon前缀,然后点击添加,接着点击添加文件。 -
将会出现一个文件选择窗口;使用它来选择所有图标文件。你可以在按住键盘上的Ctrl键的同时点击文件来选择多个文件。完成后点击打开。
-
选择
/images前缀,然后点击添加按钮,接着点击添加文件按钮。将再次弹出文件选择窗口;这次我们将选择背景图像。 -
重复前面的步骤,但这次我们将把标志图像添加到
/images前缀。完成后,按Ctrl + S保存。你的.qrc文件现在应该看起来像这样:

图 1.13 – 显示资源文件的结构
-
返回到
mainwindow.ui文件;让我们利用我们刚刚添加到项目中的资源。选择位于顶部面板上的重启按钮。滚动到属性编辑器区域,直到你看到图标属性。点击带有下拉箭头图标的小按钮,并从其菜单中选择选择资源。 -
选择资源窗口将弹出。在左侧面板上点击图标前缀,然后在右侧面板上选择重启图标。按确定。
-
按钮上会出现一个小图标。这个图标看起来非常小,因为默认图标大小设置为
16 x 16。将其更改为50 x 50;你会看到图标变得更大。重复前面的步骤为关机按钮,这次选择关机图标。 -
这两个按钮现在应该看起来像这样:

图 1.14 – 将图标应用到按钮上
-
让我们使用添加到资源文件中的图像作为我们的标志。选择标志小部件,并移除我们之前添加的样式表以渲染其轮廓。
-
滚动到属性编辑器区域,直到你看到pixmap属性。
-
点击pixmap属性后面的下拉小按钮,并从菜单中选择选择资源。选择标志图像并点击确定。标志的大小不再遵循你之前设置的维度;它遵循图像的实际维度。我们无法更改其维度,因为这正是pixmap属性的工作方式。
-
如果你想对标志的尺寸有更多控制,你可以从 pixmap 属性中移除图像,并使用样式表代替。你可以使用以下代码将图像应用到图标容器中:
border-image: url(:/images/logo.png); -
要获取图像的路径,在文件列表窗口中右键单击图像的名称,并选择 复制路径。路径将被保存到你的操作系统剪贴板;现在,你只需将其粘贴到前面的样式表中。使用这种方法将确保图像适合你应用样式的控件尺寸。现在,你的标志应该看起来像以下截图所示:

图 1.15 – 标志现在出现在登录表单的顶部
-
使用样式表将壁纸图像应用到背景上。由于背景尺寸会根据窗口大小变化,我们无法在样式表中使用
border-image属性。右键单击主窗口并选择 更改样式表... 以打开 编辑样式表 窗口。我们将在 centralWidget 小部件的样式表下添加一行新内容:#centralWidget { background: rgba(32, 80, 96, 100); border-image: url(:/images/login_bg.png); } -
真的是非常简单和容易!你的登录屏幕现在应该看起来像这样:

图 1.16 – 最终结果看起来整洁
它是如何工作的...
Qt 的资源系统在编译时将二进制文件,如图像和翻译文件,存储在可执行文件中。它读取你的项目中的 .qrc) 以定位需要存储在可执行文件中的文件,并将它们包含在构建过程中。一个 .qrc 文件看起来像这样:
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource>
<file>images/copy.png</file>
<file>images/cut.png</file>
<file>images/new.png</file>
<file>images/open.png</file>
<file>images/paste.png</file>
<file>images/save.png</file>
</qresource>
</RCC>
它使用 .qrc 文件,或其子目录之一。
自定义属性和子控件
Qt 的样式表系统使我们能够轻松地创建令人惊叹且专业的 UI。在这个例子中,我们将学习如何为我们的小部件设置自定义属性,并使用它们在不同的样式之间切换。
如何做到这一点...
按照以下步骤自定义小部件属性和子控件:
- 让我们创建一个新的 Qt 项目。我已经为此准备了 UI。UI 包含左侧的三个按钮和右侧的 标签小部件,其中包含三个页面,如下面的截图所示:

图 1.17 – 基本用户界面,包含三个标签和按钮
-
三个按钮是蓝色的,因为我已经将以下样式表添加到主窗口中(而不是单独的按钮):
QPushButton { color: white; background-color: #27a9e3; border-width: 0px; border-radius: 3px; } -
我将通过向主窗口添加以下样式表来解释 Qt 中的 伪状态 是什么。你可能已经熟悉这个了:
QPushButton:hover { color: white; background-color: #66c011; border-width: 0px; border-radius: 3px; } -
我们在 使用样式表创建登录界面 的配方中使用了前面的样式表,使按钮在鼠标悬停事件发生时改变颜色。这是通过 Qt 样式表的
hover与QPushButton类通过冒号分隔来实现的。每个小部件都有一组通用伪状态,例如QPushButton,但不是QLineEdit。让我们添加一个 pressed 伪状态,当用户点击按钮时将按钮的颜色变为黄色:QPushButton:pressed { color: white; background-color: yellow; border-width: 0px; border-radius: 3px; } -
伪状态允许用户根据适用于他们的条件加载不同的样式表集。Qt 通过在 Qt 样式表中实现 动态属性 来进一步推进这一概念。这允许我们在满足自定义条件时更改小部件的样式表。我们可以利用这个特性,根据我们可以在 Qt 中使用自定义属性设置的特定条件来更改按钮的样式表。首先,我们将添加此样式表到我们的主窗口:
QPushButton[pagematches=true] { color: white; background-color: red; border-width: 0px; border-radius: 3px; } -
这将如果
pagematches属性返回QPushButton类,则将按钮的背景颜色更改为红色。然而,我们可以使用QObject::setProperty()将它添加到我们的按钮中:-
在你的
mainwindow.cpp源代码中,在ui->setupUi(this)之后添加以下代码:ui->button1->setProperty("pagematches", true);
上述代码将为第一个按钮添加一个名为
pagematches的自定义属性,并将其值设置为 true。这将使第一个按钮默认变为红色。-
然后,从列表中右键单击
currentChanged(int)选项,并点击 OK。Qt 将为你生成一个槽函数,看起来像这样:private slots: void on_tabWidget_currentChanged(int index); -
你将在
mainwindow.cpp中看到函数的声明。让我们向该函数添加一些代码:void MainWindow::on_tabWidget_currentChanged(int index) { // Set all buttons to false ui->button1->setProperty("pagematches", false); ui->button2->setProperty("pagematches", false); ui->button3->setProperty("pagematches", false); // Set one of the buttons to true if (0 == index) ui->button1->setProperty("pagematches", true); else if (index == 1) ui->button2->setProperty("pagematches", true); else ui->button3->setProperty("pagematches", true); // Update buttons style ui->button1->style()->polish(ui->button1); ui->button2->style()->polish(ui->button2); ui->button3->style()->polish(ui->button3); }
-
-
上述代码在 Tab Widget 切换到当前页面时将所有三个按钮的
pagematches属性设置为 false。在决定哪个按钮应该变为红色之前,请确保重置一切。 -
检查由事件信号提供的
index变量;这将告诉你当前页面的索引号。将一个按钮的pagematches属性设置为index号。 -
通过调用
polish()来刷新所有三个按钮的样式。你可能还希望在mainwindow.h中添加以下头文件:#include <QStyle> -
构建并运行项目。现在你应该看到每次你将 Tab Widget 切换到不同的页面时,三个按钮都会变为红色。此外,当鼠标悬停时,按钮将变为绿色,当你点击它们时,颜色将变为黄色:

图 1.18 – 最终结果看起来是这样的
它是如何工作的…
Qt 为用户提供添加任何类型小部件自定义属性的自由。如果你想在满足特定条件时更改特定小部件,自定义属性非常有用,而 Qt 默认不提供这样的上下文。这允许用户扩展 Qt 的可用性,使其成为定制解决方案的灵活工具。
例如,如果我们主窗口上有一排按钮,并且我们需要其中一个按钮根据页面改变其颜色,我们可以使用QObject::setProperty()。要读取自定义属性,我们可以使用另一个名为QObject::property()的函数。
接下来,我们将讨论 Qt 样式表中的子控件。通常,一个小部件不仅仅是一个单独的对象,而是由多个对象或控件组合而成,用于形成一个更复杂的小部件。这些对象被称为子控件。
例如,一个旋转框小部件包含一个输入字段、一个下按钮、一个上按钮、一个上箭头和一个下箭头,与一些其他小部件相比,这相当复杂。在这种情况下,Qt 通过允许我们通过样式表更改每个子控件来赋予我们更多的灵活性。我们可以通过在控件类名后面指定子控件的名称来实现,名称由双冒号分隔。例如,如果我想将下按钮的图像更改为旋转框,我可以将我的样式表编写如下:
QSpinBox::down-button {
image: url(:/images/spindown.png);
subcontrol-origin: padding;
subcontrol-position: right bottom;
}
这只会将图像应用到我的旋转框的下按钮,而不会应用到小部件的任何其他部分。通过结合自定义属性、伪状态和子控件,Qt 为我们提供了一个非常灵活的方法来自定义我们的用户界面。
注意
访问以下链接了解有关 Qt 中伪状态和子控件的更多信息:doc.qt.io/qt-6/stylesheet-reference.html。
Qt 建模语言(QML)中的样式
Qt 元语言或Qt 建模语言(QML)是一种受 JavaScript 启发的用户界面标记语言,Qt 用它来设计用户界面。Qt 为你提供了Qt Quick 组件(由 QML 技术驱动的控件),以便在不使用 C++编程的情况下轻松设计触摸友好的 UI。我们将通过遵循本食谱中提供的步骤来学习如何使用 QML 和 Qt Quick 组件来设计我们程序的 UI。
如何做到这一点...
按照以下步骤了解 QML 中的样式:
- 自从 Qt 6 以来,Qt 公司发布了一个名为Qt Design Studio的独立程序,用于开发 Qt Quick 应用程序。它的目的是将设计师和程序员的任务分开。因此,如果你是 GUI 设计师,你应该使用Qt Design Studio,如果你是程序员,则应坚持使用 Qt Creator。一旦安装并打开 Qt Design Studio,可以通过点击大号创建项目…按钮或从顶部菜单中选择文件 | 新建项目…来创建一个新项目:

图 1.19 – 在 Qt Design Studio 中创建新的 QML 项目
-
当 新建项目 窗口出现时,输入项目窗口的默认宽度和高度,并为你的项目输入一个名称。然后,选择你想要创建项目所在的目录,选择默认的 GUI 风格,选择目标 Qt 版本,并点击 创建 按钮。现在,Qt Design Studio 将创建你的 Qt Quick 项目。
-
在项目资源中,
App.qml文件与其他文件有一些区别。这个.qml文件是使用 QML 标记语言编写的 UI 描述文件。如果你双击main.qml文件,Qt Creator 将打开脚本编辑器,你会看到如下内容:import QtQuick 6.2 import QtQuick.Window 6.2 import MyProject Window { width: mainScreen.width height: mainScreen.height visible: true title: "MyProject" Screen01 { id: mainScreen } } -
此文件指示 Qt 创建一个窗口,加载 Screen01 用户界面以及带有你的项目名称的窗口标题。Screen01 界面来自另一个名为 Screen01.ui.qml 的文件。
-
如果你打开位于你项目
scr文件夹中的main.cpp文件,你会看到以下代码行:QQmlApplicationEngine engine; const QUrl url(u"qrc:Main/main.qml"_qs); -
上述代码告诉 Qt 的 QML 引擎在程序启动时加载
main.qml文件。如果你想加载其他.qml文件,你知道在哪里查找代码。src文件夹在 Qt Design Studio 项目中是隐藏的;你可以在你的项目目录中找到它。 -
如果你现在构建项目,你将得到一个带有简单文本和显示“按我”的按钮的巨大窗口。当你按下按钮时,窗口的背景颜色和文本将发生变化:

图 1.20 – 你的第一个 Qt Quick 程序
- 要添加 UI 元素,我们将通过转到 文件 | 新建文件… 并在 文件和类 | Qt Quick 文件类别下选择 Qt Quick UI 文件 来创建一个 Qt Quick UI 文件:

图 1.21 - 创建新的 Qt Quick UI 文件
- 设置
Main,然后点击 完成 按钮:

图 1.22 – 为你的 Qt Quick 组件赋予一个有意义的名称
-
已在你的项目资源中添加了一个名为
Main.ui.qml的新文件。尝试通过双击它来打开Main.ui.qml文件,如果它没有被 Qt Design Studio 在创建时自动打开。你将看到一个与之前 C++ 项目中完全不同的 UI 编辑器。 -
让我们打开
App.qml并将 Screen01 替换为 Main,如下所示:Main { id: mainScreen } -
当 QML 引擎加载
App.qml时,它也会将Main.ui.qml导入 UI 中,因为现在在App.qml文件中正在调用Main。Qt 会通过根据命名约定搜索其.qml文件来检查Main是否是一个有效的 UI。这个概念与我们在所有之前的食谱中完成的 C++项目类似;App.qml文件就像main.cpp文件,而Main.ui.qml就像MainWindow类。您还可以创建其他 UI 模板并在App.qml中使用它们。希望这个比较会使理解 QML 的工作方式更容易。 -
打开
Main.ui.qml。您应该在导航器窗口中只看到一个项被列出:项。这是窗口的基本布局,不应该被删除。它类似于我们在之前的食谱中使用的centralWidget。 -
目前画布是空的,所以让我们从左侧的QML 类型面板中拖动一个鼠标区域项和文本项到画布上。调整鼠标区域的大小,使其填充整个画布。同时,确保鼠标区域和文本项都被放置在导航器面板中的项项下,如下面的截图所示:

图 1.23 – 将鼠标区域和文本项拖放到画布上
-
鼠标区域项是一个不可摧毁的项,当鼠标点击它时或当手指触摸它(在移动平台上)时会被触发。鼠标区域项也用于按钮组件,我们稍后会使用它。文本项是自解释的:它是一个标签,用于在应用程序中显示文本块。
-
在导航器窗口中,我们可以通过点击项旁边类似眼睛的图标来隐藏或显示一个项。当一个项被隐藏时,它将不会出现在画布或编译的应用程序中。就像 C++ Qt 项目中的小部件一样,Qt Quick 组件根据父子关系进行分层排列。所有子项都将放置在具有缩进位置的父项下。在我们的例子中,我们可以看到鼠标区域和文本元素相对于项项稍微向右偏移,因为它们都是项元素的子项。我们可以通过使用导航器窗口中的点击和拖动方法来重新排列父子关系,以及它们在层次结构中的位置。您可以尝试点击文本项并将其拖动到鼠标区域上方。然后您会看到文本项的位置已经改变,现在位于鼠标区域下方,并且有更宽的缩进:

图 1.24 – 重新排列项之间的父子关系
- 我们可以通过使用位于导航器窗口顶部的箭头按钮来重新排列它们,如前面的截图所示。父项发生的任何变化也会影响所有子项,例如移动父项,以及隐藏和显示父项。
注意
您可以通过按住鼠标中键(或鼠标滚轮)并移动鼠标来在画布视图中平移。您还可以通过在键盘上按住Ctrl键并滚动鼠标来放大和缩小。默认情况下,滚动鼠标将上下移动画布视图。然而,如果鼠标光标位于画布的水平滚动条上,则滚动鼠标将视图左右移动。
-
删除鼠标区域项和文本项,因为我们将要学习如何从头开始使用 QML 和 Qt Quick 创建用户界面。
-
设置
800 x 600,因为我们需要更大的空间来放置小部件。 -
将我们在上一个 C++项目中使用的图像复制到 QML 项目的文件夹中,因为我们将要使用 QML 重新创建相同的登录屏幕。
-
将图像添加到资源文件中,以便我们可以在 UI 中使用它们。
-
打开Qt 设计工作室并切换到资源窗口。直接将背景图像拖动到画布上。切换到属性面板上的布局选项卡并单击填充锚点按钮,如这里用红色圆圈所示。这将使背景图像始终粘附到窗口大小:

图 1.25 – 选择填充锚点按钮以使项目跟随其父对象的大小
-
从库窗口拖动一个矩形组件到画布上。我们将用它作为程序的顶部面板。
-
对于顶部面板,启用顶部锚点、左侧锚点和右侧锚点,以便面板粘附到窗口顶部并跟随其宽度。确保所有边距都设置为零。
-
将颜色更改为
#805bcce9和第二个颜色为#80000000。这将创建一个半透明的带有蓝色渐变的面板。 -
为显示目的,添加一个
周三,25-10-2023 3:14 PM。然后,将文本颜色设置为白色。 -
切换到布局选项卡并启用顶部锚点和左侧锚点,以便文本小部件始终粘附到屏幕的左上角。
-
添加一个
50 x 50。然后,通过在导航器窗口中将它拖动到顶部面板上,使其成为顶部面板的子项。 -
将鼠标区域的颜色设置为蓝色(
#27a9e3),并将其半径设置为2以使其角落略微圆滑。启用顶部锚点和右侧锚点以使其粘附到窗口的右上角。将顶部锚点的边距设置为8,将右侧锚点的边距设置为10以创建一些空间。 -
打开资源窗口,并将关机图标拖动到画布中。使其成为我们刚刚创建的鼠标区域项的子元素。然后,启用填充锚点,使其与鼠标区域的尺寸相匹配。
-
呼——这有很多步骤!现在,您的项目在导航器窗口中应该如下排列:

图 1.26 – 注意项目之间的父子关系
- 父子关系和布局锚点都非常重要,以保持小部件在主窗口更改尺寸时的正确位置。您的顶部面板应该看起来像这样:

图 1.27 – 完成顶部横幅设计
-
让我们着手处理登录表单。添加一个新的
360 x 200,并将其半径设置为15。 -
将其颜色设置为
#80000000;这将使其变为黑色,并具有 50%的不透明度。 -
启用垂直居中锚点和水平居中锚点,使矩形始终与窗口的中心对齐。然后,将垂直居中锚点的边距设置为
100,使其稍微向下移动到底部。这将确保我们有足够的空间放置标志。以下截图展示了锚点的设置:

图 1.28 – 设置对齐和边距
-
将文本对象添加到画布中。使它们成为登录表单的子元素(
用户名:和密码:)。将它们的文本颜色更改为白色,并相应地定位它们。这次我们不需要设置边距,因为它们将跟随矩形的定位。 -
在画布上添加两个文本输入对象,并将它们放置在我们刚刚创建的文本小部件旁边。确保文本输入也是登录表单的子元素。由于文本输入不包含任何背景颜色属性,我们需要在画布上添加两个矩形作为它们的背景。
-
在画布上添加两个矩形,并将它们各自设置为刚刚创建的文本输入的子元素。设置
5以给它们一些圆角。之后,在两个矩形上启用填充锚点,以便它们跟随文本输入小部件的尺寸。 -
现在,让我们在密码字段下方创建登录按钮。将鼠标区域添加到画布中,并使其成为登录表单的子元素。调整其尺寸到您喜欢的尺寸,并将其移动到合适的位置。
-
由于鼠标区域不包含任何背景颜色属性,我们需要添加一个
#27a9e3并启用填充锚点,以便它与鼠标区域很好地匹配。 -
在画布上添加一个文本对象,并使其成为登录按钮的子元素。将其文本颜色更改为白色,并设置其
登录。最后,启用水平居中锚点和垂直居中锚点,使它们与按钮的中心对齐。 -
现在,你将看到一个登录表单,它的外观与我们在 C++ 项目中制作的非常相似:

图 1.29 – 登录表单的最终设计
-
现在,是时候添加标志了,这非常简单。打开资源窗口,并将标志图像拖放到画布上。
-
让它成为登录表单的子元素,并设置其大小为
512x 200。 -
将其放置在登录表单的顶部。这样,您就完成了。
-
这是编译后的整个 UI 的样子。我们已经成功地将 C++ 项目的登录屏幕重新创建出来,但这次我们使用的是 QML 和 Qt Quick:

图 1.30 – 最终结果
它是如何工作的……
Qt Quick 编辑器在将小部件放置在应用程序中与表单编辑器相比采用了一种非常不同的方法。用户可以决定哪种方法最适合他们的需求。以下截图显示了 Qt Quick 设计器的样子:

图 1.31 – Qt Design Studio 用户界面概览
让我们看看编辑器 UI 的各个元素:
-
导航器:导航器窗口以树状结构显示当前 QML 文件中的项目。它与我们在“使用 Qt Designer 的样式表”配方中使用的其他 Qt Designer 中的对象操作窗口类似。
-
库:库窗口显示了 QML 中可用的所有 Qt Quick 组件或 Qt Quick 控件。您可以将它拖放到画布窗口中,以将其添加到您的 UI 中。您还可以创建自己的自定义 QML 组件,并将它们显示在这里。
-
资产:资产窗口以列表形式显示所有可以用于 UI 设计的资源。
-
添加模块:添加模块按钮允许您将不同的 QML 模块导入到当前的 QML 文件中,例如蓝牙模块、WebKit 模块或定位模块,以向您的 QML 项目添加额外的功能。
-
属性:与我们在前面的配方中使用的属性编辑器区域类似,QML 设计器的属性面板显示所选项目的属性。您还可以在代码编辑器中更改项目的属性。
-
画布:画布是您创建 QML 组件和设计应用程序的工作区域。
-
工作区选择器:工作区选择器区域显示了 Qt Design Studio 编辑器中可用的不同布局,允许用户选择适合他们需求的工区。
-
样式选择器:这个选择器是您可以选择不同样式以预览应用程序在特定平台上运行时的外观的地方。这对于开发跨平台应用程序非常有用。
将 QML 对象指针暴露给 C++
有时,我们希望通过 C++脚本修改 QML 对象的属性,例如更改标签的文本、隐藏/显示小部件或更改其大小。Qt 的 QML 引擎允许你将你的 QML 对象注册到 C++类型,这会自动暴露其所有属性。
如何做到这一点…
我们想在 QML 中创建一个标签并偶尔更改其文本。为了将标签对象暴露给 C++,我们可以这样做:
-
在
mylabel.h中创建一个名为MyLabel的 C++类,它扩展了QObject类:class MyLabel : public QObject { Q_OBJECT public: // Object pointer QObject* myObject; explicit MyLabel(QObject *parent = 0); // Must call Q_INVOKABLE so that this function can be used in QML Q_INVOKABLE void SetMyObject(QObject* obj); } -
在
mylabel.cpp源文件中,定义一个名为SetMyObject()的函数来保存对象指针。此函数将在 QML 中的mylabel.cpp中被调用:void MyLabel::SetMyObject(QObject* obj) { // Set the object pointer myObject = obj; } -
在
main.cpp中,包含MyLabel头文件并使用qmlRegisterType()函数将其注册到 QML 引擎:include "mylabel.h" int main(int argc, char *argv[]) { // Register your class to QML qmlRegisterType<MyLabel>("MyLabelLib", 1, 0, "MyLabel"); } -
注意,在
qmlRegisterType()中你需要声明四个参数。除了声明你的类名(MyLabel)外,你还需要声明你的库名(MyLabelLib)及其版本(1.0)。这将用于将你的类导入 QML。 -
在 QML 中将 QML 引擎映射到我们的标签对象,并通过在 QML 文件中调用
import MyLabelLib 1.0来导入我们在第 3 步中定义的类库。请注意,库的名称及其版本号必须与你在main.cpp中声明的相匹配;否则,它将抛出错误。在 QML 中声明MyLabel并将其 ID 设置为mylabel.SetMyObject(myLabel)以在标签初始化后立即将其指针暴露给 C/C++之后:import MyLabelLib 1.0 ApplicationWindow { id: mainWindow width: 480 height: 640 MyLabel { id: mylabel } Label { id: helloWorldLabel text: qsTr("Hello World!") Component.onCompleted: { mylabel.SetMyObject(hellowWorldLabel); } } } -
在将标签的指针暴露给 C/C++之前,请等待标签完全初始化;否则,你可能会使程序崩溃。为了确保它已完全初始化,请在
Component.onCompleted中调用SetMyObject()函数,而不是在其他任何函数或事件回调中。现在,QML 标签已经暴露给 C/C++,我们可以通过调用setProperty()函数更改其任何属性。例如,我们可以将其可见性设置为true并将文本更改为Byebye world!:// Qvariant automatically detects your data type myObject->setProperty("visible", Qvariant(true)); myObject->setProperty("text", Qvariant("Bye bye world!")); -
除了改变属性外,我们还可以通过以下代码调用其函数:
QVariant returnedValue; QVariant message = "Hello world!"; QMetaObject::invokeMethod(myObject, "myQMLFunction", Q_RETURN_ARG(QVariant, returnedValue), Q_ARG(QVariant, message)); qDebug() << "QML function returned:" << returnedValue.toString(); -
或者,如果我们不期望从它返回任何值,我们可以仅用两个参数调用
invokedMethod()函数:QMetaObject::invokeMethod(myObject, "myQMLFunction");
它是如何工作的…
QML 的设计方式使其可以通过 C++代码进行扩展。Qt QML 模块中的类允许从 C++使用和操作 QML 对象,QML 引擎与 Qt 的元对象系统的结合能力允许直接从 QML 调用 C++功能。要向 QML 添加一些 C++数据或用法,它应该从一个 QObject 派生类中提出。可以从 C++建立 QML 对象类型,并监督访问它们的属性、调用它们的方法和获取它们的信号警报。这是可能的,因为所有 QML 对象类型都是使用 QObject 派生类执行的,这使得 QML 引擎能够通过 Qt 元对象系统强制加载和检查对象。
还有更多…
Qt 6 提供了两种不同的 GUI 工具包——Qt Widgets 和 Qt Quick。它们各自都有相对于对方的优点和优势,为程序员提供了设计和应用界面的能力和自由,无需担心功能限制和性能问题。
Qt 6 允许你选择最适合你工作风格和项目需求的最佳方法和编程语言。通过阅读本章,你将能够迅速创建一个外观美观且功能齐全的跨平台应用程序,使用 Qt 6。
第二章:事件处理 – 信号和槽
Qt 6 中的信号和槽机制是其最重要的特性之一。这是一种允许对象之间通信的方法,这是程序图形用户界面的重要组成部分。任何 QObject 对象或其子类都可以发出信号,然后触发连接到该信号的任何对象的任何槽函数。
在本章中,我们将涵盖以下主要主题:
-
信号和槽概述
-
使用信号和槽处理 UI 事件
-
异步编程变得更容易
-
函数回调
技术要求
本章的技术要求包括 Qt 6.6.1 MinGW 64-bit 和 Qt Creator 12.0.2。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter02。
信号和槽概述
与 回调(Qt 6 也支持)相比,信号和槽机制对程序员来说更加流畅和灵活。它既类型安全,又没有与处理函数强耦合,这使得它比回调实现更好。
如何操作…
让我们按照以下步骤开始:
-
让我们创建一个
mainwindow.ui。 -
从 小部件框 将 PushButton 小部件拖放到 UI 画布上:

图 2.1 – 将推按钮拖放到 UI 画布上
- 右键单击 PushButton 小部件并选择 转到槽。会出现一个窗口:

图 2.2 – 选择 clicked() 信号并按确定
-
你将看到可用于推按钮的内置槽函数列表。让我们选择
on_pushButton_clicked(),它现在将出现在mainwindow.h和mainwindow.cpp中。在按下mainwindow.h后,Qt Creator 会自动将槽函数添加到你的源代码中,现在你应该能够在privateslots关键字下看到一个额外的函数:class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: void on_pushButton_clicked(); private: Ui::MainWindow *ui; }; -
同样,对于
mainwindow.cpp,其中已经为你添加了on_pushButton_clicked()函数:void MainWindow::on_pushButton_clicked() { } -
现在,让我们将
QMessageBox头文件添加到源文件的顶部:#include <QMessageBox> -
然后,在
on_pushButton_clicked()函数内添加以下代码:void MainWindow::on_pushButton_clicked() { QMessageBox::information(this, «Hello», «Button has been clicked!»); } -
现在,构建并运行项目。然后,单击 Push 按钮;你应该能看到一个消息框弹出:

图 2.3 – 按下推按钮后弹出消息框
- 接下来,我们想要创建自己的信号和槽函数。转到 文件 | 新建文件或项目,然后在 文件和类 类别下创建一个新的 C++ 类:

图 2.4 – 创建一个新的 C++ 类
- 然后,我们需要将我们的类命名为
MyClass并确保基类是 QObject:

图 2.5 – 定义你的自定义类,该类继承自 QObject 类
-
一旦创建了类,打开
myclass.h并添加以下代码,这里为了清晰起见进行了高亮:#include <QObject> #include <QMainWindow> #include <QMessageBox> class MyClass : public QObject { Q_OBJECT public: explicit MyClass(QObject *parent = nullptr); public slots: void doSomething(); }; -
然后,打开
myclass.cpp并实现doSomething()槽函数。我们将从上一个示例中复制消息框函数:#include "myclass.h" MyClass::MyClass(QObject *parent) : QObject(parent) {} void MyClass::doSomething() { QMessageBox::information(this, «Hello», «Button has been clicked!»); } -
现在,打开
mainwindow.h并在顶部包含myclass.h头文件:#ifndef MAINWINDOW_H #define MAINWINDOW_H #include "myclass.h" namespace Ui { class MainWindow; } -
此外,在
myclass.h中声明一个doNow()信号:signals: void doNow(); private slots: void on_pushButton_clicked(); -
之后,打开
mainwindow.cpp并定义一个MyClass对象。然后,我们将之前步骤中创建的doNow()信号与我们的doSomething()槽函数连接起来:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow){ ui->setupUi(this); MyClass* myclass = new MyClass; connect(this, &MainWindow::doNow, myclass, &MyClass::doSomething); } -
然后,我们必须将
on_pushButton_clicked()函数的代码更改为如下所示:void MainWindow::on_pushButton_clicked() { emit doNow(); } -
如果现在构建并运行程序,您将得到一个类似于上一个示例的结果。然而,我们将消息框代码放置在了
MyClass对象中,而不是在MainWindow中。
前面的步骤展示了我们如何利用 Qt 6 中的槽和信号功能,轻松地将小部件动作链接到事件函数。这真的非常简单。
它是如何工作的...
在 Qt 的最新版本中,信号和槽机制经历了一些变化,最显著的是其编码语法。Qt 6 不再支持旧语法;因此,如果您正在尝试将旧的 Qt 5 项目移植到 Qt 6,您必须更改代码以符合新的语法。
在那个美好的旧时光里,我们通常会像这样将一个信号连接到一个槽上:
connect(
sender, SIGNAL(valueChanged(QString)),
receiver, SLOT(updateValue(QString))
);
然而,从那时起,事情发生了一些变化。在新语法中,SIGNAL 和 SLOT 宏已经不再使用,您必须指定您对象的数据类型,如下面的代码所示:
connect(
sender, &Sender::valueChanged,
receiver, &Receiver::updateValue
);
新语法还允许您直接将信号连接到一个函数,而不是 QObject:
connect(
sender, &Sender::valueChanged, myFunction
);
此外,您还可以将信号连接到一个 lambda 表达式。我们将在 Asynchronous programming made easier 菜谱中更多地讨论这一点。
注意
一个 任意 类的信号可以触发一个与之无关的类的任何私有槽,这在回调中是不可能的。
还有更多...
所有 Qt 项目都附带一个以 .pro 扩展名的项目文件。这个项目文件专门用于 Qt 自身的 qmake 构建系统,它通过使用直接的声明性风格,定义标准变量来指示项目中使用的源文件和头文件,从而帮助简化了大多数复杂的构建过程。
有一个名为CMakeLists.txt的替代构建系统,与 Qt Creator 一起使用,它将以使用 Qt 项目文件相同的方式打开项目。然而,不建议初学者在用 Qt 开发他们的第一个应用程序时使用 CMake,因为 CMake 更加手动,并且需要更长的时间来掌握其功能。
注意
要了解更多关于 CMake 的信息,请访问doc.qt.io/qt-6/cmake-get-started.html。
Qt 将它的特性和功能以模块和类的形式进行分类。每个模块都包含一组相关的功能,可以在需要时单独添加到您的项目中。这允许程序员保持他们的程序在最佳大小和性能。Qt 核心和 Qt GUI 模块默认包含在每一个 Qt 项目中。要添加额外的模块,您只需将模块关键字添加到您的 Qt 项目文件中,或者如果您使用 CMake 进行项目,则在CMakeLists.txt中添加包并链接其库。
例如,如果我想将 Qt 的Network模块添加到我的项目中,我将在我的 Qt 项目文件中添加以下关键字:
QT += network
然而,在 CMake 中,这会稍微长一些:
find_package(Qt6 REQUIRED COMPONENTS Network)
target_link_libraries(mytarget PRIVATE Qt6::Network)
在您添加了 Qt 的Network模块之后,现在您可以访问其 C++类,如QNetworkAccessManager、QNetworkInterface、QNetworkRequest等。这种模块化方法为 Qt 创建了一个可扩展的生态系统,同时允许开发者轻松地维护这个复杂而强大的框架。
注意
要了解更多关于所有不同的 Qt 模块的信息,请访问doc.qt.io/qt.html。
使用信号和槽的 UI 事件
在上一个示例中,我们演示了在按钮上使用信号和槽的方法。现在,让我们探索其他常见小部件类型中可用的信号和槽。
如何做到这一点...
要学习如何使用信号和槽处理 UI 事件,请按照以下步骤操作:
-
让我们创建一个新的Qt Widgets 应用程序项目。
-
从小部件框中将按钮、组合框、行编辑、微调框和滑块小部件拖放到您的 UI 画布上:

图 2.6 – 在 UI 画布上放置多个小部件
- 然后,右键单击按钮,选择clicked(),然后按确定按钮继续。Qt Creator 将为您创建一个槽函数:

图 2.7 – 选择 clicked()信号并按确定
-
重复前面的步骤,但这次选择下一个选择,直到将
QAbstractButton中的每个函数都添加到源代码中:void on_pushButton_clicked(); void on_pushButton_clicked(bool checked); void on_pushButton_pressed(); void on_pushButton_released(); void on_pushButton_toggled(bool checked); -
接下来,在组合框上重复相同的步骤,直到将
QComboBox类下的所有槽函数都添加到源代码中:void on_comboBox_activated(const QString &arg1); void on_comboBox_activated(int index); void on_comboBox_currentIndexChanged(const QString &arg1); void on_comboBox_currentIndexChanged(int index); void on_comboBox_currentTextChanged(const QString &arg1); void on_comboBox_editTextChanged(const QString &arg1); void on_comboBox_highlighted(const QString &arg1); void on_comboBox_highlighted(int index); -
对于
lineEdit也是如此,它们都属于QLineEdit类:void on_lineEdit_cursorPositionChanged(int arg1, int arg2); void on_lineEdit_editingFinished(); void on_lineEdit_returnPressed(); void on_lineEdit_selectionChanged(); void on_lineEdit_textChanged(const QString &arg1); void on_lineEdit_textEdited(const QString &arg1); -
之后,为我们的
spin box小部件也添加来自QSpinBox类的槽函数,它相对较短:void on_spinBox_valueChanged(const QString &arg1); void on_spinBox_valueChanged(int arg1); -
最后,对我们的
slider小部件重复相同的步骤,得到类似的结果:void on_horizontalSlider_actionTriggered(int action); void on_horizontalSlider_rangeChanged(int min, int max); void on_horizontalSlider_sliderMoved(int position); void on_horizontalSlider_sliderPressed(); void on_horizontalSlider_sliderReleased(); void on_horizontalSlider_valueChanged(int value); -
完成这些后,打开
mainwindow.h并添加QDebug头文件,如下代码所示:#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QDebug> namespace Ui { class MainWindow; } -
让我们实现我们的按钮的槽函数:
void MainWindow::on_pushButton_clicked() { qDebug() << «Push button clicked»; } void MainWindow::on_pushButton_clicked(bool checked) { qDebug() << «Push button clicked: « << checked; } void MainWindow::on_pushButton_pressed() { qDebug() << «Push button pressed»; } void MainWindow::on_pushButton_released() { qDebug() << «Push button released»; } void MainWindow::on_pushButton_toggled(bool checked) { qDebug() << «Push button toggled: « << checked; } -
如果你现在构建并运行项目,然后点击按钮,你会看到打印出不同的状态,但时间略有不同。这是由于在整个点击过程中不同动作发出不同信号造成的:
Push button pressed
Push button released
Push button clicked
Push button clicked: false
- 接下来,我们将转向组合框。由于默认的组合框是空的,让我们通过在
mainwindow.ui中双击它并添加弹出窗口中显示的选项来向其中添加一些选项:

图 2.8 – 向组合框添加更多选项
-
然后,让我们在
mainwindow.cpp中实现组合框的槽函数:void MainWindow::on_comboBox_activated(const QString &arg1) { qDebug() << «Combo box activated: « << arg1; } void MainWindow::on_comboBox_activated(int index) { qDebug() << «Combo box activated: « << index; } void MainWindow::on_comboBox_currentIndexChanged(const QString &arg1) { qDebug() << «Combo box current index changed: « << arg1; } void MainWindow::on_comboBox_currentIndexChanged(int index) { qDebug() << «Combo box current index changed: « << index; } -
我们将继续实现组合框的其余槽函数:
void MainWindow::on_comboBox_currentTextChanged(const QString &arg1) { qDebug() << «Combo box current text changed: « << arg1; } void MainWindow::on_comboBox_editTextChanged(const QString &arg1) { qDebug() << «Combo box edit text changed: « << arg1; } void MainWindow::on_comboBox_highlighted(const QString &arg1) { qDebug() << «Combo box highlighted: « << arg1; } void MainWindow::on_comboBox_highlighted(int index) { qDebug() << «Combo box highlighted: « << index; } -
构建并运行项目。然后,尝试点击组合框,悬停在其他选项上,并通过点击选择一个选项。你应该在你的调试输出中看到类似以下的结果:
Combo box highlighted: 0 Combo box highlighted: "Option One" Combo box highlighted: 1 Combo box highlighted: "Option Two" Combo box highlighted: 2 Combo box highlighted: "Option Three" Combo box current index changed: 2 Combo box current index changed: "Option Three" Combo box current text changed: "Option Three" Combo box activated: 2 Combo box activated: "Option Three" -
接下来,我们将转向行编辑并实现其槽函数,如下代码所示:
void MainWindow::on_lineEdit_cursorPositionChanged(int arg1, int arg2) { qDebug() << «Line edit cursor position changed: « << arg1 << arg2; } void MainWindow::on_lineEdit_editingFinished() { qDebug() << «Line edit editing finished»; } void MainWindow::on_lineEdit_returnPressed() { qDebug() << «Line edit return pressed»; } -
我们将继续实现行编辑的其余槽函数:
void MainWindow::on_lineEdit_selectionChanged() { qDebug() << «Line edit selection changed»; } void MainWindow::on_lineEdit_textChanged(const QString &arg1) { qDebug() << «Line edit text changed: « << arg1; } void MainWindow::on_lineEdit_textEdited(const QString &arg1) { qDebug() << «Line edit text edited: « << arg1; } -
构建并运行项目。然后,点击行编辑并输入
Hey。你应该在调试输出面板中看到类似以下的结果:Line edit cursor position changed: -1 0 Line edit text edited: "H" Line edit text changed: "H" Line edit cursor position changed: 0 1 Line edit text edited: "He" Line edit text changed: "He" Line edit cursor position changed: 1 2 Line edit text edited: "Hey" Line edit text changed: "Hey" Line edit cursor position changed: 2 3 Line edit editing finished -
之后,我们需要实现滑块小部件的槽函数,如下代码所示:
void MainWindow::on_spinBox_valueChanged(const QString &arg1){ qDebug() << «Spin box value changed: « << arg1; } void MainWindow::on_spinBox_valueChanged(int arg1) { qDebug() << «Spin box value changed: « << arg1; } -
尝试构建并运行程序。然后,点击滑块上的箭头按钮,或直接在框中编辑值 - 你应该得到类似以下的结果:
Spin box value changed: "1" Spin box value changed: 1 Spin box value changed: "2" Spin box value changed: 2 Spin box value changed: "3" Spin box value changed: 3 Spin box value changed: "2" Spin box value changed: 2 Spin box value changed: "20" Spin box value changed: 20 -
最后,我们将实现水平滑块小部件的槽函数:
void MainWindow::on_horizontalSlider_actionTriggered(int action) { qDebug() << «Slider action triggered» << action; } void MainWindow::on_horizontalSlider_rangeChanged(int min, int max) { qDebug() << «Slider range changed: « << min << max; } void MainWindow::on_horizontalSlider_sliderMoved(int position) { qDebug() << «Slider moved: « << position; } -
继续实现滑块的槽函数,如下代码所示:
void MainWindow::on_horizontalSlider_sliderPressed() { qDebug() << «Slider pressed»; } void MainWindow::on_horizontalSlider_sliderReleased() { qDebug() << «Slider released»; } void MainWindow::on_horizontalSlider_valueChanged(int value) { qDebug() << «Slider value changed: « << value; } -
构建并运行程序。然后,点击并拖动滑块向左或向右 - 你应该看到类似以下的结果:
Slider pressed Slider moved: 1 Slider action triggered 7 Slider value changed: 1 Slider moved: 2 Slider action triggered 7 Slider value changed: 2 Slider moved: 3 Slider action triggered 7 Slider value changed: 3 Slider moved: 4 Slider action triggered 7 Slider value changed: 4 Slider released
几乎每个小部件都有一组与其使用或目的相关的槽函数。例如,一个按钮在被按下或释放时将开始发出触发其相关槽函数的信号。这些定义小部件的预期行为具有在用户触发动作时被调用的槽函数。作为程序员,我们只需要实现槽函数并告诉 Qt 当这些槽函数被触发时应该做什么。
异步编程变得更容易
由于信号和槽机制本质上是异步的,我们可以用它来做除了用户界面之外的事情。在编程术语中,异步操作是一个独立工作的过程,允许程序在不需要等待该过程完成的情况下继续其操作,这可能会使整个程序停滞。Qt 6 允许你利用其信号和槽机制轻松实现异步过程,而不需要付出太多努力。在 Qt 6 强制实施信号和槽的新语法之后,这一点更是如此,它允许信号触发一个普通函数,而不是从 Qobject 对象的槽函数。
在以下示例中,我们将进一步探索这个机会,并学习我们如何通过使用 Qt 6 提供的信号和槽机制中的异步操作来提高我们程序的功效。
如何做到这一点...
要了解如何使用信号和槽机制实现异步操作,请遵循以下示例:
- 创建一个 Qt 控制台应用程序 项目:

图 2.9 – 创建新的 Qt 控制台应用程序项目
-
这种类型的项目只会为你提供一个
main.cpp文件,而不是像我们之前的示例项目那样提供mainwindow.h和mainwindow.cpp。让我们打开main.cpp并向其中添加以下头文件:#include <QNetworkAccessManager> #include <QNetworkReply> #include <QDebug> -
然后,将以下代码添加到我们的
main()函数中。我们将使用QNetworkAccessManager类来向以下网页 URL 发起GET请求:int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); QString *html = new QString; qDebug() << "Start"; QNetworkAccessManager manager; QNetworkRequest req(QUrl("http://www.dustyfeet.com")); QNetworkReply* reply = manager.get(req); -
然后,我们使用 C++11 的
QNetworkReply信号内联函数:QObject::connect(reply, &QNetworkReply::readyRead, [reply, html]() { html->append(QString(reply->readAll())); }); QObject::connect(reply, &QNetworkReply::downloadProgress, reply { qDebug() << "Progress: " << bytesReceived << "bytes /" << bytesTotal << "bytes"; }); -
我们还可以使用 lambda 表达式与
connect()一起调用不在QObject类下的函数:QObject::connect(reply, &QNetworkReply::finished, [=]() { printHTML(*html); }); return a.exec(); } -
最后,我们定义
printHTML()函数,如下所示:void printHTML(QString html) { qDebug() << "Done"; qDebug() << html; } -
如果你现在构建并运行程序,你会看到即使没有声明任何槽函数,它也是可用的。Lambda 表达式使声明槽函数成为可选的,但只有在你的代码真的很短的情况下才推荐这样做:

图 2.10 – 在终端窗口中打印 HTML 源代码
- 如果你在构建并运行你的 Qt 控制台应用程序项目后终端窗口没有出现,请转到 编辑 | 首选项 | 构建和运行 并选择 启用 选项的 默认“运行在”终端。

图 2.11 – 从首选项设置中启用终端窗口
上述示例演示了如何在网络回复槽函数中运行 lambda 函数。这样,我们可以确保我们的代码更短,更容易调试,但 lambda 函数仅适用于函数只打算调用一次的情况。
它是如何工作的...
上述示例是一个非常简单的应用程序,展示了使用 lambda 表达式连接信号与 lambda 函数或普通函数,而不需要声明任何槽函数,因此不需要从 QObject 类继承。
这对于调用不在 UI 对象下的异步过程特别有用。Lambda 表达式是在另一个函数内部匿名定义的函数,这与 JavaScript 中的匿名函数非常相似。Lambda 函数的格式如下:
captured variables {
lambda code
}
你可以通过将它们放入 捕获变量 部分来将变量插入 lambda 表达式,就像我们在本食谱的示例项目中做的那样。我们捕获了名为 reply 的 QNetworkReply 对象和名为 html 的 QString 对象,并将它们放入我们的 lambda 表达式中。
然后,我们可以在我们的 lambda 代码中使用这些变量,如下面的代码所示:
[reply, html]() {
html->append(QString(reply->readAll()));
}
参数部分类似于一个普通函数,其中你输入值到参数中,并在你的 lambda 代码中使用它们。在这种情况下,bytesReceived 和 bytesTotal 的值来自 downloadProgress 信号:
QObject::connect(reply, &QNetworkReply::downloadProgress,
reply {
qDebug() << "Progress: " << bytesReceived << "bytes /" << bytesTotal << "bytes";
});
你也可以使用 = 符号捕获你函数中使用的所有变量。在这种情况下,我们捕获了 html 变量,而没有在 捕获变量 区域中指定它:
[=]() {
printHTML(*html);
}
函数回调
尽管 Qt 6 支持信号和槽机制,但 Qt 6 中的一些功能仍然使用 函数回调,例如键盘输入、窗口调整大小、图形绘制等。由于这些事件只需要实现一次,因此不需要使用信号和槽机制。
如何做到这一点…
让我们从以下示例开始:
-
创建一个
mainwindow.h文件,并添加以下头文件:#include <QDebug> #include <QResizeEvent> #include <QKeyEvent> #include <QMouseEvent> -
然后,在
mainwindow.h中声明这些函数:public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); void resizeEvent(QResizeEvent *event); void keyPressEvent(QKeyEvent *event); void keyReleaseEvent(QKeyEvent *event); void mouseMoveEvent(QMouseEvent *event); void mousePressEvent(QMouseEvent *event); mainwindow.cpp and add the following code to the class constructor:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),
ui(new Ui::MainWindow) {
ui->setupUi(this);
this->setMouseTracking(true);
ui->centralWidget->setMouseTracking(true);
}
-
然后,定义
resizeEvent()和keyPressedEvent()函数:void MainWindow::resizeEvent(QResizeEvent *event) { qDebug() << "Old size:" << event->oldSize() << ", New size:" << event->size(); } void MainWindow::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { this->close(); } qDebug() << event->text() << "has been pressed"; } -
继续实现其余的函数:
void MainWindow::keyReleaseEvent(QKeyEvent *event) { qDebug() << event->text() << "has been released"; } void MainWindow::mouseMoveEvent(QMouseEvent *event) { qDebug() << "Position: " << event->pos(); } void MainWindow::mousePressEvent(QMouseEvent *event) { qDebug() << "Mouse pressed:" << event->button(); } void MainWindow::mouseReleaseEvent(QMouseEvent *event) { qDebug() << "Mouse released:" << event->button(); } -
编译并运行程序。然后,尝试移动鼠标,调整主窗口的大小,按下键盘上的随机键,最后按下键盘上的 Esc 键来关闭程序。
你应该能看到类似于在应用程序输出窗口中打印出的调试文本:
Old size: QSize(-1, -1) , New size: QSize(400, 300)
Old size: QSize(400, 300) , New size: QSize(401, 300)
Old size: QSize(401, 300) , New size: QSize(402, 300)
Position: QPoint(465,348)
Position: QPoint(438,323)
Position: QPoint(433,317)
"a" has been pressed
"a" has been released
"r" has been pressed
"r" has been released
"d" has been pressed
"d" has been released
"\u001B" has been pressed
它是如何工作的…
Qt 6 对象,尤其是主窗口,有十几个内置的回调函数,它们作为 虚函数 存在。这些函数可以在被调用时覆盖以执行你期望的行为。Qt 6 可能在其预期条件满足时调用这些 回调函数,例如键盘按钮被按下、鼠标光标被移动、窗口被调整大小等。
我们在 mainwindow.h 文件中声明的函数是 QWidget 类中内置的虚函数。我们只是用我们自己的代码覆盖它们,以定义它们被调用时的新行为。
注意
请务必注意,为了使 mouseMoveEvent() 回调正常工作,您必须对 MainWindow 和 centralWidget 都调用 setMouseTracking(true)。
没有诸如函数回调、信号和槽等特性,我们作为程序员将更难开发出响应迅速且易于使用的应用程序。Qt 6 缩短了我们的开发周期,并允许我们更多地专注于设计用户友好的应用程序。
第三章:使用 Qt 和 QML 的状态和动画
Qt 通过其强大的动画框架提供了一个简单的方法来动画化小部件或任何继承自 QObject 类的其他对象。动画可以单独使用,也可以与 状态机框架 一起使用,这允许根据小部件的当前活动状态播放不同的动画。Qt 的动画框架还支持分组动画,允许您同时移动多个图形项或按顺序依次移动它们。
在本章中,我们将涵盖以下主要主题:
-
Qt 中的属性动画
-
使用缓动曲线控制属性动画
-
创建动画组
-
创建嵌套动画组
-
Qt 中的状态机
-
QML 中的状态、转换和动画
-
使用动画器动画化小部件属性
-
精灵动画
技术要求
本章的技术要求包括 Qt 6.6.1 MinGW 64-bit、Qt Creator 12.0.2 和 Windows 11。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter03。
Qt 中的属性动画
在本例中,我们将学习如何动画化我们的 属性动画 类,这是其强大的动画框架的一部分,允许我们以最小的努力创建流畅的动画。
如何做到这一点...
在以下示例中,我们将创建一个新的小部件项目,并通过更改其属性来动画化按钮:
- 让我们使用 Qt Designer 创建一个新的
mainwindow.ui,并在主窗口上放置一个按钮,如图所示:

图 3.1 – 将按钮拖放到 UI 画布上
-
打开
mainwindow.cpp并在源代码开头添加以下代码行:#include <QPropertyAnimation> -
然后,打开
mainwindow.cpp并在构造函数中添加以下代码:QPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, "geometry"); animation->setDuration(10000); animation->setStartValue(ui->pushButton->geometry()); animation->setEndValue(QRect(200, 200, 100, 50)); animation->start();
它是如何工作的...
动画化 GUI 元素的一种更常见的方法是通过 Qt 提供的属性动画类,称为 QPropertyAnimation 类。这个类是动画框架的一部分,它利用 Qt 中的计时器系统来改变 GUI 元素的属性。
我们在这里试图实现的是在动画按钮从一个位置移动到另一个位置的同时,沿着路径扩大按钮的大小。通过在 步骤 2 中的源代码中包含 QPropertyAnimation 头文件,我们将能够访问 Qt 提供的 QPropertyAnimation 类并利用其功能。
步骤 3 中的代码基本上创建了一个新的 属性动画 并将其应用于 属性动画 类,它更改了 按钮 的几何属性,并将其持续时间设置为 3,000 毫秒(3 秒)。
然后,动画的起始值被设置为按钮的初始几何形状,因为显然我们希望它从我们在 Qt Designer 中最初放置按钮的位置开始。然后,end值被设置为我们希望它变成的样子;在这种情况下,我们将按钮移动到新的位置x: 200和y: 200,并在移动过程中改变其大小为width: 100和height: 50。
之后,调用animation | start()来开始动画。编译并运行项目。你应该看到按钮开始缓慢地在主窗口中移动,同时每次稍微扩大一点,直到到达目的地。你可以通过更改前面代码中的值来改变动画持续时间、目标位置和缩放。使用 Qt 的属性动画系统来动画化 GUI 元素真的非常简单!
还有更多...
Qt 为我们提供了几个不同的子系统来为我们的 GUI 创建动画,包括计时器、时间线、动画框架、状态机框架和图形视图框架:
-
事件回调函数将通过 Qt 的信号-槽机制被触发。你可以使用计时器在给定的时间间隔内改变 GUI 元素的属性(颜色、位置、缩放等)以创建动画。 -
时间线:时间线定期调用槽来动画化一个 GUI 元素。它与重复计时器非常相似,但当槽被触发时,它不会一直做同样的事情,而是向槽提供一个值来指示其当前帧索引,这样你就可以根据给定的值做不同的事情(例如,偏移到精灵图的另一个空间)。
-
动画框架:动画框架通过允许其属性动画化,使动画化 GUI 元素变得简单。动画是通过使用缓动曲线来控制的。缓动曲线描述了一个函数,它控制动画的速度应该是什么,从而产生不同的加速和减速模式。Qt 支持的缓动曲线类型包括线性、二次、三次、四次、正弦、指数、圆形和弹性。
-
状态机框架:Qt 为我们提供了创建和执行状态图的类,允许每个 GUI 元素在由信号触发时从一个状态移动到另一个状态。状态机框架中的状态图是分层的,这意味着每个状态也可以嵌套在其他状态内部。
-
图形视图框架:图形视图框架是一个强大的图形引擎,用于可视化与大量自定义的 2D 图形元素交互。如果你是一个经验丰富的程序员,你可以使用图形视图框架来绘制你的 GUI,并以完全手动的方式使它们动画化。
通过利用我们在这里提到的所有强大功能,我们可以轻松地创建直观且现代的 GUI。在本章中,我们将探讨使用 Qt 动画化 GUI 元素的实际方法。
使用缓动曲线控制属性动画
在这个例子中,我们将学习如何通过利用缓动曲线使我们的动画更有趣。我们仍然会使用之前的源代码,该代码使用属性动画来动画化一个按钮。
如何实现...
在以下示例中,我们将学习如何将一个缓动曲线添加到我们的动画中:
-
在调用
start()函数之前,定义一个缓动曲线并将其添加到属性动画中:QPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, "geometry"); animation->setDuration(3000); animation->setStartValue(ui->pushButton->geometry()); animation->setEndValue(QRect(200, 200, 100, 50)); QEasingCurve curve; curve.setType(QEasingCurve::OutBounce); animation->setEasingCurve(curve); animation->start(); -
调用
setLoopCount()函数来设置它重复的次数:QPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, "geometry"); animation->setDuration(3000); animation->setStartValue(ui->pushButton->geometry()); animation->setEndValue(QRect(200, 200, 100, 50)); QEasingCurve curve; curve.setType(EasingCurve::OutBounce); animation->setEasingCurve(curve); animation->setLoopCount(2); animation->start(); -
在应用缓动曲线到动画之前,调用
setAmplitude()、setOvershoot()和setPeriod():QEasingCurve curve; curve.setType(QEasingCurve::OutBounce); curve.setAmplitude(1.00); curve.setOvershoot(1.70); curve.setPeriod(0.30); animation->setEasingCurve(curve); animation->start();
在 Qt 6 中使用内置的缓动曲线来动画化小部件或任何对象真的非常简单。
它是如何工作的...
要让缓动曲线控制动画,你只需要定义一个缓动曲线并将其添加到属性动画中,在调用start()函数之前。你也可以尝试几种其他类型的缓动曲线,看看哪一种最适合你。以下是一个示例:
animation->setEasingCurve(QEasingCurve::OutBounce);
如果你希望动画在播放完毕后循环,你可以调用setLoopCount()函数来设置它重复的次数,或者将值设置为-1以实现无限循环:
animation->setLoopCount(-1);
在应用属性动画之前,你可以设置几个参数来细化缓动曲线。这些参数包括振幅、超调和周期:
-
振幅:振幅越高,动画中应用的弹跳或弹性弹簧效果就越明显。
-
超调:由于阻尼效应,某些曲线函数会产生超调(超过其最终值)曲线。通过调整超调值,我们可以增加或减少这种效果。
-
周期:设置较小的周期值会给曲线带来高频率。较大的周期会给它带来低频率。
然而,这些参数并不适用于所有曲线类型。请参阅 Qt 文档以了解哪些参数适用于哪些曲线类型。
更多内容...
虽然属性动画工作得很好,但有时看着一个 GUI 元素以恒定速度动画化会显得有点无聊。我们可以通过添加一个缓动曲线来控制运动,使动画看起来更有趣。Qt 中有许多类型的缓动曲线可供使用,以下是一些:

图 3.2 – Qt 6 支持的缓动曲线类型
如前图所示,每种缓动曲线都会产生不同的加速和减速效果。
注意
有关 Qt 中可用的完整缓动曲线列表,请参阅 Qt 文档中的doc.qt.io/qt-6/qeasingcurve.html#Type-enum。
创建动画组
在本例中,我们将学习如何使用动画组来管理组内包含的动画的状态。
如何做到这一点…
让我们按照以下步骤创建一个动画组:
- 我们将使用之前的示例,但这次,我们将向主窗口添加两个更多的推送按钮,如下面的截图所示:

图 3.3 – 向主窗口添加三个推送按钮
-
在主窗口的构造函数中为每个推送按钮定义动画:
QPropertyAnimation *animation1 = new QPropertyAnimation(ui->pushButton, "geometry"); animation1->setDuration(3000); animation1->setStartValue(ui->pushButton->geometry()); animation1->setEndValue(QRect(50, 200, 100, 50)); QPropertyAnimation *animation2 = new QPropertyAnimation(ui->pushButton_2, "geometry"); animation2->setDuration(3000); animation2->setStartValue(ui->pushButton_2->geometry()); animation2->setEndValue(QRect(150, 200, 100, 50)); QPropertyAnimation *animation3 = new QPropertyAnimation(ui->pushButton_3, "geometry"); animation3->setDuration(3000); animation3->setStartValue(ui->pushButton_3->geometry()); animation3->setEndValue(QRect(250, 200, 100, 50)); -
创建一个缓动曲线并将相同的曲线应用于所有三个动画:
QEasingCurve curve; curve.setType(QEasingCurve::OutBounce); curve.setAmplitude(1.00); curve.setOvershoot(1.70); curve.setPeriod(0.30); animation1->setEasingCurve(curve); animation2->setEasingCurve(curve); animation3->setEasingCurve(curve); -
在将缓动曲线应用于所有三个动画后,我们将创建一个动画组并将所有三个动画添加到组中:
QParallelAnimationGroup *group = new QParallelAnimationGroup; group->addAnimation(animation1); group->addAnimation(animation2); group->addAnimation(animation3); -
从我们刚刚创建的动画组中调用
start()函数:group->start();
它是如何工作的…
Qt 允许我们创建多个动画并将它们组合成一个动画组。组通常负责管理其动画的状态(即,它决定何时开始、停止、恢复和暂停它们)。目前,Qt 为动画组提供了两种类型的类:QParallelAnimationGroup和QSequentialAnimationGroup:
-
QParallelAnimationGroup:正如其名称所暗示的,一个并行动画组同时运行其组中的所有动画。当持续时间最长的动画完成后,组被认为是完成的。 -
QSequentialAnimationGroup:一个顺序动画组按顺序运行其动画,这意味着它一次只运行一个动画,并且只有当前动画完成后才会播放下一个动画。
还有更多…
由于我们现在正在使用动画组,我们不再从单个动画中调用start()函数。相反,我们将从我们刚刚创建的动画组中调用start()函数。如果您现在编译并运行示例,您将看到所有三个按钮同时播放。这是因为我们正在使用并行动画组。您可以用顺序动画组替换它,并再次运行示例:
QSequentialAnimationGroup *group = new QSequentialAnimationGroup;
这次,只有单个按钮会播放其动画,而其他按钮将耐心等待它们的轮到。优先级是根据哪个动画首先添加到动画组中而设置的。您可以通过简单地重新排列要添加到组中的动画的顺序来更改动画顺序。例如,如果我们想按钮3首先开始动画,然后是按钮2,最后是按钮1,代码将如下所示:
group->addAnimation(animation3);
group->addAnimation(animation2);
group->addAnimation(animation1);
由于属性动画和动画组都继承自QAbstractAnimator类,这意味着你还可以将一个动画组添加到另一个动画组中,以形成一个更复杂、嵌套的动画组。
创建嵌套动画组
使用嵌套动画组的一个好例子是当你有几个并行动画组,并且你想按顺序播放这些组时。
如何做到这一点…
让我们按照以下步骤创建一个嵌套动画组,以顺序播放不同的动画组:
- 我们将使用前一个示例中的 UI,并在主窗口中添加更多按钮,如下所示:

图 3.4 – 这次我们需要更多的按钮
-
为按钮创建所有动画,然后创建一个缓动曲线并将其应用于所有动画:
QPropertyAnimation *animation1 = new QPropertyAnimation(ui->pushButton, "geometry"); animation1->setDuration(3000); animation1->setStartValue(ui->pushButton->geometry()); animation1->setEndValue(QRect(50, 50, 100, 50)); QPropertyAnimation *animation2 = new QPropertyAnimation(ui->pushButton_2, "geometry"); animation2->setDuration(3000); animation2->setStartValue(ui->pushButton_2->geometry()); animation2->setEndValue(QRect(150, 50, 100, 50)); QPropertyAnimation *animation3 = new QPropertyAnimation(ui->pushButton_3, "geometry"); animation3->setDuration(3000); animation3->setStartValue(ui->pushButton_3->geometry()); animation3->setEndValue(QRect(250, 50, 100, 50)); -
接下来,应用以下代码:
QPropertyAnimation *animation4 = new QPropertyAnimation(ui->pushButton_4, "geometry"); animation4->setDuration(3000); animation4->setStartValue(ui->pushButton_4->geometry()); animation4->setEndValue(QRect(50, 200, 100, 50)); QPropertyAnimation *animation5 = new QPropertyAnimation(ui->pushButton_5, "geometry"); animation5->setDuration(3000); animation5->setStartValue(ui->pushButton_5->geometry()); animation5->setEndValue(QRect(150, 200, 100, 50)); QPropertyAnimation *animation6 = new QPropertyAnimation(ui->pushButton_6, "geometry"); animation6->setDuration(3000); animation6->setStartValue(ui->pushButton_6->geometry()); animation6->setEndValue(QRect(250, 200, 100, 50)); -
然后,应用以下代码:
QEasingCurve curve; curve.setType(QEasingCurve::OutBounce); curve.setAmplitude(1.00); curve.setOvershoot(1.70); curve.setPeriod(0.30); animation1->setEasingCurve(curve); animation2->setEasingCurve(curve); animation3->setEasingCurve(curve); animation4->setEasingCurve(curve); animation5->setEasingCurve(curve); animation6->setEasingCurve(curve); -
创建两个动画组,一个用于上列的按钮,另一个用于下列:
QParallelAnimationGroup *group1 = new QParallelAnimationGroup; group1->addAnimation(animation1); group1->addAnimation(animation2); group1->addAnimation(animation3); QParallelAnimationGroup *group2 = new QParallelAnimationGroup; group2->addAnimation(animation4); group2->addAnimation(animation5); group2->addAnimation(animation6); -
我们将创建另一个动画组,它将用于存储我们之前创建的两个动画组:
QSequentialAnimationGroup *groupAll = new QSequentialAnimationGroup; groupAll->addAnimation(group1); groupAll->addAnimation(group2); groupAll->start();
嵌套动画组允许你通过组合不同类型的动画并按你希望的顺序执行它们来设置更复杂的窗口小部件动画。
它是如何工作的…
我们在这里试图做的是首先播放上列按钮的动画,然后是下列按钮。由于两个动画组都是start()函数被调用。
这次,然而,这个组是一个顺序动画组,这意味着一次只能播放一个并行动画组,当第一个完成时再播放其他。动画组是一个非常方便的系统,它允许我们通过简单的编码创建非常复杂的 GUI 动画。Qt 会为我们处理困难的部分,所以我们不需要。
Qt 6 中的状态机
状态机可以用于许多目的,但在这个章节中,我们只会涵盖与动画相关的主题。
如何做到这一点…
在 Qt 中实现状态机并不困难。让我们按照以下步骤开始:
- 我们将为我们的示例程序设置一个新的用户界面,看起来像这样:

图 3.5 – 为我们的状态机实验设置 GUI
-
我们将在我们的源代码中包含一些头文件:
#include <QStateMachine> #include <QPropertyAnimation> #include <QEventTransition> -
在我们的主窗口构造函数中,添加以下代码以创建一个新的状态机和两个状态,我们将在以后使用:
QStateMachine *machine = new QStateMachine(this); QState *s1 = new QState(); QState *s2 = new QState(); -
我们将定义在每种状态下我们应该做什么,在这种情况下,这将是通过更改标签的文本和按钮的位置和大小:
QState *s1 = new QState(); s1->assignProperty(ui->stateLabel, "text", "Current state: 1"); s1->assignProperty(ui->pushButton, "geometry", QRect(50, 200, 100, 50)); QState *s2 = new QState(); s2->assignProperty(ui->stateLabel, "text", "Current state: 2"); s2->assignProperty(ui->pushButton, "geometry", QRect(200, 50, 140, 100)); -
完成这些后,让我们继续通过向源代码中添加
事件转换类来操作:QEventTransition *t1 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress); t1->setTargetState(s2); s1->addTransition(t1); QEventTransition *t2 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress); t2->setTargetState(s1); s2->addTransition(t2); -
将我们刚刚创建的所有状态添加到状态机中,并将状态 1 定义为
machine->start()以运行状态机:machine->addState(s1); machine->addState(s2); machine->setInitialState(s1); machine->start(); -
如果现在运行示例程序,您会注意到一切正常,除了按钮没有经过平滑的转换,它只是瞬间跳到了我们之前设置的位子和大小。这是因为我们没有使用属性动画来创建平滑的转换。
-
返回事件转换步骤并添加以下代码行:
QEventTransition *t1 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress); t1->setTargetState(s2); t1->addAnimation(new QPropertyAnimation(ui->pushButton, "geometry")); s1->addTransition(t1); QEventTransition *t2 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress); t2->setTargetState(s1); t2->addAnimation(new QPropertyAnimation(ui->pushButton, "geometry")); s2->addTransition(t2); -
您还可以向动画添加一个缓动曲线,使其看起来更有趣:
QPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, "geometry"); animation->setEasingCurve(QEasingCurve::OutBounce); QEventTransition *t1 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress); t1->setTargetState(s2); t1->addAnimation(animation); s1->addTransition(t1); QEventTransition *t2 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress); t2->setTargetState(s1); t2->addAnimation(animation); s2->addTransition(t2);
它是如何工作的...
主窗口布局中有两个按钮和一个标签。左上角的按钮在被按下时会触发状态转换,而右上角的标签会更改其文本以显示我们当前处于哪个状态。下面的按钮将根据当前状态进行动画处理。《QEventTransition》类定义了从一个状态到另一个状态的转换将触发什么。
在我们的情况下,我们希望当assignProperty()函数自动分配了结束值时,状态从状态 1 转换为状态 2。
还有更多...
Qt 中的状态机框架提供了用于创建和执行状态图的类。Qt 的事件系统用于驱动状态机,状态之间的转换可以通过使用信号来触发,然后另一端的槽将被信号调用以执行动作,例如播放动画。
一旦您理解了状态机的基础知识,您也可以用它们做其他事情。状态机框架中的状态图是分层的。就像上一节中的动画组一样,状态也可以嵌套在其他状态内部:

图 3.6 – 以视觉方式解释嵌套状态机
您可以将嵌套状态机和动画结合起来,为您的应用程序创建一个非常复杂的 GUI。
QML 中的状态、转换和动画
如果您更喜欢使用 QML 而不是 C++,Qt 还提供了 Qt Quick 中的类似功能,允许您使用最少的代码轻松地对 GUI 元素进行动画处理。在本例中,我们将学习如何使用 QML 实现这一点。
如何做到这一点...
让我们按照以下步骤开始创建一个不断改变其背景颜色的窗口:
- 我们将创建一个新的Qt Quick 应用程序项目并设置我们的用户界面,如下所示:

图 3.7 – 一个不断改变其背景颜色的快乐应用程序
-
这就是我的
main.qml文件看起来像:import QtQuick import QtQuick.Window Window { visible: true width: 480; height: 320; Rectangle { id: background; anchors.fill: parent; color: "blue"; } Text { text: qsTr("Hello World"); anchors.centerIn: parent; color: "white"; font.pointSize: 15; } } -
向
Rectangle对象添加颜色动画:Rectangle { id: background; anchors.fill: parent; color: "blue"; SequentialAnimation on color { ColorAnimation { to: "yellow"; duration: 1000 } ColorAnimation { to: "red"; duration: 1000 } ColorAnimation { to: "blue"; duration: 1000 } loops: Animation.Infinite; } } -
向
text对象添加一个数字动画:Text { text: qsTr("Hello World"); anchors.centerIn: parent; color: "white"; font.pointSize: 15; SequentialAnimation on opacity { NumberAnimation { to: 0.0; duration: 200} NumberAnimation { to: 1.0; duration: 200} loops: Animation.Infinite; } } -
向其中添加另一个数字动画:
Text { text: qsTr("Hello World"); anchors.centerIn: parent; color: "white"; font.pointSize: 15; SequentialAnimation on opacity { NumberAnimation { to: 0.0; duration: 200} NumberAnimation { to: 1.0; duration: 200} loops: Animation.Infinite; } NumberAnimation on rotation { from: 0; to: 360; duration: 2000; loops: Animation.Infinite; } } -
定义两个状态,一个称为
PRESSED状态,另一个称为RELEASED状态。然后,将默认状态设置为RELEASED:Rectangle { id: background; anchors.fill: parent; state: "RELEASED"; states: [ State { name: "PRESSED" PropertyChanges { target: background; color: "blue"} }, State { name: "RELEASED" PropertyChanges { target: background; color: "red"} } ] } -
之后,在
Rectangle对象内部创建一个鼠标区域,以便我们可以点击它:MouseArea { anchors.fill: parent; onPressed: background.state = "PRESSED"; onReleased: background.state = "RELEASED"; } -
向
Rectangle对象添加一些过渡效果:transitions: [ Transition { from: "PRESSED" to: "RELEASED" ColorAnimation { target: background; duration: 200} }, Transition { from: "RELEASED" to: "PRESSED" ColorAnimation { target: background; duration: 200} } ]
它是如何工作的…
主窗口由一个蓝色矩形和显示Rectangle对象的静态文本组成,然后在组内创建三个不同的颜色动画,每 1000 毫秒(1 秒)改变一次对象的颜色。我们还设置了动画为无限循环。
在步骤 4中,我们想要使用数字动画来动画化静态文本的 alpha 值。我们在Text对象内部创建了一个另一个顺序动画组,并创建了两个数字动画来动画化 alpha 值从0到1再返回。然后,我们将动画设置为无限循环。
然后,在步骤 5中,我们通过添加另一个Rectangle对象来旋转Hello World文本,当点击时从一种颜色变为另一种颜色。当鼠标释放时,Rectangle对象将变回其初始颜色。为了实现这一点,我们首先需要定义两个状态,一个称为PRESSED状态,另一个称为RELEASED状态。然后,我们将默认状态设置为RELEASED。
现在,当你编译并运行示例时,按下时背景会立即变为蓝色,当鼠标释放时变回红色。这效果很好,我们可以通过在切换颜色时添加一些过渡效果来进一步增强它。这可以通过向Rectangle对象添加过渡来实现。
更多内容…
在 QML 中,你可以使用八种不同的属性动画类型,具体如下:
-
锚点动画:动画化锚点值的变化
-
颜色动画:动画化颜色值的变化
-
数字动画:动画化 qreal 类型值的变化
-
父级动画:动画化父级值的变化
-
路径动画:动画化一个项目沿着路径
-
属性动画:动画化属性值的变化
-
旋转动画:动画化旋转值的变化
-
三维向量动画:动画化 QVector3D 值的变化
就像 C++版本一样,这些动画也可以在动画组中分组在一起,以顺序或并行播放动画。你还可以使用缓动曲线来控制动画,并使用状态机来确定何时播放这些动画,就像我们在前面的部分中所做的那样。
使用动画器动画化小部件属性
在这个菜谱中,我们将学习如何使用 QML 提供的动画器功能来动画化我们的 GUI 小部件的属性。
如何做到这一点…
如果你执行以下步骤,动画化 QML 对象将变得非常简单:
-
创建一个
Rectangle对象并将其添加一个缩放动画器:Rectangle { id: myBox; width: 50; height: 50; anchors.horizontalCenter: parent.horizontalCenter; anchors.verticalCenter: parent.verticalCenter; color: "blue"; ScaleAnimator { target: myBox; from: 5; to: 1; duration: 2000; running: true; } } -
添加一个旋转动画器并设置并行动画组中的
running值,但不在任何单个动画器中:ParallelAnimation { ScaleAnimator { target: myBox; from: 5; to: 1; duration: 2000; } RotationAnimator { target: myBox; from: 0; to: 360; duration: 1000; } running: true; } -
向缩放动画师添加一个缓动曲线:
ScaleAnimator { target: myBox; from: 5; to: 1; duration: 2000; easing.type: Easing.InOutElastic; easing.amplitude: 2.0; easing.period: 1.5; running: true; }
它是如何工作的...
动画师类型可以像任何其他动画类型一样使用。我们想要在 2,000 毫秒(2 秒)内将一个矩形的尺寸从5缩放到1。我们创建了一个蓝色的Rectangle对象,并向它添加了一个缩放动画师。我们将initial值设置为5,将final值设置为1。然后,我们将动画的duration设置为2000,并将running值设置为true,以便它在程序启动时播放。
就像动画类型一样,动画师也可以被放入组中(即并行动画组或顺序动画组)。动画组也将被 QtQuick 视为动画师,并在尽可能的情况下在场景图的渲染线程上运行。在步骤 2 中,我们想要将两个不同的动画师组合成一个并行动画组,以便它们同时运行。
我们将在并行动画组中保留running值,但不会在任何单个动画师中保留。
就像 C++版本一样,QML 也支持缓动曲线,并且可以轻松地应用于任何动画或动画师类型。
还有更多...
在 QML 中有一个叫做动画师的东西,它与通常的动画类型不同,尽管它们之间有一些相似之处。与常规动画类型不同,动画师类型直接在 Qt Quick 的场景图上操作,而不是在 QML 对象及其属性上。在动画运行期间,QML 属性的值不会改变,因为它只会在动画完成后改变。使用动画师类型的优点是它直接在场景图的渲染线程上操作,这意味着它的性能将略好于在UI 线程上运行。
精灵动画
在这个例子中,我们将学习如何在 QML 中创建一个精灵动画。
如何做到这一点...
让我们按照以下步骤让一匹马在我们的应用程序窗口中奔跑:
-
我们需要将我们的精灵图集添加到 Qt 的资源系统中,以便它可以在程序中使用。打开
qml.qrc并点击添加 | 添加文件按钮。选择你的精灵图集图像,然后按Ctrl + S保存资源文件。 -
在
main.qml中创建一个新的空窗口:import QtQuick 2.9 import QtQuick.Window 2.3 Window { visible: true width: 420 height: 380 Rectangle { anchors.fill: parent color: "white" } } -
完成之后,我们将在 QML 中开始创建一个
AnimatedSprite对象:import QtQuick 2.9 import QtQuick.Window 2.3 Window { visible: true; width: 420; height: 380; Rectangle { anchors.fill: parent; color: "white"; } -
然后,设置以下内容:
AnimatedSprite { id: sprite; width: 128; height: 128; anchors.centerIn: parent; source: "qrc:///horse_1.png"; frameCount: 11; frameWidth: 128; frameHeight: 128; frameRate: 25; loops: Animation.Infinite; running: true; } } -
向窗口添加一个鼠标区域并检查
onClicked事件:MouseArea { anchors.fill: parent; onClicked: { if (sprite.paused) sprite.resume(); else sprite.pause(); } } -
如果你现在编译并运行示例程序,你将看到一个小马在窗口中间奔跑。多么有趣:

图 3.8 – 一匹马在应用程序窗口中奔跑
-
接下来,我们想要尝试做一些酷的事情。我们将让马在窗口中奔跑,并无限循环播放其奔跑动画!首先,我们需要从 QML 中移除
anchors.centerIn: parent并将其替换为x和y值:AnimatedSprite { id: sprite; width: 128; height: 128; x: -128; y: parent.height / 2; source: "qrc:///horse_1.png"; frameCount: 11; frameWidth: 128; frameHeight: 128; frameRate: 25; loops: Animation.Infinite; running: true; } -
向精灵对象添加一个数字动画并设置其属性,如下所示:
NumberAnimation { target: sprite; property: "x"; from: -128; to: 512; duration: 3000; loops: Animation.Infinite; running: true; } -
如果你现在编译并运行示例程序,你会看到小马变得疯狂,开始在窗口上奔跑!
它是如何工作的...
在这个菜谱中,我们将动画精灵对象放置在窗口中间,并将其图像源设置为刚刚添加到项目资源中的精灵图集。然后,我们计算精灵图集中属于奔跑动画的帧数,在这个例子中是 11 帧。我们还通知 Qt 动画每一帧的尺寸,在这个例子中是128 x 128。之后,我们将帧率设置为25以获得合理的速度,然后将其设置为无限循环。然后,我们将running值设置为true,以便程序启动时默认播放动画。
然后,在步骤 4中,我们希望能够通过点击窗口来暂停动画并恢复播放。我们简单地检查在鼠标区域点击时精灵是否处于暂停状态。如果精灵动画被暂停,则动画恢复;否则,动画被暂停。
在步骤 6中,我们将anchors.centerIn替换为x和y值,这样动画精灵对象就不会锚定在窗口的中心,这将使其无法移动。然后,我们在动画精灵内部创建一个数字动画来动画化其x属性。我们将start值设置为窗口左侧的外部某个位置,并将end值设置为窗口右侧的外部某个位置。之后,我们将duration设置为 3,000 毫秒(3 秒)并使其无限循环。
最后,我们也将running值设置为true,以便程序启动时默认播放动画。
还有更多...
精灵动画被广泛使用,尤其是在游戏开发中。精灵用于角色动画、粒子动画,甚至 GUI 动画。精灵图集由许多图像组合成一张,然后可以切割并逐个显示在屏幕上。从精灵图集中不同图像(或精灵)之间的转换产生了动画的错觉,我们通常称之为精灵动画。在 QML 中使用AnimatedSprite类型可以轻松实现精灵动画。
注意
在这个示例程序中,我使用了一个由bluecarrot16创建的免费开源图像,该图像遵循CC-BY 3.0/GPL 3.0/GPL 2.0/OGA-BY 3.0许可协议。该图像可以在opengameart.org/content/lpc-horse合法获取。
第四章:QPainter 和 2D 图形
在本章中,我们将学习如何使用 Qt 在屏幕上渲染 2D 图形。内部,Qt 使用一个名为 QPainter 的低级类来在主窗口上渲染其小部件。Qt 允许我们访问和使用 QPainter 类来绘制矢量图形、文本、2D 图像,甚至 3D 图形。
您可以使用 QPainter 类创建自己的自定义小部件,或者创建依赖于渲染计算机图形的程序,如视频游戏、照片编辑器和 3D 建模工具。
在本章中,我们将涵盖以下主要主题:
-
在屏幕上绘制基本形状
-
将形状导出为 可缩放矢量图形 (SVG) 文件
-
坐标变换
-
在屏幕上显示图像
-
将图像效果应用于图形
-
创建基本的绘图程序
-
在 QML 中渲染 2D 画布
技术要求
本章的技术要求包括 Qt 6.6.1 MinGW 64 位 和 Qt Creator 12.0.2。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter04。
在屏幕上绘制基本形状
在本节中,我们将学习如何使用 QPainter 类在主窗口上绘制简单的矢量形状(一条线、一个矩形、一个圆等)并显示文本。我们还将学习如何使用 QPen 类更改这些矢量形状的绘图样式。
如何做到这一点…
让我们按照这里列出的步骤来显示我们 Qt 窗口中的基本形状:
-
首先,让我们创建一个新的 Qt Widgets 应用程序 项目。
-
打开
mainwindow.ui并移除menuBar、mainToolBar和statusBar对象,以便我们得到一个干净、空的主窗口。右键单击栏小部件,从弹出菜单中选择 移除菜单栏:

图 4.1 – 从主窗口中移除菜单栏
-
然后,打开
mainwindow.h文件并添加以下代码以包含QPainter头文件:#include <QMainWindow> #include <QPainter> -
然后,在类析构函数下方声明
paintEvent()事件处理程序:public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); mainwindow.cpp file and define the paintEvent() event handler:void MainWindow::paintEvent(QPaintEvent *event) {}
-
之后,我们将使用
QPainter类在paintEvent()事件处理程序中向屏幕添加文本。我们在屏幕上(``20, 30)位置绘制文本之前设置文本字体设置:QPainter textPainter; textPainter.begin(this); textPainter.setFont(QFont("Times", 14, QFont::Bold)); textPainter.drawText(QPoint(20, 30), "Testing"); textPainter.end(); -
然后,我们将绘制一条从
(50, 60)开始到(``100, 100)结束的直线:QPainter linePainter; linePainter.begin(this); linePainter.drawLine(QPoint(50, 60), QPoint(100, 100)); linePainter.end(); -
我们也可以通过调用
drawRect()函数并使用QPainter类轻松地绘制一个矩形。然而,这次我们在绘制形状之前还应用了一个背景图案:QPainter rectPainter; rectPainter.begin(this); rectPainter.setBrush(Qt::BDiagPattern); rectPainter.drawRect(QRect(40, 120, 80, 30)); rectPainter.end(); -
接下来,声明一个
QPen类,将其颜色设置为red,并将绘制样式设置为Qt::DashDotLine。然后,将QPen类应用于QPainter并在(80, 200)位置绘制一个椭圆形状,水平半径为50,垂直半径为20:QPen ellipsePen; ellipsePen.setColor(Qt::red); ellipsePen.setStyle(Qt::DashDotLine); QPainter ellipsePainter; ellipsePainter.begin(this); ellipsePainter.setPen(ellipsePen); ellipsePainter.drawEllipse(QPoint(80, 200), 50, 20); ellipsePainter.end(); -
我们还可以使用
QPainterPath类在传递给QPainter类进行渲染之前定义一个形状:QPainterPath rectPath; rectPath.addRect(QRect(150, 20, 100, 50)); QPainter pathPainter; pathPainter.begin(this); pathPainter.setPen(QPen(Qt::red, 1, Qt::DashDotLine, Qt::FlatCap, Qt::MiterJoin)); pathPainter.setBrush(Qt::yellow); pathPainter.drawPath(rectPath); pathPainter.end(); -
您还可以使用
QPainterPath绘制任何其他形状,例如椭圆:QPainterPath ellipsePath; ellipsePath.addEllipse(QPoint(200, 120), 50, 20); QPainter ellipsePathPainter; ellipsePathPainter.begin(this); ellipsePathPainter.setPen(QPen(QColor(79, 106, 25), 5, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin)); ellipsePathPainter.setBrush(QColor(122, 163, 39)); ellipsePathPainter.drawPath(ellipsePath); ellipsePathPainter.end(); -
QPainter也可以用来在屏幕上绘制图像文件。在以下示例中,我们加载了一个名为tux.png的图像文件,并在屏幕上的(100,150)位置绘制它:QImage image; image.load("tux.png"); QPainter imagePainter(this); imagePainter.begin(this); imagePainter.drawImage(QPoint(100, 150), image); imagePainter.end(); -
最终结果应该看起来像这样:

图 4.2 – 图形和线条让企鹅图克斯感到不知所措
它是如何工作的...
如果您想使用 QPainter 在屏幕上绘制某些内容,您只需要告诉它应该绘制什么类型的图形(如文本、矢量形状、图像、多边形)以及所需的尺寸和位置。QPen 类决定了图形轮廓的外观,例如其颜色、线宽、线型(实线、虚线或点线)、端点样式、连接样式等。另一方面,QBrush 设置图形背景的样式,例如背景颜色、图案(纯色、渐变、密集刷和交叉对角线)和位图。
在调用 draw 函数(如 drawLine()、drawRect() 或 drawEllipse())之前应设置图形选项。如果您的图形没有显示在屏幕上,并且在 Qt Creator 的应用程序输出窗口中看到诸如 QPainter::setPen: Painter not active 和 QPainter::setBrush: Painter not active 的警告,这意味着 QPainter 类当前未激活,并且您的程序将不会触发其绘制事件。要解决这个问题,请将主窗口设置为 QPainter 类的父类。通常,如果您在 mainwindow.cpp 文件中编写代码,您只需要在初始化 QPainter 时在括号中放置 this。例如,注意以下内容:
QPainter linePainter(this);
QImage 可以从计算机目录和程序资源中加载图像。
还有更多...
将 QPainter 想象为一个带有笔和空画布的机器人。您只需要告诉机器人应该绘制什么类型的形状以及它在画布上的位置,然后机器人将根据您的描述完成工作。
为了使你的生活更轻松,QPainter 类还提供了许多函数,例如 drawArc()、drawEllipse()、drawLine()、drawRect() 和 drawPie(),这些函数允许你轻松渲染预定义的形状。在 Qt 中,所有的小部件类(包括主窗口)都有一个事件处理程序,称为 QWidget::paintEvent()。当操作系统认为主窗口应该重新绘制其小部件时,此事件处理程序将被触发。许多事情可能导致这个决定,例如主窗口被缩放、小部件改变其状态(即按钮被按下),或者代码中手动调用 repaint() 或 update() 函数。不同的操作系统在决定是否在相同条件下触发更新事件时可能会有不同的行为。如果你正在制作一个需要持续和一致图形更新的程序,请使用定时器手动调用 repaint() 或 update()。
将形状导出为 SVG 文件
SVG 是一种基于 XML 的语言,用于描述 2D 向量图形。Qt 提供了将向量形状保存为 SVG 文件的类。这个功能可以用来创建一个类似于 Adobe Illustrator 和 Inkscape 的简单向量图形编辑器。在下一个示例中,我们将继续使用之前示例中的相同项目文件。
如何操作…
让我们学习如何创建一个简单的程序,该程序可以在屏幕上显示 SVG 图形:
- 首先,让我们通过在层次窗口中右键单击主窗口小部件并从弹出菜单中选择 创建菜单栏 选项来创建一个菜单栏。之后,将 文件 选项添加到菜单栏中,并在其下添加 另存为 SVG 动作:

图 4.3 – 在菜单栏上创建“另存为 SVG”选项
- 之后,你会看到一个名为
triggered()的项,然后点击 确定 按钮:

图 4.4 – 为 triggered() 信号创建槽函数
-
一旦你点击了
on_actionSave_as_SVG_triggered(),它就会自动添加到你的主窗口类中。在你的mainwindow.h文件底部,你会看到类似以下内容:void MainWindow::on_actionSave_as_SVG_triggered() {} -
当你点击源文件顶部的
QSvgGenerator时,会调用前面的函数。这个头文件非常重要,因为它是生成 SVG 文件所必需的。然后,我们还需要包含另一个类头文件QFileDialog,它将被用来打开保存对话框:#include <QtSvg/QSvgGenerator> #include <QFileDialog> -
我们还需要将
svg模块添加到我们的项目文件中,如下所示:QT += core gui paintAll() within the mainwindow.h file, as shown in the following code:public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
virtual void paintEvent(QPaintEvent *event);
将 mainwindow.cpp 文件中的所有代码从 paintEvent()函数移动到 paintAll()函数中。然后,将所有单独的 QPainter 对象替换为用于绘制所有图形的单个统一 QPainter 对象。此外,在绘制任何内容之前调用 begin()函数,在完成绘制后调用 end()函数。代码应如下所示:
void MainWindow::paintAll(QSvgGenerator *generator) { QPainter painter; if (engine) painter.begin(engine); else painter.begin(this); painter.setFont(QFont("Times", 14, QFont::Bold)); painter.drawText(QPoint(20, 30), "Testing"); painter.drawLine(QPoint(50, 60), QPoint(100, 100)); painter.setBrush(Qt::BDiagPattern); painter.drawRect(QRect(40, 120, 80, 30)); -
我们继续创建ellipsePen和rectPath:
QPen ellipsePen; ellipsePen.setColor(Qt::red); ellipsePen.setStyle(Qt::DashDotLine); painter.setPen(ellipsePen); painter.drawEllipse(QPoint(80, 200), 50, 20); QPainterPath rectPath; rectPath.addRect(QRect(150, 20, 100, 50)); painter.setPen(QPen(Qt::red, 1, Qt::DashDotLine, Qt::FlatCap, Qt::MiterJoin)); painter.setBrush(Qt::yellow); painter.drawPath(rectPath); -
然后,我们继续创建
ellipsePath和image:QPainterPath ellipsePath; ellipsePath.addEllipse(QPoint(200, 120), 50, 20); painter.setPen(QPen(QColor(79, 106, 25), 5, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin)); painter.setBrush(QColor(122, 163, 39)); painter.drawPath(ellipsePath); QImage image; image.load("tux.png"); painter.drawImage(QPoint(100, 150), image); painter.end(); } -
由于我们已经将所有代码从
paintEvent()移动到paintAll(),我们现在需要在paintEvent()中调用paintAll()函数,如下所示:void MainWindow::paintEvent(QPaintEvent *event) { paintAll(); } -
然后,我们将编写将图形导出为 SVG 文件的代码。该代码将编写在由 Qt 生成的名为
on_actionSave_as_SVG_triggered()的槽函数中。我们首先调用保存文件对话框,并从用户那里获取带有所需文件名的目录路径:void MainWindow::on_actionSave_as_SVG_triggered() { QString filePath = QFileDialog::getSaveFileName(this, «Save SVG», «», «SVG files (*.svg)»); if (filePath == "") return; } -
之后,创建一个
QSvgGenerator对象,并通过将QSvgGenerator对象传递给paintAll()函数将图形保存到 SVG 文件中:void MainWindow::on_actionSave_as_SVG_triggered() { QString filePath = QFileDialog::getSaveFileName(this, "Save SVG", "", "SVG files (*.svg)"); if (filePath == "") return; QSvgGenerator generator; generator.setFileName(filePath); generator.setSize(QSize(this->width(), this->height())); generator.setViewBox(QRect(0, 0, this->width(), this->height())); generator.setTitle("SVG Example"); generator.setDescription("This SVG file is generated by Qt."); paintAll(&generator); } -
现在,编译并运行程序,你应该能够通过转到文件 | 另存为 SVG来导出图形:

图 4.5 – 在网页浏览器中比较我们的程序和 SVG 文件的结果
它是如何工作的...
默认情况下,QPainter将使用其父对象的绘图引擎来绘制分配给它的图形。如果您没有为QPainter分配任何父对象,您可以手动为其分配一个绘图引擎,这正是我们在本例中所做的。
我们将代码放入paintAll()中的原因是我们希望相同的代码用于两个不同的目的:在窗口上显示图形和将图形导出为 SVG 文件。您可以看到,paintAll()函数中生成器变量的默认值设置为0,这意味着除非指定,否则不需要QSvgGenerator对象来运行该函数。稍后,在paintAll()函数中,我们检查生成器对象是否存在。如果它存在,则使用它作为画家的绘图引擎,如下面的代码所示:
if (engine)
painter.begin(engine);
else
painter.begin(this);
否则,将主窗口传递给begin()函数(由于我们在mainwindow.cpp文件中编写代码,我们可以直接使用它来引用主窗口的指针),这样它将使用主窗口本身的绘图引擎,这意味着图形将被绘制在主窗口的表面上。在本例中,需要使用单个QPainter对象将图形保存到 SVG 文件中。如果您使用多个QPainter对象,生成的 SVG 文件将包含多个 XML 头定义,因此任何图形编辑软件都会认为该文件无效。
QFileDialog::getSaveFileName()将为用户打开原生的保存文件对话框,以便用户选择保存目录并设置一个期望的文件名。一旦用户完成操作,完整的路径将作为字符串返回,然后我们可以将此信息传递给QSvgGenerator对象以导出图形。
注意,在前面的屏幕截图中,SVG 文件中的企鹅已经被裁剪。这是因为 SVG 的画布大小被设置为跟随主窗口的大小。为了帮助可怜的企鹅找回身体,在导出 SVG 文件之前将窗口放大。
更多内容...
SVG 以 XML 格式定义图形。由于它是一种矢量图形,如果放大或调整大小,SVG 文件不会丢失任何质量。SVG 格式不仅允许你在工作文件中存储矢量图形,还允许你存储位图图形和文本,这在一定程度上类似于 Adobe Illustrator 的格式。SVG 还允许你将图形对象分组、样式化、变换和组合到之前渲染的对象中。
注意
你可以在www.w3.org/TR/SVG查看 SVG 图形的完整规范。
坐标变换
在这个例子中,我们将学习如何使用坐标变换和计时器来创建实时时钟显示。
如何做到这一点...
要创建我们的第一个图形时钟显示,让我们按照以下步骤进行:
-
首先,创建一个新的
mainwindow.ui并移除我们之前所做的menuBar、mainToolBar和statusBar。 -
之后,打开
mainwindow.h文件并包含以下头文件:#include <QTime> #include <QTimer> #include <QPainter> -
然后,声明
paintEvent()函数,如下所示:public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); mainwindow.cpp file, create three arrays to store the shapes of the hour hand, minute hand, and second hand, where each of the arrays contains three sets of coordinates:void MainWindow::paintEvent(QPaintEvent *event) {
static const QPoint hourHand[3] = {
QPoint(4, 4),
QPoint(-4, 4),
QPoint(0, -40)
};
static const QPoint minuteHand[3] = {
QPoint(4, 4),
QPoint(-4, 4),
QPoint(0, -70)
};
static const QPoint secondHand[3] = {
QPoint(2, 2),
QPoint(-2, 2),
QPoint(0, -90)
};
}
-
之后,在数组下方添加以下代码以创建绘图器和将其移动到主窗口的中心。同时,我们调整绘图器的大小,使其在窗口调整大小时也能很好地适应主窗口:
int side = qMin(width(), height()); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); painter.translate(width() / 2, height() / 2); painter.scale(side / 250.0, side / 250.0); -
完成这些后,我们将使用循环开始绘制表盘。每个表盘旋转增加 6 度,因此 60 个表盘将完成一个完整的圆圈。此外,每过五分钟,表盘看起来会略微变长:
for (int i = 0; i < 60; ++i) { if ((i % 5) != 0) painter.drawLine(92, 0, 96, 0); else painter.drawLine(86, 0, 96, 0); painter.rotate(6.0); } -
然后,我们继续绘制时钟的指针。每个指针的旋转是根据当前时间和它在 360 度中的相应位置来计算的:
QTime time = QTime::currentTime(); // Draw hour hand painter.save(); painter.rotate((time.hour() * 360) / 12); painter.setPen(Qt::NoPen); painter.setBrush(Qt::black); painter.drawConvexPolygon(hourHand, 3); painter.restore(); -
让我们绘制时钟的时针:
// Draw minute hand painter.save(); painter.rotate((time.minute() * 360) / 60); painter.setPen(Qt::NoPen); painter.setBrush(Qt::black); painter.drawConvexPolygon(minuteHand, 3); painter.restore(); -
然后,我们也绘制秒针:
// Draw second hand painter.save(); painter.rotate((time.second() * 360) / 60); painter.setPen(Qt::NoPen); painter.setBrush(Qt::black); painter.drawConvexPolygon(secondHand, 3); painter.restore(); -
最后但同样重要的是,创建一个计时器每秒刷新一次图形,这样程序就能像真正的时钟一样工作:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); QTimer* timer = new QTimer(this); timer->start(1000); connect(timer, QTimer::timeout, this, MainWindow::update); } -
现在编译并运行程序,你应该会看到如下所示的内容:

图 4.6 – 在 Qt 应用程序上显示的实时模拟时钟
它是如何工作的...
每个数组包含三个 QPoint 数据实例,这些实例形成一个细长的三角形。然后,这些数组被传递给画家,并使用 drawConvexPolygon() 函数渲染为一个凸多边形。在绘制每个时钟指针之前,我们使用 painter.save() 保存 QPainter 对象的状态,然后使用坐标变换继续绘制指针。
一旦完成绘图,我们通过调用 painter.restore() 将画家恢复到其之前的状态。这个函数将撤销 painter.restore() 之前所有的变换,这样下一个时钟指针就不会继承上一个指针的变换。如果不使用 painter.save() 和 painter.restore(),我们将在绘制下一个指针之前手动更改位置、旋转和缩放。
不使用 painter.save() 和 painter.restore() 的一个好例子是在绘制表盘时。由于每个表盘的旋转是前一个表盘的六度增量,我们根本不需要保存画家的状态。我们只需要在循环中调用 painter.rotate(6.0),每个表盘都会继承前一个表盘的旋转。我们还使用模运算符 (%) 来检查表盘所代表的单位是否能被五整除。如果可以,我们就绘制它稍微长一点。
如果不使用定时器不断调用 update() 槽,时钟将无法正常工作。这是因为当父窗口(在这种情况下是主窗口)的状态没有变化时,Qt 不会调用 paintEvent()。因此,我们需要手动告诉 Qt 我们需要每秒刷新一次图形,通过调用 update()。
我们使用 painter.setRenderHint(QPainter::Antialiasing) 函数在渲染时钟时启用反走样。没有反走样,图形看起来会非常锯齿和像素化:

图 4.7 – 反走样产生更平滑的结果
还有更多...
QPainter 类使用坐标系来确定图形在屏幕上渲染之前的位置和大小。这些信息可以被更改,使图形出现在不同的位置、旋转和大小。改变图形坐标信息的过程就是我们所说的坐标变换。有多种类型的变换:其中包含平移、旋转、缩放和剪切:

图 4.8 – 不同类型的变换
Qt 使用一个以左上角为原点的坐标系,这意味着 x 值向右增加,y 值向下增加。这个坐标系可能与物理设备(如计算机屏幕)使用的坐标系不同。Qt 通过使用 QPaintDevice 类自动处理这个问题,它将 Qt 的逻辑坐标映射到物理坐标。
QPainter 提供了四种变换操作以执行不同类型的变换:
-
QPainter::translate(): 这将图形的位置偏移给定的一组单位 -
QPainter::rotate(): 这将图形按顺时针方向围绕原点旋转 -
QPainter::scale(): 这将图形的大小偏移给定的一个因子 -
QPainter::shear(): 这将图形的坐标系围绕原点扭曲
在屏幕上显示图像
Qt 不仅允许我们在屏幕上绘制形状和图像,还允许我们将多个图像叠加在一起,并使用不同类型的算法结合所有层的像素信息,从而创建非常有趣的结果。在本例中,我们将学习如何将图像叠加在一起并应用不同的合成效果。
如何做到这一点...
让我们通过以下步骤创建一个简单的演示,展示不同图像合成效果:
-
首先,设置一个新的
menuBar、mainToolBar和statusBar,就像我们在第一个菜谱中所做的那样。 -
接下来,将
QPainter类头文件添加到mainwindow.h文件中:#include <QPainter> -
之后,声明
paintEvent()虚拟函数,如下所示:virtual void paintEvent(QPaintEvent* event); -
在
mainwindow.cpp中,我们将首先使用QImage类加载几个图像文件:void MainWindow::paintEvent(QPaintEvent* event) { QImage image; image.load("checker.png"); QImage image2; image2.load("tux.png"); QImage image3; image3.load("butterfly.png"); } -
然后,创建一个
QPainter对象并使用它来绘制两对图像,其中一对图像位于另一对图像之上:QPainter painter(this); painter.drawImage(QPoint(10, 10), image); painter.drawImage(QPoint(10, 10), image2); painter.drawImage(QPoint(300, 10), image); painter.drawImage(QPoint(300, 40), image3); -
现在,编译并运行程序,你应该会看到类似这样的内容:

图 4.9 – 正常显示图像
-
接下来,我们在屏幕上绘制每个图像之前设置合成模式:
QPainter painter(this); painter.setCompositionMode(QPainter::CompositionMode_Difference); painter.drawImage(QPoint(10, 10), image); painter.setCompositionMode(QPainter::CompositionMode_Multiply); painter.drawImage(QPoint(10, 10), image2); painter.setCompositionMode(QPainter::CompositionMode_Xor); painter.drawImage(QPoint(300, 10), image); painter.setCompositionMode(QPainter::CompositionMode_SoftLight); painter.drawImage(QPoint(300, 40), image3); -
再次编译并运行程序,你现在将看到类似这样的内容:

图 4.10 – 将不同的合成模式应用于图像
它是如何工作的...
当使用 Qt 绘制图像时,调用 drawImage() 函数的顺序将决定哪个图像先被渲染,哪个图像后被渲染。这将影响图像的深度顺序并产生不同的结果。
在上一个示例中,我们四次调用drawImage()函数来在屏幕上绘制四个不同的图像。第一个drawImage()函数渲染checker.png,第二个drawImage()函数渲染tux.png(企鹅)。稍后渲染的图像将始终出现在其他图像之前,这就是为什么企鹅出现在棋盘格图案之前。对于蝴蝶和右侧的棋盘格图案也是如此。尽管蝴蝶被渲染在它前面,但你仍然可以看到棋盘格图案的原因是蝴蝶图像不是完全不透明的。
现在,让我们反转渲染顺序,看看会发生什么。我们将尝试首先渲染企鹅,然后是棋盘格方框。对于右侧的其他图像对也是如此:蝴蝶首先被渲染,然后是棋盘格方框:

图 4.11 – 企鹅和蝴蝶都被棋盘格方框覆盖
要将合成效果应用到图像上,我们必须在绘制图像之前设置画家的合成模式,通过调用painter.setCompositionMode()函数。您可以通过输入QPainter::CompositionMode从自动完成菜单中选择所需的合成模式。
在上一个示例中,我们将QPainter::CompositionMode_Difference应用于左侧的棋盘格方框,这反转了其颜色。接下来,我们将QPainter::CompositionMode_Overlay应用于企鹅,使其与棋盘格图案混合,从而能够看到两个图像重叠。在右侧,我们将QPainter::CompositionMode_Xor应用于棋盘格方框,如果源和目标之间存在差异,则显示颜色;否则,将渲染为黑色。
由于它是与白色背景比较差异,所以棋盘格方框的不透明部分变成了完全黑色。我们还对蝴蝶图像应用了QPainter::CompositionMode_SoftLight。这会以降低对比度的方式将像素与背景混合。如果您想在继续进行下一渲染之前禁用之前设置的合成模式,只需将其设置回默认模式,即QPainter::CompositionMode_SourceOver。
还有更多...
例如,我们可以在多个图像上方叠加,并使用 Qt 的图像合成功能将它们合并在一起,并根据我们使用的合成模式计算屏幕上的结果像素。这在像 Photoshop 和 GIMP 这样的图像编辑软件中经常用于合成图像图层。
Qt 中提供了超过 30 种合成模式。以下是一些最常用的模式:
-
清除:目标像素设置为完全透明,与源无关。 -
源:输出是源像素。此模式是CompositionMode_Destination的逆模式。 -
目标: 输出是目标像素。这意味着混合没有效果。此模式是CompositionMode_Source的逆模式。 -
源覆盖: 这通常被称为QPainter。 -
目标覆盖: 输出是覆盖在源像素上的目标 alpha 值的混合。此模式的相反是CompositionMode_SourceOver。 -
源输入: 输出是源,其中 alpha 值通过目标 alpha 值进行减少。 -
目标输入: 输出是目标,其中 alpha 值通过源 alpha 值进行减少。此模式是CompositionMode_SourceIn的逆模式。 -
源输出: 输出是源,其中 alpha 值通过目标值的倒数进行减少。 -
目标输出: 输出是目标,其中 alpha 值通过源 alpha 值的倒数进行减少。此模式是CompositionMode_SourceOut的逆模式。 -
源叠加: 源像素在目标像素上方进行混合,源像素的 alpha 值通过目标像素的 alpha 值进行减少。 -
目标叠加: 目标像素在源像素上方进行混合,源像素的 alpha 值通过目标像素的 alpha 值进行减少。此模式是CompositionMode_SourceAtop的逆模式。 -
Xor: 这是“排他或”的缩写,这是一种主要用于图像分析的先进混合模式。使用此模式与这种合成模式相比要复杂得多。首先,通过目标 alpha 值的倒数减少源 alpha 值。然后,通过源 alpha 值的倒数减少目标 alpha 值。最后,将源和目标合并以产生输出。
注意
更多信息,您可以访问此链接:pyside.github.io。
以下图显示了使用不同合成模式叠加两个图像的结果:

图 4.12 – 不同类型的合成模式
将图像效果应用于图形
Qt 提供了一种简单的方法,可以将图像效果添加到使用QPainter类绘制的任何图形中。在本例中,我们将学习如何将不同的图像效果,如阴影、模糊、着色和透明度效果,应用于图形,然后再将其显示在屏幕上。
如何做到这一点…
让我们通过以下步骤学习如何将图像效果应用于文本和图形:
-
创建一个新的
menuBar、mainToolBar和StatusBar。 -
通过访问文件 | 新建文件或项目来创建一个新的资源文件,并将项目所需的所有图像添加进去:

图 4.13 – 创建新的 Qt 资源文件
- 接下来,打开
mainwindow.ui文件,并在窗口中添加四个标签。其中两个标签将是文本,另外两个我们将加载到资源文件中我们刚刚添加的图像:

图 4.14 – 填满文本和图像的应用程序
-
你可能已经注意到字体大小比默认大小大得多。这可以通过向标签小部件添加样式表来实现,例如,如下所示:
font: 26pt "MS Gothic"; -
之后,打开
mainwindow.cpp并在源代码顶部包含以下头文件:#include <QGraphicsBlurEffect> #include <QGraphicsDropShadowEffect> #include <QGraphicsColorizeEffect> #include <QGraphicsOpacityEffect> -
然后,在
MainWindow类的构造函数中,添加以下代码来创建一个DropShadowEffect并将其应用于一个标签:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect(); shadow->setXOffset(4); shadow->setYOffset(4); ui->label->setGraphicsEffect(shadow); } -
接下来,我们将创建
ColorizedEffect并将其应用于其中一张图片,在这种情况下,是蝴蝶。我们还设置了效果颜色为红色:QGraphicsColorizeEffect* colorize = new QGraphicsColorizeEffect(); colorize->setColor(QColor(255, 0, 0)); ui->butterfly->setGraphicsEffect(colorize); -
完成这些后,创建
BlurEffect并将其半径设置为12。然后,将图形效果应用于其他标签:QGraphicsBlurEffect* blur = new QGraphicsBlurEffect(); blur->setBlurRadius(12); ui->label2->setGraphicsEffect(blur); -
最后,创建一个 alpha 效果并将其应用于企鹅图片。我们将不透明度值设置为
0.2,这意味着 20%的不透明度:QGraphicsOpacityEffect* alpha = new QGraphicsOpacityEffect(); alpha->setOpacity(0.2); ui->penguin->setGraphicsEffect(alpha); -
现在,编译并运行程序,你应该能看到类似这样的效果:

图 4.15 – 将不同类型的图形效果应用于文本和图像
它是如何工作的...
每个图形效果都是一个继承自QGraphicsEffect父类的类。你可以通过创建一个新的继承自QGraphicsEffect的类并重新实现其中的一些函数来创建自己的自定义效果。
每个效果都有自己的一组变量,这些变量专门为它创建。例如,你可以设置着色效果的色彩,但在模糊效果中没有这样的变量。这是因为每个效果与其他效果大不相同,这也是为什么它需要成为一个单独的类,而不是使用相同的类来处理所有不同的效果。
在同一时间只能向小部件添加一个图形效果。如果你添加了多个效果,只有最后一个效果会被应用于小部件,因为它会替换前面的一个。除此之外,请注意,如果你创建了一个图形效果,例如,阴影效果,你不能将其分配给两个不同的小部件,因为它只会分配到最后一个应用了它的小部件。如果你需要将相同类型的效应应用于多个不同的小部件,请创建几个相同类型的图形效果,并将每个效果应用于相应的小部件。
还有更多...
目前,Qt 支持模糊、阴影、着色和不透明度效果。这些效果可以通过调用以下类来使用:QGraphicsBlurEffect、QGraphicsDropShadowEffect、QGraphicsColorizeEffect和QGraphicsOpacityEffect。所有这些类都是继承自QGraphicsEffect类的。你也可以通过创建QGrapicsEffect(或任何其他现有效果)的子类并重新实现其中的draw()函数来创建自己的自定义图像效果。
图形效果只改变源矩形的边界框。如果你想增加边界框的边距,重新实现虚拟函数boundingRectFor(),并调用updateBoundingRect()来通知框架每次此矩形变化时:
创建一个基本的绘图程序
由于我们已经学到了很多关于QPainter类及其如何在屏幕上显示图形的知识,我想现在是时候让我们做一些有趣的事情,将我们的知识付诸实践了。
在这个菜谱中,我们将学习如何制作一个基本的绘图程序,允许我们使用不同的画笔大小和颜色在画布上绘制线条。我们还将学习如何使用QImage类和鼠标事件来构建绘图程序。
如何做到这一点...
让我们按照以下步骤开始我们的有趣项目:
-
再次,我们首先创建一个新的Qt Widgets Application项目,并移除工具栏和状态栏。这次我们将保留菜单栏。
-
之后,按照如下设置菜单栏:

图 4.16 – 设置菜单栏
-
我们暂时保持菜单栏不变,所以让我们继续到
mainwindow.h文件。首先,包含以下头文件,因为它们是项目所需的:#include <QPainter> #include <QMouseEvent> #include <QFileDialog> -
接下来,声明我们将在这个项目中使用的变量,如下所示:
private: Ui::MainWindow *ui; QImage image; bool drawing; QPoint lastPoint; int brushSize; QWidget class. These functions will be triggered by Qt when the respective event happens. We will override these functions and tell Qt what to do when these events get called:public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
virtual void mousePressEvent(QMouseEvent *event);
virtual void mouseMoveEvent(QMouseEvent *event);
virtual void mouseReleaseEvent(QMouseEvent *event);
virtual void paintEvent(QPaintEvent *event);
在
mainwindow.cpp文件中添加以下代码到类构造函数中,以设置一些变量:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); image = QImage(this->size(), QImage::Format_RGB32); image.fill(Qt::white); drawing = false; brushColor = Qt::black; brushSize = 2; } -
接下来,我们将构建
mousePressEvent()事件,并告诉 Qt 当左键被按下时应该做什么:void MainWindow::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { drawing = true; lastPoint = event->pos(); } } -
然后,我们将构建
mouseMoveEvent()事件,并告诉 Qt 当鼠标移动时应该做什么。在这种情况下,如果左键被按下,我们想在画布上绘制线条:void MainWindow::mouseMoveEvent(QMouseEvent *event) { if ((event->buttons() & Qt::LeftButton) && drawing) { QPainter painter(&image); painter.setPen(QPen(brushColor, brushSize, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); painter.drawLine(lastPoint, event->pos()); lastPoint = event->pos(); this->update(); } } -
之后,我们还将构建
mouseReleaseEvent()事件,该事件将在鼠标按钮释放时触发:void MainWindow::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { drawing = false; } } -
完成这些后,我们将继续到
paintEvent()事件,与之前章节中看到的其他示例相比,这个事件非常简单:void MainWindow::paintEvent(QPaintEvent *event) { QPainter canvasPainter(this); canvasPainter.drawImage(this->rect(), image, image.rect()); } -
记得我们有一个菜单栏在那里无所事事吗?让我们在 GUI 编辑器下面的每个动作上右键单击,并在弹出菜单中选择转到槽函数…。我们想要告诉 Qt 当菜单栏上的每个选项被选中时应该做什么:

图 4.17 – 为每个菜单动作创建槽函数
-
然后,选择默认的槽函数
triggered(),并按mainwindow.h和mainwindow.cpp文件。完成所有操作后,你应该能在你的mainwindow.h文件中看到如下内容:private slots: void on_actionSave_triggered(); void on_actionClear_triggered(); void on_action2px_triggered(); void on_action5px_triggered(); void on_action10px_triggered(); void on_actionBlack_triggered(); void on_actionWhite_triggered(); void on_actionRed_triggered(); void on_actionGreen_triggered(); void on_actionBlue_triggered(); -
接下来,我们将告诉 Qt 在这些槽被触发时应该做什么:
void MainWindow::on_actionSave_triggered() { QString filePath = QFileDialog::getSaveFileName(this, «Save Image», «», «PNG (*.png);;JPEG (*.jpg *.jpeg);;All files (*.*)»); if (filePath == "") return; image.save(filePath); } void MainWindow::on_actionClear_triggered() { image.fill(Qt::white); this->update(); } -
然后,我们继续实现其他槽:
void MainWindow::on_action2px_triggered() { brushSize = 2; } void MainWindow::on_action5px_triggered() { brushSize = 5; } void MainWindow::on_action10px_triggered() { brushSize = 10; } void MainWindow::on_actionBlack_triggered() { brushColor = Qt::black; } -
最后,我们实现其余的槽函数:
void MainWindow::on_actionWhite_triggered() { brushColor = Qt::white; } void MainWindow::on_actionRed_triggered() { brushColor = Qt::red; } void MainWindow::on_actionGreen_triggered() { brushColor = Qt::green; } void MainWindow::on_actionBlue_triggered() { brushColor = Qt::blue; } -
如果我们现在编译并运行程序,我们将得到一个简单但可用的绘图程序:

图 4.18 – 我们可爱的绘图程序正在运行!
它是如何工作的...
在这个例子中,我们在程序启动时创建了一个 QImage 小部件。这个小部件充当画布,并且每当窗口大小改变时都会跟随窗口的大小。为了在画布上绘制东西,我们需要使用 Qt 提供的鼠标事件。这些事件会告诉我们光标的位置,我们可以使用这些信息来改变画布上的像素。
我们使用一个名为 drawing 的布尔变量来让程序知道当鼠标按钮被按下时是否应该开始绘图。在这种情况下,当左键被按下时,drawing 变量将被设置为 true。我们还在左键被按下时将当前光标位置保存到 lastPoint 变量中,这样 Qt 就会知道它应该从哪里开始绘图。当鼠标移动时,Qt 将触发 mouseMoveEvent() 事件。这就是我们需要检查绘图变量是否被设置为 true 的地方。如果是,那么 QPainter 可以根据我们提供的画笔设置开始在 QImage 小部件上绘制线条。画笔设置包括 brushColor 和 brushSize。这些设置被保存为变量,并且可以通过从菜单栏选择不同的设置来更改。
请记住,当用户在画布上绘图时调用 update() 函数。否则,即使我们改变了画布的像素信息,画布也将保持空白。我们还需要在从菜单栏选择 文件 | 清除 时调用 update() 函数来重置我们的画布。
在这个例子中,我们使用 QImage::save() 来保存图像文件,这非常直接。我们使用文件对话框让用户决定保存图像的位置和期望的文件名。然后,我们将信息传递给 QImage,它将自行完成剩余的工作。如果我们没有指定 QImage::save() 函数的文件格式,QImage 将通过查看期望文件名的扩展名来尝试自己找出它。
在 QML 中渲染 2D 画布
在本章的所有前例中,我们讨论了使用 Qt 的 C++ API 渲染 2D 图形的方法和技术。然而,我们还没有学习如何使用强大的 QML 脚本达到类似的效果。
如何做到这一点…
在这个项目中,我们将做一些相当不同的事情:
- 如同往常,第一步是创建一个新的项目,通过访问 文件 | 新建文件或项目 并选择 Qt 快速应用程序 作为项目模板:

图 4.19 – 创建新的 Qt Quick 应用程序项目
-
创建完新项目后,打开
main.qml,它在项目面板下的qml.qrc中列出。之后,为窗口设置一个 ID,并将它的width和height值调整到更大的值,如下所示:import QtQuick import QtQuick.Window Window { id: myWindow visible: true width: 640 height: 480 title: qsTr("Hello World") } -
然后,在
myWindow下添加一个Canvas对象,并将其命名为myCanvas。之后,我们将它的width和height值设置为与myWindow相同:Window { id: myWindow visible: true width: 640 height: 480 Canvas { id: myCanvas width: myWindow.width height: myWindow.height } } -
接下来,我们定义当
onPaint事件被触发时会发生什么;在这种情况下,我们将在窗口上绘制一个十字:Canvas { id: myCanvas width: myWindow.width height: myWindow.height onPaint: { var context = getContext('2d') context.fillStyle = 'white' context.fillRect(0, 0, width, height) context.lineWidth = 2 context.strokeStyle = 'black' -
让我们继续编写代码,如下所示:
// Draw cross context.beginPath() context.moveTo(50, 50) context.lineTo(100, 100) context.closePath() context.stroke() context.beginPath() context.moveTo(100, 50) context.lineTo(50, 100) context.closePath() context.stroke() } } -
然后,我们添加以下代码在十字交叉处绘制一个勾号:
// Draw tick context.beginPath() context.moveTo(150, 90) context.lineTo(158, 100) context.closePath() context.stroke() context.beginPath() context.moveTo(180, 100) context.lineTo(210, 50) context.closePath() context.stroke() -
然后,通过添加以下代码绘制一个三角形形状:
// Draw triangle context.lineWidth = 4 context.strokeStyle = "red" context.fillStyle = "salmon" context.beginPath() context.moveTo(50,150) context.lineTo(150,150) context.lineTo(50,250) context.closePath() context.fill() context.stroke() -
之后,使用以下代码绘制一个半圆和一个完整圆:
// Draw circle context.lineWidth = 4 context.strokeStyle = "blue" context.fillStyle = "steelblue" var pi = 3.141592653589793 context.beginPath() context.arc(220, 200, 60, 0, pi, true) context.closePath() context.fill() context.stroke() -
然后,我们绘制一个圆弧:
context.beginPath() context.arc(220, 280, 60, 0, 2 * pi, true) context.closePath() context.fill() context.stroke() -
最后,我们从文件中绘制一个 2D 图像:
// Draw image context.drawImage("tux.png", 280, 10, 150, 174) -
然而,仅凭前面的代码无法在屏幕上成功渲染图像,因为你必须先加载图像文件。在
Canvas对象中添加以下代码,以便在程序启动时让 QML 加载图像文件,然后在图像加载后调用requestPaint()信号,以便触发onPaint()事件槽:onImageLoaded: requestPaint(); onPaint: { // The code we added previously } -
然后,通过在项目面板中右键单击
qml.qrc并选择将tux.png图像文件添加到我们的项目资源中打开它:

图 4.20 – tux.png 图像文件现在在 qml.qrc 下列出
- 现在,构建并运行程序,你应该会得到以下结果:

图 4.21 – 图形形状让企鹅图克斯感到有趣
在前面的例子中,我们学习了如何使用Canvas元素在我们的屏幕上绘制简单的矢量形状。Qt 的内置模块使程序员对复杂渲染过程的处理更加简单。
第五章:OpenGL 实现
在本章中,我们将学习如何使用 开放图形库 (OpenGL),一个强大的渲染 应用程序程序接口 (API),并将其与 Qt 结合使用。OpenGL 是一个跨语言、跨平台的 API,它通过我们计算机图形芯片内的 图形处理单元 (GPU) 在屏幕上绘制 2D 和 3D 图形。在本章中,我们将学习 OpenGL 3 而不是 2,因为尽管与较新的可编程管道相比,固定功能管道对初学者来说更容易理解,但它被认为是遗留代码,并且已被大多数现代 3D 渲染软件弃用。Qt 6 支持这两个版本,因此如果你需要软件的向后兼容性,切换到 OpenGL 2 应该没有问题。
在本章中,我们将涵盖以下主要主题:
-
在 Qt 中设置 OpenGL
-
Hello World!
-
渲染 2D 形状
-
渲染 3D 形状
-
OpenGL 中的纹理
-
OpenGL 中的基本光照
-
使用键盘控制移动对象
-
Qt Quick 3D 在 QML 中
技术要求
本章的技术要求包括 Qt 6.6.1 MinGW 64 位和 Qt Creator 12.0.2。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter05。
在 Qt 中设置 OpenGL
在这个菜谱中,我们将学习如何在 Qt 6 中设置 OpenGL。
如何做…
按照以下步骤学习如何在 Qt 中设置 OpenGL:
-
创建一个新的
mainwindow.ui、mainwindow.h和mainwindow.cpp文件。 -
打开你的项目文件(
.pro)并在QT +=后面添加opengl关键字,然后运行qmake重新加载项目模块:QT += core gui opengl -
你还需要在项目文件中添加另一行,以便在启动时加载 OpenGL 和 OpenGL 实用工具 (GLU) 库。没有这两个库,你的程序将无法运行:
LIBS += -lopengl32 -lglu32 -
打开
main.cpp并将mainwindow.h替换为QtOpenGL头文件:#include <QtOpenGL> -
从你的
main.cpp文件中删除所有与MainWindow类相关的代码,并用以下片段中突出显示的代码替换:#include <QApplication> #include <QtOpenGL> int main(int argc, char *argv[]) { QApplication app(argc, argv); QOpenGLWindow window; window.setTitle("Hello World!"); window.resize(640, 480); window.show(); return app.exec(); } -
如果你现在编译并运行项目,你将看到一个背景为黑色的空窗口。不要担心——你的程序现在正在 OpenGL 上运行:

图 5.1 - 一个空的 OpenGL 窗口
它是如何工作的…
为了访问与 OpenGL 相关的头文件,例如QtOpenGL和QOpenGLFunctions,必须在项目文件(.pro)中添加OpenGL模块。我们使用QOpenGLWindow类而不是QMainWindow作为主窗口,因为它旨在轻松创建执行 OpenGL 渲染的窗口,并且由于它在其小部件模块中没有依赖项,因此与QOpenGLWidget相比提供了更好的性能。
我们必须调用setSurfaceType(QWindow::OpenGLSurface)来告诉 Qt 我们更愿意使用 OpenGL 将图像渲染到屏幕上而不是使用QPainter。QOpenGLWindow类提供了几个虚拟函数(initializeGL()、resizeGL()、paintGL()等),使我们能够方便地设置 OpenGL 并执行图形渲染。我们将在下面的示例中学习如何使用这些函数。
还有更多...
OpenGL 是我们计算机图形芯片内通过 GPU 绘制 2D 和 3D 图形的跨语言、跨平台 API。计算机图形技术多年来一直在快速发展——发展如此之快,以至于软件行业几乎跟不上其步伐。
在 2008 年,维护和开发 OpenGL 的 Khronos Group 公司宣布发布了 OpenGL 3.0 规范,这在整个行业中引起了巨大的骚动和争议。这主要是因为 OpenGL 3.0 本应从 OpenGL API 中废弃整个固定功能管道,对于大玩家来说,从固定功能管道突然一夜之间切换到可编程管道几乎是不可能的任务。这导致了维护两个不同主要版本的 OpenGL。
在本章中,我们将使用较新的 OpenGL 3 而不是较旧的已废弃的 OpenGL 2。这两个版本之间的编码风格和语法非常不同,这使得切换非常麻烦。然而,性能提升将使切换到 OpenGL 3 所花费的时间变得值得。
Hello World!
在本章中,我们将学习如何使用 Qt 6 的 OpenGL 3。常见的 OpenGL 函数,如glBegin、glVertex2f、glColor3f、glMatrixMode和glLoadIdentity,都已从 OpenGL 3 中删除。OpenGL 3 使用glVertex2f(),这会减慢渲染速度,因为需要等待 CPU 逐个提交数据。因此,我们将所有数据打包到 VBO 中,一次性发送给 GPU,并指导 GPU 通过着色器编程计算结果像素。我们还将学习如何通过类似于 C 的编程语言OpenGL 着色器语言(GLSL)创建简单的着色器程序。
如何做到这一点...
让我们按照以下步骤开始:
- 我们将创建一个新的类
RenderWindow,它继承自QOpenGLWindow类。转到RenderWindow并设置其基类为QOpenGLWindow。然后,继续创建 C++类:

图 5.2 – 定义你的自定义渲染窗口类
-
前往我们刚刚创建的
renderwindow.h文件,并在源代码顶部添加以下头文件:#include <GL/glu.h> #include <QtOpenGL> #include <QSurfaceFormat> #include <QOpenGLFunctions> #include <QOpenGLWindow> #include <QOpenGLBuffer> #include <QOpenGLVertexArrayObject> #include <QOpenGLShader> #include <QOpenGLShaderProgram> -
我们需要创建几个看起来像这样的函数和变量:
class RenderWindow : public QOpenGLWindow { public: RenderWindow(); protected: void initializeGL(); void paintGL(); void paintEvent(QPaintEvent *event); void resizeEvent(QResizeEvent *event); -
我们将继续并添加一些私有变量:
private: QOpenGLContext* openGLContext; QOpenGLFunctions* openGLFunctions; QOpenGLShaderProgram* shaderProgram; QOpenGLVertexArrayObject* vao; QOpenGLBuffer* vbo_vertices; }; -
打开
renderwindow.cpp文件,并按如下定义类构造函数。我们必须告诉渲染窗口使用 OpenGL 表面类型;启用核心配置文件(而不是兼容性配置文件),运行 3.2 版本;创建 OpenGL 上下文;最后,将我们刚刚创建的配置文件应用到上下文中:RenderWindow::RenderWindow() { setSurfaceType(QWindow::OpenGLSurface); QSurfaceFormat format; format.setProfile(QSurfaceFormat::CoreProfile); format.setVersion(3, 2); setFormat(format); openGLContext = new QOpenGLContext(); openGLContext->setFormat(format); openGLContext->create(); openGLContext->makeCurrent(this); } -
我们需要按如下定义
initializeGL()函数。这个函数将在渲染开始之前被调用。首先,我们定义顶点着色器和片段着色器:void RenderWindow::initializeGL() { openGLFunctions = openGLContext->functions(); static const char *vertexShaderSource = "#version 330 core\n" "layout(location = 0) in vec2 posAttr;\n" "void main() {\n" "gl_Position = vec4(posAttr, 0.0, 1.0); }"; static const char *fragmentShaderSource = "#version 330 core\n" "out vec4 col;\n" "void main() {\n" "col = vec4(1.0, 0.0, 0.0, 1.0); }"; -
我们初始化
shaderProgram并声明一个顶点数组。然后,我们创建一个QOpenGLVertexArrayObject对象:shaderProgram = new QOpenGLShaderProgram(this); shaderProgram->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource); shaderProgram->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource); shaderProgram->link(); // The vertex coordinates of our triangle GLfloat vertices[] = { -1.0f, -1.0f, 1.0f, -1.0f, 0.0f, 1.0f }; vao = new QOpenGLVertexArrayObject(); vao->create(); vao->bind(); -
让我们继续编写我们的代码,通过定义
vbo_vertices:vbo_vertices = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); vbo_vertices->create(); vbo_vertices->setUsagePattern(QOpenGLBuffer::StaticDraw); vbo_vertices->bind(); vbo_vertices->allocate(vertices, sizeof(vertices) * sizeof(GLfloat)); vao->release(); } -
我们将首先向
paintEvent()函数添加一些代码:void RenderWindow::paintEvent(QPaintEvent *event) { Q_UNUSED(event); glViewport(0, 0, width(), height()); // Clear our screen with corn flower blue color glClearColor(0.39f, 0.58f, 0.93f, 1.f); glClear(GL_COLOR_BUFFER_BIT); -
然后我们在调用
glDrawArrays()之前绑定 VAO 和着色器程序:vao->bind(); shaderProgram->bind(); shaderProgram->bindAttributeLocation("posAttr", 0); shaderProgram->enableAttributeArray(0); shaderProgram->setAttributeBuffer(0, GL_FLOAT, 0, 2); glDrawArrays(GL_TRIANGLES, 0, 3); shaderProgram->release(); vao->release(); } -
你可以通过添加以下代码在渲染窗口被调整大小时刷新视口:
void RenderWindow::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); glViewport(0, 0, this->width(), this->height()); this->update(); } -
如果你现在编译并运行项目,你应该能够看到在蓝色背景前绘制的一个红色矩形:

图 5.3 - 使用 OpenGL 渲染的第一个三角形
它是如何工作的...
我们必须将 OpenGL 版本设置为 3.x,并将表面格式设置为核心配置文件,以便我们可以访问全新的着色器管道,这与旧的、已弃用的兼容性配置文件完全不同。OpenGL 2.x 仍然存在于兼容性配置文件中,仅为了允许 OpenGL 程序在旧硬件上运行。创建的配置文件必须在它生效之前应用到 OpenGL 上下文中。
在 OpenGL 3 及其后续版本中,大多数计算都是在 GPU 通过着色器程序完成的,因为所有常见的固定功能函数现在都已完全弃用。因此,我们在前面的例子中创建了一个非常简单的顶点着色器和片段着色器。
着色器程序由三个不同的部分组成:几何着色器(可选)、顶点着色器和片段着色器。几何着色器在将数据传递给顶点着色器之前计算几何体的创建;顶点着色器在将数据传递给片段着色器之前处理顶点的位置和运动;最后,片段着色器计算并显示屏幕上的最终像素。
在前面的例子中,我们只使用了顶点和片段着色器,并排除了几何着色器,因为它是可选的。你可以将 GLSL 代码保存到文本文件中,并通过调用addShaderFromFile()将其加载到你的 Qt 6 程序中,但由于我们的着色器非常简单且简短,我们只是在 C++源代码中直接定义它。
之后,我们使用 VBO 批量存储顶点位置,然后将其发送到 GPU。我们还可以使用 VBO 存储其他信息,例如法线、纹理坐标和顶点颜色。只要它与你的着色器代码中的输入匹配,你就可以将任何东西发送到 GPU。然后,我们将 VBO 添加到顶点数组对象(VAO)中,并将整个 VAO 发送到 GPU 进行处理。由于 VAO 就像任何普通的 C++数组一样,你可以将许多不同的 VBO 添加到 VAO 中。
就像我们在前面的章节中学到的,所有的绘图都发生在paintEvent()函数中,并且只有当 Qt 认为有必要刷新屏幕时,它才会被调用。要强制 Qt 更新屏幕,请手动调用update()。此外,每次窗口屏幕被调整大小时,我们必须通过调用glViewport(x, y, width, height)来更新视口。
渲染 2D 形状
由于我们已经学会了如何在屏幕上绘制第一个矩形,我们将在本节中进一步改进它。我们将从上一个例子继续进行。
如何做到这一点...
让我们通过以下示例开始:
-
打开
renderwindow.h并添加两个额外的 VBO,一个称为vbo_vertices2,另一个称为vbo_colors,如下面的代码所示:private: QOpenGLContext* openGLContext; QOpenGLFunctions* openGLFunctions; QOpenGLShaderProgram* shaderProgram; QOpenGLVertexArrayObject* vao; QOpenGLBuffer* vbo_vertices; QOpenGLBuffer* vbo_vertices2; renderwindow.cpp and add the following code to the shader code, as highlighted in the following snippet:static const char *vertexShaderSource =
"#version 330 core\n"
"layout(location = 0) in vec2 posAttr;\n"
"layout(location = 1) in vec3 colAttr;\n"
"out vec3 fragCol;\n"
"void main() {\n"
"fragCol = colAttr;\n"
"gl_Position = vec4(posAttr, 1.0, 1.0); }";
-
将高亮显示的代码添加到片段着色器中,如下所示:
static const char *fragmentShaderSource = "#version 330 core\n" "in vec3 fragCol;\n" "out vec4 col;\n" "void main() {\n" "col = vec4(fragCol, 1.0); }"; -
将顶点数组更改为以下代码类似的内容。我们在这里做的是创建三个数组,它们保存两个三角形的顶点和它们的颜色,以便我们可以在稍后的阶段将它们传递给片段着色器:
GLfloat vertices[] = { -0.3f, -0.5f, 0.8f, -0.4f, 0.2f, 0.6f }; GLfloat vertices2[] = { 0.5f, 0.3f, 0.4f, -0.8f, -0.6f, -0.2f }; GLfloat colors[] = { 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f }; -
由于我们在前面的例子中已经初始化了
vbo_vertices,这次我们只需要初始化另外两个 VBO,即vbo_vertices和vbo_colors:vbo_vertices2 = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); vbo_vertices2->create(); vbo_vertices2->setUsagePattern(QOpenGLBuffer::StaticDraw); vbo_vertices2->bind(); vbo_vertices2->allocate(vertices2, sizeof(vertices2) * sizeof(GLfloat)); vbo_colors = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); vbo_colors->create(); vbo_colors->setUsagePattern(QOpenGLBuffer::StaticDraw); vbo_colors->bind(); vbo_colors->allocate(colors, sizeof(colors) * sizeof(GLfloat)); -
在我们开始使用
glDrawArrays()绘制三角形之前,我们还必须将vbo_colors的数据添加到我们的着色器的colAttr属性中。确保在将数据发送到着色器之前调用bind()来设置 VBO 为当前活动 VBO。位置 ID(在这种情况下,0和1)必须与你的着色器中使用的位置 ID 匹配:vbo_vertices->bind(); shaderProgram->bindAttributeLocation("posAttr", 0); shaderProgram->enableAttributeArray(0); shaderProgram->setAttributeBuffer(0, GL_FLOAT, 0, 2); vbo_colors->bind(); shaderProgram->bindAttributeLocation("colAttr", 1); shaderProgram->enableAttributeArray(1); shaderProgram->setAttributeBuffer(1, GL_FLOAT, 0, 3); glDrawArrays(GL_TRIANGLES, 0, 3); -
在前面的代码之后,我们将发送
vbo_vertices2和vbo_colors到着色器属性,并再次调用glDrawArrays()来绘制第二个三角形:vbo_vertices2->bind(); shaderProgram->bindAttributeLocation("posAttr", 0); shaderProgram->enableAttributeArray(0); shaderProgram->setAttributeBuffer(0, GL_FLOAT, 0, 2); vbo_colors->bind(); shaderProgram->bindAttributeLocation("colAttr", 1); shaderProgram->enableAttributeArray(1); shaderProgram->setAttributeBuffer(1, GL_FLOAT, 0, 3); glDrawArrays(GL_TRIANGLES, 0, 3); -
如果你现在构建程序,你应该能在屏幕上看到两个三角形,其中一个三角形位于另一个三角形之上:

图 5.4 – 两个相互重叠的有色三角形
它是如何工作的...
OpenGL 支持的几何原语类型包括点、线、线段、线环、多边形、四边形、四边形带、三角形、三角形带和三角形扇。在这个例子中,我们画了两个三角形,每个形状都提供了一组顶点和颜色,这样 OpenGL 就知道如何渲染这些形状。
彩虹色效果是通过给每个顶点赋予不同的颜色来创建的。OpenGL 将自动在每两个顶点之间插值颜色,并在屏幕上显示。目前,首先渲染的形状将出现在后来渲染的其他形状的后面。这是因为我们正在 2D 空间中渲染形状,没有涉及深度信息来检查哪个形状位于前面等等。我们将在下面的例子中学习如何进行深度检查。
渲染 3D 形状
在上一节中,我们学习了如何在屏幕上绘制简单的 2D 形状。然而,为了充分利用 OpenGL API,我们还需要学习如何使用它来渲染 3D 图像。简而言之,3D 图像只是使用 2D 形状创建的幻觉,这些形状堆叠在一起,使得它们看起来像是 3D 的。
如何做到这一点...
这里的主要成分是深度值,它决定了哪些形状应该出现在其他形状的前面或后面。位于另一个表面后面(深度比另一个形状浅)的原始形状将不会被渲染(或只会部分渲染)。OpenGL 提供了一种简单的方法来实现这一点:
-
让我们继续我们的上一个 2D 示例项目。通过在
renderwindow.cpp中的initializeGL()函数中添加glEnable(GL_DEPTH_TEST)来启用深度测试:void RenderWindow::initializeGL() { openGLFunctions = openGLContext->functions(); GL_DEPTH_TEST in the preceding step, we must also set the depth buffer size when setting the OpenGL profile:QSurfaceFormat format;
format.setProfile(QSurfaceFormat::CoreProfile);
format.setVersion(3, 2);
vbo_colors VBO 同样的原因:
GLfloat vertices[] = { -1.0f,-1.0f,-1.0f,1.0f,-1.0f,-1.0f,-1.0f,-1.0f, 1.0f, 1.0f,-1.0f,-1.0f,1.0f,-1.0f, 1.0f,-1.0f,-1.0f, 1.0f, -1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 1.0f,1.0f, 1.0f,-1.0f, 1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 1.0f,1.0f, 1.0f, 1.0f, -1.0f,-1.0f, 1.0f,1.0f,-1.0f, 1.0f,-1.0f, 1.0f, 1.0f, 1.0f,-1.0f, 1.0f,1.0f, 1.0f, 1.0f,-1.0f, 1.0f, 1.0f, -1.0f,-1.0f,-1.0f,-1.0f, 1.0f,-1.0f,1.0f,-1.0f,-1.0f, 1.0f,-1.0f,-1.0f,-1.0f, 1.0f,-1.0f,1.0f, 1.0f,-1.0f, -1.0f,-1.0f, 1.0f,-1.0f, 1.0f,-1.0f,-1.0f,-1.0f,-1.0f, -1.0f,-1.0f, 1.0f,-1.0f, 1.0f, 1.0f,-1.0f, 1.0f,-1.0f, 1.0f,-1.0f, 1.0f,1.0f,-1.0f,-1.0f,1.0f, 1.0f,-1.0f, 1.0f,-1.0f, 1.0f,1.0f, 1.0f,-1.0f,1.0f, 1.0f, 1.0f }; -
在
paintEvent()函数中,我们必须在glClear()函数中添加GL_DEPTH_BUFFER_BIT,因为我们已经在上一步骤中的initializeGL()中启用了深度测试:glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); -
之后,我们需要向一个称为 模型视图-投影 (MVP) 的着色器发送一段矩阵信息,这样 GPU 就知道如何在 2D 屏幕上渲染 3D 形状。MVP 矩阵是投影矩阵、视图矩阵和模型矩阵相乘的结果。乘法顺序非常重要,以确保你得到正确的结果:
QMatrix4x4 matrixMVP; QMatrix4x4 model, view, projection; model.translate(0, 1, 0); model.rotate(45, 0, 1, 0); view.lookAt(QVector3D(4, 4, 0), QVector3D(0, 0, 0), QVector3D(0, 1, 0)); projection.perspective(60.0f, ((float)this->width()/(float)this->height()), 0.1f, 100.0f); matrixMVP = projection * view * model; shaderProgram->setUniformValue("matrix", matrixMVP); -
将
glDrawArrays()中的最后一个值更改为36,因为我们现在在立方体形状中有 36 个三角形:glDrawArrays(GL_TRIANGLES, 0, 36); -
我们必须回到我们的着色器代码,并更改其中的一些部分,如下面的代码所示:
static const char *vertexShaderSource = "#version 330 core\n" "layout(location = 0) in vec3 posAttr;\n" "uniform mat4 matrix;\n" "out vec3 fragPos;\n" "void main() {\n" "fragPos = posAttr;\n" "gl_Position = matrix * vec4(posAttr, 1.0); }"; static const char *fragmentShaderSource = "#version 330 core\n" "in vec3 fragPos;\n" "out vec4 col;\n" "void main() {\n" "col = vec4(fragPos, 1.0); }"; -
如果你现在构建并运行项目,你应该在屏幕上看到一个多彩的立方体出现。我们使用相同的顶点数组来着色,这给出了这个多彩的结果:

图 5.5 – 使用 OpenGL 渲染的彩色 3D 立方体
-
即使结果看起来相当不错,如果我们真的想展示 3D 效果,那就需要通过动画化立方体来实现。要做到这一点,首先,我们需要打开
renderwindow.h并将其包含以下头文件:#include <QElapsedTimer> -
然后,将以下变量添加到
renderwindow.h中。请注意,在现代 C++标准中,您可以在头文件中初始化变量,这在旧的 C++标准中是不允许的:QElapsedTimer* time; int currentTime = 0; int oldTime = 0; float deltaTime = 0; float rotation = 0; -
打开
renderwindow.cpp并将以下高亮代码添加到类构造函数中:openGLContext = new QOpenGLContext(); openGLContext->setFormat(format); openGLContext->create(); openGLContext->makeCurrent(this); time = new QElapsedTimer(); paintEvent() function. deltaTime is the value of the elapsed time of each frame, which is used to make animation speed consistent, regardless of frame rate performance:void RenderWindow::paintEvent(QPaintEvent *event) {Q_UNUSED(event);// 每帧的 Delta 时间
currentTime = time->elapsed();deltaTime = (float)(currentTime - oldTime) / 1000.0f;将旋转变量传递给 rotate()函数,如下所示:
rotation += deltaTime * 50; QMatrix4x4 matrixMVP; QMatrix4x4 model, view, projection; model.translate(0, 1, 0); model.rotate(rotation, 0, 1, 0); -
在
paintEvent()函数的末尾调用update()函数,这样paintEvent()就会在每次绘制调用的末尾被反复调用。由于我们在paintEvent()函数中改变了旋转值,我们可以给观众一个旋转立方体的错觉:glDrawArrays(GL_TRIANGLES, 0, 36); shaderProgram->release(); vao->release(); this->update(); } -
如果现在编译并运行程序,你应该能在渲染窗口中看到一个旋转的立方体!
它是如何工作的...
在任何 3D 渲染中,深度都非常重要,因此我们需要通过调用glEnable(GL_DEPTH_TEST)在 OpenGL 中启用深度测试功能。当我们清除缓冲区时,还必须指定GL_DEPTH_BUFFER_BIT,以便深度信息也被清除,以便正确渲染下一个图像。
我们使用 OpenGL 中的 MVP 矩阵,这样 GPU 就知道如何正确渲染 3D 图形。在 OpenGL 3 及更高版本中,OpenGL 不再通过固定函数自动处理这一点。程序员被赋予了根据他们的用例定义自己的矩阵的自由和灵活性,然后只需通过着色器将其提供给 GPU 以渲染最终图像。模型矩阵包含 3D 对象的变换数据,即对象的位置、旋转和缩放。另一方面,视图矩阵是相机或视图信息。最后,投影矩阵告诉 GPU 在将 3D 世界投影到 2D 屏幕时使用哪种投影方法。
在我们的例子中,我们使用了透视投影方法,这能更好地感知距离和深度。与透视投影相反的是正交投影,它使一切看起来都扁平且平行:

图 5.6 – 透视视图和正交视图之间的差异
在这个例子中,我们使用计时器通过将deltaTime值乘以 50 来增加旋转值。deltaTime值会根据您的渲染帧率而变化。然而,它使得不同硬件在不同帧率渲染时的动画速度保持一致。
记得手动调用 update() 以刷新屏幕,否则立方体将不会动画化。
OpenGL 中的纹理化
OpenGL 允许我们将图像(也称为 BMP、JPEG、PNG、TARGA、TIFF 等)映射到纹理上,而且你不必自己实现它。我们将使用之前的旋转立方体示例,并尝试使用纹理来映射它!
如何操作…
让我们按照以下步骤学习如何在 OpenGL 中使用纹理:
-
打开
renderwindow.h并添加以下代码块中突出显示的变量:QOpenGLContext* openGLContext; QOpenGLFunctions* openGLFunctions; QOpenGLShaderProgram* shaderProgram; QOpenGLVertexArrayObject* vao; QOpenGLBuffer* vbo_vertices; QOpenGLBuffer* vbo_uvs; glEnable(GL_TEXTURE_2D) in the initializeGL() function to enable the texture mapping feature:void RenderWindow::initializeGL()
{
openGLFunctions = openGLContext->functions();
glEnable(GL_DEPTH_TEST);
在 QOpenGLTexture 类下的纹理变量。我们将从应用程序文件夹中加载一个名为 brick.jpg 的纹理,并通过调用 mirrored() 来翻转图像。OpenGL 使用不同的坐标系,因此我们需要在将纹理传递给着色器之前翻转我们的纹理。我们还将设置最小和最大过滤器为最近邻和线性,如下所示:
texture = new QOpenGLTexture(QImage(qApp->applicationDirPath() + "/brick.jpg").mirrored()); texture->setMinificationFilter(QOpenGLTexture::Nearest); texture->setMagnificationFilter(QOpenGLTexture::Linear); -
添加另一个名为
uvs的数组。这是我们保存cube对象纹理坐标的地方:GLfloat uvs[] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f }; -
我们必须修改我们的顶点着色器,以便它接受纹理坐标来计算纹理将应用到对象表面的位置。在这里,我们只是将纹理坐标传递到片段着色器而不进行修改:
static const char *vertexShaderSource = "#version 330 core\n" "layout(location = 0) in vec3 posAttr;\n" "layout(location = 1) in vec2 uvAttr;\n" "uniform mat4 matrix;\n" "out vec3 fragPos;\n" "out vec2 fragUV;\n" "void main() {\n" "fragPos = posAttr;\n" "fragUV = uvAttr;\n" "gl_Position = matrix * vec4(posAttr, 1.0); }"; -
在片段着色器中,我们通过调用
texture()函数创建一个纹理,该函数接收来自fragUV的纹理坐标信息和来自tex的图像采样器:static const char *fragmentShaderSource = "#version 330 core\n" "in vec3 fragPos;\n" "in vec2 fragUV;\n" "uniform sampler2D tex;\n" "out vec4 col;\n" "void main() {\n" "vec4 texCol = texture(tex, fragUV);\n" "col = texCol; }"; -
我们还必须初始化纹理坐标的 VBO:
vbo_uvs = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); vbo_uvs->create(); vbo_uvs->setUsagePattern(QOpenGLBuffer::StaticDraw); vbo_uvs->bind(); vbo_uvs->allocate(uvs, sizeof(uvs) * sizeof(GLfloat)); -
在
paintEvent()函数中,我们必须将纹理坐标信息发送到着色器,然后在调用glDrawArrays()之前绑定纹理:vbo_uvs->bind(); shaderProgram->bindAttributeLocation("uvAttr", 1); shaderProgram->enableAttributeArray(1); shaderProgram->setAttributeBuffer(1, GL_FLOAT, 0, 2); texture->bind(); glDrawArrays(GL_TRIANGLES, 0, 36); -
如果你现在编译并运行程序,你应该会在屏幕上看到一个旋转的砖块立方体:

图 5.7 – 我们的三维立方体现在看起来像是用砖块创建的
它是如何工作的…
Qt 6 使得加载纹理变得非常简单。只需一行代码就可以加载图像文件,翻转它,并将其转换为 OpenGL 兼容的纹理。纹理坐标是让 OpenGL 知道在屏幕上显示之前如何将纹理粘贴到对象表面的信息。
min 和 max 过滤器是当纹理应用于其分辨率无法覆盖的表面时使纹理看起来更好的过滤器。此设置的默认值为 GL_NEAREST,代表 GL_LINEAR,代表 GL_NEAREST:

图 5.8 - GL_NEAREST 和 GL_LINEAR 之间的差异
OpenGL 中的基本光照
在这个例子中,我们将学习如何通过使用 OpenGL 和 Qt 6 在我们的 3D 场景中添加一个简单的点光源。
如何操作…
让我们按照以下步骤开始:
-
再次,我们将使用之前的示例,并在旋转的立方体附近添加一个点光源。打开
renderwindow.h文件,并向文件中添加一个名为vbo_normals的变量:QOpenGLBuffer* vbo_uvs; QOpenGLBuffer* vbo_normals; QOpenGLTexture* texture; -
打开
renderwindow.cpp文件,并在initializeGL()函数中添加一个名为normals的数组:GLfloat normals[] = { 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f }; -
在
initializeGL()中初始化vbo_normalsVBO,添加以下代码:vbo_normals = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); vbo_normals->create(); vbo_normals->setUsagePattern(QOpenGLBuffer::StaticDraw); vbo_normals->bind(); vbo_normals->allocate(normals, sizeof(normals) * sizeof(GLfloat)); -
由于这次我们将编写的着色器将比之前示例中的更长,让我们将着色器代码移动到文本文件中,并通过调用
addShaderFromSourceFile()将它们加载到程序中:shaderProgram = new QOpenGLShaderProgram(this); shaderProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, qApp->applicationDirPath() + "/vertex.txt"); shaderProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, qApp->applicationDirPath() + "/fragment.txt"); shaderProgram->link(); -
完成这些后,将以下代码添加到
paintEvent()函数中,将normalsVBO 传递给着色器:vbo_normals->bind(); shaderProgram->bindAttributeLocation("normalAttr", 2); shaderProgram->enableAttributeArray(2); shaderProgram->setAttributeBuffer(2, GL_FLOAT, 0, 3); -
让我们打开我们刚刚创建的两个包含着色器代码的文本文件。首先,我们需要对顶点着色器进行一些修改,如下所示:
#version 330 core layout(location = 0) in vec3 posAttr; layout(location = 1) in vec2 uvAttr; layout(location = 2) in vec3 normalAttr; uniform mat4 matrix; out vec3 fragPos; out vec2 fragUV; out vec3 fragNormal; void main() { fragPos = posAttr; fragUV = uvAttr; fragNormal = normalAttr; gl_Position = matrix * vec4(posAttr, 1.0); } -
我们还将对片段着色器进行一些修改。在着色器代码中,我们将创建一个名为
calcPointLight()的函数:#version 330 core in vec3 fragPos; in vec2 fragUV; in vec3 fragNormal; uniform sampler2D tex; out vec4 col; vec4 calcPointLight() { vec4 texCol = texture(tex, fragUV); vec3 lightPos = vec3(1.0, 2.0, 1.5); vec3 lightDir = normalize(lightPos - fragPos); vec4 lightColor = vec4(1.0, 1.0, 1.0, 1.0); calcPointLight() and output the resulting fragment to the col variable, as follows:// 漫反射
float diffuseStrength = 1.0;
float diff = clamp(dot(fragNormal, lightDir), 0.0, 1.0);
vec4 diffuse = diffuseStrength * diff * texCol * lightColor * lightIntensity;
return diffuse;
}
void main() {
vec4 finalColor = calcPointLight();
col = finalColor;
}
-
如果现在编译并运行程序,您应该能看到光照效果:

图 5.9 – 我们的三维立方体现在有了着色
它是如何工作的...
在 OpenGL 3 及更高版本中,固定功能光照已不再存在。您不能再通过调用 glEnable(GL_LIGHT1) 来向您的 3D 场景添加光照。添加光照的新方法是自己在着色器中计算光照。这为您提供了根据需要创建所有类型光照的灵活性。旧方法在大多数硬件中最多只能支持 16 个光照,但使用新的可编程管道,您可以在场景中拥有任意数量的光照;然而,光照模型将需要您在着色器中完全编码,这并非易事。
此外,我们还需要为立方体的每个表面添加一个表面法线值。表面法线指示表面朝向何处,并用于光照计算。前面的示例非常简化,以便您了解 OpenGL 中光照的工作原理。在实际应用中,您可能需要从 C++传递一些变量,如光照强度、光照颜色和光照位置,或者从材质文件中加载它们,而不是在着色器代码中硬编码。
使用键盘控制移动对象
在本节中,我们将探讨如何使用键盘控制移动 OpenGL 中的对象。Qt 通过使用虚拟函数,如 keyPressEvent() 和 keyReleaseEvent(),提供了一个简单的方法来检测键盘事件。我们将使用之前的示例并在此基础上进行添加。
如何操作…
使用键盘控制移动对象,请按照以下步骤操作:
-
打开
renderwindow.h并声明两个名为moveX和moveZ的浮点数。然后,声明一个名为movement的QVector3D变量:QElapsedTimer* time; int currentTime = 0; int oldTime = 0; float deltaTime = 0; float rotation = 0; float moveX = 0; float moveZ = 0; keyPressEvent() and keyReleaseEvent():protected:
void initializeGL();
void paintEvent(QPaintEvent *event);
void resizeEvent(QResizeEvent *event);
void keyPressEvent(QKeyEvent *event);
void keyReleaseEvent(QKeyEvent *event);
-
我们将在
renderwindow.cpp中实现keyPressEvent()函数:void RenderWindow::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_W) { moveZ = -10; } if (event->key() == Qt::Key_S) { moveZ = 10; } if (event->key() == Qt::Key_A) { moveX = -10; } if (event->key() == Qt::Key_D) { moveX = 10; } } -
我们还将实现
keyReleaseEvent()函数:void RenderWindow::keyReleaseEvent(QKeyEvent *event) { if (event->key() == Qt::Key_W) { moveZ = 0; } if (event->key() == Qt::Key_S) { moveZ = 0; } if (event->key() == Qt::Key_A) { moveX = 0; } if (event->key() == Qt::Key_D) { moveX = 0; } } -
之后,我们将注释掉
paintEvent()中的旋转代码,并添加运动代码,如下面的片段所示。我们不想被旋转分散注意力,只想专注于运动://rotation += deltaTime * 50; movement.setX(movement.x() + moveX * deltaTime); movement.setZ(movement.z() + moveZ * deltaTime); QMatrix4x4 matrixMVP; QMatrix4x4 model, view, projection; model.translate(movement.x(), 1, movement.z()); -
如果你现在编译并运行程序,你应该能够通过按W、A、S和D来移动立方体。
它是如何工作的...
我们在这里所做的就是不断向 moveX 和 moveZ 的 movement 向量的 x 和 z 值添加。当按键被按下时,moveX 和 moveZ 将成为正数或负数,具体取决于哪个按钮被按下;否则,它将是零。在 keyPressEvent() 函数中,我们检查按下的键盘按钮是否是 W、A、S 或 D;然后相应地设置变量。要获取 Qt 使用的所有键名的完整列表,请访问 doc.qt.io/qt-6/qt.html#Key-enum。
一种创建运动输入的方法是按住相同的键而不释放它。Qt 6 将在间隔后重复按键事件,但这并不流畅,因为现代操作系统限制按键事件以防止双击。键盘输入间隔在不同操作系统之间有所不同。你可以通过调用 QApplication::setKeyboardInterval() 来设置间隔,但这可能不是每个操作系统都有效。因此,我们没有采用这种方法。
相反,我们只在按键按下或释放时设置一次 moveX 和 moveZ,然后在游戏循环中持续应用这个值到运动向量,这样它就可以连续移动而不会受到输入间隔的影响。
QML 中的 Qt Quick 3D
在这个菜谱中,我们将学习如何使用 Qt 6 渲染 3D 图像。
如何做到这一点…
让我们通过以下示例学习如何在 QML 中使用 3D 画布:
- 让我们从在 Qt Creator 中创建一个新项目开始这个示例。这次,我们将选择Qt Quick 应用程序而不是之前示例中选择的其它选项:

图 5.10 – 创建新的 Qt Quick 应用程序项目
- 一旦创建项目,你需要通过访问
resource.qrc来创建一个资源文件:

图 5.11 – 创建 Qt 资源文件
- 将图像文件添加到我们的项目资源中——我们将在本例中使用它。通过在
brick.jpg图像上右键单击,使用 Qt Creator 打开resource.qrc,该图像将被用作我们 3D 对象的表面纹理:

图 5.12 – 将砖纹理添加到资源文件中
-
之后,使用 Qt Creator 打开
main.qml。你会看到文件中已经写了几行代码。它的基本功能是打开一个空窗口,没有其他功能。让我们开始向Window对象添加我们自己的代码。 -
首先,将
QtQuick3D模块导入到我们的项目中,并在Window对象下创建一个View3D对象,我们将使用它来在 3D 场景上渲染:import QtQuick import QtQuick3D Window { width: 640 height: 480 visible: true title: qsTr("Hello World") View3D { id: view anchors.fill: parent } } -
之后,将
View3D对象的environment变量设置为一个新的SceneEnvironment对象。我们使用它来设置 3D 视图的背景颜色为天蓝色:environment: SceneEnvironment { clearColor: "skyblue" backgroundMode: SceneEnvironment.Color } -
之后,我们通过在 3D 视图中声明一个
Model对象并设置其源为Cube来重新创建我们之前的 OpenGL 示例中的 3D 立方体。然后我们沿 y 轴旋转它-30单位,并给它应用一个材质。之后,我们将材质的纹理设置为brick.jpg。这里的qrc:关键字意味着我们从我们之前创建的资源文件中获取纹理:Model { position: Qt.vector3d(0, 0, 0) source: "#Cube" eulerRotation.y: -30 materials: PrincipledMaterial { baseColorMap: Texture { source: "qrc:/brick.jpg" } } } -
在我们能够清楚地看到我们的 3D 立方体之前,我们必须创建一个光源以及一个相机,这有助于渲染我们的场景:
PerspectiveCamera { position: Qt.vector3d(0, 200, 300) eulerRotation.x: -30 } DirectionalLight { eulerRotation.x: -10 eulerRotation.y: -20 } -
完成后,构建并运行项目。你应该能在屏幕上看到一个带有砖纹理的 3D 立方体:

图 5.13 – 在 QtQuick3D 中重新创建 3D 演示
-
要重新创建旋转动画,让我们在我们的立方体模型中添加
NumberAnimation:Model { position: Qt.vector3d(0, 0, 0) source: "#Cube" eulerRotation.y: -30 materials: PrincipledMaterial { baseColorMap: Texture { source: "qrc:/brick.jpg" } } NumberAnimation on eulerRotation.y { duration: 3000 to: 360 from: 0 easing.type:Easing.Linear loops: Animation.Infinite } }
它是如何工作的...
最初,Qt 5 使用一个名为three.js库/API 的东西,它使用 WebGL 技术来在 Qt Quick 窗口中显示动画 3D 计算机图形。然而,这个特性在 Qt 6 中已被完全弃用,并已被另一个名为Qt Quick 3D的模块所取代。
Qt Quick 3D 比 Qt Canvas 3D 工作得更好,因为它使用本地方法来渲染 3D 场景,而不依赖于第三方库如three.js。它还产生更好的性能,并与任何现有的 Qt Quick 组件很好地集成。
第六章:从 Qt 5 到 Qt 6 的过渡
在本章中,我们将了解 Qt 6 中所做的更改以及如何将现有的 Qt 5 项目升级到 Qt 6。与之前的更新不同,Qt 6 几乎是从头到尾重写了整个 Qt 代码库,包括所有底层类。这些重大更改可能会破坏你现有的 Qt 5 项目,如果你只是切换到 Qt 6。
在本章中,我们将涵盖以下主要主题:
-
C++ 类更改
-
使用 Clazy 检查 对 Clang 和 C++ 进行检查
-
QML 类型更改
技术要求
本章的技术要求包括 Qt 6.6.1 MinGW 64 位、Qt 5.15.2 MinGW 64 位和 Qt Creator 12.0.2。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter06。
C++ 类更改
在本食谱中,我们将了解 Qt6 的 C++ 类发生了哪些变化。
如何实现…
按照以下步骤了解 Qt6 中的 C++ 类:
-
通过访问 文件 | 新建项目 创建一个新的 Qt 控制台应用程序。
-
我们将打开
main.cpp文件并添加以下头文件:#include <QCoreApplication> #include <QDebug> #include <QLinkedList> #include <QRegExp> #include <QStringView> #include <QTextCodec> #include <QTextEncoder> #include <QTextDecoder> -
之后,添加以下代码以演示
QLinkedList类:int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); // QLinkedList QLinkedList<QString> list; list << "string1" << "string2" << "string3"; QLinkedList<QString>::iterator it; for (it = list.begin(); it != list.end(); ++it) { qDebug() << "QLinkedList:" << *it; QRegExp class:// QRegExp
QRegExp rx("\d+");
QString text = "Jacky 有 3 个胡萝卜,15 个苹果,9 个橙子和 12 个葡萄。";
QStringList myList;
int pos = 0;
while ((pos = rx.indexIn(text, pos)) != -1)
{
// 从句子中分离所有数字
myList << rx.cap(0);
pos += rx.matchedLength();
}
qDebug() << "QRegExp:" << myList;
-
然后,在前面代码的底部添加以下代码以演示
QStringView类:// QStringView QStringView x = QString("Good afternoon"); QStringView y = x.mid(5, 5); QStringView z = x.mid(5); qDebug() << "QStringView:" << y; // after qDebug() << "QStringView:" << z; // afternoon -
不仅如此,我们还在此添加以下代码以演示
QTextCodec类:// QTextCodec QByteArray data = "\xCE\xB1\xCE\xB2\xCE\xB3"; // Alpha, beta, gamma symbols QTextCodec *codec = QTextCodec::codecForName("UTF-8"); QString str = codec->toUnicode(data);
qDebug() << "QTextCodec:" << str;
-
接下来,添加以下代码,该代码演示了如何使用
QTextEncoder类将十六进制代码转换为字符:// QTextEncoder QString str2 = QChar(0x41); // Character "A" QTextCodec *locale = QTextCodec::codecForLocale(); QTextEncoder *encoder = locale->makeEncoder(); QByteArray encoded = encoder->fromUnicode(str2); qDebug() << "QTextEncoder:" << encoded.data(); -
让我们再添加以下代码来演示如何使用
QTextDecoder类将一行文本从 Shift JIS 格式转换为 Unicode:// QTextDecoder QByteArray data2 = "\x82\xB1\x82\xF1\x82\xC9\x82\xBF\x82\xCD\x90\xA2\x8A\x45"; // "Hello world" in Japanese QTextCodec *codec2 = QTextCodec::codecForName("Shift-JIS"); QTextDecoder *decoder = codec2->makeDecoder(); QString decoded = decoder->toUnicode(data2); qDebug() << "QTextDecoder:" << decoded; -
现在我们已经完成了代码,让我们尝试使用 Qt 5 编译项目,看看会发生什么。你的程序应该可以正常编译,并在输出窗口中显示以下结果:
QLinkedList: "string1" QLinkedList: "string2" QLinkedList: "string3" QRegExp: ("3", "15", "9", "12") QStringView: "after" QStringView: "afternoon" QTextCodec: "αβγ" QTextEncoder: A QTextDecoder: "こんにちは世界" -
现在,让我们切换到 Qt 6 并重新编译项目,你应该会得到如下错误:
QLinkedList: No such file or directory fatal error: QLinkedList: No such file or directory -
打开你的项目文件(
.pro)并在顶部添加以下代码:QT += core5compat -
最后,再次使用 Qt 6 编译项目。这次你应该能够运行它。
core5compat只是过渡从 Qt 5 到 Qt 6 的临时解决方案。你可以改为使用std::list来替换QLinkedList,因为将来它将被弃用。
工作原理…
我们不需要任何 GUI,因为我们只是测试一些 C++ 类,所以使用 QDebug 类在输出窗口中打印结果。
在前一个示例中,我们使用了一些在 Qt 6 中已弃用的类,即 QLinkedList、QRegExp、QStringView、QTextCodec、QTextEncoder 和 QTextDecoder。这些只是我们在使用 Qt 时会遇到的一些常见类,它们在 Qt 6 中已被重写。如果您正在将项目从 Qt 5 迁移到 6,最佳做法是将 Core5Compat 模块添加到您的项目中,以便 Qt 5 类可以在 Qt 6 下继续运行。Core5Compat 模块是在 Qt 6 项目下支持 Qt 5 类的临时措施,以便 Qt 程序员可以安全地将项目迁移到 Qt 6,并有时间将代码逐步迁移到 Qt 6 类。
当您迁移到 Qt 7 时,Core5Compat 模块将停止工作,因此不建议长时间使用弃用的类。
更多内容…
在 Qt 6 中,许多核心功能是从头开始重写的,以使库与现代计算架构和工作流程保持最新。因此,Qt 6 被视为一个过渡阶段,其中一些类已完成,而另一些尚未完成。
为了使其正常工作,Qt 开发者引入了 Core5Compat 模块,以便 Qt 程序员在逐渐过渡到新类的同时更容易保持他们的项目运行。您可以从官方在线文档中查看这些类的替代方案。
最后,Qt 6 现在正在利用 C++ 17。强烈建议您的项目遵循 C++ 17 标准,以便您的代码可以很好地与 Qt 6 一起工作。
注意
在 Qt 6 中,许多其他 C++ 类已被弃用或正在被重写;请参阅此链接以检查 Qt 6 中已更改或弃用的完整 C++ 类列表:doc.qt.io/qt-6/obsoleteclasses.html。您还可以将 QT_DISABLE_DEPRECATED_UP_TO 宏添加到您的 Qt 项目中,以禁用项目中弃用的 C++ API。例如,将 DEFINES += QT_DISABLE_DEPRECATED_UP_TO=0x050F00 添加到您的配置文件中将禁用 Qt 5.15 中弃用的所有 C++ API。
使用 Clazy 检查 Clang 和 C++
在本章中,我们将学习如何使用 Clang 工具集中的 Clazy 检查,在您的 Qt 项目中检测到已弃用的 Qt 5 类和函数时自动显示警告。
如何操作…
让我们按照以下步骤开始:
-
我们将使用前一个示例中的相同项目。然后,通过转到 编辑 | 首选项… 来打开首选项窗口。
-
之后,转到 分析器 页面并单击 诊断配置 旁边的按钮:

图 6.1 – 打开诊断配置窗口
- 在顶部选择 默认 Clang-Tidy 和 Clazy 检查 选项,并点击如图 图 6**.2 所示的 复制… 按钮。给它起个名字,然后点击 确定。新的选项现在将出现在 自定义 类别下:

图 6.2 - 点击复制按钮
-
然后,打开
qt6-deprecated-api-fixes -
qt6-header-fixes -
qt6-qhash-signature -
qt6-fwd-fixes -
missing-qobject-macro -
完成后,关闭首选项窗口,转到 分析 | Clang-Tidy 和 Clazy...。将弹出一个包含所有源文件的 要分析的文件 窗口。我们将坚持默认选项,通过点击 分析 按钮继续:

图 6.3 – 选择所有文件并按分析按钮
- Clang-Tidy 和 Clazy 工具分析完项目后,你应在 Qt Creator 下的单独面板上看到显示的结果。它将显示在 Qt 6 中已被弃用的代码行,并给出替换建议:

图 6.4 – 分析结果
它是如何工作的...
Tidy 和 Clazy 工具随 Clang 包一起提供,因此无需单独安装。这是一个功能强大的工具,可以用来检查许多事情,例如检查代码中使用的已弃用函数、在循环中放置容器、将非 void 槽标记为常量、注册以小写字母开头的 QML 类型等等。
这是一个帮助你轻松检查和改进代码质量的工具。它应该被广泛推广,并更频繁地被 Qt 程序员使用。
QML 类型更改
在本章中,我们将学习与 Qt 5 相比,Qt 6 做了哪些改动。
如何操作…
让我们按照以下步骤开始:
-
通过转到 文件 | 新建项目 创建一个新的 Qt Quick 应用程序。
-
在定义项目详细信息时,选择 最低要求的 Qt 版本 为 Qt 6.2。

图 6.5 – 选择 Qt 6.2 作为最低要求的 Qt 版本
-
创建项目后,打开
main.qml文件并添加以下属性:import QtQuick Window { width: 640 height: 480 visible: true title: qsTr("Hello World") property variant myColor: "red" Rectangle object to main.qml, as shown in the following:Rectangle {
id: rect
x: 100
y: 100
width: 100
height: 100
color: myColor
}
-
之后,我们将在矩形下方添加另一个
Image对象:Image { id: img x: 300 y: 100 width: 150 height: 180 source: imageFolder + "/tux.png" } -
接下来,我们通过转到 文件 | 新建文件… 并在 Qt 模板下选择 Qt 资源文件 来为我们的项目创建一个新的资源文件。

图 6.6 – 创建新的 Qt 资源文件
- 然后,在资源文件中创建一个名为
images的文件夹,并将tux.png添加到images文件夹中。

图 6.7 – 将 tux.png 添加到图像文件夹
- 现在构建并运行项目,你应该得到以下类似的结果:

图 6.8 – Qt Quick 6 中的 Hello World 示例
它是如何工作的...
Qt 6 对 Qt Quick 也进行了许多更改,但它们大多是底层函数,不会对 QML 语言和对象产生很大影响。因此,在从 Qt 5 转换到 Qt 6 时,您不需要对您的 QML 脚本进行很多更改。然而,项目结构仍有一些细微的变化,代码也有一些小的差异。
最明显的区别之一是,现在 QML 脚本在项目结构下的 QML 类别中列出,而不是像在 Qt 5 中那样在 Resources 下。

图 6.9 – QML 文件现在有自己的类别
因此,当我们将在 main.cpp C++ 源代码中加载 main.qml 文件时,我们将使用以下代码:
const QUrl url(u"qrc:/qt6_qml_new/main.qml"_qs);
与我们在 Qt 5 中所做的方法相比,有一些细微的差别:
const QUrl url(QStringLiteral("qrc:/main.qml"));
字符串前面的 u 创建了一个 16 位字符串字面量,而字符串后面的 _qs 将其转换为 QString。这些运算符类似于 Qt 5 中使用的 QStringLiteral 宏,但更容易转换为所需的精确字符串格式,同时符合 C++ 17 编码风格。
Qt 6 的另一个重大区别是,从上一个示例中的 main.qml 可以看到差异:
import QtQuick
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
如您从前面的代码块中高亮显示的部分所示,现在在导入 Qt Quick 模块时版本号是可选的。Qt 将默认选择可用的最新版本。
现在,让我们看看我们在示例中声明的属性:
property variant myColor: "red"
property url imageFolder: "/images"
尽管前面的代码可以正常运行,但建议使用 Qt 函数,如 Qt.color() 和 Qt.resolvedUrl(),来返回具有正确类型的属性,而不是仅仅传递一个字符串:
property variant myColor: Qt.color("red")
property url imageFolder: Qt.resolvedUrl("/images")
另一个可能注意不到的小区别是 Qt 处理相对路径的方式。在 Qt 5 中,我们会将相对路径写成 ./images,它将返回为 qrc:/images。然而,在 Qt 6 中,./images 将返回为 qrc:/[project_name]/images/tux.png,这是不正确的。我们必须使用 /images 而不是前面的点。
注意
关于 Qt 6 中 Qt Quick 的全面更改的更多信息,请访问 doc.qt.io/qt-6/qtquickcontrols-changes-qt6.html。
第七章:使用网络和管理大型文档
在本章中,我们将学习如何使用 Qt 6 的网络模块创建网络服务器程序和客户端程序。我们还将学习如何创建一个使用文件传输协议(FTP)从服务器上传和下载文件的程序。最后,我们将学习如何使用 Qt 6 和 C++ 语言向特定的网络服务发送 HTTP 请求。
在本章中,我们将涵盖以下主要主题:
-
创建 TCP 服务器
-
创建 TCP 客户端
-
使用 FTP 上传和下载文件
技术要求
本章的技术要求是 Qt 6.6.1、Qt Creator 12.0.2 和 FileZilla。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter07。
创建 TCP 服务器
在本食谱中,我们将学习如何在 Qt 6 中创建一个传输控制协议(TCP)服务器。在我们能够创建一个允许我们上传和下载文件的服务器之前,让我们先缩小范围,学习如何创建一个接收和发送文本的网络服务器。
如何操作...
按照以下步骤创建 TCP 服务器:
- 首先,让我们从 文件 | 新建文件或项目 创建一个 Qt 控制台应用程序 项目,如下面的截图所示:

图 7.1 – 创建新的 Qt 控制台应用程序项目
- 然后,再次转到 文件 | 新建文件或项目,但这次在 C/C++ 类别下选择 C++ 类,如下面的截图所示:

图 7.2 – 创建新的 C++ 类
- 然后,将你的类命名为
server。将其基类设置为server.h和server.cpp,如下面的截图所示:

图 7.3 – 定义服务器类
-
然后,打开你的项目文件(
.pro)并添加network模块,如下面的代码所示。然后,再次运行qmake以重新加载模块:QT += core server.h and add the following headers to it:#include <QTcpServer>#include <QTcpSocket>#include <QVector>#include <QDebug> -
在此之后,声明
startServer()和sendMessageToClients()函数,如下面的代码所示:public: server(QObject *parent = nullptr); void startServer(); server class:公共槽:
void newClientConnection();void socketDisconnected();void socketReadReady();void socketStateChanged(QAbstractSocket::SocketState state); -
最后,声明两个私有变量,如下面的代码所示:
private: QTcpServer* chatServer; QVector<QTcpSocket*>* allClients; -
完成前面的步骤后,打开
server.cpp并定义startServer()函数。在这里,我们创建一个QVector容器来存储所有连接到服务器的客户端,并在后续步骤中使用它发送消息。以下是一个示例:void server::startServer() { allClients = new QVector<QTcpSocket*>; chatServer = new QTcpServer(); chatServer->setMaxPendingConnections(10); connect(chatServer, &QTcpServer::newConnection, this, &server::newClientConnection); if (chatServer->listen(QHostAddress::Any, 8001)) qDebug() << "Server has started. Listening to port 8001."; else qDebug() << "Server failed to start. Error: " + chatServer->errorString(); } -
接下来,我们实现
sendMessageToClients()函数,在这个函数中,我们将遍历我们在上一步中创建的allClients容器,并将消息发送给每个客户端,如下面的示例所示:void server::sendMessageToClients(QString message) { if (allClients->size() > 0) { for (int i = 0; i < allClients->size(); i++) { if (allClients->at(i)->isOpen() && allClients- >at(i)->isWritable()) { allClients->at(i)->write(message.toUtf8()); } }}} -
之后,我们将开始实现槽函数。让我们从以下代码开始:
void server::newClientConnection() { QTcpSocket* client = chatServer->nextPendingConnection(); QString ipAddress = client->peerAddress().toString(); int port = client->peerPort(); connect(client, &QTcpSocket::disconnected, this, &server::socketDisconnected); connect(client, &QTcpSocket::readyRead,this, &server::socketReadReady); connect(client, &QTcpSocket::stateChanged, this, &server::socketStateChanged); allClients->push_back(client); qDebug() << "Socket connected from " + ipAddress + ":" + QString::number(port); } -
接下来,我们将处理
socketDisconnected()函数。当客户端从服务器断开连接时,将调用此槽函数,如下面的示例所示:void server::socketDisconnected() { QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender()); QString socketIpAddress = client->peerAddress().toString(); int port = client->peerPort(); qDebug() << "Socket disconnected from " + socketIpAddress + ":" + QString::number(port); } -
接下来,我们将定义
socketReadReady()函数,当客户端向服务器发送文本消息时,将触发此函数,如下面的示例所示:void server::socketReadReady() { QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender()); QString socketIpAddress = client->peerAddress().toString(); int port = client->peerPort(); QString data = QString(client->readAll()); qDebug() << "Message: " + data + " (" + socketIpAddress + ":" + QString::number(port) + ")"; sendMessageToClients(data); } -
之后,让我们实现
socketStateChanged()函数,当客户端的网络状态发生变化时,将调用此函数,如下面的示例所示:void server::socketStateChanged(QAbstractSocket::SocketState state) { QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender()); QString socketIpAddress = client->peerAddress().toString(); int port = client->peerPort(); qDebug() << "Socket state changed (" + socketIpAddress + ":" + QString::number(port) + "): " + desc; } -
我们还需要在
socketStateChanged()中添加以下代码以打印客户端的状态:QString desc; if (state == QAbstractSocket::UnconnectedState) desc = "The socket is not connected."; else if (state == QAbstractSocket::HostLookupState) desc = "The socket is performing a host name lookup."; else if (state == QAbstractSocket::ConnectingState) desc = "The socket has started establishing a connection."; else if (state == QAbstractSocket::ConnectedState) desc = "A connection is established."; else if (state == QAbstractSocket::BoundState) desc = "The socket is bound to an address and port."; else if (state == QAbstractSocket::ClosingState) desc = "The socket is about to close (data may still be waiting to be written)."; else if (state == QAbstractSocket::ListeningState) desc = "For internal use only."; -
最后,让我们打开
main.cpp并在下面的示例中添加高亮显示的代码以启动服务器:#include <QCoreApplication> #include "server.h" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); server* myServer = new server(); myServer->startServer(); return a.exec(); } -
你现在可以尝试运行服务器程序,但你无法测试它,因为我们还没有创建客户端程序,如下面的截图所示:

图 7.4 – 服务器现在正在监听端口 8001
- 让我们继续下一个示例项目,学习如何创建客户端程序。我们稍后还会回来测试这个程序。
它是如何工作的…
主要有两种类型的网络连接——TCP 连接和 用户数据报协议 (UDP) 连接。TCP 是一种可靠的网络连接,而 UDP 则是不可靠的。
这两种连接是为非常不同的目的设计的:
-
TCP 网络通常用于需要按顺序发送和接收每一份数据的程序。它还确保客户端接收数据,并且服务器会得到通知。像消息软件、Web 服务器和数据库这样的程序使用 TCP 网络。
-
另一方面,UDP 网络不需要服务器和客户端之间的持续监控。由于连接不可靠,也没有关于数据是否成功接收的反馈。数据包的丢失是可以容忍的,数据可能甚至不会按照发送的顺序到达。UDP 连接通常用于不需要对数据包投递有严格要求的程序,例如视频游戏、视频会议软件和域名系统。
使用 Qt 6 创建网络软件要容易得多,这得益于其信号和槽机制。我们所需做的只是将 QTcpServer 类和 QTcpSocket 类发出的信号连接到我们的槽函数。然后我们将实现这些槽函数并定义这些函数中的操作。
注意
我们使用 QVector 容器来存储连接到服务器的所有客户端的指针,以便我们可以使用它来稍后传递消息。
为了使这个示例项目简单,我们只是向所有客户端发送文本消息,有点像群聊。您可以自由探索其他可能性并自行修改以改进程序。
创建 TCP 客户端
由于我们在前面的菜谱中创建了一个 TCP 服务器,我们现在需要一个客户端程序来完成项目。因此,在本菜谱中,我们将学习如何使用 Qt 6 和其网络模块创建一个 TCP 客户端程序。
如何做到这一点...
在 Qt 6 中创建 TCP 客户端,让我们按照以下步骤进行:
-
首先,让我们从 Files | New File or Project 创建一个新的 Qt Widgets Application 项目。
-
一旦项目创建完成,让我们打开
mainwindow.ui并设置如图所示的 GUI。请注意,中央小部件的布局方向必须是垂直的:

图 7.5 – 我们客户端程序的布局
-
然后,右键单击菜单中的
clicked()按钮槽函数。然后,在 Send 按钮上也重复相同的步骤。结果,在源代码中为您创建了两个槽函数,这些函数可能或可能不会像以下代码中我们看到的那样,这取决于您的小部件名称:void on_connectButton_clicked(); void on_sendButton_clicked(); -
接下来,打开
mainwindow.h并添加以下头文件:#include <QDebug> #include <QTcpSocket> -
然后,声明
printMessage()函数和三个槽函数:socketConnected()、socketDisconnected()和socketReadyRead(),如下面的代码所示:public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); void printMessage(QString message); private slots: void on_connectButton_clicked(); void on_sendButton_clicked(); void socketConnected(); void socketDisconnected(); void socketReadyRead(); -
之后,声明以下变量:
private: Ui::MainWindow *ui; bool connectedToHost; mainwindow.cpp and define the printMessage() function, as shown in the following example:void MainWindow::printMessage(QString message) {
ui->chatDisplay->append(message);
}
-
然后,我们将实现
on_connectButton_clicked()函数,该函数将在 Connect 按钮被点击时触发,如下面的代码所示:void MainWindow::on_connectButton_clicked() { if (!connectedToHost) { socket = new QTcpSocket(); connect(socket, &QTcpSocket::connected, this, &MainWindow::socketConnected); connect(socket, &QTcpSocket::disconnected, this, &MainWindow::socketDisconnected); connect(socket, &QTcpSocket::readyRead, this, &MainWindow::socketReadyRead); socket->connectToHost("127.0.0.1", 8001); } else { QString name = ui->nameInput->text(); socket->write("<font color=\"Orange\">" + name.toUtf8() + " has left the chat room.</font>"); socket->disconnectFromHost(); } } -
我们还定义了
on_sendButton_clicked()函数,该函数将在 Send 按钮被点击时被调用,如下面的示例所示:void MainWindow::on_sendButton_clicked() { QString name = ui->nameInput->text(); QString message = ui->messageInput->text(); socket->write("<font color=\"Blue\">" + name.toUtf8() + "</font>: " + message.toUtf8()); ui->messageInput->clear(); } -
在此之后,我们实现
socketConnected()函数,该函数将在客户端程序成功连接到服务器时被调用,如下面的代码所示:void MainWindow::socketConnected() { qDebug() << "Connected to server."; printMessage("<font color=\"Green\">Connected to server.</font>"); QString name = ui->nameInput->text(); socket->write("<font color=\"Purple\">" + name.toUtf8() + " has joined the chat room.</font>"); ui->connectButton->setText("Disconnect"); connectedToHost = true; } -
到目前为止,我们还没有完成。我们还需要实现
socketDisconnected()函数,该函数将在客户端从服务器断开连接时触发,如下面的代码所示:void MainWindow::socketDisconnected() { qDebug() << "Disconnected from server."; printMessage("<font color=\"Red\">Disconnected from server.</font>"); ui->connectButton->setText("Connect"); connectedToHost = false; } -
最后,我们还需要定义
socketReadyRead()函数,该函数将打印出从服务器发送的消息,如下面的示例所示:void MainWindow::socketReadyRead() { printMessage(socket->readAll()); } -
在我们运行客户端程序之前,我们必须首先打开我们在前面的菜谱中创建的服务器程序。然后,构建并运行客户端程序。一旦程序打开,就去点击 连接 按钮。成功连接到服务器后,在位于底部的行编辑小部件中输入一些内容,并按 发送 按钮。你应该会看到以下类似的截图:

图 7.6 – 我们的聊天程序现在正在运行
- 让我们转到下面的服务器程序截图,看看终端窗口上是否有什么打印出来的内容:

图 7.7 – 客户端活动也显示在服务器输出中
- 恭喜你,你已经成功创建了一个看起来有点像 互联网中继聊天(IRC)聊天室的程序!
它是如何工作的…
为了使这可行,我们需要两个程序:一个连接所有客户端并传递他们消息的服务器程序,以及一个用户使用的客户端程序,用于发送和接收其他用户的消息。
由于服务器程序只是幕后默默工作,处理所有事情,它不需要任何用户界面,因此我们只需要它作为一个 Qt 控制台应用程序。
然而,客户端程序需要用户友好的图形用户界面,以便用户可以阅读和发送他们的消息。因此,我们将客户端程序创建为一个 Qt 小部件应用程序。
与服务器程序相比,客户端程序相对简单。它所做的只是连接到服务器,发送用户输入的消息,并打印出服务器发送给它的所有内容。
使用 FTP 上传和下载文件
我们已经学习了如何创建简单的聊天软件,该软件可以在用户之间分发文本消息。接下来,我们将学习如何创建一个使用 FTP 上传和下载文件的程序。
如何操作…
让我们从观察以下步骤开始:
- 对于这个项目,我们需要安装一个名为 FileZilla 服务器 的软件,我们将将其用作 FTP 服务器。FileZilla 服务器可以通过点击以下截图所示的 下载 FileZilla 服务器 按钮从
filezilla-project.org下载:

图 7.8 – 从官方网站下载 FileZilla 服务器
- 下载安装程序后,运行它,并同意所有默认选项来安装 FileZilla 服务器,如下面的截图所示:

图 7.9 – 默认安装选项
- 当它完成时,打开 FileZilla Server 并按下 连接到服务器… 按钮,将弹出 连接 窗口,如下截图所示:

图 7.10 – 在连接窗口中设置主机、端口和密码
- 服务器启动后,从顶部菜单中选择 服务器 | 配置…,如下截图所示:

图 7.11 – 从顶部菜单打开设置窗口
- 一旦打开 设置 窗口,点击位于 可用用户 列表下的 添加 按钮,以添加新用户。然后,在 共享文件夹 列表下添加一个共享文件夹,用户将在此上传和下载文件,如下截图所示:

图 7.12 – 点击添加按钮添加新用户
- 我们现在已经完成了 FileZilla Server 的设置。让我们继续使用 Qt Creator 创建一个新的
mainwindow.ui并设置 GUI,如下所示:

图 7.13 – 我们 FTP 程序的布局
-
接着,在
clicked()插槽函数上右键单击,如下所示:private slots: void on_openButton_clicked(); void on_uploadButton_clicked(); itemDoubleClicked(QListWidgetItem*) option and click OK, as shown in the following screenshot:

图 7.14 – 选择 itemDoubleClicked 选项
-
然后,声明其他槽函数,如
serverConnected()、serverReply()和dataReceived(),我们将在本章后面实现它们:private slots: void on_openButton_clicked(); void on_uploadButton_clicked(); void on_setFolderButton_clicked(); void on_fileList_itemDoubleClicked(QListWidgetItem *item); void serverConnected(const QHostAddress &address, int port); void serverReply(int code, const QString ¶meters); FtpDataChannel. -
然后,打开
ftpdatachannel.h并向其中添加以下代码:#ifndef FTPDATACHANNEL_H #define FTPDATACHANNEL_H #include <QtCore/qobject.h> #include <QtNetwork/qtcpserver.h> #include <QtNetwork/qtcpsocket.h> #include <memory> class FtpDataChannel : public QObject{ Q_OBJECT public: explicit FtpDataChannel(QObject *parent = nullptr); void listen(const QHostAddress &address = QHostAddress::Any); void sendData(const QByteArray &data); void close(); QString portspec() const; QTcpServer m_server; std::unique_ptr<QTcpSocket> m_socket; signals: void dataReceived(const QByteArray &data); }; #endif -
随后,打开
ftpdatachannel.cpp源文件并写入以下代码:#include "ftpdatachannel.h" FtpDataChannel::FtpDataChannel(QObject *parent) : QObject(parent){ connect(&m_server, &QTcpServer::newConnection, this, [this](){ m_socket.reset(m_server.nextPendingConnection()); connect(m_socket.get(), &QTcpSocket::readyRead, this, [this](){ emit dataReceived(m_socket->readAll()); }); connect(m_socket.get(), &QTcpSocket::bytesWritten, this, this{ qDebug() << bytes; close(); }); }); } -
然后,我们继续实现
FtpDataChannel类的函数,例如listen()、sendData()和close():void FtpDataChannel::listen(const QHostAddress &address){ m_server.listen(address); } void FtpDataChannel::sendData(const QByteArray &data){ if (m_socket) m_socket->write(QByteArray(data).replace("\n", "\r\n")); } void FtpDataChannel::close(){ if (m_socket) m_socket->disconnectFromHost(); } -
最后,我们实现
postspec()函数,该函数以特殊格式组合 FTP 服务器的信息,以便发送回 FTP 服务器进行验证:QString FtpDataChannel::portspec() const{ QString portSpec; quint32 ipv4 = m_server.serverAddress().toIPv4Address(); quint16 port = m_server.serverPort(); portSpec += QString::number((ipv4 & 0xff000000) >> 24); portSpec += ',' + QString::number((ipv4 & 0x00ff0000) >> 16); portSpec += ',' + QString::number((ipv4 & 0x0000ff00) >> 8); portSpec += ',' + QString::number(ipv4 & 0x000000ff); portSpec += ',' + QString::number((port & 0xff00) >> 8); portSpec += ',' + QString::number(port &0x00ff); return portSpec; } -
一旦我们完成了
FtpDataChannel类,转到FtpControlChannel。 -
打开新创建的
ftpcontrolchannel.h并将以下代码添加到头文件中:#ifndef FTPCONTROLCHANNEL_H #define FTPCONTROLCHANNEL_H #include <QtNetwork/qhostaddress.h> #include <QtNetwork/qtcpsocket.h> #include <QtCore/qobject.h> class FtpControlChannel : public QObject{ Q_OBJECT public: explicit FtpControlChannel(QObject *parent = nullptr); void connectToServer(const QString &server); void command(const QByteArray &command, const QByteArray ¶ms); public slots: void error(QAbstractSocket::SocketError); signals: void opened(const QHostAddress &localAddress, int localPort); void closed(); void info(const QByteArray &info); void reply(int code, const QByteArray ¶meters); void invalidReply(const QByteArray &reply); private: void onReadyRead(); QTcpSocket m_socket; QByteArray m_buffer; }; #endif // FTPCONTROLCHANNEL_H -
然后,让我们打开
ftpcontrolchannel.cpp并写入以下代码:#include "ftpcontrolchannel.h" #include <QtCore/qcoreapplication.h> FtpControlChannel::FtpControlChannel(QObject *parent) : QObject(parent){ connect(&m_socket, &QIODevice::readyRead, this, &FtpControlChannel::onReadyRead); connect(&m_socket, &QAbstractSocket::disconnected, this, &FtpControlChannel::closed); connect(&m_socket, &QAbstractSocket::connected, this, [this]() { emit opened(m_socket.localAddress(), m_socket.localPort()); }); connect(&m_socket, &QAbstractSocket::errorOccurred, this, &FtpControlChannel::error); } -
然后,我们继续实现类的其他函数,例如
connectToServer()和command():void FtpControlChannel::connectToServer(const QString &server){ m_socket.connectToHost(server, 21); } void FtpControlChannel::command(const QByteArray &command, const QByteArray ¶ms){ QByteArray sendData = command; if (!params.isEmpty()) sendData += " " + params; m_socket.write(sendData + "\r\n"); } -
随后,我们继续为其槽函数编写代码——即
onReadyRead()和error():void FtpControlChannel::onReadyRead(){ m_buffer.append(m_socket.readAll()); int rn = -1; while ((rn = m_buffer.indexOf("\r\n")) != -1) { QByteArray received = m_buffer.mid(0, rn); m_buffer = m_buffer.mid(rn + 2); int space = received.indexOf(' '); if (space != -1) { int code = received.mid(0, space).toInt(); if (code == 0) { qDebug() << "Info received: " << received.mid(space + 1); emit info(received.mid(space + 1)); } else { qDebug() << "Reply received: " << received.mid(space + 1); emit reply(code, received.mid(space + 1)); } } else { emit invalidReply(received); } } } void FtpControlChannel::error(QAbstractSocket::SocketError error){ qWarning() << "Socket error:" << error; QCoreApplication::exit(); } -
之后,打开
mainwindow.h并添加以下头文件:#include <QDebug> #include <QNetworkAccessManager> #include <QNetworkRequest> #include <QNetworkReply> #include <QFile> #include <QFileInfo> #include <QFileDialog> #include <QListWidgetItem> #include <QMessageBox> #include <QThread> #include "ftpcontrolchannel.h" #include "ftpdatachannel.h" -
然后,声明
getFileList()函数,如下所示:public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); void getFileList(); -
随后,声明以下变量:
private: Ui::MainWindow *ui; FtpDataChannel* dataChannel; FtpControlChannel* controlChannel; QString ftpAddress; QString username; QString password; QStringList fileList; QString uploadFileName; QString downloadFileName; -
然后,打开
mainwindow.cpp并将以下代码添加到类构造函数中:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); dataChannel = new FtpDataChannel(this); connect(dataChannel, &FtpDataChannel::dataReceived, this, &MainWindow::dataReceived); connect(controlChannel, &FtpControlChannel::reply, this, &MainWindow::serverReply); connect(controlChannel, &FtpControlChannel::opened, this, &MainWindow::serverConnected); controlChannel = new FtpControlChannel(this); ftpAddress = "127.0.0.1/"; username = "myuser"; password = "123456"; controlChannel->connectToServer(ftpAddress); } -
之后,实现
getFileList()函数,如下所示:void MainWindow::getFileList() { controlChannel->command("PORT", dataChannel->portspec().toUtf8()); controlChannel->command("MLSD", "");} -
然后,定义
on_openButton_clicked()槽函数,该函数在点击打开按钮时被触发,如下面的代码所示:void MainWindow::on_openButton_clicked() { QString fileName = QFileDialog::getOpenFileName(this, "Select File", qApp->applicationDirPath()); ui->uploadFileInput->setText(fileName); } -
完成这些操作后,实现当上传按钮被点击时调用的槽函数,如下面的示例所示:
void MainWindow::on_uploadButton_clicked() { QFile* file = new QFile(ui->uploadFileInput->text()); QFileInfo fileInfo(*file); uploadFileName = fileInfo.fileName(); controlChannel->command("PORT", dataChannel->portspec().toUtf8()); controlChannel->command("STOR", uploadFileName.toUtf8()); } -
以下代码显示了
on_setFolderButton_clicked()槽函数的样子:void MainWindow::on_setFolderButton_clicked() { QString folder = QFileDialog::getExistingDirectory(this, tr("Open Directory"), qApp->applicationDirPath(), QFileDialog::ShowDirsOnly); ui->downloadPath->setText(folder); } -
接下来,定义一个槽函数,当列表小部件的某个项目被双击时将触发该函数,如下面的代码所示:
void MainWindow::on_fileList_itemDoubleClicked(QListWidgetItem *item) { downloadFileName = item->text(); QString folder = ui->downloadPath->text(); if (folder != "" && QDir(folder).exists()) { controlChannel->command("PORT", dataChannel->portspec().toUtf8()); controlChannel->command("RETR", downloadFileName.toUtf8()); } else { QMessageBox::warning(this, "Invalid Path", "Please set the download path before download."); }} -
我们还没有完成。接下来,我们将实现
serverConnected()函数,当程序成功连接到 FTP 服务器时,该函数将自动被调用,如下面的代码所示:void MainWindow::serverConnected(const QHostAddress &address, int port){ qDebug() << "Listening to:" << address << port; dataChannel->listen(address); controlChannel->command("USER", username.toUtf8()); controlChannel->command("PASS", password.toUtf8()); getFileList(); } -
我们还需要实现当 FTP 服务器回复我们的请求时将被调用的函数,如下面的示例所示:
void MainWindow::serverReply(int code, const QString ¶meters){ if (code == 150 && uploadFileName != ""){ QFile* file = new QFile(ui->uploadFileInput->text()); QFileInfo fileInfo(*file); uploadFileName = fileInfo.fileName(); if (file->open(QIODevice::ReadOnly)){ QThread::msleep(1000); QByteArray data = file->readAll(); dataChannel->sendData(data + "\n\r"); qDebug() << data; } else { QMessageBox::warning(this, "Invalid File", "Failed to open file for upload."); } } if (code == 226 && uploadFileName != ""){ uploadFileName = ""; QMessageBox::warning(this, "Upload Success", "File successfully uploaded."); } } -
dataReceived()函数用于获取从 FTP 服务器接收到的数据,其代码如下所示:void MainWindow::dataReceived(const QByteArray &data){ if (data.startsWith("type=file")){ ui->fileList->clear(); QStringList fileList = QString(data).split("\r\n"); if (fileList.length() > 0){ for (int i = 0; i < fileList.length(); ++i){ if (fileList.at(i) != ""){ QStringList fileInfo = fileList.at(i).split(";"); QString fileName = fileInfo.at(4).simplified(); ui->fileList->addItem(fileName); } } } } else { QString folder = ui->downloadPath->text(); QFile file(folder + "/" + downloadFileName); file.open(QIODevice::WriteOnly); file.write((data)); file.close(); QMessageBox::information(this, "Success", "File successfully downloaded."); } } -
最后,构建并运行程序。尝试上传一些文件到 FTP 服务器。如果成功,文件列表应该会更新并显示在列表小部件上。然后,尝试双击列表小部件上的文件名,将文件下载到您的计算机上,如下面的屏幕截图所示:

图 7.15 – 通过双击从 FTP 服务器下载文件
- 您还可以尝试通过点击打开按钮,选择所需的文件,然后按下上传按钮来上传文件,如下面的屏幕截图所示:

图 7.16 – 将文件上传到 FTP 服务器
- 恭喜您,您现在已经成功创建了一个可工作的 FTP 程序!
注意
请注意,这个示例程序仅用于展示 FTP 程序最基本实现,并不是一个功能齐全的程序。如果您尝试上传/下载非文本格式的文件,则可能无法保证其正常工作。如果 FTP 服务器上已存在同名文件,也可能无法正确上传。如果您希望在此基础上扩展项目,必须自行实现这些功能。
它是如何工作的……
尽管这个项目规模更大,代码也更长,但实际上它与我们在之前菜谱中完成的 TCP 网络项目非常相似。我们还利用了 Qt 6 提供的信号和槽机制来简化我们的工作。
在过去,Qt 曾经支持QNetworkAccessManager类中的 FTP。然而,自 Qt 6 以来,FTP 已被弃用,因此我们必须自己实现它。
我们必须了解一些最常见的 FTP 命令,并在我们的程序中利用它们。更多信息,请查看www.serv-u.com/resources/tutorial/appe-stor-stou-retr-list-mlsd-mlst-ftp-command。
FtpControlChannel 和 FtpDataChannel 类是从 Qt 的官方 Git 仓库中提取的,并进行了一些微小的修改:code.qt.io/cgit/qt/qtscxml.git/tree/examples/scxml/ftpclient。
第八章:线程基础 – 异步编程
大多数现代软件都并行运行其进程,并将任务卸载到不同的线程,以利用现代 CPU 多核架构。这样,软件可以通过同时运行多个进程来提高效率,而不会影响性能。在本章中,我们将学习如何利用 线程 来提高我们的 Qt 6 应用程序的性能和效率。
本章将涵盖以下食谱:
-
使用线程
-
QObject和QThread -
数据保护和线程间数据共享
-
与
QRunnable进程一起工作
技术要求
本章的技术要求包括 Qt 6.6.1 和 Qt Creator 12.0.2。本章使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter08。
使用线程
Qt 6 提供了多种创建和使用线程的方法。你可以选择高级方法或低级方法。高级方法更容易上手,但功能有限。相反,低级方法更灵活,但不适合初学者。在本食谱中,我们将学习如何使用一种高级方法轻松创建多线程 Qt 6 应用程序。
如何做到这一点…
让我们按照以下步骤学习如何创建多线程应用程序:
-
创建一个
main.cpp文件。然后,在文件顶部添加以下头文件:#include <QFuture> #include <QtConcurrent/QtConcurrent> #include <QFutureWatcher> #include <QThread> #include <QDebug> -
然后,在
main()函数之前创建一个名为printText()的函数:void printText(QString text, int count) { for (int i = 0; i < count; ++i) qDebug() << text << QThread::currentThreadId(); qDebug() << text << "Done"; } -
然后,在
main()函数之前添加以下代码:int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); printText("A", 100); printText("B", 100); return a.exec(); } -
如果你现在构建并运行程序,你应该会看到
A在B之前被打印出来。请注意,它们的线程 ID 都是一样的。这是因为我们正在主线程中运行printText()函数:... "A" 0x2b82c "A" 0x2b82c "A" 0x2b82c "A" Done ... "B" 0x2b82c "B" 0x2b82c "B" 0x2b82c "B" Done -
为了将它们分离到不同的线程中,让我们使用 Qt 6 提供的一个高级类
QFuture。在main()中注释掉两个printText()函数,并使用以下代码代替:QFuture<void> f1 = QtConcurrent::run(printText, QString("A"), 100); QFuture<void> f2 = QtConcurrent::run(printText, QString("B"), 100); QFuture<void> f3 = QtConcurrent::run(printText, QString("C"), 100); f1.waitForFinished(); f2.waitForFinished(); f3.waitForFinished(); -
如果你再次构建并运行程序,你应该在调试窗口中看到以下类似内容被打印出来,这意味着三个
printText()函数现在并行运行:... "A" 0x271ec "C" 0x26808 "B" 0x27a40 "A" 0x271ec "C" Done "B" 0x27a40 "A" Done "B" Done -
我们还可以使用
QFutureWatcher类通过信号和槽机制通知一个QObject类。QFutureWatcher类允许我们使用信号和槽来监控QFuture:QFuture<void> f1 = QtConcurrent::run(printText, QString("A"), 100); QFuture<void> f2 = QtConcurrent::run(printText, QString("B"), 100); QFuture<void> f3 = QtConcurrent::run(printText, QString("C"), 100); QFutureWatcher<void> futureWatcher; QObject::connect(&futureWatcher, QFutureWatcher<void>::finished, &w, MainWindow::mySlot); futureWatcher.setFuture(f1); f1.waitForFinished(); f2.waitForFinished(); f3.waitForFinished(); -
然后,打开
mainwindow.h并声明槽函数:public slots: void mySlot(); -
mySlot()函数在mainwindow.cpp中的样子如下:void MainWindow::mySlot() { qDebug() << "Done!" << QThread::currentThreadId(); } -
如果你再次构建并运行程序,这次,你会看到以下结果:
... "A" 0x271ec "C" 0x26808 "B" 0x27a40 "A" 0x271ec "C" Done "B" 0x27a40 "A" Done "B" Done QFutureWatcher is linked to f1, the Done! message only gets printed after all of the threads have finished executing. This is because mySlot() runs in the main thread, proven by the thread ID shown in the debug window alongside the Done! message.
它是如何工作的…
默认情况下,任何 Qt 6 应用程序中都有一个主线程(也称为 GUI 线程)。你创建的其他线程被称为 工作线程。
与 GUI 相关的类,如QWidget和QPixmap,只能存在于主线程中,因此处理这些类时必须格外小心。
QFuture是一个处理异步计算的高级类。
我们使用QFutureWatcher类让QFuture与信号和槽交互。你甚至可以使用它来在进度条上显示操作的进度。
QObject 和 QThread
接下来,我们想要探索一些其他方法,以便我们可以在 Qt 6 应用程序中使用线程。Qt 6 提供了一个名为QThread的类,它允许你更灵活地创建和执行线程。一个QThread对象通过调用run()函数开始在一个线程中执行其事件循环。在这个例子中,我们将学习如何通过Qthread类使QObject类异步工作。
如何做到这一点...
让我们通过以下步骤开始:
- 创建一个新的 Qt 小部件应用程序项目。然后,转到文件 | 新建文件或项目...并创建一个C++ 类文件:

图 8.1 – 创建一个新的 C++类
- 之后,将新类命名为
MyWorker并使其继承自QObject类。别忘了默认包含QObject类:

图 8.2 – 定义 MyWorker C++类
-
一旦创建了
MyWorker类,打开myworker.h并在顶部添加以下头文件:#include <QObject> #include <QDebug> -
之后,将以下信号和槽函数也添加到文件中:
signals: void showResults(int res); void doneProcess(); public slots: void process(); -
接下来,打开
myworker.cpp并实现process()函数:void MyWorker::process() { int result = 0; for (int i = 0; i < 2000000000; ++i) { result += 1; } emit showResults(result); emit doneProcess(); } -
之后,打开
mainwindow.h并在顶部添加以下头文件:#include <QDebug> #include <QThread> #include "myworker.h" -
然后,声明一个槽函数,如下面的代码所示:
public slots: void handleResults(int res); -
完成后,打开
mainwindow.cpp并实现handResults()函数:void MainWindow::handleResults(int res) { qDebug() << "Handle results" << res; } -
最后,我们将以下代码添加到
MainWindow类的构造函数中:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow){ ui->setupUi(this); QThread* workerThread = new QThread; MyWorker *workerObject = new MyWorker; workerObject->moveToThread(workerThread); connect(workerThread, &QThread::started, workerObject, &MyWorker::process); connect(workerObject, &MyWorker::doneProcess, workerThread, &QThread::quit); connect(workerObject, &MyWorker::doneProcess, workerObject, &MyWorker::deleteLater); connect(workerObject, &MyWorker::showResults, this, &MainWindow::handleResults); connect(workerThread, &QThread::finished, workerObject, &MyWorker::deleteLater); workerThread->start(); } -
现在构建并运行程序。你应该会看到主窗口弹出,几秒钟内没有任何动作,然后会在调试窗口中打印出一行消息:
Final result: 2000000000 -
结果是在一个单独的线程中计算的,这就是为什么主窗口可以在计算过程中平滑显示,甚至可以通过鼠标在计算过程中移动。为了看到在主线程上运行计算时的差异,让我们注释掉一些代码并直接调用
process()函数://QThread* workerThread = new QThread; MyWorker *workerObject = new MyWorker; //workerObject->moveToThread(workerThread); //connect(workerThread, &QThread::started, workerObject, &MyWorker::process); //connect(workerObject, &MyWorker::doneProcess, workerThread, &QThread::quit); connect(workerObject, &MyWorker::doneProcess, workerObject, &MyWorker::deleteLater); connect(workerObject, &MyWorker::showResults, this, &MainWindow::handleResults); //connect(workerThread, &QThread::finished, workerObject, &MyWorker::deleteLater); //workerThread->start(); workerObject->process(); -
现在构建并运行项目。这次,主窗口只有在计算完成后才会出现在屏幕上。这是因为计算阻塞了主线程(或 GUI 线程),阻止了主窗口的显示。
它是如何工作的...
QThread是除了使用QFuture类之外运行异步过程的一个替代方法。与QFuture相比,它提供了更多的控制,我们将在下面的菜谱中演示。
请注意,被移动到工作线程的QObject类不能有任何父类,因为 Qt 的设计方式是整个对象树必须存在于同一个线程中。因此,当你调用moveToThread()时,QObject类的所有子类也将被移动到工作线程。
如果你想让你的工作线程与主线程通信,请使用信号和槽机制。我们使用QThread类提供的started信号来通知我们的工作对象开始计算,因为工作线程已经创建。
然后,当计算完成后,我们发出showResult和doneProcess信号来通知线程退出,同时将最终结果传递给主线程以便打印。
最后,我们也使用信号和槽机制在一切完成后安全地删除工作线程和工作对象。
数据保护和线程间数据共享
尽管多线程使进程异步运行,但有时线程必须停止并等待其他线程。这通常发生在两个线程同时修改同一个变量时。通常,强制线程相互等待以保护共享资源,如数据。Qt 6 还提供了低级方法和高级机制来同步线程。
如何做到这一点...
我们将继续使用前一个示例项目中的代码,因为我们已经建立了一个具有多线程的运行程序:
-
打开
myworker.h并添加以下头文件:#include <QObject> #include <QDebug> #include <QMutex> -
然后,我们将添加两个新变量并对类构造函数进行一些修改:
public: explicit MyWorker(QMutex *mutex); int* myInputNumber; QMutex* myMutex; signals: void showResults(int res); void doneProcess(); -
之后,打开
myworker.cpp并将类构造函数更改为以下代码。由于对象将没有父类,我们不再需要父类输入:MyWorker::MyWorker(QMutex *mutex) { myMutex = mutex; } -
我们还将更改
process()函数,使其看起来像这样:void MyWorker::process() { myMutex->lock(); for (int i = 1; i < 100000; ++i){ *myInputNumber += i * i + 2 * i + 3 * i; } myMutex->unlock(); emit showResults(*myInputNumber); emit doneProcess(); } -
完成后,打开
mainwindow.cpp并对代码进行一些修改:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); int myNumber = 5; QMutex* newMutex = new QMutex; QThread* workerThread = new QThread; QThread* workerThread2 = new QThread; QThread* workerThread3 = new QThread; MyWorker *workerObject = new MyWorker(newMutex); MyWorker *workerObject2 = new MyWorker(newMutex); MyWorker *workerObject3 = new MyWorker(newMutex); -
之后,我们将工作对象的
myInputNumber变量设置为myNumber。请注意,我们引用的是其指针而不是值:workerObject->myInputNumber = &myNumber; workerObject->moveToThread(workerThread); connect(workerThread, &QThread::started, workerObject, &MyWorker::process); connect(workerObject, &MyWorker::doneProcess, workerThread, &QThread::quit); connect(workerObject, &MyWorker::doneProcess, workerObject, &MyWorker::deleteLater); connect(workerObject, &MyWorker::showResults, this, &MainWindow::handleResults); connect(workerThread, &QThread::finished, workerObject, &MyWorker::deleteLater); -
重复执行前面的步骤两次,以设置
workerObject2、workerThread2、workerObject3和workerThread3:workerObject2->myInputNumber = &myNumber; workerObject2->moveToThread(workerThread2); connect(workerThread2, &QThread::started, workerObject2, &MyWorker::process); connect(workerObject2, &MyWorker::doneProcess, workerThread2, &QThread::quit); connect(workerObject2, &MyWorker::doneProcess, workerObject2, &MyWorker::deleteLater); connect(workerObject2, &MyWorker::showResults, this, &MainWindow::handleResults); connect(workerThread2, &QThread::finished, workerObject2, &MyWorker::deleteLater); workerObject3->myInputNumber = &myNumber; workerObject3->moveToThread(workerThread3); connect(workerThread3, &QThread::started, workerObject3, &MyWorker::process); connect(workerObject3, &MyWorker::doneProcess, workerThread3, &QThread::quit); connect(workerObject3, &MyWorker::doneProcess, workerObject3, &MyWorker::deleteLater); connect(workerObject3, &MyWorker::showResults, this, &MainWindow::handleResults); connect(workerThread3, &QThread::finished, workerObject3, &MyWorker::deleteLater); -
最后,我们将通过调用
start()来启动这些线程:workerThread->start(); workerThread2->start(); workerThread3->start(); -
如果你现在构建并运行程序,你应该看到一致的结果,无论你运行多少次:
Final result: -553579035 Final result: -1107158075 Final result: -1660737115 -
每次运行程序时我们都会得到结果,因为互斥锁确保只有一条线程可以修改数据,而其他线程则等待其完成。为了看到没有互斥锁的差异,让我们注释掉代码:
void MyWorker::process() { //myMutex->lock(); for (int i = 1; i < 100000; ++i) { *myInputNumber += i * i + 2 * i + 3 * i; } //myMutex->unlock(); emit showResults(*myInputNumber); emit doneProcess(); } -
再次构建和运行程序。这次,当你运行程序时,你会得到一个非常不同的结果。例如,我在运行它三次时获得了以下结果:
1st time: Final result: -589341102 Final result: 403417142 Final result: -978935318 2nd time: Final result: 699389030 Final result: -175723048 Final result: 1293365532 3rd time: Final result: 1072831160 Final result: 472989964 Final result: -534842088 -
这是因为
myNumber数据由于并行计算的性质,所有线程同时以随机顺序进行操作。通过锁定互斥锁,我们确保数据只能由单个线程修改,从而消除这个问题。
它是如何工作的…
Qt 6 提供了两个类,即 QMutex 和 QReadWriteLock,用于在多个线程访问和修改相同数据时的数据保护。我们之前只使用了 QMutex,但这两个类在本质上非常相似。唯一的区别是 QReadWriteLock 允许其他线程在数据写入时同时读取数据。与 QMutex 不同,它将读取和写入状态分开,但一次只能发生一个(要么锁定以读取,要么锁定以写入),不能同时发生。对于复杂函数和语句,请使用高级的 QMutexLocker 类来简化代码并更容易调试。
这种方法的缺点是,当单个线程修改数据时,其他所有线程都将处于空闲状态。除非没有其他方法,否则最好不要在多个线程之间共享数据,因为这会阻止其他线程并违背并行计算的目的。
与 QRunnable 进程一起工作
在这个菜谱中,我们将学习如何使用另一种高级方法轻松创建多线程 Qt 6 应用程序。我们将在这个菜谱中使用 QRunnable 和 QThreadPool 类。
如何做到这一点…
-
创建一个新的 Qt 小部件应用程序项目,然后创建一个新的名为
MyProcess的 C++ 类,该类继承自QRunnable类。 -
接下来,打开
myprocess.h并添加以下头文件:#include <QRunnable> #include <QDebug> -
然后,声明
run()函数,如下所示:class MyProcess : public QRunnable { public: MyProcess(); void run(); }; -
之后,打开
myprocess.cpp并定义run()函数:void MyProcess::run() { int myNumber = 0; for (int i = 0; i < 100000000; ++i) { myNumber += i; } qDebug() << myNumber; } -
完成后,将以下头文件添加到
mainwindow.h中:#include <QMainWindow> #include <QThreadPool> #include "myprocess.h" -
之后,我们将通过添加以下代码来实现类构造函数:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); MyProcess* process = new MyProcess; MyProcess* process2 = new MyProcess; MyProcess* process3 = new MyProcess; MyProcess* process4 = new MyProcess; QThreadPool::globalInstance()->start(process); QThreadPool::globalInstance()->start(process2); QThreadPool::globalInstance()->start(process3); QThreadPool::globalInstance()->start(process4); qDebug() << QThreadPool::globalInstance()- >activeThreadCount(); } -
现在,构建并运行项目。你应该会看到进程在不同的线程中成功运行,其中活动线程数为四个。
-
QThreadPool类在其最后一个进程执行完毕后会自动停用线程。让我们通过暂停程序三秒并再次打印活动线程数来尝试并证明这一点:qDebug() << QThreadPool::globalInstance()->activeThreadCount(); this->thread()->sleep(3); qDebug() << QThreadPool::globalInstance()->activeThreadCount(); -
再次构建并运行程序。这次,你应该会看到活动线程数为四个,然后,在经过三秒后,活动线程数变为零。这是因为所有进程都已执行完毕。
它是如何工作的…
QRunnable 类与 QThreadPool 类紧密协作,后者管理线程集合。QThreadPool 类自动管理和回收单个 QThreads 对象,以避免频繁创建和销毁线程,这有助于降低计算成本。
要使用 QThreadPool,您必须对 QRunnable 对象进行子类化并实现名为 run() 的虚拟函数。默认情况下,QThreadPool 将在最后一个线程退出 run 函数时自动删除 QRunnable 对象。您可以通过调用 setAutoDelete() 来改变 autoDelete 变量设置为 false 来改变这种行为。
默认情况下,超过 30 秒未被使用的线程将过期。您可以在线程运行之前调用 setExpiryTimeout() 来改变这个持续时间。否则,超时设置将不会产生任何效果。
您也可以通过调用 setMaxThreadCount() 来设置可使用的最大线程数。要获取当前活动线程的总数,只需调用 activeThreadCount()。
第九章:使用 Qt 6 构建触摸屏应用程序
Qt 不仅是一个适用于 PC 平台的跨平台软件开发工具包;它还支持移动平台,如 iOS 和 Android。Qt 的开发者在 2010 年引入了Qt Quick,它提供了一种简单的方式来构建高度动态的自定义用户界面,用户可以通过仅使用最少的编码轻松创建流畅的过渡和效果。
Qt Quick 使用一种名为QML的声明性脚本语言,这与在 Web 开发中使用的JavaScript语言类似。高级用户还可以在 C++中创建自定义函数并将它们移植到 Qt Quick 中,以增强其功能。目前,Qt Quick 支持多个平台,如 Windows、Linux、macOS、iOS 和 Android。
本章将涵盖以下食谱:
-
为移动应用程序设置 Qt
-
使用 QML 设计基本用户界面
-
触摸事件
-
QML 中的动画
-
使用模型/视图显示信息
-
集成 QML 和 C++
技术要求
本章的技术要求包括 Qt 6.6.1、Qt Creator 12.0.2、Android 软件开发工具包(SDK)、Android 本地开发工具包(NDK)、Java 开发工具包(JDK)和 Apache Ant。本章使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter09。
为移动应用程序设置 Qt
在本例中,我们将学习如何在 Qt Quick 中设置我们的 Qt 项目,并使其能够构建并导出到移动设备。
如何操作…
让我们开始学习如何使用 Qt 6 创建我们的第一个移动应用程序:
- 首先,让我们通过访问文件 | 新建项目…来创建一个新的项目。然后,将弹出一个窗口供您选择项目模板。选择Qt Quick 应用程序并点击选择...按钮,如图下截图所示:

图 9.1 – 创建 Qt Quick 应用程序项目
- 之后,输入项目名称并选择项目位置。点击下一步按钮,系统将要求您选择项目所需的最低 Qt 版本。
重要提示
请确保您选择的是您计算机上存在的版本。否则,您将无法正确运行它。
-
完成这些操作后,通过点击下一步按钮继续。
-
然后,Qt Creator 将询问您希望为项目使用哪个套件。这些套件基本上是不同的编译器,您可以使用它们为不同的平台编译项目。由于我们正在为移动平台制作应用程序,我们将启用 Android 套件(如果您正在运行 Mac,则为 iOS 套件)以构建和导出您的应用到移动设备,如下面的截图所示。您还可以启用桌面套件之一,以便您可以在桌面平台上事先测试您的程序。请注意,如果您是第一次使用 Android 套件,则需要配置它,以便 Qt 可以找到 Android SDK 的目录。完成配置后,点击下一步:

图 9.2 – 为此项目创建 Android 套件
-
一旦项目创建完成,Qt Creator 将自动打开一个名为
Main.qml的项目文件。您将看到与您通常的 C/C++项目非常不同类型的脚本,如下面的代码所示:import QtQuick import QtQuick.Window Window { visible: true width: 640 height: 480 title: qsTr("Hello World") } -
现在通过点击 Qt Creator 左下角的绿色箭头按钮来构建和运行项目,如图图 9.3所示。如果您将默认套件设置为桌面套件之一,则在项目编译完成后将弹出一个空窗口:

图 9.3 – 点击三角形按钮进行构建和运行
- 如下一张截图所示,我们可以通过转到项目界面并选择您希望项目使用的套件来在不同的套件之间切换。您还可以从项目界面管理您计算机上可用的所有套件或向项目添加新的套件:

图 9.4 – 在项目界面更改任何套件
- 如果这是您第一次构建和运行项目,您需要在构建设置下为 Android 套件创建一个模板。一旦您在构建 Android APK标签页下点击了创建模板按钮,如图图 9.5所示,Qt 将生成运行您的应用所需的全部文件。如果您不打算在项目中使用 Gradle,请禁用将 Gradle 文件复制到 Android 目录选项。否则,在尝试编译和部署您的应用到移动设备时可能会遇到问题:

图 9.5 – 点击创建模板按钮以创建 Android 模板文件
- 一旦点击
AndroidManifest.xml、与 Gradle 相关的文件以及 Android 平台所需的其它资源。让我们打开AndroidManifest.xml文件:

图 9.6 – 在 AndroidManifest.xml 中设置您的应用设置
-
一旦您打开了
AndroidManifest.xml,您可以在导出应用程序之前设置您的应用程序包名、版本代码、应用程序图标和权限。要构建和测试您的 Android 应用程序,请点击 Qt Creator 上的运行按钮。现在您应该会看到一个窗口弹出,询问它应该导出到哪个设备。 -
选择当前连接到您电脑的设备,并按下确定按钮。等待片刻,让它构建项目,您应该能够在您的移动设备上运行一个空白的应用程序。
它是如何工作的...
Qt Quick 应用程序项目与窗口应用程序项目有很大不同。您大部分时间将编写 QML 脚本而不是 C/C++代码。Android 软件开发工具包(SDK)、Android 本地开发工具包(NDK)、Java 开发工具包(JDK)和Apache Ant是构建并将您的应用程序导出到 Android 平台所必需的。
或者,您也可以使用 Gradle 而不是 Apache Ant 来构建您的 Android 套件。您需要做的只是启用使用 Gradle而不是 Ant 选项,并给 Qt 提供 Gradle 的安装路径。请注意,截至本书编写时,Android Studio 目前不支持 Qt Creator:

图 9.7 – 在首选项窗口的 Android 选项卡中设置您的 Android 设置
如果您在 Android 设备上运行应用程序,请确保您已启用USB 调试模式。要启用 USB 调试模式,您需要首先通过转到设置 | 关于手机并点击构建号七次来启用您的 Android 设备上的开发者选项。之后,转到设置 | 开发者选项,您将在菜单中看到USB 调试选项。启用该选项,您现在可以将应用程序导出到您的设备进行测试。
要为 iOS 平台构建,您需要在 Mac 上运行 Qt Creator,并确保您的 Mac 上已安装最新的Xcode。要在 iOS 设备上测试您的应用程序,您需要向 Apple 注册一个开发者账户,在开发者门户注册您的设备,并将配置文件安装到您的Xcode中,这比 Android 复杂得多。一旦您从 Apple 获得开发者账户,您将获得对开发者门户的访问权限。
使用 QML 设计基本用户界面
本例将教会我们如何使用 Qt Design Studio 来设计我们程序的用户界面。
如何操作...
让我们按照以下步骤开始:
- 首先,创建一个新的Qt Quick 应用程序项目,就像我们在前面的食谱中所做的那样。然而,这次,请确保您还勾选了创建一个可以在 Qt Design Studio 中打开的项目选项:

图 9.8 – 确保您的项目可以被 Qt Design Studio 打开
-
您将在项目资源中看到一个名为
main.qml的 QML 文件。这是我们实现应用程序逻辑的地方,但我们还需要另一个 QML 文件来定义我们的用户界面。 -
在我们继续设计程序的用户界面之前,让我们从 Qt 的官方网站下载并安装Qt Design Studio:
www.qt.io/product/ui-design-tools。这是一个 Qt 为 UI/UX 设计师创建的新编辑器,用于设计他们的 Qt Quick 项目的用户界面。 -
一旦您通过按下打开项目…按钮将
.qmlproject文件安装到项目目录中:

图 9.9 – 点击“打开项目…”按钮
-
之后,Qt Design Studio将打开一个默认的 QML UI 文件,名为
Sreen01.ui.qml。您将看到一个与之前章节中使用的完全不同的用户界面编辑器。 -
自从 Qt 6 以来,Qt 团队发布了Qt Design Studio,这是一个专门用于为 Qt Quick 项目设计用户界面的新编辑器。该编辑器的组件描述如下:
-
组件:组件窗口显示您可以添加到用户界面画布上的所有预定义 QML 类型。您还可以从创建组件按钮创建自定义 Qt Quick 组件,并将它们显示在这里。
-
导航器:导航器窗口以树状结构显示当前 QML 文件中的项目。
-
连接:您可以使用连接窗口中提供的工具将对象连接到信号,指定对象的动态属性,并在两个对象的属性之间创建绑定。
-
状态:状态窗口显示项目的不同状态。您可以通过点击状态窗口右侧的+按钮为项目添加一个新状态。
-
2D/3D 画布:画布是您设计程序用户界面的地方。您可以从“组件”窗口中将一个Qt Quick组件拖放到画布上,并立即看到它在程序中的样子。您可以为不同类型的应用程序创建 2D 或 3D 画布。
-
属性:这是您更改所选项目属性的地方。
-
-
您还可以通过在右上角的下拉框中选择来为您的Qt Design Studio编辑器选择预定义的工作空间:

图 9.10 – 选择预定义的工作空间
-
我们即将制作一个简单的登录屏幕。首先,从 2D 画布中删除编辑组件。然后,从“组件”窗口中拖动两个文本小部件到画布上。
-
设置
用户名:和密码::

图 9.11 – 设置文本属性
-
从
1和5拖动两个矩形。然后,将其中一个文本字段的回显模式设置为密码。 -
现在,我们将通过将鼠标区域小部件与矩形和文本小部件组合来手动创建一个按钮小部件。将鼠标区域小部件拖放到画布上,然后拖动矩形和文本小部件到画布上,并将它们都设置为鼠标区域的小部件。将矩形的颜色设置为
#bdbdbd,然后设置其1和5。然后,设置Login并确保鼠标区域的大小与矩形相同。 -
之后,将另一个矩形拖放到画布上,作为登录表单的容器,使其看起来整洁。将其颜色设置为
#5e5858,然后设置其2。然后,设置其5以使其角落看起来稍微圆润一些。 -
确保我们在上一步中添加的矩形在导航器窗口的层次结构顶部定位,这样它就会出现在所有其他小部件的后面。你可以通过按下导航器窗口顶部的箭头按钮来安排层次结构内的小部件位置,如下所示:

图 9.12 – 点击向上移动按钮
-
接下来,我们将导出三个小部件:鼠标区域和两个文本输入小部件作为根项的别名属性,这样我们就可以在以后从
App.qml文件中访问这些小部件。可以通过点击小部件名称后面的图标来导出小部件,并确保图标变为开启状态。 -
到目前为止,你的用户界面应该看起来像这样:

图 9.13 – 一个简单的登录屏幕
-
现在,让我们打开
App.qml。Qt Creator 不会在Screen01.ui.qml中打开这个文件,App.qml仅用于定义将应用于 UI 的逻辑和函数。然而,你可以通过点击编辑器左侧侧边栏上的设计按钮,使用 Qt Design Studio 打开它来预览用户界面。 -
在脚本顶部,将第三行添加到
App.qml中导入对话框模块,如下面的代码所示:import QtQuick import QtQuick.Dialogs import yourprojectname -
之后,将以下代码替换为这个:
Window { visible: true title: "Hello World" width: 360 height: 360 Screen01 { anchors.fill: parent loginButton.onClicked: { messageDialog.text = "Username is " + userInput.text + " and password is " + passInput.text messageDialog.visible = true } } -
我们继续定义
messageDialog如下:MessageDialog { id: messageDialog title: "Fake login" text: "" onAccepted: { console.log("You have clicked the login button") Qt.quit() } } } -
在你的 PC 上构建并运行这个程序,你应该得到一个简单的程序,当你点击登录按钮时会显示一个消息框:

图 9.14 – 点击登录按钮后显示的消息框
它是如何工作的…
自从 Qt 5.4 以来,引入了一个新的文件扩展名.ui.qml。QML 引擎像处理正常的.qml文件一样处理它,但禁止在其中编写任何逻辑实现。它作为用户界面定义模板,可以在不同的.qml文件中重用。UI 定义和逻辑实现的分离提高了 QML 代码的可维护性,并创建了一个更好的工作流程。
自从 Qt 6 以来,.ui.qml文件不再由 Qt Creator 处理。相反,Qt 为您提供了一个名为 Qt Design Studio 的程序来编辑您的 Qt Quick UI。他们打算为程序员和设计师提供适合他们工作流程的独立工具。
基本下的所有小部件是我们可以用以混合匹配并创建新类型小部件的最基本小部件,如下所示:

图 9.15 – 从这里拖放小部件
在上一个示例中,我们学习了如何将三个小部件组合在一起——一个文本、一个鼠标区域和一个矩形——以形成一个按钮小部件。您也可以通过点击右上角的创建组件按钮来创建自己的自定义组件:

图 9.16 – 您也可以创建自己的自定义组件
我们在App.qml中导入了QtQuick.Dialogs模块,并创建了一个显示用户在Screen01.ui.qml中填写的用户名和密码的消息框。当我们在App.qml中无法访问它们的属性。
到目前为止,我们可以将程序导出到 iOS 和 Android,但用户界面在某些具有更高分辨率或更高每像素密度(DPI)单位的设备上可能看起来不准确。我们将在本章后面讨论这个问题。
触摸事件
在本节中,我们将学习如何使用 Qt Quick 开发一个在移动设备上运行的触摸驱动应用程序。
如何操作...
让我们按照以下步骤一步步开始:
-
创建一个新的Qt Quick 应用程序项目。
-
在 Qt Design Studio 中,点击
tux.png并将其按照以下方式添加到项目中:

图 9.17 – 将 tux.png 导入到您的项目中
-
接下来,打开
Screen01.ui.qml。从tux.png拖动一个图像小部件,并设置其200和20。 -
确保通过点击它们各自小部件名称旁边的小图标,将鼠标区域小部件和图像小部件都导出为根项的别名属性。
-
之后,通过点击位于编辑器左侧侧边栏上的编辑按钮,切换到脚本编辑器。我们需要将鼠标区域小部件更改为多点触摸区域小部件,如下面的代码所示:
MultiPointTouchArea { id: touchArea anchors.fill: parent touchPoints: [ TouchPoint { id: point1 }, TouchPoint { id: point2 } ] } -
我们还设置了图像小部件默认自动放置在窗口中心,如下所示:
Image { id: tux x: (window.width / 2) - (tux.width / 2) y: (window.height / 2) - (tux.height / 2) width: 200 height: 220 fillMode: Image.PreserveAspectFit source: "tux.png" } -
最终用户界面应该看起来像这样:

图 9.18 – 将企鹅放置到您的应用程序窗口中
-
完成这些操作后,让我们打开
App.qml。首先,清除anchors.fill: parent内的所有内容,如下面的代码所示:import QtQuick import QtQuick.Window Window { visible: true Screen01 { anchors.fill: parent } } -
之后,在MainForm对象中声明几个变量,这些变量将用于重新缩放图像小部件。如果您想了解更多关于以下代码中使用的属性关键字的信息,请查看本例末尾的更多内容部分:
property int prevPointX: 0 property int prevPointY: 0 property int curPointX: 0 property int curPointY: 0 property int prevDistX: 0 property int prevDistY: 0 property int curDistX: 0 property int curDistY: 0 property int tuxWidth: tux.width property int tuxHeight: tux.height -
使用以下代码,我们将定义当我们的手指触摸多点区域小部件时会发生什么。在这种情况下,如果多于一个手指触摸多点触摸区域,我们将保存第一个和第二个触摸点的位置。我们还保存了图像小部件的宽度和高度,以便稍后我们可以使用这些变量来计算手指开始移动时图像的缩放比例:
touchArea.onPressed: { if (touchArea.touchPoints[1].pressed) { if (touchArea.touchPoints[1].x < touchArea.touchPoints[0].x) prevDistX = touchArea.touchPoints[1].x - touchArea.touchPoints[0].x else prevDistX = touchArea.touchPoints[0].x - touchArea.touchPoints[1].x if (touchArea.touchPoints[1].y < touchArea.touchPoints[0].y) prevDistY = touchArea.touchPoints[1].y - touchArea.touchPoints[0].y else prevDistY = touchArea.touchPoints[0].y - touchArea.touchPoints[1].y tuxWidth = tux.width tuxHeight = tux.height } } -
以下图表显示了当两个手指在
touchArea边界内触摸屏幕时注册的触摸点示例。touchArea.touchPoints[0]是第一个注册的触摸点,touchArea.touchPoints[1]是第二个。然后我们计算两个触摸点之间的 X 和 Y 距离,并将它们保存为prevDistX和prevDistY,如下所示:

图 9.19 – 计算两个触摸点之间的距离
-
之后,我们将使用以下代码定义当我们的手指在保持与屏幕接触并仍在触摸区域边界内移动时会发生什么。在此点,我们将通过使用之前步骤中保存的变量来计算图像的缩放比例。同时,如果我们检测到只有一个触摸,那么我们将移动图像而不是改变其缩放比例:
touchArea.onUpdated: { if (!touchArea.touchPoints[1].pressed) { tux.x += touchArea.touchPoints[0].x - touchArea.touchPoints[0].previousX tux.y += touchArea.touchPoints[0].y - touchArea.touchPoints[0].previousY } else { if (touchArea.touchPoints[1].x < touchArea.touchPoints[0].x) curDistX = touchArea.touchPoints[1].x - touchArea.touchPoints[0].x else curDistX = touchArea.touchPoints[0].x - touchArea.touchPoints[1].x if (touchArea.touchPoints[1].y < touchArea.touchPoints[0].y) curDistY = touchArea.touchPoints[1].y - touchArea.touchPoints[0].y else curDistY = touchArea.touchPoints[0].y - touchArea.touchPoints[1].y tux.width = tuxWidth + prevDistX - curDistX tux.height = tuxHeight + prevDistY - curDistY } } -
以下图表显示了移动触摸点的示例;
touchArea.touchPoints[0]从点 A 移动到点 B,touchArea.touchPoints[1]从点 C 移动到点 D。然后我们可以通过查看先前 X 和 Y 变量与当前变量的差异来确定触摸点移动了多少单位:

图 9.20 – 比较两组触摸点以确定移动
-
您现在可以构建并将程序导出到您的移动设备上。您将无法在不支持多点触控的平台测试此程序。
-
一旦程序在移动设备(或支持多点触控的桌面/笔记本电脑)上运行,尝试两件事——只将一个手指放在屏幕上并移动它,以及将两个手指放在屏幕上并朝相反方向移动。你应该看到,如果你只使用一个手指,企鹅将被移动到另一个地方,如果你使用两个手指,它将放大或缩小,如以下截图所示:

图 9.21 – 使用手指放大和缩小
它是如何工作的……
当手指触摸设备的屏幕时,多点触控区域小部件将触发onPressed事件并记录每个触摸点的位置在一个内部数组中。我们可以通过告诉 Qt 我们想要获取哪个触摸点来获取这些数据。第一个触摸点将具有索引号 0,第二个触摸点将是 1,依此类推。然后我们将这些数据保存到变量中,以便我们可以在以后检索它们来计算企鹅图像的缩放。除了onPressed之外,如果您想在用户从触摸区域释放手指时触发事件,也可以使用onReleased。
当一个或多个手指在移动时保持与屏幕接触,多点触控区域将触发onUpdated事件。然后我们将检查有多少个触摸点;如果只找到一个触摸点,我们只需根据我们的手指移动的距离移动企鹅图像。如果有多个触摸点,我们将比较两个触摸点之间的距离,并将其与我们之前保存的变量进行比较,以确定我们应该重新缩放图像多少。
图表显示,在屏幕上轻触手指将触发onPressed事件,而在屏幕上滑动手指将触发onUpdated事件:

图 9.22 – onPressed 和 onUpdated 之间的区别
我们还必须检查第一个触摸点是否在左侧,或者第二个触摸点是否在右侧。这样,我们可以防止图像以手指移动的反方向缩放并产生不准确的结果。至于企鹅的移动,我们只需获取当前触摸位置与上一个位置之间的差异,并将其添加到企鹅的坐标中;然后,就完成了。单点触摸事件通常比多点触摸事件更直接。
还有更多...
在 Qt Quick 中,所有组件都内置了属性,例如int、float等关键字;以下是一个示例:
property int myValue;
您还可以通过在值之前使用冒号(:)将自定义property绑定到值,如下面的代码所示:
property int myValue: 100;
重要提示
要了解 Qt Quick 支持的属性类型,请查看此链接:doc.qt.io/qt-6/qtqml-typesystem-basictypes.html。
QML 中的动画
Qt 允许我们轻松地通过编写大量代码来对用户界面组件进行动画处理。在本例中,我们将学习如何通过应用动画使我们的程序用户界面更加有趣。
如何操作...
让我们按照以下步骤学习如何为我们的 Qt Quick 应用程序添加动画:
-
再次,我们将从头开始。因此,创建一个新的
Screen01.ui.qml文件。 -
打开
Screen01.ui.qml文件并转到您的项目中的QtQuick.Controls。 -
之后,你将在 QML Types 选项卡中看到一个新类别,称为 QtQuick Controls,其中包含许多可以放置在画布上的新小部件。
-
接下来,将三个按钮小部件拖到画布上,并设置它们的
45。然后,转到0。这将使按钮根据主窗口的宽度水平调整大小。之后,将第一个按钮的 y 值设置为0,第二个设置为45,第三个设置为90。现在,用户界面应该看起来像这样:

图 9.23 – 在布局中添加三个按钮
- 现在,将
fan.png打开到项目中,如下所示:

图 9.24 – 将 fan.png 添加到你的项目中
-
然后,在画布上添加两个鼠标区域小部件。之后,将一个 Rectangle 小部件和一个 Image 小部件拖到画布上。将矩形和图像设置为之前添加的鼠标区域的父级。
-
设置
#0000ff并将fan.png应用到图像小部件上。现在,你的用户界面应该看起来像这样:

图 9.25 – 在布局中放置矩形和风扇图像
- 之后,通过单击小部件名称右侧的图标,将你的
Screen01.ui.qml中的所有小部件导出为根项的别名属性,如下所示:

图 9.26 – 向小部件添加别名
-
接下来,我们将应用动画和逻辑到用户界面,但不会在
Screen01.ui.qml中进行。相反,我们将在App.qml中完成所有操作。 -
在
App.qml中,移除鼠标区域的默认代码,并添加窗口的 width 和 height,以便我们获得更多空间进行预览,如下所示:import QtQuick import QtQuick.Window Window { visible: true width: 480 height: 550 Screen01 { anchors.fill: parent } } -
之后,添加以下代码,该代码定义了
Screen01小部件中按钮的行为:button1 { Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } } onClicked: { button1.y = button1.y + (45 * 3) } } button2 { Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } } onClicked: { button2.y = button2.y + (45 * 3) } } -
在以下代码中,我们继续定义
button3:button3 { Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } } onClicked: { button3.y = button3.y + (45 * 3) } } -
然后,按照以下方式继续添加风扇图像及其附加的鼠标区域小部件的行为:
fan { RotationAnimation on rotation { id: anim01 loops: Animation.Infinite from: 0 to: -360 duration: 1000 } } -
在以下代码中,我们接着定义
mouseArea1:mouseArea1 { onPressed: { if (anim01.paused) anim01.resume() else anim01.pause() } } -
最后但同样重要的是,按照以下方式添加矩形及其附加的鼠标区域小部件的行为:
rectangle2 { id: rect2 state: "BLUE" states: [ State { name: "BLUE" PropertyChanges { target: rect2 color: "blue" } }, -
在以下代码中,我们继续添加
RED状态:State { name: "RED" PropertyChanges { target: rect2 color: "red" } } ] } -
我们接着通过如下定义
mouseArea2来完成代码:mouseArea2 { SequentialAnimation on x { loops: Animation.Infinite PropertyAnimation { to: 150; duration: 1500 } PropertyAnimation { to: 50; duration: 500 } } onClicked: { if (rect2.state == "BLUE") rect2.state = "RED" else rect2.state = "BLUE" } } -
如果你现在编译并运行程序,你应该在窗口顶部看到三个按钮,在左下角看到一个移动的矩形,在右下角看到一个旋转的风扇,如下面的截图所示。如果你点击任何按钮,它们将稍微向下移动,并带有流畅的动画。如果你点击矩形,它将从 蓝色 变为 红色。
-
同时,如果你在风扇图像动画时点击它,它将暂停动画;如果你再次点击,它将恢复动画:

图 9.27 – 现在您可以控制小部件的动画和颜色
它是如何工作的…
大多数 C++版本的 Qt 支持的动画元素,如过渡、顺序动画和并行动画,在 Qt Quick 中也是可用的。如果您熟悉 C++中的 Qt 动画框架,您应该能够很容易地掌握这一点。
在这个例子中,我们为所有三个按钮添加了一个弹簧动画元素,该元素专门跟踪各自的 y 轴。如果 Qt 检测到 y 值已更改,小部件不会立即弹跳到新位置;相反,它将被插值,在画布上移动,并在到达目的地时执行轻微的震动动画,以模拟弹簧效果。我们只需写一行代码,其余的交给 Qt 处理。
至于风扇图像,我们为其添加了一个旋转动画元素,并将其持续时间设置为1000 毫秒,这意味着它将在一秒内完成一次完整旋转。我们还将其设置为无限循环动画。当我们点击它所附加的鼠标区域小部件时,我们只需调用pause()或resume()来启用或禁用动画。
接下来,对于矩形小部件,我们为其添加了两个状态,一个称为蓝色,另一个称为红色,每个状态都携带一个颜色属性,该属性将在状态改变时应用于矩形。同时,我们在矩形所附加的鼠标区域小部件中添加了顺序动画组,然后向该组添加了两个属性动画元素。您还可以混合不同类型的组动画;Qt 可以很好地处理这一点。
使用模型/视图显示信息
Qt 包含一个模型/视图框架,它保持了数据组织和管理方式与它们向用户展示方式之间的分离。在本节中,我们将学习如何使用模型/视图;特别是通过使用列表视图来显示信息,同时,应用我们的自定义使其看起来更精致。
如何做到这一点…
让我们按照以下步骤开始:
- 将新的
home.png、map.png、profile.png、search.png、settings.png和arrow.png添加到项目中,如下所示:

图 9.28 – 向项目中添加更多图像
- 然后,创建并打开
Screen01.ui.qml,就像我们在所有之前的例子中所做的那样。从组件窗口的Qt Quick – 视图类别下拖动一个列表视图小部件到画布上。然后,通过点击布局窗口中间的按钮将其锚点设置设置为填充父大小,如图下所示:

图 9.29 – 将布局锚点设置为填充父容器
-
接下来,切换到脚本编辑器,我们将定义列表视图的外观如下:
import QtQuick Rectangle { id: rectangle1 property alias listView1: listView1 property double sizeMultiplier: width / 480 -
我们将通过添加以下列表视图来继续编写代码:
ListView { id: listView1 y: 0 height: 160 orientation: ListView.Vertical boundsBehavior: Flickable.StopAtBounds anchors.fill: parent delegate: Item { width: 80 * sizeMultiplier height: 55 * sizeMultiplier -
我们将继续向列表视图中添加行,如下所示:
Row { id: row1 Rectangle { width: listView1.width height: 55 * sizeMultiplier gradient: Gradient { GradientStop { position: 0.0; color: "#ffffff" } GradientStop { position: 1.0; color: "#f0f0f0" } } opacity: 1.0 -
然后,我们添加了一个鼠标区域和一个图像,如下所示代码片段:
MouseArea { id: mouseArea anchors.fill: parent } Image { anchors.verticalCenter: parent.verticalCenter x: 15 * sizeMultiplier width: 30 * sizeMultiplier height: 30 * sizeMultiplier source: icon } -
然后,继续添加两个文本对象,如下所示:
Text { text: title font.family: "Courier" font.pixelSize: 17 * sizeMultiplier x: 55 * sizeMultiplier y: 10 * sizeMultiplier } Text { text: subtitle font.family: "Verdana" font.pixelSize: 9 * sizeMultiplier x: 55 * sizeMultiplier y: 30 * sizeMultiplier } -
之后,添加一个图像对象,如下所示:
Image { anchors.verticalCenter: parent.verticalCenter x: parent.width - 35 * sizeMultiplier width: 30 * sizeMultiplier height: 30 * sizeMultiplier source: "images/arrow.png" } } } } -
使用以下代码,我们将定义列表模型:
model: ListModel { ListElement { title: "Home" subtitle: "Go back to dashboard" icon: "images/home.png" } ListElement { title: "Map" subtitle: "Help navigate to your destination" icon: "images/map.png" } -
我们将继续编写代码:
ListElement { title: "Profile" subtitle: "Customize your profile picture" icon: "images/profile.png" } ListElement { title: "Search" subtitle: "Search for nearby places" icon: "images/search.png" } -
我们现在将添加最终的列表元素,如下所示代码所示:
ListElement { title: "Settings" subtitle: "Customize your app settings" icon: "images/settings.png" } } } } -
之后,打开
App.qml并将代码替换为以下内容:import QtQuick import QtQuick.Window Window { visible: true width: 480 height: 480 Screen01 { anchors.fill: parent MouseArea { onPressed: row1.opacity = 0.5 onReleased: row1.opacity = 1.0 } } } -
编译并运行程序,现在您的程序应该看起来像这样:

图 9.30 – 带有不同字体和图标的导航菜单
它是如何工作的…
Qt Quick 允许我们轻松自定义列表视图中每一行的外观。委托定义了每一行将看起来是什么样子,而模型是您存储将在列表视图中显示的数据的地方。
在这个例子中,我们在每一行添加了带有渐变的背景,然后我们还在每个项目的两侧添加了图标,一个标题,一个描述,以及一个鼠标区域小部件,使得列表视图的每一行都可以点击。委托不是静态的,因为我们允许模型更改标题、描述和图标,使每一行看起来独特。
在 App.qml 中,我们定义了鼠标区域小部件的行为,当按下时会将其自身的透明度值减半,并在释放时恢复到完全不透明。由于所有其他元素,如标题和图标,都是鼠标区域小部件的子元素,因此它们也会自动遵循其父小部件的行为并变为半透明。
此外,我们最终解决了高分辨率和 DPI 的移动设备上的显示问题。这是一个非常简单的技巧;首先,我们定义了一个名为 sizeMultiplier 的变量。sizeMultiplier 的值是窗口宽度除以一个预定义值的结果,比如说 480,这是我们用于 PC 的当前窗口宽度。然后,将 sizeMultiplier 乘以所有与大小和位置相关的变量,包括字体大小。请注意,在这种情况下,您应该使用 pixelSize 属性来代替 pointSize,这样在乘以 sizeMultiplier 时您将得到正确的显示。以下截图显示了带有和不带有 sizeMultiplier 的应用程序在移动设备上的外观:

图 9.31 – 使用大小乘数校正大小
注意,一旦你将所有内容乘以sizeMultiplier变量,编辑器中的用户界面可能会变得混乱。这是因为宽度变量在编辑器中可能返回0。因此,将0乘以480,你可能会得到结果0,这使得整个用户界面看起来很奇怪。然而,当运行实际程序时,它看起来会很好。如果你想预览编辑器上的用户界面,暂时将其设置为1。
集成 QML 和 C++
Qt 支持通过 QML 引擎在 C++类之间进行桥接。这种组合允许开发者利用 QML 的简单性和 C++的灵活性。你甚至可以集成外部组件不支持的功能,然后将结果数据传递给 Qt Quick 以在 UI 中显示。在本例中,我们将学习如何将我们的用户界面组件从 QML 导出到 C++框架,并在它们显示在屏幕上之前操作它们的属性。
如何做到这一点…
让我们按以下步骤进行:
-
再次,我们将从头开始。因此,使用 Qt Design Studio 创建一个新的
Screen01.ui.qml。然后,打开Screen01.ui.qml。 -
我们可以保留鼠标区域和文本小部件,但将文本小部件放置在窗口底部。将文本小部件的
Text属性更改为18。之后,转到120,如下面的屏幕截图所示:

图 9.32 – 将其放置在布局的中心
- 接下来,从
#ff0d0d拖动一个矩形小部件。设置其200并启用垂直和水平中心锚点。之后,设置-14。你的 UI 现在应该看起来像这样:

图 9.33 – 将正方形和文本放置如图所示的图像中
- 完成后,在
myclass.h和myclass.cpp中的项目目录上右键单击——现在将创建并添加到你的项目中:

图 9.34 – 创建一个新的自定义类
-
现在,打开
myclass.h并在类构造函数下添加一个变量和函数,如下面的代码所示:#ifndef MYCLASS_H #define MYCLASS_H #include <QObject> class MyClass : public QObject { Q_OBJECT public: explicit MyClass(QObject *parent = 0); // Object pointer QObject* my Object; // Must call Q_INVOKABLE so that this function can be used in QML Q_INVOKABLE void setMyObject(QObject* obj); }; #endif // MYCLASS_H -
之后,打开
myclass.cpp并定义setMyObject()函数,如下所示:#include "myclass.h" MyClass::MyClass(QObject *parent) : Qobject(parent) { } void MyClass::setMyObject(Qobject* obj) { // Set the object pointer my Object = obj; } -
现在,我们可以关闭
myclass.cpp并打开App.qml。在文件顶部,导入我们在 C++中刚刚创建的MyClassLib组件:import QtQuick import QtQuick.Window MyClass in the Window object and call its setMyObject() function within the MainForm object, as shown in the following code:Window {
visible: true
width: 480
height: 320
MyClass {
id: myclass
}
Screen01 {
anchors.fill: parent
mouseArea.onClicked: {
Qt.quit();
}
Component.onCompleted:
myclass.setMyObject(messageText);
}
}
-
最后,打开
main.cpp并将自定义类注册到 QML 引擎。我们还将使用 C++代码更改文本小部件和矩形的属性,如下所示:#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QtQml> #include <QQuickView> #include <QQuickItem> #include <QQuickView> #include "myclass.h" int main(int argc, char *argv[]) { // Register your class to QML qmlRegisterType<MyClass>("MyClassLib", 1, 0, "MyClass"); -
然后,继续创建对象,就像以下代码中突出显示的部分一样:
QGuiApplication app(argc, argv); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/content/App.qml"))); QObject* root = engine.rootObjects().value(0); QObject* messageText = root->findChild<QObject*>("messageText"); messageText->setProperty("text", QVariant("C++ is now in control!")); messageText->setProperty("color", QVariant("green")); QObject* square = root->findChild<QObject*>("square"); square->setProperty("color", QVariant("blue")); return app.exec(); } -
现在,构建并运行程序,你应该会看到矩形的颜色和文本的颜色与你在 Qt Quick 中之前定义的完全不同,如下面的截图所示。这是因为它们的属性已经被 C++ 代码所改变:

图 9.35 – 现在可以通过 C++ 改变文本和颜色
它是如何工作的...
QML 被设计成可以通过 C++ 代码轻松扩展。Qt QML 模块中的类使 QML 对象能够从 C++ 中加载和处理。
只有继承自 QObject 基类 的类才能与 QML 集成,因为它是 Qt 生态系统的一部分。一旦类被 QML 引擎注册,我们就从 QML 引擎获取根项,并使用它来找到我们想要操作的对象。
之后,使用 setProperty() 函数来更改属于小部件的任何属性。除了 setProperty() 之外,你还可以在继承自 QObject 的类中使用 Q_PROPERTY() 宏来声明属性。以下是一个示例:
Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged)
注意,Q_INVOKABLE 宏需要放在你打算在 QML 中调用的函数之前。如果没有它,Qt 不会将函数暴露给 Qt Quick,你将无法调用它。
第十章:简化 JSON 解析
JSON 是一种名为 JavaScript Object Notation 的数据格式的文件扩展名,用于以结构化格式存储和传输信息。JSON 格式在网络上被广泛使用。大多数现代网络 应用程序编程接口 (API) 使用 JSON 格式将数据传输给其网络客户端。
本章将涵盖以下食谱:
-
JSON 格式概述
-
从文本文件处理 JSON 数据
-
将 JSON 数据写入文本文件
-
使用 Google 的 Geocoding API
技术要求
本章的技术要求包括 Qt 6.6.1 MinGW 64 位和 Qt Creator 12.0.2。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter10。
JSON 格式概述
JSON 是一种常用于网络应用程序中数据传输的易读文本格式,尤其是在 JavaScript 应用程序中。然而,它也被用于许多其他目的,因此它独立于 JavaScript,可以用于任何编程语言或平台,尽管它的名字如此。
在这个例子中,我们将了解 JSON 格式以及如何验证您的 JSON 数据是否为有效格式。
如何操作…
让我们开始学习如何编写自己的 JSON 数据并验证其格式:
-
打开您的网络浏览器并转到
jsonlint.com上的 JSONLint Online Validator and Formatter 网站。 -
在网站上的文本编辑器中编写以下 JSON 数据:
{ "members": [ { "name": "John", "age": 29, "gender": "male" }, { "name": "Alice", "age": 24, "gender": "female" }, { "name": "", "age": 26, "gender": "male" } ] } -
之后,按下 Validate JSON 按钮。你应该会得到以下结果:
JSON is valid! -
现在,尝试从
members变量中移除双引号符号:{ members: [ { "name": "John", "age": 29, "gender": "male" }, -
再次按下 Validate JSON 按钮,你应该会得到一个如下错误:
Invalid JSON! Error: Parse error on line 1: { members: [ { -----^ Expecting 'STRING', '}', got 'undefined' -
现在,通过将双引号符号添加回
members变量来恢复有效的 JSON 格式。然后,按下 Compress 按钮。你应该会得到以下结果,其中没有空格和换行符:{"members":[{"name":"John","age":29,"gender":"male"}, {"name":"Alice","age":24,"gender":"female"}, {"name":"","age":26,"gender":"male"}]} -
你现在可以按下 Prettify 按钮将其恢复到之前的结构。
它是如何工作的…
大括号 { 和 } 包含一组数据作为对象。一个 对象 是一个包含其自身属性或变量的独立数据结构,这些属性或变量以 键值对 的形式存在。键是作为人类可读变量名的唯一字符串,而值由字符串、整数、浮点数、布尔值表示,甚至可以是一个完整的对象或数组。JSON 支持递归对象,这对于许多不同的用例非常有用。
方括号 [ 和 ] 表示数据包含在数组中。数组简单地存储相同类型的值列表,可以通过在您用于项目的任何编程语言中使用标准迭代器模式遍历其内容来操作、排序或从数组中删除。
在之前的示例中,我们首先创建了一个无名的对象作为数据的主对象。你必须创建一个主对象或主数组作为起点。然后,我们添加了一个名为 members 的数组,其中包含具有 name、age 和 gender 等变量的单个对象。请注意,如果你在整数或浮点数周围添加双引号 ("),变量将被视为字符串而不是数字。
之前的示例演示了可以通过网络发送并由任何现代编程语言处理的 JSON 数据的最简单形式。
处理文本文件中的 JSON 数据
在本食谱中,我们将学习如何处理从文本文件中获取的 JSON 数据并使用流读取器提取它。
如何做到这一点…
让我们创建一个简单的程序,通过以下步骤读取和处理 XML 文件:
-
在你希望的位置创建一个新的 Qt Widgets 应用程序 项目。
-
打开任何文本编辑器并创建一个看起来像下面的 JSON 文件,然后将其保存为
scene.json:[ { "name": "Library", "tag": "building", "position": [120.0, 0.0, 50.68], "rotation": [0.0, 0.0, 0.0], "scale": [1.0, 1.0, 1.0] } ] -
继续编写 JSON 代码,在
Library对象之后添加更多对象,如下面的代码所示:{ "name": "Town Hall", "tag": "building", "position": [80.2, 0.0, 20.5], "rotation": [0.0, 0.0, 0.0], "scale": [1.0, 1.0, 1.0] }, { "name": "Tree", "tag": "prop", "position": [10.46, -0.2, 80.2], "rotation": [0.0, 0.0, 0.0], "scale": [1.0, 1.0, 1.0] } -
返回 Qt Creator 并打开
mainwindow.h。在脚本顶部#include <QMainWindow>之后添加以下头文件:#include <QJsonDocument> #include <QJsonArray> #include <QJsonObject> #include <QDebug> #include <QFile> #include <QFileDialog> -
打开
mainwindow.ui并从左侧的部件框中拖动一个按钮到 UI 编辑器。将按钮的对象名称更改为loadJsonButton并将其显示文本更改为Load JSON:

图 10.1 – 添加 Load JSON 按钮图
-
右键单击按钮并选择 转到槽…。将弹出一个窗口,其中包含可供选择的信号列表。
-
选择默认的
clicked()选项并按on_loadJsonButton_clicked()。 -
将以下代码添加到
on_loadJsonButton_clicked()函数中:void MainWindow::on_loadJsonButton_clicked() { QString filename = QFileDialog::getOpenFileName(this, "Open JSON", ".", "JSON files (*.json)"); QFile file(filename); if (!file.open(QFile::ReadOnly | QFile::Text)) qDebug() << "Error loading JSON file."; QByteArray data = file.readAll(); file.close(); QJsonDocument json = QJsonDocument::fromJson(data); -
我们继续编写代码。以下代码遍历 JSON 文件并打印出每个属性的名称和值:
if (json.isArray()) { QJsonArray array = json.array(); if (array.size() > 0) { for (int i = 0; i < array.size(); ++i) { qDebug() << "[Object]================================="; QJsonObject object = json[i].toObject(); QStringList keys = object.keys(); for (int j = 0; j < keys.size(); ++j) { qDebug() << keys.at(j) << object.value(keys.at(j)); } qDebug() << "========================================="; } } } -
构建并运行项目,你将看到一个弹出窗口,其外观类似于你在 步骤 5 中创建的:

图 10.2 – 构建和启动程序
- 点击 加载 JSON 按钮,你应该会在屏幕上弹出 文件选择器 窗口。选择你在 步骤 2 中创建的 JSON 文件并按 选择 按钮。你应该会在 Qt Creator 的 应用程序输出 窗口中看到以下调试文本出现,这表明程序已成功加载你刚刚选择的 JSON 文件中的数据:

图 10.3 – 应用输出窗口中打印的结果
它是如何工作的…
在本例中,我们正在尝试使用QJsonDocument类从 JSON 文件中提取和处理数据。想象一下你正在制作一个电脑游戏,你正在使用 JSON 文件来存储游戏场景中所有对象的属性。在这种情况下,JSON 格式在以结构化方式存储数据方面发挥着重要作用,这使得数据提取变得容易。
我们需要将相关的 JSON 类头添加到我们的源文件中,在这种情况下,是QJsonDocument类。QJsonDocument类是 Qt 核心库的一部分,因此不需要包含任何额外的模块,这也意味着它是推荐用于在 Qt 中处理 JSON 数据的类。一旦我们点击on_loadJsonButton_clicked()槽,就会调用;这就是我们编写代码来处理 JSON 数据的地方。
我们使用文件对话框来选择我们想要处理的 JSON 文件。然后,我们将所选文件的文件名及其路径发送到QFile类以打开和读取 JSON 文件的文本数据。之后,文件的数据被发送到QJsonDocument类进行处理。
我们首先检查主 JSON 结构是否为数组。如果是数组,我们接着检查数组中是否有数据。之后,使用for循环遍历整个数组,并提取存储在数组中的单个对象。
然后,我们从对象中提取键值对数据,并打印出键和值。
还有更多…
除了网络应用之外,许多商业游戏引擎和交互式应用也使用 JSON 格式来存储用于其产品中的游戏场景、网格以及其他形式资产的信息。这是因为 JSON 格式相较于其他文件格式提供了许多优势,例如紧凑的文件大小、高度的灵活性和可扩展性、易于文件恢复,以及允许其用于高度高效和性能关键的应用程序,如搜索引擎、智能数据挖掘服务器和科学模拟。
注意
要了解更多关于 XML 格式的信息,请访问www.w3schools.com/js/js_json_intro.asp。
将 JSON 数据写入文本文件
由于我们已经在前一个菜谱中学习了如何处理从 JSON 文件中获得的数据,我们将继续学习如何将数据保存到 JSON 文件中。我们将继续使用前一个示例并对其进行扩展。
如何做…
我们将通过以下步骤学习如何在 JSON 文件中保存数据:
- 在
mainwindow.ui中添加另一个按钮,然后将其对象名称设置为saveJsonButton,标签设置为保存 JSON:

图 10.4 – 添加保存 JSON 按钮
- 右键单击按钮并选择
clicked()选项,然后点击on_saveJsonButton_clicked()将自动添加到你的mainwindow.h和mainwindow.cpp文件中,由 Qt 完成:

图 10.5 – 选择 clicked() 信号并按 OK
-
将以下代码添加到
on_saveJsonButton_clicked()函数中:QQString filename = QFileDialog::getSaveFileName(this, "Save JSON", ".", "JSON files (*.json)"); QFile file(filename); if (!file.open(QFile::WriteOnly | QFile::Text)) qDebug() << "Error saving JSON file."; QJsonDocument json; QJsonArray array; -
让我们也编写第一个
contact元素:QJsonObject contact1; contact1["category"] = "Friend"; contact1["name"] = "John Doe"; contact1["age"] = 32; contact1["address"] = "114B, 2nd Floor, Sterling Apartment, Morrison Town"; contact1["phone"] = "0221743566"; array.append(contact1); -
按照以下方式编写第二个
contact元素:QJsonObject contact2; contact2["category"] = "Family"; contact2["name"] = "Jane Smith"; contact2["age"] = 24; contact2["address"] = "13, Ave Park, Alexandria"; contact2["phone"] = "0025728396"; array.append(contact2); -
最后,将数据保存到文本文件中:
json.setArray(array); file.write(json.toJson()); file.close(); } -
构建并运行程序,你应该在程序 UI 中看到额外的按钮:

图 10.6 – 你的应用程序现在应该看起来像这样
-
点击 保存 JSON 按钮,屏幕上会出现一个保存文件对话框。输入所需的文件名,然后点击 保存 按钮。
-
使用任何文本编辑器打开你刚刚保存的 JSON 文件。文件的前一部分应该看起来像这样:
[ { "address": "114B, 2nd Floor, Sterling Apartment, Morrison Town", "age": 32, "category": "Friend", "name": "John Doe", "phone": "0221743566" }, { "address": "13, Ave Park, Alexandria", "age": 24, "category": "Family", "name": "Jane Smith", "phone": "0025728396" } ]
它是如何工作的…
保存过程与上一个示例中加载 JSON 文件的过程类似。唯一的区别是,我们不是使用 QJsonDocument::fromJson() 函数,而是切换到使用 QJsonDocument::toJson() 函数。我们仍然使用了文件对话框和 QFile 类来保存 XML 文件。这次,在将字节数据传递给 QFile 类之前,我们必须将打开模式从 QFile::ReadOnly 更改为 QFile::WriteOnly。
接下来,我们开始编写 JSON 文件,通过创建 QJsonDocument 和 QJsonArray 变量,然后为每个联系人创建一个 QJsonObject 对象。然后,我们填写每个联系人的信息,并将其追加到 QJsonArray 数组中。
最后,我们使用 QJsonDocument::setArray() 将我们之前创建的 QJsonArray 数组应用于 JSON 数据的主数组,在将其作为字节数据使用 QJsonDocument::toJson() 函数传递给 QFile 类之前。
使用 Google 的 Geocoding API
在这个例子中,我们将学习如何使用 Google 的 Geocoding API 通过地址获取特定位置的完整地址。
如何做到这一点…
让我们按照以下步骤创建一个利用 Geocoding API 的程序:
-
创建一个新的 Qt Widgets 应用程序 项目。
-
打开
mainwindow.ui并添加几个文本标签、输入字段和一个按钮,使你的 UI 看起来类似于以下内容:

图 10.7 – 设置你的 UI
-
打开你的项目(
.pro)文件,并将网络模块添加到你的项目中。你可以通过在core和gui之后简单地添加单词network来做到这一点,如下面的代码所示:QT += core gui mainwindow.h and add the following headers to the source code:#include <QMainWindow>#include <QDebug>#include <QtNetwork/QNetworkAccessManager>#include <QtNetwork/QNetworkReply>#include <QJsonDocument>#include <QJsonArray>#include <QJsonObject> -
手动声明一个槽函数并调用它
getAddressFinished():private slots: void getAddressFinished(QNetworkReply* reply); -
声明一个名为
addressRequest的private变量:private: QNetworkAccessManager* addressRequest; -
再次打开
mainwindow.ui,在clicked()选项上右键单击,然后按mainwindow.h和mainwindow.cpp源文件。 -
打开
mainwindow.cpp,并将以下代码添加到类构造函数中:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); addressRequest = new QNetworkAccessManager(); connect(addressRequest, &QNetworkAccessManager::finished, this, &MainWindow::getAddressFinished); } -
将以下代码添加到我们刚刚手动声明的
getAddressFinished()槽函数中:void MainWindow::getAddressFinished(QNetworkReply* reply) { QByteArray bytes = reply->readAll(); //qDebug() << QString::fromUtf8(bytes.data(), bytes.size()); QJsonDocument json = QJsonDocument::fromJson(bytes); -
继续从 JSON 数组中获取第一组结果以获取格式化地址:
QJsonObject object = json.object(); QJsonArray results = object["results"].toArray(); QJsonObject first = results.at(0).toObject(); QString address = first["formatted_address"].toString(); qDebug() << "Address:" << address; } -
将以下代码添加到 Qt 创建的
clicked()槽函数中:void MainWindow::on_getAddressButton_clicked() { QString latitude = ui->latitude->text(); QString longitude = ui->longitude->text(); QNetworkRequest request; request.setUrl(QUrl("https://maps.googleapis.com/maps/api/geocode/json?latlng=" + latitude + "," + longitude + "&key=YOUR_KEY")); addressRequest->get(request); } -
编译并运行程序,你应该能够通过插入经度和纬度值并点击获取 地址按钮来获取地址:

图 10.8 – 插入坐标并点击获取地址按钮
-
让我们尝试使用经度为
-73.9780838和纬度为40.6712957。点击获取地址按钮,你将在应用程序输出窗口中看到以下结果:Address: "185 7th Ave, Brooklyn, NY 11215, USA"
它是如何工作的…
我不能确切地告诉你 Google 是如何从其后端系统中获取地址的,但我可以教你如何使用QNetworkRequest请求 Google 的数据。你需要做的只是将网络请求的 URL 设置为我在前面的源代码中使用的 URL,并将纬度和经度信息附加到 URL 上。
之后,我们所能做的就是等待来自 Google API 服务器的响应。在向 Google 发送请求时,我们需要指定 JSON 作为期望的格式;否则,它可能会以 JSON 格式返回结果。这可以通过在网络请求 URL 中添加json关键字来实现,如下所示:
request.setUrl(QUrl("https://maps.googleapis.com/maps/api/geocode/xml?keylatlng=" + latitude + "," + longitude + "&key=YOUR_CODE"));
当程序收到 Google 的响应时,getAddressFinished()槽函数将被调用,我们就能通过QNetworkReply获取 Google 发送的数据。
Google 通常以 JSON 格式回复一段长文本,其中包含我们不需要的大量数据。我们只需要 JSON 数据中存储在formatted_address元素中的文本。由于存在多个名为formatted_address的元素,我们只需找到第一个并忽略其余的。你也可以通过向 Google 提供一个地址,并从其网络响应中获取位置坐标来实现相反的操作。
更多内容…
Google 的地理编码 API是Google Maps APIs网络服务的一部分,为你的地图应用程序提供地理数据。除了地理编码 API 之外,你还可以使用他们的位置 API、地理位置 API和时区 API来实现你想要的结果。
注意
有关 Google Maps APIs 网络服务的更多信息,请访问developers.google.com/maps/web-services。
第十一章:转换库
在我们的计算机环境中保存的数据以各种方式编码。有时,它可以直接用于某个目的;其他时候,它需要转换为另一种格式,以便适应任务的上下文。将数据从一种格式转换为另一种格式的过程也取决于源格式以及目标格式。
有时,这个过程可能非常复杂,尤其是在处理功能丰富且敏感的数据时,如图像或视频转换。即使在转换过程中出现的小错误也可能使文件无法使用。
本章将涵盖以下食谱:
-
转换数据
-
转换图像
-
转换视频
-
转换货币
技术要求
本章的技术要求包括 Qt 6.6.1 MinGW-64 位和 Qt Creator 12.0.2。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter11。
转换数据
Qt 提供了一套类和函数,可以轻松地在不同类型的数据之间进行转换。这使得 Qt 不仅仅是一个 GUI 库;它是一个完整的软件开发平台。在以下示例中,我们将使用的 QVariant 类与 C++ 标准库提供的类似转换功能相比,使 Qt 更加灵活和强大。
如何做到这一点…
让我们按照以下步骤学习如何在 Qt 中转换各种数据类型:
- 打开 Qt Creator 并创建一个新的 Qt 控制台应用程序项目,方法是通过 文件 | 新建项目…:

图 11.1 – 创建 Qt 控制台应用程序项目
-
打开
main.cpp并向其中添加以下头文件:#include <QCoreApplication> #include <QDebug> #include <QtMath> #include <QDateTime> #include <QTextCodec> main() function, add the following code to convert a string into a number:int numberA = 2;
QString numberB = "5";
qDebug() << "1) " << "2 + 5 =" << numberA + numberB.toInt();
-
将数字转换回字符串:
float numberC = 10.25; float numberD = 2; QString result = QString::number(numberC * numberD); qDebug() << "2) " << "10.25 * 2 =" << result; -
让我们看看如何使用
qFloor()来向下舍入一个值:float numberE = 10.3; float numberF = qFloor(numberE); qDebug() << "3) " << "Floor of 10.3 is" << numberF; -
使用
qCeil(),我们可以将一个数字向下舍入到不小于其初始值的最小整数:float numberG = 10.3; float numberH = qCeil(numberG); qDebug() << "4) " << "Ceil of 10.3 is" << numberH; -
通过将字符串格式的日期时间数据转换来创建日期时间变量:
QString dateTimeAString = "2016-05-04 12:24:00"; QDateTime dateTimeA = QDateTime::fromString(dateTimeAString, "yyyy-MM-dd hh:mm:ss"); qDebug() << "5) " << dateTimeA; -
使用我们的自定义格式将日期时间变量转换回字符串:
QDateTime dateTimeB = QDateTime::currentDateTime(); QString dateTimeBString = dateTimeB.toString("dd/MM/yy hh:mm"); qDebug() << "6) " << dateTimeBString; -
调用
QString::toUpper()函数将字符串变量转换为大写字母:QString hello1 = "hello world!"; qDebug() << "7) " << hello1.toUpper(); -
调用
QString::toLower()将字符串完全转换为小写:QString hello2 = "HELLO WORLD!"; qDebug() << "8) " << hello2.toLower(); -
Qt 提供的
QVariant类是一个非常强大的数据类型,可以轻松地转换为其他类型,而无需程序员做任何努力:QVariant aNumber = QVariant(3.14159); double aResult = 12.5 * aNumber.toDouble(); qDebug() << "9) 12.5 * 3.14159 =" << aResult; -
这演示了单个
QVariant变量如何同时转换为多个数据类型,而无需程序员做任何努力:qDebug() << "10) "; QVariant myData = QVariant(10); qDebug() << myData; myData = myData.toFloat() / 2.135; qDebug() << myData; myData = true; qDebug() << myData; myData = QDateTime::currentDateTime(); qDebug() << myData; myData = "Good bye!"; qDebug() << myData; -
main.cpp中的完整源代码现在看起来是这样的:#include <QCoreApplication> #include <QDebug> #include <QtMath> #include <QDateTime> #include <QStringConverter> #include <iostream> int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); -
然后,让我们添加代码将字符串转换为数字,反之亦然:
// String to number int numberA = 2; QString numberB = "5"; qDebug() << "1) " << "2 + 5 =" << numberA + numberB.toInt(); // Number to string float numberC = 10.25; float numberD = 2; QString result = QString::number(numberC * numberD); qDebug() << "2) " << "10.25 * 2 =" << result; -
编写代码将浮点数转换为最接近的整数,分别向上取整或向下取整:
// Floor float numberE = 10.3; float numberF = qFloor(numberE); qDebug() << "3) " << "Floor of 10.3 is" << numberF; // Ceil float numberG = 10.3; float numberH = qCeil(numberG); qDebug() << "4) " << "Ceil of 10.3 is" << numberH; -
将字符串转换为日期时间格式,反之亦然:
// Date time from string QString dateTimeAString = "2016-05-04 12:24:00"; QDateTime dateTimeA = QDateTime::fromString(dateTimeAString, "yyyy-MM-dd hh:mm:ss"); qDebug() << "5) " << dateTimeA; // Date time to string QDateTime dateTimeB = QDateTime::currentDateTime(); QString dateTimeBString = dateTimeB.toString("dd/MM/yy hh:mm"); qDebug() << "6) " << dateTimeBString; -
继续添加代码将字符串转换为大写或小写字符:
// String to all uppercase QString hello1 = "hello world!"; qDebug() << "7) " << hello1.toUpper(); // String to all lowercase QString hello2 = "HELLO WORLD!"; qDebug() << "8) " << hello2.toLower(); -
将
QVariant数据类型转换为其他类型:// QVariant to double QVariant aNumber = QVariant(3.14159); double aResult = 12.5 * aNumber.toDouble(); qDebug() << "9) 12.5 * 3.14159 =" << aResult; // QVariant different types qDebug() << "10) "; QVariant myData = QVariant(10); qDebug() << myData; myData = myData.toFloat() / 2.135; qDebug() << myData; myData = true; qDebug() << myData; -
将
QVariant数据类型转换为QDateTime和QString:myData = QDateTime::currentDateTime(); qDebug() << myData; myData = "Good bye!"; qDebug() << myData; return a.exec(); } -
编译并运行项目,你应该会看到类似这样的结果:

图 11.2 – 在应用程序输出窗口中打印转换结果
它是如何工作的…
Qt 提供的所有数据类型,如 QString、QDateTime 和 QVariant,都包含使转换到其他类型变得简单直接的函数。Qt 还提供了自己的对象转换函数 qobject_cast(),它不依赖于标准库。它也与 Qt 更为兼容,并且可以很好地在 Qt 的控件类型和数据类型之间进行转换。
Qt 还为你提供了 QtMath 类,它可以帮助你操作数字变量,例如向上取整一个浮点数或将角度从度转换为弧度。QVariant 是一个特殊类,可以用来存储各种类型的数据,例如 int、float、char 和 string。它可以通过检查变量中存储的值来自动确定数据类型。你还可以通过调用单个函数,如 toFloat()、toInt()、toBool()、toChar() 或 toString(),轻松地将数据转换为 QVariant 类支持的任何类型。
还有更多…
请注意,这些转换都需要计算能力。尽管现代计算机在处理这些操作方面非常快,但你应该小心不要一次性处理大量数据。如果你正在为复杂计算转换大量变量,这可能会显著减慢你的计算机速度,因此请尽量仅在必要时转换变量。
转换图像
在本节中,我们将学习如何构建一个简单的图像转换器,它可以将图像从一种格式转换为另一种格式。Qt 支持读取和写入不同类型的图像格式,由于许可问题,这种支持以外部 DLL 文件的形式提供。
然而,你不必担心这一点,因为只要你在项目中包含那些 DLL 文件,它就可以在不同格式之间无缝工作。某些格式只支持读取而不支持写入,而某些格式则两者都支持。
注意
你可以在 doc.qt.io/qt-6/qtimageformats-index.html 查看有关转换图像的完整详细信息。
如何做到这一点…
Qt 的内置图像库使得图像转换变得非常简单:
-
打开 Qt Creator 并创建一个新的 Qt Widgets 应用程序 项目。
-
打开
mainwindow.ui并在画布上添加一个用于选择图像文件的文本框和一个按钮,一个用于选择所需文件格式的组合框,以及另一个用于启动转换过程的按钮:

图 11.3 – 按照此处所示布局 UI
- 双击组合框,然后会出现一个窗口,您可以在其中编辑框。我们将通过点击
PNG、JPEG和BMP来向组合框列表中添加三项:

图 11.4 – 向组合框添加三个选项
- 右键单击一个按钮,选择转到槽…,然后点击确定按钮。将自动为您添加一个槽函数。对其他按钮也重复此步骤:

图 11.5 – 选择 clicked() 信号并点击确定
-
让我们转到源代码。打开
mainwindow.h并添加以下头文件:#include <QMainWindow> #include <QFileDialog> #include <QMessageBox> mainwindow.cpp and define what will happen when the Browse button is clicked, which in this case is opening the file dialog to select an image file:void MainWindow::on_browseButton_clicked() {
QString fileName = QFileDialog::getOpenFileName(this, "Open Image", "", "Image Files (*.png *.jpg *.bmp)");
ui->filePath->setText(fileName);
}
-
定义当转换按钮被点击时会发生什么:
void MainWindow::on_convertButton_clicked() { QString fileName = ui->filePath->text(); if (fileName != "") { QFileInfo fileInfo = QFileInfo(fileName); QString newFileName = fileInfo.path() + "/" + fileInfo.completeBaseName(); QImage image = QImage(ui->filePath->text()); if (!image.isNull()) { -
检查使用的格式:
// 0 = PNG, 1 = JPG, 2 = BMP int format = ui->fileFormat->currentIndex(); if (format == 0) { newFileName += ".png"; } else if (format == 1) { newFileName += ".jpg"; } else if (format == 2) { newFileName += ".bmp"; } -
检查图像是否已转换:
qDebug() << newFileName << format; if (image.save(newFileName, 0, -1)) { QMessageBox::information(this, "Success", "Image successfully converted."); } else { QMessageBox::warning(this, "Failed", "Failed to convert image."); } } -
显示消息框:
else { QMessageBox::warning(this, "Failed", "Failed to open image file."); } } else { QMessageBox::warning(this, "Failed", "No file is selected."); } } -
现在构建并运行程序,我们应该得到一个看起来像这样的简单图像转换器:

图 11.6 – 浏览图像,选择格式,然后点击转换按钮
它是如何工作的…
之前的例子使用了 Qt 的原生QImage类,它包含可以访问像素数据并操作它的函数。它也被用来通过不同的解压缩方法加载图像文件并提取其数据,具体取决于图像的格式。
一旦数据被提取,您就可以对它做任何您想做的事情,例如在屏幕上显示图像,操作其颜色信息,调整图像大小,或者用另一种格式压缩它并保存为文件。
我们使用了QFileInfo来将文件名与扩展名分开,这样我们就可以使用用户从组合框中选择的新的格式来修改扩展名。这样,我们就可以将新转换的图像保存到与原始图像相同的文件夹中,并且自动给它相同的文件名,除了不同的格式。
只要您尝试将图像转换为 Qt 支持的格式,您只需调用 QImage::save()。内部,Qt 会为您处理其余部分并将图像输出到所选格式。在 QImage::save() 函数中,有一个参数用于设置图像质量,另一个参数用于设置格式。在这个例子中,我们只是将两者都设置为默认值,这样图像就会以最高质量保存,并且 Qt 会根据输出文件名中声明的扩展名来确定格式。
还有更多...
您还可以使用 Qt 提供的 QPdfWriter 类将图像转换为 PDF。本质上,您将选定的图像绘制到新创建的 PDF 文档的布局中,并相应地设置其分辨率。
注意
想了解更多关于 QPdfWriter 类的信息,请访问 doc.qt.io/qt-6/qpdfwriter.html。
转换视频
在这个菜谱中,我们将使用 Qt 和 Qt 提供的 QProcess 类创建一个简单的视频转换器。
如何做到这一点...
让我们按照以下步骤制作一个简单的视频转换器:
-
从
ffmpeg.zeranoe.com/builds下载FFmpeg(一个静态包)并将其内容解压到您喜欢的位置 – 例如,C:/FFmpeg/。 -
打开 Qt Creator 并创建一个新的 Qt Widgets 应用程序 项目,方法是转到 文件 | 新建项目...。
-
打开
mainwindow.ui– 我们将处理程序的用户界面。它的 UI 与之前的例子非常相似,只是我们在组合框下方添加了一个额外的文本编辑小部件:

图 11.7 – 按照这样设计你的视频转换器 UI
- 双击组合框,然后会出现一个窗口来编辑该框。我们将通过点击
AVI、MP4和MOV添加三个项目到组合框列表中:

图 11.8 – 向组合框添加三个视频格式
-
右键单击其中一个按钮,选择 转到槽...,然后点击 确定 按钮。然后会自动将槽函数添加到您的源文件中。对其他按钮重复此步骤。
-
打开
mainwindow.h并将以下头文件添加到顶部:#include <QMainWindow> #include <QFileDialog> #include <QProcess> #include <QMessageBox> #include <QScrollBar> public keyword:public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
QProcess* process;
QString outputText;
QString fileName;
QString outputFileName;
-
在之前 Qt 为我们创建的两个函数(转换 图像 菜单)下添加三个额外的槽函数:
private slots: void on_browseButton_clicked(); void on_convertButton_clicked(); void processStarted(); void readyReadStandardOutput(); void processFinished(); -
打开
mainwindow.cpp并在类构造函数中添加以下代码:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); process = new QProcess(this); connect(process, QProcess::started, this, MainWindow::processStarted); connect(process, QProcess::readyReadStandardOutput, this, MainWindow::readyReadStandardOutput); connect(process, QProcess::finished, this, MainWindow::processFinished); } -
定义当 浏览 按钮被点击时会发生什么,在这种情况下是打开文件对话框以允许我们选择视频文件:
void MainWindow::on_browseButton_clicked() { QString fileName = QFileDialog::getOpenFileName(this, "Open Video", "", "Video Files (*.avi *.mp4 *.mov)"); ui->filePath->setText(fileName); } -
定义当
FFmpeg(它将随后处理转换过程)发生时会发生什么:void MainWindow::on_convertButton_clicked() { QString ffmpeg = "C:/FFmpeg/bin/ffmpeg"; QStringList arguments; fileName = ui->filePath->text(); if (fileName != "") { QFileInfo fileInfo = QFileInfo(fileName); outputFileName = fileInfo.path() + "/" + fileInfo.completeBaseName(); -
检查文件的格式 – 特别是它是否为
.avi、.mp4或.mov:if (QFile::exists(fileName)) { int format = ui->fileFormat->currentIndex(); if (format == 0) { outputFileName += ".avi"; // AVI } else if (format == 1) { outputFileName += ".mp4"; // MP4 } else if (format == 2) { outputFileName += ".mov"; // MOV } -
使用以下代码开始转换:
qDebug() << outputFileName << format; arguments << "-i" << fileName << outputFileName; qDebug() << arguments; process->setProcessChannelMode(QProcess::MergedChannels); process->start(ffmpeg, arguments); } -
显示消息框:
else { QMessageBox::warning(this, "Failed", "Failed to open video file."); } } else { QMessageBox::warning(this, "Failed", "No file is selected."); } } -
当转换过程开始时,告诉程序要做什么:
void MainWindow::processStarted() { qDebug() << "Process started."; ui->browseButton->setEnabled(false); ui->fileFormat->setEditable(false); ui->convertButton->setEnabled(false); } -
编写在转换过程中
FFmpeg向程序返回输出时被调用的槽函数:void MainWindow::readyReadStandardOutput() { outputText += process->readAllStandardOutput(); ui->outputDisplay->setText(outputText); ui->outputDisplay->verticalScrollBar()->setSliderPosition(ui->outputDisplay->verticalScrollBar()->maximum()); } -
定义在转换过程完成后被调用的槽函数:
void MainWindow::processFinished() { qDebug() << "Process finished."; if (QFile::exists(outputFileName)) { QMessageBox::information(this, "Success", "Video successfully converted."); } else { QMessageBox::information(this, "Failed", "Failed to convert video."); } ui->browseButton->setEnabled(true); ui->fileFormat->setEditable(true); ui->convertButton->setEnabled(true); } -
构建并运行项目,你应该得到一个简单但实用的视频转换器:

图 11.9 – 由 FFmpeg 和 Qt 驱动的您的视频转换器
它是如何工作的…
Qt 提供的QProcess类用于启动外部程序并与它们通信。在本例中,我们将位于C:/FFmpeg/bin/的ffmpeg.exe作为一个进程启动,并开始与之通信。我们还向它发送了一组参数,告诉它在启动时应该做什么。在这个例子中,我们使用的参数相对基础——我们只告诉FFmpeg源图像的路径和输出文件名。
注意
想了解更多关于FFmpeg中可用参数设置的信息,请查看www.ffmpeg.org/ffmpeg.html。
FFmpeg不仅能转换视频文件,还可以用来转换音频文件和图片。
注意
想了解更多关于FFmpeg支持的所有格式的信息,请查看www.ffmpeg.org/general.html#File-Formats。
此外,您还可以通过运行位于C:/FFmpeg/bin的ffplay.exe播放视频或音频文件,或者通过运行ffprobe.exe以人类可读的方式打印视频或音频文件的信息。
注意
在www.ffmpeg.org/about.html查看FFmpeg的完整文档。
更多内容…
使用这种方法,您可以做很多事情。您不仅限于 Qt 提供的内容,而且可以通过仔细选择提供您所需功能的第三方程序来突破这些限制。一个这样的例子是利用市场上仅提供命令行扫描程序的杀毒软件,如Avira ScanCL、Panda Antivirus Command Line Scanner、SAV32CLI和ClamAV。您可以使用 Qt 构建自己的 GUI,并基本上向杀毒进程发送命令,告诉它要做什么。
货币转换
在本例中,我们将学习如何使用 Qt 创建一个简单的货币转换器,借助名为Fixer.io的外部服务提供商。
如何做到这一点…
按照以下简单步骤制作自己的货币转换器:
-
打开 Qt Creator,从文件 | 新建项目...创建一个新的Qt Widgets 应用程序项目。
-
打开项目文件(
.pro),将网络模块添加到我们的项目中:QT += core gui mainwindow.ui and remove the menu bar, toolbar, and status bar from the UI. -
向画布添加三个水平布局、一条水平线和一个推按钮。在画布上左键单击,然后通过点击
Convert继续操作。UI 应该看起来像这样:

图 11.10 – 在转换按钮上方放置三个垂直布局
- 在顶部布局中添加两个标签,并将左侧标签的文本设置为
From:,右侧标签的文本设置为To:。添加两个1:

图 11.11 – 向布局中添加标签和行编辑小部件
- 选择右侧的行编辑,并在 属性 面板中启用 只读 复选框:

图 11.12 – 为第二个行编辑启用只读属性
- 将光标属性设置为 禁止,以便用户知道在鼠标悬停在控件上时它不可编辑:

图 11.13 – 显示禁止光标以让用户知道它已被禁用
- 在底部布局的第三个布局中添加两个组合框。我们现在将它们留空:

图 11.14 – 向最终布局添加两个组合框
-
右键单击
clicked()信号作为选择,并点击mainwindow.h和mainwindow.cpp。 -
打开
mainwindow.h并确保以下头文件被添加到源文件顶部:#include <QMainWindow> #include <QDoubleValidator> #include <QNetworkAccessManager> #include <QNetworkRequest> #include <QNetworkReply> #include <QJsonDocument> #include <QJsonObject> #include <QDebug> finished():私有槽位:
void on_convertButton_clicked();
void finished(QNetworkReply* reply);
-
在
private标签下添加两个变量:private: Ui::MainWindow *ui; QNetworkAccessManager* manager; QString targetCurrency; -
打开
mainwindow.cpp文件。在类构造函数中向两个组合框添加几个货币简码。将验证器设置到finished()信号到我们的finished()槽函数:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); QStringList currencies; currencies.push_back("EUR"); currencies.push_back("USD"); currencies.push_back("CAD"); currencies.push_back("MYR"); currencies.push_back("GBP"); -
我们从前面的代码继续,并将货币简写形式插入到组合框中。然后,我们声明一个新的网络访问管理器,并将其 finished 信号连接到我们的自定义槽函数:
ui->currencyFrom->insertItems(0, currencies); ui->currencyTo->insertItems(0, currencies); QValidator *inputRange = new QDoubleValidator(this); ui->amountFrom->setValidator(inputRange); manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, &MainWindow::finished); } -
定义当用户点击 转换 按钮时会发生什么:
void MainWindow::on_convertButton_clicked() { if (ui->amountFrom->text() != "") { ui->convertButton->setEnabled(false); QString from = ui->currencyFrom->currentText(); QString to = ui->currencyTo->currentText(); targetCurrency = to; QString url = "http://data.fixer.io/api/latest?base=" + from + "&symbols=" + to + "&access_key=YOUR_KEY"; -
通过调用
get()来启动请求:QNetworkRequest request = QNetworkRequest(QUrl(url)); manager->get(request); } else { QMessageBox::warning(this, "Error", "Please insert a value."); } } -
定义当
finished()信号被触发时会发生什么:void MainWindow::finished(QNetworkReply* reply) { QByteArray response = reply->readAll(); qDebug() << response; QJsonDocument jsonResponse = QJsonDocument::fromJson(response); QJsonObject jsonObj = jsonResponse.object(); QJsonObject jsonObj2 = jsonObj.value("rates").toObject(); double rate = jsonObj2.value(targetCurrency).toDouble(); -
继续编写前面的代码,如下面的代码片段所示:
if (rate == 0) rate = 1; double amount = ui->amountFrom->text().toDouble(); double result = amount * rate; ui->amountTo->setText(QString::number(result)); ui->convertButton->setEnabled(true); } -
编译并运行项目,然后你应该得到一个看起来像这样的简单货币转换器:

图 11.15 – 一个可用的货币转换器已完成
它是如何工作的…
与我们之前看到的示例类似,该示例使用外部程序来完成特定任务,这次我们使用了一个外部服务提供商,它为我们提供了一个对所有用户免费且易于使用的 应用程序编程接口 (API)。
这样,我们就不必考虑获取最新货币汇率的方法。相反,服务提供商已经为我们完成了这项工作;我们只需礼貌地请求它。然后,我们等待从他们的服务器返回的响应,并根据我们的目的处理数据。
除了 Fixer.io (fixer.io)之外,您还可以选择相当多的不同服务提供商。有些是免费的,但没有任何高级功能;有些提供您以高端价格。这些替代方案中的一些是Open Exchange Rates (openexchangerates.org)、currencylayer API (currencylayer.com)、Currency API (currency-api.appspot.com)、XE Currency Data API (www.xe.com/xecurrencydata)和jsonrates (jsonrates.com)。
在之前的代码中,您应该已经注意到一个访问密钥被传递给了 Fixer.io API,这是我为此教程注册的一个免费访问密钥。如果您将其用于自己的项目,您应该在 Fixer.io 上创建一个账户。
还有更多...
除了货币汇率,您还可以使用这种方法执行更高级的任务,这些任务可能太复杂而无法自行完成,或者除非您使用专家提供的服务,否则根本无法访问,例如可编程的短信服务(SMS)和语音服务、网站分析和统计数据生成,以及在线支付网关。大多数这些服务都不是免费的,但您可以在几分钟内轻松实现这些功能,甚至无需设置服务器基础设施和后端系统;这绝对是快速且成本最低的方式,让您的产品快速运行而无需太多麻烦。
第十二章:使用 SQL 驱动器和 Qt 访问数据库
结构化查询语言(SQL)是一种特殊的编程语言,用于管理关系数据库管理系统中的数据。SQL 服务器是一个数据库系统,旨在使用多种 SQL 编程语言之一来管理其数据。
在本章中,我们将介绍以下食谱:
-
设置数据库
-
连接到数据库
-
编写基本的 SQL 查询
-
使用 Qt 创建登录界面
-
在模型视图中显示数据库信息
-
高级 SQL 查询
技术要求
本章的技术要求包括 Qt 6.6.1 MinGW 64 位和 Qt Creator 12.0.2。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter12。
设置数据库
Qt 支持多种不同类型的 SQL 驱动程序,以插件/附加组件的形式存在,例如SQLite、ODBC、PostgreSQL、MySQL等。然而,将这些驱动程序集成到 Qt 项目中非常容易。我们将在下面的示例中学习如何做到这一点。
如何操作…
在本例中,我们将学习如何使用 Qt 与SQLite。在我们深入 Qt 之前,让我们设置我们的 SQLite 编辑器:
- 从
sqlitestudio.pl下载SQLiteStudio并安装它以管理您的 SQLite 数据库:


- 打开SQLiteStudio,您应该看到类似以下内容:


- 在我们开始之前,我们需要创建一个新的数据库;转到数据库 | 添加数据库。选择您的数据库类型为SQLite 3,然后选择您的文件名并设置数据库名称。然后,点击测试连接按钮。您应该在按钮旁边看到一个绿色的勾号。之后,点击确定按钮:

图 12.3 – 创建新的 SQLite 3 数据库
- 数据库创建完成后,您应该在数据库窗口中看到数据库出现。然后,右键单击表,从弹出的菜单中选择创建表选项:

图 12.4 – 从菜单中选择创建表选项
- 将表名设置为
employee。然后,点击位于表名输入字段上方的添加列(lns)按钮。此时,列窗口将弹出:

图 12.5 – 创建一个名为 emp_id 的新列
- 将列名设置为
emp_id,将数据类型设置为整数,并勾选主键复选框。然后,点击主键复选框右侧的配置按钮。现在将弹出编辑约束窗口。勾选自动递增复选框并点击应用:

图 12.6 – 启用自动递增复选框
- 之后,按
emp_id。让我们重复上述步骤(不启用主键)来创建其他列。您可以遵循此处看到的相同设置:

图 12.7 – 创建所有五个列
- 列实际上在此点并未创建。点击位于表名上方的带绿色勾选图标按钮。将弹出一个窗口以确认列的创建。按确定继续:

图 12.8 – 点击“确定”按钮进行确认
- 现在,我们已经创建了
employee表。让我们从当前为空的employee表继续。通过点击带绿色加号图标的插入行(Ins)按钮将虚拟数据插入到employee表中。然后,简单地插入一些虚拟数据如下:

图 12.9 – 将虚拟数据插入到员工表中
-
让我们为我们的 Qt 项目设置 SQL 驱动程序。只需转到您的 Qt 安装文件夹,并查找
sqldrivers文件夹。例如,我的位于C:\Qt\6.4.2\mingw_64\plugins\sqldrivers。 -
将整个
sqldrivers文件夹复制到您的项目构建目录中。您可以删除与您运行的 SQL 服务器不相关的 DLL 文件。在我们的例子中,因为我们使用qsqlite.dll。 -
上一步中提到的 DLL 文件是使 Qt 能够与不同类型的 SQL 架构通信的驱动程序。您可能还需要 SQL 客户端库的 DLL 文件,以便驱动程序能够工作。在我们的例子中,我们需要
sqlite3.dll位于我们的程序可执行文件相同的目录中。您可以从SQLiteStudio的安装目录或SQLite的官方网站www.sqlite.org/download.html获取它。
它是如何工作的…
Qt 为我们提供了 SQL 驱动程序,这样我们就可以轻松地连接到不同类型的 SQL 服务器,而无需自己实现它们。
目前,Qt 正式支持 SQLite、ODBC 和 PostgreSQL。如果你需要直接连接到 MySQL,你需要自己重新编译 Qt 驱动程序,这超出了本书的范围。出于安全原因,不建议你直接从你的应用程序连接到 MySQL。相反,你的应用程序应该通过使用 QNetworkAccessManager 发送 HTTP 请求到你的后端脚本(如 PHP、ASP 和 JSP)来间接与 MySQL 数据库(或任何其他 Qt 未官方支持的 SQL 服务器)交互,然后该脚本可以与数据库通信。
如果你只需要一个简单的基于文件的数据库,并且不打算使用基于服务器的数据库,SQLite 是你的一个不错的选择,这也是我们本章所选择的方法。
在 连接到数据库 菜谱中,我们将学习如何使用 Qt 的 SQL 模块连接到我们的 SQL 数据库。
连接到数据库
在这个菜谱中,我们将学习如何将我们的 Qt 6 应用程序连接到 SQL 服务器。
如何做到这一点…
在 Qt 中连接到 SQL 服务器非常简单:
-
打开 Qt Creator 并创建一个新的 Qt Widgets 应用程序项目。
-
打开你的项目文件(
.pro),将sql模块添加到你的项目中,并像这样运行qmake:QT += core gui mainwindow.ui and drag seven label widgets, a combo box, and a checkbox to the canvas. Set the text properties of four of the labels to Name:, Age:, Gender:, and Married:. Then, set the objectName properties of the rest to name, age, gender, and married. There is no need to set the object name for the previous four labels because they’re for display purposes only:

图 12.10 – 设置文本属性
-
打开
mainwindow.h并在QMainWindow头部下方添加以下头文件:#include <QMainWindow> #include <QtSql> #include <QSqlDatabase> #include <QSqlQuery> mainwindow.cpp and insert the following code into the class constructor:MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent), ui(new Ui::MainWindow) {
ui->setupUi(this);
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("database.db3");
-
在数据库连接打开后开始 SQL 查询:
if (db.open()) { QSqlQuery query; if (query.exec("SELECT emp_name, emp_age, emp_gender, emp_married FROM employee")) { while (query.next()) { qDebug() << query.value(0) << query.value(1) << query.value(2) << query.value(3); ui->name->setText(query.value(0).toString()); ui->age->setText(query.value(1).toString()); ui->gender->setCurrentIndex(query.value(2).toInt()); ui->married->setChecked(query.value(3).toBool()); } } -
打印出任何错误文本:
else { qDebug() << query.lastError().text(); } db.close(); } else { qDebug() << "Failed to connect to database."; } } -
如果你现在编译并运行你的项目,你应该会得到类似这样的结果:

图 12.11 – 我们数据库的数据现在显示在 Qt 程序中
它是如何工作的…
之前的例子展示了如何使用从 SQL 模块派生的 QSqlDatabase 类连接到你的 SQL 数据库。如果不将模块添加到你的 Qt 项目中,你将无法访问任何与 SQL 相关的类。
我们必须在调用 addDatabase() 函数时提到我们正在运行的 SQL 架构,Qt 支持的选项包括 QSqlDatabase: QMYSQL driver not loaded,你应该检查 DLL 文件是否放置在正确的目录中。
我们可以通过 QSqlQuery 类将我们的 SQL 语句发送到数据库,并等待它返回结果,这些结果通常是请求的数据或由于无效语句而产生的错误信息。如果有任何数据来自数据库服务器,它们都将存储在 QSqlQuery 类中。你只需要在 QSqlQuery 类上执行一个“while”循环来检查所有现有记录并通过调用 value() 函数来检索它们。
由于我们在前面的示例中使用了 SQLite,因此连接到数据库时我们不需要设置服务器主机、用户名和密码。SQLite 是一个基于文件的 SQL 数据库;因此,我们只需要在调用 QSqlDatabase::setDatabaseName() 时设置文件名。
重要提示
Qt 6 不再官方支持 QMYSQL 或 QMYSQL3。你可以通过从源代码重新编译 Qt 来添加 MySQL 支持。然而,这种方法不建议初学者使用。更多信息,请查看 doc.qt.io/qt-6/sql-driver.html#compile-qt-with-a-specific-driver。
编写基本的 SQL 查询
在上一个示例中,我们编写了我们非常第一个 SQL 查询,它涉及 SELECT 语句。这次,我们将学习如何使用一些其他的 SQL 语句,例如 INSERT、UPDATE 和 DELETE。
如何做到这一点…
让我们按照以下步骤创建一个简单的程序,通过它演示基本的 SQL 查询命令:
- 我们可以使用我们之前的项目文件,但有一些事情我们需要更改。打开
mainwindow.ui并替换UPDATE、INSERT和DELETE的标签:

图 12.12 – 将 UI 修改为这样
-
打开
mainwindow.h并在私有继承下添加以下变量:private: Ui::MainWindow *ui; QSqlDatabase db; bool connected; mainwindow.cpp and go to the class constructor. It is still pretty much the same as the previous example, except we store the database connection status in a Boolean variable called connected, and we also obtain the ID of the data from the database and store it in an integer variable called currentID:MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent), ui(new Ui::MainWindow) {
ui->setupUi(this);
db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("database.db3");
connected = db.open();
-
让我们在连接到数据库后执行一个查询:
if (connected) { QSqlQuery query; if (query.exec("SELECT emp_id, emp_name, emp_age, emp_gender, emp_married FROM employee")) { while (query.next()) { currentID = query.value(0).toInt(); ui->name->setText(query.value(1).toString()); ui->age->setText(query.value(2).toString()); ui->gender->setCurrentIndex(query.value(3).toInt()); ui->married->setChecked(query.value(4).toBool()); } } -
打印出任何错误信息:
else { qDebug() << query.lastError().text(); } } else { qDebug() << "Failed to connect to database."; } } -
前往
mainwindow.ui并在步骤 1 中添加到画布上的一个按钮上右键单击。选择mainwindow.h和mainwindow.cpp:private slots: void on_updateButton_clicked(); void on_insertButton_clicked(); void on_deleteButton_clicked(); -
打开
mainwindow.cpp并声明当我们点击 更新 按钮时程序将执行的操作:void MainWindow::on_updateButton_clicked() { if (connected) { if (currentID == 0) { qDebug() << "Nothing to update."; } else { QString id = QString::number(currentID); QString name = ui->name->text(); QString age = ui->age->text(); QString gender = QString::number(ui->gender->currentIndex()); QString married = QString::number(ui->married->isChecked()); -
创建一个类似的
UPDATE查询:qDebug() << "UPDATE employee SET emp_name = '" + name + "', emp_age = '" + age + "', emp_gender = " + gender + ", emp_married = " + married + " WHERE emp_id = " + id; QSqlQuery query; if (query.exec("UPDATE employee SET emp_name = '" + name + "', emp_age = '" + age + "', emp_gender = " + gender + ", emp_married = " + married + " WHERE emp_id = " + id)) { qDebug() << "Update success."; } -
如果有的话,打印出最后的错误信息:
else { qDebug() << query.lastError().text(); } } } else { qDebug() << "Failed to connect to database."; } } -
声明当点击 INSERT 按钮时将发生什么:
void MainWindow::on_insertButton_clicked() { if (connected) { QString name = ui->name->text(); QString age = ui->age->text(); QString gender = QString::number(ui->gender->currentIndex()); QString married = QString::number(ui->married->isChecked()); qDebug() << "INSERT INTO employee (emp_name, emp_age, emp_gender, emp_married) VALUES ('" + name + "','" + age + "', " + gender + "," + married + ")"; -
创建一个类似的
INSERT查询:QSqlQuery query; if (query.exec("INSERT INTO employee (emp_name, emp_age, emp_gender, emp_married) VALUES ('" + name + "','" + age + "', " + gender + "," + married + ")")) { currentID = query.lastInsertId().toInt(); qDebug() << "Insert success."; } else { qDebug() << query.lastError().text(); } } else { qDebug() << "Failed to connect to database."; } } -
声明当点击 Delete 按钮时将发生什么:
void MainWindow::on_deleteButton_clicked() { if (connected) { if (currentID == 0) { qDebug() << "Nothing to delete."; } else { QString id = QString::number(currentID); qDebug() << "DELETE FROM employee WHERE emp_id = " + id; QSqlQuery query; if (query.exec("DELETE FROM employee WHERE emp_id = " + id)) { currentID = 0; qDebug() << "Delete success."; } else { qDebug() << query.lastError().text(); } } } else { qDebug() << "Failed to connect to database."; } } -
在类析构函数中调用
QSqlDatabase::close()以在退出程序之前正确终止 SQL 连接:MainWindow::~MainWindow() { db.close(); delete ui; } -
如果你现在编译并运行程序,你应该能够从数据库中选择默认数据。然后,你可以选择更新它或从数据库中删除它。你也可以通过点击插入按钮将新数据插入到数据库中。你可以使用 SQLiteStudio 来检查数据是否被正确修改:

图 12.13 – SQLite 中数据成功修改
它是如何工作的…
在我们向数据库发送 SQL 查询之前,检查数据库是否连接是非常重要的。因此,我们将该状态保存在一个变量中,并在发送任何查询之前使用它进行检查。然而,对于长时间保持打开的复杂程序,这并不推荐,因为在这些期间数据库可能会断开连接,而固定的变量可能不准确。在这种情况下,最好通过调用 QSqlDatabase::isOpen() 来检查实际状态。
currentID 变量用于保存从数据库中获取的当前数据的 ID。当你想要更新数据或从数据库中删除它们时,这个变量对于让数据库知道你正在尝试更新或删除什么数据至关重要。如果你正确设置了数据库表,SQLite 将将每条数据视为一个唯一的条目,因此你可以确信在保存新数据时数据库中不会产生重复的 ID。
在将新数据插入数据库后,我们调用 QSqlQuery::lastInsertId() 来获取新数据的 ID 并将其保存为 currentID 变量,以便它成为我们可以更新或从数据库中删除的当前数据。在将 SQL 查询用于 Qt 之前在 SQLiteStudio 上测试你的 SQL 查询是一个好习惯。你可以立即发现你的 SQL 语句是否正确,而不是等待你的项目构建,尝试它,然后重新构建它。作为程序员,我们必须以最有效的方式工作。
努力工作,聪明工作。
使用 Qt 创建登录界面
在本教程中,我们将学习如何将我们的知识付诸实践,并使用 Qt 和 SQLite 创建一个功能性的登录界面。
如何做到这一点...
按照以下步骤创建你的第一个功能性的登录界面:
- 打开一个网络浏览器并转到
user,看起来像这样:

图 12.14 – 创建新的用户表
- 让我们将第一条数据插入到新创建的表中,并将
user_employeeID设置为现有员工的 ID。这样,我们创建的用户账户将与其中一位员工的资料相链接:

图 12.15 – user_employeeID 列与员工的 emp_id 列相链接
- 打开
mainwindow.ui。在画布上放置一个堆叠小部件,并确保它包含两页。然后,按照以下方式设置堆叠小部件中的两页:

图 12.16 – 在堆叠小部件内创建两页 UI
- 在堆叠小部件的第一页上,点击小部件顶部的 编辑标签顺序 图标,这样你就可以调整程序中小部件的顺序:

图 12.17 – 通过按此按钮更改小部件的顺序
- 一旦你点击了编辑标签顺序图标,你会在画布中每个小部件的上方看到一些数字。确保这些数字与下面的截图中的数字相同。如果不是,请点击数字以更改它们的顺序。我们只为堆叠小部件的第一页做这件事;第二页保持原样即可:

图 12.18 – 每个小部件的顺序显示
-
右键单击clicked()选项并按确定。Qt 将随后在你的项目源文件中为你创建一个槽函数。同样,为注销按钮重复此步骤。
-
打开
mainwindow.h并在#include <QMainWindow>行之后添加以下头文件:#include <QMainWindow> #include <QtSql> #include <QSqlDatabase> #include <QSqlQuery> #include <QMessageBox> mainwindow.h:private:
Ui::MainWindow *ui;
QSqlDatabase db;
-
打开
mainwindow.cpp并在类构造函数中放入以下代码:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); ui->stackedWidget->setCurrentIndex(0); db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("database.db3"); if (!db.open()) { qDebug() << "Failed to connect to database."; } } -
定义如果点击登录按钮会发生什么:
void MainWindow::on_loginButton_clicked() { QString username = ui->username->text(); QString password = ui->password->text(); QSqlQuery query; if (query.exec("SELECT user_employeeID from user WHERE user_username = '" + username + "' AND user_password = '" + password + "'")) { int resultSize = 0; while (query.next()) { QString employeeID = query.value(0).toString(); QSqlQuery query2; -
编写一个 SQL 查询:
if (query2.exec("SELECT emp_name, emp_age, emp_gender, emp_married FROM employee WHERE emp_id = " + employeeID)) { while (query2.next()) { QString name = query2.value(0).toString(); QString age = query2.value(1).toString(); int gender = query2.value(2).toInt(); bool married = query2.value(3).toBool(); ui->name->setText(name); ui->age->setText(age); -
我们继续使用前面的代码,并在切换到堆叠小部件的第二页之前设置性别和已婚文本:
if (gender == 0) ui->gender->setText("Male"); else ui->gender->setText("Female"); if (married) ui->married->setText("Yes"); else ui->married->setText("No"); ui->stackedWidget->setCurrentIndex(1); } } resultSize++; } -
如果登录失败,打印错误信息:
if (resultSize == 0) { QMessageBox::warning(this, "Login failed", "Invalid username or password."); } } else { qDebug() << query.lastError().text(); } } -
定义如果点击注销按钮会发生什么:
void MainWindow::on_logoutButton_clicked() { ui->stackedWidget->setCurrentIndex(0); } -
当主窗口关闭时关闭数据库:
MainWindow::~MainWindow() { db.close(); delete ui; } -
编译并运行程序,你应该能够使用虚拟账户登录。登录后,你应该能够看到与用户账户关联的虚拟员工信息。你也可以通过点击注销按钮来注销:

图 12.19 – 一个简单、可用的登录界面
它是如何工作的…
在这个例子中,我们从用户表中选择与我们在文本字段中插入的用户名和密码匹配的数据。如果没有找到任何内容,这意味着我们提供了无效的用户名或密码。否则,从用户账户中获取 user_employeeID 数据,并对 employee 表执行另一个 SQL 查询以查找与 user_employeeID 变量匹配的信息。然后,根据你的程序界面显示数据。
我们必须在编辑标签顺序模式下设置小部件顺序,以便程序启动时,第一个获得焦点的小部件是用户名行编辑小部件。如果用户按下键盘上的Tab键,焦点应切换到第二个小部件,即密码行编辑。不正确的小部件顺序会破坏用户体验并驱赶任何潜在用户。请确保密码行编辑的echoMode选项设置为密码。该设置将隐藏实际插入到行编辑中的密码,并用点符号替换以实现安全目的。
由于 SQLite 不支持返回查询大小,我们无法使用 QSqlQuery::size() 函数来确定返回多少结果;结果始终为 -1。因此,我们在 while 循环操作中声明了一个 resultSize 变量来计数结果。
在模型视图中显示数据库信息
按照以下步骤在模型视图小部件上显示数据库信息:
如何操作…
在本食谱中,我们将学习如何在程序中的模型视图中显示从我们的 SQL 数据库中获取的多组数据:
- 我们将使用名为
employee的数据库表,这是我们之前在 使用 Qt 创建登录界面 的示例中使用的。这次,我们需要在员工表中添加更多的数据。打开你的 SQLiteStudio 控制面板。为几个更多的员工添加数据,以便我们可以在程序中稍后显示:

图 12.20 – 向员工表添加更多虚拟数据
-
打开 Qt Creator,创建一个新的 Qt Widgets Application 项目,然后向你的项目添加 SQL 模块。
-
打开
mainwindow.ui并从 Widget(基于项)下的 Item Widget 中添加一个表格小部件(不是表格视图)。在画布上选择主窗口,然后点击 Lay Out Vertically 或 Lay Out Horizontally 按钮使表格小部件粘附到主窗口的大小,即使它被调整大小:

图 12.21 – 点击“垂直布局”按钮
- 双击表格小部件,将出现一个窗口。在 列 选项卡下,通过点击左上角的 + 按钮添加五个项目。将项目命名为 ID、Name、Age、Gender 和 Married。完成后点击 OK:

图 12.22 – 我们还设置了文本居中对齐
-
右键单击表格小部件,在弹出窗口中选择
itemChanged(QTableWidgetItem*)选项,然后按 OK。将在两个源文件中创建一个槽函数。 -
打开
mainwindow.h并将这些私有变量添加到MainWindow类中:private: Ui::MainWindow *ui; bool hasInit; mainwindow.h:include
include
include
include
include
include
-
打开
mainwindow.cpp;我们将在那里写大量的代码。我们需要声明程序启动时会发生什么。将以下代码添加到MainWindow类的构造函数中:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { hasInit = false; ui->setupUi(this); db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("database.db3"); ui->tableWidget->setColumnHidden(0, true); -
SQL 查询代码看起来像这样:
if (db.open()) { QSqlQuery query; if (query.exec("SELECT emp_id, emp_name, emp_age, emp_gender, emp_married FROM employee")) { while (query.next()) { qDebug() << query.value(0) << query.value(1) << query.value(2) << query.value(3) << query.value(4); QString id = query.value(0).toString(); QString name = query.value(1).toString(); QString age = query.value(2).toString(); int gender = query.value(3).toInt(); bool married = query.value(4).toBool(); -
创建几个
QTableWidgetItem对象:ui->tableWidget->setRowCount(ui->tableWidget->rowCount() + 1); QTableWidgetItem* idItem = new QTableWidgetItem(id); QTableWidgetItem* nameItem = new QTableWidgetItem(name); QTableWidgetItem* ageItem = new QTableWidgetItem(age); QTableWidgetItem* genderItem = new QTableWidgetItem(); if (gender == 0) genderItem->setData(0, "Male"); else genderItem->setData(0, "Female"); QTableWidgetItem* marriedItem = new QTableWidgetItem(); if (married) marriedItem->setData(0, "Yes"); else marriedItem->setData(0, "No"); -
将这些对象移动到表格小部件中:
ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 0, idItem); ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 1, nameItem); ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 2, ageItem); ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 3, genderItem); ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 4, marriedItem); } hasInit = true; } else { qDebug() << query.lastError().text(); } } else { qDebug() << "Failed to connect to database."; } } -
声明当表格小部件的项被编辑时会发生什么。将以下代码添加到
on_tableWidget_itemChanged()槽函数中:void MainWindow::on_tableWidget_itemChanged(QTableWidgetItem *item) { if (hasInit) { QString id = ui->tableWidget->item(item->row(), 0)->data(0).toString(); QString name = ui->tableWidget->item(item->row(), 1)->data(0).toString(); QString age = QString::number(ui->tableWidget->item(item->row(), 2)->data(0).toInt()); ui->tableWidget->item(item->row(), 2)->setData(0, age); QString gender; if (ui->tableWidget->item(item->row(), 3)->data(0).toString() == "Male") { gender = "0"; } else { ui->tableWidget->item(item->row(), 3)->setData(0,"Female"); gender = "1"; } QString married; if (ui->tableWidget->item(item->row(), 4)->data(0).toString() == "No") { married = "0"; } else { ui->tableWidget->item(item->row(), 4)->setData(0, "Yes"); married = "1"; } qDebug() << id << name << age << gender << married; QSqlQuery query; if (query.exec("UPDATE employee SET emp_name = '" + name + "', emp_age = '" + age + "', emp_gender = '" + gender + "', emp_married = '" + married + "' WHERE emp_id = " + id)) { QMessageBox::information(this, "Update Success", "Data updated to database."); } else { qDebug() << query.lastError().text(); } } } -
在类析构函数中关闭数据库:
MainWindow::~MainWindow() { db.close(); delete ui; } -
如果你现在编译并运行示例,你应该会得到类似这样的结果:

图 12.23 – 我们已经创建了自己的 SQL 编辑器
它是如何工作的…
表小部件与你在类似 Microsoft Excel 和 OpenOffice Calc 这样的电子表格应用中看到的小部件类似。与其他类型的模型查看器,如列表视图或树形视图相比,表格视图(或表小部件)是一个二维模型查看器,它以行和列的形式显示数据。
在 Qt 中,表格视图与表格小部件的主要区别在于,表格小部件是建立在表格视图类之上的,这意味着表格小部件更容易使用,更适合初学者。然而,表格小部件不如表格视图灵活,并且往往不如表格视图可扩展,如果你想要自定义表格,这并不是最佳选择。在从 SQLite 中检索数据后,我们为每个数据项创建了一个 QTableWidgetItem 项目,并设置了应该添加到表格小部件中的列和行。在将项目添加到表格小部件之前,我们必须通过调用 QTableWidget::setRowCount() 来增加表格的行数。我们也可以通过简单地调用 QTableWidget::rowCount() 来获取表格小部件的当前行数。
最左侧的列被隐藏起来,因为我们只使用它来保存数据的 ID,以便在行中的某个数据项发生变化时,我们可以用它来更新数据库。当其中一个单元格中的数据发生变化时,将调用 on_tableWidget_itemChanged() 插槽函数。它不仅会在你编辑单元格中的数据时被调用,也会在从数据库检索后首次将数据添加到表格中时被调用。为了确保这个函数只在我们编辑数据时被触发,我们使用一个名为 hasInit 的布尔变量来检查我们是否已经完成了初始化过程(将第一批数据添加到表格中)。如果 hasInit 为假,则忽略函数调用。
为了防止用户输入完全无关的数据类型,例如在只允许数字的数据单元格中插入字母,我们手动检查在它们被编辑时数据是否接近我们期望的任何有效内容。如果不符合任何有效内容,则将其还原为默认值。这当然是一个简单的技巧,虽然它能完成任务,但不是最专业的方法。或者,你可以尝试创建一个新的类,该类继承自 QItemDelegate 类,并定义你的模型视图应该如何行为。然后,通过调用 QTableWidget::setItemDelegate() 将该类应用到你的表格小部件上。
高级 SQL 查询
通过遵循这个食谱,你将学习如何使用高级 SQL 语句,例如 INNER JOIN、COUNT、LIKE 和 DISTINCT。
如何做到这一点…
你可以在 SQL 数据库上执行的操作远不止简单的查询。让我们遵循以下步骤来学习如何:
- 在我们可以进入编程部分之前,我们需要在我们的数据库中添加几个表。打开你的 SQLiteStudio。为了使这个示例正常工作,我们需要几个表:

图 12.24 – 我们需要为这个例子创建的附加表
- 我将向您展示本项目中所需每个表的架构以及为测试插入到表中的虚构数据。第一个表称为
branch,用于存储虚构公司不同分支的 ID 和名称:

图 12.25 – 分支表
- 其次,我们有
department表,它存储了虚构公司不同部门的 ID 和名称,这些部门通过分支 ID 与branch数据相链接:

图 12.26 – 部门表,它与分支表相链接
- 我们还有一个
employee表,它存储了虚构公司中所有员工的信息。这个表与我们在前面的例子中使用过的表类似,但它有两个额外的列,emp_birthday和emp_departmentID:

图 12.27 – 员工表,它与部门表相链接
- 我们还有一个名为
log的表,其中包含每个员工登录时间的虚构记录。log_loginTime将被设置为日期时间变量类型:

图 12.28 – 日志表,它与用户表相链接
- 我们有
user表,我们也在前面的例子中使用过:

图 12.29 – 用户表
- 打开 Qt Creator。这次,我们不是选择 Qt Widgets Application,而是选择 Qt Console Application:

图 12.30 – 创建 Qt 控制台应用程序项目
-
打开你的项目文件 (.pro) 并将
sql模块添加到你的项目中:QT += core sql main.cpp and add the following header files to the top of the source file:#include <QSqlDatabase>#include <QSqlQuery>#include <QSqlError>#include <QDate>#include <QDebug> -
添加以下函数以显示年龄超过 30 岁的员工:
void filterAge() { qDebug() << "== Employees above 40 year old ============="; QSqlQuery query; if (query.exec("SELECT emp_name, emp_age FROM employee WHERE emp_age > 40")) { while (query.next()) { qDebug() << query.value(0).toString() << query.value(1).toString(); } } else { qDebug() << query.lastError().text(); } } -
添加以下函数以显示每个员工的
department和branch信息:void getDepartmentAndBranch() { qDebug() << "== Get employees' department and branch ============="; QSqlQuery query; if (query.exec("SELECT emp_name, dep_name, brh_name FROM (SELECT emp_name, emp_departmentID FROM employee) AS myEmployee INNER JOIN department ON department.dep_id = myEmployee.emp_departmentID INNER JOIN branch ON branch.brh_id = department.dep_branchID")) { while (query.next()) { qDebug() << query.value(0).toString() << query.value(1).toString() << query.value(2).toString(); } } else { qDebug() << query.lastError().text(); } } -
添加以下函数,用于显示在纽约分支工作且年龄低于 40 岁的员工:
void filterBranchAndAge() { qDebug() << "== Employees from New York and age below 40 ============="; QSqlQuery query; if (query.exec("SELECT emp_name, emp_age, dep_name, brh_name FROM (SELECT emp_name, emp_age, emp_departmentID FROM employee) AS myEmployee INNER JOIN department ON department.dep_id = myEmployee.emp_departmentID INNER JOIN branch ON branch.brh_id = department.dep_branchID WHERE branch.brh_name = 'New York' AND myEmployee.emp_age < 40")) { while (query.next()) { qDebug() << query.value(0).toString() << query.value(1).toString() << query.value(2).toString() << query.value(3).toString(); } } else { qDebug() << query.lastError().text(); } } -
添加以下函数,用于计算虚构公司中女性员工的总数:
void countFemale() { qDebug() << "== Count female employees ============="; QSqlQuery query; if (query.exec("SELECT COUNT(emp_gender) FROM employee WHERE emp_gender = 1")) { while (query.next()) { qDebug() << query.value(0).toString(); } } else { qDebug() << query.lastError().text(); } } -
添加以下函数,用于过滤员工列表并仅显示以
Ja开头的名字:void filterName() { qDebug() << "== Employees name start with 'Ja' ============="; QSqlQuery query; if (query.exec("SELECT emp_name FROM employee WHERE emp_name LIKE '%Ja%'")) { while (query.next()) { qDebug() << query.value(0).toString(); } } else { qDebug() << query.lastError().text(); } } -
添加以下函数,用于显示八月份生日的员工:
void filterBirthday() { qDebug() << "== Employees birthday in August ============="; QSqlQuery query; if (query.exec("SELECT emp_name, emp_birthday FROM employee WHERE strftime('%m', emp_birthday) = '08'")) { while (query.next()) { qDebug() << query.value(0).toString() << query.value(1).toDate().toString("d-MMMM-yyyy"); } } else { qDebug() << query.lastError().text(); } } -
添加以下函数,用于检查谁在 2024 年 4 月 27 日登录了虚构系统,并在终端上显示他们的名字:
void checkLog() { qDebug() << "== Employees who logged in on 27 April 2024 ============="; QSqlQuery query; if (query.exec("SELECT DISTINCT emp_name FROM (SELECT emp_id, emp_name FROM employee) AS myEmployee INNER JOIN user ON user.user_employeeID = myEmployee.emp_id INNER JOIN log ON log.log_userID = user.user_id WHERE DATE(log.log_loginTime) = '2024-04-27'")) { while (query.next()) { qDebug() << query.value(0).toString(); } } else { qDebug() << query.lastError().text(); } } -
在
main()函数中,将程序连接到 SQLite 数据库并调用我们在前面步骤中定义的所有函数。关闭数据库连接,任务完成:int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("database.db3"); if (db.open()) { filterAge(); getDepartmentAndBranch(); filterBranchAndAge(); countFemale(); filterName(); filterBirthday(); checkLog(); db.close(); } else { qDebug() << "Failed to connect to database."; } return a.exec(); } -
如果你现在编译并运行项目,你应该会看到一个显示过滤结果的终端窗口:

图 12.31 – 在应用程序输出窗口中打印结果
它是如何工作的…
控制台应用程序没有 GUI,只在一个终端窗口中显示文本显示。这通常用于后端系统,因为它与使用小部件的应用程序相比,使用的资源更少。我们在这个例子中使用它是因为它更快地显示结果,无需在程序中放置任何小部件,而这在本例中是不需要的。
我们将 SQL 查询分成了不同的函数,这样更容易维护代码,并且不会变得过于杂乱。请注意,在 C++ 中,函数必须位于 main() 函数之前;否则,它们将无法被 main() 函数调用。
还有更多…
在前面的示例中使用的 INNER JOIN 语句将两个表连接起来,并选择两个表中的所有行,只要两个表中的列之间存在匹配即可。在 SQLite(以及一些其他类型的 SQL 架构)中,你可以使用许多其他类型的 JOIN 语句,例如 LEFT JOIN、RIGHT JOIN 和 FULL OUTER JOIN。
以下图表显示了不同类型的 JOIN 语句及其效果:

图 12.32 – 不同类型的 JOIN 语句
以下项目符号解释了我们在本食谱示例代码中使用的 LIKE 和 DISTINCT 语句:
-
LIKE语句通常用于在数据库中搜索不包含完整单词的字符串变量。请注意,在搜索关键字前后各有一个 % 符号。 -
在前面的示例中使用的
DISTINCT语句过滤掉了具有完全相同变量的结果。例如,如果没有使用DISTINCT语句,你将在终端中看到两个版本的拉里·金,因为他在同一天登录系统的记录有两条。通过添加DISTINCT语句,SQLite 将消除其中一个结果,并确保每个结果都是唯一的。 -
你可能想知道
d-MMMM-yyyy代表什么,以及为什么我们在前面的示例中使用它。这实际上是一个提供给 Qt 中的QDateTime类的表达式,用于使用给定格式显示日期时间结果。在这种情况下,它将我们从 SQLite 获取的日期时间数据,2024-08-06,转换为指定的格式,结果为 6-August-2024。
更多信息,请参阅 Qt 的文档doc.qt.io/qt-6/qdatetime.html#toString,其中包含可以用来确定日期时间字符串格式的完整表达式列表。
SQL 提供了一种简单高效的方法来保存和加载用户数据,无需重新发明轮子。Qt 为您提供了一个现成的解决方案,用于将您的应用程序与 SQL 数据库连接;在本章中,我们通过逐步的方法学习了如何实现这一点,并且能够将我们的用户数据加载和保存到 SQL 数据库中。
第十三章:使用 Qt WebEngine 开发 Web 应用程序
Qt 包含一个名为 Qt WebEngine 的模块,它允许我们将网页浏览器小部件嵌入到我们的程序中,并使用它来显示网页或本地 HTML 内容。在版本 5.6 之前,Qt 使用另一个类似的模块,名为 Qt WebKit,该模块现已弃用,并已被基于 Chromium 的 WebEngine 模块所取代。Qt 还允许通过 Qt WebChannel 在 JavaScript 和 C++ 代码之间进行通信,这使得我们能够更有效地使用此模块。
在本章中,我们将介绍以下食谱:
-
介绍 Qt WebEngine
-
使用
webview和网页设置 -
在你的项目中嵌入 Google 地图
-
从 JavaScript 调用 C++ 函数
-
从 C++ 调用 JavaScript 函数
技术要求
本章的技术要求包括 Qt 6.6.1 MSVC 2019 64 位、Qt Creator 12.0.2 和 Microsoft Visual Studio。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter13。
介绍 Qt WebEngine
在此示例项目中,我们将探索 Qt 中 WebEngine 模块的基本功能,并尝试构建一个简单的可工作的网页浏览器。由于 Qt 5.6,Qt 的 WebKit 模块已被弃用,并由基于 Google Chromium 引擎的 WebEngine 模块所取代。
如何做到这一点…
首先,让我们设置我们的 WebEngine 项目:
- 目前,Qt 的 WebEngine 模块仅与 Visual C++ 编译器兼容,而不与其他编译器兼容,例如 MinGW 或 Cygwin。这可能在将来改变,但这完全取决于 Qt 开发者是否希望将其移植到其他编译器。确保你电脑上安装的 Qt 版本支持 Visual C++ 编译器。你可以使用 Qt 的维护工具将 MSVC 2019 64-bit 组件添加到你的 Qt 安装中。此外,确保你在 Qt 版本中已安装 Qt WebEngine 组件:

图 13.1 – 确保已安装 MSVC 2019 和 Qt WebEngine
- 打开 Qt Creator 并创建一个新的 Qt Widgets 应用程序 项目。选择使用 Visual C++ 编译器的工具包:

图 13.2 – 只有 MSVC 被 Qt WebEngine 官方支持
-
打开你的项目文件(
.pro)并将以下模块添加到你的项目中。之后,你必须运行qmake以应用更改:QT += core gui mainwindow.ui and remove the menuBar, mainToolBar, and statusBar objects, as we don’t need those in this project:

图 13.3 – 移除菜单栏、主工具栏和状态栏
- 在画布上放置两个水平布局,然后在布局顶部放置一个 Line Edit 小部件和一个按钮:

图 13.4 – 将行编辑小部件和按钮放置在布局中
- 选择画布并单击位于编辑器顶部的垂直布局按钮:

图 13.5 – 点击“垂直布局”按钮
- 布局将扩展并跟随主窗口的大小。行编辑也会根据水平布局的宽度水平扩展:

图 13.6 – 行编辑现在正在水平扩展
- 在行编辑的左侧添加两个按钮。我们将使用这两个按钮在页面历史记录之间移动。稍后使用 C++代码将其添加到步骤 15,并将占用空间:

图 13.7 – 向 UI 添加两个按钮和一个进度条
-
右键单击其中一个按钮并选择
clicked(),然后点击mainwindow.h和mainwindow.cpp。对其他所有按钮重复此步骤。 -
右键单击行编辑并选择
returnPressed(),然后点击mainwindow.h和mainwindow.cpp。 -
让我们跳转到
mainwindow.h。我们需要做的第一件事是将以下头文件添加到mainwindow.h中:#include <QtWebEngineWidgets/QtWebEngineWidgets> -
在类析构函数下声明一个
loadUrl()函数:public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); loading() to mainwindow.h:private slots:
void on_goButton_clicked();
void on_address_returnPressed();
void on_backButton_clicked();
void on_forwardButton_clicked();
void loading(int progress);
-
声明一个
QWebEngineView对象并命名为webview:private: Ui::MainWindow *ui; QWebEngineView* webview; -
打开
mainwindow.cpp文件并初始化WebEngine视图。将其添加到第二个水平布局中,并将其loadProgress()信号连接到我们刚刚添加到mainwindow.h中的loading()槽函数:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); webview = new QWebEngineView; ui->horizontalLayout_2->addWidget(webview); connect(webview, &QWebEngineView::loadProgress, this, &MainWindow::loading); } -
声明当
loadUrl()函数被调用时会发生什么:void MainWindow::loadUrl() { QUrl url = QUrl(ui->address->text()); url.setScheme("http"); webview->page()->load(url); } -
当Go按钮被点击或当Enter键被按下时调用
loadUrl()函数:void MainWindow::on_goButton_clicked() { loadUrl(); } MainWindow::on_address_returnPressed() { loadUrl(); } -
至于其他两个按钮,我们将要求
webview加载历史记录堆栈中可用的上一页或下一页:void MainWindow::on_backButton_clicked() { webview->back(); } void MainWindow::on_forwardButton_clicked() { webview->forward(); } -
在网页加载时更改
progressBar的值:void MainWindow::loading(int progress) { ui->progressBar->setValue(progress); } -
现在构建并运行程序,你将得到一个非常基础但功能齐全的网页浏览器:


它是如何工作的…
旧的 webview 系统基于苹果的 WebKit 引擎,并且仅在 Qt 5.5 及其之前的版本中可用。从 5.6 版本开始,Qt 完全放弃了 WebKit,并替换为谷歌的 Chromium 引擎。API 已经完全改变,因此一旦迁移到 5.6,所有与 Qt WebKit 相关的代码将无法正确工作。如果你是 Qt 的新手,建议你跳过 WebKit,学习 WebEngine API,因为它正在成为 Qt 的新标准。
注意
如果你以前使用过 Qt 的 WebKit,这个网页会教你如何将你的旧代码迁移到 WebEngine:wiki.qt.io/Porting_from_QtWebKit_to_QtWebEngine
在上一节的 步骤 15 中,我们将属于 webview 小部件的 loadProgress() 信号连接到了 loading() 插槽函数。当在 步骤 17 中通过调用 QWebEnginePage::load() 加载你请求的网页时,信号将自动被调用。如果你需要,你也可以连接 loadStarted() 和 loadFinished() 信号。
在 步骤 17 中,我们使用了 QUrl 类将来自行编辑器的文本转换为 URL 格式。默认情况下,如果我们没有指定 URL 方案(HTTP、HTTPS、FTP 等),我们插入的地址将指向本地路径。如果我们给出的是 google.com 而不是 http://google.com,我们可能无法加载页面。因此,我们通过调用 QUrl::setScheme() 手动指定了一个 URL 方案,以确保在传递给 webview 之前地址格式正确。
还有更多…
如果出于某种原因你需要你的项目中使用 WebKit 模块而不是 WebEngine,你可以从 GitHub 获取模块代码并自行构建:github.com/qt/qtwebkit
使用 webview 和 web 设置
在这个菜谱中,我们将更深入地探讨 Qt 的 webview 中可用的功能。我们将使用前一个示例中的源文件,并为其添加更多代码。
如何操作…
让我们探索一下 Qt WebEngine 模块的一些基本功能:
-
打开
mainwindow.ui并在进度条下方添加一个垂直布局。将 纯文本编辑 小部件的plaintext属性添加到以下内容:<Img src="img/googlelogo_color_272x92dp.png"></img> <h1>Hello World!</h1> <h3>This is our custom HTML page.</h3> <script>alert("Hello!");</script>这是在你向 纯文本 编辑 小部件上方添加代码后的样子:

图 13.9 – 向底部添加纯文本编辑小部件和按钮
- 前往 文件 | 新建文件。一个窗口将会弹出,并要求你选择一个文件模板。在 Qt 类别下选择 Qt 资源文件,然后点击 选择… 按钮。输入你想要的文件名,点击 下一步,然后点击 完成:

图 13.10 – 创建 Qt 资源文件
- 通过在
/上右键单击我们刚刚创建的资源文件并点击.exe图像文件来打开它(一旦编译完成):

![图 13.11 – 将 tux.png 图像文件添加到我们的资源文件中]
-
打开
mainwindow.h并在其中添加以下头文件:#include <QMainWindow> #include <QtWebEngineWidgets/QtWebEngineWidgets> #include <QDebug> mainwindow.h:public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
void loadUrl();
private slots:
void on_goButton_clicked();
void on_address_returnPressed();
void on_backButton_clicked();
void on_forwardButton_clicked();
void startLoading();
void loading(int progress);
void loaded(bool ok);
void on_loadHtml_clicked();
private:
Ui::MainWindow *ui;
在 mainwindow.cpp 中添加以下代码到类构造函数中:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); webview = new QWebEngineView; ui->horizontalLayout_2->addWidget(webview); //webview->page()->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, false); //webview->page()->settings()->setAttribute(QWebEngineSettings::AutoLoadImages, false); //QString fontFamily = webview->page()->settings()->fontFamily(QWebEngineSettings::SerifFont); QString fontFamily = webview->page()->settings()->fontFamily(QWebEngineSettings::SansSerifFont); int fontSize = webview->page()->settings()->fontSize(QWebEngineSettings::MinimumFontSize); QFont myFont = QFont(fontFamily, fontSize); webview:QFile file("😕/tux.png");
if (file.open(QFile::ReadOnly)) {
QByteArray data = file.readAll();
webview->page()->setContent(data, "image/png");
}
else {
qDebug() << "文件无法打开。";
}
connect(webview, &QWebEngineView::loadStarted, this, &MainWindow::startLoading()));
connect(webview, &QWebEngineView::loadProgress, this, &MainWindow::loading(int)));
connect(webview, &QWebEngineView::loadFinished, this, &MainWindow::loaded(bool)));
}
-
在Qt WebEngine 入门中的前一个示例中,
MainWindow::loadUrl()函数保持不变,它在加载页面之前将 URL 方案设置为 HTTP:void MainWindow::loadUrl() { QUrl url = QUrl(ui->address->text()); url.setScheme("http"); webview->page()->load(url); } -
以下函数也是如此,它们与Qt WebEngine 入门中的前一个示例保持一致:
void MainWindow::on_goButton_clicked() { loadUrl(); } void MainWindow::on_address_returnPressed() { loadUrl(); } void MainWindow::on_backButton_clicked() { webview->back(); } void MainWindow::on_forwardButton_clicked() { webview->forward(); } -
添加
MainWindow::startLoading()和MainWindow::loaded()槽函数,这两个函数将由loadStarted()和loadFinished()信号调用。这两个函数基本上在页面开始加载时显示进度条,在页面加载完成后隐藏进度条:void MainWindow::startLoading() { ui->progressBar->show(); } void MainWindow::loading(int progress) { ui->progressBar->setValue(progress); } void MainWindow::loaded(bool ok) { ui->progressBar->hide(); } -
当点击加载 HTML按钮时,调用
webview->loadHtml()将纯文本转换为 HTML 内容:void MainWindow::on_loadHtml_clicked() { webview->setHtml(ui->source->toPlainText()); } -
构建并运行程序,你应该会看到以下内容:

图 13.12 – webview 将显示由您的 HTML 代码生成的结果
它是如何工作的...
在这个例子中,我们使用 C++加载了一个图像文件并将其设置为webview的默认内容(而不是空白页面)。我们也可以通过在启动时加载包含图像的默认 HTML 文件来实现相同的结果。
类构造函数中的部分代码已被注释掉。你可以移除双斜杠(//)并查看它带来的差异 – JavaScript 警告将不再出现(因为 JavaScript 已被禁用),图像将不再在webview中显示。
你还可以尝试将字体家族从QWebEngineSettings::SansSerifFont更改为QWebEngineSettings::SerifFont。你将注意到webview中字体显示的细微差别:

图 13.13 – 在 webview 中显示不同类型的字体
通过点击webview将纯文本编辑小部件的内容视为 HTML 代码并加载为 HTML 页面。你可以使用这个功能创建一个由 Qt 驱动的简单 HTML 编辑器!
在你的项目中嵌入谷歌地图
在这个菜谱中,我们将学习如何通过 Qt 的WebEngine模块将谷歌地图嵌入到我们的项目中。这个示例并不侧重于 Qt 和 C++,而是侧重于 HTML 代码中的谷歌地图API。
如何做到这一点…
让我们按照以下步骤创建一个显示谷歌地图的程序:
-
创建一个新的Qt Widgets 应用程序项目,并移除状态栏、菜单栏和主工具栏对象。
-
打开你的项目文件(
.pro)并添加以下模块到你的项目中:QT += core gui webenginewidgets -
打开
mainwindow.ui并在画布上添加一个垂直布局。然后,选择画布并点击画布顶部的垂直布局按钮。你会得到以下内容:

图 13.14 – 向中央小部件添加垂直布局
-
打开
mainwindow.cpp并在源代码顶部添加以下头文件:#include <QtWebEngineWidgets/QWebEngineView> -
在
MainWindow构造函数中添加以下代码:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); QWebEngineView* webview = new QWebEngineView; QUrl url = QUrl("qrc:/map.html"); webview->page()->load(url); ui->verticalLayout->addWidget(webview); } -
前往
.qrc)。我们将向项目中添加一个 HTML 文件,命名为map.html:

图 13.15 – 将 map.html 添加到资源文件
-
使用你喜欢的文本编辑器打开
map.html。不建议使用 Qt Creator 打开 HTML 文件,因为它不提供 HTML 语法的颜色编码。 -
开始编写 HTML 代码,声明重要的标签,如
<html>、<head>和<body>:<!DOCTYPE html> <html> <head> </head> <body ondragstart="return false"> </body> </html> -
在
<body>中添加一个<div>标签并设置其 ID 为map-canvas:<body ondragstart="return false"> <div id="map-canvas" /> </body> -
将以下代码添加到 HTML 文档的头部:
<meta name="viewport" content="initial-scale=1.0, userscalable=no" /> <style type="text/css"> html { height: 100% } body { height: 100%; margin: 0; padding: 0 } #map-canvas { height: 100% } </style> <script type="text/javascript" src="img/js?key=YOUR_KEY_HERE&libraries=drawing"></script> -
将以下代码也添加到 HTML 文档的头部,在之前步骤中插入的代码下方:
<script type="text/javascript"> var map; function initialize() { // Add map var mapOptions = { center: new google.maps.LatLng(40.705311, -74.2581939), zoom: 6 }; map = new google.maps.Map(document.getElementById("mapcanvas"),mapOptions); // Add event listener google.maps.event.addListener(map, 'zoom_changed', function() { //alert(map.getZoom()); }); -
创建一个标记并将其放置在地图上:
// Add marker var marker = new google.maps.Marker({ position: new google.maps.LatLng(40.705311, -74.2581939), map: map, title: "Marker A", }); google.maps.event.addListener (marker, 'click', function() { map.panTo(marker.getPosition()); }); marker.setMap(map); -
向地图添加一条折线:
// Add polyline var points = [ new google.maps.LatLng(39.8543, -73.2183), new google.maps.LatLng(41.705311, -75.2581939), new google.maps.LatLng(40.62388, -75.5483) ]; var polyOptions = { path: points, strokeColor: '#FF0000', strokeOpacity: 1.0, strokeWeight: 2 }; historyPolyline = new google.maps.Polyline(polyOptions); historyPolyline.setMap(map); -
添加一个多边形形状:
// Add polygon var points = [ new google.maps.LatLng(37.314166, -75.432), new google.maps.LatLng(40.2653, -74.4325), new google.maps.LatLng(38.8288, -76.5483) ]; var polygon = new google.maps.Polygon({ paths: points, fillColor: '#000000', fillOpacity: 0.2, strokeWeight: 3, strokeColor: '#fff000', }); polygon.setMap(map); -
创建一个绘图管理器并将其应用到地图上:
// Setup drawing manager var drawingManager = new google.maps.drawing.DrawingManager(); drawingManager.setMap(map); } google.maps.event.addDomListener(window, 'load', initialize); </script> -
编译并运行项目。你应该看到以下内容:

图 13.16 – 你应该在谷歌地图上看到一个标记、一条折线和三角形
它是如何工作的…
谷歌允许你使用其 JavaScript 库(称为webview小部件)将谷歌地图嵌入到网页中,该小部件使用谷歌地图API。这种方法唯一的缺点是我们无法在没有互联网连接的情况下加载地图。
谷歌地图API 可以通过你的网站调用,因为谷歌允许这样做。如果你的计划是处理大量流量,请选择免费 API。
前往console.developers.google.com获取一个免费密钥,并将 JavaScript 源路径中的YOUR_KEY_HERE替换为你从谷歌获得的 API 密钥。
我们必须定义一个<div>对象,它作为地图的容器。然后,当我们初始化地图时,我们指定<div>对象的 ID,这样Google Maps API 就知道在嵌入地图时查找哪个 HTML 元素。默认情况下,我们将地图中心设置为纽约的坐标,并将默认缩放级别设置为 6。然后,我们添加一个事件监听器,当地图的缩放级别发生变化时,它会触发。
从代码中移除双斜杠(//)以查看其效果。之后,我们通过 JavaScript 在地图上添加一个标记。该标记还附加了一个事件监听器,当标记被点击时,将触发panTo()函数。
它基本上将地图视图平移到被点击的标记。尽管我们已经将绘图管理器添加到地图中(位于地图和卫星按钮旁边的图标按钮),允许用户在地图上绘制任何类型的形状,但也可以使用 JavaScript 手动添加形状,类似于我们在步骤 12中在如何做 it... 部分添加标记的方式。
最后,你可能已经注意到,头文件被添加到mainwindow.cpp而不是mainwindow.h。这完全没问题,除非你在mainwindow.h中声明类指针——那么,你必须将这些头文件包含在内。
从 JavaScript 调用 C++函数
在这个菜谱中,我们将学习如何将我们的知识付诸实践,并使用 Qt 和 SQLite 创建一个功能性的登录屏幕。
如何做…
让我们学习如何使用以下步骤从 JavaScript 调用 C++函数:
-
创建一个
.pro)并将以下模块添加到项目中:QT += core gui mainwindow.ui and delete the mainToolBar, menuBar, and statusBar objects, as we don’t need any of these in this example program. -
将垂直布局添加到画布中,然后选择画布并点击
Hello!。通过以下方式设置其styleSheet属性来使其字体更大:font: 75 26pt "MS Shell Dlg 2";这就是我们应用了字体属性后的样子:

图 13.17 – 将字体属性应用于“Hello!”文本
- 前往文件 | 新建文件…并创建一个资源文件。将属于jQuery、Bootstrap和Font Awesome的空 HTML 文件、所有 JavaScript 文件、CSS 文件、字体文件等添加到项目资源中:

图 13.18 – 将所有文件添加到项目的资源中
-
打开你的 HTML 文件,在这个例子中称为
test.html。将所有必要的 JavaScript 和 CSS 文件链接到 HTML 源代码,位于<head>标签之间:<!DOCTYPE html> <html> <head> <script src="img/qwebchannel.js"></script> <script src="img/jquery.min.js"></script> <script src="img/bootstrap.js"></script> <link rel="stylesheet" type="text/css" href="css/bootstrap.css"> <link rel="stylesheet" type="text/css" href="css/fontawesome.css"> </head> <body> </body> </html> -
将以下 JavaScript 添加到
<head>元素中,位于<script>标签之间:<script> $(document).ready(function() { new QWebChannel(qt.webChannelTransport, function(channel) { mainWindow = channel.objects.mainWindow; }); $("#login").click(function(e) { e.preventDefault(); var user = $("#username").val(); var pass = $("#password").val(); mainWindow.showLoginInfo(user, pass); }); -
当点击changeText按钮并使用以下代码时,打印再见:
$("#changeText").click(function(e) { e.preventDefault(); mainWindow.changeQtText("Good bye!"); }); }); </script> -
将以下代码添加到
<body>元素中:<div class="container-fluid"> <form id="example-form" action="#" class="container-fluid"> <div class="form-group"> <div class="col-md-12"><h3>Call C++ Function from Javascript</h3></div> <div class="col-md-12"> <div class="alert alert-info" role="alert"><i class="fa fa-info-circle"></i> <span id="infotext">Click "Login" to send username and password variables to C++. Click "Change Cpp Text" to change the text label on Qt GUI.</span> </div> </div> -
从前面的代码继续,这次我们创建用户名和密码的输入字段,底部有两个按钮,分别称为登录和更改 Cpp 文本:
<div class="col-md-12"><label>Username:</label><input id="username" type="text"><p /> </div> <div class="col-md-12"> <label>Password:</label> <input id="password" type="password"><p /> </div> <div class="col-md-12"> <button id="login" class="btn btn-success" type="button"><i class="fa fa-check"></i> Login</button> <button id="changeText" class="btn btn-primary" type="button"> <i class="fa fa-pencil"></i> Change Cpp Text</button> </div> </div> </form> </div> -
打开
mainwindow.h文件,并将以下公共函数添加到MainWindow类中:public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); Q_INVOKABLE void changeQtText(QString newText); mainwindow.cpp and add the following headers to the top of the source code:包含
<QtWebEngineWidgets/QWebEngineView>包含
<QtWebChannel/QWebChannel>包含
<QMessageBox> -
将以下代码添加到
MainWindow构造函数中:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "1234"); ui->setupUi(this); QWebEngineView* webview = new QWebEngineView(); ui->verticalLayout->addWidget(webview); QWebChannel* webChannel = new QWebChannel(); webChannel->registerObject("mainWindow", this); webview->page()->setWebChannel(webChannel); webview->page()->load(QUrl("qrc:///html/test.html")); } -
声明当
changeQtText()和showLoginInfo()被调用时会发生什么:void MainWindow::changeQtText(QString newText) { ui->label->setText(newText); } void MainWindow::showLoginInfo(QString user, QString pass) { QMessageBox::information(this, "Login info", "Username is " + user + " and password is " + pass); } -
编译并运行程序;你应该会看到以下截图类似的内容。如果你点击更改 Cpp 文本按钮,顶部的单词Hello!将变为Goodbye!如果你点击登录按钮,将弹出一个消息框,显示你输入的用户名和密码输入字段中的确切内容:

图 13.19 – 点击按钮以调用 C++函数
它是如何工作的...
在这个例子中,我们使用了两个 JavaScript 库:jQuery和Bootstrap。我们还使用了一个名为Font Awesome的图标字体包。这些第三方插件被用来使 HTML 用户界面更加有趣,并且能够对不同屏幕分辨率做出响应。
我们还使用了jQuery来检测文档的就绪状态,以及获取输入字段的值。
注意
您可以从jquery.com/download下载jQuery,从getbootstrap.com/getting-started/#download下载Bootstrap,以及从fontawesome.io下载Font Awesome。
Qt 的WebEngine模块使用一种称为WebChannel的机制,它使得 C++程序和 HTML 页面之间的点对点(P2P)通信成为可能。WebEngine模块提供了一个 JavaScript 库,使得集成变得更加容易。JavaScript 默认嵌入到你的项目资源中,因此你不需要手动将其导入到你的项目中。你只需要通过调用以下代码将其包含在你的 HTML 页面中:
<script src="img/qwebchannel.js"></script>
一旦你包含了qwebchannel.js,你可以初始化QWebChannel类,并将我们之前在 C++中注册的 Qt 对象分配给一个 JavaScript 变量。
在 C++中,这样做如下:
QWebChannel* webChannel = new QWebChannel();
webChannel->registerObject("mainWindow", this);
webview->page()->setWebChannel(webChannel);
然后,在 JavaScript 中,这样做如下:
new QWebChannel(qt.webChannelTransport, function(channel) {
mainWindow = channel.objects.mainWindow;
});
你可能想知道这一行代表什么:
qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "1234");
Qt 的1234定义了你想要用于远程调试的端口号。
一旦你启用了远程调试,你可以通过打开一个基于 Chromium 的网页浏览器,例如 Google Chrome(这不会在 Firefox 和其他浏览器中工作),并输入http://127.0.0.1:1234来访问调试页面。你将看到一个看起来像这样的页面:

图 13.20 – 可检查的页面使调试更加容易
第一页将显示您程序中当前运行的所有 HTML 页面,在本例中是 test.html。点击页面链接,它将带您到另一个页面进行检查。您可以使用此功能检查 CSS 错误、JavaScript 错误和缺失的文件。
注意,一旦您的程序无错误且准备部署,应禁用远程调试。这是因为远程调试需要时间来初始化,并将增加您程序的启动时间。
如果您想从 JavaScript 调用一个 C++ 函数,必须在函数声明前放置 Q_INVOKABLE 宏;否则,它将不起作用:
Q_INVOKABLE void changeQtText(QString newText);
从 C++ 调用 JavaScript 函数
在前面的配方中,我们学习了如何通过 Qt 的 WebChannel 系统从 JavaScript 调用 C++ 函数。在本例中,我们将尝试做相反的事情:从 C++ 代码调用 JavaScript 函数。
如何做到这一点…
我们可以通过以下步骤从 C++ 调用 JavaScript 函数:
-
为您的项目创建一个新的
webenginewidgets模块。 -
打开
mainwindow.ui并删除 mainToolBar、menuBar 和 statusBar 对象。 -
将一个垂直布局和一个水平布局添加到画布上。选择画布并点击 垂直布局。确保水平布局位于垂直布局的底部。
-
向水平布局添加两个按钮;一个称为
clicked()选项并点击 确定。Qt 将自动将槽函数添加到您的源代码中。对另一个按钮重复此步骤:

图 13.21 – 将按钮放置在底部布局中
-
打开
mainwindow.h并向其中添加以下头文件:#include <QtWebEngineWidgets/QWebEngineView> #include <QtWebChannel/QWebChannel> #include <QMessageBox> -
声明
QWebEngineView对象的类指针,称为webview:public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); mainwindow.cpp and add the following code to the MainWindow constructor:MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
//qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "1234");
ui->setupUi(this);
webview = new QWebEngineView();
ui->verticalLayout->addWidget(webview);
QWebChannel* webChannel = new QWebChannel();
webChannel->registerObject("mainWindow", this);
webview->page()->setWebChannel(webChannel);
webview->page()->load(QUrl("qrc:///html/test.html"));
}
-
定义当点击
changeHtmlText按钮和playUIAnimation按钮时会发生什么:void MainWindow::on_changeHtmlTextButton_clicked() { webview->page()->runJavaScript("changeHtmlText('Text has been replaced by C++!');"); } void MainWindow::on_playUIAnimationButton_clicked() { webview->page()->runJavaScript("startAnim();"); } -
让我们通过转到 文件 | 新建文件... 来为我们的项目创建一个资源文件。在 Qt 类别下选择 Qt 资源文件 并点击 选择...。插入您想要的文件名并点击 下一步,然后点击 完成。
-
添加一个空的 HTML 文件以及所有必需的附加组件(
tux.png图像文件也添加到资源文件中,因为我们将在 步骤 14 中使用它)。 -
打开我们刚刚创建的 HTML 文件并将其添加到项目资源中;在我们的例子中,它被称为
test.html。向文件中添加以下 HTML 代码:<!DOCTYPE html> <html> <head> <script src="img/qwebchannel.js"></script> <script src="img/jquery.min.js"></script> <script src="img/bootstrap.js"></script> <link rel="stylesheet" type="text/css" href="css/bootstrap.css"> <link rel="stylesheet" type="text/css" href="css/fontawesome.css"> </head> <body> </body> </html> -
将以下 JavaScript 代码(包含在
<script>标签内)添加到我们 HTML 文件的<head>元素中:<script> $(document).ready(function() { $("#tux").css({ opacity:0, width:"0%", height:"0%" }); $("#listgroup").hide(); $("#listgroup2").hide(); new QWebChannel(qt.webChannelTransport, function(channel) { mainWindow = channel.objects.mainWindow; }); }); function changeHtmlText(newText) { $("#infotext").html(newText); } -
定义一个
startAnim()函数:function startAnim() { // Reset $("#tux").css({ opacity:0, width:"0%", height:"0%" }); $("#listgroup").hide(); $("#listgroup2").hide(); $("#tux").animate({ opacity:1.0, width:"100%", height:"100%" }, 1000, function() { // tux animation complete $("#listgroup").slideDown(1000, function() { // listgroup animation complete $("#listgroup2").fadeIn(1500); }); }); } </script> -
将以下代码添加到我们的 HTML 文件的
<body>元素中:<div class="container-fluid"> <form id="example-form" action="#" class="container-fluid"> <div class="form-group"> <div class="col-md-12"><h3>Call Javascript Function from C++</h3></div> <div class="col-md-12"> <div class="alert alert-info" role="alert"><i class="fa fa-info-circle"></i> <span id="infotext"> Change this text using C++.</span></div> </div> <div class="col-md-2"> <img id="tux" src="img/tux.png"></img> </div> -
继续编写以下代码,我们已添加了一个列表:
<div class="col-md-5"> <ul id="listgroup" class="list-group"> <li class="list-group-item">Cras justoodio</li> <li class="list-group-item">Dapibus acfacilisis in</li> <li class="list-group-item">Morbi leorisus</li> <li class="list-group-item">Porta acconsectetur ac</li> <li class="list-group-item">Vestibulum ateros</li> </ul> </div> <div id="listgroup2" class="col-md-5"> <a href="#" class="list-group-item active"> <h4 class="list-group-item-heading">Item heading</h4> <p class="list-group-item-text">Cras justo odio</p> </a> -
代码继续,当我们向第二个列表添加剩余的项目时:
<a href="#" class="list-group-item"> <h4 class="list-group-item-heading">Item heading</h4> <p class="list-group-item-text">Dapibus ac facilisis in</p> </a> <a href="#" class="list-group-item"> <h4 class="list-group-item-heading">Item heading</h4> <p class="list-group-item-text">Morbi leo risus</p> </a> </div> </div> </form> </div> -
编译并运行程序;你应该会得到以下屏幕截图中的类似结果。当你点击 更改 HTML 文本 按钮时,信息文本位于顶部面板中。如果你点击 播放 UI 动画 按钮,企鹅图像以及两组小部件将依次出现,并带有不同的动画:

图 13.22 – 点击底部的按钮查看结果
它是如何工作的…
此示例与 从 JavaScript 调用 C++ 函数 菜单中的上一个示例类似。一旦我们包含了 QWebChannel 类,我们就可以通过调用 webview->page()->runJavascript("jsFunctionNameHere();") 来从 C++ 调用任何 JavaScript 函数。别忘了将 C++ 中创建的 Web 频道应用到 webview 页面上;否则,它将无法与你的 HTML 文件中的 QWebChannel 类进行通信。
默认情况下,我们更改企鹅图像的 CSS 属性,将其不透明度设置为 0,宽度设置为 0%,高度设置为 0%。我们还通过调用 hide() jQuery 函数隐藏了两个列表组。当点击 播放 UI 动画 按钮时,我们重复这些步骤,以防动画已经播放过(即,之前已经点击过相同的按钮),然后再次隐藏列表组,以便重新播放动画。
jQuery 的一个强大功能是,你可以定义动画完成后发生的事情,这允许我们按顺序播放动画。在这个例子中,我们首先从企鹅图像开始,并在一秒(1,000 毫秒)内将其 CSS 属性插值到目标设置。一旦完成,我们开始另一个动画,使第一个列表组在 1 秒内从顶部滑动到底部。之后,我们运行第三个动画,使第二个列表组在 1.5 秒内从无到有淡入。
为了替换顶部面板中的信息文本,我们在函数内部创建了一个名为 changeHtmlText() 的 JavaScript 函数,并通过引用其 ID 并调用 html() 来更改其内容。
第十四章:性能优化
Qt 6 以其优化的性能而闻名。然而,如果您的代码编写得不好,性能问题仍然可能发生。有许多方法可以帮助我们识别这些问题并在发布软件给用户之前修复它们。
在本章中,我们将介绍以下食谱:
-
优化表单和 C++
-
分析和优化 QML
-
渲染和动画
技术要求
本章的技术要求包括 Qt 6.6.1 MinGW 64 位、Qt Creator 12.0.2 和 Windows 11。本章中使用的所有代码都可以从以下 GitHub 仓库下载:github.com/PacktPublishing/QT6-C-GUI-Programming-Cookbook---Third-Edition-/tree/main/Chapter14。
优化表单和 C++
学习如何优化使用 C++ 构建的基于表单的 Qt 6 应用程序非常重要。做到这一点最好的方法就是学习如何衡量和比较所使用的方法,并决定哪一种最适合您。
如何做到这一点……
让我们按照以下步骤开始:
-
让我们创建一个
mainwindow.cpp文件。之后,将以下头文件添加到源代码的顶部:#include <QPushButton> #include <QGridLayout> #include <QMessageBox> #include <QElapsedTimer> #include <QDebug> -
创建一个
QGridLayout对象并将其父对象设置为centralWidget:MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); QElapsedTimer object. We will be using this to measure the performance of our next operation:QElapsedTimer* time = new QElapsedTimer;
time->start();
-
我们将使用两个循环将 600 个按钮添加到我们的网格布局中,并在点击时将它们连接到一个 lambda 函数。然后我们将测量经过的时间并打印出结果,如下所示:
for (int i = 0; i < 40; ++i) { for (int j = 0; j < 15; ++j) { QPushButton* newWidget = new QPushButton(); newWidget->setText("Button"); layout->addWidget(newWidget, i, j); connect(newWidget, QPushButton::clicked, [this]() { QMessageBox::information(this, "Clicked", "Button has been clicked!"); }); } } qDebug() << "Test GUI:" << time->elapsed() << "msec"; -
如果我们现在构建并运行项目,我们会看到一个窗口充满了许多按钮。当我们点击其中一个时,屏幕上会弹出一个消息框。在我的电脑上,创建和布局主窗口中的所有 600 个按钮只花了大约九毫秒。当我们移动窗口或调整其大小时,也没有性能问题,这相当令人印象深刻。这证明了 Qt 6 可以很好地处理这种情况。然而,请注意,您的用户可能在使用较旧的机器,因此在设计用户界面时,您可能需要格外小心:

图 14.1 – 在 Qt 窗口中生成 600 个按钮
-
让我们给每个按钮添加一个样式表,如下所示:
QPushButton* newWidget = new QPushButton(); newWidget->setText("Button"); newWidget->setStyleSheet("background-color: blue; color: white;"); layout->addWidget(newWidget, i, j); -
再次构建并运行程序。这次,设置 GUI 大约花了 75 毫秒。这意味着样式表确实对您程序的性能有一定影响:

图 14.2 – 将样式表应用于所有 600 个按钮
-
完成这些后,让我们对不同类型的 C++ 容器进行一些性能测试。打开
main.cpp并添加以下头文件:#include "mainwindow.h" #include <QApplication> #include <QDebug> #include <QElapsedTimer> #include <vector> #include <QVector> -
在
main()函数之前创建一个testArray()函数:int testArray(int count) { int sum = 0; int *myarray = new int[count]; for (int i = 0; i < count; ++i) myarray[i] = i; for (int j = 0; j < count; ++j) sum += myarray[j]; delete [] myarray; return sum; } -
创建一个名为
testVector()的函数,如下所示:int testVector(int count) { int sum = 0; std::vector<int> myarray; for (int i = 0; i < count; ++i) myarray.push_back(i); for (int j = 0; j < count; ++j) sum += myarray.at(j); return sum; } -
完成这些后,继续创建另一个名为
testQtVector()的函数:int testQtVector(int count) { int sum = 0; QVector<int> myarray; for (int i = 0; i < count; ++i) myarray.push_back(i); for (int j = 0; j < count; ++j) sum += myarray.at(j); return sum; } -
在
main()函数中,定义一个QElapsedTimer对象和一个名为lastElapse的整型变量:int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); QElapsedTimer* time = new QElapsedTimer; time->start(); int lastElapse = 0; -
我们将调用之前步骤中创建的三个函数来测试它们的性能:
int result = testArray(100000000); qDebug() << "Array:" << (time->elapsed() - lastElapse) << "msec"; lastElapse = time->elapsed(); int result2 = testVector(100000000); qDebug() << "STL vector:" << (time->elapsed() - lastElapse) << "msec"; lastElapse = time->elapsed(); int result3 = testQtVector(100000000); qDebug() << "Qt vector:" << (time->elapsed() - lastElapse) << "msec"; lastElapse = time->elapsed(); -
现在构建并运行程序;我们将看到这些容器之间的性能差异。在我的电脑上,数组执行耗时 650 毫秒,而 STL 向量大约耗时 3,830 毫秒,Qt 向量执行耗时约为 5,400 毫秒。
注意
因此,尽管与另外两个相比缺少一些功能,数组仍然是性能最佳的容器。令人惊讶的是,Qt 自己的向量类比 C++标准库提供的向量容器运行得略慢。
它是如何工作的…
当创建Qt Widgets 应用程序项目时,尝试以下操作以提高性能:
-
避免向堆叠小部件添加太多页面并将它们填满小部件,因为 Qt 需要在渲染过程和事件处理中递归地找到所有这些,这将极大地影响程序的性能。
-
请注意,
QWidget类使用光栅引擎,一个软件渲染器,来渲染小部件,而不是使用 GPU。然而,它足够轻量,足以在大多数时候保持良好的性能。或者,你也可以考虑为程序的 GUI 使用 QML,因为它完全硬件加速。 -
如果你的小部件不需要,请关闭mouseTracking、tabletTracking和其他事件捕获。这种跟踪和捕获会增加程序 CPU 使用成本:

图 14.3 – 为优化禁用 mouseTracking 和 tabletTracking
-
尽可能保持样式表简单。一个大的样式表需要更长的时间让 Qt 将信息解析到渲染系统中,这也会影响性能。
-
不同的 C++容器产生不同的速度,正如我们在前面的例子中所展示的。令人惊讶的是,Qt 的向量容器比 STL(C++标准库)的向量容器略慢。总的来说,古老的 C++数组仍然是最快的,但它不提供排序功能。使用最适合你需求的方法。
-
对于大型操作,尽可能使用异步方法,因为它不会阻塞主进程,并保持程序平稳运行。
-
多线程对于在并行事件循环中运行不同的操作非常好。然而,如果操作不当,它也可能变得相当丑陋,例如,频繁创建和销毁线程,或者线程间通信没有计划好。
-
如果不是绝对必要,请尽量避免使用网络引擎。这是因为将完整的网络浏览器嵌入到您的程序中是非常沉重的,尤其是对于小型应用程序。如果您想创建以用户界面为中心的软件,可以考虑使用 QML 而不是创建混合应用程序。
-
通过像前一个示例项目中所做的那样进行性能测试,您可以轻松地确定哪种方法最适合您的项目,以及如何使您的程序表现更佳。
注意
在 Qt 5 中,我们可以使用 QTime 类进行测试,如本节所示。然而,start() 和 elapsed() 等函数已被从 Qt 6 中的 QTime 类中弃用。从 Qt 6 开始,玩家必须使用 QElapsedTimer 来处理此功能。
分析和优化 QML
Qt 6 中的 QML 引擎利用了硬件加速,使其渲染能力和性能优于旧的 widgets 用户界面。但这并不意味着您不需要担心优化,因为小的性能问题可能会随着时间的推移而累积成更大的问题,并损害您产品的声誉。
如何操作…
按照以下步骤开始分析并优化 QML 应用程序:
- 让我们创建一个 Qt Quick 应用程序 项目:

图 14.4 – 创建 Qt Quick 应用程序项目
- 然后,转到 分析 | QML 分析器 并运行 QML 分析器 工具:

图 14.5 – 运行 QML 分析器以检查 QML 性能
- 然后,您的 Qt Quick 项目将由 QML 分析器运行。在代码编辑器下也会出现 QML 分析器 窗口。在程序通过测试点(在这种情况下意味着成功创建空窗口)后,点击位于 QML 分析器 窗口顶部栏的 停止 按钮:

图 14.6 – 通过按下带有红色矩形图标的按钮停止 QML 分析器
- 在停止分析器分析后,在 QML 分析器 窗口下的 时间线 标签将显示一个时间线。您可以在以下四个标签之间切换,即 时间线、火焰图、Quick3D 帧 和 统计信息,位于 QML 分析器 窗口的底部:

图 14.7 – 您可以在不同的标签页上查看不同的数据
- 让我们检查一下 Timeline 标签页。在时间线显示下,我们可以看到六个不同的类别:Scene Graph、Memory Usage、Input Events、Compiling、Creating 和 Binding。这些类别为我们提供了程序在整个执行过程中的不同阶段和过程的概述。我们还可以在时间线上看到一些彩色条形。让我们点击 Creating 类别下名为 QtQuick/Window 的一个条形。一旦点击,我们将在 QML Profiler 窗口顶部的矩形窗口中看到此操作的持续时间以及代码的位置:

图 14.8 – 时间线标签页
- 完成这些后,让我们继续并打开 Flame Graph 标签页。在 Flame Graph 标签页下,你会看到以百分比形式显示的应用程序的总时间、内存和分配的视觉化。你可以通过点击位于 QML Profiler 窗口右上角的选项框来在总时间、内存和分配之间切换:

图 14.9 – 火焰图标签页
- 不仅如此,你还会在 QML 代码编辑器上看到显示的百分比值:

图 14.10 – 百分比值显示在右侧
-
在 QML Profiler 窗口下打开 Quick3D Frame 类别。这个标签页是检查 3D 渲染性能的地方。目前它是空的,因为我们没有进行任何 3D 渲染。
-
接下来,让我们打开 Statistics 类别。这个标签页基本上以表格形式显示关于进程的信息:

图 14.11 – 统计标签页
它是如何工作的…
这与我们在之前使用 C++ 和小部件的示例项目中做的是类似的,只是这次它是通过 Qt 6 提供的 QML Profiler 工具自动分析的。
QML Profiler 不仅生成特定进程运行的总时间,还显示内存分配、应用程序的执行时间线以及其他能让你深入了解软件性能的信息。
通过查看 QML Profiler 分析的数据,你将能够找出代码中哪个部分减慢了程序,让你能够快速修复任何问题。
当你编写 QML 时,有一些规则你需要注意,以避免性能瓶颈。例如,类型转换有时可能很昂贵,尤其是在不紧密匹配的类型之间(例如字符串到数字)。随着项目随时间增长而变大,这类小问题很可能会演变成瓶颈。
除了这些,尽量避免在经常运行的代码块中多次使用id进行项目查找,如下例所示:
Item {
width: 400
height: 400
Rectangle {
id: rect
anchors.fill: parent
color: "green"
}
Component.onCompleted: {
for (var i = 0; i < 1000; ++i) {
console.log("red", rect.color.r);
console.log("green", rect.color.g);
console.log("blue", rect.color.b);
console.log("alpha", rect.color.a);
}
}
相反,我们可以使用一个变量来缓存数据,避免在同一个项目上重复多次查找:
Component.onCompleted: {
var rectColor = rect.color;
for (var i = 0; i < 1000; ++i) {
console.log("red", rectColor.r);
console.log("green", rectColor.g);
console.log("blue", rectColor.b);
console.log("alpha", rectColor.a);
}
}
此外,如果你更改绑定表达式的属性,尤其是在循环中,Qt 将被迫反复重新评估它。这将导致一些性能问题。而不是这样做,用户应该遵循以下代码片段:
Item {
id: myItem
width: 400
height: 400
property int myValue: 0
Text {
anchors.fill: parent
text: myItem.myValue.toString()
}
Component.onCompleted: {
for (var i = 0; i < 1000; ++i) {
myValue += 1;
}
}
}
相反,我们可以使用一个临时变量来存储myValue的数据,然后在循环完成后将最终结果重新应用到myValue上:
Component.onCompleted: {
var temp = myValue;
for (var i = 0; i < 1000; ++i) {
temp += 1;
}
myValue = temp;
}
考虑使用锚点来定位你的 UI 项目,而不是使用绑定。使用绑定进行项目定位非常慢且效率低下,尽管它提供了最大的灵活性。
渲染和动画
当涉及到渲染图形和动画的应用程序时,良好的性能至关重要。当图形在屏幕上没有平滑动画时,用户很容易注意到性能问题。在以下示例中,我们将探讨如何进一步优化一个图形密集型的 Qt Quick 应用程序。
如何做到这一点…
要了解如何在 QML 中渲染动画,请参考以下示例:
- 创建一个
tux.png并将其添加到项目的资源中:

图 14.12 – 将 main.qml 和 tux.png 包含到你的项目资源中
-
打开
650x650。我们还将向window项目添加id并将其命名为window:Window { id: window visible: true width: 650 height: 650 -
在
window项目内部添加以下代码:property int frame: 0; onAfterRendering: { frame++; } Timer { id: timer interval: 1000 running: true repeat: true onTriggered: { frame = 0; } } -
紧接着,在下面添加
Repeater和Image:Repeater { model: 10 delegate: Image { id: tux source: "tux.png" sourceSize.width: 50 sourceSize.height: 60 width: 50 height: 60 smooth: false antialiasing: false asynchronous: true -
我们将继续添加以下代码:
property double startX: Math.random() * 600; property double startY: Math.random() * 600; property double endX: Math.random() * 600; property double endY: Math.random() * 600; property double speed: Math.random() * 3000 + 1000; RotationAnimation on rotation{ loops: Animation.Infinite from: 0 to: 360 duration: Math.random() * 3000 + 1000; } -
完成上述操作后,在之前的代码下面添加以下代码:
SequentialAnimation { running: true loops: Animation.Infinite ParallelAnimation { NumberAnimation { target: tux property: "x" from: startX to: endX duration: speed easing.type: Easing.InOutQuad } -
上一段代码动画化了图像的
x属性。我们需要另一个NumberAnimation属性来动画化y属性:NumberAnimation { target: tux property: "y" from: startY to: endY duration: speed easing.type: Easing.InOutQuad } } -
之后,我们重复整个
ParallelAnimation的代码,但这次我们将from和to的值交换,如下所示:ParallelAnimation { NumberAnimation { target: tux property: "x" from: endX to: startX duration: speed easing.type: Easing.InOutQuad } -
对于
y属性的NumberAnimation也是如此:NumberAnimation { target: tux property: "y" from: endY to: startY duration: speed easing.type: Easing.InOutQuad } } -
然后,我们添加一个
Text项目来显示我们应用程序的帧率:Text { property int frame: 0 color: "red" text: "FPS: 0 fps" x: 20 y: 20 font.pointSize: 20 -
让我们在
Text下面添加Timer并更新帧率以每秒显示一次:Timer { id: fpsTimer repeat: true interval: 1000 running: true onTriggered: { parent.text = "FPS: " + frame + " fps" } } } -
如果我们现在构建并运行程序,我们将能够看到几只企鹅在屏幕上以稳定的 60 fps 移动:

图 14.13 – 10 只企鹅在窗口周围漂浮
- 让我们回到我们的代码,将
Repeater项目的model属性更改为10000。重新构建并运行程序;你应该会看到你的窗口充满了移动的企鹅,并且帧率显著下降到大约 39 fps,考虑到企鹅的数量,这并不太糟糕:

图 14.14 – 10,000 只企鹅在窗口周围漂浮
-
接下来,让我们回到我们的源代码,并注释掉两个
sourceSize属性。我们还设置了平滑和抗锯齿属性为false,同时将异步属性设置为false:Image { id: tux source: "tux.png" //sourceSize.width: 50 //sourceSize.height: 60 width: 50 height: 60 smooth: true antialiasing: false asynchronous: false -
让我们再次构建并运行程序。这次,帧率略有下降至 32 fps,但企鹅看起来更加平滑,质量也更好,即使在移动时也是如此:

图 14.15 – 现在我们的企鹅看起来更加平滑,而且没有减慢太多
它是如何工作的...
驱动 Qt Quick 应用程序的 QML 引擎在屏幕上渲染动画图形时非常优化且强大。然而,我们仍然可以遵循一些提示来使其更快。
尝试使用 Qt 6 提供的内置功能,而不是实现自己的功能,例如Repeater、NumberAnimation和SequentialAnimation。这是因为 Qt 6 开发者已经投入了大量努力来优化这些功能,以便你不必这样做。
sourceSize属性告诉 Qt 在将其加载到内存之前调整图像大小,这样大图像就不会使用比必要的更多内存。
当启用时,平滑属性告诉 Qt 在缩放或从其自然大小转换图像时过滤图像,使其看起来更平滑。如果图像以与sourceSize值相同的分辨率渲染,则此属性不会产生任何差异。此属性将影响某些较旧硬件上应用程序的性能。
抗锯齿属性告诉 Qt 移除图像边缘的锯齿状伪影,使其看起来更平滑。此属性也会影响程序的性能。
异步属性告诉 Qt 在一个低优先级的线程中加载图像,这意味着当加载大图像文件时,你的程序不会停滞。
我们使用帧率来表示我们程序的性能。由于onAfterRendering总是在每一帧被调用,因此我们可以在每一帧渲染时累积帧变量。然后,我们使用Timer每秒重置帧值。
最后,我们使用Text项目在屏幕上显示值。


浙公网安备 33010602011771号