C---Qt6-跨平台开发-全-

C++ Qt6 跨平台开发(全)

原文:zh.annas-archive.org/md5/E50463D8611423ACF3F047AAA5FD4529

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Qt 是一个跨平台应用程序开发框架,旨在为桌面、嵌入式和移动平台创建出色的软件应用程序和令人惊叹的用户界面。它为开发人员提供了一套出色的工具,用于设计和构建出色的应用程序,而无需担心平台依赖性。

在本书中,我们将专注于 Qt 6,这是 Qt 框架的最新版本。本书将帮助您创建用户友好且功能性的图形用户界面。您还将通过提供外观和感觉在不同平台上保持一致的更美观的应用程序,获得与竞争对手的优势。

想要构建跨平台应用程序并拥有交互式 GUI 的开发人员将能够通过这本实用指南将他们的知识付诸实践。本书提供了一种实践方法来实现概念和相关机制,让您的应用程序可以立即投入运行。您还将获得关键概念的解释,并通过示例获得完整的学习体验。

您将首先探索不同平台上的 Qt 框架。您将学习如何在不同平台上配置 Qt,了解不同的 Qt 模块,学习核心概念,以及了解它们如何用于构建高效的 GUI 应用程序。您将能够在不同平台上构建、运行、测试和部署应用程序。您还将学习如何自定义应用程序的外观和感觉,并开发一个支持翻译的应用程序。除了学习完整的应用程序过程,本书还将帮助您识别瓶颈,并了解如何解决这些问题以增强应用程序的性能。

通过本书的学习,您将能够在不同平台上构建和部署自己的 Qt 应用程序。

这本书适合谁

本书旨在面向希望构建基于 GUI 的应用程序的开发人员和程序员。它也适用于之前使用过 C++编码的软件工程师。入门门槛并不高,所以如果你了解基本的 C++和面向对象编程的概念,那么你就可以踏上这段旅程。

此外,本书还可以帮助中级 Qt 开发人员,他们希望在其他平台上构建和部署应用程序。希望开始学习 Qt 编程的工作专业人士或学生,以及对 Qt 新手程序员,都会发现本书很有用。

本书涵盖内容

第一章介绍 Qt 6,将向您介绍 Qt,并描述如何在计算机上设置 Qt。通过本章的学习,读者将能够从源代码构建 Qt,并在他们选择的平台上开始学习。

第二章介绍 Qt Creator,向您介绍了 Qt Creator 集成开发环境及其用户界面。本章还将教您如何在 Qt Creator 中创建和管理项目。您将学习如何使用 Qt Creator 开发一个简单的“Hello World”应用程序,并了解不同的快捷键和实用技巧。

第三章使用 Qt Widgets 进行 GUI 设计,探讨了 Qt Widgets 模块。在这里,您将学习创建 GUI 所需的各种小部件。您还将了解布局、Qt Designer,并学习如何创建自定义控件。本章将帮助您使用 Qt 开发您的第一个 GUI 应用程序。

第四章Qt Quick 和 QML,介绍了 Qt Quick 和 QML 的基础知识,Qt Quick Controls,Qt Quick Designer,Qt Quick Layouts 和基本的 QML 脚本。在本章中,您将学习如何使用 Qt Quick 控件以及如何将 C++代码与 QML 集成。通过本章的学习,您将能够使用 QML 创建具有流畅用户界面的现代应用程序。

第五章, 跨平台开发,探讨了使用 Qt 进行跨平台开发。您将了解 Qt Creator 中的不同设置。在本章中,您将能够在您喜爱的桌面和移动平台上运行示例应用程序。

第六章, 信号和槽,深入介绍了信号和槽机制。您将能够在不同的 C++类之间以及在 C++和 QML 之间进行通信。您还将了解事件、事件过滤器和事件循环。

第七章, 模型视图编程,介绍了 Qt 中的模型/视图架构及其核心概念。在这里,您将能够编写自定义模型和委托。您可以使用这些内容在基于 Qt Widget 或 Qt Quick 的 GUI 应用程序上显示所需的信息。

第八章, 图形和动画,介绍了 2D 图形和动画的概念。您将学习如何使用绘图 API 在屏幕上绘制不同的形状。我们还将讨论使用 Qt 的图形视图框架和场景图表示图形数据的可能性。本章将指导您创建引人注目的用户界面动画。本章还涉及状态机框架。

第九章, 测试和调试,探讨了 Qt 应用程序的不同调试技术。您将在本章中了解单元测试和 Qt 测试框架。我们还将讨论如何在 Qt 测试中使用 Google 测试框架,并了解可用的 Qt 工具和 GUI 特定的测试技术。

第十章, 部署 Qt 应用程序,讨论了软件部署的重要性。您将学习如何在各种平台上部署 Qt 应用程序,包括桌面和移动平台。您将了解可用的部署工具和创建安装程序包的步骤。

第十一章, 国际化,介绍了国际化。Qt 提供了优秀的支持,可以将 Qt Widgets 和 Qt Quick 应用程序翻译成本地语言。在本章中,您将学习如何制作支持多语言的应用程序。您还将了解内置工具和制作翻译感知应用程序的各种考虑因素。

第十二章, 性能考虑,介绍了性能优化技术以及如何在 Qt 编程环境中应用这些技术。在这里,我们将讨论不同的性能分析工具,以诊断性能问题,特别集中在 Windows 上可用的工具。在本章中,您将学习如何使用 QML Profiler 对性能进行分析并对代码进行基准测试。本章还将帮助您编写高性能优化的 QML 代码。

为了充分利用本书

我们将只使用开源软件,因此您不需要购买任何许可证。随着我们逐渐进行每一章,我们将介绍安装程序和详细信息。要安装所需的软件,您需要一个功能齐全的互联网连接和台式电脑或笔记本电脑。除此之外,在开始本书之前,没有特定的软件要求。

重要提示

对于 Android 设置,您将需要以下内容:

OpenJDK 8 (JDK-8.0.275.1)

Android SDK 4.0

NDK r21 (21.3.6528147)

Clang 工具链

Android OpenSSL

如果您使用本书的数字版本,我们建议您自己输入代码或通过 GitHub 存储库访问代码(链接在下一节中提供)。这样做将有助于避免与复制和粘贴代码相关的潜在错误。

所有代码示例都是在 Windows 平台上使用 Qt 6 进行测试的。如果您使用 Qt 5,可能会出现失败。但是,它们也应该适用于将来的版本发布。请确保您安装到计算机上的版本至少是 Qt 6.0.0 或更高版本,以便代码与本书兼容。

下载示例代码文件

您可以从 GitHub 上下载本书的示例代码文件,网址为github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp。此外,您还可以在上述 GitHub 链接中找到一些具有 C++17 特性的额外示例。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还提供了来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在此处下载:static.packt-cdn.com/downloads/9781800204584_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

文本中的代码:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:"通常,exec() 方法用于显示对话框。"

代码块设置如下:


    QMessageBox messageBox;
    messageBox.setText("This is a simple QMessageBox.");
    messageBox.exec(); 

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:


    QMessageBox messageBox;
    messageBox.setText("This is a simple QMessageBox.");
    messageBox.exec(); 

任何命令行输入或输出都以以下形式书写:

> lrelease *.ts

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单中的单词或对话框中的单词会以此形式出现在文本中。例如:"最后一步是构建并运行应用程序。在 Qt Creator 中点击运行按钮。"

提示或重要说明

会显示为这样。

第一部分:基础知识

在本节中,您将学习框架的基础知识和演变,以及如何在不同平台上安装 Qt。在本节中,您将了解 Qt 的演变。然后,我们将继续使用最新版本的 Qt,即 Qt 6,构建我们的第一个示例程序。您将学习使用 Qt Creator 集成开发环境。本节将向您介绍 Qt Widgets、Qt Designer 和创建自定义控件。您将学习样式表、QSS 文件和主题。本节还将向您介绍 Qt Quick 和 QML。

本节包括以下章节:

  • 第一章, Qt 6 简介

  • 第二章, Qt Creator 简介

  • 第三章, 使用 Qt Widgets 进行 GUI 设计

  • 第四章, Qt Quick 和 QML

第一章:介绍 Qt 6

Qt(发音为cute,而不是que-tee)是一个跨平台应用程序开发框架,旨在为桌面、嵌入式和移动平台创建具有统一用户界面UI)的优秀软件应用程序。它为开发人员提供了一套强大的工具,设计和构建出色的应用程序,而无需担心平台依赖性。在本章中,您将学习有关该框架的基础知识、其历史以及如何在不同平台上安装 Qt。您将了解 Qt 是什么,以及为什么使用它是有益的。在本章结束时,您将能够安装 Qt 并在您选择的平台上开始使用。

在本章中,我们将涵盖以下主要主题:

  • 介绍 Qt

  • 使用 Qt 的原因

  • 下载和安装 Qt

  • 从源代码构建 Qt 6

技术要求

要开始使用,您应该有一台运行 Windows、Linux 或 macOS 的台式机或笔记本电脑。请使用更新的 Windows 10 或 Ubuntu 20.04 长期支持LTS)。或者,使用最新版本的 macOS(高于 macOS 10.14),如 macOS Catalina。

为了使您的集成开发环境IDE)运行顺畅,您的系统应至少配备英特尔酷睿 i5 处理器,以及至少 4GB随机存取存储器RAM)。

您需要有一个活动的互联网连接来下载和安装 Qt。作为先决条件,您还应该熟悉 C++,因为 Qt 需要 C++编程知识。

介绍 Qt

Qt 是一个用于桌面、嵌入式和移动平台的跨平台软件开发框架。它遵循少写代码,创造更多,随处部署的理念。它支持 Windows、Linux、macOS、VxWorks、QNX、Android、iOS 等平台。该软件还支持来自 NXP、Renesas 和 STMicroelectronics 的多个微控制器单元MCUs),这些单元在裸机或 FreeRTOS 上运行。

Qt 诞生的初衷是为了提供统一的图形用户界面GUI),在不同平台上具有相同的外观、感觉和功能。Qt 通过提供一个框架来编写代码一次,并确保它在其他平台上以最少或没有修改的方式运行来实现这一目标。它不是一种编程语言,而是用 C++编写的框架。Qt 框架和工具在开源和商业许可下都有双重许可。

Qt 使用模块化方法将相关功能组合在一起。Qt Essentials 是所有平台上 Qt 的基础。这些模块是通用的,对大多数基于 Qt 的应用程序都很有用。基本模块可供开源使用。Qt Essentials 模块的示例包括 Qt Core、Qt GUI、Qt QML、Qt Widgets 等。还有一些特定用途的附加模块,提供特定功能并带有特定的许可义务。附加模块的示例包括 Qt 3D、Qt Bluetooth、Qt Charts、Qt Data Visualization 等。此外,还有增值模块,如 Qt Automotive Suite、Qt for Device Creation 和 Qt for MCUs 等,可在商业许可下使用。

要了解更多关于不同 Qt 模块的信息,请访问doc.qt.io/qt-6/qtmodules.html

Qt 于 1995 年发布供公众使用。自那时以来,有许多改进和重大变化。Qt 6 是 Qt 的新主要版本。它的主要目标是为 2020 年及以后的要求做好准备,删除过时的模块,并更易于维护。基于这一重点,Qt 6 中存在着一些架构变化,可能会破坏与早期版本的某些程度的向后兼容性。

Qt 6 中的一些基本修改如下:

  • 引入强类型

  • JavaScript 作为Qt 建模语言QML)的可选功能

  • 删除 QML 版本

  • 在 QObject 和 QML 之间删除重复的数据结构

  • 避免创建运行时数据结构

  • 将 QML 编译成高效的 C++和本机代码

  • 支持隐藏实现细节

  • 更好地集成工具

既然我们已经介绍了基础知识,让我们来看看使用 Qt 的主要原因…

使用 Qt 的原因

Qt 是一个模块化的跨平台应用程序开发框架。关于 Qt 最大的误解是很多人认为它是一个 GUI 框架。然而,Qt 远不止是一个 GUI 框架。它不仅包括一个 GUI 模块,还包括一组模块,使应用程序开发更快速、更容易在各种平台上扩展。使用 Qt 的最大好处是它能够为各种平台提供可移植性。以下是使用 Qt 的一些优势:

  • 您可以为您的客户创建令人难以置信的用户体验,并通过 Qt 提升您的公司品牌。

  • 跨平台开发既节省时间又节省金钱。您可以使用相同的代码库针对多个平台进行开发。

  • Qt 以使 C++易于使用和访问而闻名。使用 Qt,开发人员可以轻松创建具有流畅 UI 的高性能、可扩展的应用程序。

  • 由于开源模型,该框架是未来的保障,同时拥有一个伟大的生态系统。

  • 它进一步支持不同的编程语言,是一个非常灵活和可靠的框架。因此,有很多大公司如 Adobe、微软、三星、AMD、惠普、飞利浦和 MathWorks 都在他们的应用程序中使用 Qt。许多开源项目如 VLC(以前称为 VideoLAN 客户端)、Open Broadcaster Software(OBS)和 WPS Office(其中 WPS 代表 Writer、Presentation 和 Spreadsheets)也是基于 Qt 构建的。

Qt 的核心价值如下所述:

  • 跨平台性质

  • 高度可扩展

  • 非常易于使用

  • 内置世界一流的应用程序编程接口(API)、工具和文档

  • 可维护、稳定和兼容

  • 庞大的用户社区

无论您是业余爱好者、学生还是为公司工作,Qt 都提供了很大的灵活性,可以根据您的需求使用其模块。许多大学正在将 Qt 作为他们的课程科目之一。因此,Qt 是程序员开始构建具有现成功能的新应用程序的绝佳选择。让我们从在您的计算机上下载并安装 Qt 6 开始。

下载和安装 Qt

有多种方式可以在您的系统上安装 Qt 框架和工具。您可以从 Qt 网站下载在线或离线安装程序,也可以自己构建源代码包。Qt 建议首次安装使用在线安装程序,以及使用 Qt Maintenance Tool 进行后续安装的修改。

安装程序允许您下载和安装以下组件:

  • Qt 库

  • Qt Creator IDE

  • 文档和示例

  • Qt 源代码

  • 附加模块

在线安装程序允许您根据所选择的许可证选择 Qt 的开源或商业版本、工具和附加模块进行安装。在线安装程序不包含 Qt 组件,但它是一个下载客户端,用于下载所有相关文件。下载完成后,您可以进行安装。您需要一个 Qt 帐户来下载和安装 Qt。商业 Qt 的评估版本为您提供免费试用期访问权限,包括所有商业套餐和官方 Qt 支持。安装程序要求您使用 Qt 帐户登录。如果您没有 Qt 帐户,可以在安装过程中注册。安装程序从 Qt 服务器获取附加到帐户的许可证,并根据您的许可证列出模块。如果您是 Qt 的新手,我们建议您从开源版本开始。

离线安装程序是一个特定于平台的软件包,其中包含了平台相关的所有 Qt 模块和附加组件。由于官方政策的变化,自 Qt 5.15 起不再提供开源离线安装程序。如果您有商业许可证,那么您可以在安装过程中提供凭据。您可以在您的Qt 帐户Web 门户中找到您的许可密钥。

您可以从以下链接下载:

重要提示

Qt 公司为用户提供了双重许可选项。作为初学者,您可以从开源许可证开始探索 Qt。如果您为公司工作,那么请与您的经理或信息技术IT)或法律团队讨论获取商业许可证或了解法律义务。您可以在www.qt.io/licensing/了解有关 Qt 许可的更多信息。

下载 Qt

让我们开始将 Qt 下载到您的计算机上,步骤如下:

  1. 首先,访问www.qt.io/download下载页面。

  2. 单击右上角的“下载。尝试。购买。”按钮。您将在此处看到不同的下载选项。

  3. 如果您想尝试商业版本,请单击“尝试 Qt”部分。如果您已经有 Qt 帐户,那么您可以在“现有客户”部分登录帐户。

  4. 考虑到您是 Qt 的新手,我们将从开源版本开始。单击“转到开源”按钮,如下截图所示:图 1.1 - Qt 网站下载选项

图 1.1 - Qt 网站下载选项

  1. 在下一个屏幕上,您将找到“下载 Qt 在线安装程序”按钮。单击它以继续到下载链接。

  2. 网页将自动从浏览器中检测到底层平台详细信息,并向您显示“下载”文件夹。

接下来,让我们从 Windows 平台上的安装过程开始。

在 Windows 上安装 Qt

现在,让我们在 Windows 上开始安装过程!请按照以下步骤进行:

  1. 您将在下载文件夹中找到一个名为qt-unified-windows-x86-%VERSION%-online.exe的文件。双击可执行文件,您将看到一个“欢迎”屏幕。

  2. 单击“下一步”按钮,将出现凭据屏幕,要求您使用 Qt 帐户登录。如果您没有帐户,那么您可以在同一页上注册,如下截图所示:图 1.2 - 安装程序的登录屏幕

图 1.2 - 安装程序的登录屏幕

  1. 在下一个屏幕上,您将看到与开源使用义务协议相关的选项。如果您使用商业许可证进行安装,您将不会看到此屏幕。单击第一个复选框,表示我已阅读并批准使用开源 Qt 的义务,并承认您不会将 Qt 用于商业目的。确保您阅读了协议中提到的条款和条件!然后,单击“下一步”按钮。

  2. 下一个屏幕将为您提供与在 Qt Creator 中跟踪和共享匿名数据相关的选项。您可以根据自己的喜好允许或禁用这些选项。然后,单击“下一步”按钮,以继续到下一个屏幕。

  3. 在下一个屏幕上,您可以指定安装路径。您可以继续使用默认路径,或者如果默认驱动器上没有足够的空间,可以更改为任何其他路径。您还可以选择是否要通过选择底部的复选框选项将常见文件类型与 Qt Creator 关联起来。单击“下一步”按钮。

  4. 接下来,您将看到一个列表,您可以在其中选择要在系统上安装的 Qt 版本。您可以简单地使用默认选项。如果您不需要某些组件,则可以取消选择它们以减小下载的大小。您随时可以使用维护工具更新 Qt 组件。要完成安装过程,请点击下一步按钮。组件选择屏幕如下所示:图 1.3 - 安装程序的组件选择屏幕

图 1.3 - 安装程序的组件选择屏幕

  1. 在下一个屏幕上,您将看到许可协议。点击第一个单选按钮,上面写着我已阅读并同意许可协议中包含的条款。再次确保您阅读了许可协议中提到的条款和条件,然后点击下一步按钮。

  2. 在下一个屏幕上,您可以在 Windows 上创建开始菜单快捷方式。此屏幕将不适用于其他平台。完成后,点击下一步按钮。

  3. 现在,Qt 已经准备好在您的系统中安装。确保您有可用的互联网连接和数据余额。点击安装按钮开始安装。下载过程将根据您的互联网速度而花费时间。一旦所需文件下载完成,安装程序将自动将它们安装在先前选择的路径中。

  4. 安装完成后,安装程序将为维护工具创建一个条目,以后将帮助您对库进行更改。点击下一步按钮进入安装程序的最后一个屏幕。

  5. 为了完成安装过程,点击完成按钮。如果您留下了启动 Qt Creator复选框选中,则 Qt Creator 将被启动。我们将在下一章中更详细地讨论这个问题。现在,Qt 已经准备好在您的 Windows 机器上使用。点击完成按钮退出向导。

在 Linux 上安装 Qt

现在,让我们在最新的LTS 版本 Linux上安装 Qt 框架,例如 Ubuntu 20.04、CentOS 8.1 或 openSUSE 15.1。我们将专注于最受欢迎的 Linux 发行版 Ubuntu。您可以按照之前提到的相同步骤从 Qt 网站下载在线安装程序。

在 Ubuntu 上,您将获得一个安装程序文件,例如qt-unified-linux-x64-%VERSION%-online.run,其中%VERSION%是最新版本,例如:qt-unified-linux-x86-4.0.1-1-online.run

  1. 在执行下载的文件之前,您可能需要给予写入权限。要做到这一点,打开终端并运行以下命令:
$ chmod +x qt-unified-linux-x64-%VERSION%-online.run
  1. 您可以通过双击下载的安装程序文件来开始安装过程。安装需要超级用户访问权限。在安装过程中,您可能需要在授权对话框中输入密码。您也可以从终端运行安装程序,如下所示:
$ ./qt-unified-linux-x64-%VERSION%-online.run
  1. 您将看到与 Windows 平台相似的屏幕。除了操作系统OS)特定的标题栏更改外,所有屏幕在 Ubuntu 或类似的 Linux 版本中的安装过程中保持不变。

在撰写本书时,由于各自的维护者已经退出,Qt 6 在 Ubuntu 或 Debian 上没有可用的软件包。因此,您可能无法从终端获取 Qt 6 软件包。

在 macOS 上安装 Qt

如果您是 macOS 用户,您也可以按照之前讨论的方式进行安装。您可以按照之前提到的相同步骤从 Qt 网站下载在线安装程序。

您将获得一个安装程序文件,例如qt-unified-mac-x64-%VERSION%-online.dmg,其中%VERSION%是最新版本(例如qt-unified-mac-x64-4.0.1-1-online.dmg)。

Qt 依赖于 Xcode。要在 Mac 上安装 Qt,您需要在计算机上安装 Xcode,否则它将拒绝安装。如果您是苹果开发人员,则您的 Mac 可能已安装 Xcode。如果您的计算机上没有安装 Xcode,则可以继续安装 Xcode 的命令行工具而不是 Xcode。这将节省计算机上的时间和存储空间。

  1. 首先,在终端上键入以下命令:
$ xcode-select --install    
  1. 如果终端显示以下输出,则您的系统已准备好进行下一步操作:
xcode-select: error: command line tools are already installed, use
"Software Update" to install updates
  1. 下一步是安装 Qt 框架。双击安装程序文件以启动安装界面。

  2. 如果安装程序仍然抱怨 Xcode 未安装,则继续单击确定直到消息永久消失。记住安装路径。安装完成后,您就可以在计算机上使用 Qt 了。

在 macOS 上有关 Qt 的进一步说明可以在以下链接找到:

doc.qt.io/qt-6/macos.html

更新或删除 Qt

安装 Qt 后,您可以使用安装目录下的维护工具修改组件,包括更新、添加和删除组件。对于所有桌面平台,目录结构保持不变。安装目录包含文件夹和文件,如下屏幕截图所示(在 Windows 上):

图 1.4 - 安装文件夹中的维护工具

图 1.4 - 安装文件夹中的维护工具

让我们开始维护过程!您可以使用维护工具添加、删除和更新模块。请按照以下步骤进行:

  1. 单击MaintenanceTool.exe可执行文件以启动维护界面。单击下一步按钮,将出现凭据屏幕,要求您使用 Qt 帐户登录。登录详细信息将从上次登录会话中预填。您可以单击下一步以添加或更新组件,或选择仅卸载复选框以从系统中删除 Qt。以下屏幕截图显示了凭据屏幕的外观:图 1.5 - 维护工具的欢迎屏幕

图 1.5 - 维护工具的欢迎屏幕

  1. 登录后,工具将向您提供添加、删除或更新组件的选项,如下屏幕截图所示。单击下一步按钮继续:图 1.6 - 维护工具的设置屏幕

图 1.6 - 维护工具的设置屏幕

  1. 在下一个屏幕上,您可以从最新版本或存档版本中选择新组件。您可以单击筛选器按钮根据需要筛选版本。您还可以从组件列表中添加新的特定于平台的组件,例如 Android。如果组件已存在并取消选中它,则在更新期间将从桌面中删除它。选择组件后,单击下一步按钮。以下屏幕截图显示了组件选择屏幕的外观:图 1.7 - 组件选择屏幕

图 1.7 - 组件选择屏幕

  1. 然后您将遇到更新屏幕。此屏幕将告诉您安装需要多少存储空间。如果存储空间不足,则可以返回并删除一些现有组件。单击更新按钮开始该过程,如下屏幕截图所示:图 1.8 - 维护工具的准备更新屏幕

图 1.8 - 维护工具的准备更新屏幕

  1. 您可以通过单击取消按钮中止更新安装过程。Qt 会在中止安装过程之前警告您并要求确认,如下截图所示。一旦过程中止,单击下一步按钮退出向导:图 1.9 – 取消对话框

图 1.9 – 取消对话框

  1. 再次启动维护工具,以从最新版本更新现有组件。您可以单击退出按钮退出维护工具。请等待安装程序从远程存储库获取元信息。单击下一步按钮查看可用组件。更新选项如下截图所示:图 1.10 – 维护工具中的更新选项

图 1.10 – 维护工具中的更新选项

  1. 接下来,您可以从复选框中选择要更新的组件。您可以选择全部更新,也可以选择性更新。安装程序将显示更新所需的存储空间,如下截图所示。单击下一步进入更新屏幕并开始更新。然后,在下一个屏幕上,单击更新按钮下载更新包:图 1.11 – 可用于更新的组件

图 1.11 – 可用于更新的组件

  1. 安装完成后,安装程序会为维护工具创建条目,以帮助您稍后对库进行更改。如下截图所示。单击下一步按钮进入安装程序的最后一个屏幕:图 1.12 – 维护工具中的更新完成屏幕

图 1.12 – 维护工具中的更新完成屏幕

  1. 在最后一个屏幕上,您将看到重新启动完成按钮。单击完成按钮退出 Qt 向导。

  2. 同样,您可以重新启动或启动维护工具,并选择删除所有组件单选按钮。单击下一步按钮开始卸载过程,如下截图所示:

图 1.13 – 维护工具中的删除选项

图 1.13 – 维护工具中的删除选项

请注意,单击卸载按钮后,所有 Qt 组件将从系统中删除;如果您想再次使用它们,您将需要重新安装 Qt。如果您不打算从系统中删除 Qt 组件,请单击取消,如下截图所示。如果您打算删除现有版本并使用更新版本的 Qt,则选择添加或删除组件选项,如前所述。这将删除旧的 Qt 模块并释放磁盘空间:

图 1.14 – 维护工具中的卸载屏幕

图 1.14 – 维护工具中的卸载屏幕

在本节中,我们了解了通过维护工具修改现有的 Qt 安装。现在,让我们学习如何从源代码构建和安装 Qt。

从源代码构建 Qt 6

如果您想自己构建框架和工具,或者尝试最新的未发布代码,那么您可以从源代码构建 Qt。如果您要从源代码开发特定的 Qt 版本,那么可以从官方发布链接下载 Qt 6 源代码,如下所示:download.qt.io/official_releases/qt/6.0/.

如果您是商业客户,那么可以从 Qt 帐户门户下载源代码包。平台特定的构建说明将在接下来的小节中讨论。

您还可以从 GitHub 存储库克隆,并检出所需的分支。在撰写本书时,Qt 6 分支仍位于 Qt 5 超级模块内。您可以从以下链接克隆存储库:git://code.qt.io/qt/qt5.git

qt5.git存储库可能会在未来更名为qt.git以便维护。请参考QTQAINFRA-4200 Qt 票。有关如何从 Git 构建 Qt 的详细说明,请访问以下链接:wiki.qt.io/Building_Qt_6_from_Git

确保在您的机器上安装最新版本的 Git、Perl 和 Python。在进入下一节的特定于平台的说明之前,请确保有一个可用的 C++编译器。

在 Windows 上从源代码安装 Qt

要在 Windows 上从源代码安装 Qt 6,请按照以下步骤进行:

  1. 首先,从 Git 或之前提到的开源下载链接下载源代码。您将得到一个压缩文件,名称为qt-everywhere-src--%VERSION%.zip,其中%VERSION%是最新版本(例如qt-everywhere-src-6.0.3.zip)。请注意,后缀如-everywhere-src-可能会在未来被移除。

  2. 下载源代码存档后,将其解压缩到所需的目录,例如C:\Qt6\src

  3. 在下一步中,使用支持的编译器和所需的构建工具配置构建环境。

  4. 然后,将CMakeninjaPerlPython的相应安装目录添加到您的PATH环境变量中。

  5. 下一步是构建 Qt 库。要为您的机器配置 Qt 库,请在源目录中运行configure.bat脚本。

  6. 在此步骤中,通过在命令提示符中输入以下命令来构建 Qt:

>cmake --build . –parallel
  1. 接下来,在命令提示符中输入以下命令以在您的机器上安装 Qt:
>cmake --install .

您的 Windows 机器现在已经准备好使用 Qt。

要了解更多关于配置选项的信息,请访问以下链接:

doc.qt.io/qt-6/configure-options.html

详细的构建说明可以在以下链接找到:

doc.qt.io/qt-6/windows-building.html

在 Linux 上从源代码安装 Qt

要在 Linux 发行版上构建源包,请在终端上运行以下一组指令:

  1. 首先,从 Git 或之前提到的开源下载链接下载源代码。您将得到一个压缩文件,名称为qt-everywhere-src--%VERSION%.tar.xz,其中%VERSION%是最新版本(例如qt-everywhere-src-6.0.3.tar.xz)。请注意,后缀如-everywhere-src-可能会在未来被移除。

  2. 下载源代码存档后,解压缩存档并解压到所需的目录,例如/qt6,如下面的代码片段所示:

$ cd /qt6
$ tar xvf qt-everywhere-opensource-src-%VERSION%.tar.xz 
$ cd /qt6/qt-everywhere-opensource-src-%VERSION%
  1. 要为您的机器配置 Qt 库,请在源目录中运行./configure脚本,如下面的代码片段所示:
$ ./configure
  1. 要创建库并编译所有示例、工具和教程,请输入以下命令:
$ cmake --build . --parallel
$ cmake --install .
  1. 下一步是设置环境变量。在.profile(如果您的 shell 是bashkshzshsh),添加以下代码行:
PATH=/usr/local/Qt-%VERSION%/bin:$PATH
export PATH

.login(如果您的 shell 是cshtcsh),添加以下代码行:

setenv PATH /usr/local/Qt-%VERSION%/bin:$PATH

如果您使用不同的 shell,请相应修改您的环境变量。Qt 现在已经准备好在您的 Linux 机器上使用。

Linux/X11 的详细构建说明可以在以下链接找到:

doc.qt.io/qt-6/linux-building.html

在 macOS 上从源代码安装 Qt

Qt 依赖于Xcode。要在 Mac 上安装 Qt,您需要在您的机器上安装 Xcode。如果您的机器上没有安装 Xcode,则可以继续安装 Xcode 的命令行工具

  1. 首先,在终端上输入以下命令:
$ xcode-select --install    
  1. 如果终端显示以下输出,则您的系统已准备好进行下一步:
xcode-select: error: command line tools are already installed, use
"Software Update" to install updates
  1. 要构建源包,请在终端上运行以下一组指令:
$ cd /qt6
$ tar xvf qt-everywhere-opensource-src-%VERSION%.tar          
$ cd /qt6/qt-everywhere-opensource-src-%VERSION%
  1. 要为您的 Mac 配置 Qt 库,请在源目录中运行./configure脚本,如下面的代码片段所示:
$ ./configure  
  1. 创建库,请运行make命令,如下所示:
$ make
  1. 如果-prefix在构建目录之外,则输入以下行以安装库:
$ sudo make -j1 install
  1. 下一步是设置环境变量。在.profile(如果您的 shell 是bash),添加以下代码行:
PATH=/usr/local/Qt-%VERSION%/bin:$PATH
export PATH

.login(如果您的 shell 是cshtcsh),添加以下代码行:

setenv PATH /usr/local/Qt-%VERSION%/bin:$PATH

您的计算机现在已准备好进行 Qt 编程。

macOS 的详细构建说明可以在这里找到:

doc.qt.io/qt-6/macos-building.html

在本节中,我们学习了如何在您喜爱的平台上从源代码安装 Qt。现在,让我们总结一下我们的学习。

摘要

本章介绍了 Qt 框架的基础知识以及它的用途。在这里,我们讨论了 Qt 的历史、不同的模块以及使用 Qt 的优势。我们还了解了不同的安装方法和许可义务,为不同的桌面平台上的 Qt 提供了逐步安装过程。现在,您的计算机已准备好探索 Qt。

在下一章中,我们将讨论 Qt Creator IDE。您将了解 IDE 的用户界面、不同的配置以及如何将其用于 Qt 项目。

第二章:Qt Creator 简介

Qt Creator是 Qt 自己的集成开发环境IDE),用于跨平台应用程序开发。在本章中,您将学习 Qt Creator IDE 的基础知识,以及 IDE 的用户界面UI)。我们还将看看如何在 Qt Creator 中创建和管理项目。Qt 的这个模块涵盖了使用 Qt Creator 开发简单 Qt 应用程序的内容,包括开发人员的快捷方式和实用技巧。

更具体地说,我们将涵盖以下主要主题:

  • Qt Creator 基础知识

  • 配置 IDE 和管理项目

  • 用户界面

  • 编写一个示例应用程序

  • 高级选项

Qt Creator 可以通过许多有用的工具和示例使您更轻松地学习 Qt。您只需要最少的 IDE 知识即可开始。在本章结束时,您将熟悉 Qt Creator 的使用。您还将能够在您喜爱的桌面平台上构建和运行您的第一个 Qt 应用程序,并了解 IDE 中可用的高级选项,您将能够根据自己的喜好进行自定义。

技术要求

本章的技术要求与第一章**,Qt 6 简介相同。您将需要最新的 Qt 版本,即 Qt 6.0.0 MinGW 64 位,Qt Creator 4.13.0 或更高版本,以及 Windows 10、Ubuntu 20.04 LTS 或最新版本的 macOS(至少高于 macOS 10.13),如 macOS Catalina。Qt 支持较早版本的操作系统,如 Windows 8.1 或 Ubuntu 18.04。但是,我们建议您升级到首选操作系统的最新版本,以确保顺畅运行。在本章中,我们使用了来自 Windows 10 平台的屏幕截图。

探索 Qt Creator UI

Qt Creator 是由 Qt 公司生产的 IDE。它集成了多个工具,包括代码编辑器、图形用户界面GUI)设计器、编译器、调试器、Qt Designer、Qt Quick Designer 和 Qt Assistant 等。

Qt Designer 帮助设计基于小部件的 GUI,而 Qt Quick Designer 提供了一个 UI,可以在设计模式下创建和编辑基于 QML 的 GUI。Qt Assistant 是一个集成的文档查看器,可以通过按下F1键打开与给定 Qt 类或函数相关的内容。

让我们开始启动 Qt Creator。二进制文件可以在Qt\Tools\QtCreator\bin中找到。您将看到一个类似于图 2.1所示的屏幕:

图 2.1 – Qt Creator 界面

图 2.1 – Qt Creator 界面

您可以在 UI 中看到以下 GUI 部分:

  1. IDE 菜单栏:为用户提供了一个标准的窗口位置,以找到大多数应用程序特定功能。这些功能包括创建项目、打开和关闭文件、开发工具、分析选项、帮助内容以及退出程序的方法。

  2. 模式选择器:此部分根据活动任务提供不同的模式。欢迎按钮提供打开示例、教程、最近的会话和项目的选项。编辑按钮打开代码窗口,并帮助导航项目。设计按钮根据 UI 文件的类型打开 Qt Designer 或 Qt Quick Designer。调试提供分析应用程序的选项。项目按钮帮助管理项目设置,帮助按钮用于浏览帮助内容。

  3. 套件选择器:这有助于选择活动项目配置并更改套件设置。

  4. 运行按钮:构建完成后,此按钮运行活动项目。

  5. 调试按钮:这有助于使用调试器调试活动项目。

  6. 构建按钮:用于构建活动项目。

  7. 定位器:用于从任何打开的项目中打开文件。

  8. 输出窗格:包括几个窗口,用于显示项目信息,如编译和应用程序输出。它还显示构建问题、控制台消息以及测试和搜索结果。

  9. 进度指示器:此控件显示与运行任务相关的进度。

当您第一次启动 Qt Creator 时,您还可以从菜单栏中的帮助 | UI Tour选项启动交互式 UI 导览,如图 2.2所示:

图 2.2 – Qt Creator UI 导览菜单选择

图 2.2 – Qt Creator UI 导览菜单选择

注意

如果按下Alt键,您将看到菜单标题中的下划线助记符字母。按下相应的键打开相应的上下文菜单。

在本节中,我们学习了 IDE 中的各个部分。在下一节中,我们将使用 Qt Creator IDE 构建一个简单的 Qt 应用程序。

构建一个简单的 Qt 应用程序

让我们从一个简单的Hello World项目开始。Hello World程序是一个非常简单的程序,显示Hello World!并检查 SDK 配置是否没有错误。这些项目使用最基本、非常简洁的代码。对于这个项目,我们将使用 Qt Creator 创建的项目骨架。

按照以下步骤构建您的第一个 Qt 应用程序:

  1. 要在 Qt 中创建一个新项目,请单击菜单栏上的文件菜单选项,或按下Ctrl + N。或者,您也可以单击欢迎屏幕上的+ 新建按钮来创建一个新项目,如图 2.3所示:图 2.3 – 新项目界面

图 2.3 – 新项目界面

  1. 接下来,您可以选择项目的模板。您可以创建不同类型的应用程序,包括控制台应用程序或 GUI 应用程序。您还可以创建非 Qt 项目以及库项目。在右上角的部分,您将看到一个下拉菜单,用于过滤特定于所需目标平台的模板。选择Qt Widgets 应用程序模板,然后单击选择...按钮:图 2.4 – 项目模板界面

图 2.4 – 项目模板界面

  1. 在下一步中,您将被要求选择项目名称和项目位置。您可以通过单击浏览...按钮导航到所需的项目位置。然后单击下一步按钮,进入下一个屏幕:图 2.5 – 新项目位置屏幕

图 2.5 – 新项目位置屏幕

  1. 您现在可以选择构建系统。默认情况下,将选择 Qt 自己的构建系统qmake。我们将在第六章中更多地讨论 qmake,信号和槽。点击下一步按钮,进入下一个屏幕:图 2.6 – 构建系统选择屏幕

图 2.6 – 构建系统选择屏幕

  1. 接下来,您可以指定类信息和要自动生成项目骨架的基类。如果您需要一个带有MainWindow功能的桌面应用程序,比如menubartoolbarstatusbar,那么在第三章**, 使用 Qt Widgets 进行 GUI 设计中选择QMainWindow。点击下一步按钮,进入下一个屏幕:图 2.7 – 源代码骨架生成屏幕

图 2.7 – 源代码骨架生成屏幕

  1. 在下一步中,您可以指定翻译的语言。Qt Creator 带有Qt Linguist工具,允许您将应用程序翻译成不同的语言。您现在可以跳过这一步。我们将在第十一章中讨论国际化i18n),国际化。点击下一步按钮,进入下一个屏幕:图 2.8 – 翻译文件创建屏幕

图 2.8 - 创建翻译文件屏幕

  1. 在下一步中,您可以选择一个套件来构建和运行您的项目。要构建和运行项目,至少必须激活并可选择一个套件。如果您期望的套件显示为灰色,则可能存在一些套件配置问题。当您为目标平台安装 Qt 时,通常会自动配置开发目标的构建和运行设置。单击复选框以选择其中一个桌面套件,例如Desktop Qt 6.0.0 MinGW 64 位。单击下一步按钮以继续到下一个屏幕:图 2.9 - 套件选择屏幕

图 2.9 - 套件选择屏幕

  1. 版本控制允许您或您的团队将代码更改提交到集中系统,以便每个团队成员都可以获取相同的代码,而无需手动传递文件。您可以将项目添加到安装在您的计算机上的版本控制系统中。Qt 在 Qt Creator IDE 中支持多个版本控制系统。您可以通过选择来跳过此项目的版本控制。单击完成按钮以完成项目创建:图 2.10 - 项目管理屏幕

图 2.10 - 项目管理屏幕

  1. 现在您将在编辑器窗口的左侧看到生成的文件。单击任何文件以在编码窗口中打开它,这是 Qt Creator 中最常用的组件。代码编辑器用于编辑模式。您可以在此窗口中编写、编辑、重构和美化代码。您还可以修改字体、字体大小、颜色和缩进。我们将在本章后面的理解 高级选项部分中了解更多信息:图 2.11 - 生成的文件和代码编辑器窗口

图 2.11 - 生成的文件和代码编辑器窗口

  1. 现在您可以在项目文件夹中看到一个.pro文件。在当前项目中,HelloWorld.pro文件是项目文件。这包含了 qmake 构建应用程序所需的所有信息。此文件在项目创建期间自动生成,并以结构化方式包含相关详细信息。您可以在此文件中指定文件、资源和目标平台。如果对.pro文件内容进行任何修改,则需要再次运行 qmake,如图 2.12所示。让我们跳过修改此项目的内容:图 2.12 - 项目文件的内容

图 2.12 - 项目文件的内容

  1. 您可以在编辑器窗口的左侧找到一个带有.ui扩展名的表单文件。双击打开mainwindow.ui文件。在这里,您可以看到文件在不同的界面下打开:Qt Designer。您可以看到模式选择面板已切换到设计模式。我们将在下一章中更多地讨论 Qt Designer。

  2. 现在,将Label控件从显示小部件类别下拖动到右侧表单的中心,如图 2.13所示。

  3. 接下来,双击您拖动的项目,并键入Hello World!。按下键盘上的Enter键或单击控件外的任何位置以保存文本:图 2.13 - 设计师屏幕

图 2.13 - 设计师屏幕

  1. 最后一步是按下套件选择按钮下方的运行按钮。读者点击运行按钮后,项目将自动构建。Qt Creator 足够智能,可以确定需要先构建项目。您可以分别构建和运行应用程序。编译几秒钟后,您将看到一个带有文本“Hello World!”的窗口:

图 2.14 - 示例 GUI 应用程序的显示输出

图 2.14 - 示例 GUI 应用程序的显示输出

恭喜,您已经创建了您的第一个基于 Qt 的 GUI 应用程序!现在让我们探索 Qt Creator 中提供的不同高级选项。

理解高级选项

安装 Qt Creator 时,它会以默认配置安装。您可以自定义 IDE 并配置其外观或设置您喜欢的编码风格。

转到顶部菜单栏,单击工具选项,然后选择选项...。您将看到左侧边栏上可用类别的列表。每个类别都提供一组选项来自定义 Qt Creator。作为初学者,您可能根本不需要更改设置,但让我们熟悉一下可用的不同选项。我们将从管理工具包开始。

管理工具包

Qt Creator 可以自动检测已安装的 Qt 版本和可用的编译器。它将用于构建和运行项目的配置分组,以使它们跨平台兼容。这组配置被存储为一个工具包。每个工具包包含一组描述环境的参数,例如目标平台、编译器和 Qt 版本。

首先点击左侧边栏中的Kits选项。这将自动检测并列出可用的工具包,如图 2.15所示。如果任何工具包显示为黄色或红色警告标记,则表示配置中存在故障。在这种情况下,您可能需要选择正确的编译器和 Qt 版本。您还可以通过单击添加按钮来创建自定义工具包。如果要使用新工具包,则不要忘记单击应用按钮。我们将继续使用默认的桌面配置,如下所示:

图 2.15 - Kits 配置屏幕

图 2.15 - Kits 配置屏幕

现在让我们继续到Kits部分下的Qt 版本选项卡。

Qt 版本

在此选项卡中,您可以看到系统上可用的 Qt 版本。理想情况下,版本会自动检测到。如果没有检测到,然后单击添加...按钮并浏览到 qmake 的路径以添加所需的 Qt 版本。Qt 使用其发布的定义编号方案。例如,Qt 6.0.0 表示 Qt 6.0 的第一个补丁版本,6 表示主要 Qt 版本。每个版本都对可接受的更改量有限制,以确保稳定的 API。Qt 试图在版本之间保持兼容性。但是,由于主要版本中的代码清理和架构更改,这并不总是可能:

图 2.16 - 可用的 Qt 版本

图 2.16 - 可用的 Qt 版本

重要提示

Qt 软件版本使用主要.次要.补丁的版本格式。主要版本可能会破坏二进制和源代码的向后兼容性,尽管可能会保持源代码兼容性。次要版本具有二进制和源代码的向后兼容性。补丁版本对二进制和源代码都具有向后和向前兼容性。

我们不会讨论Kits部分下的所有选项卡,因为其他选项卡需要对编译器、调试器和构建系统有所了解。如果您是一名经验丰富的开发人员,可以探索选项卡并根据需要进行更改。让我们继续到左侧边栏中的环境类别。

环境

此选项允许用户选择他们喜欢的语言和主题。默认情况下,Qt Creator 使用系统语言。它不支持许多语言,但大多数流行的语言都可用。如果您切换到不同的语言,然后单击应用按钮并重新启动 Qt Creator 以查看更改。请注意,这些环境选项与构建环境不同。您将看到一个类似于图 2.17的界面,如下所示:

图 2.17 - 环境设置选项

图 2.17 - 环境设置选项

你还会看到一个复选框,上面写着启用高 DPI 缩放。Qt Creator 在不同的操作系统上处理高每英寸点数DPI)缩放的方式不同,具体如下:

  • 在 Windows 上,Qt Creator 会检测默认的缩放因子并相应地使用它。

  • 在 Linux 上,Qt Creator 将是否启用高 DPI 缩放的决定留给用户。这是因为有许多 Linux 发行版和窗口系统。

  • 在 macOS 上,Qt Creator 强制 Qt 使用系统缩放因子进行 Qt Creator 缩放。

要覆盖默认方法,你可以切换复选框选项并点击应用按钮。更改将在重新启动 IDE 后生效。现在让我们来看看键盘选项卡。

键盘快捷键

键盘部分允许用户探索现有的键盘快捷键并创建新的快捷键。Qt Creator 有许多内置的键盘快捷键,对开发人员非常有用。如果你喜欢的快捷键缺失,你也可以创建自己的快捷键。你还可以为在列表中不出现的功能指定自己的键盘快捷键,比如在文本编辑器中选择单词或行。

一些日常开发中常用的快捷键列举如下:

图 2.18 - 一些常用的键盘快捷键

图 2.18 - 一些常用的键盘快捷键

快捷键按类别分组。要在列表中找到一个键盘快捷键,输入一个函数名或快捷键在new中:

图 2.19 - 键盘快捷选项

](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/xplat-dev-qt6-mod-cpp/img/Figure_2.19_B16231.jpg)

图 2.19 - 键盘快捷选项

前面的屏幕截图显示了关键字new的可用快捷键列表。你可以看到Ctrl + N用于创建新文件或项目。你也可以导入或导出.kms格式的键盘映射方案文件。

重要提示

内置的 Qt 快捷键比我们在这里讨论的要多得多。你可以在以下文章中了解更多关于快捷键的信息:

doc.qt.io/qtcreator/creator-keyboard-shortcuts.html

wiki.qt.io/Qt_Creator_Keyboard_Shortcuts

shortcutworld.com/Qt-Creator/win/Qt-Creator_Shortcuts

Qt Creator 的键盘快捷键和窗口管理器快捷键之间可能会发生冲突。在这种情况下,窗口管理器快捷键将覆盖 Qt Creator 快捷键。你也可以在窗口管理器中配置键盘快捷键。如果这受到限制,那么你可以改变 Qt Creator 的快捷键。现在,让我们继续下一个侧边栏类别。

文本编辑器

左侧边栏中的下一个类别是文本编辑器。在这里,你可以在第一个选项卡中选择颜色方案、字体和字体大小。下一个选项卡列出了文本编辑器中的不同行为。正如你在图 2.20中所看到的,Qt 在键盘上使用空格缩进来代替Tab键:

图 2.20 - 文本编辑器行为选项卡

](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/xplat-dev-qt6-mod-cpp/img/Figure_2.20_B16231.jpg)

图 2.20 - 文本编辑器行为选项卡

一些开发人员更喜欢制表符缩进而不是空格缩进。你可以在C++Qt Quick设置中更改缩进行为。由于有专门的设置作为不同的侧边栏类别,所以文本编辑器中的这一部分在未来的版本中可能会被弃用。

你可以在文件编码组中找到当前文件的文件编码。要修改文件编码,从下拉菜单中选择新编码。要用新编码查看文件,点击应用按钮。

我们不会讨论所有侧边栏类别,因为那些都是非常高级的选项。一旦你学会了基础知识,你可以在以后探索它们。在下一节中,我们将讨论管理编码窗口。

分割编码窗口

您可以将编码窗口拆分并在同一屏幕或外部屏幕上查看多个文件。您可以以多种不同的方式同时查看多个文件(这些选项在菜单栏的窗口选项下可用):

图 2.21– 展示拆分屏幕选项的截图

图 2.21– 展示拆分屏幕选项的截图

现在让我们讨论拆分编码窗口和删除拆分窗口的各种方法:

  • 要将编码窗口分割为上下视图,请按Ctrl + E,然后按2,或在菜单栏中选择窗口选项,然后单击拆分选项。这将在当前活动窗口下方创建一个额外的编码窗口。

  • 要将编码窗口分割为相邻视图,请选择并排拆分或按Ctrl + E,然后按3。并排拆分会在当前活动编码窗口的右侧创建视图。

  • 要在独立窗口中打开编码窗口,请按Ctrl + E,然后按4,或选择在新窗口中打开。您可以将窗口拖到外部监视器上以方便使用。

  • 要在拆分视图和独立编辑器窗口之间移动,请选择下一个拆分或按Ctrl + E,然后按O

  • 要删除拆分视图,请单击要删除的窗口,然后选择删除当前拆分,或按Ctrl + E,然后按0

  • 要删除所有拆分编码窗口,请选择删除所有拆分或按Ctrl + E,然后按1

在本节中,您了解了如何拆分编码编辑器窗口。这在编码时同时引用多个代码文件时非常有用。在下一节中,我们将讨论 IDE 菜单栏中的构建菜单。

构建选项

在菜单栏中,您可以看到构建选项。如果单击该选项,那么您将看到各种构建选项,如图 2.22所示。在这里,您可以构建、重新构建或清理您的项目。在复杂的项目中,您可能有多个子项目。您可以单独构建子项目以减少总体构建时间:

图 2.22 – 构建菜单选项

图 2.22 – 构建菜单选项

Qt Creator 项目向导允许您在创建新项目时选择构建系统,包括 qmake、CMake 和 Qbs。它使开发人员可以自由地将 Qt Creator 用作代码编辑器,并控制构建项目时使用的步骤或命令。默认情况下,qmake 已安装并配置为您的新项目。您可以在以下链接了解有关使用其他构建系统的更多信息:doc.qt.io/qtcreator/creator-project-other.html

现在让我们讨论在哪里以及如何查找框架的文档。

Qt Assistant

Qt Creator 还包括一个名为 Qt Assistant 的内置文档查看器。这真的很方便,因为你可以通过简单地将鼠标悬停在源代码中的类名上并按下F1键来查找某个 Qt 类或函数的解释。然后 Qt Assistant 将被打开,并显示与该 Qt 类或函数相关的文档。

图 2.23 – 集成帮助界面

图 2.23 – 集成帮助界面

Qt Assistant 还支持交互式帮助,并使您能够为 Qt 应用程序创建帮助文档。

注意

在 Windows 平台上,Qt Assistant 作为 Qt Creator 菜单栏上的一个菜单选项。在 Linux 发行版上,您可以打开终端,输入assistant,然后按Enter。在 macOS 上,它安装在/Developer/Applications/Qt目录中。

在本节中,我们了解了 Qt Assistant 和帮助文档。现在,让我们总结一下本章的要点。

总结

本章介绍了 Qt Creator IDE 的基本原理以及它可以用于什么。Qt Creator 是一个带有一套强大工具的集成开发环境。它帮助您轻松地为多个平台创建出色的 GUI 应用程序。开发人员不需要编写冗长的代码来创建一个简单的按钮,也不需要改变大量的代码来对齐文本标签 - 当我们设计 GUI 时,Qt Designer 会自动生成代码。我们只需点击几下就创建了一个 GUI 应用程序,并且还学习了 IDE 中各种高级选项,包括如何管理工具包和快捷键。内置的 Qt 助手提供了有用的示例,并可以帮助我们编写自己的文档。

在下一章中,我们将讨论使用 Qt 小部件进行 GUI 设计。在这里,您将学习不同的小部件,如何创建自己的 GUI 元素,以及如何创建自定义的 GUI 应用程序。

第三章:使用 Qt 小部件进行 GUI 设计

Qt 小部件是一个模块,提供了一组用于构建经典 UI 的用户界面(UI)元素。在本章中,您将介绍 Qt 小部件模块,并了解基本小部件。我们将看看小部件是什么,以及可用于创建图形 UI(GUI)的各种小部件。除此之外,您还将通过 Qt Designer 介绍布局,并学习如何创建自定义控件。我们将仔细研究 Qt 在设计时如何为我们提供时尚的 GUI。在本章开始时,您将了解 Qt 提供的小部件类型及其功能。之后,我们将逐步进行一系列步骤,并使用 Qt 设计我们的第一个表单应用程序。然后,您将了解样式表、Qt 样式表(QSS 文件)和主题。

本章将涵盖以下主要主题:

  • 介绍 Qt 小部件

  • 使用 Qt Designer 创建 UI

  • 管理布局

  • 创建自定义小部件

  • 创建 Qt 样式表和自定义主题

  • 探索自定义样式

  • 使用小部件、窗口和对话框

在本章结束时,您将了解 GUI 元素及其相应的 C++类的基础知识,如何在不编写一行代码的情况下创建自己的 UI,以及如何使用样式表自定义 UI 的外观和感觉。

技术要求

本章的技术要求包括 Qt 6.0.0 MinGW 64 位,Qt Creator 4.14.0 和 Windows 10/Ubuntu 20.04/macOS 10.14。本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp/tree/master/Chapter03

注意

本章中使用的屏幕截图来自 Windows 环境。您将在您的机器上基于底层平台看到类似的屏幕。

介绍 Qt 小部件

小部件是 GUI 的基本元素。它也被称为QObjectQWidget是一个基本小部件,是所有 UI 小部件的基类。它包含描述小部件所需的大多数属性,以及几何、颜色、鼠标、键盘行为、工具提示等属性。让我们在下图中看一下QWidget的继承层次结构:

图 3.1 – QWidget 类层次结构

图 3.1 – QWidget 类层次结构

大多数 Qt 小部件的名称都是不言自明的,并且很容易识别,因为它们以Q开头。以下是其中一些:

  • QPushButton用于命令应用程序执行特定操作。

  • QCheckBox允许用户进行二进制选择。

  • QRadioButton允许用户从一组互斥选项中只做出一个选择。

  • QFrame显示一个框架。

  • QLabel用于显示文本或图像。

  • QLineEdit允许用户输入和编辑单行纯文本。

  • QTabWidget用于在选项卡堆栈中显示与每个选项卡相关的页面。

使用 Qt 小部件的优势之一是其父子系统。从QObject继承的任何对象都具有父子关系。这种关系使开发人员的许多事情变得方便,例如以下内容:

  • 当小部件被销毁时,由于父子关系层次结构,所有子项也会被销毁。这可以避免内存泄漏。

  • 您可以使用findChild()findChildren()找到给定QWidget类的子项。

  • QWidget中的子小部件会自动出现在父小部件内部。

典型的 C++程序在主函数返回时终止,但在 GUI 应用程序中我们不能这样做,否则应用程序将无法使用。因此,我们需要 GUI 一直存在,直到用户关闭窗口。为了实现这一点,程序应该在发生这种情况之前一直运行。GUI 应用程序等待用户输入事件。

让我们使用QLabel来显示一个简单 GUI 程序的文本,如下所示:

#include <QApplication>
#include <QLabel>
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QLabel myLabel;
    myLabel.setText("Hello World!");
    myLabel.show();
    return app.exec();
}

请记住将以下行添加到helloworld.pro文件中以启用 Qt Widgets 模块:

QT += widgets

在对.pro文件进行更改后,您需要运行qmake。如果您正在使用命令行,则继续执行以下命令:

>qmake
>make

现在,点击Run按钮来构建和运行应用程序。很快您将看到一个显示Hello World!的 UI,如下截图所示:

图 3.2 - 简单的 GUI 应用程序

图 3.2 - 简单的 GUI 应用程序

您也可以在 Windows 命令行中运行应用程序,如下所示:

>helloworld.exe

您可以在 Linux 发行版的命令行中运行应用程序,如下所示:

$./helloworld

在命令行模式下,如果库未在应用程序路径中找到,您可能会看到一些错误对话框。您可以将 Qt 库和插件文件复制到二进制文件夹中以解决此问题。为了避免这些问题,我们将坚持使用 Qt Creator 来构建和运行我们的示例程序。

在这一部分,我们学习了如何使用 Qt Widgets 模块创建一个简单的 GUI。在下一节中,我们将探索可用的小部件,并使用 Qt Designer 创建 UI。

使用 Qt Designer 创建 UI

在我们开始学习如何设计自己的 UI 之前,让我们熟悉一下 Qt Designer 的界面。以下截图显示了Qt Designer的不同部分。在设计我们的 UI 时,我们将逐渐了解这些部分:

图 3.3 - Qt Designer UI

图 3.3 - Qt Designer UI

Qt Widgets 模块带有现成的小部件。所有这些小部件都可以在Widget Box部分找到。Qt 提供了通过拖放方法创建 UI 的选项。让我们通过简单地从Widget Box区域拖动它们并将它们放入Form Editor区域来探索这些小部件。您可以通过抓取一个项目,然后在预定区域上按下并释放鼠标或触控板来执行此操作。在项目到达Form Editor区域之前,请不要释放鼠标或触控板。

以下截图显示了Widget Box部分提供的不同类型的小部件。我们已经将几个现成的小部件,如LabelPush ButtonRadio ButtonCheck BoxCombo BoxProgress BarLine Edit添加到Form Editor区域。这些小部件是非常常用的小部件。您可以在Property Editor中探索特定于小部件的属性:

图 3.4 - 不同类型的 GUI 小部件

图 3.4 - 不同类型的 GUI 小部件

您可以通过在Form菜单下选择Preview…选项来预览您的 UI,如下截图所示,或者您可以按下Ctrl + R。您将看到一个带有 UI 预览的窗口:

图 3.5 - 预览您的自定义 UI

图 3.5 - 预览您的自定义 UI

您可以通过在Form菜单下选择View C++ Code…选项来查找 UI 的创建的 C++代码,如下截图所示。您将看到一个显示生成代码的窗口。您可以在创建动态 UI 时重用该代码:

图 3.6 - 查看相应的 C++代码的选项

图 3.6 - 查看相应的 C++代码的选项

在本节中,我们熟悉了 Qt Designer UI。您还可以在.ui文件中找到相同的界面。在下一节中,您将学习不同类型的布局以及如何使用它们。

管理布局

Qt 提供了一组方便的布局管理类,以自动安排另一个小部件中的子小部件,以确保 UI 保持可用。QLayout类是所有布局管理器的基类。您还可以通过重新实现setGeometry()sizeHint()addItem()itemAt()takeAt()minimumSize()函数来创建自己的布局管理器。请注意,一旦布局管理器被删除,布局管理也将停止。

以下列表提供了主要布局类的简要描述:

  • QVBoxLayout将小部件垂直排列。

  • QHBoxLayout将小部件水平排列。

  • QGridLayout以网格形式布置小部件。

  • QFormLayout管理输入小部件及其关联标签的表单。

  • QStackedLayout提供了一个小部件堆栈,一次只有一个小部件可见。

QLayout通过从QObjectQLayoutItem继承来使用多重继承。QLayout的子类包括QBoxLayoutQGridLayoutQFormLayoutQStackedLayoutQVBoxLayoutQHBoxLayout是从QBoxLayout继承的,并添加了方向信息。

让我们使用 Qt Designer 模块来布置一些QPushButtons

QVBoxLayout

QVBoxLayout类中,小部件垂直排列,并且它们在布局中从上到下对齐。此时,您可以做以下事情:

  1. 将四个按钮拖放到表单编辑器上。

  2. 重命名按钮并按下键盘上的Ctrl键选择按钮。

  3. 表单工具栏中,单击垂直布局按钮。您可以通过悬停在工具栏按钮上找到这个按钮,该按钮上写着垂直布局

您可以在以下屏幕截图中看到按钮垂直排列在从上到下的方式:

图 3.7 – 使用 QVBoxLayout 进行布局管理

图 3.7 – 使用 QVBoxLayout 进行布局管理

您还可以通过 C++代码动态添加垂直布局,如下面的代码片段所示:

    QWidget *widget = new QWidget;
    QPushButton *pushBtn1 = new QPushButton("Push Button 
                                            1");
    QPushButton *pushBtn2 = new QPushButton("Push Button 
                                            2");
    QPushButton *pushBtn3 = new QPushButton("Push Button 
                                            3");
    QPushButton *pushBtn4 = new QPushButton("Push Button 
                                            4");
    QVBoxLayout *verticalLayout = new QVBoxLayout(widget);
    verticalLayout->addWidget(pushBtn1);
    verticalLayout->addWidget(pushBtn2);
    verticalLayout->addWidget(pushBtn3);
    verticalLayout->addWidget(pushBtn4);
    widget->show ();

该程序演示了如何使用垂直布局对象。请注意,QWidget实例widget将成为应用程序的主窗口。在这里,布局直接设置为顶级布局。添加到addWidget()方法的第一个按钮占据布局的顶部,而最后一个按钮占据布局的底部。addWidget()方法将一个小部件添加到布局的末尾,带有拉伸因子和对齐方式。

如果您在构造函数中没有设置父窗口,那么您将不得不稍后使用QWidget::setLayout()来安装布局并将其重新设置为widget实例的父对象。

接下来,我们将看看QHBoxLayout类。

QHBoxLayout

QHBoxLayout类中,小部件水平排列,并且它们从左到右对齐。

现在我们可以做以下事情:

  1. 将四个按钮拖放到表单编辑器上。

  2. 重命名按钮并按下键盘上的Ctrl键选择按钮。

  3. 表单工具栏中,单击水平布局按钮。您可以通过悬停在工具栏按钮上找到这个按钮,该按钮上写着水平布局

您可以在此屏幕截图中看到按钮水平排列在左到右的方式:

图 3.8 – 使用 QHBoxLayout 进行布局管理

图 3.8 – 使用 QHBoxLayout 进行布局管理

您还可以通过 C++代码动态添加水平布局,如下面的代码片段所示:

    QWidget *widget = new QWidget;
    QPushButton *pushBtn1 = new QPushButton("Push 
                                           Button 1");
    QPushButton *pushBtn2 = new QPushButton("Push 
                                           Button 2");
    QPushButton *pushBtn3 = new QPushButton("Push 
                                           Button 3");
    QPushButton *pushBtn4 = new QPushButton("Push 
                                           Button 4");
    QHBoxLayout *horizontalLayout = new QHBoxLayout(
                                        widget);
    horizontalLayout->addWidget(pushBtn1);
    horizontalLayout->addWidget(pushBtn2);
    horizontalLayout->addWidget(pushBtn3);
    horizontalLayout->addWidget(pushBtn4);
    widget->show ();

上面的示例演示了如何使用水平布局对象。与垂直布局示例类似,QWidget实例将成为应用程序的主窗口。在这种情况下,布局直接设置为顶级布局。默认情况下,添加到addWidget()方法的第一个按钮占据布局的最左侧,而最后一个按钮占据布局的最右侧。您可以使用setDirection()方法在将小部件添加到布局时更改增长方向。

在下一节中,我们将看一下QGridLayout类。

QGridLayout

QGridLayout类中,通过指定行数和列数将小部件排列成网格。它类似于具有行和列的网格结构,并且小部件被插入为项目。

在这里,我们应该执行以下操作:

  1. 将四个按钮拖放到表单编辑器中。

  2. 重命名按钮并按下键盘上的Ctrl键选择按钮。

  3. 表单工具栏中,单击网格布局按钮。您可以在工具栏按钮上悬停,找到标有以网格形式布局的按钮。

您可以在以下截图中看到按钮以网格形式排列:

图 3.9 - 使用 QGridLayout 进行布局管理

图 3.9 - 使用 QGridLayout 进行布局管理

您还可以通过 C++代码动态添加网格布局,如下段代码所示:

    QWidget *widget = new QWidget;
    QPushButton *pushBtn1 = new QPushButton(
                               "Push Button 1");
    QPushButton *pushBtn2 = new QPushButton(
                               "Push Button 2");
    QPushButton *pushBtn3 = new QPushButton(
                               "Push Button 3");
    QPushButton *pushBtn4 = new QPushButton(
                               "Push Button 4");
    QGridLayout *gridLayout = new QGridLayout(widget);
    gridLayout->addWidget(pushBtn1);
    gridLayout->addWidget(pushBtn2);
    gridLayout->addWidget(pushBtn3);
    gridLayout->addWidget(pushBtn4);
    widget->show();

上述代码段解释了如何使用网格布局对象。布局概念与前几节中的相同。您可以从 Qt 文档中探索QFormLayoutQStackedLayout布局。让我们继续下一节,了解如何创建自定义小部件并将其导出到 Qt 设计师模块。

创建自定义小部件

Qt 提供了现成的基本QLabel作为我们的第一个自定义小部件。自定义小部件集合可以有多个自定义小部件。

按照以下步骤构建您的第一个 Qt 自定义小部件库:

  1. 要在 Qt 中创建新的自定义小部件项目,请单击菜单栏上的文件菜单选项或按下Ctrl + N。或者,您也可以单击欢迎屏幕上的新建项目按钮。选择其他项目模板,然后选择Qt 自定义设计师小部件,如下截图所示:图 3.10 - 创建自定义小部件库项目

图 3.10 - 创建自定义小部件库项目

  1. 在下一步中,您将被要求选择项目名称和项目位置。单击MyWidgets以导航到所需的项目位置。然后,单击下一步按钮,进入下一个屏幕。以下截图说明了这一步骤:图 3.11 - 创建自定义控件库项目

图 3.11 - 创建自定义控件库项目

  1. 在下一步中,您可以从一组套件中选择一个套件来构建和运行您的项目。要构建和运行项目,至少一个套件必须处于活动状态且可选择。选择默认的桌面 Qt 6.0.0 MinGW 64 位套件。单击下一步按钮,进入下一个屏幕。以下截图说明了这一步骤:图 3.12 - 套件选择屏幕

图 3.12 - 套件选择屏幕

  1. 在这一步中,您可以定义自定义小部件类名称和继承详细信息。让我们使用类名MyLabel创建自己的自定义标签。单击下一步按钮,进入下一个屏幕。以下截图说明了这一步骤:图 3.13 - 从现有小部件屏幕创建自定义小部件

图 3.13 - 从现有小部件屏幕创建自定义小部件

  1. 在下一步中,您可以添加更多自定义小部件以创建一个小部件集合。让我们使用类名MyFrame创建自己的自定义框架。您可以在描述选项卡中添加更多信息,或者稍后进行修改。选中小部件是一个容器的复选框,以将框架用作容器。单击下一步按钮,进入下一个屏幕。以下截图说明了这一步骤:图 3.14 - 创建自定义小部件容器

图 3.14 - 创建自定义小部件容器

  1. 在这一步中,您可以指定集合类名称和插件信息,以自动生成项目骨架。让我们将集合类命名为MyWidgetCollection。单击下一步按钮,进入下一个屏幕。以下截图说明了这一步骤:图 3.15 - 指定插件和集合类信息的选项

图 3.15 - 指定插件和集合类信息的选项

  1. 下一步是将您的自定义小部件项目添加到已安装的版本控制系统中。您可以跳过此项目的版本控制。单击完成按钮以使用生成的文件创建项目。以下截图说明了这一步骤:图 3.16 - 项目管理屏幕

图 3.16 - 项目管理屏幕

  1. 展开mylabel.h文件。我们将修改内容以扩展功能。在自定义小部件类名之前添加QDESIGNER_WIDGET_EXPORT宏,以确保在插入宏后将类正确导出到#include <QtDesigner>头文件中。以下截图说明了这一步骤:图 3.17 - 修改创建的骨架中的自定义小部件

图 3.17 - 修改创建的骨架中的自定义小部件

重要提示

在一些平台上,构建系统可能会删除 Qt Designer 模块创建新小部件所需的符号,使它们无法使用。使用QDESIGNER_WIDGET_EXPORT宏可以确保这些符号在这些平台上被保留。这在创建跨平台库时非常重要。其他平台没有副作用。

  1. 现在,打开mylabelplugin.h文件。您会发现插件类是从一个名为QDesignerCustomWidgetInterface的新类继承而来。这个类允许 Qt Designer 访问和创建自定义小部件。请注意,为了避免弃用警告,您必须按照以下方式更新头文件:

#include <QtUiPlugin/QDesignerCustomWidgetInterface>

  1. mylabelplugin.h中会自动生成几个函数。不要删除这些函数。您可以在name()group()icon()函数中指定在 Qt Designer 模块中显示的值。请注意,如果在icon()中没有指定图标路径,那么 Qt Designer 将使用默认的 Qt 图标。group()函数在以下代码片段中说明:
QString MyFramePlugin::group() const
{
    return QLatin1String("My Containers");
}
  1. 您可以在以下代码片段中看到,isContainer()MyLabel中返回false,在MyFrame中返回true,因为MyLabel不设计用来容纳其他小部件。Qt Designer 调用createWidget()来获取MyLabelMyFrame的实例:
bool MyFramePlugin::isContainer() const
{
    return true;
}
  1. 要创建具有定义几何形状或其他属性的小部件,您可以在domXML()方法中指定这些属性。该函数返回MyLabel宽度为100 16像素,如下所示:
QString MyLabelPlugin::domXml() const
{
    return "<ui language=\"c++\" 
             displayname=\"MyLabel\">\n"
            " <widget class=\"MyLabel\" 
               name=\"myLabel\">\n"
            "  <property name=\"geometry\">\n"
            "   <rect>\n"
            "    <x>0</x>\n"
            "    <y>0</y>\n"
            "    <width>100</width>\n"
            "    <height>16</height>\n"
            "   </rect>\n"
            "  </property>\n"
            "  <property name=\"text\">\n"
            "   <string>MyLabel</string>\n"
            "  </property>\n"
            " </widget>\n"
            "</ui>\n";
}
  1. 现在,让我们来看看MyWidgets.pro文件。它包含了qmake构建自定义小部件集合库所需的所有信息。您可以在以下代码片段中看到,该项目是一个库类型,并配置为用作插件:
CONFIG      += plugin debug_and_release
CONFIG      += c++17
TARGET      = $$qtLibraryTarget(
              mywidgetcollectionplugin)
TEMPLATE    = lib
HEADERS     = mylabelplugin.h myframeplugin.h mywidgetcollection.h
SOURCES     = mylabelplugin.cpp myframeplugin.cpp \ 
                        mywidgetcollection.cpp
RESOURCES   = icons.qrc
LIBS        += -L. 
greaterThan(QT_MAJOR_VERSION, 4) {
    QT += designer
} else {
    CONFIG += designer
}
target.path = $$[QT_INSTALL_PLUGINS]/designer
INSTALLS    += target
include(mylabel.pri)
include(myframe.pri)
  1. 我们已经完成了自定义小部件创建过程。让我们运行qmake并在inside release文件夹中构建库。在 Windows 平台上,您可以手动将创建的mywidgetcollectionplugin.dll插件库复制到D:\Qt\6.0.0\mingw81_64\plugins\designer路径。这个路径和扩展名在不同的操作系统上会有所不同:图 3.18 - 生成自定义小部件库的选项

图 3.18 - 生成自定义小部件库的选项

  1. 我们已经创建了我们的自定义插件。现在,关闭插件项目,然后单击D:\Qt\6.0.0\mingw81_64\bin中的designer.exe文件。您可以在自定义小部件部分下看到MyFrame,如下面的屏幕截图所示。单击创建按钮或使用小部件模板。您还可以通过进行特定于平台的修改来将自己的表单注册为模板。让我们使用 Qt Designer 提供的小部件模板:图 3.19–新表单屏幕中的自定义容器

图 3.19–新表单屏幕中的自定义容器

  1. 您可以在左侧的小部件框部分看到我们的自定义小部件,位于底部。将MyLabel小部件拖到表单中。您可以在属性编辑器下找到创建的属性,例如multiLinefontCase以及QLabel属性,如下面的屏幕截图所示:

图 3.20–在 Qt Designer 中可用的导出小部件

图 3.20–在 Qt Designer 中可用的导出小部件

您还可以在以下 Qt 文档链接中找到详细的带有示例的说明:

doc.qt.io/qt-6/designer-creating-custom-widgets.html

恭喜!您已成功创建了具有新属性的自定义小部件。您可以通过组合多个小部件来创建复杂的自定义小部件。在下一节中,您将学习如何自定义小部件的外观和感觉。

创建 Qt 样式表和自定义主题

在上一节中,我们创建了我们的自定义小部件,但是小部件仍然具有本机外观。Qt 提供了几种自定义 UI 外观和感觉的方法。用大括号{}分隔,并用分号分隔。

让我们看一下简单的QPushButton样式表语法,如下所示:

QPushButton { color: green; background-color: rgb (193, 255, 216);}

您还可以通过在 Qt Designer 中使用样式表编辑器来改变小部件的外观和感觉,方法如下:

  1. 打开 Qt Designer 模块并创建一个新表单。将一个按钮拖放到表单上。

  2. 然后,右键单击按钮或表单中的任何位置以获取上下文菜单。

  3. 接下来,单击更改样式表…选项,如下面的屏幕截图所示:图 3.21–使用 Qt Designer 添加样式表

图 3.21–使用 Qt Designer 添加样式表

  1. 我们使用了以下样式表来创建之前的外观和感觉。您还可以在属性编辑器中从QWidget属性中更改样式表:
QPushButton {
    background-color: rgb(193, 255, 216);
    border-width: 2px;
    border-radius: 6;
    border-color: lime;
    border-style: solid;
    padding: 2px;
    min-height: 2.5ex;
    min-width: 10ex;
}
QPushButton:hover {
    background-color: rgb(170, 255, 127);
}
QPushButton:pressed {
    background-color: rgb(170, 255, 127);
    font: bold;
}

在上面的示例中,只有Push Button将获得样式表中描述的样式,而所有其他小部件将具有本机样式。您还可以为每个按钮创建不同的样式,并通过在样式表中提及它们的对象名称来将样式应用于相应的按钮,方法如下:

QPushButton#pushButtonID

重要提示

要了解更多关于样式表及其用法的信息,请阅读以下链接中的文档:

doc.qt.io/qt-6/stylesheet-reference.html

doc.qt.io/qt-6/stylesheet-syntax.html

doc.qt.io/qt-6/stylesheet-customizing.html

使用 QSS 文件

您可以将所有样式表代码组合在一个定义的.qss文件中。这有助于确保在所有屏幕中应用程序的外观和感觉保持一致。QSS 文件类似于.css文件,其中包含 GUI 元素的外观和感觉的定义,如颜色、背景颜色、字体和鼠标交互行为。它们可以使用任何文本编辑器创建和编辑。您可以创建一个新的样式表文件,使用.qss文件扩展名,然后将其添加到资源文件(.qrc)中。您可能并非所有项目都有.ui文件。GUI 控件可以通过代码动态创建。您可以将样式表应用于小部件或整个应用程序,如下面的代码片段所示。这是我们为自定义小部件或表单执行的方式:

MyWidget::MyWidget(QWidget *parent)
    : QWidget(parent)
{
    setStyleSheet("QWidget { background-color: green }");
}

这是我们为整个应用程序应用的方式:

#include "mywidget.h"
#include <QApplication>
#include <QFile>
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QFile file(":/qss/default.qss");
    file.open(QFile::ReadOnly);
    QString styleSheet = QLatin1String(file.readAll());
    app.setStyleSheet(styleSheet);
    Widget mywidget;
    mywidget.show();
    return app.exec();
}

上述程序演示了如何为整个 Qt GUI 应用程序使用样式表文件。您需要将.qss文件添加到资源中。使用QFile打开.qss文件,并将自定义的 QSS 规则作为参数传递给QApplication对象上的setStyleSheet()方法。您会看到所有屏幕都应用了样式表。

在本节中,您了解了使用样式表自定义应用程序外观和感觉的方法,但还有更多改变应用程序外观和感觉的方法。这些方法取决于您的项目需求。在下一节中,您将了解自定义样式。

探索自定义样式

Qt 提供了几个QStyle子类,模拟 Qt 支持的不同平台的样式。这些样式可以在 Qt GUI 模块中轻松获得。您可以构建自己的QStyle来渲染 Qt 小部件,以确保它们的外观和感觉与本机小部件一致。

在 Unix 发行版上,您可以通过运行以下命令为您的应用程序获取 Windows 风格的用户界面:

$./helloworld -style windows

您可以使用QWidget::setStyle()方法为单个小部件设置样式。

创建自定义样式

您可以通过创建自定义样式来自定义 GUI 的外观和感觉。有两种不同的方法可以创建自定义样式。在静态方法中,您可以子类化QStyle类并重新实现虚拟函数以提供所需的行为,或者从头开始重写QStyle类。通常使用QCommonStyle作为基类,而不是QStyle。在动态方法中,您可以子类化QProxyStyle并在运行时修改系统样式的行为。您还可以使用QStyle函数(如drawPrimitive()drawItemText()drawControl())开发样式感知的自定义小部件。

这部分是一个高级的 Qt 主题。您需要深入了解 Qt 才能创建自己的样式插件。如果您是初学者,可以跳过本节。您可以在以下链接的 Qt 文档中了解有关 QStyle 类和自定义样式的信息:

doc.qt.io/qt-6/qstyle.html

使用自定义样式

在 Qt 应用程序中应用自定义样式有几种方法。最简单的方法是在创建QApplication对象之前调用QApplication::setStyle()静态函数,如下所示:

#include "customstyle.h"
int main(int argc, char *argv[])
{
    QApplication::setStyle(new CustomStyle);
    QApplication app(argc, argv);
    Widget helloworld;
    helloworld.show();
    return app.exec();
}

您还可以将自定义样式作为命令行参数应用,方法如下:

>./customstyledemo -style customstyle

自定义样式可能难以实现,但可能更快速和更灵活。QSS 易于学习和实现,但性能可能会受到影响,特别是在应用程序启动时,因为 QSS 解析可能需要时间。您可以选择适合您或您的组织的方法。我们已经学会了如何自定义 GUI。现在,让我们在本章的最后一节中了解小部件、窗口和对话框是什么。

使用小部件、窗口和对话框

小部件是可以显示在屏幕上的 GUI 元素。这可能包括标签、按钮、列表视图、窗口、对话框等。所有小部件在屏幕上向用户显示某些信息,并且大多数允许用户通过键盘或鼠标进行交互。

窗口是一个没有父窗口的顶级小部件。通常,窗口具有标题栏和边框,除非指定了任何窗口标志。窗口样式和某些策略由底层窗口系统确定。Qt 中一些常见的窗口类包括QMainWindowQMessageBoxQDialog。主窗口通常遵循桌面应用程序的预定义布局,包括菜单栏、工具栏、中央小部件区域和状态栏。QMainWindow即使只是一个占位符,也需要一个中央小部件。主窗口中的其他组件可以被移除。图 3.22说明了QMainWindow的布局结构。我们通常调用show()方法来显示一个小部件或主窗口。

QMenuBar位于QMainWindow的顶部。您可以添加诸如QMenuBar之类的菜单选项,还有QToolBarQDockWidget提供了一个可以停靠在QMainWindow内或作为顶级窗口浮动的小部件。中央小部件是主要的视图区域,您可以在其中添加您的表单或子小部件。使用子小部件创建自己的视图区域,然后调用setCentralWidget()

图 3.22 – QMainWindow 布局

图 3.22 – QMainWindow 布局

重要提示

QMainWindow不应与QWindow混淆。QWindow是一个方便的类,表示底层窗口系统中的窗口。通常,应用程序使用QWidgetQMainWindow来构建 UI。但是,如果您希望保持最小的依赖关系,也可以直接渲染到QWindow

对话框是用于提供通知或接收用户输入的临时窗口,通常具有QMessageBox是一种用于显示信息和警报或向用户提问的对话框类型。通常使用exec()方法来显示对话框。对话框显示为模态对话框,在用户关闭它之前是阻塞的。可以使用以下代码片段创建一个简单的消息框:


    QMessageBox messageBox;
    messageBox.setText("This is a simple QMessageBox.");
    messageBox.exec(); 

重点是所有这些都是小部件。窗口是顶级小部件,对话框是一种特殊类型的窗口。

总结

本章介绍了 Qt Widgets 模块的基础知识以及如何创建自定义 UI。在这里,您学会了如何使用 Qt Designer 设计和构建 GUI。传统的桌面应用程序通常使用 Qt Designer 构建。诸如自定义小部件插件之类的功能允许您在 Qt Designer 中创建和使用自己的小部件集合。我们还讨论了使用样式表和样式自定义应用程序的外观和感觉,以及查看小部件、窗口和对话框之间的用途和区别。现在,您可以使用自己的自定义小部件创建具有扩展功能的 GUI 应用程序,并为桌面应用程序创建自己的主题。

在下一章中,我们将讨论QtQuick和 QML。在这里,您将学习关于QtQuick控件、Qt Quick Designer 以及如何构建自定义 QML 应用程序。我们还将讨论使用 Qt Quick 而不是小部件进行 GUI 设计的另一种选择。

第四章:Qt Quick 和 QML

Qt 由两个不同的模块组成,用于开发图形用户界面GUI)应用程序。第一种方法是使用 Qt Widgets 和 C++,我们在上一章中学习过。第二种方法是使用 Qt Quick Controls 和Qt 建模语言QML),我们将在本章中介绍。

在本章中,您将学习如何使用 Qt Quick Controls 和 QML 脚本语言。您将学习如何使用 Qt Quick 布局和定位器,并创建一个响应式 GUI 应用程序。您将学习如何将后端 C++代码与前端 QML 集成。您将学习 Qt Quick 和 QML 的基础知识,以及如何开发触摸友好和视觉导向的 Qt 应用程序。您还将学习有关鼠标和触摸事件的知识,以及如何开发一个触摸感知的应用程序。

在本章中,我们将涵盖以下主要主题:

  • 开始使用 QML 和 Qt Quick

  • 理解 Qt Quick Controls

  • 创建一个简单的 Qt Quick 应用程序

  • 使用 Qt Quick Designer 设计用户界面UI

  • QML 中的定位器和布局

  • 将 QML 与 C++集成

  • 将 QML 与JavaScriptJS)集成

  • 处理鼠标和触摸事件

在本章结束时,您将了解 QML 的基础知识,与 C++的集成,以及如何创建自己的流畅 UI。

技术要求

本章的技术要求包括在最新的桌面平台上安装 Qt 6.0.0 和 Qt Creator 4.14.0 的最低版本,如 Windows 10,Ubuntu 20.04 或 macOS 10.14。

本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp/tree/master/Chapter04

重要提示

本章使用的屏幕截图来自 Windows 平台。您将在您的机器上看到基于底层平台的类似屏幕。

开始使用 QML 和 Qt Quick

QML 是一种 UI 标记语言。它是 Qt 框架的一部分,是一种声明性语言。它使得构建流畅且触摸友好的 UI 成为可能,并随着触摸屏移动设备的发展而出现。它被创建为高度动态的,开发人员可以轻松地使用最少的编码创建流畅的 UI。Qt QML 模块实现了 QML 架构,并提供了一个开发应用程序的框架。它定义和实现了语言和基础设施,并提供了应用程序编程接口API)来将 QML 语言与 JS 和 C++集成。

Qt Quick 为 QML 提供了一系列类型和功能的库。它包括交互类型、可视类型、动画、模型、视图和图形效果。它用于触摸输入、流畅动画和用户体验至关重要的移动应用程序。Qt QML 模块为 QML 应用程序提供了语言和基础设施,而 Qt Quick 模块提供了许多可视元素、动画和许多其他模块,用于开发面向触摸和视觉吸引力的应用程序。您可以使用 QML 和 Qt Quick Controls 而不是 Qt Widgets 来设计 UI。Qt Quick 支持多个平台,如 Windows、Linux、Mac、iOS 和 Android。您可以在 C++中创建自定义类,并将其移植到 Qt Quick 以扩展其功能。此外,该语言与 C++和 JS 的集成非常顺畅。

理解 QML 类型系统

让我们熟悉QML 类型系统和各种 QML 类型。QML 文件中的类型可以来自各种来源。在 QML 文件中使用的不同类型在这里概述:

  • QML 本身提供的基本类型,如intboolreallist

  • JS 类型,如varDateArray

  • QML 对象类型,如ItemRectangleImageComponent

  • 通过 QML 模块由 C++注册的类型,如BackendLogic

  • 作为 QML 文件提供的类型,例如MyPushButton

基本类型可以包含诸如intbool类型的简单值。除了本机基本类型外,Qt Quick 模块还提供了其他基本类型。QML 引擎还支持 JS 对象和数组。任何标准 JS 类型都可以使用通用的var类型创建和存储。请注意,variant类型已经过时,只存在于支持旧应用程序的情况下。QML 对象类型是可以创建 QML 对象的类型。可以通过创建定义类型的.qml文件来定义自定义 QML 对象类型。QML 对象类型可以具有属性、方法、信号等。

要在您的 QML 文件中使用基本的 QML 类型,请使用以下代码行导入QtQml模块:import QtQml

Item是 Qt Quick 中所有可视元素的基本类型。Qt Quick 中的所有可视项都是从Item继承的,它是一个可以用作容器的透明可视元素。Qt Quick 提供Rectangle作为绘制矩形的可视类型,并提供Image类型来显示图像。Item为可视元素提供了一组通用属性。我们将在整本书中探索这些类型的用法。

您可以在以下链接了解更多关于 QML 类型的信息:

doc.qt.io/qt-6/qmltypes.html

在本节中,我们学习了 QML 和 Qt Quick 的基础知识。在下一节中,我们将讨论 Qt Quick Controls。

了解 Qt Quick Controls

Qt Quick Controls提供了一组 UI 元素,可用于使用 Qt Quick 构建流畅的 UI。为了避免与小部件产生歧义,我们将使用术语控件来表示 UI 元素。Qt Quick Controls 1最初设计用于支持桌面平台。随着移动设备和嵌入式系统的发展,该模块需要进行更改以满足性能期望。因此,Qt Quick Controls 2诞生了,并进一步增强了对移动平台的支持。自 Qt 5.11 起,Qt Quick Controls 1 已被弃用,并已从 Qt 6.0 中删除。Qt Quick Controls 2 现在简称为 Qt Quick Controls。

可以在您的.qml文件中使用以下import语句导入 QML 类型:

import QtQuick.Controls

重要提示

在 Qt 6 中,QML 导入和版本控制系统发生了一些变化。版本号现在是可选的。如果导入模块时没有指定版本号,则会自动导入模块的最新版本。如果只导入模块的主要版本号,则会导入指定主要版本和最新次要版本的模块。Qt 6 引入了import <module> auto。这确保了导入的模块和导入模块具有相同的版本号。

有关 Qt 6 中 Qt Quick Controls 的更改,请访问以下链接:

doc.qt.io/qt-6/qtquickcontrols-changes-qt6.html

Qt Quick Controls 提供了用于创建 UI 的 QML 类型。这里提供了 Qt Quick Controls 的示例:

  • ApplicationWindow:带有标题和页脚支持的样式化顶层窗口

  • BusyIndicator:指示后台活动,例如内容正在加载时

  • Button:可单击以执行命令或回答问题的推按钮

  • CheckBox:可以切换打开或关闭的复选框

  • ComboBox:用于选择选项的组合按钮和弹出列表

  • 拨号:旋转以设置值的圆形拨号

  • 对话框:带有标准按钮和标题的弹出对话框

  • 标签:带有继承字体的样式文本标签

  • Popup:类似弹出式 UI 控件的基本类型

  • ProgressBar:指示操作进度

  • RadioButton:可以切换打开或关闭的互斥单选按钮

  • 滚动条:垂直或水平交互式滚动条

  • ScrollView:可滚动视图

  • Slider:用于通过沿轨道滑动手柄来选择值

  • SpinBox:允许用户从一组预设值中进行选择

  • Switch:可以切换打开或关闭的按钮

  • TextArea:多行文本输入区域

  • TextField:单行文本输入字段

  • ToolTip:为任何控件提供工具提示

  • Tumbler:可旋转的可选择项目的轮子

要为 qmake 构建配置 Qt Quick Controls 模块,请将以下行添加到项目的.pro文件中:

QT += quickcontrols2

在本节中,我们了解了 Qt Quick 提供的不同类型的 UI 元素。在下一节中,我们将讨论 Qt Quick 提供的不同样式以及如何应用它们。

Qt Quick Controls 的样式

Qt Quick Controls 带有一套标准样式。它们在这里列出:

  • 基本

  • 融合

  • 想象

  • 材料

  • 通用

在 Qt Quick Controls 中有两种应用样式的方式,如下:

  • 编译时间

  • 运行时

您可以通过导入相应的样式模块来应用编译时样式,如下所示:

import QtQuick.Controls.Universal

您可以通过以下方法之一应用运行时样式:

图 4.1-运行时应用样式的不同方法

图 4.1-运行时应用样式的不同方法

在本节中,我们了解了 Qt Quick 中提供的样式。在下一节中,我们将创建我们的第一个 Qt Quick GUI 应用程序。

创建一个简单的 Qt Quick 应用程序

让我们使用 Qt 6 创建我们的第一个 Qt Quick 应用程序。Hello World 程序是一个非常简单的程序,显示Hello World!。该项目使用最少的——和最基本的——代码。对于这个项目,我们将使用 Qt Creator 创建的项目骨架。所以,让我们开始吧!按照以下步骤进行:

  1. 要创建一个新的 Qt Quick 应用程序,请单击菜单栏上的文件菜单选项或按下Ctrl + N。或者,您也可以单击欢迎屏幕上的新建项目按钮。然后,将弹出一个窗口供您选择项目模板。选择Qt Quick Application - Empty并单击选择...按钮,如下截图所示:图 4.2-新的 Qt Quick 应用程序向导

图 4.2-新的 Qt Quick 应用程序向导

  1. 在下一步中,您将被要求选择项目名称和项目位置。您可以通过单击SimpleQtQuickApp导航到所需的项目位置。然后,单击下一步按钮继续到下一个屏幕,如下截图所示:图 4.3-项目位置选择屏幕

图 4.3-项目位置选择屏幕

  1. 在下一步中,您可以从一组工具包中选择一个工具包来构建和运行您的项目。要构建和运行项目,至少必须激活并可选择一个工具包。选择默认的Desktop Qt 6.0.0 MinGW 64 位工具包。单击下一步按钮继续到下一个屏幕。可以在以下截图中看到:图 4.4-工具包选择屏幕

图 4.4-工具包选择屏幕

  1. 下一步是将您的 Qt Quick 项目添加到已安装的版本控制系统VCS)中。您可以跳过此项目的版本控制。单击完成按钮以创建带有生成文件的项目,如下截图所示:图 4.5-项目管理屏幕

图 4.5-项目管理屏幕

  1. 创建项目后,Qt Creator 将自动打开项目中的一个文件,名为main.qml。您将看到一种与您平常的 C/C++项目非常不同的脚本类型,如下截图所示:图 4.6-显示 main.qml 文件的代码编辑器屏幕
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    engine.load(url);
    return app.exec();
}

您也可以使用QQuickView类,它提供了一个用于显示 Qt Quick UI 的窗口。这种方法有点老了。QQmlApplicationEngine具有方便的 QML 中央应用功能,而QQuickView通常是从 C++控制的。以下代码片段显示了如何使用QQuickView来加载.qml文件:

#include <QGuiApplication>
#include <QQuickView>
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQuickView view;
    view.setResizeMode(
        QQuickView::SizeRootObjectToView);
    view.setSource(QUrl("qrc:/main.qml"));
    view.show();
    return app.exec();
}

QQuickView不支持将Window作为根项。如果您想要从 QML 创建您的根窗口,那么选择QQmlApplicationEngine。在使用QQuickView时,您可以直接使用任何 Qt Quick 元素,如下面的代码片段所示:

import QtQuick
Item  {
    width: 400
    height: 400
    Text {
          anchors.centerIn: parent
          text: "Hello World!"
    }
}
  1. 接下来,您可以通过点击位于集成开发环境IDE)左下角的绿色箭头按钮来构建和运行 Qt Quick 项目,如下截图所示:图 4.7 – Qt Creator 中的构建和运行选项

图 4.7 – Qt Creator 中的构建和运行选项

  1. 现在,点击运行按钮来构建和运行应用程序。很快,您将会看到一个带有Hello World!的 UI,如下截图所示:

图 4.8 – Hello World UI 的输出

图 4.8 – Hello World UI 的输出

您可以在 Windows 的命令行中运行应用程序,如下所示:

>SimpleQtQuickApp.exe

您也可以在 Linux 发行版的命令行中运行应用程序,如下所示:

$./SimpleQtQuickApp

在命令行模式下,如果在应用程序路径中找不到库文件,您可能会看到一些错误对话框。您可以将 Qt 库和插件文件复制到二进制文件夹中以解决这个问题。为了避免这些问题,我们将坚持使用 Qt Creator 来构建和运行我们的示例程序。您可以通过转到项目界面并根据您的偏好选择一个工具包来在不同的工具包之间切换。请记住,在对.pro文件进行更改后,您需要运行qmake。如果您正在使用命令行,则继续执行以下命令:

>qmake
>make

您还可以创建一个带有 QML 入口点的 Qt Quick 2 UI 项目,而不使用任何 C++代码。要使用它,您需要设置一个 QML 运行时环境,比如qmlscene。Qt Creator 使用.qmlproject来处理仅包含 QML 的项目:

  1. 创建一个 Qt Quick 2 UI 项目,从新项目模板屏幕中选择Qt Quick 2 UI Prototype,如下截图所示:图 4.9 – Qt Quick UI Prototype 向导

图 4.9 – Qt Quick UI Prototype 向导

  1. 继续点击QtQuickUIPrototype.qmlprojectQtQuickUIPrototype.qml这两个由 Qt Creator 生成的文件。

  2. 让我们修改QtQuickUIPrototype.qml的内容,添加一个Text元素并显示Hello World!,如下截图所示:图 4.10 – Qt Quick UI Prototype 项目的示例内容

图 4.10 – Qt Quick UI Prototype 项目的示例内容

  1. 现在,点击运行按钮来构建和运行应用程序。很快,您将会看到一个带有Hello World!的 UI。

您也可以在命令行中运行应用程序,如下所示:

>qmlscene QtQuickUIPrototype.qml

您可能需要在命令行中提到qmlsceneqml文件路径。只有在原型设计时才使用这个。您不能用这个来创建一个完整的应用程序。考虑使用 Qt Quick 应用程序项目来创建一个完整的应用程序。

在本节中,我们学习了如何使用 Qt Quick 模块创建一个简单的 GUI。在下一节中,我们将学习如何使用 Qt Quick Designer UI 设计自定义 UI。

使用 Qt Quick Designer 设计 UI

在本节中,您将学习如何使用 Qt Quick Designer 设计您的 UI。与 Qt Widgets 中的.ui文件类似,您也可以在 QML 中创建一个 UI 文件。该文件具有.ui.qml文件扩展名。有两种类型的 QML 文件:一种是.qml扩展名,另一种是.ui.qml扩展名。QML 引擎将其视为标准的.qml文件,但禁止其中的逻辑实现。它为多个.qml文件创建了可重用的 UI 定义。通过分离 UI 定义和逻辑实现,增强了 QML 代码的可维护性。

在开始学习如何设计自己的 UI 之前,让我们熟悉一下 Qt Quick Designer 的界面。以下截图显示了 Qt Quick Designer 的不同部分。在设计我们的 UI 时,我们将逐渐了解这些部分:

图 4.11 - Qt Quick Designer 界面的各个部分

图 4.11 - Qt Quick Designer 界面的各个部分

Qt Quick Designer 的界面包括以下主要部分:

  • 导航器:将当前 QML 文件中的项目列为树结构。这类似于我们在上一章中学习的 Qt Designer 中的对象操作器窗口。

  • 控件库:此窗口显示了 QML 中所有可用的 Qt Quick 控件。您可以将控件拖放到画布窗口中,以修改您的 UI。

  • 资源:显示了可以用于 UI 设计的所有资源的列表。

  • 导入浏览器导入浏览器便于将不同的 QML 模块导入到当前 QML 文件中,以为您的 QML 项目添加新功能。您还可以创建自己的自定义 QML 模块,并从这里导入。

  • 文本编辑器:有六个工具按钮,每个按钮都用于特定操作,如复制和粘贴。

  • 属性编辑器:类似于 Qt Designer 中的属性编辑器。Qt Quick Designer 中的属性部分显示了所选项目的属性。您还可以在文本编辑器中更改项目的属性。

  • 表单编辑器表单编辑器是一个画布,您可以在其中为 Qt Quick 应用程序设计 UI。

  • 状态编辑器:此窗口列出了 QML 项目中的不同状态,并描述了它们的 UI 定义和行为。

  • 连接编辑器:此部分类似于 Qt Designer 中的信号/槽编辑器。在这里,您可以为您的 QML 组件定义信号和槽机制。

您现在已经熟悉了 Qt Quick Designer UI。让我们创建一个 Qt Quick UI 文件,并探索 Qt Quick 控件,如下所示:

  1. 要创建一个 Qt Quick UI,选择ui.qml文件扩展名。默认情况下,Qt Creator 将打开 Qt Quick Designer。您可以通过单击左侧面板上的编辑按钮切换到代码编辑模式:图 4.12 - QtQuick UI 文件向导

图 4.12 - QtQuick UI 文件向导

  1. 让我们向ItemRectangleImageText等添加一些 QML 元素。Item是一个可以用作容器的透明 UI 元素:图 4.13 - Qt Quick Designer 显示基本的 QML 类型

图 4.13 - Qt Quick Designer 显示基本的 QML 类型

  1. 默认情况下,库只包含一些基本的 QML 类型。您可以通过 QML QtQuick.Controls包将 Qt Quick 模块导入到 Qt Quick Designer 中,如下一张截图所示:图 4.14 - Qt Quick Designer 显示了 QML 模块导入选项

图 4.14 - Qt Quick Designer 显示了 QML 模块导入选项

  1. 一旦导入模块,您就可以在库中看到一个带有Qt Quick - Controls 2的部分,如下一张截图所示:

图 4.15 - Qt Quick Designer 显示 Qt Quick 控件

图 4.15 - Qt Quick Designer 显示 Qt Quick 控件

在本节中,我们熟悉了 Qt Quick Designer 的界面。在下一节中,您将学习不同的定位器和布局。

QML 中的位置器和布局

在 QML 中有不同的定位项目的方法。您可以通过提及xy坐标或使用锚点、位置器或布局手动定位控件。让我们讨论如何通过上述方法定位控件。

手动定位

通过设置相应的xy属性,可以将控件定位在特定的xy坐标上。根据视觉坐标系统规则,这将使控件相对于其父级的左上角定位。

以下代码片段显示了如何将Rectangle项目放置在位置(50,50)处:

import QtQuick
Rectangle {
    // Manually positioned at 50,50
    x: 50 // x position
    y: 50 // y position
    width: 100; height: 80
    color: "blue"
}

当您运行上述代码时,您将看到一个蓝色矩形被创建在(50,50)位置。更改xy值,您将看到位置相对于左上角如何改变。Qt 允许您在一行中用分号分隔写入多个属性。您可以在同一行中用分号分隔写入xy位置。

在本节中,您学习了如何通过指定其坐标来定位可视项。在下一节中,我们将讨论锚点的使用。

使用锚点定位

Qt Quick 提供了一种将控件锚定到另一个控件的方法。每个项目有七条不可见的锚线:leftrighttopbottombaselinehorizontalCenterverticalCenter。您可以为每个边设置边距或不同的边距。如果特定项目有多个锚点,那么它们可以被分组。

让我们看下面的例子:

import QtQuick
import QtQuick.Window
Window {
    width: 400; height: 400
    visible: true
    title: qsTr("Anchoring Demo")
    Rectangle {
        id: blueRect
        anchors {
            left: parent.left; leftMargin:10
            right: parent.right; rightMargin: 40
            top: parent.top; topMargin: 50
            bottom: parent.bottom; bottomMargin: 100
        }
        color: "blue"
        Rectangle {
            id: redRect
            anchors.centerIn: blueRect
            color:"red"
            width: 150; height: 100
        }
    }
}

如果您运行此示例,您将在输出窗口中看到一个红色矩形,它位于蓝色矩形内部,具有不同的边距,如下所示:

图 4.16 - 锚定在窗口内部定位控件

图 4.16 - 锚定在窗口内部定位控件

在本节中,您学习了如何使用锚点定位可视项。在下一节中,我们将讨论位置器的使用。

位置器

Positioners是在声明性 UI 中管理可视元素位置的容器。Positioners 的行为方式类似于Qt widgets中的布局管理器。

一组标准的位置器在基本的 Qt Quick 元素集中提供。它们概述如下:

  • Column将其子项放置在列中。

  • Row将其子项放置在一行中。

  • Grid将其子项放置在网格中。

  • Flow将其子项放置在页面上的单词中。

让我们看看如何在 Qt Quick Designer 中使用它们。首先,创建三个具有不同颜色的Rectangle项目,然后将它们放置在一个Row元素内,如下截图所示:

图 4.17 - 位置器内的矩形

图 4.17 - 位置器内的矩形

您还可以编写代码来定位位置器内的控件。如果使用 Qt Quick Designer,Qt Creator 会自动生成代码。生成的代码可以通过Form Editor旁边的Text Editor选项卡查看和修改。代码如下所示:

Row {
    id: row     
    Rectangle {
        id: yellowRect
        width: 150; height: 100
        color: "yellow"
        border.color: "black"
    }
    Rectangle {
        id: redRect
        width: 150; height: 100
        color: "red"
        border.color: "black"
    }
    Rectangle {
        id: greenRect
        width: 150; height: 100
        color: "green"
        border.color: "black"
    }
}

在本节中,我们学习了不同的位置器。在下一节中,我们将讨论重复器和模型的使用,以及位置器。

Repeater

Repeater使用提供的模型创建多个可视元素,以及用于与位置器一起使用的模板元素,并使用模型中的数据。重复器放置在位置器内,并创建遵循定义的位置器排列的可视元素。当有许多类似的项目时,使用重复器的位置器在规则布局中排列时更容易维护。

让我们使用Repeater创建一个排列在一行中的五个矩形,如下所示:

import QtQuick
import QtQuick.Window
Window {
    width: 400; height: 200
    visible: true
    title: qsTr("Repeater Demo")
    Row {
        anchors.centerIn: parent
        spacing: 10
        Repeater {
            model: 5
            Rectangle {
                width: 60; height: 40
                border{ width: 1; color: "black";}
                color: "green"
            }
        }
    }
}

当您运行上述示例时,您将看到五个矩形排列在一行中,如下所示:

图 4.18 - 位置器内的矩形

图 4.18 - 位置器内的矩形

在本节中,我们了解了使用位置器和重复器。在下一节中,我们将深入了解 Qt Quick 布局。

Qt Quick 布局

Qt Quick 布局是一组 QML 类型,可用于在 UI 中排列可视元素。Qt Quick 布局可以调整其子元素的大小,因此它们用于可调整大小的 UI。位置器和布局之间的基本区别在于布局可以在窗口调整大小时调整其子元素。

可以通过以下import语句将 Qt Quick 布局导入到您的 QML 文件中:

import QtQuick.Layouts

这里有五种不同类型的 QML 布局,如下所述:

  • RowLayout:按行排列元素。它类似于GridLayout,但只有一行。

  • ColumnLayout:按列排列元素。它类似于GridLayout,但只有一列。

  • GridLayout:允许在网格中动态排列元素。

  • Layout:为推送到ColumnLayoutRowLayoutGridLayout布局类型的项目提供附加属性。

  • StackLayout:以堆栈方式排列元素,一次只有一个元素可见。

让我们看一下以下RowLayout示例:

import QtQuick
import QtQuick.Window
import QtQuick.Layouts
Window {
    width: 640; height: 480
    visible: true
    title: qsTr("Layout Demo")
    RowLayout {
        id: layout
        anchors.fill: parent
        spacing: 6
        Rectangle {
            color: 'yellow'
            Layout.fillWidth: true
            Layout.minimumWidth: 50
            Layout.preferredWidth: 150
            Layout.maximumWidth: 200
            Layout.minimumHeight: 100
            Layout.margins: 10
        }
        Rectangle {
            color: 'red'
            Layout.fillWidth: true
            Layout.minimumWidth: 50
            Layout.preferredWidth: 100
            Layout.preferredHeight: 80
            Layout.margins: 10
        }
    }
}

请注意,Row类型是位置器,而RowLayout类型是布局。何时使用它们主要取决于您的目标,与往常一样。让我们继续下一节,看看如何将 QML 与 C++集成。

将 QML 与 C++集成

QML 应用程序通常需要在 C++中处理更高级和性能密集型的任务。这样做的最常见和最快速的方法是将 C++类暴露给 QML 运行时,前提是 C++实现派生自QObject

QML 可以很容易地与 C++代码集成。可以从 C++加载和操作 QML 对象。QML 与 Qt 的元对象系统集成允许从 QML 调用 C++功能。这有助于构建混合应用程序,其中混合了 C++、QML 和 JS。要将 C++数据、属性或方法暴露给 QML,它应该派生自QObject类。这是可能的,因为所有 QML 对象类型都是使用QObject派生类实现的,允许 QML 引擎通过 Qt 元对象系统加载和检查对象。

您可以以以下方式将 QML 与 C++集成:

  • 使用上下文属性将 C++对象嵌入到 QML 中

  • 向 QML 引擎注册类型

  • 创建 QML 扩展插件

让我们在以下各节中逐一讨论每种方法。

重要提示

要快速确定哪种集成方法适合您的项目,请查看 Qt 文档中以下链接中的流程图:

doc.qt.io/qt-6/qtqml-cppintegration-overview.html

使用上下文属性将 C++对象嵌入到 QML 中

您可以使用上下文属性将 C++对象暴露到 QML 环境中。上下文属性适用于简单的应用程序。它们将您的对象导出为全局对象。上下文在由 QML 引擎实例化后暴露给 QML 环境。

让我们看一下以下示例,在这个示例中,我们已将radius导出到 QML 环境。您也可以以类似的方式导出 C++模型:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("radius", 50);
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    engine.load(url);
    return app.exec();
}

您可以直接在 QML 文件中使用导出的值,如下所示:

import QtQuick
import QtQuick.Window
Window {
    width: 640; height: 480
    visible: true
    title: qsTr("QML CPP integration")
    Text {
        anchors.centerIn: parent
        text: "C++ Context Property Value: "+ radius
    }
}

您还可以在 QML 环境中注册您的 C++类并实例化它。让我们在下一节中学习如何实现这一点。

使用 QML 引擎注册 C++类

注册 QML 类型允许开发人员从 QML 环境中控制 C++对象的生命周期。这不能通过上下文属性实现,也不会填充全局命名空间。不过,所有类型都需要首先注册,并且在应用程序启动时需要链接所有库,这在大多数情况下并不是真正的问题。

这些方法可以是公共槽或使用Q_INVOKABLE标记的公共方法。现在,让我们将 C++类导入到 QML 文件中。看一下以下 C++类:

#ifndef BACKENDLOGIC_H
#define BACKENDLOGIC_H
#include <QObject>
class BackendLogic : public QObject
{
    Q_OBJECT
public:
    explicit BackendLogic(QObject *parent = nullptr) { 
             Q_UNUSED(parent);}
    Q_INVOKABLE int getData() {return mValue; }
private:
    int mValue = 100;
};
#endif // BACKENDLOGIC_H

您需要在main.cpp文件中使用qmlRegisterType()将 C++类注册为模块,如下所示:

qmlRegisterType<BackendLogic>("backend.logic", 1, 0,"BackendLogic");

任何派生自Qobject的 C++类都可以注册为 QML 对象类型。一旦一个类被注册到 QML 类型系统中,该类就可以像任何其他 QML 类型一样使用。现在,C++类已准备好在您的.qml文件中实例化。您需要导入模块并创建一个对象,如下面的代码片段所示:

import QtQuick
import QtQuick.Window
import backend.logic
Window {
    width: 640; height: 480
    visible: true
    title: qsTr("QML CPP integration")
    BackendLogic {
        id: backend
    }
    Text {
        anchors.centerIn: parent
        text: "From Backend Logic : "+ backend.getData()
    }
}

当您运行上述程序时,您会看到程序正在从后端 C++类中获取数据并在 UI 中显示。

您还可以使用qmlRegisterSingletonType()将 C++类公开为 QML 单例。通过使用 QML 单例,您可以防止全局命名空间中的重复对象。让我们跳过这部分,因为它需要对设计模式有所了解。详细的文档可以在以下链接找到:

doc.qt.io/qt-6/qqmlengine.html#qmlRegisterSingletonType

在 Qt 6 中,您可以通过使用QML_ELEMENT宏实现 C++集成。该宏将声明封闭类型作为 QML 中可用,使用其类或命名空间名称作为 QML 元素名称。要在 C++头文件中使用此宏,您将需要包含qml.h头文件,如#include <QtQml>

让我们看一下以下示例:

#ifndef USINGELEMENT_H
#define USINGELEMENT_H
#include <QObject>
#include <QtQml>
class UsingElements : public QObject
{
    Q_OBJECT
    QML_ELEMENT
public:
    explicit UsingElements(QObject *parent = nullptr) { 
              Q_UNUSED(parent);}
    Q_INVOKABLE int readValue() {return mValue; }
private:
    int mValue = 500;
};
#endif // USINGELEMENT_H

.pro文件中,您需要将qmltypes选项添加到CONFIG变量,并且需要提到QML_IMPORT_NAMEQML_IMPORT_MAJOR_VERSION,如下面的代码片段所示:

CONFIG += qmltypes
QML_IMPORT_NAME = backend.element
QML_IMPORT_MAJOR_VERSION = 1

您的 C++类现在已准备好在您的.qml文件中实例化。您需要导入模块并创建一个对象,如下面的代码片段所示:

import QtQuick
import QtQuick.Window
import backend.element
Window {
    width: 640; height: 480
    visible: true
    title: qsTr("QML CPP integration")
    UsingElements {
        id: backendElement
    }
    Text {
        anchors.centerIn: parent
        text: "From Backend Element : "+ 
              backendElement.readValue()
    }
}

在本节中,您学习了如何将您的 C++类导出到 QML 环境中,并从 QML 访问其函数。在这个例子中,数据只有在调用方法时才被检索。您还可以通过添加带有NOTIFY信号的Q_PROPERTY()宏在 C++内部更改数据时得到通知。在使用之前,您需要了解信号和槽机制。因此,我们将跳过这部分,并在第六章中进一步讨论信号和槽。在下一节中,我们将讨论如何创建一个 QML 扩展插件。

创建 QML 扩展插件

QML 扩展插件提供了与 C++集成的最灵活的方式。它允许您在插件中注册类型,在第一个 QML 文件调用导入标识符时加载该插件。您可以在项目之间使用插件,这在构建复杂项目时非常方便。

Qt Creator 有一个向导可以创建QqmlExtensionPlugin,并且应该实现registerTypes()函数。需要使用Q_PLUGIN_METADATA宏来标识插件为 QML 扩展插件:

图 4.19 - Qt Quick 2 QML 扩展插件向导

图 4.19 - Qt Quick 2 QML 扩展插件向导

这一部分是一个高级的 Qt 主题。您需要深入了解 Qt 才能创建自己的 QML 扩展插件。如果您是初学者,可以跳过本节,但您可以在以下链接的 Qt 文档中了解更多关于 QML 扩展插件的信息:

doc.qt.io/qt-6/qtqml-modules-cppplugins.html

让我们继续下一节,了解如何在 C++类中调用 QML 方法。

在 C++类中调用 QML 方法

所有 QML 方法都暴露给元对象系统,并可以使用QMetaObject::invokeMethod()从 C++中调用。您可以在冒号字符后指定参数和返回值的类型,如下一个代码片段所示。当您想要将 C++中的信号连接到 QML 定义的特定签名的方法时,这可能很有用。如果省略类型,则 C++签名将使用QVariant

让我们看一个调用 QML 方法的应用程序,使用QMetaObject::invokeMethod()

在 QML 文件中,让我们添加一个名为qmlMethod()的方法,如下所示:

import QtQuick
Item {
    function qmlMethod(msg: string) : string {
        console.log("Received message:", msg)
        return "Success"
    }
    Component.onCompleted: {
        console.log("Component created successfully.")
    }
}

main.cpp文件中,按照以下代码片段调用QMetaObject::invokeMethod()

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlComponent>
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    QQmlComponent component(&engine, 
                            "qrc:/CustomItem.qml");
    QObject *myObject = component.create();
    QString retValue = "";
    QString msg = "Message from C++";
    QMetaObject::invokeMethod(myObject, "qmlMethod",
                              Q_RETURN_ARG(QString, 
                              retValue),
                              Q_ARG(QString, msg));
    qDebug() << "QML method returned:" << retValue;
    delete myObject;
    return app.exec();
}

请注意,必须指定参数和返回类型。基本类型和对象类型都允许作为类型名称。如果类型在 QML 类型系统中未提及,则在调用QMetaObject::invokeMethod时,您必须使用Q_RETURN_ARG()Q_ARG()声明QVariant作为类型。或者,如果您不需要任何返回值,可以只用两个参数调用invokeMethod(),如下所示:

QMetaObject::invokeMethod(myObject, "qmlMethod");

在本节中,您学会了从 QML 方法中接收数据。在下一节中,您将学习如何在 C++中访问 QML 对象指针。

将 QML 对象指针暴露给 C++

有时,您可能希望通过 C++修改 QML 对象的属性,例如修改控件的文本、更改控件的可见性或更改自定义属性。QML 引擎允许您将 QML 对象注册为 C++类型,从而自动公开 QML 对象的属性。

让我们看一个示例,我们将一个 QML 对象导出到 C++环境中:

#ifndef CUSTOMOBJECT_H
#define CUSTOMOBJECT_H
#include <QObject>
#include <QVariant>
class CustomObject : public QObject
{
    Q_OBJECT
public:
    explicit CustomObject(QObject *parent = nullptr);
    Q_INVOKABLE void setObject(QObject* object)
    {
        object->setProperty("text", QVariant("Clicked!"));
    }
};
#endif // CUSTOMOBJECT_H

在 QML 文件中,您需要创建C++类的实例并调用C++方法。如下面的代码片段所示,在C++类内部操作属性:

import QtQuick
import QtQuick.Window
import QtQuick.Controls
import MyCustomObject
Window {
    width: 640; height: 480;
    visible: true
    title: qsTr("QML Object in C++")
    CustomObject{
        id: customObject
    }
    Button {
        id: button
        anchors.centerIn: parent
        text: qsTr("Click Me!")
        onClicked: {
            customObject.setObject(button);
        }
    }
}

重要说明

Qt QML 模块提供了几个用于注册不可实例化类型的宏。QML_ANONYMOUS注册一个不可实例化且无法从 QML 引用的 C++类型。QML_INTERFACE注册一个现有的 Qt 接口类型。该类型无法从 QML 实例化,并且您不能使用它声明 QML 属性。QML_UNCREATABLE注册一个命名的不可实例化的 C++类型,但应该作为 QML 类型系统中的类型可识别。QML_SINGLETON注册一个可以从 QML 导入的单例类型。

恭喜!您已经学会了如何集成 QML 和 C++。在下一节中,我们将讨论如何在 QML 中使用 JS。

将 QML 与 JS 集成

QML 与 JS 有很好的集成,并使用类似JavaScript 对象表示JSON)的语法,允许定义表达式和方法作为 JS 函数。它还允许开发人员导入 JS 文件并使用现有功能。QML 引擎提供了一个 JS 环境,与 Web 浏览器提供的 JS 环境相比有一些限制。Qt Quick 应用程序的逻辑可以在 JS 中定义。JS 代码可以内联编写在 QML 文件中,也可以编写在单独的 JS 文件中。

让我们看看如何在 QML 文档中使用内联 JS。下面的示例演示了btnClicked()内联 JS 函数。当单击Button控件时,将调用该方法:

import QtQuick
import QtQuick.Window
import QtQuick.Controls
Window {
    width: 640; height: 480;
    visible: true
    title: qsTr("QML JS integration")
    function btnClicked(controlName) {
        controlName.text = "JS called!"
    }
    Column  {
        anchors.centerIn: parent
        Button {
            text:"Call JS!"
            onClicked: btnClicked(displayText)
        }
        Text {
            id: displayText
        }
    }
}

前面的示例展示了如何将 JS 代码与 QML 集成。我们使用了btnClicked()内联 JS 函数。当您运行应用程序时,将收到一条消息,上面写着JS called!

如果您的逻辑非常复杂或在多个 QML 文档中使用,则使用单独的 JS 文件。您可以按如下方式导入 JS 文件:

import "<JavaScriptFile>" as <Identifier>

例如,您可以运行以下代码行:

import "constants.js" as Constants

在前面的示例中,我们将constants.js导入到 QML 环境中。Constants是我们 JS 文件的标识符。

您还可以创建一个共享的 JS 库。您只需在 JS 文件的开头包含以下代码行:

.pragma library

重要提示

如果脚本是单个表达式,则建议将其内联写入。如果脚本有几行长,则使用块。如果脚本超过几行长或被不同对象需要,则创建一个函数并根据需要调用它。对于长脚本,创建一个 JS 文件并在 QML 文件中导入它。避免使用Qt.include(),因为它已被弃用,并将在未来的 Qt 版本中删除。

要了解有关在 QML 中导入 JS 的更多信息,请阅读以下文档:

doc.qt.io/qt-6/qtqml-javascript-imports.html

在本节中,您学习了如何将 JS 与 QML 集成。在下一节中,我们将讨论如何在 QML 中导入目录。

在 QML 中导入目录

您可以直接在另一个 QML 文件中导入包含 QML 文件的本地目录,而无需添加资源。您可以使用目录的绝对或相对文件系统路径来实现这一点,为 QML 类型提供了一种方便的方式,将其排列为可重用的目录在文件系统上。

目录导入的常见形式如下所示:

import "<DirectoryPath>" [as <Qualifier>]

例如,如果您的目录名称是customqmlelements,那么您可以按如下方式导入它:

import "../customqmlelements"

还可以将目录作为限定的本地命名空间导入,如下面的代码片段所示:

import "../customqmlelements" as CustomQMLElements

您还可以按以下方式从资源路径导入文件:

import "qrc:/qml/customqmlelements"

您还可以从远程服务器导入一个包含 QML 文件的目录。有两种不同类型的qmldir文件:QML 目录列表文件和 QML 模块定义文件。在这里,我们讨论的是使用qmldir QML 目录列表文件。可以使用qmldir文件导入目录。为了避免恶意代码,您必须小心处理网络文件。

以下文档提供了有关qmldir QML 目录列表文件的更多信息:

doc.qt.io/qt-6/qtqml-syntax-directoryimports.html

您可以在以下链接了解有关不同类型的qmldir文件的更多信息:

doc.qt.io/qt-6/qtqml-modules-qmldir.html

在本节中,您学习了如何在 QML 中导入目录。在下一节中,我们将讨论如何在 QML 中处理鼠标和触摸事件。

处理鼠标和触摸事件

QML 通过输入处理程序提供了对鼠标和触摸事件的出色支持,这些处理程序让 QML 应用程序处理鼠标和触摸事件。QML 类型,如MouseAreaMultiPointTouchAreaTapHandler用于检测鼠标和触摸事件。我们将在下一节中查看这些 QML 类型。

MouseArea

MouseArea是一个不可见的项目,用于与可见项目(如ItemRectangle)一起,以便为该项目提供鼠标和触摸处理事件。MouseAreaItem的定义区域内接收鼠标事件。您可以通过使用anchors.fill属性将MouseArea锚定到其父级区域来定义此区域。如果将 visible 属性设置为false,则鼠标区域对鼠标事件变得透明。

让我们看看如何在以下示例中使用MouseArea

import QtQuick
import QtQuick.Window
Window {
    width: 640; height: 480
    visible: true
    title: qsTr("Mouse Area Demo")
    Rectangle {
        anchors.centerIn: parent
        width: 100; height: 100
        color: "green"
        MouseArea {
            anchors.fill: parent
            onClicked: { parent.color = 'red' }
        }
    }
}

在前面的例子中,您可以看到只有rectangle区域收到了鼠标事件。窗口的其他部分没有收到鼠标事件。您可以根据鼠标事件执行相应的操作。MouseArea还提供了方便的信号,可以提供有关鼠标事件的信息,如鼠标悬停、鼠标按下、按住、鼠标退出和鼠标释放事件。编写相应的信号处理程序,并尝试使用entered()exited()pressed()released()信号。您还可以检测按下了哪个鼠标按钮,并执行相应的操作。

MultiPointTouchArea

MultiPointTouchArea QML 类型使多点触摸屏幕上的多个触摸点处理成为可能。与MouseArea一样,MultiPointTouchArea是一个不可见的项。您可以跟踪多个触摸点并相应地处理手势。当禁用时,触摸区域对触摸和鼠标事件都变得透明。在MultiPointTouchArea类型中,鼠标事件被处理为单个触摸点。您可以将mouseEnabled属性设置为false以停止处理鼠标事件。

让我们看一下以下示例,其中有两个矩形跟随我们的触摸点:

import QtQuick
import QtQuick.Window
Window {
    width: 640; height: 480
    visible: true
    title: qsTr("Multitouch Example")
    MultiPointTouchArea {
        anchors.fill: parent
        touchPoints: [
            TouchPoint { id: tp1 },
            TouchPoint { id: tp2 }
        ]
    }
    Rectangle {
        width: 100; height: 100
        color: "blue"
        x: tp1.x; y: tp1.y
    }
    Rectangle {
        width: 100; height: 100
        color: "red"
        x: tp2.x; y: tp2.y
    }
}

MultiPointTouchArea类型中,TouchPoint定义了一个触摸点。它包含有关触摸点的详细信息,如压力、当前位置和区域。现在,在您的移动设备上运行应用程序并进行验证!

在本节中,您了解了使用MouseAreaMultiPointTouchArea来处理鼠标和触摸事件。让我们在下一节中了解TapHandler

TapHandler

TapHandler是鼠标点击事件和触摸屏上的轻拍事件的处理程序。您可以使用TapHandler来对轻拍和触摸手势做出反应,并允许您同时处理多个嵌套项中的事件。有效轻拍手势的识别取决于gesturePolicygesturePolicy的默认值是TapHandler.DragThreshold,其中事件点不得显着移动。如果将gesturePolicy设置为TapHandler.WithinBounds,则TapHandler独占按下事件,但一旦事件点离开父项的边界,就会释放独占。同样,如果将gesturePolicy设置为TapHandler.ReleaseWithinBounds,则TapHandler独占按下事件,并保持独占直到释放,以便检测此手势。

让我们创建一个TapHandler类型,以识别不同的鼠标按钮事件和触笔轻拍,如下所示:

import QtQuick
import QtQuick.Window
Window {
    width: 640; height: 480
    visible: true
    title: qsTr("Hello World")
    Item {
        anchors.fill:parent
        TapHandler {
            acceptedButtons: Qt.LeftButton
            onTapped: console.log("Left Button Clicked!")
        }
        TapHandler {
            acceptedButtons: Qt.MiddleButton
            onTapped: console.log("Middle Button Clicked!")
        }
        TapHandler {
            acceptedButtons: Qt.RightButton
            onTapped: console.log("Right Button Clicked!")
        }
        TapHandler {
             acceptedDevices: PointerDevice.Stylus
             onTapped: console.log("Stylus Tap!")
         }
    }
}

您可以使用MouseArea。输入处理程序使得形成复杂的触摸交互变得更简单,这是使用MouseAreaTouchArea难以实现的。

Qt 提供了一些现成的控件来处理通用手势,如捏合、轻扫和滑动。PinchArea是一个方便的 QML 类型,用于处理简单的捏合手势。它是一个不可见项,与另一个可见项一起使用。Flickable是另一个方便的 QML 类型,提供了一个用于轻扫手势的表面。探索相关文档和示例,以了解更多关于这些 QML 元素的信息。

让我们在下一节中看看SwipeView

SwipeView

SwipeView用于通过侧向滑动导航页面。它使用基于滑动的导航模型,并提供了一种简化的水平分页滚动方式。您可以在底部添加页面指示器以显示当前活动页面。

让我们看一个简单的例子,如下所示:

import QtQuick
import QtQuick.Window
import QtQuick.Controls
Window {
    width: 640; height: 480
    visible: true
    title: qsTr("Swipe Demo")
    SwipeView {
        id: swipeView
        currentIndex: 0
        anchors.fill: parent
        Rectangle { id: page1; color: "red" }
        Rectangle { id: page2; color: "green"}
        Rectangle { id: page3; color: "blue" }   
    }     
    PageIndicator {
        id: pageIndicator
        count: swipeView.count
        currentIndex: swipeView.currentIndex
        anchors {
            bottom: swipeView.bottom
            horizontalCenter: parent.horizontalCenter
        }
    }
}

如您所见,我们只需向SwipeView添加子项。您可以将SwipeView当前索引设置为PageIndicator当前索引。SwipeView是导航模型之一,还包括StackViewDrawer。您可以探索这些 QML 类型,以在移动设备上体验手势。

在本节中,您了解了使用各种 QML 类型来处理鼠标、触摸和手势事件。接下来,我们将总结本章学到的内容。

总结

本章解释了 Qt Quick 模块的基础知识以及如何创建自定义 UI。您学会了如何使用 Qt Quick Designer 设计和构建 GUI,并了解了 Qt Quick Controls 以及如何构建自定义 Qt Quick 应用程序。您还学会了如何将 QML 与 C++和 JS 集成。现在您应该了解 Qt Widgets 和 Qt Quick 之间的相似之处和不同之处,并能够为您的项目选择最合适的框架。在本章中,我们学习了 Qt Quick 以及如何使用 QML 创建应用程序。您还学会了如何将 QML 与 JS 集成,并了解了鼠标和触摸事件。

在下一章中,我们将讨论使用 Qt Creator 进行跨平台开发。您将学习在 Windows、Linux、Android 和 macOS 操作系统(OSes)上配置和构建应用程序。我们将学习如何将我们的 Qt 应用程序移植到不同的平台,而不会遇到太多挑战。让我们开始吧!

第二部分:跨平台开发

本节将向您介绍跨平台开发。跨平台开发的理念是软件应用在多个平台上运行良好,而不需要进行重大的代码更改。这样可以节省在移植和维护代码库方面的时间。这符合 Qt 的理念:“少写代码,创造更多,到处部署”。在本节中,您将了解 Qt Creator IDE 及其用法,以及如何在不同平台上开发和运行相同的应用程序。

本节包括以下章节:

  • 第五章, 跨平台开发

第五章:跨平台开发

自其最初发布以来,Qt 以其跨平台能力而闻名——这是创建该框架的主要愿景。您可以在 Windows、Linux 和 macOS 等喜爱的桌面平台上使用 Qt Creator,并使用相同的代码库或稍作修改创建流畅、现代、触摸友好的图形用户界面GUI)和桌面、移动或嵌入式应用程序。您可以轻松修改您的代码并将其部署到目标平台上。Qt 具有几个内置工具,可分析您的应用程序及其在各种支持的平台上的性能。此外,与其他跨平台框架不同,它易于使用,并且具有直观的用户界面UI)。

在本章中,您将学习跨平台开发的基本知识以及如何在不同平台上构建应用程序。有了这些,您将能够在您喜爱的桌面和移动平台上运行示例应用程序。

在本章中,我们将涵盖以下主要主题:

  • 了解跨平台开发

  • 了解编译器

  • 使用qmake构建

  • Qt 项目(.pro)文件

  • 了解构建设置

  • 特定于平台的设置

  • 在 Microsoft Visual Studio 中使用 Qt

  • 在 Linux 上运行 Qt 应用程序

  • 在 macOS 和 iOS 上运行 Qt 应用程序

  • 其他 Qt 支持的平台

  • 从 Qt 5 迁移到 Qt 6

本章结束时,您将了解 Qt 项目文件、基本设置以及如何在移动设备上运行 Qt 应用程序。让我们开始吧!

技术要求

本章的技术要求包括在最新的桌面平台(如 Windows 10、Ubuntu 20.04 或 macOS 10.14)上安装 Qt 6.0.0 和 Qt Creator 4.14.0 的最低版本。

本章中使用的所有代码都可以从以下 GitHub 链接下载:

github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp/tree/master/Chapter05/HelloWorld

重要说明

本章中使用的屏幕截图是在 Windows 平台上拍摄的。您将在您的机器上看到基于底层平台的类似屏幕。

了解跨平台开发

市场上有几种跨平台框架可供选择,但由于其成熟度和可用的社区支持,Qt 是更好的选择。对于传统的 C++开发人员来说,很容易适应 Qt 并开发高质量的应用程序。Qt 框架允许开发人员开发与多个平台兼容的应用程序,如 Windows、Linux、macOS、QNX(最初称为Quick Unix [Qunix])、iOS 和 Android。它通过一次编码和随处部署的理念,促进更快的应用程序开发和更好的代码质量。Qt 在内部处理特定于平台的实现,并且还能让您在微控制器驱动的设备上构建令人印象深刻的超轻量级应用程序。

要使用 Qt 开发嵌入式平台的应用程序,您将需要商业许可证来使用Qt for Device Creation。Qt 还支持一些微控制器单元MCU)平台,如瑞萨、STM32 和 NXP。在撰写本书时,Qt for MCUs 1.8 已推出,提供了具有较小内存占用的超轻量级模块。

使用 Qt 框架进行跨平台开发的一些优势列在这里:

  • 降低开发成本的成本效益

  • 更好的代码可重用性

  • 便利性

  • 更快的上市时间TTM

  • 更广泛的市场覆盖

  • 提供接近本机体验

  • 性能优越

也有一些缺点,比如:

  • 无法使用特定于平台的功能和访问所有平台的应用程序编程接口API

  • 本地和非本地组件之间的通信挑战

  • 特定设备功能和硬件兼容性挑战

  • 延迟的平台更新

在本节中,您对 Qt 的跨平台特性有了基本了解,并了解了跨平台开发的利弊。在您可以在任何平台上运行应用程序之前,您需要一个编译器来为目标平台编译应用程序。在下一节中,我们将了解 Qt 框架支持的编译器。

了解编译器

在本节中,您将学习什么是编译器,以及如何在跨平台开发中使用它。编译器是一种软件,它将您的程序转换为计算机可以读取和执行的机器代码或低级指令。这些低级机器指令因平台而异。您可以使用不同的编译器(如GNU 编译器集合GCC))编译 Qt 应用程序,或者使用供应商提供的编译器。在 Qt Creator 中,您可以在Kits选项卡下找到一个支持的编译器,以及在特定平台(如 Windows、Linux 或 macOS)上构建应用程序所需的其他基本工具。并非所有支持的编译器都包含在 Qt 安装程序中,但您可以在推荐的工具包中自动列出最常用的编译器。Qt 可能会停止支持某些工具包配置,或者用最新版本替换它们。

目前,Qt 支持以下编译器:

  • GCC

  • Windows 的极简 GNUMinGW

  • Microsoft Visual C++MSVC

  • 低级虚拟机LLVM

  • 英特尔 C++编译器ICC

  • clang-cl

  • Nim

  • QCC

此外,Qt Creator 裸机设备插件提供以下编译器的支持:

  • IAR 嵌入式工作台IAREW

  • KEIL

  • 小型设备 C 编译器SDCC

除了上述编译器,Qt 在构建 Qt 项目时还使用特定的内置编译器。这些列在这里:

  • moc)

  • uic)

  • rcc)

您可以使用上述编译器构建目标平台的应用程序,或者添加自定义编译器配置。在下一节中,您将学习如何创建自定义编译器配置。

添加自定义编译器

要添加 Qt Creator 未自动检测到或不可用的编译器,请使用自定义选项。您可以指定编译器和工具链路径到相应的目录,并进行相应的配置。

要添加自定义编译器配置,请按照以下步骤操作:

  1. 要在 Qt 中创建新的编译器配置,请单击菜单栏上的工具菜单,然后从左侧窗格中选择Kits选项卡。

  2. 然后,单击编译器选项卡,并从添加下拉菜单中选择自定义。您将在上下文菜单中看到CC++选项。根据您的需求选择类型。您可以在以下截图中看到这个概述:图 5.1-自定义编译器选项

图 5.1-自定义编译器选项

  1. 在下一步中,使用自定义名称填写名称字段。

  2. 接下来,在编译器路径字段中,选择编译器所在目录的路径。

  3. 接下来,指定make工具的位置。

  4. 在下一步中,在ABI字段中指定应用程序二进制接口ABI)版本。

您可以在以下截图中看到这个概述:

图 5.2-自定义编译器所需字段

图 5.2-自定义编译器所需字段

  1. 接下来,您可以在MACRO[=value]中指定默认所需的宏。

  2. 在下一步中,在头文件路径字段中指定编译器检查头文件的路径。

  3. 接下来,在C++11支持中。

  4. 在下一步中,在Qt mkspecs字段中指定mkspecs(一组编译规则)的位置。

  5. 接下来,在错误解析器字段中,选择合适的错误解析器。

  6. 单击应用按钮以保存配置。

在本节中,您了解了支持的编译器以及如何在 Qt Creator 中创建新的编译器配置,但是要构建和运行项目,我们需要比编译器更多的工具。Qt 提供了qmake作为我们方便使用的内置构建工具。在下一节中,我们将讨论qmake是什么。

使用 qmake 构建

Makefile并构建可执行程序和库。qmake是 Qt 提供的一个构建工具,可简化跨多个平台的开发项目的构建过程。它将每个项目文件中的信息扩展到一个Makefile中,以执行必要的编译和链接命令。它也可以用于非 Qt 项目。qmake根据项目文件中的信息生成一个Makefile,并包含支持 Qt 开发的附加功能,自动包括mocuic的构建规则。qmake还可以创建 Microsoft Visual Studio 项目,而无需开发人员更改项目文件。

作为一个社区驱动的框架,Qt 对开发者非常灵活,并且给予他们选择最合适的工具来进行项目开发的自由,而不是强迫他们使用自己的构建系统。Qt 支持以下类型的构建系统:

  • qmake

  • CMake

  • Qbs

  • Meson

  • Incredibuild

您可以从 Qt Creator UI 或命令行中运行qmake。每次对项目文件进行更改时,都应该运行qmake。以下是从命令行运行qmake的语法:

>qmake [mode] [options] files

qmake提供了两种不同的操作模式。在默认模式下,qmake使用项目文件中的信息生成Makefile,但它也可以生成项目文件。模式如下所示:

  • -makefile

  • -project

qmake中,将生成一个用于构建项目的Makefile。运行qmake以 Makefile 模式的语法如下所示:

>qmake -makefile [options] files

在项目模式下,qmake将生成一个项目文件。运行qmake的语法如下所示:

>qmake -project [options] files

如果您将 Visual Studio 作为qmake项目,qmake可以创建一个包含开发环境所需的所有基本信息的 Visual Studio 项目。它可以递归生成子目录中的.vcproj文件和主目录中的.sln文件,使用以下命令:

>qmake -tp vc -r

例如,您可以通过运行以下命令为您的HelloWorld项目生成一个 Visual Studio 项目:

>qmake -tp vc HelloWorld.pro

请注意,每次修改项目文件时,都需要运行qmake以生成更新的 Visual Studio 项目。

您可以在以下链接找到有关qmake的更多详细信息:

doc.qt.io/qt-6/qmake-manual.html

大多数qmake项目文件使用name = valuename += value定义的列表定义项目中使用的源文件和头文件,但qmake中还有其他高级功能,使用其他运算符、函数、平台范围和条件来创建跨平台应用程序。有关qmake语言的更多详细信息,请访问以下链接:doc.qt.io/qt-6/qmake-language.html

Qt 团队在 Qt 6 中付出了很多努力,使其具有未来的可扩展性,通过使用广泛采用的流行构建工具CMake。已经实施了一些变化,通过使用Conan作为一些附加组件的包管理器,使 Qt 更加模块化。在 Qt 6 中,一些 Qt 模块不再作为 Qt 在线安装程序中的二进制包可用,而是作为 Conan 配方可用。您可以在以下链接了解有关构建系统更改以及将 CMake 作为默认构建工具的更多信息:doc.qt.io/qt-6/qt6-buildsystem.html

重要提示

在 Qt 5 中,构建系统是基于qmake构建的,但在 Qt 6 中,CMake 是构建 Qt 源代码的构建系统。这种变化只影响想要从源代码构建 Qt 的开发人员。您仍然可以使用qmake作为 Qt 应用程序的构建工具。

在本节中,您了解了qmake。我们将跳过高级的qmake主题,以便自行探索。在下一节中,我们将讨论 Qt 项目文件,这些文件由qmake解析。

Qt 项目(.pro)文件

在早期示例中由 Qt Creator 创建的.pro文件实际上是 Qt 项目文件。.pro文件包含qmake构建应用程序、库或插件所需的所有信息。项目文件支持简单和复杂的构建系统。简单的项目文件可以使用直接的声明,定义标准变量以指示项目中使用的源文件和头文件。复杂的项目可能使用多个流结构来优化构建过程。项目文件包含一系列声明,用于指定资源,例如指向项目所需的源文件和头文件的链接、项目所需的库、不同平台的自定义构建过程等。

Qt 项目文件有几个部分,并使用某些预定义的qmake变量。让我们看一下我们早期的HelloWorld示例.pro文件:

QT       += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++17
# You can make your code fail to compile if it uses 
# deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    
# disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
    main.cpp \
    widget.cpp
HEADERS += \
    widget.h
FORMS += \
    widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

项目文件只是告诉qmake项目中所需的 Qt 模块,以及可执行程序的名称。它还链接到需要包含在项目中的头文件、源文件、表单文件和资源文件。所有这些信息对于qmake创建配置文件和构建应用程序至关重要。对于更复杂的项目,您可能需要为不同的操作系统不同地配置项目文件。

以下列表描述了最常用的变量,并描述了它们的目的:

  • QT:项目中使用的 Qt 模块列表

  • CONFIG:一般项目配置选项

  • DESTDIR:可执行文件或二进制文件将放置在其中的目录

  • FORMS:要由 UI 编译器(uic)处理的 UI 文件列表

  • HEADERS:构建项目时使用的头文件(.h)文件名列表

  • RESOURCES:要包含在最终项目中的资源(.qrc)文件列表

  • SOURCES:在构建项目时要使用的源代码(.cpp)文件列表

  • TEMPLATE:用于项目的模板

您可以向项目添加不同的 Qt 模块、配置和定义。让我们看看如何做到这一点。要添加额外的模块,只需在QT +=之后添加模块关键字,如下所示:

QT += core gui sql

您还可以在前面添加条件,以确定何时向项目添加特定模块,如下所示:

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

您还可以向项目添加配置设置。例如,如果要在编译项目时指定c++17规范,则将以下行添加到您的.pro文件中:

CONFIG += c++17

您可以向项目文件添加注释,以井号(#)开头,构建系统将忽略相应的文本行。现在,让我们看一下TEMPLATE变量。这确定构建过程的输出是应用程序、库还是插件。有不同的变量可用于概述qmake将生成的文件类型。这些列在下面:

  • app用于构建应用程序。

  • lib用于构建库。

  • aux用于构建空内容。如果不需要调用编译器来创建目标(例如,因为项目是用解释语言编写的),则使用此选项。

  • subdirs用于使用SUBDIRS变量指定的子目录。每个子目录必须包含自己的项目文件。

  • vcapp用于创建用于构建应用程序的 Visual Studio 项目文件。

  • vclib用于创建一个 Visual Studio 项目文件,以构建库。

  • vcsubdirs用于创建一个 Visual Studio 解决方案文件,以在子目录中构建项目。

Qt 项目文件有时需要依赖于include功能。在 Qt 项目文件中,您还可以定义两个重要的变量:INCLUDEPATHDEPENDPATH。您可以使用SUBDIRS变量来编译一组依赖库或模块。

现在,让我们讨论一下.pri文件是什么。

了解.pro.pri文件之间的区别

您可以创建一个.pri文件来包含复杂项目中的项目文件。这样可以提高可读性并将不同模块分隔开。.pri文件通常被称为qmake包含文件,其格式与.pro文件类似。主要区别在于使用意图;.pro文件是我们期望直接在其上运行qmake的文件,而.pri文件是由.pro文件包含的。您可以将常见配置,如源文件、头文件、.ui文件和.qrc文件添加到.pri文件中,并根据项目需求从多个.pro文件中包含它们。

您可以在.pro文件中包含一个.pri文件,如下所示:

include($$PWD/common.pri)

在本节中,您了解了 Qt 项目文件是什么,以及其中使用的不同变量。在下一节中,我们将讨论不同的构建设置。

了解构建设置

在编译或构建项目之前,编译器需要某些细节,这些细节称为构建设置。这是编译过程中非常重要的一部分。在本节中,您将了解构建设置以及如何以正确的方式配置它们。您可以为同一个项目拥有多个构建配置。通常,Qt Creator 会自动创建调试、发布和配置文件构建配置。调试构建包含用于调试应用程序的额外调试符号,而发布版本是一个经过优化的版本,不包含这样的符号。通常,开发人员使用调试配置进行测试,使用发布配置创建最终的二进制文件。配置文件构建是一个经过优化的发布构建,附带单独的调试信息,最适合于分析应用程序。

构建设置可以在项目模式中指定。如果 IDE 中没有打开项目,则可能会发现项目按钮被禁用。您可以通过单击添加下拉按钮,然后选择要添加的配置类型来添加新的构建配置。选项可能取决于为项目选择的构建系统。您可以根据需要添加多个构建配置。您可以单击克隆…按钮,以基于当前构建配置添加一个构建配置,或单击重命名…按钮来重命名当前选定的构建配置。单击删除按钮来删除一个构建配置。

您可以在以下截图中看到这个概述:

图 5.3 - 构建设置和 Qt Quick 编译器选项

图 5.3 - 构建设置和 Qt Quick 编译器选项

通常,Qt Creator 在与源目录不同的目录中构建项目,称为影子构建。这样可以将为每个构建和运行工具生成的文件分隔开。如果您只想使用单个工具包构建和运行,则可以取消选择影子构建复选框。Qt Creator 项目向导创建了一个可以编译使用Qt 资源系统的 Qt Quick 项目。要使用默认设置,请选择保持默认。要编译 Qt Quick 代码,请在Qt Quick 编译器字段中选择启用,如图 5.3所示。

您可以在以下链接中了解有关不同构建配置的更多信息:

doc.qt.io/qtcreator/creator-build-settings.html

在本节中,我们讨论了构建设置。在构建跨平台应用程序时,向项目文件添加特定于平台的配置非常重要。在下一节中,我们将学习有关特定于平台的设置。

特定于平台的设置

您可以为不同的平台定义不同的配置,因为并非每种配置都适用于所有用例。例如,如果您想为不同的操作系统包含不同的头文件路径,您可以将以下代码行添加到您的.pro文件中:

win32: INCLUDEPATH += "C:/mylibs/windows_headers"
unix:INCLUDEPATH += "/home/user/linux_headers"

在上述代码片段中,我们添加了一些特定于 Windows 和特定于 Linux 的头文件。您还可以像这样在 C++中放置配置,例如if语句:

win32 {
    SOURCES += windows_code.cpp
}

上述代码仅适用于 Windows 平台,这就是为什么我们在前面加了一个win32关键字。如果您的目标平台是基于 Linux 的,那么您可以添加一个unix关键字来添加特定于 Linux 的配置。

要在 Windows 平台上为应用程序设置自定义图标,您应该将以下代码行添加到您的项目(.pro)文件中:

RC_ICONS = myapplication.ico

要在 macOS 上为应用程序设置自定义图标,您应该将以下代码行添加到您的项目(.pro)文件中:

ICON = myapplication.icns

请注意,Windows 和 macOS 的图标格式不同。对于 Linux 发行版,制作每种风格的桌面条目有不同的方法。

在本节中,我们讨论了一些特定于平台的设置。在下一节中,我们将学习如何在 Qt VS 工具中使用 Visual Studio。

在 Microsoft Visual Studio 中使用 Qt

一些开发人员选择 Visual Studio 作为他们首选的 IDE。因此,如果您喜欢的 IDE 是 Visual Studio,那么您可以将 Qt VS 工具与 Microsoft Visual Studio 集成。这将允许您在标准的 Windows 开发环境中使用,而无需担心与 Qt 相关的构建步骤或工具。您可以直接从 Microsoft Visual Studio 安装和更新 Qt VS 工具。

您可以从 Visual Studio Marketplace 找到相应版本的 Qt Visual Studio 工具。对于 Visual Studio 2019,您可以从以下链接下载该工具:marketplace.visualstudio.com/items?itemName=TheQtCompany.QtVisualStudioTools2019。您还可以从以下 Qt 下载链接下载VS插件:download.qt.io/official_releases/vsaddin/

这些是 Qt VS 工具的一些重要功能:

  • 创建新项目和类的向导

  • mocuicrcc编译器的自动构建设置

  • 导入和导出.pro.pri文件

  • 将 Qt VS 工具项目自动转换为qmake项目

  • 集成 Qt 资源管理

  • 能够创建 Qt 翻译文件并与Qt Linguist集成

  • 集成Qt Designer

  • 集成 Qt 文档

  • 用于 Qt 数据类型的调试扩展

要开始在 Visual Studio 环境中使用这些功能,您必须设置 Qt 版本。从.pro文件中选择适当的版本与qmake或从 Visual Studio 中的.vcproj文件构建您的项目。由于 Visual Studio 用于特定于 Windows 的开发,建议将 Qt Creator 用作跨平台开发的 IDE。

如果您没有.vcproj文件,那么您可以通过命令行或通过 VS 工具从.pro文件生成一个。我们已经在使用 qmake 构建部分讨论了命令行指令。您还可以通过使用.vcproj文件将您的.pro文件转换为.vcproj文件,该文件仅包含特定于 Windows 的设置。

在本节中,我们讨论了VS插件。在下一节中,我们将学习如何在 Linux 上运行一个示例应用程序。我们将跳过在 Windows 上构建和运行 Qt 应用程序的讨论,因为我们已经在前几章中讨论过这个问题。

在 Linux 上运行 Qt 应用程序

在 Linux 上构建和运行 Qt 应用程序与在 Windows 上运行类似,但 Linux 有许多发行版,因此很难构建一个完美运行在所有 Linux 变体上的应用程序。在大多数发行版中,应用程序将会顺利运行。我们将以 Ubuntu 20.04 作为目标平台。当你在 Ubuntu 上安装 Qt 时,它会自动检测套件和配置。你也可以配置一个带有适当编译器和 Qt 版本的套件,如下截图所示:

图 5.4 - Ubuntu 上的桌面套件配置

图 5.4 - Ubuntu 上的桌面套件配置

让我们在 Ubuntu 上运行我们的HelloWorld示例。点击左侧窗格上的运行按钮。一个显示Hello World!的 UI 将立即出现,如下截图所示:

图 5.5 - Ubuntu 上运行的应用程序

图 5.5 - Ubuntu 上运行的应用程序

你也可以从命令行运行应用程序,如下面的代码片段所示:

$./HelloWorld

在本节中,我们讨论了如何在 Linux 发行版上运行我们的应用程序。在下一节中,我们将学习如何在 macOS 和 iOS 上运行 Qt 应用程序。

在 macOS 和 iOS 上运行 Qt 应用程序

我们已经在前几章讨论了如何在 Windows 和 Linux 平台上构建和运行应用程序。让我们继续学习如何在 macOS 和 iOS 等平台上运行我们的应用程序。要在 macOS 和 iOS 上构建 Qt 应用程序,你需要从 App Store 下载 Xcode。Xcode 是 macOS 的 IDE,包括一套用于在 macOS 和 iOS 中开发应用程序的软件开发工具。如果你已经安装了 Xcode,Qt Creator 将检测到其存在并自动检测到合适的套件。至于套件选择,Qt for macOS 支持 Android、clang 64 位、iOS 和 iOS 模拟器的套件。

你可以在下面的截图中看到 macOS 上的桌面套件配置示例:

图 5.6 - macOS 上的桌面套件配置

](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/xplat-dev-qt6-mod-cpp/img/Figure_5.6_B16231.jpg)

图 5.6 - macOS 上的桌面套件配置

如果你不想使用自动检测的调试器,你也可以在调试器选项卡中手动添加调试器,如下截图所示:

图 5.7 - macOS 上的调试器选项

图 5.7 - macOS 上的调试器选项

在 macOS 上运行应用程序与在 Windows 上运行类似。只需点击运行按钮,你将立即看到应用程序运行。

移动平台与 Windows、Linux 和 macOS 等桌面平台同等重要。让我们探讨如何设置运行 iOS 应用程序的环境。

为 iOS 配置 Qt Creator

在 iOS 上运行 Qt 应用程序非常简单。你可以连接你的 iOS 设备,并从设备选择列表中选择合适的设备类型。你可以从套件选择屏幕中选择设备类型。你也可以在 iOS 模拟器上运行应用程序,如下截图所示:

图 5.8 - macOS 上的 iOS 模拟器选项

图 5.8 - macOS 上的 iOS 模拟器选项

配置好套件后,只需将 iPhone 连接上并点击运行按钮。你可以在下面的截图中看到一个示例输出:

图 5.9 - Qt Creator 在 iPhone 上运行应用程序

](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/xplat-dev-qt6-mod-cpp/img/Figure_5.9_B16231.jpg)

图 5.9 - Qt Creator 在 iPhone 上运行应用程序

在 iOS 平台上构建和运行应用程序相对容易。然而,分发应用程序并不容易,因为 App Store 是一个非常封闭的生态系统。你需要一个 Apple ID,并且需要在分发应用程序给用户之前登录你的 iOS 应用程序。你无法避开这些步骤,但现在让我们跳过部署部分。

你可以在以下链接了解更多关于 App Store 提交的信息:

developer.apple.com/app-store/submissions

在本节中,我们学习了如何在 macOS 和 iOS 上运行应用程序。在下一节中,我们将学习如何为 Android 平台配置和构建应用程序。

为 Android 配置 Qt Creator

Android 是当今最流行的移动平台,因此开发人员希望为 Android 构建应用程序。尽管 Android 是基于 Linux 的操作系统,但它与其他 Linux 发行版非常不同。为了使用它,您必须配置 Qt Creator 并安装某些软件包。

为了使 Qt Creator 配置 Android 顺利运行,请使用 OpenJDK 8,带有 clang 工具链的 NDK r21。您可以从ANDROID_SDK_ROOT\cmdline-tools\latest\bin运行 sdkmanager,并使用必要的参数配置所需的依赖项。

您可以在以下链接中了解有关 Android 特定要求和说明的更多信息:

doc.qt.io/qt-6/android-getting-started.html

让我们开始按照以下步骤配置您的机器以用于 Android:

  1. 要在 Android 上构建 Qt 应用程序,您必须在开发 PC 上安装 Android软件开发工具包SDK),Android本机开发工具包NDK),Java 开发工具包JDK)和 OpenSSL,无论您的桌面平台如何。您将在每个相应字段旁边找到带有地球图标或下载按钮的下载选项,以从各自软件包的页面下载。

  2. 安装所有必需的软件包后,重新启动 Qt Creator。Qt Creator 应该能够自动检测构建和平台工具。

  3. 但是,您可能需要进一步配置以修复Android设置中的错误。您可能会发现 SDK 管理器、平台 SDK 和必要的软件包缺失,如下截图所示:图 5.10 - Android 设置屏幕

图 5.10 - Android 设置屏幕

  1. Android 设置下选择正确的 SDK 和 NDK 路径。点击应用按钮以保存更改。

  2. 点击SDK 管理器选项卡,然后点击更新已安装按钮。您可能会看到一个消息框,提示您安装缺少的软件包,如下截图所示。点击按钮来安装这些软件包:图 5.11 - 显示缺少 Android 软件包的信息消息

图 5.11 - 显示缺少 Android 软件包的信息消息

  1. 您可能会收到另一条消息,警告 Android SDK 更改,列出缺少的基本软件包,如下截图所示。点击确定按钮:图 5.12 - 关于缺少 Android 软件包的警告

图 5.12 - 关于缺少 Android 软件包的警告

  1. 点击--verbose,然后点击确定按钮。您可以在以下截图中看到概述:图 5.13 - Android SDK 管理器工具

图 5.13 - Android SDK 管理器工具

  1. 一旦问题解决,您将看到所有 Android 设置已经正确配置,如下截图所示:图 5.14 - Qt Creator 中正确的 Android 配置

图 5.14 - 在 Qt Creator 中正确的 Android 配置

  1. 如果问题仍未解决,或者您想安装特定平台,您可以输入适当的命令,如下截图所示。您还可以从命令行安装所需的软件包。Qt 将自动检测 SDK 位置中可用的构建工具和平台:图 5.15 - Android SDK 管理器工具

图 5.15 - Android SDK 管理器工具

  1. 一旦 Android 设置正确配置,您可以看到 Android kit 已准备好进行开发,如下面的截图所示:图 5.16 - 正确配置的 Android kit

图 5.16 - 正确配置的 Android kit

  1. Kit选择选项中选择一个 Android kit,如下面的截图所示:图 5.17 - Android Kit 选择选项

图 5.17 - Android Kit 选择选项

  1. 在这一步中,您可以选择目标 Android 版本,并通过 Qt Creator 创建AndroidManifest.xml文件来配置您的 Android 应用程序。您可以设置包名称、版本代码、SDK 版本、应用程序图标、权限等。设置如下截图所示:图 5.18 - 构建设置中的 Android 清单选项

图 5.18 - 构建设置中的 Android 清单选项

  1. 您的计算机现在已准备好进行 Android 开发。但是,您的 Android 硬件需要启用开发者选项,或者使用 Android 模拟器。要启用Developer模式,转到Settings,点击System,然后点击About phone

  2. 然后,点击Software info,找到构建号。不断点击Builder number,直到看到Developer模式已激活。可能需要点击七次才能激活Developer模式。现在,返回到Settings面板,您现在将找到Developer选项。

  3. 您的 Android 设备已准备好运行 Android 应用程序。单击Run按钮,然后从Compatible device列表屏幕中选择设备。

  4. 接下来,点击build文件夹中生成的.apk文件。

恭喜!您已成功开发了 Android 应用程序。与 iOS 不同,Android 是一个开放系统。您可以将.apk文件复制或分发到运行相同 Android 版本的其他 Android 设备上,然后安装它。但是,如果您想在 Google Play 商店上分发您的应用程序,那么您将需要注册为 Google Play 开发者并签署包。

在本节中,我们学习了如何配置和构建 Android 平台。在下一节中,我们将讨论在本书编写时 Qt 6 支持的其他平台。

其他 Qt 支持的平台

Qt 5 支持广泛的平台,从桌面和移动平台到嵌入式和 Web 平台。Qt 6 尚未支持 Qt 5 中支持的所有平台,但随着 Qt 6 的成熟,这些平台将逐渐得到支持。目前,在商业许可下,Qt 6 的最新版本仅支持嵌入式 Linux。您可能需要等一段时间才能将应用程序移植到不同的嵌入式平台上的 Qt 6。否则,如果您想立即迁移到 Qt 6 以适用于您喜爱的嵌入式平台,您必须从源代码构建并进行必要的修改。

以下链接提供了 Qt 6.2 中嵌入式 Linux 支持的快照:doc-snapshots.qt.io/qt6-dev/embedded-linux.html。随着 Qt 迈向下一个版本,此链接可能会更新。

Qt 还为商业许可下的嵌入式 Linux 系统提供了Boot to Qt软件堆栈。这是一个轻量级的、经过 Qt 优化的完整软件堆栈,安装在目标系统上。Boot to Qt 软件堆栈使用传统的嵌入式 Linux 内核,设计有 Poky 和 Yocto 软件包。

在以下链接中了解更多关于 Boot to Qt 的信息:

doc.qt.io/QtForDeviceCreation/b2qt-index.html

Qt for WebAssembly 允许您为 Web 平台构建 Qt 应用程序。它不一定需要任何客户端安装,并节省服务器资源。它是一个平台插件,可以让您构建可以嵌入到网页中的 Qt 应用程序。在 Qt 6 中,尚未向开源开发人员提供此插件。商业许可证持有人可能会提前获得使用此插件的权限。

您可以在以下链接上了解有关 Qt for WebAssembly 插件的更多信息:

wiki.qt.io/Qt_for_WebAssembly

在本节中,我们了解了 Qt 6 支持的其他平台。在下一节中,我们将讨论如何将应用程序从 Qt 5 迁移到 Qt 6。

从 Qt 5 迁移到 Qt 6

Qt 6 是 Qt 框架的重大变化,因此它会破坏一些向后兼容性。因此,在升级到 Qt 6 之前,请确保您的 Qt 5 应用程序已更新到 Qt 5.15。从 Qt 5.15 迁移到 Qt 6 将更容易,需要的更改最少。但是,在 Qt 5.15 中标记为已弃用或过时的 API 在 Qt 6.0 中可能已被移除。

Qt 5 和 Qt 6 中的 CMake API 在语义上几乎是相同的。因此,Qt 5.15 引入了无版本目标和命令,允许编写完全独立于 Qt 版本的 CMake 代码。无版本导入目标对于需要同时进行 Qt 5 和 Qt 6 编译的项目非常有用。不建议默认使用它们,因为缺少目标属性。您可以在以下链接上阅读更多信息:doc.qt.io/qt-6/cmake-qt5-and-qt6-compatibility.html

在 Qt 6 中,一些类和模块已被移除,但这些类和模块在 Qt5Compat 中保留以便于迁移。除了构建系统的更改之外,您可能需要修复过时类的包含指令,例如,Qt6 中的类如 QLinkedListQRegExpQTextCodec 都被新类替换。但为了便于迁移,您需要将 core5compat 添加到您的 .pro 文件中,如下所示:

QT += core5compat

关于绘图机制也有一些变化。如果您使用了 OpenGL 风格的 qsb 工具,您的着色器代码应该编译成 Standard Portable Intermediate Representation-Vulkan (SPIR-V) 格式。我们将在 第八章 中详细讨论图形和动画。更多细节可以在以下链接找到:doc.qt.io/qt-6/qtshadertools-index.html

QtGraphicalEffects 也有一些变化,已从 Qt 6 中移除,并将以不同的许可证提供。Qt Quick MultiEffect 可在 Qt Marketplace 上获得,并提供更好的性能。您还可以考虑将 QML 中的早期信号连接更新为使用 JavaScript 函数声明,如以下代码片段所示:

Connections {
    target: targetElement
    function onSignalName() {//Do Something}
}

Qt 状态机模块在很大程度上与 Qt 5 版本兼容,因此您应该能够继续在其项目上工作,而不需要或只需要进行轻微的更改。要使用状态机模块的类,请将以下代码添加到您的 Qt 项目(.pro)文件中:

QT += statemachine

要在 QML 文件中导入状态机模块,请使用以下 import 语句:

import QtQml.StateMachine

Qt 提供了详细的迁移指南。如果您希望将 Qt 5 应用程序迁移到 Qt 6,请查看以下文档:

doc.qt.io/qt-6/portingguide.html

www.qt.io/blog/porting-from-qt-5-to-qt-6-using-qt5compat-library

doc.qt.io/qt-6/porting-to-qt6-using-clazy.html

在本节中,您学习了如何将您的应用程序从 Qt 5 迁移到 Qt 6。在下一节中,我们将总结本章学到的内容。

总结

本章介绍了使用 Qt Creator 进行跨平台开发。您了解了各种编译器、构建工具以及构建和特定平台的设置。在本章中,您学会了在桌面和移动平台上配置和构建应用程序,以及如何在 iPhone 和 Android 设备上运行应用程序。我们讨论了如何在不太多的挑战下将您的 Qt 项目移植到不同的平台。

在下一章中,您将学习有关信号和槽机制、Qt 元对象系统和事件处理的知识。让我们继续吧!

第三部分:高级编程、调试和部署

在本节中,您将学习高级编程和开发方法。您将学习在各种平台上调试、测试和部署 Qt 应用程序。您还将学习国际化以及如何构建高性能应用程序。

在本节中,有以下章节:

  • 第六章,信号和槽

  • 第七章,模型视图编程

  • 第八章,图形和动画

  • 第九章,测试和调试

  • 第十章,部署 Qt 应用程序

  • 第十一章,国际化

  • 第十二章,性能考虑

第六章:信号和槽

在之前的章节中,我们学习了如何使用 Qt Widgets 和 Qt Quick 创建 GUI 应用程序。但是为了使我们的应用程序可用,我们需要添加一个通信机制。信号机制是 Qt 的一个独特特性,使其与其他框架不同。信号和槽是通过 Qt 的元对象系统实现的。

在本章中,您将深入了解信号和槽以及它们的内部工作原理。您将能够从不同的类中接收通知并采取相应的行动。

在本章中,我们将讨论以下主题:

  • 理解 Qt 信号和槽

  • Qt 信号和槽的工作机制

  • 了解 Qt 的属性系统

  • 理解信号和处理程序事件系统

  • 理解事件和事件循环

  • 使用事件过滤器管理事件

  • 拖放

通过本章结束时,您将能够在 C++类与 QML 之间以及 QML 组件之间进行通信。

技术要求

本章的技术要求包括在最新的桌面平台上安装 Qt(6.0.0)和 Qt Creator(4.14.0)的最低版本,例如 Windows 10、Ubuntu 20.04 或 macOS 10.14。

本章中的所有代码都可以从以下 GitHub 链接下载:

github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp/tree/master/Chapter06

重要提示

本章中的屏幕截图是在 Windows 机器上拍摄的。您将在您的机器上看到基于底层平台的类似屏幕。

理解 Qt 信号和槽

在 GUI 编程中,当用户对任何 UI 元素执行任何操作时,另一个元素应该得到更新,或者应该执行某个特定的任务。为了实现这一点,我们需要对象之间的通信。例如,如果用户点击标题栏上的关闭按钮,预期窗口会关闭。不同的框架使用不同的方法来实现这种通信。回调是最常用的方法之一。回调是作为参数传递给另一个函数的函数。回调可能有多个缺点,并且可能在确保回调参数的类型正确性方面出现复杂性。

在 Qt 框架中,我们有一个称为信号和槽的回调技术的替代方法。信号是传递的消息,用于传达对象状态已更改。这个信号可能携带有关已发生更改的信息。槽是在特定信号的响应中调用的特殊函数。由于槽是函数,它们包含执行某个动作的逻辑。Qt Widgets 有许多预定义的信号,但您始终可以扩展您的类并向其添加自己的信号。同样,您也可以添加自己的槽来处理预期的信号。信号和槽使得实现观察者模式变得容易,同时避免样板代码。

为了能够通信,您必须连接相应的信号和槽。让我们了解信号和槽连接的连接机制和语法。

理解语法

要将信号连接到槽,我们可以使用QObject::connect()。这是一个线程安全的函数。标准语法如下:

QMetaObject::Connection QObject::connect(
       const QObject *senderObject, const char *signalName, 
       const QObject *receiverObject, const char *slotName, 
       Qt::ConnectionType type = Qt::AutoConnection)

在前面的连接中,第一个参数是发送方对象,而下一个参数是发送方的信号。第三个参数是接收方对象,而第四个是槽方法。最后一个参数是可选的,描述要建立的连接类型。它确定通知是立即传递给槽还是排队等待。在 Qt 6 中可以建立六种不同类型的连接。让我们来看看连接类型:

  • 使用Qt::DirectConnection;否则,使用Qt::QueuedConnection

  • Qt::DirectConnection:在这种情况下,信号和槽都位于同一线程中。信号发射后立即调用槽。

  • Qt::QueuedConnection:在这种情况下,槽位于另一个线程中。一旦控制返回到接收者线程的事件循环,就会调用槽。

  • Qt::QueuedConnection,除了发出信号的线程会阻塞,直到槽返回。如果发送者和接收者在同一线程中,则不能使用此连接以避免死锁。

  • 按位或。这用于避免重复连接。如果连接已经存在,则连接将失败。

  • Qt::BlockingQueuedConnection以避免死锁。您正在向同一线程发送事件,然后锁定线程,等待事件被处理。由于线程被阻塞,事件将永远不会被处理,线程将永远被阻塞,导致死锁。如果知道自己在做什么,请使用此连接类型。在使用此连接类型之前,必须了解两个线程的实现细节。

有几种连接信号和槽的方法。在指定信号和槽函数时,必须使用SIGNAL()SLOT()宏。最常用的语法如下:

QObject::connect(this, SIGNAL(signalName()), 
                 this, SLOT(slotName()));

这是自 Qt 诞生以来就存在的原始语法。但是,它的实现已经多次更改。新功能已添加,而不会破坏基本的应用程序编程接口API)。建议使用新的函数指针语法,如下所示:

connect(sender, &MyClass::signalName, this, 
        &MyClass::slotName);

这两种语法各有优缺点。您可以在以下链接中了解有关基于字符串基于函数对象连接之间的区别的更多信息:

doc.qt.io/qt-6/signalsandslots-syntaxes.html

如果连接失败,则前面的语句返回false。您还可以按如下方式连接到函数对象或 C++11 lambda:

connect(sender, &MyClass::signalName, this, [=]()
        { sender->doSomething(); });

您可以检查返回值以验证信号是否成功连接到槽。如果签名不兼容,或者信号和槽缺失,连接可能会失败。

重要说明

Qt::UniqueConnection不适用于 lambda、非成员函数和函数对象;它只能用于连接到成员函数。

信号和槽的签名可能包含参数,并且这些参数可能具有默认值。如果信号的参数至少与槽的参数一样多,并且相应参数的类型之间存在可能的隐式转换,则可以将信号连接到槽。让我们看一下具有不同参数数量的可行连接:

connect(sender, SIGNAL(signalName(int)), this, 
        SLOT(slotName(int)));
connect(sender, SIGNAL(signalName(int)), this, 
        SLOT(slotName()));
connect(sender, SIGNAL(signalName()), this, 
        SLOT(slotName()));

但是,以下情况将无法正常工作,因为槽的参数比信号的参数多:

connect(sender, SIGNAL(signalName()), this, 
        SLOT(slotName(int)));

您建立的每个连接都会发射一个信号,因此重复的连接会发射两个信号。您可以使用disconnect()来断开连接。

您还可以将 Qt 与第三方信号/槽机制一起使用。如果要在同一项目中使用两种机制,则将以下配置添加到 Qt 项目(.pro)文件中:

 CONFIG += no_keywords

让我们创建一个简单的信号和槽连接的示例。

声明信号和槽

要创建信号和槽,必须在自定义类中声明信号和槽。类的头文件将如下所示:

#ifndef MYCLASS_H
#define MYCLASS_H
#include <QObject>
class MyClass : public QObject
{
    Q_OBJECT
public:
    explicit MyClass(QObject *parent = nullptr);
signals:
    void signalName();
public slots:
    void slotName();
};
#endif // MYCLASS_H

如您所见,我们已向类添加了Q_OBJECT以便于信号和槽机制。您可以在头文件中使用signals关键字声明信号,如前面的代码片段所示。类似地,可以使用slots关键字声明槽。信号和槽都可以带有参数。在此示例中,我们使用相同的对象作为发送者和接收者,以使解释更简单。在大多数情况下,信号和槽将位于不同的类中。

接下来,我们将讨论如何将信号连接到槽。

将信号连接到槽

之前,我们声明了一个自定义信号和槽。现在,让我们看看如何连接它们。您可以在MyClass内定义信号和槽的连接,并发出信号,如下所示:

#include "myclass.h"
#include <QDebug>
MyClass::MyClass(QObject *parent) : QObject(parent)
{
    QObject::connect(this, SIGNAL(signalName()), 
               this, SLOT(slotName()));
    emit signalName();
}
void MyClass::slotName()
{
    qDebug()<< "Slot called!";
}

在连接后需要发出信号以调用槽。在前面的例子中,我们使用了信号和槽声明的传统方式。您可以将连接替换为最新的语法,如下所示:

connect(this, &MyClass::signalName, this, 
        &MyClass::slotName);

不仅可以将一个信号连接到一个槽,还可以连接多个槽和信号。同样,许多信号可以连接到一个槽。我们将在下一节中学习如何做到这一点。

将单个信号连接到多个槽

您可以将相同的信号连接到多个槽。这些槽将按照连接的顺序依次调用。假设一个名为signalX()的信号连接到名为slotA()slotB()slotC()的三个槽。当发出signalA()时,所有三个槽都将被调用。

让我们来看看传统的连接方式:

    QObject::connect(this, SIGNAL(signalX()),this, 
                     SLOT(slotA()));
    QObject::connect(this, SIGNAL(signalX()),this, 
                     SLOT(slotB()));
    QObject::connect(this, SIGNAL(signalX()),this, 
                     SLOT(slotC()));

您还可以按照新的语法创建连接,如下所示:

connect(this, &MyClass:: signalX, this, &MyClass:: slotA);
connect(this, &MyClass:: signalX, this, &MyClass:: slotB);
connect(this, &MyClass:: signalX, this, &MyClass:: slotC);

在下一节中,我们将学习如何将多个信号连接到单个槽。

将多个信号连接到单个槽

在前面的部分中,您学习了如何在单个信号和多个槽之间创建连接。现在,让我们看一下以下代码,以了解如何将多个信号连接到单个槽:

    QObject::connect(this, SIGNAL(signalX()),this, 
                     SLOT(slotX()));
    QObject::connect(this, SIGNAL(signalY()),this, 
                     SLOT(slotX()));
    QObject::connect(this, SIGNAL(signalZ()),this, 
                     SLOT(slotX()));

在这里,我们使用了三个不同的信号,分别是signalX()signalY()signalZ(),但是只定义了一个名为slotX()的槽。当任何一个这些信号被发出时,都会调用该槽。

在下一节中,我们将学习如何将一个信号连接到另一个信号。

连接一个信号到另一个信号

有时,您可能需要转发一个信号,而不是直接连接到一个槽。您可以按照以下方式将一个信号连接到另一个信号:

connect(sender, SIGNAL(signalA()),forwarder, 
        SIGNAL(signalB())));

您还可以按照新的语法创建连接,如下所示:

connect(sender,&ClassName::signalA,forwarder,&ClassName::
        signalB);

在前面的行中,我们已经将signalA()连接到signalB()。因此,当发出signalA()时,signalB()也将被发出,并且连接到signalB()的相应槽将被调用。假设我们的 GUI 中有一个按钮,并且我们希望将按钮点击转发为不同的信号。以下代码片段显示了如何转发信号:

#include <QWidget>
class QPushButton;
class MyClass : public QWidget
{
    Q_OBJECT
public:
    MyClass(QWidget *parent = nullptr);
    ~MyClass();
signals:
     void signalName();
 private:
     QPushButton *myButton;
};
MyClass::MyClass(QWidget *parent)
    : QWidget(parent)
{
    myButton = new QPushButton(this);
    connect(myButton, &QPushButton::clicked,
            this, &MyClass::signalName);
} 

在前面的例子中,我们将按钮点击信号转发到我们的自定义信号。我们可以调用连接到自定义信号的槽,就像之前讨论的那样。

在本节中,我们学习了如何进行连接以及如何使用信号和槽。现在,你可以在不同的类之间进行通信并共享信息。在下一节中,我们将学习信号和槽背后的工作机制。

Qt 信号和槽的工作机制

在前面的部分中,我们学习了信号和槽的语法以及如何连接它们。现在,我们将了解它是如何工作的。

在创建连接时,Qt 会查找信号和槽的索引。Qt 使用查找字符串表来找到相应的索引。然后,创建一个QObjectPrivate::Connection对象并将其添加到内部链接列表中。由于一个信号可以连接到多个槽,每个信号可以有一个连接的槽列表。每个连接包含接收者的名称和槽的索引。每个对象都有一个连接向量,与QObjectPrivate::Connection的链接列表中的每个信号相关联。

以下图示了ConnectionList如何在发件人和接收者对象之间创建连接:

图 6.1 - 发件人和接收者之间连接机制的说明

图 6.1 - 发件人和接收者之间连接机制的说明

ConnectionList是一个包含与对象之间所有连接的单向链表。signalVector包含给定信号的连接列表。每个Connection也是senders链表的一部分。使用链表是因为它们允许更快地添加和删除对象。每个对象还有一个反向连接列表,用于自动删除对象。有关详细的内部实现,请查看最新的qobject_p.h

woboq网站上有很多关于信号和槽工作原理的文章。您还可以在 woboq 网站上探索 Qt 源代码。如果需要更多信息,请访问以下链接:

woboq.com/blog/how-qt-signals-slots-work.html

现在,让我们了解一下 Qt 的元对象系统。

Qt 的元对象系统

Qt 的元对象系统是信号和槽机制背后的核心机制。它提供了诸如对象间通信、动态属性系统和运行时类型信息等功能。

元对象系统是通过三部分机制实现的。这些机制如下:

  • QObject

  • Q_OBJECT 宏

  • 元对象编译器

QObject类是所有 Qt 对象的基类。它是一个非常强大的机制,可以促进信号和槽机制。QObject类为可以利用元对象系统的对象提供了一个基类。QObject派生类在对象树中排列,从而在类之间创建了父子关系。当您创建一个QObject派生类,并将另一个QObject派生类作为父类时,该对象将自动添加到父类的children()列表中。父类将拥有该对象。GUI 编程需要运行时效率和高度的灵活性。Qt 通过将 C++的速度与 Qt 对象模型的灵活性相结合来实现这一点。Qt 通过基于从 QObject 继承的标准 C++技术来提供所需的功能。

您可以在以下链接了解有关 Qt 对象模型的更多信息:

doc.qt.io/qt-6/object.html

Q_OBJECT宏出现在类声明的私有部分。它用于启用 Qt 元对象系统提供的信号、槽和其他服务。

QObject派生类用于实现元对象特性。它提供了在运行时检查对象的能力。默认情况下,C++不支持内省。因此,Qt 创建了moc。这是一个处理 Qt 的 C++扩展的代码生成程序。该工具读取 C++头文件,如果找到Q_OBJECT宏,那么它会创建另一个包含元对象代码的 C++源文件。生成的文件包含了内省所需的代码。这两个文件被编译和链接在一起。除了为对象之间的通信提供信号和槽机制之外,元对象代码还提供了几个额外的功能,可以找到类名和继承详情,并且还可以帮助在运行时设置属性。Qt 的moc提供了一种超越编译语言功能的清晰方式。

您可以使用qobject_cast()QObject派生类上执行类型转换。qobject_cast()函数类似于标准的 C++ dynamic_cast()。优点是它不需要QObject,但如果您不添加Q_OBJECT宏,那么信号和槽以及其他元对象系统功能将不可用。没有元代码的QObject派生类等同于包含元对象代码的最近祖先。还有一个更轻量级的Q_OBJECT宏的版本,称为Q_GADGET,可以用于利用QMetaObject提供的一些功能。使用Q_GADGET的类没有信号或槽。

我们在这里看到了一些新关键字,如Q_OBJECTsignalsslotsemitSIGNALSLOT。这些被称为 C++的 Qt 扩展。它们是非常简单的宏,旨在被moc看到,定义在qobjectdefs.h中。其中,emit是一个空的宏,不会被moc解析。它只是为了给开发人员提供提示。

您可以在doc.qt.io/qt-6/why-moc.html了解为什么 Qt 使用moc来处理信号和槽。

在本节中,我们了解了 Qt 的元对象系统。在下一节中,我们将讨论moc生成的代码并讨论一些底层实现。

MOC 生成的代码

在本节中,我们将看一下 Qt6 中由moc生成的代码。当您构建之前的信号和槽示例时,您会在构建目录下看到生成的文件:moc_myclass.cppmoc_predefs.h。让我们用文本编辑器打开moc_myclass.cpp文件:

#include <memory>
#include "../../SignalSlotDemo/myclass.h"
#include <QtCore/qbytearray.h>
#include <QtCore/qmetatype.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'myclass.h' doesn't include 
        <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 68
#error "This file was generated using the moc from 6.0.2\. 
        It"
#error "cannot be used with the include files from this 
        version of Qt."
#error "(The moc has changed too much.)"
#endif

您可以在文件顶部找到有关 Qt 元对象编译器版本的信息。请注意,对此文件所做的所有更改将在重新编译项目时丢失。因此,请不要修改此文件中的任何内容。我们正在查看该文件以了解工作机制。

让我们看一下QMetaObject的整数数据。您可以看到有两列;第一列是计数,而第二列是数组中的索引:

static const uint qt_meta_data_MyClass[] = {
 // content:
       9,       // revision
       0,       // classname
       0,    0, // classinfo
       2,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       1,       // signalCount
 // signals: name, argc, parameters, tag, flags, initial 
 // metatype offsets
       1,    0,   26,    2, 0x06,    0 /* Public */,
 // slots: name, argc, parameters, tag, flags, initial 
 // metatype offsets
       3,    0,   27,    2, 0x0a,    1 /* Public */,
 // signals: parameters
    QMetaType::Void,
 // slots: parameters
    QMetaType::Void,
       0        // eod
};

在这种情况下,我们有一个方法,方法的描述从索引 14 开始。您可以在signalCount中找到可用信号的数量。对于每个函数,moc还保存每个参数的返回类型、它们的类型和它们的索引到名称。在每个元对象中,方法被赋予一个索引,从 0 开始。它们按信号、然后是槽,然后是其他函数排列。这些索引是相对索引,不包括父对象的索引。

当您进一步查看代码时,您会发现MyClass::metaObject()函数。这个函数返回动态元对象的QObject::d_ptr->dynamicMetaObject()metaObject()函数通常返回类的staticMetaObject

const QMetaObject *MyClass::metaObject() const
{
    return QObject::d_ptr->metaObject 
? QObject::d_ptr->dynamicMetaObject() 
: &staticMetaObject;
}

当传入的字符串数据匹配当前类时,必须将此指针转换为 void 指针并传递给外部世界。如果不是当前类,则调用父类的qt_metacast()来继续查询:

void *MyClass::qt_metacast(const char *_clname)
{
    if (!_clname) return nullptr;
    if (!strcmp(_clname, 
                qt_meta_stringdata_MyClass.stringdata0))
        return static_cast<void*>(this);
    return QObject::qt_metacast(_clname);
}

Qt 的元对象系统使用qt_metacall()函数来访问特定QObject对象的元信息。当我们发出一个信号时,会调用qt_metacall(),然后调用真实的信号函数:

int MyClass::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QObject::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 2)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 2;
    } else if (_c == QMetaObject::
               RegisterMethodArgumentMetaType) {
        if (_id < 2)
            *reinterpret_cast<QMetaType *>(_a[0]) = 
                                           QMetaType();
        _id -= 2;
    }
    return _id;
}

当您调用一个信号时,它调用了moc生成的代码,内部调用了QMetaObject::activate(),如下面的代码片段所示。然后,QMetaObject::activate()查看内部数据结构,以了解连接到该信号的槽。

您可以在qobject.cpp中找到此函数的详细实现:

void MyClass::signalName()
{
    QMetaObject::activate(this, &staticMetaObject, 0, 
                          nullptr);
}

通过这样做,您可以探索完整生成的代码并进一步查看符号。现在,让我们看一下moc生成的代码,其中调用了槽。槽是通过qt_static_metacall函数中的索引来调用的,如下所示:

void MyClass::qt_static_metacall(QObject *_o, 
    QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<MyClass *>(_o);
        (void)_t;
        switch (_id) {
        case 0: _t->signalName(); break;
        case 1: _t->slotName(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = void (MyClass::*)();
            if (*reinterpret_cast<_t *>(_a[1]) == 
                static_cast<_t>(&MyClass::signalName)) {
                *result = 0;
                return;
            }
        }
    }
    (void)_a;
}

参数的数组指针的格式与信号相同。_a[0]没有被触及,因为这里的一切都返回 void:

bool QObject::isSignalConnected(const QMetaMethod &signal) const

这将返回true,如果信号连接到至少一个接收器;否则,它将返回false

当对象被销毁时,QObjectPrivate::senders列表被迭代,并且所有Connection::receiver被设置为0。此外,Connection::receiver->connectionLists->dirty被设置为true。还要迭代每个QObjectPrivate::connectionLists以删除发送者列表中的连接

在本节中,我们浏览了一些moc生成的代码部分,并了解了信号和槽背后的工作机制。在下一节中,我们将学习 Qt 的属性系统。

了解 Qt 的属性系统

Qt 的属性系统类似于其他一些编译器供应商。但是它提供了跨平台的优势,并且可以与 Qt 在不同平台上支持的标准编译器一起使用。要添加一个属性,您必须将Q_PROPERTY()宏添加到QObject派生类中。这个属性就像一个类数据成员,但它提供了通过元对象系统可用的额外功能。一个简单的语法如下所示:

Q_PROPERTY(type variableName READ getterFunction 
           WRITE setterFunction  NOTIFY signalName)

在上面的语法中,我们使用了一些最常见的参数。但是语法支持更多的参数。您可以通过阅读 Qt 文档了解更多信息。让我们看一下下面使用MEMBER参数的代码片段:

     Q_PROPERTY(QString text MEMBER m_text NOTIFY 
                textChanged)
signals:
     void textChanged(const QString &newText);
private:
     QString m_text;

在上面的代码片段中,我们使用MEMBER关键字将一个成员变量导出为 Qt 属性。这里的类型是QStringNOTIFY信号用于实现 QML 属性绑定。

现在,让我们探讨如何使用元对象系统读取和写入属性。

使用元对象系统读取和写入属性

让我们创建一个名为MyClass的类,它是QWidget的子类。让我们在其私有部分添加Q_OBJECT宏以启用属性系统。在这个例子中,我们想在MyClass中创建一个属性来跟踪版本的值。属性的名称将是version,其类型将是QString,它在MyClass中定义。让我们看一下下面的代码片段:

class MyClass : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(QString version READ version WRITE 
               setVersion NOTIFY versionChanged)
public:
    MyClass(QWidget *parent = nullptr);
    ~MyClass();
    void setVersion(QString version)
    {
        m_version = version;
        emit versionChanged(version);
    }
    QString version() const { return m_version; }
    signals:
        void versionChanged(QString version);
    private:
       QString m_version;
};

要获得属性更改通知,您必须在更改version值后发出versionChanged()

让我们看一下上面示例的main.cpp文件:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyClass myClass;
    myClass.setVersion("v1.0");
    myClass.show();
    return a.exec();
}

在上面的代码片段中,通过调用setVersion()来设置属性。您可以看到每次更改版本时都会发出versionChanged()信号。

您还可以使用QObject::property()读取属性,并使用QObject::setProperty()写入属性。您还可以使用QObject::property()查询动态属性,类似于编译时的Q_PROPERTY()声明。

您也可以这样设置属性:

QObject *object = &myClass;
object->setProperty("version", "v1.0");

在本节中,我们讨论了属性系统。在下一节中,我们将学习 Qt Designer 中的信号和槽。

在 Qt Designer 中使用信号和槽

如果您使用 Qt Widgets 模块,那么可以使用 Qt Designer 在表单中编辑信号和槽连接。Qt 默认小部件带有许多信号和槽。让我们看看如何在 Qt Designer 中实现信号和槽而不编写任何代码。

您可以将Dial控件和Slider控件拖放到表单上。您可以通过底部选项卡上的信号和槽编辑器添加连接,如下面的截图所示:

图 6.2 - 使用 Qt Designer 创建信号和槽连接

图 6.2 - 使用 Qt Designer 创建信号和槽连接

或者,您可以按下F4或从顶部工具栏中选择编辑信号/槽按钮。然后,您可以选择控件并通过将连接拖动到接收器来创建连接。如果您为自定义类定义了自定义信号或槽,它们将自动显示在信号和槽编辑器中。但是,大多数开发人员更喜欢在 C++源文件中定义连接。

在本节中,我们讨论了使用 Qt Designer 在 Qt Widgets 中实现信号和槽。现在,让我们看一下在 QML 中如何处理信号。

了解 QML 中的信号和处理程序事件系统

之前,我们学习了如何在 C++源文件中连接信号和槽,并在 Qt Widgets 模块中使用它们。现在,让我们看看如何在 QML 中进行通信。QML 具有类似信号和槽的信号和处理程序机制。在 QML 文档中,信号是一个事件,通过信号处理程序响应信号。与 C++中的槽一样,当在 QML 中发射信号时,将调用信号处理程序。在 Qt 术语中,该方法是连接到信号的槽;在 QML 中定义的所有方法都被创建为 Qt 槽。因此,在 QML 中没有单独的槽声明。信号是来自对象的通知,表明发生了某个事件。您可以在 JavaScript 或方法内放置逻辑以响应信号。

让我们看看如何编写信号处理程序。您可以按如下方式声明信号处理程序:

onSignalName : {
//Logic
}

这里,signalName是信号的名称。在编写处理程序时,信号的名称的第一个字母应大写。因此,这里的信号处理程序被命名为onSignalName。信号和信号处理程序应该在同一个对象内定义。信号处理程序内的逻辑是一段 JavaScript 代码块。

例如,当用户在鼠标区域内点击时,将发射clicked()信号。要处理clicked()信号,我们必须添加onClicked:{...}信号处理程序。

信号处理程序是由 QML 引擎在关联信号被发射时调用的简单函数。当您向 QML 对象添加信号时,Qt 会自动向对象定义中添加相应的信号处理程序。

让我们首先在 QML 文档中添加一个自定义信号。

在 QML 中添加信号

要在 QML 类中添加信号,必须使用signal关键字。定义新信号的语法如下:

signal <name>[([<type> <parameter name>[...]])]

以下是一个示例:

signal composeMessage(string message)

信号可以带参数也可以不带参数。如果没有为信号声明参数,则可以省略()括号。您可以通过调用它作为函数来发射信号:

Rectangle {
    id: mailBox
    signal composeMessage(string message)
    anchors.fill: parent
    Button {
        id:sendButton
        anchors.centerIn: parent
        width: 100
        height: 50
        text: "Send"
        onClicked:  mailBox.composeMessage("Hello World!")
    }
    onComposeMessage: {
        console.log("Message Received",message)
    }
}

在前面的示例中,我们在 QML 文件中添加了一个自定义信号composeMessage()。我们使用了相应的信号处理程序onComposeMessage()。然后,我们添加了一个按钮,当点击按钮时会发射composeMessage()信号。当您运行此示例时,您将看到在点击按钮时信号处理程序会自动调用。

在本节中,您学习了如何声明信号以及如何实现相应的信号处理程序。在下一节中,我们将把信号连接到函数。

将信号连接到函数

您可以将信号连接到 QML 文档中定义的任何函数。您可以使用connect()将信号连接到函数或另一个信号。当信号连接到函数时,每当信号被发射时,该函数将自动调用。这种机制使得信号可以被函数而不是信号处理程序接收。

在以下代码片段中,使用connect()函数将composeMessage()信号连接到transmitMessage()函数:

Rectangle {
    id: mailBox
    signal composeMessage(string message)
    anchors.fill: parent
    Text {
        id: textElement
        anchors {
            top:  parent.top
            left: parent.left
            right:parent.right
        }
        width: 100
        height:50
        text: ""
        horizontalAlignment: Text.AlignHCenter
    }
    Component.onCompleted: {
        mailBox.composeMessage.connect(transmitMessage)
        mailBox.composeMessage("Hello World!")
    }
    function transmitMessage(message) {
        console.log("Received message: " + message)
        textElement.text = message
    }
}

在 QML 中,信号处理是使用以下语法实现的:

sender.signalName.connect(receiver.slotName)

您还可以使用disconnect()函数来删除连接。您可以这样断开连接:

sender.signalName.disconnect(receiver.slotName)

现在,让我们探讨如何在 QML 中转发信号。

将信号连接到另一个信号

您可以在 QML 中将信号连接到另一个信号。您可以使用connect()函数实现这一点。

让我们通过以下示例来探讨如何做到这一点:

Rectangle {
    id: mailBox
    signal forwardButtonClick()
    anchors.fill: parent
    Button {
        id:sendButton
        anchors.centerIn: parent
        width: 100
        height: 50
        text: "Send"
    }
    onForwardButtonClick: {
        console.log("Fordwarded Button Click Signal!")
    }
    Component.onCompleted: {
        sendButton.clicked.connect(forwardButtonClick)
    }
}

在前面的示例中,我们将clicked()信号连接到forwardButtonClick()信号。您可以在onForwardButtonClick()信号处理程序内部的根级别实现必要的逻辑。您还可以从按钮点击处理程序中发射信号,如下所示:

onClicked: {
    mailBox.forwardButtonClick()
}

在本节中,我们讨论了如何连接两个信号并处理它们。在下一节中,我们将讨论如何使用信号和槽在 C++类和 QML 之间进行通信。

定义属性属性并理解属性绑定

之前,我们学习了如何通过注册类的Q_PROPERTY来定义 C++中的类型,然后将其注册到 QML 类型系统中。在 QML 文档中也可以创建自定义属性。属性绑定是 QML 的核心特性,允许我们创建各种对象属性之间的关系。您可以使用以下语法在 QML 文档中声明属性:

[default] property <propertyType> <propertyName> : <value>

通过这种方式,您可以将特定参数暴露给外部对象,或更有效地维护内部状态。让我们看一下以下属性声明:

property string version: "v1.0"

当您声明自定义属性时,Qt 会隐式创建该属性的属性更改信号。相关的信号处理程序是on<PropertyName>Changed,其中<PropertyName>是属性的名称,首字母大写。对于先前声明的属性,相关的信号处理程序是onVersionChanged,如下所示:

onVersionChanged:{…}

如果属性被分配了静态值,那么它将保持不变,直到显式分配新值。要动态更新这些值,您应该在 QML 文档中使用属性绑定。我们之前使用了简单的属性绑定,如下面的代码片段所示:

width: parent.width

然而,我们可以将其与后端 C++类暴露的属性结合使用,如下所示:

property string version: myClass.version

在上一行中,myClass是已在 QML 引擎中注册的后端 C++对象。在这种情况下,每当从 C++端发出versionChanged()变化信号时,QML 的version属性会自动更新。

接下来,我们将讨论如何在 C++和 QML 之间集成信号和槽。

在 C++和 QML 之间集成信号和槽

在 C++中,要与 QML 层交互,可以使用信号、槽和Q_INVOKABLE函数。您还可以使用Q_PROPERTY宏创建属性。要响应来自对象的信号,可以使用Connections QML 类型。当 C++文件中的属性发生变化时,Q_PROPERTY会自动更新值。如果属性与任何 QML 属性绑定,它将自动更新 QML 中的属性值。在这种情况下,信号槽机制会自动建立。

让我们看一下以下示例,它使用了上述的机制:

class CPPBackend : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int counter READ counter WRITE setCounter 
               NOTIFY counterChanged)
public:
    explicit CPPBackend(QObject *parent = nullptr);
     Q_INVOKABLE  void receiveFromQml();
    int counter() const;
    void setCounter(int counter);
signals:
    void sendToQml(int);
    void counterChanged(int counter);
private:
    int m_counter = 0;
};

在上面的代码中,我们声明了基于 Q_PROPERTY 的通知。当发出counterChanged()信号时,我们可以获取新的counter值。然而,我们使用了receiveFromQml()函数作为Q_INVOKABLE函数,这样我们就可以直接在 QML 文档中调用它。我们正在发出sendToQml(),这在main.qml中进行处理:

void CPPBackend::setCounter(int counter)
{
    if (m_counter == counter)
        return;
    m_counter = counter;
    emit counterChanged(m_counter);
}
void CPPBackend::receiveFromQml()
{
    // We increase the counter and send a signal with new 
    // value
    ++m_counter;
    emit sendToQml(m_counter);
}

现在,让我们看一下 QML 的实现:

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("C++ QML Signals & Slots Demo")
    property int count: cppBackend.counter
    onCountChanged:{
        console.log("property is notified. Updated value 
                    is:",count)
    }
    Connections {
        target: cppBackend
        onSendToQml: {
            labelCount.text ="Fetched value is " 
                              +cppBackend.counter
        }
    }
    Row{
        anchors.centerIn: parent
        spacing: 20
        Text {
            id: labelCount
            text: "Fetched value is " + cppBackend.counter
        }
        Button {
            text: qsTr("Fetch")
            width: 100 ;height: 20
            onClicked: {
                cppBackend.receiveFromQml()
            }
        }
    }
}

在上面的示例中,我们使用Connections来连接到 C++信号。在按钮点击时,我们调用receiveFromQml() C++函数,在那里我们发出信号。我们还声明了count属性,它也监听counterChanged()。我们在相关的信号处理程序onCountChanged中处理数据;也就是说,我们也可以根据通知更新labelCount数据:

图 6.3 - 在这个例子中使用的机制

图 6.3 - 在这个例子中使用的机制

上图说明了此示例中的通信机制。为了解释的目的,我们在同一个示例中保留了多种方法,以解释 C++和 QML 之间的通信机制。

在本节中,您通过示例学习了信号和槽机制。在下一节中,我们将学习 Qt 中的事件和事件循环。

理解事件和事件循环

Qt 是一个基于事件的系统,所有 GUI 应用程序都是事件驱动的。在事件驱动的应用程序中,通常有一个主循环,它监听事件,然后在检测到其中一个事件时触发回调函数。事件可以是自发的或合成的。自发事件来自外部环境。合成事件是应用程序生成的自定义事件。在 Qt 中,事件是表示已发生的事情的通知。Qt 事件是值类型,派生自QEvent,为每个事件提供了类型枚举。在 Qt 应用程序内部产生的所有事件都封装在从QEvent类继承的对象中。所有QObject派生类都可以重写QObject::event()函数,以处理其实例所针对的事件。事件可以来自应用程序内部和外部。

当事件发生时,Qt 通过构造适当的QEvent子类实例来产生一个事件对象,然后通过调用其event()函数将其传递给特定的QObject实例。与信号和槽机制不同,信号连接的槽通常会立即执行,事件必须等待其轮次,直到事件循环分发所有先前到达的事件。您必须根据您的预期实现选择正确的机制。以下图表说明了事件在事件驱动应用程序中是如何创建和管理的:

图 6.4 - 使用事件循环的事件驱动应用程序的说明

图 6.4 - 使用事件循环的事件驱动应用程序的说明

我们可以通过调用QCoreApplication::exec()进入 Qt 的主事件循环。应用程序会一直运行,直到调用QCoreApplication::exit()QCoreApplication::quit(),这将终止循环。QCoreApplication可以在 GUI 线程中处理每个事件并将事件转发给 QObjects。请注意,事件不会立即传递;相反,它们会排队在事件队列中,并稍后依次处理。事件调度程序循环遍历此队列,将它们转换为QEvent对象,然后将事件分派到目标QObject

简化的事件循环调度器可能如下所示:

while(true) 
{
  dispatchEventsFromQueue();
  waitForEvents();
}

与事件循环相关的一些重要 Qt 类如下:

  • event队列。

  • event循环。

  • 非 GUI 应用程序的event循环。

  • GUI 应用程序的event循环。

  • QThread用于创建自定义线程和管理线程。

  • QSocketNotifier用于监视文件描述符上的活动。

  • event循环。

您可以在 Qt 文档中了解这些类。以下链接提供了有关事件系统的更深入了解:

wiki.qt.io/Threads_Events_QObjects

在本节中,我们讨论了事件和 Qt 的事件循环。在下一节中,我们将学习如何使用事件过滤器过滤事件。

使用事件过滤器管理事件

在本节中,您将学习如何管理事件,如何过滤特定事件并执行任务。您可以通过重新实现事件处理程序和安装事件过滤器来实现事件过滤。您可以通过对感兴趣的小部件进行子类化并重新实现该事件处理程序来重新定义事件处理程序应该执行的操作。

Qt 提供了五种不同的事件处理方法,如下所示:

  • 重新实现特定事件处理程序,如paintEvent()

  • 重新实现QObject::event()函数

  • QObject实例上安装事件过滤器

  • QApplication实例上安装事件过滤器

  • 子类化QApplication并重新实现notify()

以下代码处理了自定义小部件上的鼠标左键单击,同时将所有其他按钮点击传递给基类QWidget

void MyClass::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) 
    {
        // Handle left mouse button here
    } 
    else 
    {
        QWidget::mousePressEvent(event);
    }
}

在前面的示例中,我们仅过滤了左键按下事件。您可以在相应的块内添加所需的操作。以下图示了高级事件处理机制:

图 6.5 - 事件过滤器机制的说明

图 6.5 - 事件过滤器机制的说明

事件过滤器可以安装在应用程序实例或本地对象上。如果事件过滤器安装在QCoreApplication对象中,则所有事件将通过此事件过滤器。如果它安装在派生自QObject的类中,则发送到该对象的事件将通过事件过滤器。有时,可能没有适合特定操作的 Qt 事件类型。在这种情况下,可以通过从QEvent创建子类来创建自定义事件。您可以重新实现QObject::event()以过滤所需的事件,如下所示:

#include <QWidget>
#include <QEvent>
class MyCustomEvent : public QEvent
{
public:
    static const QEvent::Type MyEvent 
                 = QEvent::Type(QEvent::User + 1);
};
class MyClass : public QWidget
{
    Q_OBJECT
public:
    MyClass(QWidget *parent = nullptr);
    ~MyClass();
protected:
    bool event(QEvent *event);
}; 

在这里,我们创建了一个名为MyCustomEvent的自定义事件类,并创建了一个自定义类型。

现在,让我们通过重新实现event()来过滤这些事件:

bool MyClass::event(QEvent *event)
{
    if (event->type() == QEvent::KeyPress)
    {
        QKeyEvent *keyEvent= static_cast<QKeyEvent 
                                         *>(event);
        if (keyEvent->key() == Qt::Key_Enter)
        {
            // Handle Enter event event
            return true;
        }
    }
    else if (event->type() == MyCustomEvent::MyEvent)
    {
        MyCustomEvent *myEvent = static_cast<MyCustomEvent 
                                 *>(event);
        // Handle custom event
        return true;
    }
    return QWidget::event(event);
}

如您所见,我们已将其他事件传递给QWidget::event()以进行进一步处理。如果要阻止事件进一步传播,则return true;否则,return false

事件过滤器是一个接收发送到对象的所有事件的对象。过滤器可以停止事件或将其转发给对象。如果对象已被安装为监视对象的事件过滤器,则它会筛选事件。还可以使用事件过滤器监视另一个对象的事件并执行必要的任务。以下示例显示了如何使用事件过滤器方法重新实现最常用的事件之一 - 按键事件。

让我们看一下以下代码片段:

#include <QMainWindow>
class QTextEdit;
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
protected:
    bool eventFilter(QObject *obj, QEvent *event) override;
private:
    QTextEdit *textEdit;
};

在前面的代码中,我们创建了一个名为MainWindow的类,并重写了eventFilter()。让我们使用installEventFilter()textEdit上安装过滤器。您可以在一个对象上安装多个事件过滤器。但是,如果在单个对象上安装了多个事件过滤器,则最后安装的过滤器将首先被激活。您还可以通过调用removeEventFilter()来移除事件过滤器:

#include "mainwindow.h"
#include <QTextEdit>
#include <QKeyEvent>
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    textEdit = new QTextEdit;
    setCentralWidget(textEdit);
    textEdit->installEventFilter(this);
}

在前面的代码中,我们在textEdit对象上安装了一个eventFilter。现在,让我们看一下eventFilter()函数:

bool MainWindow::eventFilter(QObject *monitoredObj, QEvent *event)
{
    if (monitoredObj == textEdit)
    {
        if (event->type() == QEvent::KeyPress)
        {
            QKeyEvent *keyEvent = static_cast<QKeyEvent*>
                                  (event);
            qDebug() << "Key Press detected: " << 
                                          keyEvent->text();
            return true;
        }
        else
        {
            return false;
        }
    }
    else
    {
        return QMainWindow::eventFilter(monitoredObj, 
                                        event);
    }
}

在这里,textEdit是被监视的对象。每次按键时,如果textEdit处于焦点状态,则会捕获事件。由于可能有更多的子对象和QMainWindow可能需要事件,不要忘记将未处理的事件传递给基类以进行进一步的事件处理。

重要提示

eventFilter()函数中消耗了事件后,确保return true。如果接收对象被删除并且return false,那么可能导致应用程序崩溃。

您还可以将信号和槽机制与事件结合使用。您可以通过过滤事件并发出与该事件对应的信号来实现这一点。希望您已经了解了 Qt 中的事件处理机制。现在,让我们来看看拖放。

拖放

在本节中,我们将学习拖放DnD)。在 GUI 应用程序中,DnD 是一种指向设备手势,用户通过抓取虚拟对象然后释放到另一个虚拟对象来选择虚拟对象。拖放操作在用户进行被识别为开始拖动操作的手势时开始。

让我们讨论如何使用 Qt 小部件实现拖放。

Qt 小部件中的拖放

在基于 Qt Widgets 的 GUI 应用程序中,使用拖放时,用户从特定的小部件开始拖动,并将被拖动的对象放到另一个小部件上。这要求我们重新实现几个函数并处理相应的事件。需要重新实现的最常见函数如下:

void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dropEvent(QDropEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;

一旦您重新实现了上述函数,可以使用以下语句在目标小部件上启用放置:

setAcceptDrops(true);

要开始拖动,创建一个QDrag对象,并传递一个指向开始拖动的小部件的指针。拖放操作由QDrag对象处理。此操作要求附加数据描述为多用途互联网邮件扩展MIME)类型。

QMimeData *mimeData = new QMimeData;
mimeData->setData("text/csv", csvData);
QDrag *dragObject = new QDrag(event->widget());
dragObject->setMimeData(mimeData);
dragObject->exec();

上面的代码显示了如何创建一个拖动对象并设置自定义 MIME 类型。在这里,我们使用text/csv作为 MIME 类型。您可以使用拖放操作提供多种类型的 MIME 编码数据。

要拦截拖放事件,可以重新实现dragEnterEvent()。当拖动正在进行并且鼠标进入小部件时,将调用此事件处理程序。

您可以在 Qt Creator 的示例部分中找到几个相关示例。由于 Qt 小部件在当今并不十分流行,我们将跳过使用小部件进行拖放的示例。在下一节中,我们将讨论 QML 中的拖放。

在 QML 中进行拖放

在前面的部分中,我们讨论了使用小部件进行拖放。由于 QML 用于创建现代和触摸友好的应用程序,拖放是一个非常重要的功能。Qt 提供了几种方便的 QML 类型来实现拖放。在内部,相应的事件处理方式是相似的。这些函数在QQuickItem类中声明。

例如,dragEnterEvent()也在QQuickItem中可用,用于拦截拖放事件,如下所述:

void QQuickItem::dragEnterEvent(QDragEnterEvent *event)

让我们讨论如何使用可用的 QML 类型来实现这一点。使用Drag附加属性,任何Item都可以在 QML 场景中成为拖放事件的源。DropArea是一个可以在其上拖动项目时接收事件的不可见项目。当项目上存在拖动操作时,对其位置进行的任何更改都将生成一个拖动事件,该事件将发送到任何相交的DropAreaDragEvent QML 类型提供有关拖动事件的信息。

以下代码片段显示了在 QML 中进行简单拖放操作:

Rectangle {
    id: dragItem
    property point beginDrag
    property bool caught: false
    x: 125; y: 275
    z: mouseArea.drag.active ||  mouseArea.pressed ? 2 : 1
    width: 50; height: 50
    color: "red"
    Drag.active: mouseArea.drag.active
    Drag.hotSpot.x: 10 ; Drag.hotSpot.y: 10
    MouseArea {
    id: mouseArea
    anchors.fill: parent
    drag.target: parent
    onPressed: dragItem.beginDrag = Qt.point(dragItem.x, 
                                             dragItem.y)
    onReleased: {
          if(!dragItem.caught) {
          dragItem.x = dragItem.beginDrag.x
          dragItem.y = dragItem.beginDrag.y
      }
    }
  }
}

在上面的代码中,我们创建了一个 ID 为dragItem的可拖动项。它包含一个MouseArea来捕获鼠标按下事件。拖动不仅限于鼠标拖动。任何可以生成拖动事件的东西都可以触发拖动操作。可以通过调用Drag.cancel()或将Drag.active状态设置为false来取消拖动。

通过调用Drag.drop()可以完成放置操作。让我们添加一个DropArea

Rectangle {
    x: parent.width/2
    width: parent.width/2 ; height:parent.height
    color: "lightblue"
    DropArea {
    anchors.fill: parent
    onEntered: drag.source.caught = true
    onExited: drag.source.caught = false
    }
}

在上面的代码片段中,我们使用浅蓝色矩形将其区分为屏幕上的DropArea。当dragItem进入DropArea区域时,我们捕获它。当dragItem离开DropArea区域时,放置操作被禁用。因此,当放置不成功时,项目将返回到其原始位置。

在本节中,我们了解了拖放操作及其相应的事件。我们讨论了如何在 Qt Widgets 模块以及在 QML 中实现它们。现在,让我们总结一下本章学到的内容。

摘要

在本章中,我们了解了 Qt 中信号和槽的核心概念。我们讨论了连接信号和槽的不同方式。我们还学习了如何将一个信号连接到多个槽,以及多个信号连接到单个槽。然后,我们看了如何在 Qt 小部件中使用它们,以及在 QML 中使用它们,以及信号和槽连接背后的机制。之后,您学会了如何使用信号和槽在 C++和 QML 之间进行通信。

本章还讨论了 Qt 中的事件和事件循环。我们探讨了如何使用事件而不是信号槽机制。在这之后,我们创建了一个带有自定义事件处理程序的示例程序,以捕获事件并对其进行过滤。

在了解了事件之后,我们实现了一个简单的拖放示例。现在,您可以在类之间、在 C++和 QML 之间进行通信,并根据事件实现必要的操作。

在下一章中,我们将学习关于模型视图编程以及如何创建自定义模型。

第七章:模型视图编程

模型/视图编程用于在 Qt 中处理数据集时将数据与视图分离。模型/视图(M/V)架构区分了功能,使开发人员可以以多种方式修改和呈现用户界面(UI)上的信息。我们将讨论架构的每个组件,Qt 提供的相关便利类,以及如何使用实际示例。在本章中,我们将讨论模型视图模式并了解基本核心概念。

在本章中,我们将讨论以下主题:

  • M/V 架构的基本原理

  • 使用模型和视图

  • 创建自定义模型和委托

  • 在 Qt 小部件中使用 M/V 显示信息

  • 在 QML 中使用 M/V 显示信息

  • 使用 C++模型与 QML

在本章结束时,您将能够创建数据模型并在自定义 UI 上显示信息。您将能够编写自定义模型和委托。您还将学会通过 Qt 小部件和 QML 在 UI 中表示信息。

技术要求

本章的技术要求包括在最新的桌面平台之一(如 Windows 10、Ubuntu 20.04 或 macOS 10.14)上安装 Qt 6.0.0 和 Qt Creator 4.14.0 的最低版本。

本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp/tree/master/Chapter07

重要提示

本章中使用的屏幕截图是在 Windows 平台上获取的。您将在您的机器上基于底层平台看到类似的屏幕。

理解 M/V 架构

传统上,在构建 UI 时经常使用模型-视图-控制器(MVC)设计模式。顾名思义,它由三个术语组成:模型、视图和控制器。模型是具有动态数据结构和逻辑的独立组件,视图是视觉元素,控制器决定 UI 如何响应用户输入。在 MVC 出现之前,开发人员通常将这些组件放在一起。虽然开发人员希望将控制器与其他组件分离,但并不总是可能。MVC 设计将组件解耦以增加灵活性和重用。以下图示了传统 MVC 模式的组件:

图 7.1 – 传统 MVC 设计模式

图 7.1 – 传统 MVC 设计模式

在 MVC 模式中,用户看到视图并与控制器交互。控制器将数据发送到模型,模型更新视图。如果视图和控制器组件合并,则会得到 M/V 架构。它提供了更灵活的架构。它基于相同的原则,但使实现变得更简单。修改后的架构允许我们在多个不同的视图中显示相同的数据。开发人员可以实现新类型的视图而不更改底层数据结构。为了将这种灵活性带入我们对用户输入的处理中,Qt 引入了委托的概念。视图接收通过委托更新的数据,而不是通过控制器。它有两个主要目的:

  • 为了帮助视图呈现每个值

  • 为了帮助视图在用户想要进行一些更改时

因此,在某种程度上,控制器已与视图合并,并且视图还通过委托执行了一些控制器的工作。拥有委托的好处在于它提供了渲染和修改数据元素的手段。

让我们通过图表了解 M/V 的实现和其组件:

图 7.2 – Qt 模型-视图-委托框架

图 7.2 - Qt 模型-视图-委托框架

图 7.2所示,M/V 组件分为模型视图委托三个部分。模型与数据库交互,并作为架构其他组件的接口。通信的目的由数据源和模型的实现确定。视图获取称为模型索引的数据项的引用。视图可以通过使用这个模型索引从数据模型中检索单个数据项。在标准视图中,委托渲染数据项。当数据项被修改时,委托使用模型索引通知模型。

图 7.3说明了模型如何向视图提供数据,并在单个委托上显示:

图 7.3 - 模型-视图-委托实现示意图

图 7.3 - 模型-视图-委托实现示意图

Qt 框架提供了一组标准类,实现了 M/V 架构,用于管理数据与用户视图之间的关系。通过解耦功能,该架构提供了灵活性,可以定制数据的呈现方式,并允许将广泛的数据源与视图结合起来。

模型、视图和委托使用信号和槽机制进行通信。模型发出信号通知数据源中发生的数据更改。当用户与视图交互时,视图发出信号通知用户操作。委托发出信号通知模型和视图有关编辑状态的变化。

现在,您已经了解了 M/V 架构的基础知识。接下来的部分将解释如何在 Qt 中使用 M/V 模式。我们将从 Qt 框架提供的标准类开始,然后讨论在 Qt 部件中使用 M/V。您将学习如何根据 M/V 架构创建新组件。让我们开始吧!

模型

M/V 消除了标准部件可能出现的数据一致性挑战。它使得可以更容易地为相同数据使用多个视图,因为一个模型可以传递给多个视图。Qt 提供了几个 M/V 实现的抽象类,具有共同的接口和特定的功能实现。您可以对抽象类进行子类化,并添加其他组件期望的功能。在 M/V 实现中,模型提供了供视图和委托访问数据的标准接口。

Qt 提供了一些现成的模型类,如QStandardItemModelQFileSystemModelQSqlTableModelQAbstractItemModel是 Qt 定义的标准接口。QAbstractItemModel的子类表示分层结构中的数据。图 7.4说明了模型类的层次结构:

图 7.4 - Qt 中模型类的层次结构

图 7.4 - Qt 中模型类的层次结构

视图使用这种方法访问模型中的单个数据项,但在呈现信息给用户的方式上并没有受到限制。通过模型传递的数据可以保存在数据结构或数据库中,也可以是其他应用程序组件。所有的项模型都是基于QAbstractItemModel类的。

图 7.5显示了不同类型的模型中数据的排列方式:

图 7.5 - 不同类型的模型和数据排列方式

图 7.5 - 不同类型的模型和数据排列方式

数据通过模型以表格形式表示,以行和列的形式表示,或者使用数据的分层表示。在 M/V 模式中,小部件不会在单元格后面存储数据。它们直接使用数据。您可能需要创建一个包装器,使您的数据与QAbstractItemModel接口兼容。视图使用此接口来读取和写入数据。任何从QAbstractItemModel派生的类都称为模型。它提供了一个处理以列表、表格和树形式表示数据的视图的接口。要为列表或类似表格的数据结构实现自定义模型,可以从QAbstractListModelQAbstractTableModel派生以使用可用的功能。子类提供了适用于特定列表和表格的模型。

Qt 框架提供了两种标准类型的模型。它们如下:

  • QStandardItemModel

  • QFileSystemModel

QStandardItemModel是一个多用途模型,可以存储自定义数据。每个元素都指代一个项目。它可以用于显示列表、表格和树形视图所需的各种数据结构。它提供了一种传统的基于项目的处理模型。QStandardItem提供了在QStandardItemModel中使用的项目。

QFileSystemModel是一个保持目录内容信息的模型。它简单地表示本地文件系统上的文件和目录,并不保存任何数据项。它提供了一个现成的模型,用于创建一个示例应用程序,并且可以使用模型索引来操作数据。现在,让我们讨论一下委托是什么。

委托

委托提供对视图中显示的项目呈现的控制。M/V 模式与 MVC 模式不同,它没有一个完全不同的组件来处理用户交互。视图主要负责将模型数据显示给用户,并允许用户与其交互。为了增加用户操作的灵活性,委托处理这些交互。它赋予了某些小部件作为模型中可编辑项目的编辑器。委托用于提供交互功能并渲染视图中的单个字段。QAbstractItemDelegate类定义了管理委托的基本接口。Qt 提供了一些现成的委托类,可用于与内置小部件一起使用以修改特定的数据类型。

为了更好地理解,我们将看一下 Qt 框架中委托类的层次结构(见图 7.6):

图 7.6 - Qt 框架中委托类的层次结构

图 7.6 - Qt 框架中委托类的层次结构

正如我们在前面的图表中所看到的,QAbstractItemDelegate是委托的抽象基类。QStyledItemDelegate提供了默认的委托实现。Qt 的标准视图将其用作默认委托。用于在视图中绘制和创建编辑器的其他选项是QStyledItemDelegateQItemDelegate。您可以使用QItemDelegate来自定义项目的显示特性和编辑器小部件。这两个类之间的区别在于,与QItemDelegate不同,QStyledItemDelegate使用当前样式来绘制其项目。QStyledItemDelegate可以处理最常见的数据类型,如intQString。在创建新委托或使用 Qt 样式表时,建议从QStyledItemDelegate派生子类。通过编写自定义委托,您可以使用自定义数据类型或自定义渲染。

在本节中,我们讨论了不同类型的模型和委托。让我们讨论一下 Qt Widgets 提供的视图类。

Qt Widgets 中的视图

有几个便利类是从标准 View 类派生出来实现 M/V 模式的。这些便利类的示例包括QListWidgetQTableWidgetQTreeWidget。根据 Qt 文档,这些类比 View 类更不灵活,不能用于随机模型。根据项目要求,您必须选择适合实现 M/V 模式的小部件类。

如果您想使用基于项目的界面并利用 M/V 模式,建议使用以下 View 类与QStandardItemModel一起使用:

  • QListView显示项目列表。

  • QTableView在表格中显示模型数据。

  • QTreeView以分层列表显示模型数据项。

Qt 框架中 View 类的层次结构如下:

图 7.7 - Qt 框架中 View 类的层次结构

图 7.7 - Qt 框架中 View 类的层次结构

QAbstractItemView是上述类的抽象基类。尽管这些类提供了可直接使用的实现,但这些类可以派生为具有专门视图,最适合用于QFileSystemModel的视图是QListViewQTreeView。每个视图都必须与模型相关联。Qt 提供了几个预定义的模型。如果现成的模型不符合您的标准,您可以添加自定义模型。

与 View 类不同(类名以View结尾),便利小部件(类名以Widget结尾)不需要由模型支持,可以直接使用。使用便利小部件的主要优势是,它们需要的工作量最少。

让我们看看 Qt Widgets 模块中的不同 View 类以及可以与它们一起使用的现成模型:

图 7.8 - 在 M/V 模式中用作 View 的不同类型的 Qt 小部件

图 7.8 - 在 M/V 模式中用作 View 的不同类型的 Qt 小部件

委托用于在QListViewQTableViewQTreeView中显示单个字段数据。当用户开始与项目交互时,委托提供一个编辑器小部件进行编辑。

您可以在以下链接找到上述类的比较概述,并了解相应小部件的用途:

doc.qt.io/qt-6/modelview.html

在本节中,您了解了 M/V 架构并熟悉了所使用的术语。让我们使用 Qt Widgets 创建一个简单的 GUI 应用程序来实现 M/V。

使用 M/V 模式创建一个简单的 Qt Widgets 应用程序

现在是时候使用Qt Widgets创建一个简单的示例了。本节中的示例演示了如何将预定义的QFileSystemModel与内置的QListViewQTreeView小部件关联使用。当双击视图时,委托会自动处理。

按照以下步骤创建一个实现 M/V 模式的简单应用程序:

  1. 使用 Qt Creator 创建一个新项目,从项目创建向导中选择Qt Widgets模板。它将生成一个带有预定义项目骨架的项目。

  2. 创建应用程序骨架后,打开.ui表单并将QListViewQTreeView添加到表单中。您可以添加两个标签以区分视图,如下所示:

图 7.9 - 使用 Qt Designer 创建一个带有 QListView 和 QTreeView 的 UI

  1. 打开mainwindow.cpp文件并添加以下内容:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileSystemModel>
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    QFileSystemModel *model = new QFileSystemModel;
    model->setRootPath(QDir::currentPath());
    ui->treeView->setModel(model);
    ui->treeView->setRootIndex(
        model->index(QDir::currentPath()));
    ui->listView->setModel(model);
    ui->listView->setRootIndex(
        model->index(QDir::currentPath()));
}

在前面的 C++实现中,我们使用了预定义的QFileSystemModel作为 View 的模型。

  1. 接下来,点击左侧窗格中的运行按钮。一旦您点击运行按钮,您将看到一个窗口,如图 7.10所示:

](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/xplat-dev-qt6-mod-cpp/img/Figure_7.10_B16231.jpg)

图 7.10 - 显示 QListView 和 QTreeView 的示例应用程序的输出

  1. 让我们修改现有的应用程序,使用从QAbstractItemModel派生的自定义模型。在以下示例中,我们创建了一个简单的ContactListModel自定义类,它是从QAbstractItemModel派生的:
void ContactListModel::addContact(QAbstractItemModel *model, 
const QString &name,const QString &phoneno, const QString &emailid)
{
    model->insertRow(0);
    model->setData(model->index(0, 0), name);
    model->setData(model->index(0, 1), phoneno);
    model->setData(model->index(0, 2), emailid);
}
QAbstractItemModel* ContactListModel::
        getContactListModel()
{
    QStandardItemModel *model = new 
        QStandardItemModel(0, 3, this);
    model->setHeaderData(0,Qt::Horizontal, 
                         QObject::tr("Name"));
    model->setHeaderData(1,Qt::Horizontal, 
                         QObject::tr("Phone No"));
    model->setHeaderData(2,Qt::Horizontal, 
                         QObject::tr("Email ID"));
    addContact(model,"John","+1 
               1234567890","john@abc.com");
    addContact(model,"Michael","+44 
               213243546","michael@abc.com");
    addContact(model,"Robert","+61 
               5678912345","robert@xyz.com");
    addContact(model,"Kayla","+91 
               9876554321","kayla@xyz.com");
    return model;
}
  1. 接下来,修改 UI 表单以实现QTableView,并将联系人列表模型设置为以下代码段所示:
ContactListModel *contactModel = new ContactListModel;
ui->tableView->setModel(
               contactModel->getContactListModel());
ui->tableView->horizontalHeader()->setStretchLastSection(true);
  1. 您可以将QStringListModel添加到QListView中以使用简单的列表模型:
    QStringListModel *model = new QStringListModel(this);
    QStringList List;
    List << "Item 1" << "Item 2" << "Item 3" <<"Item 4";
    model->setStringList(List);
    ui->listView->setModel(model);
  1. 接下来,点击左侧窗格中的运行按钮。一旦您点击运行按钮,您将看到一个窗口,如图 7.11所示:

图 7.11 - 使用自定义模型在 QListView 和 QTableView 中的应用程序输出

图 7.11 - 使用自定义模型在 QListView 和 QTableView 中的应用程序输出

恭喜!您已经学会了如何在 Qt 小部件项目中使用 M/V。

重要提示

要了解更多关于方便类的实现,例如QTableWidgetQtTreeWidget,请在 Qt Creator 欢迎屏幕和本章的源代码中探索相关示例。

您还可以创建自己的自定义委托类。要创建自定义委托,您需要对QAbstractItemDelegate或任何方便类(如QStyledItemDelegateQItemDelegate)进行子类化。自定义委托类可能如下面的代码片段所示:

class CustomDelegate: public QStyledItemDelegate
{
  Q_OBJECT
public:
  CustomDelegate(QObject* parent = nullptr);
  void paint(QPainter* painter, 
             const QStylestyleOptionViewItem& styleOption,
             const QModelIndex& modelIndex) const override;
  QSize sizeHint(const QStylestyleOptionViewItem& styleOption,
                 const QModelIndex& modelIndex) const override;
  void setModelData(QWidget* editor, QAbstractItemModel* model,
                    const QModelIndex& modelIndex)                     const override;
  QWidget *createEditor(QWidget* parent, 
                  const QStylestyleOptionViewItem& styleOption,
                  const QModelIndex & modelIndex)                   const override;
  void setEditorData(QWidget* editor, 
                    const QModelIndex& modelIndex)                     const override;
  void updateEditorGeometry(QWidget* editor, 
                  const QStylestyleOptionViewItem& styleOption, 
                  const QModelIndex& modelIndex)                   const override;
};

您必须重写虚拟方法,并根据项目需求添加相应的逻辑。您可以在以下链接了解有关自定义委托和示例的更多信息:

doc.qt.io/qt-6/model-View-programming.html

在本节中,我们学习了如何创建使用 M/V 模式的 GUI 应用程序。在下一节中,我们将讨论它在 QML 中的实现方式。

了解 QML 中的模型和视图

与 Qt 小部件一样,Qt Quick 也实现了模型、视图和委托来显示数据。该实现将数据的可视化模块化,使开发人员能够管理数据。您可以通过最小的更改来将一个视图更改为另一个视图。

要可视化数据,将视图的model属性绑定到模型,将delegate属性绑定到组件或其他兼容类型。

让我们讨论在 Qt Quick 应用程序中实现 M/V 模式的可用 QML 类型。

Qt Quick 中的视图

视图是显示数据的容器,用于项目集合。这些容器功能丰富,可以根据特定的样式或行为要求进行定制。

在 Qt Quick 图形类型的基本集中提供了一组标准视图:

  • ListView:以水平或垂直列表方式布置项目

  • GridView:以网格方式布置项目

  • TableView:以表格形式布置项目

  • PathView:在路径上布置项目

ListViewGridViewTableView继承自Flickable QML 类型。PathView继承自ItemTreeView QML 类型已经过时。让我们看一下这些 QML 类型的继承关系:

图 7.12 - Qt Quick 中视图类的层次结构

图 7.12 - Qt Quick 中视图类的层次结构

每种 QML 类型的属性和行为都不同。它们根据 GUI 需求使用。如果您想了解更多关于 QML 类型的信息,可以参考它们各自的文档。让我们在下一节中探索 Qt Quick 中的模型。

Qt Quick 中的模型

Qt 提供了几种方便的 QML 类型来实现 M/V 模式。这些模块提供了非常简单的模型,而无需在 C++中创建自定义模型类。这些方便类的示例包括ListModelTableModelXmlListModel

QtQml.Models 模块提供以下用于定义数据模型的 QML 类型:

  • ListModel 定义了一个自由形式的列表数据源。

  • ListElement 定义了 ListModel 中的数据项。

  • DelegateModel 封装了一个模型和委托。

  • DelegateModelGroup 封装了一组经过筛选的可视数据项目。

  • ItemSelectionModel 继承自 QItemSelectionModel,它跟踪视图的选定项目。

  • ObjectModel 定义了一组要用作模型的项目。

  • Instantiator 动态实例化对象。

  • Package 描述了一组命名的项目。

要在您的 Qt Quick 应用程序中使用上述 QML 类型,请使用以下行导入模块:

import QtQml.Models

让我们讨论在 Qt Quick 中可用的现成模型。ListModel 是包含包含数据角色的 ListElement 定义的简单容器。它与 ListView 一起使用。Qt.labs.qmlmodels 提供了用于模型的实验性 QML 类型。这些模型可用于快速原型设计和显示非常简单的数据。TableModel 类型将 JavaScript/JSON 对象作为表模型的数据进行存储,并与 TableView 一起使用。您可以通过以下方式导入这些实验性类型:

import Qt.labs.qmlmodels

如果您想从 XML 数据创建模型,那么可以使用 XmlListModel。它可以与 ListViewPathViewGridView 等视图一起使用作为模型。要使用此模型,您必须按照以下方式导入模块:

import QtQuick.XmlListModel

您可以使用 ListModelXmlListModelTableView 一起创建 TableView 中的一列。要处理多行和多列,您可以使用 TableModel 或者通过子类化 QAbstractItemModel 创建自定义的 C++ 模型。

您还可以使用 Repeater 与 Models。整数可以用作定义项目数量的模型。在这种情况下,模型没有任何数据角色。让我们创建一个简单的示例,使用 ListViewText 项目作为委托组件:

import QtQuick
import QtQuick.Window
Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Simple M/V Demo")
    ListView {
        anchors.fill: parent
        model: 10
        delegate: itemDelegate
    }
    Component {
        id: itemDelegate
        Text { text: "  Item :  " + index }
    }
} 

在前面的示例中,我们使用了 Text 作为委托,而没有使用组件。

现在,让我们探讨如何将 ListModelListView 一起使用。ListModel 是在 QML 中指定的一组简单的类型层次结构。可用的角色由 ListElement 属性指定。让我们使用 ListModelListView 创建一个简单的应用程序。

假设您想创建一个简单的通讯录应用程序。您可能需要一些用于联系人的字段。在以下代码片段中,我们使用了一个包含一些联系人的姓名、电话号码和电子邮件地址的 ListModel

ListModel {
    id: contactListModel
    ListElement {
        name: "John" ; phone: "+1 1234567890" ; 
        email: "john@abc.com"
    }
    ListElement {
        name: "Michael" ; phone: "+44 213243546" ; 
        email: "michael@abc.com"
    }
    ListElement {
        name: "Robert" ; phone: "+61 5678912345" ; 
        email: "robert@xyz.com"
    }
    ListElement {
        name: "Kayla" ; phone: "+91 9876554321" ; 
        email: "kayla@xyz.com"
    }
}

我们现在已经创建了模型。接下来,我们必须使用委托来显示它。因此,让我们修改之前创建的委托组件,使用三个 Text 元素。根据您的需求,您可以创建具有图标、文本或自定义类型的复杂委托类型。您可以添加一个突出显示的项目,并根据焦点更新背景。您需要为视图提供一个委托,以在列表中直观地表示一个项目:

Component {
    id: contactDelegate
    Row {
        id: contact
        spacing: 20
        Text { text: " Name: " + name; }
        Text { text: " Phone no: " + phone }
        Text { text: " Email ID: " + email }
    }
}
ListView {
    anchors.fill: parent
    model: contactListModel
    delegate: contactDelegate
}

在前面的示例中,我们使用了 ListElementListModel。视图根据委托定义的模板显示每个项目。可以通过 index 属性或项目的属性访问模型中的项目。

您可以在以下链接中了解有关不同类型的模型以及如何操作模型数据的更多信息:

doc.qt.io/qt-6/qtquick-modelviewsdata-modelview.html

在本节中,您了解了 QML 中的 M/V。您可以尝试使用自定义模型和委托,并创建个性化的视图。看一看您手机上的电话簿或最近的通话列表,并尝试实现它。在下一节中,您将学习如何将 QML 前端与 C++ 模型集成。

使用 C++ 模型与 QML

到目前为止,我们已经讨论了如何在 Qt Widgets 和 QML 中使用模型和视图。但在大多数现代应用程序中,您将需要在 C++中编写模型,并在 QML 中编写前端。Qt 允许我们在 C++中定义模型,然后在 QML 中访问它们。这对于将现有的 C++数据模型或其他复杂数据集暴露给 QML 非常方便。对于复杂的逻辑操作,原生 C++始终是正确的选择。它可以优于使用 JavaScript 编写的 QML 中的逻辑。

有许多原因您应该创建一个 C++模型。C++是类型安全的,并且编译为对象代码。它增加了应用程序的稳定性并减少了错误的数量。它灵活,并且可以提供比 QML 类型更多的功能。您可以与现有代码或使用 C++编写的第三方库集成。

您可以使用以下类定义 C++模型:

  • QStringList

  • QVariantList

  • QObjectList

  • QAbstractItemModel

前三个类有助于暴露更简单的数据集。QAbstractItemModel提供了一个更灵活的解决方案来创建复杂的模型。QStringList包含QString实例的列表,并通过modelData角色提供列表的内容。类似地,QVariantList包含QVariant类型的列表,并通过modelData角色提供列表的内容。如果QVariantList发生变化,则必须重置模型。QObjectList嵌入了一个QObject*列表,该列表提供了列表中对象的属性作为角色。QObject*可以作为modelData属性访问。为了方便起见,可以直接在委托的上下文中访问对象的属性。

Qt 还提供了处理 SQL 数据模型的 C++类,例如QSqlQueryModelQSqlTableModelQSqlRelationalTableModelQSqlQueryModel提供了基于 SQL 查询的只读模型。这些类减少了运行 SQL 查询以进行基本的 SQL 操作(如插入、创建或更新)的需要。这些类是从QAbstractTableModel派生的,使得在 View 类中轻松呈现来自数据库的数据变得容易。

您可以通过访问以下链接了解有关不同类型的 C++模型的更多信息:

doc.qt.io/qt-6/qtquick-modelviewsdata-cppmodels.html

在本节中,我们讨论了 C++模型以及为什么要使用它们。现在,您可以从 C++后端获取数据,并在 QML 中开发的 UI 中呈现它。在下一节中,我们将使用上述概念创建一个简单的 Qt Quick 应用程序,并解释如何在 QML 中使用它们。

使用 Qt Quick 创建一个简单的 M/V 应用程序

在前面的部分中,我们讨论了 Qt 的模型-视图-委托框架。您学会了如何创建自定义模型和委托,以及如何使用 C++模型。但您一定想知道如何与我们的 QML 前端集成。在本节中,我们将创建一个 C++模型并将其暴露给 QML 引擎。我们还将讨论如何将自定义模型注册为 QML 类型。

让我们创建一个应用程序,从 C++代码中获取模型并在基于 Qt Quick 的应用程序中显示它:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QStringListModel>
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    QStringList stringList;
    stringList << "Item 1" << "Item 2" << "Item 3" 
               <<"Item 4";
    engine.rootContext()->setContextProperty("myModel", 
        QVariant::fromValue(stringList));
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    engine.load(url);
    return app.exec();
}

在上面的代码片段中,我们创建了一个基于QStringList的简单模型。字符串列表包含四个不同的字符串。我们使用setContextProperty()将模型暴露给 QML 引擎。现在,让我们在 QML 文件中使用该模型:

import QtQuick
import QtQuick.Window
Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("QML CPP M/V Demo")
    ListView {
        id: listview
        width: 120
        height: 200
        model: myModel
        delegate: Text { text: modelData }
    }
}

上面的示例使用QQmlContext::setContextProperty()在 QML 组件中直接设置模型值。另一种方法是将 C++模型类注册为 QML 类型,如下所示:

qmlRegisterType<MyModel>("MyModel",1,0,"MyModel");

上述行将允许直接在 QML 文件中将模型类创建为 QML 类型。第一个字段是 C++类名,然后是所需的包名称,然后是版本号,最后一个参数是 QML 中的类型名称。您可以使用以下行将其导入到 QML 文件中:

Import MyModel 1.0

让我们在我们的 QML 文件中创建一个MyModel的实例,如下所示:

MyModel {
    id: myModel
}
ListView {
    width: 120
    height: 200
    model: myModel
    delegate: Text { text: modelData }
} 

您还可以使用setInitialProperties()QQuickView中使用模型,如下面的代码所示:

QQuickView view;
view.setResizeMode(QQuickView::SizeRootObjectToView);
view.setInitialProperties({
                  {"myModel",QVariant::fromValue(myModel)}});
view.setSource(QUrl("qrc:/main.qml"));
view.show();

在前面的代码片段中,我们使用了QQuickView来创建一个 UI,并将自定义的 C++模型传递给了 QML 环境。

在本节中,我们学习了如何将简单的 C++模型与 QML 集成。您可以添加信号和属性来扩展自定义类的功能。接下来,让我们总结一下本章的学习成果。

总结

在本章中,我们深入了解了 Qt 中的 Model-View-Delegate 模式的核心概念。我们解释了它与传统 MVC 模式的不同之处。我们讨论了在 Qt 中使用 M/V 的不同方式以及 Qt 中提供的便利类。我们学习了如何在 Qt Widgets 和 Qt Quick 中应用 M/V 概念。我们讨论了如何将 C++模型集成到 QML 视图中。我们还创建了一些示例,并在我们的 Qt 应用程序中实现了这些概念。您现在可以创建自己的模型、委托和视图。我希望您已经理解了这个框架的重要性,以及使用它满足您需求的充分理由。

第八章图形和动画中,我们将学习关于图形框架以及如何将动画添加到您的 Qt Quick 项目。

第八章:图形和动画

在本章中,您将学习 Qt 图形框架的基础知识以及如何在屏幕上渲染图形。您将了解 Qt 中如何进行一般绘图。我们将从讨论使用QPainter进行 2D 图形开始。我们将探讨如何使用绘图工具绘制不同的形状。然后,您将了解QGraphicsViewQGraphicsScene使用的图形视图架构。之后,我们将讨论 Qt Quick 使用的场景图机制。在本章中,您还将学习如何通过添加动画和状态使用户界面更有趣。

在本章中,我们将讨论以下内容:

  • 了解 Qt 的图形框架

  • QPainter和 2D 图形

  • 图形视图框架

  • OpenGL 实现

  • Qt Quick 场景图

  • QML 中的动画

  • Qt 中的状态机

通过本章,您将了解 Qt 使用的图形框架。您将能够在屏幕上绘制并向 UI 元素添加动画。

技术要求

本章的技术要求包括 Qt 6.0.0 和 Qt Creator 4.14.0 的最低版本,安装在 Windows 10、Ubuntu 20.04 或 macOS 10.14 等最新版本的桌面平台上。

本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp/tree/master/Chapter08

重要说明

本章中使用的屏幕截图来自 Windows 平台。您将在您的机器上基于底层平台看到类似的屏幕。

了解 Qt 的图形框架

Qt 是最受欢迎的 GUI 应用程序框架之一。开发人员可以使用 Qt 构建出色的跨平台 GUI 应用程序,而不必担心底层图形实现。Qt 渲染硬件接口RHI)将 Qt 应用程序的图形指令解释为目标平台上可用的图形 API。

RHI 是硬件加速图形 API 的抽象接口。rhi模块中最重要的类是QRhiQRhi实例由特定图形 API 的后端支持。后端的选择在运行时确定,并由创建QRhi实例的应用程序或库决定。您可以通过将以下行添加到项目文件中来添加模块:

QT += rhi

RHI 支持的不同类型的图形 API 如下:

  • OpenGL

  • OpenGL ES

  • Vulkan

  • Direct3D

  • 金属

图 8.1显示了 Qt 图形框架中的主要图层:

图 8.1 - Qt 6 图形堆栈的主要图层

图 8.1 - Qt 6 图形堆栈的主要图层

让我们熟悉一下前面图中显示的图形 API。OpenGL是最受欢迎的图形 API,具有跨语言和跨平台应用程序支持。它用于与 GPU 交互,实现硬件加速渲染。OpenGL ES是 OpenGL API 的一种适用于嵌入式设备的变体。它允许在嵌入式和移动设备上渲染高级 2D 和 3D 图形。iOS 设备上的 OpenGL ES也称为EAGL。OpenGL ES 也可在 Web 平台上作为 WebGL 使用。OpenGL 和 OpenGL ES 由技术硬件和软件公司的联盟 Khronos Group 开发和维护。您可以在以下链接了解有关 OpenGL 的更多信息:

https://www.opengl.org/about/

Vulkan是一个新一代的图形 API,有助于为现代 GPU 创建跨平台和高性能的应用程序。它由 Khronos Group 创建。Vulkan 的显式 API 设计允许在各种桌面、嵌入式和移动平台上进行高效实现。Qt 6 提供了对 Vulkan API 的支持。要使用 Vulkan,Qt 应用程序需要 LunarG Vulkan SDK。在以下链接中探索更多关于 Vulkan 的信息:

https://www.lunarg.com/vulkan-sdk/

Direct3D是微软专有的图形 API,提供了利用底层 GPU 功能进行 2D 和 3D 图形渲染的函数。微软公司为 Windows 平台创建了它。它是一个低级 API,可用于使用渲染管线绘制基元或使用计算着色器执行并行操作。

Direct3D 暴露了 3D 图形硬件的高级图形能力,包括模板缓冲、W 缓冲、Z 缓冲、透视纹理映射、空间反锯齿、可编程 HLSL 着色器和特效。Direct3D 与其他 DirectX 技术的集成使其能够提供包括视频映射、硬件 2D 叠加平面中的 3D 渲染,甚至精灵,并允许在交互媒体中使用 2D 和 3D 图形的多个功能。Direct3D 旨在通常虚拟化 3D 硬件接口。相比之下,OpenGL 旨在成为可以在软件中模拟的 3D 硬件加速渲染系统。这两个 API 在设计上有根本的不同。以下链接提供了对 Direct3D 的进一步了解:

https://docs.microsoft.com/en-in/windows/win32/getting-started-with-direct3d

Metal是苹果的低级计算机图形 API,它提供了对图形处理单元GPU)的几乎直接访问,使您能够优化 iOS、macOS 和 tvOS 应用程序的图形和计算能力。它还具有低开销的架构,包括预编译的 GPU 着色器、细粒度资源管理和多线程支持。在 Metal 宣布之前,苹果为 macOS 提供了 OpenGL,为 iOS 提供了 OpenGL ES,但由于高度抽象的硬件,存在性能问题。另一方面,Metal 由于其苹果特定的 API,比 OpenGL 具有更好的性能。Metal 通过支持多达 100 倍于 OpenGL 的绘制调用,实现了全新一代的专业图形输出。您可以在以下链接中了解更多关于 Metal 的信息:

https://developer.apple.com/documentation/metal

在本节中,我们熟悉了 Qt 的图形框架和 RHI。您现在对这个框架有了基本的了解。在下一节中,我们将进一步讨论使用 QPainter 进行 2D 图形。

QPainter 和 2D 图形

Qt 具有先进的窗口、绘图和排版系统。Qt GUI 模块中最重要的类是QWindowQGuiApplication。该模块包括用于 2D 图形、图像、字体和高级排版的类。此外,GUI 模块还包括用于集成窗口系统、OpenGL 集成、事件处理、2D 图形、基本图像、字体和文本的类。Qt 的用户界面技术在内部使用这些类,但也可以直接用于编写使用低级 OpenGL 图形 API 的应用程序。

根据平台,QWindow类支持使用 OpenGL 和 OpenGL ES 进行渲染。Qt 包括QOpenGLPaintDevice类,它允许使用 OpenGL 加速的QPainter渲染和几个便利类。这些便利类通过隐藏扩展处理的复杂性和 OpenGL ES 2.0 与桌面 OpenGL 之间的差异,简化了 OpenGL 中的编写代码。QOpenGLFunctions是一个便利类,它提供了跨平台访问桌面 OpenGL 上的 OpenGL ES 2.0 函数,而无需手动解析 OpenGL 函数指针。

要在基于 qmake 的应用程序中使用这些 API 和类,您必须在项目文件(.pro)中包含gui模块,如下所示:

QT += gui 

如果您正在使用基于Cmake的构建系统,则将以下内容添加到CMakeLists.txt文件中:

find_package(Qt6 COMPONENTS Gui REQUIRED)
target_link_libraries(mytarget PRIVATE Qt6::Gui)

QPainter类主要用于绘图操作,为绘制矢量图形、文本和图像到不同表面或QPaintDevice实例(包括QImageQOpenGLPaintDeviceQWidgetQPrinter)提供 API。对于 Qt Widgets 用户界面,Qt 使用软件渲染器。

以下是 Qt GUI 的高级绘图 API:

  • 绘制系统

  • 坐标系统

  • 绘制和填充

我们将在接下来的章节中探讨这些 API。

理解绘制系统

Qt 的绘制系统提供了几个方便的类来在屏幕上绘制。最重要的类是QPainterQPaintDeviceQPaintEngine。您可以使用QPainter在小部件和其他绘图设备上绘制。这个类可以用来从简单的线条到复杂的形状(比如在paintEvent()函数内部或在paintEvent()调用的函数内部绘制QPainter)绘制东西。QPaintDevice是允许使用QPainter实例进行 2D 绘制的对象的基类。QPaintEngine提供了定义QPainter如何在指定平台上的指定设备上绘制的接口。QPaintEngine类是QPainterQPaintDevice内部使用的抽象类。

让我们来看看与绘制相关的类的层次结构,以更好地了解在使用绘制系统时如何选择合适的类。

图 8.2 – Qt 中绘制类的层次结构

图 8.2 – Qt 中绘制类的层次结构

前面的层次结构方法说明了所有绘图方法都遵循相同的机制。因此,很容易为新功能添加规定,并为不受支持的功能提供默认实现。

让我们在下一节讨论坐标系统。

使用坐标系统

QPainter类控制坐标系统。它与QPaintDeviceQPaintEngine类一起构成了 Qt 的绘制系统的基础。绘图设备的默认坐标系统的原点在左上角。QPainter的主要功能是执行绘图操作。而QPaintDevice类是一个二维空间的抽象,可以使用QPainter进行绘制,QPaintEngine类提供了一个绘图器,用于在不同类型的设备上绘制。QPaintDevice类是可以进行绘制的对象的基类,它从QWidgetQImageQPixmapQPictureQOpenGLPaintDevice类继承了其绘图能力。

您可以在以下文档中了解更多关于坐标系统的信息:

https://doc.qt.io/qt-6/coordsys.html

绘制和填充

QPainter提供了一个高度优化的绘图器,用于大多数 GUI 上的绘图需求。它可以绘制各种类型的形状,从简单的图形基元(如QPointQLineQRectQRegionQPolygon类)到复杂的矢量路径。矢量路径由QPainterPath类表示。QPainterPath作为绘制操作的容器,允许构建和重复使用图形形状。它可用于填充、轮廓和裁剪。QPainter还可以绘制对齐的文本和像素图。要填充QPainter绘制的形状,可以使用QBrush类。它具有颜色、样式、纹理和渐变属性,并且通过颜色和样式进行定义。

在下一节中,我们将使用到目前为止讨论的 API 来使用QPainter进行绘制。

使用 QPainter 进行绘制

QPainter有几个便利函数来绘制大多数基本形状,例如drawLine()drawRect()drawEllipse()drawArc()drawPie()drawPolygon()。您可以使用fillRect()函数填充形状。QBrush类描述了QPainter绘制的形状的填充图案。刷子可以用于定义样式、颜色、渐变和纹理。

让我们看一下下面的paintEvent()函数,我们在其中使用QPainter来绘制文本和不同的形状:

void PaintWindow::paintEvent(QPaintEvent *event)
{
    QPainter painter;
    painter.begin(this);
    //draws a line
    painter.drawLine(QPoint(50, 50), QPoint(200, 50));
    //draws a text
    painter.drawText(QPoint(50, 100), "Text");
    //draws an ellipse
    painter.drawEllipse(QPoint(100,150),50,20);
    //draws an arc
    QRectF drawingRect(50, 200, 100, 50);
    int startAngle = 90 * 16;
    int spanAngle = 180 * 16;
    painter.drawArc(drawingRect, startAngle, spanAngle);
    //draws a pie
    QRectF drawingRectPie(150, 200, 100, 50);
    startAngle = 60 * 16;
    spanAngle = 70 * 16;
    painter.drawPie(drawingRectPie, startAngle, spanAngle);
    painter.end();
    QWidget::paintEvent(event);
}

在前面的示例中,我们创建了一个QPainter实例,并使用可用的默认绘图函数绘制了一条线、文本、椭圆、弧和扇形。当您将上述代码添加到自定义类中并运行项目时,您将看到以下输出:

图 8.3 – 使用 QPainter 绘图示例的输出

图 8.3 – 使用 QPainter 绘图示例的输出

Qt 提供了几个离屏绘图类,每个类都有其自己的优缺点。QImageQBitmapQPixmapQPicture是涉及的类。在大多数情况下,您必须在QImageQPixmap之间进行选择。

Qt 中的QImage类允许轻松读取、写入和操作图像。如果您正在处理资源、合并多个图像并进行一些绘图,则应使用QImage类:

QImage image(128, 128, QImage::Format_ARGB32); 
QPainter painter(&image);

第一行创建了一个 128 像素正方形的图像,每个像素编码为 32 位整数 - 每个通道的不透明度、红色、绿色和蓝色各占 8 位。第二行创建了一个可以在QImage实例上绘制的QPainter实例。接下来,我们执行了您在上一节中看到的绘图,完成后,我们将图像写入 PNG 文件,代码如下:

image.save("image.png"); 

QImage支持多种图像格式,包括 PNG 和 JPEG。QImage还有一个load方法,可以从文件或资源加载图像。

QBitmap类是一个单色离屏绘图设备,提供深度为 1 位的位图。QPixmap类提供了一个离屏绘图设备。QPicture类是一个序列化QPainter命令的绘图设备。

您还可以使用QImageReaderQImageWriter类来更精细地控制图像的加载和保存。要添加对 Qt 提供的图像格式之外的图像格式的支持,可以使用QImageIOHandlerQImageIOPlugin创建图像格式插件。QPainterPath类有助于绘制可以创建和重复使用的不同图形形状。以下代码片段演示了如何使用QPainterPath

void MyWidget:: paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    QPolygon polygon;
    polygon << QPoint(100, 185) << QPoint(175, 175)
            << QPoint(200, 110) << QPoint(225, 175)
            << QPoint(300, 185) << QPoint(250, 225)
            << QPoint(260, 290) << QPoint(200, 250)
            << QPoint(140, 290) << QPoint(150, 225)
            << QPoint(100, 185);
    QBrush brush;
    brush.setColor(Qt::yellow);
    brush.setStyle(Qt::SolidPattern);
    QPen pen(Qt::black, 3, Qt::DashDotDotLine, 
             Qt::RoundCap, Qt::RoundJoin);
    painter.setPen(pen);
    QPainterPath path;
    path.addPolygon(polygon);
    painter.drawPolygon(polygon);
    painter.fillPath(path, brush);
    QWidget:: paintEvent(event);
}

在上述代码中,我们创建了一个自定义绘制的多边形对象,并使用所需的绘图路径。

注意

请注意,在进行绘制操作时,请确保在绘制背景和绘制内容之间没有延迟。否则,如果延迟超过 16 毫秒,您将在屏幕上看到闪烁。您可以通过将背景渲染到一个像素图中,然后在该像素图上绘制内容来避免这种情况。最后,您可以将该像素图绘制到小部件上。这种方法称为双缓冲

在本节中,我们不仅学习了如何在屏幕上绘制图像,还学习了如何在屏幕外绘制图像并将其保存为图像文件。在下一节中,我们将学习图形视图框架的基础知识。

引入图形视图框架

Graphics View 框架是一个强大的图形引擎,允许您可视化和与大量自定义的 2D 图形项进行交互。如果您是一名经验丰富的程序员,可以使用图形视图框架手动绘制 GUI 并进行完全手动动画化。为了一次绘制数百或数千个相对轻量级的自定义项,Qt 提供了一个独立的视图框架,即 Graphics View 框架。如果您正在从头开始创建自己的小部件集,或者需要一次在屏幕上显示大量项,每个项都有自己的位置和数据,您可以利用 Graphics View 框架。这对于处理和显示大量数据的应用程序尤为重要,例如地理信息系统或计算机辅助设计软件。

Graphics View 提供了一个表面,用于管理和与大量自定义创建的 2D 图形项进行交互,并提供用于可视化这些项的视图小部件,支持缩放和旋转。该框架包括一个事件传播架构,可以为场景的项提供交互功能。这些项响应键盘事件;鼠标按下、移动、释放和双击事件;以及跟踪鼠标移动。Graphics View 使用二进制空间分区(BSP)树来提供非常快速的项发现,使其能够实时可视化大型场景,即使有数百万个项也可以。

该框架遵循基于项的模型/视图编程方法。它包括三个组件,QGraphicsSceneQGraphicsViewQGraphicsItem

QGraphicsItem公开了一个接口,您的子类可以重写该接口以管理鼠标和键盘事件、拖放、接口层次结构和碰撞检测。每个项都有自己的本地坐标系,并且助手函数允许您快速将项的坐标转换为场景的坐标。Graphics View 框架使用一个或多个QGraphicsView实例来显示QGraphicsScene类的内容。为了查看场景的不同部分,可以将多个视图附加到同一个场景,每个视图都有自己的平移和旋转。由于QGraphicsView小部件是一个滚动区域,因此可以将滚动条附加到视图,并允许用户在其中滚动。视图接收键盘和鼠标输入,为场景生成场景事件,并将这些场景事件分派给场景,然后将这些相同的事件分派给场景的项。以前,该框架被用于游戏开发。

重要提示

我们将跳过关于框架用法和示例的细节,因为在 Qt Quick 2 出现后,它失去了流行度。Qt Quick 2 配备了场景图形 API,提供了以前由 Graphics View 框架提供的大部分功能。如果您仍然想了解更多关于 Graphics View 框架的信息,可以阅读以下文档:

https://doc.qt.io/qt-6/graphicsview.html

在本节中,我们讨论了 Qt 的 Graphics View 框架。在下一节中,我们将学习关于 Qt 与 OpenGL 集成。

了解 Qt OpenGL 模块

Qt Quick 和 Qt Widgets 是 Qt 中用户界面(UI)开发的两种主要方法。它们存在以支持各种类型的 UI,并构建在分别针对每种 UI 进行了优化的独立图形引擎上。在 Qt 中,可以将 OpenGL 图形 API 代码与这两种 UI 类型结合使用。当应用程序包含自己的 OpenGL 依赖代码或与第三方基于 OpenGL 的渲染器集成时,这将非常有用。OpenGL/OpenGL ES XML API 注册表用于生成 OpenGL 头文件。

Qt OpenGL 模块旨在与需要 OpenGL 访问的应用程序一起使用。Qt OpenGL 模块中的便利类帮助开发人员更轻松、更快地构建应用程序。这个模块负责与 Qt 5 应用程序和 Qt GUI 保持兼容。QOpenGLWidget是一个可以将 OpenGL 场景添加到使用QWidget的 UI 中的部件。

随着 Qt RHI 作为 Qt 的渲染基础的引入,在 Qt 6 中,大多数以QOpenGL表示的类已经移动到了 Qt OpenGL 模块中。这些类仍然可用,并且对仅依赖于 OpenGL 的应用程序提供完全支持。它们不再被认为是必不可少的,因为 Qt 已经扩展到支持其他图形 API,如 Direct3D、Metal 和 Vulkan。

现有的应用程序代码大部分仍将继续工作,但现在应该在项目文件中包含 Qt OpenGL,并且如果以前是通过 Qt GUI 间接包含的话,也应该包含头文件。

Qt 6 不再直接使用兼容 OpenGL 的 GLSL 源代码片段。着色器现在以 Vulkan 风格的 GLSL 编写,反射并转换为其他着色语言,并打包成可序列化的QShader对象,供QRhi消费。

Qt 6 中的着色器准备流水线如下:

图 8.4 - 在 Qt 博客中描述的着色器准备流水线的插图

图 8.4 - 在 Qt 博客中描述的着色器准备流水线的插图

在 Qt 6.1 中,Qt 数据可视化仅支持 OpenGL RHI 后端。它需要将环境变量QSG_RHI_BACKEND设置为opengl。您可以在系统级别进行设置,或者在main()中定义如下:

qputenv("QSG_RHI_BACKEND","opengl");

让我们在下一节讨论框架如何与 Qt Widgets 一起使用。

Qt OpenGL 和 Qt Widgets

Qt Widgets 通常由高度优化和准确的软件光栅化器进行渲染,最终的内容通过适合应用程序运行平台的方法显示在屏幕上。然而,Qt Widgets 和 OpenGL 可以结合使用。QOpenGLWidget类是这样做的主要入口点。这个类可以用于为部件树的特定部分启用 OpenGL 渲染,并且 Qt OpenGL 模块的类可以用于帮助处理任何应用程序端的 OpenGL 代码。

重要说明

基于QWindowQWidget的 OpenGL 实现的应用程序,没有其他选择,只能在运行时直接调用 OpenGL API。对于 Qt Quick 和 Qt Quick 3D 应用程序,Qt 6 除了 OpenGL 外,还引入了对 Direct3D 11、Vulkan 和 Metal 的支持。在 Windows 上,默认选择仍然是 Direct3D,因此通过支持除 OpenGL 以外的图形 API,简化了 ANGLE 的移除。

在本节中,我们学习了如何使用 Qt 的 OpenGL 模块。让我们继续下一节,详细讨论 Qt Quick 中的图形。

Qt Quick 中的图形

Qt Quick 旨在利用硬件加速渲染。它将默认构建在最适合目标平台的低级图形 API 上。例如,在 Windows 上,它将默认使用 Direct3D,而在 macOS 上,它将默认使用 Metal。对于渲染,Qt Quick 应用程序使用场景图。场景图渲染器可以进行更有效的图形调用,从而提高性能。场景图具有可访问的 API,允许您创建复杂但快速的图形。Qt Quick 2D 渲染器也可以用于渲染 Qt Quick。这个光栅绘图引擎允许 Qt Quick 应用程序在不支持 OpenGL 的平台上进行渲染。

Qt 默认情况下使用目标平台上最合适的图形 API。但是,可以配置 Qt 的渲染路径以使用特定的 API。在许多情况下,选择特定的 API 可以提高性能,并允许开发人员在支持特定图形 API 的平台上部署。要更改QQuickWindow中的渲染路径,可以使用QRhi接口。

在接下来的几节中,我们将看一些功能,这些功能将进一步增强您在 Qt Quick 中与图形相关的技能。让我们从讨论如何在 Qt Quick 中使用 OpenGL 开始。

Qt OpenGL 和 Qt Quick

在支持 OpenGL 的平台上,可以手动选择它作为活动的图形 API。为了在使用 Qt Quick 时使用这个功能,应用程序应该手动将渲染后端设置为 OpenGL,同时调整项目文件并包含头文件。

在 Qt 6 中,没有直接使用 Qt Quick 进行 OpenGL 渲染的方法。基于 QRhi 的 Qt Quick 场景图的渲染路径现在是新的默认值。除了默认值之外,配置使用哪个 QRhi 后端以及因此使用哪个图形 API 的方法与 Qt 5.15 基本保持不变。Qt 6 中的一个关键区别是改进的 API 命名。现在,可以通过调用QQuickWindow::setGraphicsApi()函数来设置 RHI 后端,而在早期,这是通过调用QQuickWindow::setSceneGraphBackend()函数来实现的。

您可以在以下文章中了解更多关于这些变化的信息:

https://www.qt.io/blog/graphics-in-qt-6.0-qrhi-qt-quick-qt-quick-3d

使用 QPainter 自定义 Qt Quick 项

您还可以在 Qt Quick 应用程序中使用QPainter。这可以通过对QQuickPaintedItem进行子类化来实现。借助这个子类,您可以使用QPainter实例来渲染内容。为了渲染其内容,QQuickPaintedItem子类使用间接的 2D 表面,可以使用软件光栅化或使用OpenGL 帧缓冲对象FBO)。渲染是一个两步操作。在绘制之前,绘制表面被光栅化。然而,使用场景图进行绘制比这种光栅化方法要快得多。

让我们探索 Qt Quick 使用的场景图机制。

了解 Qt Quick 场景图

Qt Quick 2 使用专用的场景图,使用图形 API 进行遍历和渲染,包括 OpenGL、OpenGL ES、Metal、Vulkan 或 Direct 3D。使用场景图进行图形渲染而不是传统的命令式绘图系统(QPainter等),允许在渲染开始之前保留场景,并且在整个原语集合渲染之前就已知。这允许各种优化,包括批处理渲染以减少状态更改和丢弃被遮挡的原语。

假设一个 GUI 包括一个包含 10 个元素的列表,每个元素都有不同的背景颜色、文本和图标。这将给我们 30 个绘制调用和相同数量的状态更改,使用传统的绘图技术。相反,场景图重新组织原语以便绘制,这样一个调用就可以绘制所有的背景、图标和文本,将绘制调用的总数减少到三个。这种批处理和状态更改的减少可以显著提高一些硬件的性能。

场景图与 Qt Quick 2 密不可分,不能独立使用。QQuickWindow类管理和渲染场景图,自定义Item类型可以通过调用QQuickItem::updatePaintNode()将它们的图形原语添加到场景图中。

场景图以图形方式表示一个Item场景,并且是一个自包含的结构,具有足够的信息来渲染所有的项。一旦配置,它可以在项的状态不管如何被操作和渲染。在一些平台上,场景图甚至在单独的渲染线程上进行渲染,而 GUI 线程则准备下一帧的状态。

在接下来的部分中,我们将深入探讨场景图结构,然后学习渲染机制。此外,在使用 Qt Quick 3D 时,我们将混合使用场景图和本机图形 API。

Qt Quick 场景图结构

场景图由各种预定义的节点类型组成,每种类型都有特定的用途。尽管我们称之为场景图,但节点树是更精确的定义。树是从 QML 场景中的QQuickItem类型构建的,然后场景由渲染器在内部处理,绘制场景。节点本身没有活动的绘制代码。

尽管节点树大多是由现有的 Qt Quick QML 类型在内部构建的,用户可以添加包括代表 3D 模型的完整子树在内的自己的内容。

  • 节点

  • 材料

QSGGeometryNode对用户来说是最重要的节点。它通过指定几何图形和材料来创建定制的图形。QSGGeometry类描述了图形原语的形状或网格,并用于定义几何图形。它可以定义一切,无论是线条、矩形、多边形、一组不连续的矩形,还是复杂的 3D 网格。材料定义了特定形状的像素如何填充。一个节点可以有多个子节点。几何节点按照子节点顺序进行渲染,父节点可以在其子节点后面找到。

材料描述了QSGGeometryNode中几何图形的内部是如何填充的。它封装了用于图形管线的顶点和片段阶段的图形着色器,并提供了很大的灵活性,即使大多数 Qt Quick 项目只使用非常基本的材料,如纯色和纹理填充。

场景图 API 是低级的,优先考虑性能而不是便利性。从头开始创建最基本的自定义几何图形和材料需要大量的代码输入。因此,API 包括一些方便的类,使最常用的自定义节点易于访问。

在接下来的部分中,我们将讨论场景图中的渲染是如何进行的。

使用场景图进行渲染

场景图在QQuickWindow类中进行内部渲染,没有公共 API 可以访问它。但是,在渲染管道中有一些点,用户可以插入应用程序代码。这些点可以用于添加自定义场景图内容,或者通过直接调用场景图的图形 API(OpenGL、Vulkan、Metal 等)来插入任意的渲染命令。渲染循环确定了集成点。

场景图中有两种类型的渲染循环:

  • basic是单线程渲染器。

  • threaded是一个多线程渲染器,它在不同的线程上进行渲染。

Qt 尝试根据平台和底层图形能力选择适当的渲染循环。当这不够用时,或者在测试期间,可以使用环境变量QSG_RENDER_LOOP来强制使用特定类型的渲染器循环。您可以通过启用qt.scenegraph.general日志类别来查找正在使用的渲染循环类型。

在大多数使用场景图的应用程序中,渲染是在单独的渲染线程上进行的。这是为了提高多核处理器的并行性,并更好地利用等待阻塞交换缓冲调用等停顿时间。这提供了显著的性能改进,但它限制了与场景图的交互发生的位置和时间。

以下图表描述了如何使用线程化渲染循环和 OpenGL 渲染帧。除了 OpenGL 上下文的特定之外,其他图形 API 的步骤也是相同的:

图 8.5 - 在线程化渲染循环中遵循的渲染顺序

图 8.5 - 在线程化渲染循环中遵循的渲染顺序

目前,在 Windows 上默认使用 Direct3D 11 或更高版本的线程化渲染器。您可以通过将环境中的QSG_RENDER_LOOP设置为threaded来强制使用线程化渲染器。但是,线程化渲染循环取决于图形 API 实现的节流。在 macOS 上使用 Xcode 10 或更高版本和 OpenGL 构建时,不支持线程化渲染循环。对于 Metal,没有这样的限制。

如果您的系统无法提供基于 Vsync 的节流功能,则通过将环境变量QSG_RENDER_LOOP设置为basic来使用基本渲染循环。以下步骤描述了在基本或非线程化渲染循环中如何渲染帧:

图 8.6 - 非线程化渲染循环中的渲染顺序

图 8.6 - 非线程化渲染循环中的渲染顺序

当平台的标准 OpenGL 库未被使用时,默认情况下在启用 OpenGL 的平台上使用非线程化渲染循环。这主要是为后者制定的预防策略,因为并未验证所有 OpenGL 驱动程序和窗口系统的组合。即使使用非线程化渲染循环,您可能需要将代码编写为使用线程化渲染器,否则您的代码将无法移植。

要了解更多有关场景图渲染器工作原理的信息,请访问以下链接:

https://doc-snapshots.qt.io/qt6-dev/qtquick-visualcanvas-scenegraph.html

在本节中,您了解了场景图背后的渲染机制。在下一节中,我们将讨论如何将场景图与原生图形 API 混合使用。

使用原生图形的场景图

场景图提供了两种方法来将场景图与原生图形 API 混合。第一种方法是直接向底层图形引擎发出命令,第二种方法是在场景图中生成纹理节点。应用程序可以通过连接到QQuickWindow::beforeRendering()QQuickWindow::afterRendering()信号来直接在与场景图相同的上下文中进行 OpenGL 调用。使用 Metal 或 Vulkan 等 API 的应用程序可以通过QSGRendererInterface请求原生对象,例如场景图的命令缓冲区。然后用户可以在 Qt Quick 场景内部或外部渲染内容。混合两者的优势是执行渲染不需要额外的帧缓冲区或内存,并且可以避免潜在的昂贵的纹理步骤。缺点是 Qt Quick 选择何时调用信号。OpenGL 引擎只允许在那个时间绘制。

从 Qt 6.0 开始,在调用QQuickWindow::beginExternalCommands()QQuickWindow::endExternalCommands()函数之前必须调用原生图形 API 的直接使用。这种方法与QPainter::beginNativePainting()相同,目的也相同。它允许场景图识别当前记录的渲染通道内的任何缓存状态或对状态的假设。如果存在任何内容,则会因为代码可能直接与原生图形 API 交互而使其无效。

重要提示

在将 OpenGL 内容与场景图渲染相结合时,应用程序不能使 OpenGL 上下文保持绑定缓冲区、启用属性或模板缓冲区中的特定值等。如果忘记了这一点,就会看到意外的行为。自定义渲染代码必须具有线程意识。

场景图还提供了几个日志类别的支持。这些对于查找性能问题和错误的根本原因非常有用。场景图除了公共 API 外还具有适配层。该层允许您实现某些特定于硬件的适配。它具有内部和专有的插件 API,允许硬件适配团队充分利用其硬件。

重要提示

如果您观察到与图形相关的问题,或者想要找出当前使用的渲染循环或图形 API 的类型,请通过将环境变量QSG_INFO设置为1或至少启用qt.scenegraph.generalqt.rhi.*来启动应用程序。在初始化期间,这将打印一些关键信息,以便调试图形问题。

使用 Qt Quick 3D 进行 3D 图形

Qt Quick 3D 是 Qt Quick 的附加组件,提供了用于创建 3D 内容和 3D 用户界面的高级 API。它扩展了 Qt Quick 场景图,允许您将 3D 内容集成到 2D Qt Quick 应用程序中。Qt Quick 3D 是用于在 Qt Quick 平台上创建 3D 内容和 3D 用户界面的高级 API。我们提供了空间内容扩展到现有的 Qt Quick 场景图,以及该扩展场景图的渲染器,而不是依赖外部引擎,这会引入同步问题和额外的抽象层。在使用空间场景图时,也可以混合使用 Qt Quick 2D 和 3D 内容。

在您的.qml文件中的以下import语句可用于将 QML 类型导入您的应用程序:

import QtQuick3D 

除了基本的 Qt Quick 3D 模型外,以下模块导入还提供了其他功能:

import QtQuick3D.Effects
import QtQuick3D.Helpers

Qt Quick 3D 可以在商业许可下购买。在构建源代码时,请确保首先构建qtdeclarativeqtshadertools存储库中的模块和工具,因为没有它们,Qt Quick 3D 无法使用。

让我们在下一节讨论着色器工具和着色器效果。

着色器效果

对于将着色器导入到 3D 场景中,Qt Quick 3D 有自己的框架。着色器效果使得可以通过顶点和片段着色器直接利用图形处理单元的全部原始能力。使用过多的着色器效果可能会导致增加的功耗和有时的性能下降,但是当谨慎使用时,着色器可以允许将复杂和视觉上吸引人的效果应用于视觉对象。

这两个着色器都绑定到vertexShaderfragmentShader属性。每个着色器的代码都需要一个由 GPU 执行的main(){...}函数。以qt_为前缀的变量由 Qt 提供。要了解着色器代码中的变量,请查看 OpenGL API 参考文档。

在使用 Qt Quick 的 QML 应用程序中使用ShaderEffect或对QSGMaterialShader进行子类化时,应用程序必须提供一个.qsb文件形式的烘焙着色器包。Qt Shader Tools 模块包括一个名为.qsb文件的命令行工具。特别是ShaderEffect QML 类型和QSGMaterial子类可以使用 qsb 输出。它还可以用于检查.qsb包的内容。输入文件扩展名用于确定着色器的类型。因此,扩展名必须是以下之一:

  • .vert – 顶点着色器

  • .frag – 片段着色器

  • .comp – 计算着色器

该示例假定myeffect.vertmyeffect.frag包含 Vulkan 风格的 GLSL 代码,通过qsb工具处理以生成.qsb文件。现在,通过以下命令将该 Vulkan 风格着色器用qsb进行转换:

>qsb --glsl 100es,120,150 --hlsl 50 --msl 12 -o <Output_File.qsb> <Input_File.frag>

您可以在以下命令中看到使用上述语法的示例:

>C:\Qt\6.0.2\mingw81_64\bin>qsb --glsl 100es,120,150 --hlsl 50 --msl 12 -o myeffect.frag.qsb myeffect.frag

不需要同时指定vertexShaderfragmentShader。实际上,许多ShaderEffect实现只会提供片段着色器,而不是依赖内置的顶点着色器。

您可以在以下链接中了解有关着色器工具的更多信息:

https://doc.qt.io/qt-6/qtshadertools-qsb.html

让我们在一个示例中使用着色器效果:

import QtQuick
import QtQuick.Window
Window {
    width: 512
    height: 512
    visible: true
    title: qsTr("Shader Effects Demo")
    Row {
        anchors.centerIn: parent
        width: 300
        spacing: 20
        Image {
            id: originalImage
            width: 128; height: 94
            source: "qrc:/logo.png"
        }
        ShaderEffect {
            width: 160; height: width
            property variant source: originalImage
            vertexShader: "grayeffect.vert.qsb"
            fragmentShader: "grayeffect.frag.qsb"
        }
    }
}

在前面的示例中,我们将两个图像排成一行。第一个是原始图像,第二个是带有着色器效果的图像。

在这一部分,您了解了 Qt Quick 中不同类型的着色器效果以及如何使用qsb工具创建兼容的片段文件。在下一部分,您将学习如何使用Canvas进行绘制。

使用 Canvas QML 类型

Canvas输出为图像。它提供了一个使用Context2D对象进行绘制并实现绘制信号处理程序的 2D 画布。

让我们看一下以下示例:

import QtQuick
import QtQuick.Window
Window {
    width: 512
    height: 512
    visible: true
    title: qsTr("Canvas Demo")
    Canvas {
        id: canvas
        anchors.fill: parent
        onPaint: {
            var context = getContext("2d")
            context.lineWidth = 2
            context.strokeStyle = "red"
            context.beginPath()
            context.moveTo(100,100)
            context.lineTo(250,100)
            context.lineTo(250,150)
            context.lineTo(100,150)
            context.closePath()
            context.stroke()
        }
    }
}

在前面的示例中,首先我们从getContext("2d")获取了上下文。然后我们用红色边框绘制了一个矩形。输出如下所示:

图 8.7 – 使用 Canvas 绘制矩形的示例应用程序的输出

图 8.7 – 使用 Canvas 绘制矩形的示例应用程序的输出

在这一部分,你已经熟悉了使用Canvas进行绘制。在下一部分,我们将讨论 Qt Quick 中的粒子系统。

理解粒子模拟

使用粒子系统,您可以模拟爆炸、烟花、烟雾、雾气和风等效果。Qt Quick 包括一个粒子系统,可以实现这些复杂的 2D 模拟,包括对重力和湍流等环境效果的支持。粒子最常用于游戏中,以为当前选定的列表项或活动通知器添加微妙且视觉上吸引人的效果。

ParticleSystemPaintersEmittersAffectors是这个粒子系统中的四种主要 QML 类型。ParticleSystem系统包括 painter、emitter 和 affector 类型。ParticleSystem类型连接所有这些类型并管理共享的时间轴。它们必须共享相同的ParticleSystem才能相互交互。在此约束条件下,您可以拥有尽可能多的粒子系统,因此逻辑上的分离是为所有要交互的类型使用一个ParticleSystem类型,或者如果类型数量较少且易于控制,则只使用一个。

要使用ParticleSystem,请使用以下行导入模块:

 import QtQuick.Particles

发射器产生粒子。发射器在发射后不能再改变粒子。您可以使用affectors类型来影响发射后的粒子。

每种affector类型对粒子的影响都不同:

  • Age:修改粒子的寿命

  • Attractor:将粒子吸引到特定位置

  • 摩擦:根据粒子当前速度减慢移动

  • 重力:设置一个角度上的加速度

  • 湍流:基于噪声图像的液体行为

  • Wander:随机改变路线

  • GroupGoal:改变粒子组的状态

  • SpriteGoal:改变精灵粒子的状态

让我们通过以下示例了解ParticleSystem的用法:

    ParticleSystem {
        id: particleSystem
        anchors.fill: parent
        Image {
            source: "qrc:/logo.png"
            anchors.centerIn: parent
        }
        ImageParticle {
            system: particleSystem
            source: "qrc:/particle.png"
            colorVariation: 0.5
            color: "#00000000"
        }
        Emitter {
            id: emitter
            system: particleSystem
            enabled: true
            x: parent.width/2; y: parent.height/2
            maximumEmitted: 8000; emitRate: 6000
            size: 4 ; endSize: 24
            sizeVariation: 4
            acceleration: AngleDirection {
             angleVariation: 360; magnitude: 360; 
           }
        }
    }

在前面的代码中,我们使用了 Qt 标志,它在周围发射粒子。我们创建了一个ImageParticle的实例,它创建了由Emitter发射的粒子。AngleDirection类型用于决定粒子发射的角度和方向。由于我们希望粒子在标志周围发射,所以我们为两个属性都使用了360。前面示例的输出如图 8.8所示:

图 8.8 – 上述粒子系统示例的输出

图 8.8 – 上述粒子系统示例的输出

您可以在以下网站上了解更多关于这些 QML 类型的信息:

https://qmlbook.github.io/

在这一部分,我们讨论了 Qt Quick 中不同类型的绘制机制和组件。在下一部分,我们将学习如何在 Qt Widgets 中进行动画。

Qt Widgets 中的动画

动画框架通过允许动画化 GUI 元素的属性来简化动画 GUI 元素的过程。QPropertyAnimation类是动画 GUI 元素的更常见的方式之一。这个类是动画框架的一部分,它使用 Qt 的定时器系统在指定的时间段内改变 GUI 元素的属性。

为了为我们的 GUI 应用程序创建动画,Qt 为我们提供了几个子系统,包括定时器、时间轴、动画框架、状态机框架和图形视图框架。

让我们讨论如何在以下代码中使用QPushButton的属性动画:

QPropertyAnimation *animatateButtonA = new
QPropertyAnimation(ui->pushButtonA, "geometry");
animatateButtonA->setDuration(2000);
animatateButtonA->setStartValue(ui->pushButtonA->geometry());
animatateButtonA->setEndValue(QRect(100, 150, 200, 300));

在前面的代码片段中,我们将一个按钮从一个位置动画到另一个位置,并改变了按钮的大小。您可以通过在调用start()函数之前将缓动曲线添加到属性动画中来控制动画。您还可以尝试不同类型的缓动曲线,看哪种对您最有效。

属性动画和动画组都是从QAbstractAnimator类继承的。因此,您可以将一个动画组添加到另一个动画组中,创建一个更复杂的嵌套动画组。Qt 目前提供两种类型的动画组类,QParallelAnimationGroupQSequentialAnimationGroup

让我们使用QSequentialAnimationGroup组来管理其中的动画状态:

QSequentialAnimationGroup *group = new QSequentialAnimationGroup;
group->addAnimation(animatateButtonA);
group->addAnimation(animatateButtonB);
group->addAnimation(animatateButtonC);

您可以在以下链接中了解更多关于 Qt 的动画框架:

https://doc.qt.io/qt-6/animation-overview.html

在本节中,我们讨论了 Qt Widgets 中的动画。在下一节中,您将学习如何在 Qt Quick 中进行动画。

Qt Quick 中的动画和过渡

在本节中,您将学习如何在 Qt Quick 中创建动画并添加过渡效果。要创建动画,您需要为要动画化的属性类型选择适当的动画类型,然后将动画应用于所需的行为。

Qt Quick 有不同类型的动画,例如以下:

  • Animator:它是一种特殊类型的动画,直接作用于 Qt Quick 的场景图。

  • AnchorAnimation:用于动画化锚点更改。

  • ParallelAnimation:并行运行动画。

  • ParentAnimation:用于动画化父级更改。

  • PathAnimation:沿着路径动画化一个项目。

  • PauseAnimation:它允许在动画期间暂停。

  • PropertyAnimation:它动画化属性值的变化。

  • SequentialAnimation:按顺序运行动画。

  • ScriptAction:在动画期间,允许执行 JavaScript。

  • PropertyAction:它可以在动画期间立即更改属性,而无需动画化属性更改。

图 8.9显示了动画类的层次结构:

图 8.9 - Qt Quick 中动画类的层次结构

图 8.9 - Qt Quick 中动画类的层次结构

PropertyAnimation提供了一种方式来动画化属性值的变化。PropertyAnimation的不同子类如下:

  • ColorAnimation:动画化颜色值的变化

  • NumberAnimation:动画化qreal类型值的变化

  • RotationAnimation:动画化旋转值的变化

  • Vector3dAnimation:动画化QVector3d值的变化

可以以多种方式定义动画:

  • Transition

  • Behavior

  • 作为property

  • signal处理程序中

  • 独立的

通过应用动画类型来动画化属性值。为了创建平滑的过渡效果,动画类型将插值属性值。状态转换也可以将动画分配给状态变化:

  • SmoothedAnimation:它是一个专门的NumberAnimation子类。在动画中,当目标值发生变化时,SmoothAnimation确保平滑变化。

  • SpringAnimation:具有质量、阻尼和 epsilon 等专门属性,提供弹簧式动画。

可以通过不同方式为对象设置动画:

  • 直接属性动画

  • 预定义的目标和属性

  • 动画作为行为

  • 状态变化期间的过渡

动画是通过将动画对象应用于属性值来逐渐改变属性,从而创建的。通过在属性值变化之间插值来实现平滑的动作。属性动画允许通过缓动曲线进行不同的插值和时间控制。

以下代码片段演示了使用预定义属性的两个PropertyAnimation对象:

Rectangle {
    id: rect
    width: 100; height: 100
    color: "green"
    PropertyAnimation on x { to: 200 }
    PropertyAnimation on y { to: 200 }
}

在前面的示例中,动画将在Rectangle加载后立即开始,并自动应用于其xy值。在这里,我们使用了<AnimationType> on <Property>语法。因此,不需要将目标和属性值设置为xy

动画可以按顺序或并行显示。顺序动画依次播放一组动画,而并行动画同时播放一组动画。因此,当动画被分组在SequentialAnimationParallelAnimation中时,它们将被顺序或并行播放。SequentialAnimation也可以用于播放Transition动画,因为过渡动画会自动并行播放。您可以将动画分组以确保组内的所有动画都应用于同一属性。

让我们在下面的示例中使用SequentialAnimation来对矩形的color进行动画处理:

import QtQuick
import QtQuick.Window
Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Sequential Animation Demo")
    Rectangle {
        anchors.centerIn: parent
        width: 100; height: 100
        radius: 50
        color: "red"
        SequentialAnimation on color {
            ColorAnimation { to: "red"; duration: 1000 }
            ColorAnimation { to: "yellow"; duration: 1000 }
            ColorAnimation { to: "green"; duration: 1000 }
            running:true
            loops: Animation.Infinite
        }
    }
}

在前面的示例中,我们使用了<AnimationType> on <Property>语法在color属性上使用了SequentialAnimation。因此,子ColorAnimation对象会自动添加到此属性,不需要设置targetproperty动画值。

您可以使用Behavior动画来设置默认的属性动画。在Behavior类型中指定的动画将应用于属性,并使任何属性值的变化发生动画。要有意地启用或禁用行为动画,可以使用enabled属性。您可以使用多种方法将行为动画分配给属性。其中一种方法是Behavior on <property>声明。它可以方便地将行为动画分配到属性上。

Animator类型与普通的Animation类型不同。让我们创建一个简单的例子,通过使用Animator来旋转图像:

import QtQuick
import QtQuick.Window
Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Animation Demo")
    Image {
        anchors.centerIn: parent
        source: "qrc:/logo.png"
        RotationAnimator on rotation {
            from: 0; to: 360;
            duration: 1000
            running:true
            loops: Animation.Infinite
        }
    }
}

在前面的示例中,我们使用了RotationAnimator类型,用于动画处理ImageQML 类型的旋转。

在本节中,我们讨论了 Qt Quick 中不同类型的动画,并创建了几个示例。在下一节中,我们将讨论如何控制动画。

控制动画

Animation类型是所有动画类型的祖先。这种类型不允许创建Animation对象。它为用户提供了使用动画类型所需的属性和方法。所有动画类型都包括start()stop()resume()pause()restart()complete(),它们控制动画的执行方式。

动画在开始和结束值之间的插值由缓动曲线定义。不同的缓动曲线可能超出定义的插值范围。缓动曲线使得更容易创建动画效果,如弹跳、加速、减速和循环动画。

在 QML 对象中,每个属性动画可能具有不同的缓动曲线。曲线可以通过各种参数进行控制,其中一些参数是特定于特定曲线的。请访问缓动文档以获取有关缓动曲线的更多信息。

在本节中,您了解了如何在 Qt Quick 中控制动画。在下一节中,您将学习如何使用状态和过渡。

Qt Quick 中的状态、状态机和转换

Qt Quick 状态是属性配置,其中属性的值可以更改以反映不同的状态。状态更改会导致属性的突然变化;动画平滑过渡,以创建视觉上吸引人的状态更改。声明性状态机框架提供了用于在 QML 中创建和执行状态图的类型。考虑使用 QML 状态和转换来创建用户界面,其中多个视觉状态独立于应用程序的逻辑状态。

您可以通过添加以下语句将状态机模块和 QML 类型导入到您的应用程序中:

import QtQml.StateMachine

请注意,在 QML 中有两种定义状态的方式。一种由QtQuick提供,另一种由QtQml.StateMachine模块提供。

重要提示

在单个 QML 文件中使用QtQuickQtQml.StateMachine时,请确保在QtQuick之后导入QtQml.StateMachine。在这种方法中,State类型由声明性状态机框架提供,而不是由QtQuick提供。为了避免与 QtQuick 的State项产生任何歧义,您可以将QtQml.StateMachine导入到不同的命名空间中。

为了插值由状态更改引起的属性更改,Transition类型可以包括动画类型。将转换绑定到transitions属性以将其分配给对象。

按钮可以有两种状态:pressedreleased。对于每个状态,我们可以分配不同的属性配置。转换将动画化从pressedreleased的过渡。同样,在从releasedpressed状态切换时也会有动画。

让我们看看以下示例。

使用Rectangle QML 类型创建一个圆形 LED,并向其添加MouseArea。将默认状态分配为OFF,颜色为绿色。在鼠标按下时,我们希望将 LED 颜色更改为红色,一旦释放鼠标,LED 再次变为绿色

Rectangle {
     id:led
     anchors.centerIn: parent
     width: 100
     height: 100
     radius: 50
     color: "green"
     state: "OFF"
     MouseArea {
        anchors.fill: parent
        onPressed: led.state = "ON"
        onReleased: led.state = "OFF"
    }
}

接下来,定义状态。在此示例中,我们有两个状态,ONOFF。在这里,我们根据状态更改来操作color属性:

states: [
       State {
            name: "ON"
            PropertyChanges { target: led; color: "red"}
       },
       State {
            name: "OFF"
            PropertyChanges { target: led; color: "green"}
       }
   ]

您可以向转换添加动画。让我们向转换添加ColorAnimation,使其平滑而有吸引力:

transitions: [
    Transition {
        from: "ON"
        to: "OFF"
        ColorAnimation { target: led; duration: 100}
     },
     Transition {
         from: "OFF"
         to: "ON"
         ColorAnimation { target: led; duration: 100}
     }
]

在上面的示例中,我们使用了两个状态,ONOFF。我们使用MouseArea根据鼠标按下和释放事件来更改状态。当状态为ON时,矩形颜色变为红色,当状态为OFF时,颜色变为绿色。在这里,我们还使用了Transition来在状态之间切换。

tofrom属性绑定到状态的名称时,转换将与状态更改相关联。对于简单或对称的转换,将to属性设置为通配符符号"*"意味着转换适用于任何状态更改:

transitions: Transition {
    to: "*"
    ColorAnimation { target: led; duration: 100 }
}

您可以在以下链接中了解有关状态机 QML API 的更多信息:

https://doc.qt.io/qt-6/qmlstatemachine-qml-guide.html

在本节中,您了解了 Qt Quick 中的状态机。在下一节中,您将学习如何在 Qt Widgets 中使用状态机。

Qt Widgets 中的状态机

状态机框架中的类可用于创建和执行状态图。状态机框架为在 Qt 应用程序中有效地嵌入状态图元素和语义提供了 API 和执行模型。该框架与 Qt 的元对象系统紧密集成。

在 Qt 6 中,状态机框架发生了重大变化。API 在 Qt 6.0.x 核心模块中丢失了。在 Qt 6.1 中,该模块被恢复为statemachine,以便在.pro文件中使用该框架。

如果您正在使用基于qmake的构建系统,则将以下行添加到您的.pro文件中:

QT += statemachine

如果您正在使用基于CMake的构建系统,则将以下内容添加到CMakeLists.txt中:

find_package(Qt6 COMPONENTS StateMachine REQUIRED)
target_link_libraries(mytarget PRIVATE Qt6::StateMachine)

您需要在 C++源文件中包含以下标头:

#include <QStateMachine>
#include <QState>

让我们创建一个简单的 Qt Widgets 应用程序,实现状态机。通过添加QLabelQPushButton修改 UI 表单:

  1. 将以下代码添加到您自定义的 C++类的构造函数中:
QState *green = new QState();
green->assignProperty(ui->pushButton, "text", "Green");
green->assignProperty(ui->led, 
"styleSheet","background-color: rgb(0, 190, 0);");
green->setObjectName("GREEN");
  1. 在上面的代码中,我们创建了一个状态来显示绿色 LED。接下来,我们将为红色 LED 创建另一个状态:
QState *red = new QState();
red->setObjectName("RED");
red->assignProperty(ui->pushButton, "text", "Red");
red->assignProperty(ui->led, "styleSheet", "background-color: rgb(255, 0, 0);");
  1. 为按钮切换时的状态改变事件添加转换:
green->addTransition(ui->pushButton,  
&QAbstractButton::clicked,red);
red->addTransition(ui->pushButton,
&QAbstractButton::clicked,green);
  1. 现在创建一个状态机实例并向其添加状态:
QStateMachine *machine = new QStateMachine(this);
machine->addState(green);
machine->addState(red);
machine->setInitialState(green);
  1. 最后一步是启动状态机:
machine->start();
  1. 当您运行上面的示例时,您将看到一个输出窗口,如下所示:

图 8.10 - 在 Qt Widgets 中使用状态机的应用输出

图 8.10 - 在 Qt Widgets 中使用状态机的应用输出

前面的图表强调了在父状态机中,只能将子状态机的状态指定为转换目标。另一方面,父状态机的状态不能被指定为子状态机中的转换目标。

以下文章很好地捕捉了在使用状态机时的性能考虑:

https://www.embedded.com/how-to-ensure-the-best-qt-state-machine-performance/

在本节中,我们学习了状态机及其在 Qt Widgets 中的使用。我们讨论了如何在 Qt Widgets 和 Qt Quick 中实现状态机。让我们总结一下本章学到的内容。

摘要

在本章中,我们讨论了不同的图形 API,并学习了如何使用QPainter类在屏幕上和屏幕外绘制图形。我们还研究了图形视图框架和场景图渲染机制。我们看到了 Qt 如何在整个本章中提供QPaintDevice接口和QPainter类来执行图形操作。我们还讨论了图形视图类、OpenGL 框架和着色器工具。在本章末尾,我们探讨了 Qt Widgets 和 Qt Quick 中的动画和状态机框架。

第九章测试和调试中,我们将学习在 Qt 中进行调试和测试。这将帮助您找到问题的根本原因并修复缺陷。

第九章:测试和调试

调试和测试是软件开发的重要部分。在本章中,您将学习如何调试 Qt 项目,不同的调试技术以及 Qt 支持的调试器。调试是发现错误或不良行为根本原因并解决它的过程。我们还将讨论使用 Qt 测试框架进行单元测试。Qt Test 是一个用于 Qt 应用程序和库的单元测试框架。它具有大多数单元测试框架提供的所有功能。此外,它还提供了对图形用户界面GUI)的支持。该模块有助于以方便的方式为基于 Qt 的应用程序和库编写单元测试。您还将学习使用不同 GUI 测试工具测试 GUI 的技术。

具体来说,我们将讨论以下主题:

  • 在 Qt 中调试

  • 调试策略

  • 调试 C++应用程序

  • 调试 Qt Quick 应用程序

  • 在 Qt 中进行测试

  • 与 Google 的 C++测试框架集成

  • 测试 Qt Quick 应用程序

  • GUI 测试工具

在本章结束时,您将熟悉调试和测试技术,以用于您的 Qt 应用程序。

技术要求

本章的技术要求包括在最新版本的桌面平台(如 Windows 10、Ubuntu 20.04 或 macOS 10.14)上安装的 Qt 6.0.0 和 Qt Creator 4.14.0 的最低版本。

本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp/tree/master/Chapter09

重要提示

本章中使用的屏幕截图来自 Windows 平台。您将在您的机器上基于底层平台看到类似的屏幕。

在 Qt 中调试

在软件开发中,技术问题经常出现。为了解决这些问题,我们必须首先识别并解决所有问题,然后才能将应用程序发布到公众以保持质量和声誉。调试是一种定位这些潜在技术问题的技术。

在接下来的章节中,我们将讨论软件工程师使用的流行调试技术,以确保其软件的稳定性和质量。

Qt 支持的调试器

Qt 支持多种不同类型的调试器。您使用的调试器可能会因项目所用的平台和编译器而有所不同。以下是与 Qt 广泛使用的调试器列表:

  • GNU Symbolic DebuggerGDB)是由 GNU 项目开发的跨平台调试器。

  • Microsoft Console DebuggerCDB)是微软为 Windows 开发的调试器。

  • Low Level Virtual Machine DebuggerLLDB)是由 LLVM 开发组开发的跨平台调试器。

  • QML/JavaScript Debugger是 Qt 公司提供的 QML 和 JavaScript 调试器。

如果您在 Windows 上使用 MinGW 编译器,则不需要对 GDB 进行任何手动设置,因为它通常包含在 Qt 安装中。如果您使用其他操作系统,如 Linux,在将其链接到 Qt Creator 之前,您可能需要手动安装它。Qt Creator 会自动检测 GDB 的存在并将其添加到其调试器列表中。

您还可以通过指定--vgdb=yes--vgdb=full来使用gdbserver。您可以指定--vgdb-error=number来在显示一定数量的错误后激活gdbserver。如果将值设置为0,则gdbserver将在初始化时激活,允许您在应用程序启动之前设置断点。值得注意的是,vgdb包含在Valgrind发行版中。它不需要单独安装。

如果您喜欢的平台是 Windows,您可以在计算机上安装 CDB。默认情况下,Visual Studio 的内置调试器将不可用。因此,在安装 Windows SDK 时,您必须选择调试工具作为可选组件单独安装 CDB 调试器。Qt Creator 通常会识别 CDB 的存在,并将其添加到选项下的调试器列表中。

Android 调试比在常规桌面环境中调试要困难一些。Android 开发需要不同的软件包,如 JDK、Android SDK 和 Android NDK。在桌面平台上,您需要Android 调试桥ADB)驱动程序来允许 USB 调试。您必须在 Android 设备上启用开发者模式并接受 USB 调试才能继续。

macOS 和 iOS 上使用的调试器是LLDB。它默认包含在 Xcode 中。Qt Creator 将自动检测其存在并将其链接到一个工具包。如果您熟悉调试器并知道自己在做什么,还可以将非 GDB 调试器添加到您喜爱的 IDE 中。

调试器插件根据计算机上可用的内容,为每个软件包确定合适的本地调试器。您可以通过添加新的调试器来克服这种偏好。您可以在选项菜单下的Kits设置中的调试器选项卡中找到可用的调试器,如图 9.1所示:

图 9.1 - 选取屏幕下的调试器选项卡显示添加按钮

图 9.1 - 选取屏幕下的调试器选项卡显示添加按钮

调试器选项卡中,您可以在右侧看到添加克隆删除按钮。您可以克隆现有的调试器配置并修改以满足您的要求。或者,如果您了解调试器的详细信息和配置,那么您可以使用添加按钮创建新的调试器配置。您还可以通过单击删除按钮删除有问题或过时的调试器配置。不要忘记单击应用按钮以保存更改。请注意,您无法修改自动检测到的调试器配置。

在本节中,我们了解了各种支持的调试器。在下一节中,我们将讨论如何调试应用程序。

调试策略

有不同的调试策略来找到问题的根本原因。在尝试定位应用程序中的错误之前,深入了解程序或库至关重要。如果您不知道自己在做什么,就无法找到错误。只有对系统及其运行方式有深入了解,才能够识别应用程序中的错误。以往的经验可以帮助检测类似类型的错误以及解决错误。个人专家的知识决定了开发人员能够多快地定位错误。您可以添加调试打印语句和断点来分析程序的流程。您可以进行前向分析或后向分析来跟踪错误的位置。

在调试时,以下步骤用于查找根本原因并解决问题:

  1. 确定问题。

  2. 定位问题。

  3. 分析问题。

  4. 解决问题。

  5. 修复副作用。

无论编程语言或平台如何,调试应用程序时最重要的是知道代码的哪一部分导致了问题。您可以通过多种方式找到有问题的代码。

如果缺陷是由您的 QA 团队或用户提出的,请询问问题发生的时间。查看日志文件或任何错误消息。注释掉怀疑的代码部分,然后再次构建和运行应用程序,以查看问题是否仍然存在。如果问题是可重现的,通过打印消息和注释掉代码行来进行前向和后向分析,直到找到导致问题的代码行。

您还可以在内置调试器中设置断点,以搜索目标功能中的变量更改。如果其中一个变量已更新为意外值,或者对象指针已成为无效指针,则可以轻松识别它。检查您在安装程序中使用的所有模块,并确保您和您的用户使用的是应用程序的相同版本号。如果您使用的是不同版本或不同分支,请检出带有指定版本标签的分支,然后调试代码。

在下一节中,我们将讨论如何通过打印调试消息和添加断点来调试您的 C++代码。

调试 C++应用程序

QDebug类可用于将变量的值打印到应用程序输出窗口。QDebug类似于标准库中的std::cout,但它的好处是它是 Qt 的一部分,这意味着它支持 Qt 类,并且可以在不需要转换的情况下显示其值。

要启用调试消息,我们必须包含QDebug头文件,如下所示:

#include <QDebug>

Qt 提供了几个用于生成不同类型调试消息的全局宏。它们可以用于不同的目的,如下所述:

  • qDebug()提供自定义调试消息。

  • qInfo()提供信息性消息。

  • qWarning()报告警告和可恢复错误。

  • qCritical()提供关键错误消息和报告系统错误。

  • qFatal()在退出之前提供致命错误消息。

您可以使用qDebug()来查看您的功能是否正常工作。在查找错误完成后,删除包含qDebug()的代码行,以避免不必要的控制台日志。让我们看看如何使用qDebug()来打印变量到输出窗格的示例。创建一个样本QWidget应用程序,并添加一个函数setValue(int value),并在函数定义内添加以下代码:

int value = 500;
qDebug() << "The value is : " << value;

上述代码将在 Qt Creator 底部的输出窗口中显示以下输出:

The value is : 500

您可以通过查看函数的使用次数和在应用程序内调用的次数来确定值是否被另一个函数更改。如果调试消息多次打印,则它是从多个位置调用的。检查是否将正确的值发送到所有调用函数。在查找问题完成后,删除包含qDebug()的代码行,以消除输出控制台窗口中不必要的控制台日志。或者,您可以实现条件编译。

让我们进一步了解 Qt Creator 中的调试和调试选项:

  1. 您可以在菜单栏中看到一个调试菜单。单击它时,您将看到一个上下文菜单,其中包含如图 9.2所示的子菜单:图 9.2 - Qt Creator 中的调试菜单

图 9.2 - Qt Creator 中的调试菜单

  1. 要开始调试,请按F5或单击 Qt Creator 左下角的开始调试按钮,如下所示:图 9.3 - Qt Creator 中的开始调试按钮

图 9.3 - Qt Creator 中的开始调试按钮

  1. 如果 Qt Creator 以错误消息抱怨调试器,则检查您的项目包是否有调试器。

  2. 如果错误仍然存在,请关闭 Qt Creator 并转到您的项目文件夹,您可以在那里删除.pro.user文件。

  3. 然后在 Qt Creator 中重新加载项目。Qt Creator 将重新配置您的项目,并且调试模式现在应该可用。

调试应用程序的一个很好的方法是设置断点:

  1. 当您在 Qt Creator 中右键单击脚本的行号时,将会看到一个包含三个选项的弹出菜单。

  2. 您还可以单击行号添加断点。单击行号设置断点。您将在行号上看到一个红点出现。

  3. 接下来,按下键盘上的F5键或单击Debug按钮。运行应用程序以调试模式,您会注意到第一个红点上方出现了一个黄色箭头:图 9.4 -  Qt Creator 显示调试窗口和断点

图 9.4 - Qt Creator 显示调试窗口和断点

  1. 调试器已在第一个断点处停止。现在,变量及其含义和类型将显示在 Qt Creator 右侧的LocalsExpression窗口中。

  2. 这种方法可以快速检查应用程序。要删除断点,只需再次单击红点图标或从右键单击上下文菜单中删除:

图 9.5 - 上下文菜单显示断点标记的右键单击选项

图 9.5 - 上下文菜单显示断点标记的右键单击选项

重要的是要记住,必须在调试模式下运行应用程序。这是因为在调试模式下编译时,您的应用程序或库将具有额外的调试符号,允许调试器从二进制源代码中访问信息,例如标识符、变量和函数的名称。这就是为什么在调试模式下编译的应用程序或库二进制文件在文件大小上更大的原因。

您可以在以下文档中了解更多功能及其用法:

doc.qt.io/qt-6/debug.html

重要提示

一些防病毒应用程序会阻止调试器检索信息。Avira 就是这样的防病毒软件。如果在生产 PC 上安装了它,调试器在 Windows 平台上可能会失败。

在下一节中,我们将讨论如何调试 Qt Quick 应用程序并定位 QML 文件中的问题。

调试 Qt Quick 应用程序

在上一节中,我们讨论了如何调试 C++代码。但您可能仍然想知道如何调试 QML 中编写的代码。Qt 还提供了调试 QML 代码的功能。在开发 Qt Quick 应用程序时,有很多选项可以解决问题。在本节中,我们将讨论与 QML 相关的各种调试技术以及如何使用它们。

就像QDebug类一样,在 QML 中有不同的控制台 API 可用于调试。它们如下:

  • Log:用于打印一般消息。

  • Assert:用于验证表达式。

  • Timer:用于测量调用之间花费的时间。

  • Trace:用于打印 JavaScript 执行的堆栈跟踪。

  • Count:用于查找对函数的调用次数。

  • Profile:用于对 QML 和 JavaScript 代码进行分析。

  • Exception:用于打印错误消息。

控制台 API 提供了几个方便的函数来打印不同类型的调试消息,例如console.log()console.debug()console.info()console.warn()console.error()。您可以按以下方式打印带有参数值的消息:

console.log("Value is:", value)

您还可以通过在Components.onCompleted:{…}中添加消息来检查组件的创建:

Components.onCompleted: { 
     console.log("Component created") 
}

要验证表达式是否为真,您可以使用console.assert(),例如以下示例:

console.assert(value == 100, "Reached the maximum limit");

您会发现console.time()console.timeEnd()记录了调用之间花费的时间。console.trace()打印了 JavaScript 执行的堆栈跟踪。堆栈跟踪详细信息包括函数名、文件名、行号和列号。

console.count()返回代码执行次数以及消息。当使用console.profile()时,QML 和 JavaScript 分析被激活,当调用console.profileEnd()时被停用。您可以使用console.exception()打印错误消息以及 JavaScript 执行的堆栈跟踪。

您可以以与前一节讨论的相同方式添加断点,如下所示:

  • 进入堆栈中的代码,单击工具栏上的Step Into按钮或按下F11键。

  • 要退出,请按Shift + F11。要命中断点,请在方法末尾添加断点,然后单击Continue

  • 打开 QML 调试器控制台输出窗格,以在当前上下文中运行 JavaScript 命令。

在运行 Qt Quick 应用程序时,您可以找到问题并观察值。这将帮助您找到导致意外行为并需要修改的代码部分。

在本节中,我们了解了在 QML 环境中进行调试。在下一节中,我们将讨论 Qt 中的测试框架。

在 Qt 中进行测试

单元测试是使用自动化工具测试简单应用程序、类或函数的一种方法。在讨论如何将其纳入我们的方法之前,我们将讨论它是什么以及为什么我们希望这样做。单元测试是将应用程序分解为最小的功能单元,然后在倡议框架内使用真实世界的情况对每个单元进行测试的过程。单元是可以测试的应用程序的最小组件。在过程式编程中,单元测试通常侧重于函数或过程。

在面向对象编程中,单元通常是接口、类或单个函数。单元测试早期识别实施过程中的问题。这涵盖了程序员实现中的缺陷,以及单元规范中的缺陷或不完整部分。在创建过程中,单元测试是由要测试的单元的开发人员开发的短代码片段。有许多单元测试工具可用于测试您的 C++代码。让我们探讨 Qt 测试框架的优势和特点。

在 Qt 中进行单元测试

Qt Test 是用于基于 Qt 的应用程序和库的单元测试平台。Qt Test 包括传统单元测试应用程序中的所有功能,以及用于测试图形用户界面的插件。它有助于更轻松地为基于 Qt 的程序和库编写单元测试。图 9.6显示了选项下的测试部分:

图 9.6–显示 Qt Creator 选项菜单下的 Qt Test 首选项的屏幕截图

图 9.6–显示 Qt Creator 选项菜单下的 Qt Test 首选项的屏幕截图

以前,单元测试可能是手动完成的,特别是对于 GUI 测试,但现在有一个工具可以让您编写代码自动验证代码,这乍一看似乎有些违反直觉,但它确实有效。Qt Test 是一个基于 Qt 的专门单元测试框架。

您必须在项目文件(.pro)中添加testlib以使用 Qt 的内置单元测试模块:

QT += core testlib

接下来,运行qmake以将模块添加到您的项目中。为了使测试系统找到并实现它,您必须使用QTest头文件并将测试函数声明为私有槽。QTest头文件包含与 Qt Test 相关的所有函数和语句。要使用QTest功能,只需在您的 C++文件中添加以下行:

#include <QTest>

您应该为每种可能的情况编写测试用例,然后在基线代码更改时运行测试,以确保系统继续按预期行为。这是一个非常有用的工具,可以确保任何编程更新不会破坏现有功能。

让我们使用 Qt Creator 内置的向导创建一个简单的测试应用程序。从新建项目菜单中选择自动测试项目,如图 9.7所示:

图 9.7–项目向导中的新自动测试项目选项

图 9.7–项目向导中的新自动测试项目选项

生成测试项目框架后,您可以修改生成的文件以满足您的需求。打开测试项目的.pro文件,并添加以下代码行:

QT += testlib
QT -= gui
CONFIG += qt console warn_on depend_includepath testcase
CONFIG -= app_bundle
TEMPLATE = app
SOURCES +=  tst_testclass.cpp

让我们创建一个名为TestClass的 C++类。我们将把我们的测试函数添加到这个类中。这个类必须派生自QObject。让我们看一下tst_testclass.cpp

#include <QtTest>
class TestClass : public QObject
{
    Q_OBJECT
public:
    TestClass() {}
    ~TestClass(){}
private slots:
    void initTestCase(){}
    void cleanupTestCase() {}
    void test_compareStrings();
    void test_compareValues();
};

在前面的代码中,我们声明了两个测试函数来测试样本字符串和值。您需要为声明的测试用例实现测试函数的测试场景。让我们比较两个字符串并进行简单的算术运算。您可以使用诸如QCOMPAREQVERIFY之类的宏来测试值:

void TestClass::test_compareStrings()
{
    QString string1 = QLatin1String("Apple");
    QString string2 = QLatin1String("Orange");
    QCOMPARE(string1.localeAwareCompare(string2), 0);
}
void TestClass::test_compareValues()
{
    int a = 10;
    int b = 20;
    int result = a + b;
    QCOMPARE(result,30);
}

要执行所有测试用例,您必须在文件底部添加诸如QTEST_MAIN()的宏。QTEST_MAIN()宏扩展为一个简单的main()方法,用于运行所有测试函数。QTEST_APPLESS_MAIN()宏适用于简单的独立非 GUI 测试,其中不使用QApplication对象。如果不需要 GUI 但需要事件循环,则使用QTEST_GUILESS_MAIN()

QTEST_APPLESS_MAIN(TestClass)
#include "tst_testclass.moc"

为了使测试用例成为一个独立的可执行文件,我们添加了QTEST_APPLESS_MAIN()宏和类的moc生成文件。您可以使用许多其他宏来测试应用程序。有关更多信息,请访问以下链接:

doc.qt.io/qt-6/qtest.html#macros

当您运行上面的示例时,您将看到如下所示的测试结果输出:

********* Start testing of TestClass *********
Config: Using QtTest library 6.1.0, Qt 6.1.0 (x86_64-little_endian-llp64 shared (dynamic) release build; by GCC 8.1.0), windows 10
64bit HCBT_CREATEWND event start
PASS   : TestClass::initTestCase()
FAIL!  : TestClass::test_compareStrings() Compared values are not the same
   Actual   (string1.localeAwareCompare(string2)): -1
   Expected (0)                                  : 0
..\TestProject\tst_testclass.cpp(26) : failure location
PASS   : TestClass::test_compareValues()
PASS   : TestClass::cleanupTestCase()
Totals: 3 passed, 1 failed, 0 skipped, 0 blacklisted, 7ms
********* Finished testing of TestClass *********

您可以看到一个测试用例失败,因为它未满足测试标准。类似地,您可以添加更多的测试用例,并从另一个类中获取参数来测试功能。您还可以使用运行所有测试选项从 Qt Creator 菜单栏的测试上下文菜单中运行所有测试,如图 9.8所示:

图 9.8 - 工具菜单下的测试选项

图 9.8 - 工具菜单下的测试选项

您还可以在左侧的项目资源管理器视图中查看所有测试用例。从项目资源管理器下拉菜单中选择测试。您可以在此窗口中启用或禁用某些测试用例。图 9.9显示了我们之前编写的两个测试用例。您还可以看到我们没有在这个测试项目中使用其他测试框架:

图 9.9 - 项目资源管理器下拉菜单中的测试资源管理器选项

图 9.9 - 项目资源管理器下拉菜单中的测试资源管理器选项

您可以使用几个QTest便利函数来模拟 GUI 事件,如键盘或鼠标事件。让我们看一个简单的代码片段的用法:

QTest::keyClicks(testLineEdit, "Enter");
QCOMPARE(testLineEdit->text(), QString("Enter"));

在前面的代码中,测试代码模拟了lineedit控件上的键盘文本Enter事件,然后验证了输入的文本。您还可以使用QTest::mouseClick()来模拟鼠标点击事件。您可以按照以下方式使用它:

QTest::mouseClick(testPushBtn, Qt::LeftButton);

Qt 的测试框架在测试驱动开发TDD)中也很有用。在 TDD 中,您首先编写一个测试,然后编写实际的逻辑代码。由于没有实现,测试最初会失败。然后,您编写必要的最少代码以通过测试,然后再进行下一个测试。这是在实现必要功能之前迭代开发功能的方法。

在本节中,我们学习了如何创建测试用例并模拟 GUI 交互事件。在下一节中,您将学习如何使用 Google 的 C++测试框架。

与 Google 的 C++测试框架集成

GoogleTest是由 Google 开发的测试和模拟框架。GoogleMock项目已合并到 GoogleTest 中。GoogleTest 需要支持至少 C++11 标准的编译器。它是一个跨平台的测试框架,支持 Windows、Linux 和 macOS 等主要桌面平台。它可以帮助您使用高级功能(如模拟)编写更好的 C++测试。您可以将 Qt Test 与 GoogleTest 集成,以充分利用两个框架的优势。如果您打算使用两个测试框架的功能,则应将 GoogleTest 用作主要测试框架,并在测试用例中使用 Qt Test 的功能。

Qt Creator 内置支持 GoogleTest。您可以在选项屏幕的测试部分中找到Google 测试选项卡,并设置全局的 GoogleTest 偏好,如图 9.10所示:

图 9.10 - 选项菜单下测试部分中的 Google 测试选项卡

图 9.10 - 选项菜单下测试部分中的 Google 测试选项卡

您可以从以下链接下载 GoogleTest 源代码:

github.com/google/googletest

您可以在以下文档中了解更多关于功能及其用法的信息:

google.github.io/googletest/primer.html

下载源代码后,在创建示例应用程序之前构建库。您还可以将统一的 GoogleTest 源代码与测试项目一起构建。生成库后,按照以下步骤运行您的 GoogleTest 应用程序:

  1. 要使用 Qt Creator 内置的向导创建一个简单的 GoogleTest 应用程序,请从新建项目菜单中选择自动测试项目。然后按照屏幕操作直到出现项目和测试信息

  2. 项目和测试信息屏幕上,选择Google 测试作为测试框架。然后按照图 9.11所示添加测试套件名称测试用例名称字段的信息:图 9.11 - 项目创建向导中的 Google 测试选项

图 9.11 - 项目创建向导中的 Google 测试选项

  1. 接下来,您可以填写.pro文件。图 9.12 - 在项目创建向导中添加 GoogleTest 源目录的选项

图 9.12 - 在项目创建向导中添加 GoogleTest 源目录的选项

  1. 单击下一步,按照说明生成项目的框架。

  2. 要使用 GoogleTest,您必须将头文件添加到测试项目中:

#include "gtest/gtest.h"
  1. 您可以看到主函数已经被向导创建:
#include "tst_calculations.h"
#include "gtest/gtest.h"
int main(int argc,char *argv[])
{
    ::testing::InitGoogleTest(&argc,argv);
    return RUN_ALL_TESTS();
}
  1. 您可以使用以下语法创建一个简单的测试用例:
TEST(TestCaseName, TestName) { //test logic }
  1. GoogleTest 还提供了诸如ASSERT_*EXPECT_*的宏来检查条件和值:
ASSERT_TRUE(condition)
ASSERT_EQ(expected,actual)
ASSERT_FLOAT_EQ(expected,actual)
EXPECT_DOUBLE_EQ (expected, actual)

在大多数情况下,在运行多个测试之前进行一些自定义的初始化工作是标准的程序。如果您想评估测试的时间/内存占用情况,您将不得不编写一些特定于测试的代码。测试装置有助于设置特定的测试要求。fixture类是从::testing::Test类派生的。请注意,使用TEST_F宏而不是TEST。您可以在构造函数或SetUp()函数中分配资源和进行初始化。同样,您可以在析构函数或TearDown()函数中释放资源。测试装置中的测试函数定义如下:

TEST_F(TestFixtureName, TestName) { //test logic }
  1. 创建和使用测试装置,创建一个从::testing::Test类派生的类,如下所示:
class PushButtonTests: public ::testing::Test
{
protected:
    virtual void SetUp()
    {
        pushButton = new MyPushButton(0);
        pushButton ->setText("My button");
    }
};
TEST_F(PushButtonTests, sizeConstraints)
{
    EXPECT_EQ(40, pushButton->height());
    EXPECT_EQ(200, pushButton->width());
    pushButton->resize(300,300);
    EXPECT_EQ(40, pushButton->height());
    EXPECT_EQ(200, pushButton->width());
}
TEST_F(PushButtonTests, enterKeyPressed)
{
    QSignalSpy spy(pushButton, SIGNAL(clicked()));
    QTest::keyClick(pushButton, Qt::Key_Enter);
    EXPECT_EQ(spy.count(), 1);
}

在上述代码中,我们在SetUp()函数中创建了一个自定义的按钮。然后我们测试了两个测试函数来测试大小和Enter键处理。

  1. 当您运行上述测试时,您将在输出窗口中看到测试结果。

GoogleTest 在运行时为使用TEST_F()指定的每个测试构建一个新的测试装置。它通过调用SetUp()函数立即进行初始化并运行测试。然后调用TearDown()进行清理,并移除测试装置。重要的是要注意,同一测试套件中的不同测试可以具有不同的测试装置对象。在构建下一个测试装置之前,GoogleTest 始终删除先前的测试装置。它不会为多个测试重用测试装置。一个测试对测试装置所做的任何修改对其他测试没有影响。

我们讨论了如何使用简单的测试用例创建 GoogleTest 项目以及如何设计测试夹具或测试套件。现在您可以为现有的 C++应用程序创建测试用例。GoogleTest 是一个非常成熟的测试框架。它还集成了早期在 GoogleMock 下可用的模拟机制。探索不同的功能并尝试测试用例。

还有一个现成的 GUI 工具,集成了两个测试框架,用于测试您的 Qt 应用程序。GTest Runner是一个基于 Qt 的自动化测试运行器和 GUI,具有强大的功能,适用于 Windows 和 Linux 平台。但是,该代码目前没有得到积极维护,并且尚未升级到 Qt 6。您可以在以下链接了解有关 GTest Runner 功能和用法的更多信息:

github.com/nholthaus/gtest-runner

在本节中,您学习了如何同时使用QTestGoogleTest。您已经了解了两种测试框架的特点。您可以使用 GoogleTest 框架的 GoogleMock 功能创建模拟对象。现在您可以为自定义的 C++类或自定义小部件编写自己的测试夹具。在下一节中,我们将讨论 Qt Quick 中的测试。

测试 Qt Quick 应用程序

TestCase QML 类型。以test_开头的函数被识别为需要执行的测试用例。测试工具会递归搜索tst_ *.qml文件所需的源目录。您可以将所有测试.qml文件放在一个目录下,并定义QUICK_TEST_SOURCE_DIR。如果未定义,则只有当前目录中可用的.qml文件将在测试执行期间包含在内。Qt 不保证 Qt Quick 测试模块的二进制兼容性。您必须使用模块的适当版本。

您需要将QUICK_TEST_MAIN()添加到 C++文件中,以开始执行测试用例,如下所示:

#include <QtQuickTest>
QUICK_TEST_MAIN(testqml)

您需要添加qmltest模块以启用 Qt Quick 测试。将以下代码添加到.pro文件中:

QT += qmltest
TEMPLATE = app
TARGET = tst_calculations
CONFIG += qmltestcase
SOURCES += testqml.cpp

让我们看一个基本算术计算的演示,以了解模块的工作原理。我们将进行一些计算,如加法、减法和乘法,并故意犯一些错误,以便测试用例失败:

import QtQuick
import QtTest
TestCase {
    name: "Logic Tests"
    function test_addition() {
        compare(4 + 4, 8, "Logic: 4 + 4 = 8")
    }
    function test_subtraction() {
        compare(9 - 5, 4, "Logic: 9 - 5 = 4")
    }
    function test_multiplication() {
        compare(3 * 3, 6, "Logic: 3 * 3 = 6")
    }
}

当您运行上述示例时,您将看到以下测试结果的输出:

********* Start testing of testqml *********
Config: Using QtTest library 6.1.0, Qt 6.1.0 (x86_64-little_endian-llp64 shared (dynamic) release build; by GCC 8.1.0), windows 10
PASS   : testqml::Logic Tests::initTestCase()
PASS   : testqml::Logic Tests::test_addition()
FAIL!  : testqml::Logic Tests::test_multiplication()Logic: 3 * 3 = 6
   Actual   (): 9
   Expected (): 6
C:\Qt6Book\Chapter09\QMLTestDemo\tst_calculations.qml(15) : failure location
PASS   : testqml::Logic Tests::test_subtraction()
PASS   : testqml::Logic Tests::cleanupTestCase()
Totals: 4 passed, 1 failed, 0 skipped, 0 blacklisted, 3ms
********* Finished testing of testqml *********

请注意,cleanupTestCase()在测试执行完成后立即调用。此函数可用于在一切被销毁之前进行清理。

您还可以执行数据驱动的测试,如下所示:

import QtQuick
import QtTest
TestCase {
    name: "DataDrivenTests"
    function test_table_data() {
        return [
            {tag: "10 + 20 = 30", a: 10, b: 20, result: 30         
},
            {tag: "30 + 60 = 90", a: 30, b: 60, result: 90  
},
            {tag: "50 + 50 = 100", a: 50, b: 50, result: 50 
},
        ]
    }
    function test_table(data) {
        compare(data.a + data.b, data.result)
    }
}

请注意,可以使用以_data结尾的函数名向测试提供表格数据。当您运行上述示例时,您将看到以下测试结果的输出:

********* Start testing of main *********
Config: Using QtTest library 6.1.0, Qt 6.1.0 (x86_64-little_endian-llp64 shared (dynamic) release build; by GCC 8.1.0), windows 10
PASS   : main::DataDrivenTests::initTestCase()
PASS   : main::DataDrivenTests::test_table(10 + 20 = 30)
PASS   : main::DataDrivenTests::test_table(30 + 60 = 90)
FAIL!  : main::DataDrivenTests::test_table(50 + 50 = 100) Compared values are not the same
   Actual   (): 100
   Expected (): 50
C:\Qt6Book\Chapter09\QMLDataDrivenTestDemo\tst_datadriventests.qml(14) : failure location
PASS   : main::DataDrivenTests::cleanupTestCase()
Totals: 4 passed, 1 failed, 0 skipped, 0 blacklisted, 3ms
********* Finished testing of main *********

您还可以在 QML 中运行基准测试。Qt 基准测试框架将多次运行以benchmark_开头的函数,并记录运行的平均时间值。这类似于 C++版本中的QBENCHMARK宏,用于获得QBENCHMARK_ONCE宏的效果。让我们看一个基准测试的示例:

import QtQuick
import QtTest
TestCase {
    id: testObject
    name: "BenchmarkingMyItem"
    function benchmark_once_create_component() {
        var component = Qt.createComponent("MyItem.qml")
        var testObject = component.createObject(testObject)
        testObject.destroy()
        component.destroy()
    }
}

在上面的示例中,我们创建了一个自定义的 QML 元素。我们想要测量创建该元素所需的时间。因此,我们编写了上述基准测试代码。普通的基准测试会多次运行并显示操作的持续时间。在这里,我们对创建进行了基准测试一次。这种技术在评估您的 QML 代码的性能时非常有用。

当您运行上述示例时,您将看到以下测试结果的输出:

********* Start testing of testqml *********
Config: Using QtTest library 6.1.0, Qt 6.1.0 (x86_64-little_endian-llp64 shared (dynamic) release build; by GCC 8.1.0), windows 10
PASS   : testqml::BenchmarkingMyItem::initTestCase()
PASS   : testqml::BenchmarkingMyItem::benchmark_once_create_component()
PASS   : testqml::BenchmarkingMyItem::benchmark_once_create_component()
RESULT : testqml::benchmark_once_create_component:
     0 msecs per iteration (total: 0, iterations: 1)
PASS   : testqml::BenchmarkingMyItem::cleanupTestCase()
QWARN  : testqml::UnknownTestFunc() QQmlEngine::setContextForObject(): Object already has a QQmlContext
Totals: 4 passed, 0 failed, 0 skipped, 0 blacklisted, 5ms
********* Finished testing of testqml *********

要多次运行基准测试,可以从测试用例中删除once关键字,如下所示:function benchmark_create_component() {...}。您还可以使用Qt.createQmlObject()测试动态创建的对象。

还有一个名为qmlbench的基准测试工具,用于基准测试 Qt 应用程序的整体性能。这是一个功能丰富的基准测试工具,可在qt-labs下使用。该工具还有助于测量用户界面的刷新率。您可以在以下链接中了解更多关于此工具的信息:

github.com/qt-labs/qmlbench

与 C++实现一样,您还可以在 QML 中模拟键盘事件,例如keyPress()keyRelease()keyClick()。事件将传递到当前正在聚焦的 QML 对象。让我们看看以下示例:

import QtQuick
import QtTest
MouseArea {
    width: 100; height: 100
    TestCase {
        name: "TestRightKeyPress"
        when: windowShown
        function test_key_click() {
            keyClick(Qt.Key_Right)
        }
    }
}

在前面的例子中,键盘事件是在显示 QML 查看窗口后传递的。在此之前尝试传递事件将不成功。为了跟踪窗口何时显示,使用了whenwindowShown属性。

当您运行前面的例子时,您将看到以下测试结果的输出:

********* Start testing of testqml *********
Config: Using QtTest library 6.1.0, Qt 6.1.0 (x86_64-little_endian-llp64 shared (dynamic) release build; by GCC 8.1.0), windows 10
PASS   : testqml::TestRightKeyPress::initTestCase()
QWARN  : testqml::TestRightKeyPress::test_key_click() QQmlEngine::setContextForObject(): Object already has a QQmlContext
PASS   : testqml::TestRightKeyPress::test_key_click()
PASS   : testqml::TestRightKeyPress::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 25ms
********* Finished testing of testqml *********

您可以使用SignalSpy来监视信号发射。在以下示例中,我们使用SignalSpy来检测Button上的clicked信号。当信号被发射时,clickSpy计数会增加:

import QtQuick
import QtQuick.Controls
import QtTest
Button {
    id: pushButton
    SignalSpy {
        id: clickSpy
        target: pushButton
        signalName: "clicked"
    }
    TestCase {
        name: "PushButton"
        function test_click() {
            compare(clickSpy.count, 0)
            pushButton.clicked();
            compare(clickSpy.count, 1)
        }
    }
}

当您运行前面的例子时,您将看到以下测试结果的输出:

********* Start testing of testqml *********
Config: Using QtTest library 6.1.0, Qt 6.1.0 (x86_64-little_endian-llp64 shared (dynamic) release build; by GCC 8.1.0), windows 10
PASS   : testqml::PushButton::initTestCase()
PASS   : testqml::PushButton::test_click()
PASS   : testqml::PushButton::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 5ms
********* Finished testing of testqml *********

QUICK_TEST_MAIN_WITH_SETUP宏用于在运行任何 QML 测试之前执行 C++代码。这对于在 QML 引擎上设置上下文属性非常有用。测试应用程序可以包括多个TestCase实例。运行所有测试用例后,应用程序将终止。您可以从Tests资源管理器中启用或禁用测试用例:

图 9.13 - 测试资源管理器显示具有可用测试用例的快速测试

图 9.13 - 测试资源管理器显示具有可用测试用例的快速测试

在本节中,我们讨论了测试 QML 对象的不同测试方法。在下一节中,我们将熟悉 GUI 测试,并了解一些流行的工具。

GUI 测试工具

您可以轻松地将一个或多个类评估为单元测试,但我们必须手动编写所有测试用例。GUI 测试是一项特别具有挑战性的任务。我们如何记录用户交互,例如鼠标点击,而不需要在 C++或 QML 中编写代码?这个问题困扰着开发人员。市场上有许多 GUI 测试工具可帮助我们做到这一点。其中一些价格昂贵,一些是开源的。我们将在本节中讨论一些此类工具。

但您可能不需要一个完整的 GUI 测试框架。一些问题可以通过简单的技巧解决。例如,在处理 GUI 时,您可能还需要检查不同属性,如可视元素的对齐和边界。其中最简单的方法之一是添加一个Rectangle来检查边界,如下面的代码所示:

Rectangle {
    id: container
    anchors {
        left: parent.left
        leftMargin: 100
        right: parent.right
        top: parent.top
        bottom: parent.bottom
    }
    Rectangle {
        anchors.fill : parent
        color: "transparent"
        border.color: "blue"    }
    Text {
        text: " Sample text"
        anchors.centerIn: parent
        Rectangle {
            anchors.fill : parent
            color: "transparent"
            border.color: "red"
        }
    }
}

当您运行前面的代码片段时,您将看到 GUI 中的元素边界以颜色显示,如下一张截图所示:

图 9.14 - 使用矩形输出 GUI 元素的视觉边界

图 9.14 - 使用矩形输出 GUI 元素的视觉边界

在前面的例子中,您可以看到文本元素被放置在带有蓝色边框的矩形内部。如果没有蓝色边框,您可能会想知道为什么它没有在 GUI 中央放置。您还可以看到每个元素的边界和边距。当文本元素的宽度小于字体宽度时,您将观察到裁剪。您还可以找出用户界面元素之间是否有重叠区域。通过这种方式,您可以在不使用SG_VISUALIZE环境变量的情况下找到 GUI 特定元素的问题。

让我们讨论一些 GUI 测试工具。

Linux 桌面测试项目(LDTP)

Linux 桌面测试项目LDTP)提供了一个高质量的测试自动化基础设施和尖端工具,用于测试和改进 Linux 桌面平台。LDTP 是一个在所有平台上运行的 GUI 测试框架。它使用可访问性库在应用程序的用户界面中进行探测。该框架还包括根据用户与 GUI 交互的方式记录测试用例的工具。

要单击按钮,请使用以下语法:

click('<window name>','<button name>')

要获取给定对象的当前滑块值,请使用以下代码:

getslidervalue('<window name>','<slider name>')

要为您的 GUI 应用程序使用 LDTP,必须为所有 QML 对象添加可访问名称。您可以使用对象名称作为可访问名称,如下所示:

Button {
     id: quitButton
     objectName: "quitButton"
     Accessible.name: objectName 
}

在上述代码中,我们为 QML 控件添加了可访问名称,以便 LDTP 工具可以找到此按钮。LDTP 需要用户界面的窗口名称来定位子控件。假设窗口名称是Example,那么要生成单击事件,请在 LDTP 脚本上使用以下命令:

>click('Example','quitButton')

上述 LDTP 命令定位quitButton并生成按钮单击事件。

您可以在以下链接了解其特点和用途:

ldtp.freedesktop.org/user-doc/

GammaRay

KDAB 开发了一个名为QObject内省机制的软件内省工具。这个工具可以在本地机器和远程嵌入式目标上使用。它扩展了指令级调试器的功能,同时遵循底层框架的标准。这对于使用场景图、模型/视图、状态机等框架的复杂项目特别有用。有几种工具可用于检查对象及其属性。然而,它与 Qt 复杂框架的深度关联使其脱颖而出。

您可以从以下链接下载 GammaRay:

github.com/KDAB/GammaRay/wiki/Getting-GammaRay

您可以在以下链接了解其特点和用途:

www.kdab.com/development-resources/qt-tools/gammaray/

Squish

Squish是一个用于桌面、移动、嵌入式和 Web 应用程序的跨平台 GUI 测试自动化工具。您可以使用 Squish 自动化 GUI 测试,用于使用 Qt Widgets 或 Qt Quick 编写的跨平台应用程序。Squish 被全球数千家组织用于通过功能回归测试和系统测试测试其 GUI。

您可以在以下链接了解有关该工具的更多信息:

www.froglogic.com/squish/

在本节中,我们讨论了各种 GUI 测试工具。探索它们,并尝试在您的项目中使用它们。让我们总结一下本章的学习成果。

总结

在本章中,我们学习了调试是什么,以及如何使用不同的调试技术来识别 Qt 应用程序中的技术问题。除此之外,我们还看了 Qt 在各种操作系统上支持的各种调试器。最后,我们学习了如何使用单元测试来简化一些调试措施。我们讨论了单元测试,并学习了如何使用 Qt 测试框架。您看到了如何调试 Qt Quick 应用程序。我们还讨论了 Qt 支持的各种其他测试框架和工具。现在,您可以为自定义类编写单元测试。如果有人意外修改了某些特定逻辑,单元测试将失败并自动发出警报。

第十章部署 Qt 应用程序,您将学习如何在各种平台上部署 Qt 应用程序。这将帮助您为目标平台创建可安装的软件包。

第十章:部署 Qt 应用程序

在之前的章节中,您学习了如何使用 Qt 6 开发和测试应用程序。您的应用程序已经准备就绪并在您的桌面上运行,但它并不是独立的。您必须遵循一定的步骤来发布您的应用程序,以便最终用户可以使用。这个过程被称为部署。一般来说,最终用户希望有一个可以双击打开以运行您的软件的单个文件。软件部署包括使软件可用于其预期用户的不同步骤和活动,这些用户可能没有任何技术知识。

在本章中,您将学习如何在不同平台上部署 Qt 项目。在整个过程中,您将了解可用的部署工具以及创建部署软件包时需要考虑的重要要点。

在本章中,我们将涵盖以下主题:

  • 部署策略

  • 静态与动态构建

  • 在桌面平台上部署

  • Qt 安装程序框架

  • 其他安装工具

  • 在 Android 上部署

通过本章结束时,您将能够创建一个可部署的软件包并与他人共享。

技术要求

本章的技术要求包括 Qt 6.0.0 和 Qt Creator 4.14.0 的最低版本,安装在最新的桌面平台上,如 Windows 10 或 Ubuntu 20.04 或 macOS 10.14。

本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp/tree/master/Chapter10/HelloWorld

重要说明

本章使用的屏幕截图是在 Windows 平台上进行的。您将在您的设备上看到基于底层平台的类似屏幕。

理解部署的必要性

使软件在目标设备上运行的过程,无论是测试服务器、生产环境、用户的桌面还是移动设备,都被称为软件部署。通常,最终用户希望有一个可以打开以访问您的应用程序的单个文件。用户不希望经历多个过程来获取各种外来文件。通常,用户寻找可以双击或轻点启动的软件。用户不希望经历一系列步骤来获取一些未知文件。在本章中,我们将讨论在部署 Qt 应用程序时需要考虑的步骤和事项。我们将讨论在 Windows、Mac、Linux 和 Android 平台上部署应用程序。

到目前为止,我们一直在运行我们迄今为止构建的应用程序的调试版本。您应该生成发布二进制文件以生成部署软件包。这两种选择之间的区别在于调试版本包含有关您编写的代码的信息,如果遇到问题,这将使调试变得更加容易。但是,您不希望向用户发送多个文件,因为这对他们来说是没有用的。用户只想运行您的应用程序。这就是为什么您必须向他们提供您应用程序的发布版本。因此,为了发布应用程序,我们将以发布模式创建它,这将为我们提供一个发布二进制文件,我们可以交付给我们的用户。一旦您获得了二进制文件,您将需要根据您想要部署应用程序的平台创建单独的软件包。如果您想在 Windows 上部署,您将采取特定的方法,同样适用于 Linux、macOS 或 Android。

标准的 Qt 部署包包括一个单独的可执行文件,但需要其他文件的存在才能运行。除了可执行文件,还需要以下文件:

  • 动态库

  • 第三方库

  • 附加模块

  • 可分发文件

  • Qt 插件

  • 翻译文件

  • 帮助文件

  • 许可证

当我们在 Qt Creator 中启动一个 Qt 项目时,默认情况下设置为使用动态链接。因此,我们的应用程序将需要 Qt 动态链接库。我们还需要您喜欢的编译器的 C++运行时(MinGW/MSVC/Clang/GCC)和标准库实现。这些通常作为 Windows 上的.dll文件、Linux 上的.so文件以及 macOS 上的.so.dylib文件提供。如果您的项目是一个庞大复杂的项目,您可能有多个库。您的应用程序包还可能需要第三方库,如 opengl、libstdc++、libwinpthread 和 openssl。

如果您的应用程序基于 Qt Quick,那么您还需要标准模块,如 QtQuick、QtQml、QtStateMachine、QtCharts 和 Qt3D。它们以动态库的形式提供,还有一些额外的文件提供 QML 模块元数据,或者纯 QML 文件。不幸的是,实现 Qt 的 C++和 QML API 的动态库是不足以让我们的可执行文件运行的。Qt 还使用插件来启用扩展,以及用于相当标准的 GUI 功能,如图像文件加载和显示的插件。同样,一些插件封装了 Qt 运行的平台。

如果您正在使用 Qt 的翻译支持,那么您还需要部署翻译文件。我们将在第十一章“国际化”中更多地讨论翻译。如果您正在使用 Qt 帮助框架甚至简单的 PDF 手册,您可能还需要部署文档文件。您还可能需要部署一些图标、脚本或许可协议供您的应用程序使用。您还必须确保 Qt 库可以自行定位平台插件、文档和翻译,以及预期的可执行文件。

在静态和动态库之间进行选择

您可以使用静态链接或动态链接构建您的 Qt 应用程序。在构建应用程序时,链接器使用这两种方法之一将所有使用的库函数的副本复制到可执行文件中。我们假设您已经了解这两种方法。在本节中,我们将讨论何时使用静态链接和何时使用动态链接来构建您的 Qt 应用程序。

在 Linux 中是.a文件扩展名,在 Windows 中是.lib文件扩展名。

在 Linux 中是.so文件扩展名,在 Windows 中是.dll文件扩展名。

静态构建由单个可执行文件组成。但在动态构建中,您必须注意动态库。静态构建更简单,因为它们可能已经在可执行文件中包含了 Qt 插件和 QML 导入。静态构建还便于指定-static配置选项。这种 Qt 应用程序部署模式仅适用于商业许可。如果您是开源开发人员,应避免静态链接应用程序。由于本书中使用的是开源 Qt 版本,我们不会详细介绍静态构建。相反,我们将坚持使用常规的动态构建和部署。

您可以在以下链接了解有关使用上述方法部署 Qt 应用程序的更多信息:

doc.qt.io/qt-6/deployment.html

在接下来的章节中,我们将专注于主要的桌面和移动平台。我们不会讨论嵌入式平台,因为这超出了本书的范围。

在桌面平台上部署

您已经看到,在部署 Qt 应用程序时有很多要考虑的事情。幸运的是,Qt 提供了一个工具,可以通过扫描生成的应用程序二进制文件,识别所有依赖项,并将它们复制到部署目录中来协助我们进行这个过程。我们将在各种平台上部署我们的应用程序以实现不同的目标,但概念将保持不变。一旦我们构建好我们的二进制文件,我们需要做的第一件事就是添加依赖项,以便用户可以无困难地执行应用程序。

我们可以以两种方式加载依赖项。我们可以手动操作,也可以使用 Qt 框架或第三方提供的某些工具。在 Windows 上,我们可以使用windeployqt来加载我们的依赖项。在 macOS 上,我们可以使用macdeployqt来为我们的二进制文件加载依赖项。还有另一个工具叫做linuxdeployqt,您可以使用它来为您的二进制文件添加依赖项。linuxdeployqt非常适合我们的需求,在本章中我们将讨论它。然而,这个 Linux 部署实用工具不是官方的,也不受 Qt 支持。一旦生成了您的二进制文件,您需要找到并添加依赖项。您可以手动操作,也可以根据您所在的位置使用这些工具之一来部署您的应用程序。

在本章中,我们将使用一个简单的HelloWorld示例来讨论如何在不同平台上部署应用程序。我们将找到依赖项并创建一个独立的包。让我们从 Windows 部署开始。

在 Windows 上部署

大多数为 Windows 构建的桌面应用程序通常以两种方式交付。首先,应用程序作为一个独立的应用程序交付,无需安装。在这种方法中,应用程序通常作为一个带有所有依赖库的可执行文件(.exe)出现在同一目录中。这种类型的应用程序称为.exe.msi格式。您将学习如何创建一个可安装的.exe文件。在本节中,我们将讨论如何使用这两种方法创建独立部署包。

按照以下步骤创建一个便携式应用程序:

  1. 首先创建一个简单的 Qt 应用程序。您可以选择 Qt Widget 或 Qt Quick-based 应用程序。这里我们将讨论基于 Qt Widget 的应用程序。这两种类型的应用程序的过程是相同的。

  2. 创建示例应用程序后,您可以选择通过在main.cpp文件中添加几行代码来添加应用程序名称、版本、组织名称和域,如下所示:

QApplication app (argc, argv);
app.setOrganizationName("Awesome Company");
app.setOrganizationDomain("www.abc.com");
app.setApplicationName("Deployment Demo");
app.setApplicationVersion("1.0.0");
  1. 创建应用程序后,以发布模式构建它。您可以在构建设置中更改构建模式。发布模式会创建一个较小的二进制文件,因为它会消除调试符号。您可以通过单击并选择发布选项来快速从套件选择器部分更改构建模式,如图 10.1所示:图 10.1 - Qt Creator 中的发布选项

图 10.1 - Qt Creator 中的发布选项

  1. 您可以看到二进制文件是在发布目录中创建的。在这个例子中,我们使用了影子构建。您还可以从构建设置屏幕下的常规部分更改发布目录:图 10.2 - 具有发布二进制文件的目录

图 10.2 - 具有发布二进制文件的目录

  1. 现在,创建一个部署目录,并从发布目录中复制可执行文件。

  2. 现在,双击可执行文件。您会注意到应用程序无法启动,并出现了几个错误对话框。错误对话框会提到缺少哪个库。如果您没有看到这些错误,那么您可能已经在系统环境中添加了库路径。您可以在未安装 Qt 库的干净系统上尝试:图 10.3 - 显示 Qt 库依赖的错误

图 10.3 - 显示 Qt 库依赖的错误

  1. 下一步是找到在 IDE 之外独立运行应用程序所需的缺失的 Qt 库。

  2. 由于我们在这里使用的是 Qt 的开源版本和动态链接方法,您会注意到缺失的库将具有.dll扩展名。在这里,我们看到缺失的库是Qt6Core.dll

  3. 错误的数量将取决于程序中使用的模块数量。您可以从QTDIR/6.x.x/<CompilerName>/bin目录中找到 Qt 依赖库。在这里,QTDIR是 Qt 6 安装的位置。在我们的示例中,我们使用了Qt 6.1.0作为版本,mingw81_64作为编译器,因此路径是D:/Qt/6.1.0/mingw81_64/bin。这个路径可能会根据您的 Qt 安装路径、Qt 版本和选择的编译器而有所不同。以下截图显示了bin目录下动态库的存在:图 10.4 – bin 目录中所需的 Qt 库

图 10.4 – bin 目录中所需的 Qt 库

  1. 图 10.4所示,将缺失的.dll文件复制到最近创建的部署目录中。

  2. 重复这个过程,直到您将错误消息中提到的所有丢失的库都复制到部署目录中。您可能还需要部署特定于编译器的库以及您的应用程序。您还可以使用Dependency Walkerdepends.exe)工具找到依赖库。这个工具是一个专门针对 Windows 的免费工具。它提供了一个依赖库列表。然而,在最近的版本中,这个工具并不是很有用,经常无法提供所需的信息。您还可以尝试一些其他工具,比如 PeStudio、MiTeC EXE Explorer 和 CFF Explorer。请注意,我没有探索过这些工具。

  3. 一旦您复制了所有丢失的库,请尝试再次运行应用程序。这一次,您会注意到一个新的错误弹出。这次,消息与平台插件有关:图 10.5 – 错误对话框指示缺少 Qt 平台插件

图 10.5 – 错误对话框指示缺少 Qt 平台插件

  1. 在部署目录中创建一个名为platforms的目录:图 10.6 – 显示 Qt Windows 平台插件的目录

图 10.6 – 显示 Qt Windows 平台插件的目录

  1. 然后,将qwindows.dll文件从C:\Qt\6.x.x\<compiler_name>\plugins\platforms复制到新的platforms子目录中。图 10.7说明了部署目录中文件的组织结构:图 10.7 – 在发布目录中复制平台插件

图 10.7 – 在发布目录中复制平台插件

  1. 现在,双击HelloWorld.exe文件。您会注意到HelloWorld! GUI 立即出现。现在,Qt Widgets 应用程序可以在没有安装 Qt 6 的 Windows 平台上运行:图 10.8 – 运行已解决依赖关系的独立应用程序

图 10.8 – 运行已解决依赖关系的独立应用程序

  1. 下一步,也是最后一步,是将文件夹压缩并与您的朋友分享。

恭喜!您已成功部署了您的第一个独立应用程序。然而,这种方法对于一个有许多依赖文件的大型项目来说效果不佳。Qt 提供了几个方便的工具来处理这些挑战,并轻松创建安装包。在下一节中,我们将讨论 Windows 部署工具以及它如何帮助我们处理这些挑战。

Windows 部署工具

Windows 部署工具随 Qt 6.x 安装包一起提供。您可以在<QTDIR>/bin/下找到它,命名为windeployqt.exe。您可以从 Qt 命令提示符中运行这个工具,并将可执行文件作为参数传递,或者使用目录作为参数。如果您正在构建一个 Qt Quick 应用程序,您还需要额外添加.qml文件的目录路径。

让我们看看windeployqt中一些重要的命令行选项。在下面的列表中探索一些有用的选项:

  • -?-h--help显示命令行选项的帮助信息。

  • --help-all显示包括 Qt 特定选项在内的帮助信息。

  • --libdir <path>将依赖库复制到路径。

  • --plugindir <path>将依赖插件复制到路径。

  • --no-patchqt指示不要修补 Qt6Core 库。

  • --no-plugins指示跳过插件部署。

  • --no-libraries指示跳过库部署。

  • --qmldir <directory>从源目录扫描 QML 导入。

  • --qmlimport <directory>将给定路径添加到 QML 模块搜索位置。

  • --no-quick-import指示跳过 Qt Quick 导入的部署。

  • --no-system-d3d-compiler指示跳过 D3D 编译器的部署。

  • --compiler-runtime在桌面上部署编译器运行时。

  • --no-compiler-runtime防止在桌面上部署编译器运行时。

  • --no-opengl-sw防止部署软件光栅化器库。

您可以在bin文件夹中找到windeployqt工具,如下面的屏幕截图所示:

图 10.9 - bin 目录中的 windeployqt 工具

图 10.9 - bin 目录中的 windeployqt 工具

使用windeployqt的最简单方法是将其路径添加到Path变量中。要将其添加到Path,在 Windows 机器上打开系统属性,然后单击高级系统设置。您会发现系统属性窗口出现了。在系统属性窗口的底部,您会看到环境变量…按钮。单击它,然后选择Path变量,如下面的屏幕截图所示。然后,单击编辑…按钮。添加 Qt bin 目录的路径,然后单击确定按钮:

图 10.10 - 将 bin 目录添加到系统环境路径

图 10.10 - 将 bin 目录添加到系统环境路径

关闭系统属性屏幕并启动 Qt 命令提示符。然后,您可以使用以下语法为基于 Qt Widget 的应用程序创建部署包:

>windeployqt <your-executable-path>

如果您正在使用 Qt Quick,请按照下一个语法:

>windeployqt --qmldir <qmlfiles-path> <your-executable-path>

之后,该工具将复制识别出的依赖项到部署目录,确保我们将所有所需的组件放在一个位置。它还将构建插件和其他 Qt 资源的子目录结构,这是您所期望的。如果 ICU 和其他文件不在 bin 目录中,则必须在运行该工具之前将它们添加到Path变量中。

让我们从相同的HelloWorld示例开始。要使用windeployqt创建示例的部署,请执行以下步骤:

  1. 创建一个部署目录,并将HelloWorld.exe文件复制到部署目录。

  2. 现在您可以调用部署工具,如下所示:

D:\Chapter10\HelloWorld\deployment>windeployqt HelloWorld.exe
  1. 输入命令后,工具将开始收集有关依赖项的信息:
>D:\Chapter10\HelloWorld\deployment\HelloWorld.exe 64 bit, release executable
Adding Qt6Svg for qsvgicon.dll
Direct dependencies: Qt6Core Qt6Widgets
All dependencies   : Qt6Core Qt6Gui Qt6Widgets
To be deployed     : Qt6Core Qt6Gui Qt6Svg Qt6Widgets
  1. 您会注意到该工具不仅列出了依赖项,还将所需的文件复制到目标目录。

  2. 打开部署目录,您会发现已添加了多个文件和目录:图 10.11 - windeployqt 复制了所有必需的文件到部署目录

图 10.11 - windeployqt 复制了所有必需的文件到部署目录

  1. 在前一节中,我们不得不自己识别和复制所有依赖项,但现在这项任务已委托给了windeployqt工具。

  2. 如果您正在使用Qt Quick 应用程序,请运行以下命令:

>D:\Chapter10\qmldeployment>windeployqt.exe --qmldir D:\Chapter10\HelloWorld D:\Chapter10\qmldeployment
  1. 您会看到该工具已经收集了依赖项,并将所需的文件复制到部署目录:
D:\Chapter10\qmldeployment\HelloWorld.exe 64 bit, release executable [QML]
Scanning D:\Chapter10\HelloWorld:
QML imports:
  'QtQuick' D:\Qt\6.1.0\mingw81_64\qml\QtQuick
  'QtQuick.Window' D:\Qt\6.1.0\mingw81_64\qml\QtQuick\Window
  'QtQml' D:\Qt\6.1.0\mingw81_64\qml\QtQml
  'QtQml.Models' D:\Qt\6.1.0\mingw81_64\qml\QtQml\Models
  'QtQml.WorkerScript' D:\Qt\6.1.0\mingw81_64\qml\QtQml\WorkerScript
Adding Qt6Svg for qsvgicon.dll
Direct dependencies: Qt6Core Qt6Gui Qt6Qml
All dependencies   : Qt6Core Qt6Gui Qt6Network Qt6OpenGL Qt6Qml Qt6Quick Qt6QuickParticles Qt6Sql
To be deployed     : Qt6Core Qt6Gui Qt6Network Qt6OpenGL Qt6Qml Qt6Quick Qt6QuickParticles Qt6Sql Qt6Svg
  1. 现在,您可以双击启动独立应用程序。

  2. 下一步是压缩文件夹并与朋友分享。

Windows 部署工具的命令行选项可用于微调识别和复制过程。基本说明可以在以下链接中找到:

doc.qt.io/qt-6/windows-deployment.html

wiki.qt.io/Deploy_an_Application_on_Windows.

干杯!您已经学会了使用 Windows 部署工具部署 Qt 应用程序。但是,还有很多工作要做。Qt 安装程序框架提供了几个方便的工具,用于处理这些挑战并轻松创建可安装的软件包。在下一节中,我们将讨论 Linux 部署工具以及如何使用它创建独立应用程序。

在 Linux 上部署

在 Linux 发行版中,我们有多种选项来部署我们的应用程序。您可以使用安装程序,但也可以选择应用程序包的选项。在 Debian、Ubuntu 或 Fedora 上有一种称为 apt 的技术,您的应用程序可以通过这种方式使用。但是,您也可以选择一个更简单的方法,比如 app image 选项,它将为您提供一个文件。您可以将该文件提供给用户,他们只需双击即可运行应用程序。

Qt 文档提供了在 Linux 上部署的特定说明。您可以在以下链接中查看:

doc.qt.io/qt-6/linux-deployment.html.

Qt 并未为 Linux 发行版提供类似于 windeployqt 的现成工具。这可能是由于 Linux 发行版的数量众多。但是,有一个名为 linuxdeployqt 的非官方开源 Linux 部署工具。它接受应用程序作为输入,并通过将项目资源复制到包中将其转换为自包含软件包。用户可以将生成的包作为 AppDirAppImage 获取,或者可以将其包含在跨发行版软件包中。使用诸如 CMake、qmake 和 make 等系统,它可以作为构建过程的一部分来部署用 C、C++ 和其他编译语言编写的应用程序。它可以打包运行基于 Qt 的应用程序所需的特定库和组件。

您可以从以下链接下载 linuxdeployqt

github.com/probonopd/linuxdeployqt/releases.

下载后,您将得到 linuxdeployqt-x86_64.AppImage,在运行之前执行 chmod a+x

您可以在 github.com/probonopd/linuxdeployqt 上阅读完整的文档并找到源代码。

如果您想轻松地获得单个应用程序包,那么请使用 -appimage 标志运行 linuxdeployqt

还有一些其他部署工具,如 SnapFlatpak,可以打包应用程序及其依赖项,使其在多个 Linux 发行版上运行而无需进行任何修改。

您可以在以下链接中了解如何创建一个 snap:snapcraft.io/docs/creating-a-snap

您可以通过访问以下链接了解更多关于 Flatpak 的信息:docs.flatpak.org/en/latest/qt.html

在下一节中,我们将讨论 macOS 部署工具以及如何使用它为您的 Mac 用户创建独立应用程序。

在 macOS 上部署

您可以按照前几节讨论的类似过程来为 macOS 生成安装程序文件。我们将讨论您可以遵循的步骤来生成应用程序包。您可以在 macOS 上测试该软件包并将其发送给您的 Mac 用户。该过程与在 Linux 上基本相同。毕竟,macOS 是基于 Unix 的。因此,您可以在 macOS 上创建我们称之为 bundle 的安装程序。

您可以在QTDIR/bin/macdeployqt中找到 macOS 部署工具。它旨在自动化创建包含 Qt 库作为私有框架的可部署应用程序包的过程。Mac 部署工具还部署 Qt 插件,除非您指定-no-plugins选项。默认情况下,Qt 插件(如平台、图像格式、打印支持和辅助功能)始终被部署。只有在应用程序使用时,才会部署 SQL 驱动程序和 SVG 插件。设计师插件不会被部署。如果要在应用程序包中包含第三方库,必须在构建后手动将库复制到包中。

几年前,苹果推出了一个名为.dmg的新文件系统。为了与 Qt 当前支持的所有 macOS 版本兼容,macdeployqt默认使用较旧的 HFS+文件系统。要选择不同的文件系统,请使用-fs选项。

您可以在以下链接找到详细的说明:doc.qt.io/qt-6/macos-deployment.html

在下一节中,我们将讨论 Qt Installer Framework 以及如何使用它为用户创建完整的安装包。

使用 Qt Installer Framework

Qt Installer FrameworkQIFW)是一个跨平台工具和实用程序集合,用于为支持的桌面 Qt 平台创建安装程序,包括 Linux、Windows 和 macOS。它允许您在所有支持的桌面 Qt 平台上分发应用程序,而无需重写源代码。Qt Installer Framework 工具创建包含一系列页面的安装程序,帮助用户完成安装、更新和卸载过程。您提供可安装的内容以及有关其的信息,如产品名称、安装程序和法律协议。

您可以通过向预定义页面添加小部件或添加整个页面来个性化安装程序,以提供更多选项给消费者。您可以通过编写脚本向安装程序添加操作。根据您的用例,您可以为最终用户提供离线或在线安装,或两者兼有。它在 Windows、Linux 和 Mac 上都能很好地运行。我们将使用它为我们的应用程序创建安装程序,并且将详细讨论在 Windows 上的工作原理。Linux 和 macOS 的过程与 Windows 类似。因此,我们只会讨论 Windows 平台。您可以在您喜欢的平台上尝试类似的步骤。

您可以在以下链接了解有关预定义页面的更多信息:doc.qt.io/qtinstallerframework/ifw-use-cases-install.html

在开始之前,请确认 Qt Installer Framework 已安装在您的计算机上。如果不存在,请启动Qt 维护工具,并从选择组件页面安装,如下截图所示:

图 10.12 - Qt 维护工具中的 Qt Installer Framework 下载选项

图 10.12 - Qt 维护工具中的 Qt Installer Framework 下载选项

安装应用程序成功后,您将在QTDIR\Tools\QtInstallerFramework\下找到安装文件:

图 10.13 - Windows 上 Qt Installer Framework 目录中的工具

图 10.13 - Windows 上 Qt Installer Framework 目录中的工具

您可以看到在 Qt Installer Framework 目录中创建了五个可执行文件:

    • archivegen工具用于将文件和目录打包成 7zip 存档。
    • binarycreator工具用于创建在线和离线安装程序。
    • devtool用于使用新的安装程序基础更新现有安装程序。
    • installerbase工具是打包所有数据和元信息的核心安装程序。
    • repogen工具用于生成在线存储库。

在本节中,我们将使用binarycreator工具为我们的 Qt 应用程序创建安装程序。此工具可用于生成离线和在线安装程序。某些选项具有默认值,因此您可以将它们省略。

要在 Windows 机器上创建离线安装程序,您可以在 Qt 命令提示符中输入以下命令:

><location-of-ifw>\binarycreator.exe -t <location-of-ifw>\installerbase.exe -p <package_directory> -c <config_directory>\<config_file> <installer_name>

类似地,在 Linux 或 Mac 机器上创建离线安装程序,您可以在 Qt 命令提示符中输入以下命令:

><location-of-ifw>/binarycreator -t <location-of-ifw>/installerbase -p <package_directory> -c <config_directory>/<config_file> <installer_name>

例如,要创建离线安装程序,请执行以下命令:

>binarycreator.exe --offline-only -c installer-config\config.xml -p packages-directory -t installerbase.exe SDKInstaller.exe

上述说明将创建一个包含所有依赖项的 SDK 的离线安装程序。

要创建仅在线安装程序,可以使用--online-only,它定义了从 Web 服务器上的在线存储库安装的所有软件包。例如,要创建在线安装程序,请执行以下命令:

>binarycreator.exe -c installer-config\config.xml -p packages-directory -e org.qt-project.sdk.qt,org.qt-project.qtcreator -t installerbase.exe SDKInstaller.exe

您可以在以下页面了解有关binarycreator和不同选项的更多信息:doc.qt.io/qtinstallerframework/ifw-tools.html#binarycreator

使用binarycreator的最简单方法是将其路径添加到QIFW bin 目录中,然后单击OK按钮。以下屏幕截图说明了如何执行此操作:

图 10.14–将 QIFW bin 目录添加到系统环境路径

图 10.14–将 QIFW bin 目录添加到系统环境路径

关闭系统属性屏幕并启动 Qt 命令提示符。

让我们继续部署我们的示例* HelloWorld *应用程序。我们将为我们的用户创建一个可安装的软件包,这样他们就可以双击并安装它:

  1. 创建一个与安装程序设计相匹配并允许将来扩展的目录结构。目录中必须存在configpackages子目录。QIFW 部署的目录放在哪里并不重要;重要的是它具有这种结构。

  2. 创建一个包含构建安装程序二进制文件和在线存储库的说明的配置文件。在 config 目录中创建一个名为config.xml的文件,并添加以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<Installer>
    <Name>Deployment Example </Name>
    <Version>1.0.0</Version>
    <Title>Deployment Example</Title>
    <Publisher>Packt</Publisher>
    <StartMenuDir>Qt6 HelloWorld</StartMenuDir>
    <TargetDir>@HomeDir@/HelloWorld</TargetDir>
</Installer>

Title标签提供了安装程序在标题栏中显示的名称。应用程序名称使用Name标签添加到页面名称和介绍性文本中。软件版本号由Version标签指定。Publisher标签定义了软件的发布者。产品在 Windows 开始菜单中的默认程序组名称由StartMenuDir标签指定。向用户呈现的默认目标目录是当前用户主目录中的InstallationDirectory,由TargetDir标签指定。您可以在文档中了解更多标签。

您还可以在config.xml中指定应用程序包图标。在 Windows 上,它使用.ico进行扩展,并可用作.exe文件的应用程序图标。在 Linux 上,您可以使用.png扩展名指定图标,并将其用作窗口图标。在 macOS 上,您可以使用.icns指定图标,并将其用作新生成的包的图标。

  1. 现在在packages目录内创建一个子目录。这将是您的component名称。您可以使用您的组织名称和应用程序名称或您的组织域作为component,例如CompanyName.ApplicationName。目录名称充当类似域的标识符,用于标识所有组件。

  2. 创建一个包含有关可能安装的组件的详细信息的软件包信息文件。在这个简单的例子中,安装程序只需处理一个组件。让我们在packages\{component}\meta目录中创建一个名为package.xml的软件包信息文件。

  3. 在 meta 目录中添加文件,其中包含有关组件的信息,以提供给安装程序。

让我们创建package.xml并将以下内容添加到其中:

<?xml version="1.0"?>
<Package>
    <DisplayName>Hello World</DisplayName>
    <Description>This is a simple deployment example.
    </Description>
    <Version>1.0.1</Version>
    <ReleaseDate>2021-05-19</ReleaseDate>
</Package>

以下元素的信息将在安装过程中的组件选择页面上显示:

  • DisplayName标签指定了组件在组件列表中的名称。

  • Description标签指定了在选择组件时显示的文本。

  • Version标签使您能够在更新可用时向用户推广更新。

  • Default标签指定组件是否默认选择。值true将组件设置为已选择。

  • 您可以向安装程序添加许可信息。指定了在许可检查页面上显示的许可协议文本的文件名由License标签指定。

  1. 您可以将所需内容复制到package目录下的data子目录中。将之前使用windeployqt创建的所有文件和目录复制到data子目录中。以下屏幕截图显示了复制到data子目录中的内容:图 10.15 – windeployqt 生成的内容复制到 data 子目录中

图 10.15 – windeployqt 生成的内容复制到 data 子目录中

  1. 下一步是使用binarycreator工具创建安装程序。在 Qt 命令提示符中输入以下指令:
>binarycreator.exe -c config/config.xml -p packages HelloWorld.exe
  1. 您可以看到在我们的部署目录中生成了一个安装程序文件:图 10.16 – 部署目录中创建的安装程序包
$./binarycreator -c config/config.xml -p packages HelloWorld
  1. 我们得到了期望的结果。现在,让我们运行安装程序,验证部署包是否已正确创建。

  2. 双击安装程序文件开始安装。您将看到一个漂亮的安装向导出现在屏幕上:图 10.17 – 运行部署示例的安装向导

图 10.17 – 安装向导运行部署示例

  1. 按照页面提示完成安装。退出安装向导。

  2. 现在,从 Windows 的开始菜单启动应用程序。您应该很快就会看到HelloWorld用户界面出现。

  3. 您还可以在添加/删除程序中找到已安装的应用程序:图 10.18 – Windows 程序列表中的部署示例条目

图 10.18 – Windows 程序列表中的部署示例条目

  1. 您可以使用与安装包一起安装的维护工具来更新、卸载和添加应用程序组件。您可以在安装目录中找到该工具,如下面的屏幕截图所示:

图 10.19 – 安装目录中的维护工具

图 10.19 – 安装目录中的维护工具

恭喜!您已为示例应用程序创建了一个安装程序包。现在,您可以将开发的 Qt 应用程序发送给用户和朋友。

您还可以通过自定义设置向导页面进行进一步定制。您可以在以下链接找到可与 QIFW 一起使用的安装程序的完整模板列表:

doc.qt.io/qtinstallerframework/ifw-customizing-installers.html

doc.qt.io/qtinstallerframework/qtifwexamples.html

您可以在这里探索框架的更多功能:doc.qt.io/qtinstallerframework/ifw-overview.html

在本节中,我们创建了一个可安装的软件包,以供最终用户使用。在下一节中,我们将学习在 Android 平台上部署。

在 Android 上部署

除了桌面平台如 Windows、Linux 和 macOS 之外,移动平台同样重要,因为用户数量庞大。许多开发人员希望将他们的应用程序提供给移动平台。让我们看看如何做到这一点。我们将简要讨论 Android 上的部署注意事项。

第五章跨平台开发中,您已经学会了如何创建一个.apk文件,这是 Android 平台的部署包。因此,我们不会再讨论这些步骤。在本节中,我们将讨论上传到 Play 商店之前的一些必要更改:

  1. 使用 kit 选择屏幕从 Android Kit 创建一个简单的HelloWorld应用程序。

  2. 将构建模式更改为发布模式。

  3. 打开项目的构建设置。您会在屏幕上看到几个选项:图 10.20 - 屏幕截图显示构建设置中的 Android 清单选项

图 10.20 - 屏幕截图显示构建设置中的 Android 清单选项

  1. 您可以在应用程序签名部分下看到密钥库字段。单击浏览...按钮选择现有的密钥库文件,或使用创建...按钮创建新的密钥库文件。它可以保护密钥材料免受未经授权的使用。这是一个可选步骤,只有在签署部署二进制文件时才需要。

  2. 当您单击创建...按钮时,您将看到一个对话框,其中有几个字段。填写相关字段,然后单击保存按钮。图 10.21显示了密钥库创建对话框:图 10.21 - 屏幕截图显示密钥库创建屏幕

图 10.21 - 屏幕截图显示密钥库创建屏幕

  1. 将密钥库文件保存在任何地方,确保文件名以.keystore结尾。

下一步是对应用程序包进行签名。这也是一个可选步骤,只有在发布到 Play 商店时才需要。您可以在官方文档中了解有关应用程序签名的更多信息,网址为developer.android.com/studio/publish/app-signing

  1. 您可以选择目标 Android 版本,并通过在 Qt Creator 中创建AndroidManifect.xml文件来配置您的 Android 应用程序。要做到这一点,单击构建 Android APK屏幕上的创建 模板按钮。您将看到一个对话框出现,如下图所示:图 10.22 - 屏幕截图显示清单文件创建向导

图 10.22 - 屏幕截图显示清单文件创建向导

  1. 打开清单文件。您将看到 Android 应用程序的几个选项。

  2. 您可以设置包名称、版本代码、SDK 版本、应用程序图标、权限等。如果添加一个独特的图标,那么您的应用程序在设备上不会显示默认的 Android 图标。这将使您的应用程序在屏幕上独特且易于发现。

  3. 让我们将HelloWorld作为应用程序名称,并将 Qt 图标作为我们的应用程序图标,如下图所示:图 10.23 - Android 清单文件显示不同的可用选项

图 10.23 - Android 清单文件显示不同的可用选项

  1. 如果使用任何第三方库,如 OpenSSL,则添加额外的库。

  2. 单击 Qt Creator 左下角的运行按钮,在 Android 设备上构建和运行应用程序。您还可以单击运行按钮下方的部署按钮来创建部署二进制文件。

  3. 您会看到屏幕上出现一个新的对话框。此对话框允许您选择物理 Android 硬件或软件仿真虚拟设备。

  4. 连接您的 Android 设备并单击刷新设备列表按钮。不要忘记从 Android 设备设置中启用开发者选项。当您的 Android 设备提示时,请允许USB 调试图 10.24 – Android 设备选择对话框

图 10.24 – Android 设备选择对话框

  1. 如果您想使用虚拟设备,请单击创建 Android 虚拟设备按钮。您将看到以下屏幕出现:

图 10.25 – Android 虚拟设备创建屏幕

  1. 如果屏幕警告您无法创建新 AVD,则请从 Android SDK 管理器中更新 Android 平台工具和系统映像。您可以按照以下命令行更新这些内容:
>sdkmanager "platform-tools" "platforms;android-30"
>sdkmanager "system-images;android-30;google_apis;x86"
>sdkmanager --licenses
  1. 然后,运行以下命令来运行avdmanager
>avdmanager create avd -n Android30 -k "system-images;android-30;google_apis;x86"
  1. 最后一步是单击build文件夹中的.apk扩展名:图 10.26 – 生成在 build 目录中的 Android 安装程序文件

图 10.26 – 生成在 build 目录中的 Android 安装程序文件

  1. 在内部,Qt 运行androiddeployqt实用程序。有时,该工具可能无法创建包,并显示以下错误:
error: aidl.exe …Failed to GetFullPathName

在这种情况下,请将您的应用程序放在较短的文件路径中,并确保您的文件路径中没有目录包含空格。然后,构建应用程序。

  1. 您可以将.apk文件分发给您的朋友或用户。用户必须在其 Android 手机或平板电脑上接受一个选项,即从未知来源安装。为了避免这种情况,您应该在 Play 商店上发布您的应用程序。

  2. 但是,如果您想在 Google Play 商店上分发您的应用程序,那么您必须注册为 Google Play 开发者并对软件包进行签名。Google 会收取一笔小额费用,以允许开发者发布他们的应用程序。

  3. 请注意,Qt 将 Android 应用视为闭源。因此,如果您希望保持 Android 应用代码私有,您将需要商业 Qt 许可证。

恭喜!您已成功生成了一个可部署的 Android 应用程序。与 iOS 不同,Android 是一个开放系统。您可以将.apk文件复制或分发到运行相同 Android 版本的其他 Android 设备上并进行安装。

在本节中,我们为我们的 Android 设备创建了一个可安装的软件包。在下一节中,我们将学习更多安装工具。

其他安装工具

在本节中,我们将讨论一些其他工具,您可以使用这些工具创建安装程序。请注意,我们不会详细讨论这些工具。我尚未验证这些安装框架是否与 Qt 6 兼容。您可以访问各自工具的网站并从其文档中了解更多信息。除了 Qt 提供的安装框架和工具之外,您还可以在 Windows 机器上使用以下工具:

  • CQtDeployer是一个应用程序,用于提取可执行文件的所有依赖库并为您的应用程序创建启动脚本。该工具声称可以更快地部署应用程序并提供灵活的基础设施。它支持 Windows 和 Linux 平台。您可以在以下链接了解更多关于该工具的信息:github.com/QuasarApp/CQtDeployer

  • Nullsoft Scriptable Install SystemNSIS)是来自 Nullsoft 的基于脚本的安装工具,该公司也创建了 Winamp。它已成为专有商业工具(如 InstallShield)的流行替代品。NSIS 的当前版本具有现代图形用户界面、LZMA 压缩、多语言支持和简单的插件系统。您可以在nsis.sourceforge.io/Main_Page了解更多有关该工具的信息。

  • InstallShield是一款专有软件应用程序,允许您创建安装程序和软件捆绑包。InstallShield 通常用于在 Windows 平台桌面和服务器系统上安装软件,但也可以用于管理各种便携式和移动设备上的软件应用程序和软件包。查看其功能并试用试用版。您可以在以下链接下载试用版并了解更多信息:www.revenera.com/install/products/installshield.html

  • Inno Setup是一个由 Delphi 创建的免费软件脚本驱动安装系统。它于 1997 年首次发布,但仍然凭借其出色的功能集和稳定性与许多商业安装程序竞争。在以下链接了解更多关于此安装程序的信息:jrsoftware.org/isinfo.php

您可以选择任何安装框架并部署您的应用程序。最终,它应该能够满足您的安装目标。

在本节中,我们讨论了一些可能有益于您需求的安装工具。现在让我们总结一下本章的要点。

概要

我们首先讨论了应用程序部署问题,并学习了静态库和动态库之间的区别。然后我们讨论了 Qt 中的不同部署工具,以及 Windows 部署和安装的特定情况。凭借这些知识,我们在 Windows 上部署了一个示例应用程序,并使用了 Qt 安装程序框架创建了一个安装程序。此外,我们还发现了在 Linux 和 macOS 上部署应用程序,并磨练了在各种平台上部署应用程序的技能。之后,我们解释了在将基于 Qt 的 Android 应用程序发布到 Play 商店之前需要考虑的一些重要问题。

最后,我们看了一些第三方安装程序工具。总之,您已经学会了在各种平台上开发、测试和部署 Qt 应用程序。有了这些知识,您应该能够创建自己的安装包并与世界分享。

第十一章国际化中,我们将学习开发一个支持翻译的 Qt 应用程序。

第十一章:国际化

在之前的章节中,我们学习了如何使用 Qt Widgets 或 Qt Quick 创建 GUI 应用程序。为了使我们的应用程序在全球范围内可用,我们需要为应用程序添加翻译。

使您的应用程序支持翻译的过程被称为国际化。这使得为来自不同文化、地区或语言的观众本地化内容变得容易。使用 Qt 将 Qt Widgets 和 Qt Quick 应用程序翻译成本地语言非常容易。将应用程序适应目标市场的不同语言、地理和技术标准的过程被称为国际化。

您将学习如何制作一个支持多语言的应用程序。在本章中,我们将探讨不同的工具和流程,以制作一个支持翻译的应用程序。在本章中,我们将讨论以下内容:

  • 国际化的基础知识

  • 为翻译编写源代码

  • 加载翻译文件

  • 使用 Qt Widgets 进行国际化

  • 使用 Qt Quick 进行国际化

  • 部署翻译

通过本章的学习,您将能够使用 Qt Widgets 和 Qt Quick 创建一个支持翻译的应用程序。

技术要求

本章的技术要求包括在最新的桌面平台上安装 Qt 6.0.0 和 Qt Creator 4.14.0 的最低版本,如 Windows 10、Ubuntu 20.04 或 macOS 10.14。

本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp/tree/master/Chapter11

重要提示

本章中使用的屏幕截图是在 Windows 平台上进行的。您将在您的机器上看到基于底层平台的类似屏幕。

了解国际化和 Qt Linguist

调整应用程序以适应目标市场的不同语言、地理变化和技术规范的过程被称为国际化和本地化。国际化是指创建一个软件应用程序,可以在不需要进行重大技术更改的情况下被翻译成各种语言和不同地区的过程。国际化通常缩写为 i18n,其中 18 是英语单词中字母 i 和 n 之间的字母数。产品能够被本地化的便利程度受到其国际化的影响。为全球市场创建一个语言和文化聚焦的应用程序是一个更加复杂和耗时的过程。因此,公司在产品开发的开始阶段就专注于为全球市场创建 i18n 感知的应用程序。

对于国际化,您应该设计您的应用程序,以避免以后在本地化或全球部署时出现障碍。这涵盖了允许 Unicode 或在适当的情况下维护对旧字符编码的小心处理,小心处理字符串连接,防止代码依赖于用户界面字符串值等方面。您应该提供对于识别可翻译字符串和以后可能需要的国际化的系统语言的支持。

您的应用程序应该了解本地语言、日期和时间格式、数字系统或文化偏好。修改产品、应用程序或文档的内容以满足特定目标市场的语言、文化和其他偏好,被称为本地化。本地化通常用英文l10n表示,其中 10 是ln之间的字母数。本地化包括合并特定地区的要求,并为特定地区或语言翻译应用程序。可本地化的功能应该与源代码分离,以便根据用户的文化偏好进行调整。

lupdatelrelease。这些程序可以与 qmake 项目或直接与文件系统一起使用。

lupdate工具会定位项目的源代码、头文件和.ui.qml文件中的可翻译字符串。然后它会创建或更新翻译文件(.ts文件)。您可以在命令行或.pro文件中指定要处理的文件作为参数。.ts文件使用文档类型定义DTD)格式,描述在以下链接中:

doc.qt.io/qt-6/linguist-ts-file-format.html

Qt 为国际化提供了出色的支持。Qt 在所有用户界面元素中内置了对许多语言的支持。但是,在为应用程序编写源代码时,您必须遵循某些实践。这包括标记可翻译的字符串,避免模糊的字符串,使用带编号的参数(%n)作为占位符,并加载正确的翻译文件。您可以在 C++和用户界面文件中使用,也可以在两个源文件中使用可翻译的字符串。该工具会定位并将所有源文件中的字符串添加到一个带有相应上下文的单个.ts文件中。

带有.ts扩展名的翻译文件在应用程序开发过程中使用。这些文件可以编译成紧凑的二进制格式。编译后的翻译文件以QM格式编码,并具有.qm文件扩展名。在运行应用程序时,Qt 运行时使用.qm文件而不是.ts文件。您可以使用lrelease工具将.ts文件转换为.qm文件。.qm文件是一种轻量级的二进制文件。它允许快速的翻译查找。您可以在命令行或.pro项目文件中指定要由lrelease处理的.ts文件。每次发布应用程序时都会使用此工具,从测试版本到最终生产版本。如果没有.qm文件,则应用程序仍将正常工作,并使用源文件中的原始文本。

对于语言的选择,Qt Linguist 和lrelease使用某些内部规则。您可以在以下链接中找到有关这些规则的详细信息:

doc.qt.io/qt-6/i18n-plural-rules.html

让我们来看看 Qt Linguist 用户界面。您可以通过双击Linguist可执行文件或从命令提示符中选择它来启动 Qt Linguist。您会在屏幕上看到以下用户界面出现:

图 11.1 - Qt Linguist 用户界面

图 11.1 - Qt Linguist 用户界面

在上图中,您可以看到多个部分,并且工具栏中有一些禁用的按钮。您可以从文件菜单中打开一个.ts文件。我们将在本章的后面部分讨论一个示例时讨论这些部分。

您可以在以下网页了解更多关于 Qt Linguist 和 GUI 界面的信息:

doc.qt.io/qt-6/linguist-translators.html

在本节中,您熟悉了与国际化相关的术语和 Qt 框架提供的工具。在对基础知识有了很好的理解之后,我们准备在下一节中编写一个支持翻译的应用程序。

编写用于翻译的源代码

在本节中,我们将讨论如何标记字符串为可翻译字符串以及如何使用 Qt 提供的工具。无论您的应用程序在何处使用对用户可见的带引号的字符串,都要确保QCoreApplication::translate()方法对其进行处理。要做到这一点,只需使用tr()方法标记那些用于显示目的的可翻译字符串。此功能用于显示 C++源文件中哪些文本字符串是可翻译的。

例如,如果您想要使用QLabel在用户界面上显示文本,那么将文本嵌入tr()方法中如下:

QLabel *label = new QLabel(tr("Welcome"));

类名是QObject及其派生类的翻译上下文。要覆盖上下文,QObject派生类必须在其类定义中使用Q_OBJECT宏。此宏为派生类设置上下文。

Qt 为国际化提供了几个方便的宏和方法。一些最常用于翻译的宏如下:

  • 如果在 C++源文件中有可用的翻译,tr()会返回一个翻译后的字符串。

  • 如果在 QML 文件中有可用的翻译,qsTr()会返回一个翻译后的字符串。

  • qtTrId()在 C++文件中查找并返回一个由 ID 标识的翻译后的字符串。

  • qsTrId()在 QML 文件中查找并返回一个由 ID 标识的翻译后的字符串。

  • QT_TR_NOOP()告诉lupdate收集当前上下文中的字符串以便以后翻译。

  • QT_TRID_NOOP()标记动态翻译的 ID。

  • QCoreApplication::translate()通过查询已安装的翻译文件提供翻译。

  • qsTranslate()在 QML 文件中为给定上下文提供一个翻译版本。

  • QQmlEngine::retranslate()更新所有标记为翻译的字符串的绑定表达式。

在 C++文件中,可翻译的字符串使用tr()标记,在 QML 文件中使用qsTr()。我们将在本章中讨论这些宏和方法。

所有可翻译的字符串都由lupdate工具获取,并更新到翻译源TS)中。TS 文件是一个 XML 文件。通常,TS 文件遵循以下命名约定:

ApplicationName>_<LanguageCode>_<CountryCode>.ts

在这个约定中,LanguageCode是小写的 ISO 639 语言代码,CountryCode是大写的 ISO 3166 两字母国家代码。您可以通过使用特定的国家代码为相同的语言创建不同国家的翻译。在通过 Qt Creator 的新项目向导创建 Qt 应用程序时,您可以创建一个带有语言代码和国家代码的默认翻译文件。

创建.ts文件后,您可以运行lupdate来更新所有用户可见的字符串的.ts文件。您可以从命令行以及从 Qt Creator 和 Visual Studio 插件运行lupdate。让我们使用 Qt 的命令提示符来运行HelloWorld应用程序的以下命令:

>lupdate HelloWorld.pro

lupdate从不同的源文件(如.cpp.h.qml.ui)中获取可翻译的字符串。为了使lupdate有效工作,您应该在应用的.pro文件中的TRANSLATIONS变量下指定翻译文件。看下面的.pro文件部分,我们已经添加了六个翻译源文件:

TRANSLATIONS = \
        HelloWorld_de_DE.ts \
        HelloWorld_fi_FI \
        HelloWorld_es_ES.ts \
        HelloWorld_zh_CN.ts \
        HelloWorld_zh_TW.ts \
        HelloWorld_ru_RU.ts

您还可以使用*.ts添加基于通配符的翻译文件选择。

要翻译 Qt Quick 应用程序,使用qsTr()方法标记.qml文件中的字符串。您可以按以下方式为单个 QML 文件创建翻译文件:

>lupdate main.qml -ts HelloWorld_de_DE.ts

您可以为不同语言创建多个翻译文件,并将它们放在.qrc文件中:

RESOURCES += translations.qrc 

您可以按以下方式使用lupdate处理.qrc文件中的所有 QML 文件:

>lupdate qml.qrc -ts HelloWorld_de_DE.ts

要处理所有 QML 文件而不使用.qrc文件,可以在 Qt 的命令提示符中输入以下内容:

>lupdate -extensions qml -ts HelloWorld_de_DE.ts

您还可以将 C++源文件作为参数与资源文件一起传递。在.pro文件中提及翻译文件是可选的。您可以通过在命令行中指定翻译文件来实现:

>lupdate qml.qrc messages.cpp -ts HelloWorld_de_DE.ts HelloWorld _es_ES.ts

lrelease 集成了标记为finished的翻译。如果一个字符串缺少翻译并且被标记为unfinished,那么将使用原始文本。翻译者或开发人员可以修改 TS 文件内容,并按以下步骤将其标记为finished

  1. 启动 Qt Linguist 并打开项目结构中的.ts文件,如下所示:图 11.2 - 在 Qt Creator 中使用 Qt Linguist 选项打开

图 11.2 - 在 Qt Creator 中使用 Qt Linguist 选项打开

  1. 然后点击Context视图中的任何上下文,以查看Strings视图中该上下文的可翻译字符串。

  2. Source text视图中,输入当前字符串的翻译。您可以在Phrases and Guesses视图中找到现有的翻译和类似短语。

  3. 翻译者可以在Translator comments字段中输入评论。

  4. 要完成翻译,按下Ctrl + Enter并从工具栏中选择勾号图标。您将看到已翻译字符串的绿色勾号。

  5. 最后,保存文件并退出 Qt Linguist 工具。

您可以在不指定.pro文件的情况下运行lrelease。当您运行lrelease来读取.ts文件时,它会生成应用程序在运行时使用的.qm文件:

>lrelease *.ts

一旦生成了.qm文件,将它们添加到.qrc文件中。您的应用程序现在已准备好进行翻译。

您还可以使用基于文本 ID 的翻译机制。在这种方法中,应用程序中的每个可翻译字符串都被分配一个唯一的标识符。这些唯一的文本标识符直接用作源代码中实际字符串的替代。用户界面开发人员需要在这方面付出更多的努力,但是如果您的应用程序包含大量翻译字符串,这种方法更容易维护。

在某些应用程序中,某些类可能不使用QObject作为基类,或者在其类定义中使用Q_OBJECT宏。但是这些类可能包含一些需要翻译的字符串。为解决此问题,Qt 提供了一些宏来添加翻译支持。

您可以使用以下方式使用Q_DECLARE_TR_FUNCTIONS(ClassName)来启用非 Qt 类的翻译:

class CustomClass
{
    Q_DECLARE_TR_FUNCTIONS(CustomClass)
public:
    CustomClass();
    ...
}; 

此宏在qcoreapplication.h中定义。当您添加此宏时,Qt 会向您的类添加以下函数以启用翻译:

static inline QString tr(const char *sourceString, const char 
*disambiguation = nullptr, int n = -1)
{ 
    return QCoreApplication::translate(#className, sourceString, 
disambiguation, n); 
}

从上述代码中,您可以注意到 Qt 使用类名作为上下文调用QCoreApplication::translate()

您还可以在类或方法之外使用可翻译字符串;QT_TR_NOOP()QT_TRANSLATE_NOOP()用于标记这些字符串为可翻译。有不同的宏和函数可用于基于文本 ID 的翻译。您可以使用qsTrId()代替qsTr(),以及QT_TRID_NOOP()代替QT_TR_NOOP()。您可以在用户界面中使用相同的文本 ID 作为用户界面字符串,而不是在用户界面中使用普通字符串。

在 Qt Linguist 中,可以同时加载和编辑多个翻译文件。您还可以使用phrase books来重用现有的翻译。Phrase books 是包含典型短语及其翻译的标准 XML 文件。这些文件由 Qt Linguist 创建和更新,并可被任意数量的项目和应用程序使用。如果要翻译源字符串,而这些源字符串在短语书中可用,可以使用 Qt Linguist 的批量翻译功能。选择Batch Translation来指定批量翻译过程中要使用的短语书及其顺序。只有没有当前翻译的条目应该被考虑,批量翻译的条目应该被标记为Accepted。您还可以从New Phrase Book选项创建一个新的短语书。

重要提示

lupdate默认要求所有源代码以 UTF-8 编码。具有CODECFORSRCqmake 变量为UTF-16的文件将解析为 UTF-16 而不带 BOM。默认情况下,某些编辑器(如 Visual Studio)使用单独的编码。通过将源代码限制为 ASCII 并为可翻译的字符串使用转义序列,您可以避免编码问题。

在本节中,我们讨论了如何使用lupdatelrelease创建和更新翻译文件。接下来,我们将学习如何安装翻译器并在 Qt 应用程序中加载翻译文件。

在 Qt 应用程序中加载翻译

在前一节中,我们创建了翻译文件并了解了工具的用途。在 TS 文件中查找翻译时,使用QTranslator函数。必须在应用程序的 GUI 对象之前实例化翻译器。

让我们看看如何在以下代码片段中使用QTranslator加载这些翻译文件:

QTranslator translator;
if(translator.load(QLocale(),QLatin1String("MyApplication") 
            , QLatin1String("_"), QLatin1String(":/i18n"))) 
    {
         application.installTranslator(&translator);
    } 
    else 
    {
        qDebug() << "Failed to load. " 
                 << QLocale::system().name();
    }

在上面的代码中,您可以看到我们创建了一个translator对象并加载了相应的翻译文件。QLocale用于获取底层系统语言。您还可以使用QLocale本地化数字、日期、时间和货币字符串。

或者,您可以按以下方式加载翻译文件:

QString fileName = ":/i18n/MyApplication_"+QLocale::
                   system().name()
+".qm";
translator.load(fileName); 

在这里,我们正在查看系统语言并加载相应的翻译文件。当您希望将系统语言用作应用程序语言时,前面的方法效果很好。但是,有些用户可能希望使用与系统语言不同的区域语言。在这种情况下,我们可以根据用户选择更改语言。我们将在下一节中学习如何做到这一点。

动态切换语言

到目前为止,您已经学会了如何为 Qt 应用程序使用系统语言或默认语言。在大多数应用程序中,您只需在main()中检测语言并加载适当的.qm文件。有时,您的应用程序必须能够在运行时支持用户语言设置的更改。需要在多人轮班使用的应用程序可能需要在无需重新启动的情况下切换语言。

要在基于 Qt Widgets 的应用程序中实现这一点,您可以重写QWidget::changeEvent()。然后,您必须检查事件是否属于QEvent::LanguageChange类型。您可以相应地重新翻译用户界面。

以下代码片段解释了如何在基于 Qt Widgets 的 GUI 中实现动态翻译:

void CustomWidget::changeEvent(QEvent *event)
{
    if (QEvent::LanguageChange == event->type()) 
    {
        ui->retranslateUi(this);
    }
    QWidget::changeEvent(event);
}

QEvent::LocaleChange可能会导致安装的翻译器列表切换。您可以创建一个带有用户界面的应用程序,让用户选择更改当前应用程序语言的选项。当发生QEvent::LanguageChange事件时,QWidget子类的默认事件处理程序将调用此方法。如果您使用QCoreApplication::installTranslator()函数安装新的翻译,您将收到一个LanguageChange事件。此外,通过向其他小部件发送LanguageChange事件,GUI 将强制它们更新。任何其他事件都可以传递给基类进行进一步处理。

实现动态翻译,您可以在命令行或 GUI 中提供选项。默认情况下,Qt 将所有可翻译的字符串放在.ui文件中的retranslateUi()中。每当语言更改时,您都必须调用此函数。您还可以创建并调用自定义方法,根据QEvent::LanguageChange事件重新翻译通过 C++代码创建的字符串。

在本节中,我们讨论了如何在应用程序运行时实现动态翻译。在下一节中,我们将使用 Qt Widgets 创建一个支持翻译的应用程序。

使用 Qt Widgets 进行国际化

在前面的部分中,我们讨论了如何创建翻译文件以及如何使用QTranslator来加载翻译文件。让我们使用 Qt Widgets 创建一个简单的示例并实现我们的学习。

按照以下步骤创建示例应用程序:

  1. 使用 Qt Creator 的新项目创建向导创建基于 Qt 小部件的应用程序,并按照之前章节中讨论的屏幕进行操作。

  2. 翻译文件屏幕上,选择德语(德国)作为语言选项,或者选择任何首选语言。

  3. 完成项目创建。您将看到Simplei18nDemo_de_DE.ts在项目结构中创建了。

  4. 接下来,将QLabel添加到.ui 文件中,并添加欢迎文本。

  5. 接下来,运行lupdate。您可以从命令行以及从 Qt Creator 界面运行lupdate,如图 11.3所示:图 11.3 - Qt Creator 中的 Qt 语言家选项

图 11.3 - Qt Creator 中的 Qt 语言家选项

  1. 当您运行lupdate时,您将在控制台窗口中看到以下输出:
C:\Qt6Book\Chapter11\Simplei18nDemo>lupdate Simplei18nDemo.pro
Info: creating stash file C:\Qt6Book\Chapter11\Simplei18nDemo\.qmake.stash
Updating 'Simplei18nDemo_de_DE.ts'...
    Found 2 source text(s) (2 new and 0 already existing)
  1. 现在,.ts 文件已更新为字符串。使用纯文本编辑器打开Simplei18nDemo_de_DE.ts。您应该看到以下内容:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de_DE">
<context>
    <name>CustomWidget</name>
    <message>
        <location filename="customwidget.ui" 
            line="14"/>
        <source>Simple i18n Demo</source>
        <translation type="unfinished"></translation>
    </message>
    <message>
        <location filename="customwidget.ui" 
            line="25"/>
        <source>Welcome</source>
        <translation type="unfinished"></translation>
    </message>
</context>
</TS>

您可以看到在.ts 文件中更新了用户界面字符串,并且在文件顶部定义了翻译的语言。通过修改代码中的此字段,您可以创建相应的翻译文件:

<TS version="2.1" language="de_DE">

您还会看到翻译状态为未完成

  1. 因此,让我们打开 Qt 语言家并完成翻译:图 11.4 - 显示 Qt 语言家界面的不同部分的示例

图 11.4 - 显示 Qt 语言家界面的不同部分的示例

  1. 您将在用户界面中看到六个不同的部分。在上下文视图中选择上下文以加载相应的字符串。

  2. 源文本视图中添加翻译。您可以使用谷歌翻译将字符串翻译成所需的语言。在这里,我们使用谷歌翻译将字符串翻译成德语。

注意

有多种翻译被使用。如果字符串的确切含义不符,请忽略。我对德语不熟悉。我用这个来进行演示。因此,我添加了翻译者的评论。

  1. 要完成翻译,请按下Ctrl + Enter,或者单击工具栏上的绿色勾号图标。

  2. 下一步是保存翻译。对上下文中列出的所有可翻译字符串重复此操作。

  3. 从 Qt 的命令提示符或 IDE 选项中运行lrelease。您将看到生成了.qm文件:

C:\Qt6Book\Chapter11\Simplei18nDemo>lrelease *.ts
Updating 'Simplei18nDemo_de_DE.qm'...
    Generated 2 translation(s) (2 finished and 0 unfinished)
  1. 让我们将翻译器添加到main.cpp并加载翻译文件:
#include "customwidget.h"
#include <QApplication>
#include <QTranslator>
#include <QDebug>
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QTranslator translator;
    if(translator.load(":/translations
                       /Simplei18nDemo_de_DE.qm"))
    {
        app.installTranslator(&translator);
        qDebug()<<"Loaded successfully!";
    }
    else
    {
        qWarning()<<"Loading failed.";
    }
    CustomWidget customUI;
    customUI.show();
    return app.exec();
} 
  1. 最后一步是运行 qmake 并构建应用程序。然后,点击左下角的运行按钮。

  2. 我们已成功将 GUI 翻译成德语。您将看到以下输出:

图 11.5 - 使用 Qt 小部件输出翻译示例

图 11.5 - 使用 Qt 小部件输出翻译示例

恭喜!您学会了如何将应用程序翻译成其他语言。您现在可以将 Qt 应用程序翻译成本地语言,并与朋友和同事分享。

在本节中,我们讨论了如何使用 Qt 小部件创建一个支持翻译的应用程序。在下一节中,我们将为 Qt 小部件应用程序添加动态翻译功能。

为 Qt 小部件应用程序添加动态翻译

在上一节中,您学习了如何创建一个基于 Qt 小部件的应用程序,并将语言更改为首选语言。然而,像大多数全球应用程序一样,您可能需要提供更多的翻译,并允许用户随时更改语言。

让我们修改前一节的示例,添加一些额外的实现:

  1. 在.ui 文件中添加一个下拉框,并向其中添加三种语言。为了说明目的,我们使用了英语、德语和西班牙语。我们在中心添加了一条消息,并在下拉菜单中添加了语言切换选项:图 11.6 - Qt Designer 中显示示例中使用的布局的表单

图 11.6 - 在 Qt Designer 中显示示例中使用的布局的表单

  1. 将新的翻译文件添加到项目文件中如下:
TRANSLATIONS += \
    WidgetTranslationDemo_en_US.ts \
    WidgetTranslationDemo_de_DE.ts \
    WidgetTranslationDemo_es_ES.ts 
  1. 让我们修改CustomWidget类并添加以下方法进行动态翻译:
#ifndef CUSTOMWIDGET_H
#define CUSTOMWIDGET_H
#include <QWidget>
#include <QTranslator>
QT_BEGIN_NAMESPACE
namespace Ui { class CustomWidget; }
QT_END_NAMESPACE
class CustomWidget : public QWidget
{
    Q_OBJECT
public:
    CustomWidget(QWidget *parent = nullptr);
    ~CustomWidget();
  public slots:
    void languageChanged(int index);
    void switchTranslator(const QString& filename);
    void changeEvent(QEvent *event);
private:
    Ui::CustomWidget *ui;
    QTranslator m_translator;
};
#endif // CUSTOMWIDGET_H 
  1. 下一步是连接信号和槽。我们已经在构造函数中创建了连接:
CustomWidget::CustomWidget(QWidget *parent)
    : QWidget(parent), ui(new Ui::CustomWidget)
{
    ui->setupUi(this);
    connect(ui->languageSelectorCmbBox, 
            SIGNAL(currentIndexChanged(int)),this, 
            SLOT(languageChanged(int)));
    qApp->installTranslator(&m_translator);
} 
  1. 让我们将以下代码添加到槽定义中:
void CustomWidget::languageChanged(int index)
{
    switch(index)
    {
    case 0: //English
        switchTranslator(":/i18n/
            WidgetTranslationDemo_en_US.qm");
        break;
    case 1: //German
        switchTranslator(":/i18n/
            WidgetTranslationDemo_de_DE.qm");
        break;
    case 2: //Spanish
        switchTranslator(":/i18n/
            WidgetTranslationDemo_es_ES.qm");
        break;
    }
}

在这里,我们通过组合框索引更改信号从用户界面接收语言选择。

  1. 下一步是安装一个新的翻译器:
void CustomWidget::switchTranslator(const QString& filename)
{
    qApp->removeTranslator(&m_translator);
    if(m_translator.load(filename))
    {
        qApp->installTranslator(&m_translator);
    }
} 
  1. 最后一步是重新实现changeEvent()
void CustomWidget::changeEvent(QEvent *event)
{
    if (event->type() == QEvent::LanguageChange)
    {
        ui->retranslateUi(this);
    }
    QWidget::changeEvent(event);
}
  1. 运行 qmake 并在 IDE 上点击运行按钮。

将出现以下屏幕:

图 11.7 - 当选择英语时显示输出的示例

图 11.7 - 当选择英语时显示输出的示例

  1. 从语言选择下拉菜单中更改语言。让我们选择德语作为新语言。您将看到整个 GUI 都使用德语字符串更改了:图 11.8 - 当选择德语时显示输出的示例

图 11.8 - 当选择德语时显示输出的示例

  1. 再次将语言切换为西班牙语。您将看到 GUI 文本已更改为西班牙语:

图 11.9 - 当选择西班牙语时显示输出的示例

图 11.9 - 当选择西班牙语时显示输出的示例

恭喜!您已成功创建了一个多语言 Qt Widgets 应用程序。

在本节中,您学会了如何在运行时翻译基于 Qt Widgets 的 GUI。在下一节中,我们将使用 Qt Quick 创建一个具有翻译意识的应用程序。

使用 Qt Quick 进行国际化

在前一节中,我们讨论了 Qt Widgets 中的国际化。在本节中,我们将讨论国际化 Qt Quick 应用程序的不同方面。Qt Quick 应用程序中的基础本地化方案与 Qt Widgets 应用程序类似。Qt Quick 中也使用了 Qt Linguist 手册中描述的相同一组工具。您可以翻译同时使用 C++和 QML 的应用程序。

在 Qt 项目文件中,SOURCES变量用于 C++源文件。如果您在此变量下列出 QML 或 JavaScript 文件,则编译器将尝试将这些文件视为 C++文件来使用。作为一种解决方法,您可以使用lupdate_only {...}条件声明,使 QML 文件对lupdate工具可见,但对 C++编译器不可见。

考虑以下示例。应用程序的.pro文件片段列出了两个 QML 文件:

lupdate_only {
SOURCES = main.qml \
          HomeScreen.qml
}

您还可以使用通配符匹配来指定 QML 源文件。由于搜索不是递归的,因此您必须列出源代码中可以找到用户界面字符串的每个目录:

lupdate_only{
SOURCES = *.qml \
          *.js 
}

让我们创建一个简单的翻译示例。我们将创建一个与 Qt Widgets 应用程序中创建的类似屏幕相似的屏幕。按照以下步骤进行:

  1. 使用 Qt Creator 的新项目创建向导创建基于 Qt Quick 的应用程序,并按照之前章节中讨论的屏幕进行操作。

  2. 翻译文件屏幕上,选择德语(德国)作为语言选项或任何首选语言。

  3. 完成项目创建。您将看到QMLTranslationDemo_de_DE.ts已创建在您的项目结构中。

  4. 接下来,在.qml文件中添加一个Text并添加Welcome文本:

import QtQuick
import QtQuick.Window
Window {
    width: 512
    height: 512
    visible: true
    title: qsTr("QML Translation Demo")
    Text {
        id: textElement
        anchors.centerIn: parent
        text: qsTr("Welcome")
    }
}
  1. 将以下代码添加到main.cpp中:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QTranslator>
#include <QDebug>
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QTranslator translator;
    if(translator.load(":/translations/
        QMLTranslationDemo_de_DE.qm"))
    {
        app.installTranslator(&translator);
        qDebug()<<"Loaded successfully!";
    }
    else
    {
        qWarning()<<"Loading failed.";
    }
    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, 
        &QQmlApplicationEngine::objectCreated,
             &app, url 
            {
              if (!obj && url == objUrl)
                  QCoreApplication::exit(-1);
            }, Qt::QueuedConnection);
    engine.load(url);
    return app.exec();
}
  1. 这些步骤与 Qt Widgets 示例类似。接下来运行lupdate

  2. 按照相同的步骤使用 Qt Linguist 更新.ts文件中的翻译。

  3. 从 Qt 的命令提示符或 IDE 选项中运行lrelease。您将看到生成了.qm文件。

  4. .qm文件添加到资源(.qrc)文件中并运行 qmake。

  5. 最后一步是构建和运行应用程序。在 Qt Creator 中点击运行按钮。

  6. 您将看到与 Qt Widgets 示例中相同的输出:

图 11.10 - 使用 Qt Quick 进行翻译示例的输出

图 11.10 - 使用 Qt Quick 进行翻译示例的输出

在上面的示例中,我们将我们的 Qt Quick 应用程序翻译成了德语。

在本节中,我们讨论了如何使用 Qt Quick 创建一个支持翻译的应用程序。在下一节中,我们将为 Qt Quick 应用程序添加动态翻译功能。

在 Qt Quick 应用程序中进行动态翻译

在上一节中,您学习了如何创建一个基于 Qt Quick 的应用程序,以及如何将语言更改为首选语言。就像 Qt Widgets 示例一样,您也可以向 Qt Quick 应用程序添加动态翻译。

让我们通过一些额外的实现修改前面的示例:

  1. 创建一个名为TranslationSupport的 i18n 支持类,并添加以下行:
#ifndef TRANSLATIONSUPPORT_H
#define TRANSLATIONSUPPORT_H
#include <QObject>
#include <QTranslator>
class TranslationSupport : public QObject
{
    Q_OBJECT
public:
    explicit TranslationSupport(QObject *parent = 
                                nullptr);
public slots:
    void languageChanged(int index);
    void switchTranslator(const QString& filename);
signals:
    void updateGUI();
private:
    QTranslator m_translator;
};
#endif // TRANSLATIONSUPPORT_H

上述代码是一个辅助类,支持 QML 中的翻译功能。它用于更新翻译文件中的翻译。

  1. 接下来,添加以下代码以切换翻译器:
void TranslationSupport::switchTranslator(const QString& filename)
{
    qApp->removeTranslator(&m_translator);
    if(m_translator.load(filename))
    {
        qApp->installTranslator(&m_translator);
        emit updateGUI();
    }
}
  1. 然后,在 QML 的INVOKABLE方法定义中添加以下代码:
void TranslationSupport::languageChanged(int index)
{
    switch(index)
    {
    case 0: //English
        switchTranslator(":/i18n/
            QMLDynamicTranslation_en_US.qm");
        break;
    case 1: //German
        switchTranslator(":/i18n/
            QMLDynamicTranslation_de_DE.qm");
        break;
    case 2: //Spanish
        switchTranslator(":/i18n/
            QMLDynamicTranslation_es_ES.qm");
        break;
    }
} 
  1. main.cpp文件中,添加以下代码。请注意,我们已将TranslationSupport实例暴露给 QML 引擎:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "translationsupport.h"
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    TranslationSupport i18nSupport;
    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty(
        "i18nSupport", &i18nSupport);
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&i18nSupport, 
        &TranslationSupport::updateGUI, &engine, 
        &QQmlApplicationEngine::retranslate);
    engine.load(url);
    return app.exec();
}
  1. 然后使用QQmlApplicationEngine::retranslate()方法添加updateGUI()信号。

  2. 让我们看一下main.qml文件。我们在.qml文件中添加了一个下拉框,并添加了三种语言。为了说明目的,我们使用了英语、德语和西班牙语:

Text {
    id: textElement
    anchors.centerIn: parent
    text: qsTr("Welcome!")
} 
Row {
    anchors {
        top: parent.top;   topMargin: 10 ;
        right: parent.right; rightMargin: 10;
    }
    spacing: 10
    Text{
        text: qsTr("Select language")
        verticalAlignment: Text.AlignVCenter
        height: 20
    } 
    ComboBox {
        height: 20
        model: ListModel {
            id: model
            ListElement { text: qsTr("English")}
            ListElement { text: qsTr("German")}
            ListElement { text: qsTr("Spanish")}
        }
        onCurrentIndexChanged: {
            i18nSupport.languageChanged(currentIndex)
        }
    }
}
  1. 运行lupdate并继续翻译过程。

  2. 按照相同步骤使用 Qt Linguist 更新.ts文件中的翻译。

  3. 从 Qt 的命令提示符或 IDE 选项中运行lrelease。您将看到生成了.qm文件。

  4. .qm文件添加到资源(.qrc)文件中并运行 qmake。

  5. 最后一步是构建和运行应用程序。在 Qt Creator 中点击运行按钮。

将出现以下屏幕:

图 11.11 - 当选择英语语言时,Qt Quick 示例显示的输出

图 11.11 - 当选择英语语言时,Qt Quick 示例显示的输出

  1. 从语言选择下拉框中更改语言。让我们选择德语作为新语言。您将看到整个 GUI 都改为了德语字符串:图 11.12 - 当选择德语语言时,Qt Quick 示例显示的输出

图 11.12 - 当选择德语语言时,Qt Quick 示例显示的输出

  1. 再次将语言切换为西班牙语。您将看到 GUI 文本已更改为西班牙语:

图 11.13 - 当选择西班牙语言时,Qt Quick 示例显示的输出

图 11.13 - 当选择西班牙语言时,Qt Quick 示例显示的输出

恭喜!您已成功创建了一个多语言 Qt Quick 应用程序。

在本节中,您学习了如何在运行时翻译基于 Qt Quick 的 GUI。在下一节中,我们将讨论如何部署翻译文件。

部署翻译

在之前的章节中,我们学习了如何使用 Qt Widgets 和 QML 创建支持翻译的应用程序。您不必将.ts文件与应用程序一起发布。要部署翻译,您的发布团队必须使用更新的.qm文件并将其与应用程序包一起发布。应用程序所需的.qm文件应放在QTranslator可以找到它们的位置。通常,这是通过将qm文件嵌入到资源(.qrc)文件中或指定包含.qm文件的路径相对于QCoreApplication::applicationDirPath()来完成的。rcc工具用于在构建过程中将翻译文件嵌入到 Qt 应用程序中。它通过生成包含指定数据的相应 C++文件来工作。

您可以通过将脚本添加到您的.pro文件中来自动生成.qm文件。您可以按照以下步骤进行操作:

  1. 首先,在 Qt 项目(.pro)文件中使用语言代码来声明LANGUAGES变量下的语言。

  2. lreleaseembed_translations添加到CONFIG变量中。

  3. 然后添加一个函数来生成所需语言的.ts文件。

  4. 最后,定义TRANSLATIONS_FILES变量,使用lrelease创建.qm文件,并将其嵌入到应用程序资源中。

前面的步骤将自动运行lrelease并生成.qm文件。lrelease工具处理TRANSLATIONSEXTRA_TRANSLATIONS下列出的翻译文件。与TRANSLATIONS变量不同,列在EXTRA_TRANSLATIONS下的文件只由lrelease工具处理,而不是由lupdate处理。您需要将.qm文件嵌入到资源中或者将.qm文件与部署包一起发布。

您可以在这里了解更多关于自动化生成 QM 文件的信息:wiki.qt.io/Automating_generation_of_qm_files.

在本节中,您学会了如何部署您的翻译文件。在下一节中,我们将总结本章的要点。

摘要

在本章中,我们深入了解了 Qt 中国际化和本地化的核心概念。我们讨论了 Qt 提供的不同国际化工具。我们学会了如何使用 Qt Linguist。我们还学习了如何将 Qt Widgets 应用程序翻译成不同的语言。然后,我们学会了如何动态翻译。

在本章的后半部分,我们讨论了如何翻译 Qt Quick 应用程序。之后,我们学会了如何在 Qt Quick 应用程序中动态切换语言。现在,您可以创建一个具有多种语言的应用程序,并与您在不同地理区域的客户或朋友分享。

第十二章性能考虑中,我们将学习有关在 Qt 应用程序中优化性能的工具和技巧。

第十二章:性能考虑

在本章中,我们将概述性能优化技术以及如何在基于 Qt 的应用程序开发环境中应用它们。性能是应用程序成功的非常重要的因素。性能失败可能导致业务失败、客户关系恶化、竞争力降低和收入损失。延迟性能优化可能会在声誉和组织形象方面产生巨大的成本。因此,进行性能调优非常重要。

您还将了解性能瓶颈以及如何克服它们。我们将讨论不同的分析工具来诊断性能问题,特别关注一些流行的工具。然后,您将学习如何对性能进行分析和基准测试。本章还介绍了 Qt 建模语言(QML)分析器和火焰图,以找出 Qt Quick 应用程序中的潜在瓶颈。您还将了解在开发 Qt 应用程序时应遵循的一些最佳实践。

我们将讨论以下主题:

  • 了解性能优化

  • 优化 C++代码

  • 使用并发、并行和多线程

  • 使用 QML 分析器和火焰图对 Qt Quick 应用程序进行分析

  • 其他 Qt Creator 分析工具

  • 优化图形性能

  • 创建基准测试

  • 不同的分析工具和优化策略

  • Qt 小部件的性能考虑

  • 学习 QML 编码的最佳实践

在本章结束时,您将学会为基于 C++和 QML 的应用程序编写高性能优化代码。

技术要求

本章的技术要求包括在最新的桌面平台上安装 Qt 6.0.0 和 Qt Creator 4.14.0 的最低版本,如 Windows 10、Ubuntu 20.04 或 macOS 10.14。

本章中使用的所有代码都可以从以下 GitHub 链接下载:github.com/PacktPublishing/Cross-Platform-Development-with-Qt-6-and-Modern-Cpp/tree/master/Chapter12/QMLPerformanceDemo

重要提示

本章中使用的屏幕截图是在 Windows 平台上拍摄的。您将在您的机器上看到基于底层平台的类似屏幕。

了解性能优化

性能优化是为了提高应用程序的性能。您可能想知道为什么这是必要的。应用程序需要性能优化有很多原因。当用户或质量保证团队报告性能问题时,开发人员可能会发现影响整体应用程序性能的问题。这可能是由于底层硬件限制、代码实现不佳或可扩展性挑战引起的。

优化是应用程序开发过程的一部分。这可能涉及对性能进行优化或对内存使用进行优化。优化旨在优化应用程序的行为,以满足产品对速度、内存占用、功耗等方面的要求。因此,优化几乎和在生产阶段编写功能一样重要。客户可能会报告性能问题,如故障、响应缓慢和功能缺失。更快的应用程序执行效率更高,同时消耗更少的资源,并且可以在相同的时间内处理更多的任务,而更慢的应用程序则不能。在当今竞争激烈的世界中,更快的软件意味着与竞争对手的竞争优势。性能在嵌入式和移动平台上非常重要,速度、内存和功耗等因素非常普遍。

在瀑布流程中,性能改进是在应用程序开发之后,在集成和验证阶段进行的。然而,在今天的敏捷世界中,代码性能应该在每两个迭代中进行评估,以提高整体应用程序性能。性能优化是一个持续的过程,而缺陷修复是一次性的任务。这是一个迭代的过程,在这个过程中,您总会发现有些地方可以改进,您的应用程序总会有改进的空间。根据约束理论,一个复杂应用程序通常会有一个限制应用程序达到最佳性能的问题。这些约束被称为瓶颈。一个应用程序的最佳性能受到瓶颈的限制,因此您应该在应用程序开发生命周期中考虑性能优化。如果忽视了这一点,您的新产品可能会变成一个完全的灾难,甚至可能会毁掉您的声誉。

在进行优化之前,您应该先确定一个目标。然后,您应该确定瓶颈或约束条件。之后,考虑如何解决约束条件。您可以改进您的代码并重新评估性能。如果达不到设定的目标,您需要重复这个过程。然而,请记住,过早的优化可能是万恶之源。在验证产品并实施早期用户反馈之前,您应该先实现主要功能。记住先让应用程序运行起来,然后确保其功能正确,最后再提高其速度。

当您设定性能目标时,您需要选择正确的技术。可能会有多个目标,比如更快的启动时间、更小的应用程序二进制文件,或者更少的随机访问内存(RAM)使用。一个目标可能会影响另一个目标,因此您必须根据预期的标准找到平衡点,例如,为了性能而优化代码可能会影响内存优化。提高整体性能的方法可能有很多,但您也应该遵循组织的编码准则和最佳实践。如果您正在为一个开源项目做贡献或者是自由应用程序开发人员,您应该遵循标准的编码实践以保持整体代码质量。

我们将遵循的一些重要技巧来提高性能如下:

  • 使用更好的算法和库

  • 使用最佳数据结构

  • 负责分配内存和优化内存

  • 避免不必要的复制

  • 消除重复计算

  • 增加并发性

  • 使用编译器二进制优化标志

在接下来的章节中,我们将讨论如何改进我们的 C++代码以提高整体应用程序性能的机会。

优化 C++代码

在大多数 Qt 应用程序中,很大一部分编码是用 C++完成的,因此您应该了解 C++优化技巧。本节是关于在编写 C++代码时实施一些最佳实践。如果不对 C++实现进行优化,它们会运行缓慢并消耗大量资源。更好地优化您的 C++代码还可以更好地控制内存管理和复制。有许多机会可以改进算法,从小的逻辑块到使用标准模板库(STLs),再到编写更好的数据结构和库。关于这个主题有几本优秀的书籍和文章。我们将讨论一些重要的点,以使代码运行更快并使用更少的资源。

以下是一些重要的 C++优化技巧:

  • 专注于算法,而不是微优化

  • 不要不必要地构造对象和复制

  • 使用 C++11 特性,如移动构造函数、lambda 和 constexpr 函数

  • 选择静态链接和位置相关代码

  • 优先选择 64 位代码和 32 位数据

  • 最小化数组写入,优先使用数组索引而不是指针

  • 优先选择正常的内存访问模式

  • 减少控制流

  • 避免数据依赖

  • 使用最佳算法和数据结构

  • 使用缓存

  • 使用预计算表来避免重复计算

  • 首选缓冲和批处理

由于本书需要对 C++有先前的了解,我们期望您已经了解这些最佳实践。作为一名 C++程序员,始终要了解最新的 C++标准,如 C++17 和 C++20。这将帮助您编写具有出色功能的高效代码。我们不会在本节详细讨论这些内容,而是留给您自行探索。

您可以在以下链接了解有关 C++核心指南的更多信息:isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines

您可以在以下链接了解有关优化 C++代码的更多信息:www.agner.org/optimize/

浏览列出的方法来改进您的 C++代码。接下来,我们将在下一节讨论如何通过并发和多线程来提高应用程序性能。

使用并发、并行和多线程

既然您已经是一名 C++开发人员,您可能已经了解这些术语,这些术语可以互换使用。然而,这些术语之间存在差异。让我们在这里重新审视这些术语:

  • 并发性是同时执行多个程序(并发)。

  • 并行性是在多核处理器中并行运行程序的一部分,利用多个核心。

  • 多线程中央处理单元CPU)同时运行同一程序的多个线程的能力,由操作系统支持。

例如,您可以启动多个便携式文档格式PDF)阅读器和 Qt Creator 的实例。Qt Creator 可以自行运行多个工具。您的系统任务管理器可以显示所有同时运行的进程。这被称为并发性。它也通常被称为多任务处理。

但是,如果您使用并行计算技术来处理数据,那么这被称为并行性。具有巨大数据处理需求的复杂应用程序使用这种技术。请注意,并行计算在单核处理器上是一种幻觉。

线程是进程的最小可执行单元。一个进程中可以有多个线程,但只有一个主线程。多线程是在同一进程内的并发。传统的单线程应用程序只使用一个核心。具有多个线程的程序可以分布到多个核心,从而实现真正的并发。因此,多线程应用程序在多核硬件上提供更好的性能。

让我们讨论 Qt 中提供并发和多线程的一些重要类,如下所示:

  • QThread用于在程序内管理一个线程控制。

  • QThreadPool用于管理和回收单个QThread对象,以帮助减少多线程应用程序中的线程创建成本。

  • QRunnable是一个表示需要执行的任务或代码片段的接口类。

  • QtConcurrent提供高级应用程序编程接口API),帮助编写多线程程序而不使用低级线程原语。

  • QFuture允许线程在稍后的时间点同步多个计算结果。

  • QFutureWatcher使用信号和槽提供有关QFuture对象的信息和通知。

  • QFutureSynchronizer是一个方便的类,简化了一个或多个QFuture对象的同步。

线程主要用于两种情况,如下所示:

  • 利用多核 CPU 加速处理

  • 将长时间运行的处理或阻塞调用卸载到其他线程,以保持图形用户界面GUI)线程或其他时间关键的线程的响应

让我们简要讨论一下最基本的并发概念,即QThread类在 Qt 中提供了一个线程抽象,并提供了方便的方法。您可以通过对QThread类进行子类化来启动一个新的自定义线程,如下所示:

class CustomThread : public QThread
{
    public:
    void run(){…}
};

您可以创建此类的新实例并调用其start()函数。这将创建一个新线程,然后在这个新线程的上下文中调用run()函数。另一种方法是直接创建一个QThread对象并调用start()函数,这将启动一个事件循环。与传统的 C++线程类相比,QThread支持线程中断,而 C++11 及以后的版本不支持。您可能会想知道为什么我们不能只使用 C++标准线程类。这是因为您可以以多线程安全的方式使用QThread的信号和槽机制。

您还可以在 QML 中使用多线程机制,使用WorkerScript。 JavaScript 代码可以使用WorkerScript QML 类型与 GUI 线程并行执行。要在 Qt Quick 应用程序中启用线程的使用,可以按照以下方式导入模块:

import QtQml.WorkerScript

每个WorkerScript对象可以附加一个 JavaScript。当调用WorkerScript.sendMessage()时,脚本将在不同的线程和 QML 上下文中运行。脚本完成后,它可以向 GUI 线程发送响应,调用WorkerScript.onMessage()信号处理程序。您可以使用信号和信号处理程序在线程之间交换数据。让我们看一个简单的WorkerScript用法,如下所示:

WorkerScript {
   id: messagingThread
   source: "messaging.mjs"
   onMessage: (messageObject)=> textElement.text = 
               messageObject.reply
}

上述代码片段使用了一个名为messaging.mjs的 JavaScript 文件,在新线程中执行操作。让我们看一下示例脚本,如下所示:

WorkerScript.onMessage = function(message) {
    //Perform complex operations here
    WorkerScript.sendMessage({ 'reply': 'Message '+
                              message})
}

您可以从按钮点击或基于某些用户操作发送消息。它将调用sendMessage(jsobject message)方法,其中将进行复杂的消息传递操作。您可以在以下链接了解有关不同线程机制和用例的更多信息:doc.qt.io/qt-6/threads-technologies.html

由于本书是为有经验的 C++开发人员编写的,因此预期您将熟悉诸如mutexsemaphoreread-write lock等术语。Qt 提供了方便的类来使用这些机制来实现多线程应用程序。我们不会深入研究这些 Qt 类及其示例。您可以在以下链接了解有关QMutexQSemaPhoreQReadWriteLockQWaitCondition的使用:doc.qt.io/qt-6/threads-synchronizing.html

在本节中,我们学习了如何使用并发机制来提高整体应用程序性能。不要为简单的任务不必要地实现它,因为这可能会导致性能下降。在下一节中,我们将讨论使用 QML Profiler 工具来对 Qt Quick 应用程序进行性能分析。

使用 QML Profiler 和 Flame Graph 对 Qt Quick 应用程序进行性能分析

Qt 6 中的 QML 利用图形处理单元GPU)并使用硬件加速进行渲染。这个特性使得 QML 在性能方面优于 Qt Widgets。然而,您的 QML 代码中可能存在性能瓶颈。在本节中,我们将专注于使用内置工具来找到这些瓶颈。Qt Creator 提供了与多个工具的无缝集成。最重要的工具是QML Profiler。它由 Qt 提供,并在所有 Qt 支持的平台上工作。除了 QML Profiler,Qt Creator 还提供第三方工具,如ValgrindHeobPerformance Analyzer。您可以在关于插件...(位于帮助菜单下)中启用新插件或删除一些插件。

让我们讨论一下 QML Profiler,这是您在大部分时间内用来找到 QML 代码中瓶颈的工具。QML Profiler 的目标是通过提供诸如代码块执行某个操作所需的时间等细节来帮助您识别瓶颈,之后您可以决定是否重新实现代码,使用合适的 GUI 元素或更好的数据结构或算法。

按照以下步骤开始分析和优化您的 Qt Quick 应用程序:

  1. 打开现有的 Qt Quick 项目或使用 Qt Creator 的新建项目创建向导创建新的 Qt Quick 应用程序。

  2. 项目创建后,向其中添加一些代码。然后,在分析菜单下选择QML Profiler运行 QML Profiler 工具。根据安装的插件,分析上下文菜单在不同平台上可能有所不同。以下截图显示了 Windows 平台上的QML Profiler选项。在 Linux 上,您可能会看到一些其他选项,例如Valgrind Memory AnalyzerValgrind Memory Analyzer with GDBValgrind Function Profiler图 12.1 - Qt Creator 集成开发环境(IDE)中的 QML Profiler 选项

图 12.1 - Qt Creator 集成开发环境(IDE)中的 QML Profiler 选项

  1. 当您点击QML Profiler选项时,您的 Qt Quick 应用程序将通过 QML Profiler 运行。您会看到QML Profiler窗口出现在代码编辑器下方。您可能还会看到以下消息:图 12.2 - QML Profiler 重试消息

图 12.2 - QML Profiler 重试消息

  1. 如果您看到此弹出窗口,只需点击重试。您会注意到分析将开始,并且您还会注意到输出屏幕。在示例应用程序中,我们在鼠标点击时创建新的矩形,如下截图所示:图 12.3 - 示例 Qt Quick 应用程序的输出

图 12.3 - 示例 Qt Quick 应用程序的输出

  1. 用户界面UI)上执行一些用户交互 - 例如点击按钮执行某个操作。然后,点击分析器窗口标题栏上的停止按钮。您还会在停止按钮的两侧看到另外两个按钮。如果将鼠标悬停在它们上面,您会看到它们的功能,例如开始 QML Profiler 分析禁用分析

以下截图显示了QML Profiler窗口的概述:

图 12.4 - 显示停止按钮和选项卡视图的 QML Profiler 窗口

图 12.4 - 显示停止按钮和选项卡视图的 QML Profiler 窗口

  1. 一旦您停止分析器,您会看到QML Profiler窗口已更新并显示了一些视图。您会注意到分析器窗口下有三个选项卡 - 分别是时间轴火焰图统计

  2. 让我们看看 QML Profiler 的第一个选项卡 - 点击时间轴选项卡。以下截图显示了输出的示例视图:图 12.5 - 显示时间轴详细信息的 QML Profiler

图 12.5 - 显示时间轴详细信息的 QML Profiler

您会注意到时间轴显示下有六个不同的部分:场景图内存使用编译创建绑定JavaScript。这些部分给我们提供了应用程序处理的不同阶段的概述,例如编译,组件创建和逻辑执行。

  1. 您可以在时间轴上找到彩色的条形图。您可以使用鼠标滚轮放大和缩小特定的时间轴部分。您还可以通过按住时间轴底部区域的鼠标左键并向任何方向移动来移动时间轴,以定位感兴趣的区域。

时间轴选项卡的不同部分如下截图所示:

图 12.6 - 显示不同部分的时间轴选项卡

图 12.6 - 显示不同部分的时间轴选项卡

  1. 您可以点击展开按钮以查看每个部分下的更多细节,如下图所示:图 12.7 - 时间轴选项卡显示场景图下不同子部分和分析选项

图 12.7 - 时间轴选项卡显示场景图下不同子部分和分析选项

  1. 如果您点击QtQuick/Rectangle类型下的柱状图之一,将会显示创建对象所花费的总时间以及代码位置的弹出窗口,覆盖QML Profiler窗口。您可以使用左上角的黄色箭头跳转到上一个或下一个事件。本节在下图中有所说明:图 12.8 - 创建部分下对象的详细信息

图 12.8 - 创建部分下对象的详细信息

  1. 您可以在QML Profiler窗口底部在不同选项卡之间切换。一旦您探索了时间轴选项卡,让我们打开火焰图选项卡。在此选项卡下,您将以百分比形式找到应用程序的总时间内存分配的可视化。您可以通过单击QML Profiler窗口右上角的下拉菜单在这些视图之间切换,如下图所示:图 12.9 - 火焰图显示分配视图

图 12.9 - 火焰图显示分配视图

  1. 火焰图视图提供了更紧凑的统计摘要。水平条形图显示了针对某个函数收集的样本的一个方面,与所有样本组合的相同方面进行比较。嵌套表示呈现了一个调用树,例如显示了哪些函数调用了其他函数。

  2. 如下截图所示,您还可以在代码编辑器的左侧看到显示的百分比值。根据哪个组件消耗了更多时间,您可以调整您的代码:图 12.10 - QML Profiler 显示代码特定部分所花费的时间百分比

图 12.10 - QML Profiler 显示代码特定部分所花费的时间百分比

  1. 由于数据收集需要时间,您可能会注意到数据显示之前有一点延迟。当您点击启用分析按钮时,数据将传输到 QML Profiler,因此不要立即终止应用程序。

  2. 要禁用应用程序启动时自动开始数据收集,请选择禁用分析按钮。切换按钮时,数据收集将重新开始。

  3. 让我们转到下一个选项卡:QML Profiler窗口。该选项卡以表格结构显示了有关进程的统计详细信息。下图说明了我们示例代码的代码执行统计信息:图 12.11 - QML Profiler 显示代码执行的统计信息

图 12.11 - QML Profiler 显示代码执行的统计信息

  1. 您还可以通过分析菜单下的QML Profiler(附加到等待应用程序)将 QML Profiler 附加到外部启动的应用程序。选择该选项后,您将看到以下对话框:图 12.12 - QML Profiler 显示远程执行选项

图 12.12 - QML Profiler 显示远程执行选项

  1. 要保存收集的所有数据,请右键单击任何 QML Profiler 视图,并在上下文菜单中选择保存 QML 跟踪。您可以选择加载 QML 跟踪以查看保存的数据。您还可以将保存的数据发送给其他人进行审查,或加载他们保存的数据。

在本节中,我们讨论了 QML Profiler 中提供的不同选项。使用这个工具,您可以轻松找到导致性能问题的代码。更多详细信息请访问此链接:doc.qt.io/qtcreator/creator-qml-performance-monitor.html

在下一节中,我们将进一步讨论如何使用其他分析工具来优化您的 Qt 代码。

其他 Qt Creator 分析工具

在前面的部分,我们讨论了 QML Profiler,但您可能需要分析您的 C++和 Qt Widgets 代码。Qt Creator 提供了与一些著名分析工具的集成,以帮助您分析 Qt 应用程序。Qt Creator 附带的一些工具列在这里:

  • Heob

  • 性能分析器

  • Valgrind

  • Clang 工具:Clang-Tidy 和 Clazy

  • Cppcheck

  • Chrome 跟踪格式CTF)可视化器

在深入研究这些工具并熟悉它们的文档之前,让我们简要讨论一下。

要使用 Heob,您首先需要下载并安装它。使用 Heob 可以轻松检测缓冲区溢出和内存泄漏。它通过覆盖调用者进程的堆函数来工作。当发生缓冲区溢出时,会引发访问冲突,并记录违规代码和缓冲区分配的堆栈跟踪。当应用程序正常退出时,您将找到堆栈跟踪。它不需要重新编译或重新链接目标应用程序。

您可以在官方文档链接上阅读有关其用法的更多信息:doc.qt.io/qtcreator/creator-heob.html

您可以从SourceForge.net下载二进制文件,也可以从源代码构建。Heob 的源代码可以在以下链接找到:github.com/ssbssa/heob

Linux 性能分析器工具与 Qt Creator 集成,可用于分析 Linux 桌面或基于 Linux 的嵌入式系统上的应用程序的 CPU 和内存利用率。perf工具定期对应用程序的调用树进行快照,并使用 Linux 内核附带的实用程序将它们可视化为时间线视图或火焰图。您可以在 Linux 机器上从分析菜单下的性能分析器选项中启动它,如下截图所示:

图 12.13 - Qt Creator 显示性能分析器选项

图 12.13 - Qt Creator 显示性能分析器选项

请注意,使用perf实用程序,您将会得到一个等效的警告对话框,如下截图所示:

图 12.14 - Qt Creator 显示性能分析器警告对话框

图 12.14 - Qt Creator 显示性能分析器警告对话框

使用以下命令在您的 Ubuntu 机器上安装perf工具:

$sudo apt install linux-tools-common

如果您使用不同的 Linux 发行版,您可以使用相应的命令。perf可能会因特定的 Linux 内核而失败,并显示有关内核版本的警告。在这种情况下,请使用适当内核版本的以下命令:

$sudo apt install linux-tools-5.8.0-53-generic

完成perf设置后,您可以使用以下命令在命令提示符中查看预定义事件:

$perf list

接下来,启动 Qt Creator 并打开一个 Qt 项目。选择perf工具,并在 Qt Creator 中创建几秒钟后,可能会出现一个额外的辅助程序。处理延迟字段包含此延迟的估计值。数据收集将持续进行,直到您单击停止收集配置文件数据按钮或关闭应用程序。

您还可以从分析菜单下的性能分析器选项加载perf.data并分析应用程序,如下所示:

图 12.15 - 上下文菜单显示性能分析器选项

图 12.15 - 上下文菜单显示性能分析器选项

您可以在以下链接阅读有关性能分析器的更多用法:doc.qt.io/qtcreator/creator-cpu-usage-analyzer.html

在 macOS 上,有一个名为Instructions的等效工具;但是,这个工具没有与 Qt Creator 集成。您可以单独启动它,并查看时间分析器部分。

在 Linux 和 macOS 上,memcheck。您可以从分析器下拉选项中将其更改为callgrind

您可以在以下链接了解有关 Valgrind 的更多信息:doc.qt.io/qtcreator/creator-valgrind-overview.html

Qt Creator 中的下一个工具是Clang-TidyClazy...。这些工具可用于通过静态分析在您的 C++代码中定位问题。Clang-Tidy提供了常见编程错误的诊断和修复,如样式违规或接口误用。另一方面,Clazy突出显示与 Qt 相关的编译器错误,如浪费的内存分配和 API 使用,并建议重构活动以解决一些问题。Clang-Tidy 包括 Clang 静态分析器的功能。您无需单独设置 Clang 工具,因为它们已经与 Qt Creator 集成和分发。当您运行Clang-Tidy 和 Clazy...,如下面的屏幕截图所示,您将在分析器窗口下看到分析细节,并在代码编辑器下方的应用程序输出窗口中看到进度:

图 12.16 - 显示 Clang-Tidy 和 Clazy...选项的上下文菜单

图 12.16 - 显示 Clang-Tidy 和 Clazy...选项的上下文菜单

让我们在现有的 Qt 示例上运行该工具。在应用程序窗口中,您将看到分析正在运行,并在分析器窗口中,您将看到结果。

您可以在以下链接进一步探索文档:doc.qt.io/qtcreator/creator-clang-tools.html

Qt Creator 还包括另一个工具称为cppcheck。这个工具与 Qt Creator 有实验性的集成。您可以在帮助菜单下的关于插件...中启用它。您可以使用它来检测未定义的行为和危险的编码结构。该工具提供了检查警告、样式、性能、可移植性和信息的选项。

与 Qt Creator 集成的最后一个分析工具是CTF 可视化器。您可以与 QML Profiler 一起使用它。跟踪信息可能会让您进一步了解 QML Profiler 收集的数据。您可以找出为什么简单的绑定需要这么长时间,比如可能受到 C++代码或慢磁盘操作的影响。完整的堆栈跟踪可以用于从顶层 QML 或 JavaScript 跟踪到 C++,一直跟踪到内核区域。这使您能够评估应用程序的性能,并确定性能不佳是由 CPU 还是同一系统上的其他程序引起的。跟踪提供了关于系统正在做什么以及应用程序为何以不希望的方式行为的见解。要查看 Chrome 跟踪事件,请使用 CTF 可视化器。

您可以在以下链接了解有关 CTF 可视化器的更多信息:doc.qt.io/qtcreator/creator-ctf-visualizer.html

在本节中,我们讨论了 Qt Creator 中可用的不同分析工具。在下一节中,我们将进一步讨论如何优化和定位图形性能问题。

优化图形性能

我们在第八章中讨论了图形和动画,图形和动画。在本节中,我们将探讨影响图形和动画性能的因素。图形性能在任何应用程序中都是至关重要的。如果您的应用程序实现不佳,用户可能会在 UI 中看到闪烁,或者 UI 可能无法按预期更新。作为开发人员,您必须尽一切努力确保渲染引擎保持 60 帧每秒的刷新率。在每帧之间只有 16 毫秒,其中应该以 60 帧每秒的速度进行处理,这包括将绘制基元上传到图形硬件所需的处理。

为了避免图形性能出现任何故障,您应该尽可能使用异步、事件驱动的编程。如果您的应用程序具有巨大的数据处理需求和复杂的计算,则使用工作线程进行处理。您不应该手动旋转事件循环。不要在阻塞函数中每帧花费超过几毫秒的时间。如果不遵循这些要点,用户将看到 GUI 闪烁或冻结,导致糟糕的用户体验(UX)。在生成 UI 上的图形和动画方面,QML 引擎非常高效和强大。但是,您可以使用一些技巧使事情变得更快。不要编写自己的代码,而是利用 Qt 6 的内置功能。

在绘制图形时,如果可能的话,应选择不透明的基元。不透明的基元在渲染器上更快,也更快地绘制在 GPU 上。因此,在将照片传递给QQuickImageProvider时,应选择QImage::Format_RGB32。请注意,重叠的复合项无法进行批处理。尽量避免裁剪,因为它会破坏批处理。而是使用QQuickImageProvider裁剪图像。需要单色背景的应用程序应该使用QQuickWindow::setColor()而不是顶层的Rectangle元素。QQuickWindow::setColor()调用glClear(),速度更快。

在使用Image时,请使用sourceSize属性。sourceSize属性使 Qt 能够在将图像加载到内存之前将其缩小,确保巨大的图像消耗的内存不超过所需的量。当smooth属性设置为true时,Qt 会对图像进行滤波,使其在缩放或从原始大小更改时看起来更平滑。如果图像以与其sourceSize属性相同的大小呈现,则不会有任何区别。在一些较旧的硬件上,此属性将影响应用程序的性能。antialiasing属性指示 Qt 平滑处理图像边缘周围的混叠伪影。此属性将影响程序的性能。

通过有效的批处理可以实现更好的图形性能。渲染器可以提供有关批处理运行情况、使用了多少批处理、保留了哪些批处理、哪些是不透明的以及哪些不是的统计信息。要启用此功能,请添加一个环境变量,如QSG_RENDERER_DEBUG,并将值设置为render。除非图像太大,否则ImageBorderImage QML 类型将使用纹理图集。如果您正在使用 C++创建纹理,则调用QQuickWindow::createTexture()并传递QQuickWindow::TextureCanUseAtlas。您可以使用另一个环境变量QSG_ATLAS_OVERLAY来给图集纹理上色,这有助于轻松识别它们。

为了可视化场景图的默认渲染器的各个方面,可以将QSG_VISUALIZE环境变量设置为以下值之一。您可以在 Qt Creator 中通过转到QSG_VISUALIZE并设置该变量的值来执行此操作:

  • QSG_VISUALIZE = overdraw

  • QSG_VISUALIZE = batches

  • QSG_VISUALIZE = clip

  • QSG_VISUALIZE = changes

QSG_VISUALIZE设置为overdraw时,渲染器中会可视化过度绘制。为了突出过度绘制,所有元素都以Rectangle形式可视化,只是为了绘制白色背景,因为Window也有白色背景。在这种情况下,使用Item属性而不是Rectangle可以提高性能。

QSG_VISUALIZE设置为batches会导致批次在渲染器中可视化。未合并的批次以对角线图案绘制,而合并的批次以纯色绘制。少量不同的颜色表示有效的批处理。如果未合并的批次包含大量单独的节点,则是不理想的。

所有从Item派生的 QML 组件都有一个名为clip的属性。默认情况下,clip值设置为false。此属性通知场景图不要渲染超出其父级边界的任何子元素。当QSG_VISUALIZE设置为clip时,红点会出现在场景顶部以指示剪切。因为 Qt Quick Items默认不剪切,所以通常不会显示剪切。剪切会阻止将多个组件批处理在一起,这会影响图形性能。

QSG_VISUALIZE设置为changes时,渲染器中的更改会显示出来。随机颜色的闪烁叠加用于突出显示场景图中的更改。对基元的修改显示为纯色,但对祖先的更改(例如对矩阵或不透明度的更改)则显示为图案。

在您的 Qt Quick 应用程序中尝试这些环境变量。您可以在以下链接了解有关这些渲染标志的更多信息:doc.qt.io/qt-6/qtquick-visualcanvas-scenegraph-renderer.html

Qt Quick 有助于构建具有流畅 UI 和动态过渡的出色应用程序。但是,您应该考虑一些因素以避免性能影响。当您向属性添加动画时,所有绑定都会受到影响并重新评估,这会引用该属性。为了避免性能问题,您可以在运行动画之前删除绑定,然后在动画完成后重新分配它。在动画期间,避免使用 JavaScript。应谨慎使用脚本动画,因为它们在主线程中运行。

您可以使用 Qt Quick 粒子创建漂亮的粒子效果。但是,其性能取决于底层硬件能力。要渲染更多粒子,您需要更快的图形硬件。您的图形硬件应能够以 60 FPS 或更高的速度绘制。您可以在以下链接了解有关优化粒子性能的更多信息:doc.qt.io/qt-6/qtquick-particles-performance.html

在本节中,我们讨论了优化图形性能的不同考虑因素。在下一节中,我们将进一步讨论如何对应用程序进行基准测试。

创建基准测试

我们已经在第九章中学习了基准测试,测试和调试。让我们看一下基准测试的一些方面,以评估性能问题。我们已经讨论了 Qt Test 对基准测试的支持,这是对特定任务所需的平均时间的计算。QBENCHMARK宏用于对函数进行基准测试。

以下代码片段显示了对行编辑中关键点击的基准测试:

void LineEditTest::testClicks()
{
    auto tstLineEdit = ui->lineEdit;
    QBENCHMARK {QTest::keyClicks(tstLineEdit, "Some 
                Inputs");}
}

您还可以对 Qt 提供的便利函数进行基准测试。以下代码对QString::localeAwareCompare()函数进行基准测试。让我们看一下这里的示例代码:

void TestQStringBenchmark::simpleBenchmark()
{
    QString string1 = QLatin1String("Test string");
    QString string2 = QLatin1String("Test string");
    QBENCHMARK {string1.localeAwareCompare(string2);}
}

您还可以在 QML 中运行基准测试。Qt 基准测试框架将以benchmark_开头的函数运行多次,并记录运行的平均时间值。这类似于 C++版本的QTestLib中的QBENCHMARK宏。您可以在测试函数名称前加上benchmark_once_以获得QBENCHMARK_ONCE宏的效果。

您还可以使用BenchmarkCreationBenchmark。它还允许您执行自动化和手动基准测试。自动化测试可用于回归测试,而手动测试可用于了解新硬件的功能。它具有内置功能,如 FPS 计数器,对于 GUI 应用程序非常重要。您可以通过运行以下命令找到帧速率:

>qmlbench --shell frame-count

您还可以通过简单的命令运行所有自动化测试,如下所示:

>qmlbench benchmarks/auto/

要了解有关该工具的更多信息并查看示例,请参阅以下链接:github.com/qt-labs/qmlbench

我们已经看到了在 Qt Widgets 和 QML 中基准测试对象创建,我们还对 Qt 函数进行了基准测试。您还可以在不使用任何宏的情况下进行分析。您可以简单地使用QTimeQElapsedTimer来测量代码或函数部分所花费的时间,如下面的代码片段所示:

QTime* time = new QTime;
time->start();
int lastElapsedTime = 0;
qDebug()<<"Start:"<<(time->elapsed()-
        lastElapsedTime)<<"msec";
//Do some operation or call a function
qDebug()<<"End:"<<(time->elapsed()-
        lastElapsedTime)<<"msec";

在前面的代码片段中,我们使用了elapsed()来测量代码段所花费的时间。不同之处在于您可以在函数内评估几行代码,而不必编写单独的测试项目。这是一种快速找到性能问题的方法,而不必评估整个项目。

您还可以对您的 Qt Quick 3D 应用程序进行基准测试。以下是如何执行的文章:www.qt.io/blog/introducing-qtquick3d-benchmarking-application

在本节中,我们讨论了基准测试技术。在下一节中,我们将讨论更多的分析工具。

不同的分析工具和优化策略

您可以在代码级别以外的多个级别优化您的应用程序。优化也可以在内存或二进制上进行。您可以修改应用程序以使其更有效地使用更少的资源。但是,内存和性能之间可能存在权衡。根据您的硬件配置,您可以决定内存使用或处理时间哪个更重要。在一些具有内存限制的嵌入式平台上,您可以允许处理时间稍长一些,以使用更少的内存并保持应用程序的响应性。您还可以将优化任务的一部分委托给编译器。

让我们看看我们可以使用哪些不同的策略来构建、分析和部署更快。

内存分析和分析工具

在本节中,我们将讨论一些额外的工具,您可以使用这些工具来分析您的应用程序。请注意,我们不会详细讨论这些工具。您可以访问各自的工具网站,并从其文档中学习。除了 Qt Creator 中提供的工具之外,您还可以在 Windows 机器上使用以下工具。

让我们看看工具列表,如下所示:

  • AddressSanitizerASan)是由 Google 开发的地址监视工具,是 Sanitizers 的一部分。

  • AQTime Pro通过应用程序运行时分析和性能分析找到问题和内存泄漏。

  • Deleaker是一个面向 C++开发人员的工具,他们希望在其项目中找到所有可能的已知泄漏。它可以检测内存泄漏、图形设备接口GDI)泄漏和其他泄漏。

  • Intel Inspector XE是英特尔的内存和线程调试器。

  • PurifyPlus是一个运行时分析工具套件,可在程序运行时监视您的程序并报告其行为的关键方面。

  • Visual Leak Detector是 Visual C++的免费、强大、开源的内存泄漏检测系统。

  • Very Sleepy是基于采样的 CPU 分析器。

  • Visual Studio ProfilerVSTS)可用于 CPU 采样、插装和内存分配。

  • MTuner采用一种新颖的方法进行内存分析和分析,保留了整个基于时间的内存操作历史记录。

  • Memory Leak Detection Tool是一个高性能的内存泄漏检测工具。

  • Heob可以检测缓冲区溢出和内存泄漏。集成到 Qt Creator 中。

  • Process Explorer可以查询和可视化每个进程的多个系统和性能计数器,我经常用它进行初步调查。

  • System Explorer显示任何运行进程发出的所有系统调用的长列表,并支持选择我们想要观察的进程的过滤器。

  • RAMMap检查系统的全局内存使用情况,这需要相当多的 Windows 内部知识。

  • VMMap显示单个应用程序的内存使用的详细信息。

  • Coreinfo提供有关处理器的详细信息,这是您在进行低级优化工作时可能需要的信息。

  • Bloaty对二进制文件进行深入分析。它旨在准确地将二进制文件的每个字节归因于产生它的符号或编译单元。

在本节中,我们向您简要介绍了一些第三方分析工具。在下一节中,我们将讨论如何在链接期间优化二进制文件。

链接期间进行优化

在前面的部分中,我们讨论了如何找到瓶颈并优化影响应用程序性能的代码段。幸运的是,大多数编译器现在都包括一种机制,允许您在保持代码的模块化和清晰性的同时进行这些优化。这被称为链接时代码生成LTCG)或链接时优化LTO)。LTO 是在链接过程中对程序进行优化。链接器收集所有目标文件并将它们集成到一个程序中。由于链接器可以查看整个程序,它可以进行整个程序的分析和优化。然而,链接器通常只在程序被转换为机器代码之后才能看到程序。我们不是将每个源文件逐个转换为机器代码,而是将代码生成过程推迟到最后—链接时间。在链接时间进行代码生成不仅可以智能地内联代码,还可以进行诸如取消虚拟函数和更好地消除冗余代码等优化。这种技术可用于改善应用程序的启动时间。

要在 Qt 中启用此机制,您必须从源代码构建。在配置步骤中,将-ltcg添加到命令行选项。在编译阶段一次性编译所有源代码将为您提供完整 LTO 的所有优化好处。您可以在工具链、平台和应用程序级别优化应用程序的启动时间。

在以下链接了解更多关于这些性能提示:wiki.qt.io/Performance_Tip_Startup_Time

有时您可以将优化任务委托给编译器。当您启用优化标志时,编译器将尝试提高性能并优化代码块,代价是编译时间和—可能—调试能力。您可以为所需的编译器启用编译器级优化标志,如GNU 编译器集合GCC)或 Clang。

查看 GCC 优化选项,可用 C++编译器的链接:gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

您可以在以下链接了解 Clang 中的不同标志:clang.llvm.org/docs/CommandGuide/clang.html

在本节中,您了解了链接时优化。在下一节中,我们将讨论如何更快地构建您的 Qt 应用程序。

更快地构建 Qt 应用程序

在大型复杂项目中,构建项目所花费的时间越来越有价值。一般来说,构建时间越长,每天失去的时间就越多。如果将这个时间乘以一个完整团队的时间,你就会花费很多时间等待构建完成。虽然不得不等待每个小改动重新构建数小时可能会使您更加注意细节,并迫使您深入思考每一步,但它也可能限制更敏捷的流程或协作。在本节中,我们将提供一个简短的指南,介绍如何使用 Qt 处理 C++中的优化。

请注意以下几点,以加快构建过程:

  • 使用并行构建标志

  • 利用预编译头(pch

  • 从 makefile 中删除冗余的目标

  • 在类中使用前向声明

在构建大型项目时,最有效的方法是使用并行构建方法。可以通过传递额外的参数来启用并行构建。在 Qt Creator 中,您可以启用-j8。您可以通过以下命令行语句指示编译器以并行方式构建:

>make -j8

最后的数字取决于您的硬件。-j8指示并行运行八个线程。根据您的机器配置,您可以使用-j4

您还可以为-MP标志启用并行构建。您可以通过在.pro文件中添加以下标志来指示cl并行运行:

*msvc* {
    QMAKE_CXXFLAGS += -MP
}

预编译头是一种极大地减少编译器负担的优秀技术。当编译器解析文件时,它必须解析整个代码,包括标准头文件和其他第三方源。pch允许您定义哪些文件经常使用,以便编译器在开始构建之前对它们进行预编译,并在构建每个.cpp文件时利用结果。

要使用预编译头文件,请将以下代码添加到.pro文件中:

PRECOMPILED_HEADER = ../pch/your_precompiled_header.h
CONFIG += precompile_header

如果您使用Q_OBJECT宏,元对象编译器会生成额外的文件。除非您需要相关功能,如信号和槽机制或翻译,否则不要不必要地使用Q_OBJECT宏。当您添加Q_OBJECT宏时,moc将生成一个moc_<ClassName>.cpp文件,这会增加编译的复杂性。

您可以在.cpp文件的末尾包含此文件,如下所示:

#include "moc_<ClassName>.cpp"

您还可以通过在小型项目中使用前向声明和在大型项目中使用前向头文件来降低每个.cpp文件的依赖关系。前向类将缩短标准工作期间部分构建的持续时间。大多数类可以在forwards.h文件中包含前向声明。通过拥有这样的文件,您可以大大减少头文件中的包含数量,通常是包含forwards.h

因此,qmake会注意到这一点,并将此文件从目标列表中删除。这将减少编译器的负担。

在本节中,您学会了如何减少应用程序的构建时间。在下一节中,我们将讨论基于 Qt Widgets 的应用程序中的一些最佳实践。

Qt Widgets 的性能考虑

Qt Widgets 模块利用光栅引擎渲染小部件,这是一种使用 CPU 而不是 GPU 的软件渲染。在大多数情况下,它可以提供所需的性能。但是,Qt Widgets 模块非常古老,缺乏最新的功能。由于 QML 完全是硬件加速的,您应该考虑在应用程序的 UI 中采用它。

如果您的小部件不需要mouseTrackingtabletTracking或类似的事件捕获,请关闭它。由于此跟踪,您的应用程序将使用更多的 CPU 时间。保持较小的样式表,并将其全部放在一个样式表中,而不是应用于单个小部件。大型样式表将需要更长的时间才能将信息处理到渲染系统中,这可能会影响应用程序的性能。使用自定义样式而不是样式表,因为这可以为您提供更好的性能。

不要不必要地创建屏幕并将其隐藏。只有在需要时才创建屏幕。在使用QStackedWidget时,避免添加太多页面并用许多小部件填充它们。这需要 Qt 在渲染和事件处理阶段递归地发现它们,导致程序运行缓慢。

在可能的情况下,使用异步方法进行大型操作,以避免阻塞主进程,并保持软件平稳运行。多线程对于在事件循环中并行化多个进程非常有用。但是,如果不正确地执行,例如通过重复创建和删除线程或实现不良的线程间通信,可能会导致不良结果。

不同的 C++容器产生不同的速度。Qt 的向量容器比 STL 中的向量容器稍慢。总的来说,旧的 C++数组仍然是最快的,但它缺乏排序能力。使用最适合您需求的内容。

在本节中,您学习了在使用 Qt Widgets 模块时的最佳实践。在下一节中,我们将讨论 QML 的最佳实践。

学习 QML 编码的最佳实践

在 QML 编码时遵循某些最佳实践非常重要。您应该保持文件在一定的行限制内,并且应该具有一致的缩进和结构属性,以及遵循标准的命名约定。

您可以按照以下顺序结构化您的 QML 对象属性:

Rectangle {
// id of the object
// property declarations
// signal declarations
// javascript functions
// object properties
// child objects
// states
// transitions
}

如果您正在使用一组属性中的多个属性,则使用组表示法,如下所示:

Rectangle {
    anchors { 
        left: parent.left; top: parent.top
        right: parent.right; leftMargin: 20
    }
}

将一组属性视为一个块可以减少混乱,并有助于将属性与其他属性相关联。

QML 和 JavaScript 不像 C++那样强制执行私有属性。有必要隐藏这些私有属性,例如当属性是实现的一部分时。为了在 QML 项中有效地获得私有属性,您可以将其嵌入到QtObject{...}中以隐藏这些属性。这可以防止在 QML 文件和 JavaScript 之外访问这些属性。为了最小化对性能的影响,尝试将所有私有属性分组到同一个QtObject范围中。

以下代码片段说明了如何使用QtObject

Item {
    id: component
    width: 40; height: 40
    QtObject {
        id: privateObject
        property real area: width * height //private 
                                           //property
    }
}

属性解析需要时间。虽然查找的结果有时可以被缓存和重复使用,但通常最好避免额外的工作。您应该尝试在循环中只使用共同的基础一次。

如果任何属性发生变化,属性绑定表达式将被重新评估。如果您有一个循环,在其中进行一些处理,但只有结果很重要,那么最好创建一个临时累加器,然后将其赋值给要更新的属性,而不是逐步更新属性本身,以防止触发绑定表达式的重新评估。

为了避免因为它们是不可见元素的子元素而持续产生开销,它们应该在需要时进行延迟初始化,并在不再使用时进行销毁。使用Loader元素加载的对象可以通过重置LoadersourcesourceComponent属性来释放,但其他项目可以显式销毁。在某些情况下可能需要保持项目处于活动状态,在这种情况下应该使其不可见。

一般来说,不透明内容的绘制速度比半透明内容快得多。原因在于半透明内容需要混合,而渲染器可能能够更好地优化不透明内容。即使一幅图像只有一个半透明像素,它也被视为完全透明。对于具有半透明边缘的BorderImage元素也可以这样说。

避免在 QML 中进行长时间的逻辑计算。使用 C++来实现业务逻辑。如果仍然需要使用基于 JavaScript 的实现来执行一些复杂的操作或处理,则使用WorkerScript

Qt Quick 编译器允许您将 QML 源代码编译成最终的二进制文件。通过启用这一功能,可以大大减少应用程序的启动时间。您不必将.qml文件与应用程序一起部署。您可以通过将以下行添加到 Qt 项目(.pro)文件中来启用 Qt Quick 编译器:

CONFIG += qtquickcompiler

要了解更多关于 Qt Quick 最佳实践的信息,请阅读以下链接的文档:doc.qt.io/qt-6/qtquick-bestpractices.html

您还可以在以下链接找到的文档中了解更多关于 Qt Quick 性能的信息:doc.qt.io/qt-6/qtquick-performance.html

在本节中,我们学习了在 QML 编码时的一些最佳实践。现在我们将在本章总结我们的学习。

总结

在本章中,我们讨论了性能考虑因素以及如何提高整体应用程序性能。我们从改进 C++代码开始。然后,我们解释了并发技术如何帮助加快应用程序速度。您了解了 QML Profiler 和其他性能分析工具的重要性。您还了解了在 Qt 编码时使用最佳实践的重要性。现在,您可以在日常编码中使用这些技术。您不必成为非凡的应用程序开发人员来进行性能优化。如果您遵循最佳实践、设计模式并编写更好的算法,那么您的应用程序将有更少的缺陷和更少的客户投诉。这是一个持续的过程,您将逐渐变得更好。

恭喜!您已经学会了性能优化的基础知识。如果您想了解更多,可以阅读专门为性能调优编写的更多书籍。在 Qt 中愉快地编码。记住,编写更好和高性能的代码可以减少 CPU 周期,从而减少碳足迹,因此,如果您编写更好的代码,您可以拯救地球,抵抗气候变化!

posted @ 2024-05-15 15:26  绝不原创的飞龙  阅读(107)  评论(0编辑  收藏  举报