C++-Qt5-GUI-编程(全)

C++ Qt5 GUI 编程(全)

原文:annas-archive.org/md5/63069ff6b9b588d5c75e8d5b8dbfb5ed

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Qt 5 是 Qt 的最新版本,它使您能够为多个目标开发具有复杂用户界面的应用程序。它为您提供了更快速、更智能的方式来创建现代 UI 和多平台应用程序。本书将教您如何设计和构建功能齐全、吸引人和用户友好的图形用户界面。

通过本书,您将成功学习高端 GUI 应用程序,并能够构建更多功能强大的跨平台应用程序。

本书适合对象

本书适合希望构建基于 GUI 的应用程序的开发人员和程序员。需要基本的 C++知识,了解 Qt 的基础知识会有所帮助。

充分利用本书

为了成功执行本书中的所有代码和指令,您需要以下内容:

  • 基本的 PC/笔记本电脑

  • 工作的互联网连接

  • Qt 5.10

  • MariaDB 10.2(或 MySQL Connector)

  • Filezilla Server 0.9

我们将在每一章中处理安装过程和详细信息。

下载示例代码文件

您可以从www.packtpub.com的帐户中为本书下载示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,文件将直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packtpub.com

  2. 选择“支持”选项卡。

  3. 点击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

文件下载后,请确保使用最新版本的解压缩或提取文件夹:

  • WinRAR/Windows 7-Zip

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-GUI-Programming-with-CPP-and-Qt5。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在此处下载:www.packtpub.com/sites/default/files/downloads/HandsOnGUIProgrammingwithCPPandQt5_ColorImages.pdf

使用的约定

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

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。这是一个例子:“我们在MainWindow构造函数中调用test()函数。”

代码块设置如下:

void MainWindow::test() 
{ 
   int amount = 100; 
   amount -= 10; 
   qDebug() << "You have obtained" << amount << "apples!"; 
} 

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

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 
   test(); 
} 

任何命令行输入或输出都以以下方式编写:

********* Start testing of MainWindow ********* 
Config: Using QtTest library 5.9.1, Qt 5.9.1 (i386-little_endian-ilp32 shared (dynamic) debug build; by GCC 5.3.0) 
PASS   : MainWindow::initTestCase() 
PASS   : MainWindow::_q_showIfNotHidden() 
PASS   : MainWindow::testString() 
PASS   : MainWindow::testGui() 
PASS   : MainWindow::cleanupTestCase() 
Totals: 5 passed, 0 failed, 0 skipped, 0 blacklisted, 880ms 
********* Finished testing of MainWindow ********* 

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这样的方式出现在文本中。这是一个例子:“第三个选项是切换书签,它允许您为自己设置书签。”

警告或重要说明会以这种方式出现。

提示和技巧会以这种方式出现。

第一章:介绍 Qt

Qt(发音为可爱)自从首次发布以来,已经被软件工程师和开发人员使用了二十多年,用于创建跨平台应用程序。经过多次所有权变更和大量的重大代码改进,Qt 变得更加功能丰富,支持的平台也比以前更多。Qt 不仅在桌面应用程序开发方面表现出色,而且在移动和嵌入式系统开发方面也非常出色。

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

  • 什么是 Qt?

  • 为什么使用 Qt?

  • 在 Qt 中使用工具

  • 下载和安装 Qt

  • 建立工作环境

  • 运行我们的第一个Hello WorldQt 程序

在本章中,我们将更多地了解 Qt 的历史。然后,我们将继续使用 Qt 的最新版本构建我们的第一个示例程序,该版本是 Qt 5 版。为了方便我们的读者,我们将在整本书中简称为 Qt。

什么是 Qt?

目前,Qt 的最新版本(本书撰写时)是版本 5.10。这个版本包含了许多新功能以及成千上万的错误修复,使 Qt 成为软件开发人员和系统工程师的强大稳定的开发工具包。Qt 有一个庞大的 SDK(软件开发工具包),包含了各种工具和库,帮助开发人员完成工作,而不用太担心特定平台的技术问题。Qt 在幕后处理所有混乱的集成和兼容性问题,这样你就不必处理它们。这不仅提高了效率,还降低了开发成本,特别是当您尝试开发迎合更广泛用户群的跨平台应用程序时。

Qt 有两种许可证:

  • 第一种是开源许可证,免费,但只有在您的项目/产品符合其条款和条件时才免费。例如,如果您对 Qt 的源代码进行了任何更改,您有义务将这些更改提交给 Qt 开发人员。不这样做可能会导致严重的法律问题,因此您可能希望选择第二个选项。

  • 第二种许可证是商业许可证,它给予您对专有 Qt 源代码修改的全部权利,并保持您的应用程序私有。但当然,这些特权是需要付费的。

如果你刚开始学习 Qt,不要被这些术语吓倒,因为你肯定不会修改 Qt 库的源代码,也不会重新编译它,至少现在不会。

有关 Qt 许可的更多信息,请访问www.qt.io/licensing-comparison.

为什么使用 Qt?

不难看出为什么 Qt 有机会在市场上击败所有其他现有的 SDK;首先是跨平台兼容性。几乎找不到其他开发工具包支持这么多平台而不需要为每个平台编写不同的代码。通过消除这些额外的步骤,程序员可以专注于开发他们的应用程序,而不需要担心每个平台特定功能的实现。此外,您的代码将看起来干净,没有所有的#ifdef宏和需要为不同平台加载不同的依赖项。

Qt 通常使用 C++,这是一种生成小型高效代码的编译语言。它也有很好的文档,并遵循一套非常一致的命名约定,这减少了开发人员的学习曲线。

请注意,Qt 确实包含一小部分仅适用于特定平台的功能。但是,这些功能很少,通常用于特殊用例,例如仅在移动平台上工作的 Qt 传感器,仅在桌面上工作的 Qt Web Engine,仅适用于 Android 和 Linux 的 Qt NFC 等。这些都是一些非常特定的功能,只存在于支持它们的特定平台上。除此之外,通常所有平台都支持常见功能。

Qt Designer

Qt Designer 通常由开发人员用于设计桌面应用程序的 GUI,而 Qt Quick Designer 通常用于移动和嵌入式平台。话虽如此,两种格式在桌面和移动格式上都可以正常运行,唯一的区别是外观和所使用的语言类型。

Qt Designer 保存的 GUI 文件具有.ui扩展名,保存为 XML 格式。该文件存储了 GUI 设计人员放置的每个小部件的属性,例如位置、大小、边距、工具提示、布局方向等。它还在文件内部保存了信号和槽事件名称,以便在后期轻松连接代码。该格式不支持编码,仅适用于 Qt C++项目,即基于小部件的应用程序项目。

Qt Quick Designer

另一方面,Qt Quick Designer 以.ui.qml.qml格式保存 GUI 文件。从技术概念和开发方法来看,Qt Quick 是一种非常不同的 GUI 系统,我们将在第十四章《Qt Quick 和 QML》中进行介绍。Qt Quick Designer 保存其数据的格式不是 XML,而是一种类似 JavaScript 的声明性语言称为QML。QML 不仅允许设计人员以类似于 CSS(层叠样式表)的方式自定义他们的 GUI,还允许程序员在 QML 文件中编写功能性 JavaScript。正如我们之前提到的,.ui.qml是仅用于视觉装饰的文件格式,而.qml包含应用程序逻辑。

如果您正在使用 Qt Quick 编写简单的程序,您根本不需要接触任何 C++编码。这对 Web 开发人员来说尤其受欢迎,因为他们可以立即开始使用 Qt Quick 开发自己的应用程序,无需经历陡峭的学习曲线;一切对他们来说都是如此熟悉。对于更复杂的软件,您甚至可以在 QML 中链接 C++函数,反之亦然。同样,如果您对 Qt Quick 和 QML 想了解更多信息,请转到第十四章《QtQuick 和 QML》。

由于 Qt Creator 本身也是用 Qt 库编写的,因此它也是完全跨平台的。因此,您可以在不同的开发环境中使用相同的一组工具,并为您的团队开发统一的工作流程,从而提高效率和节约成本。

除此之外,Qt 还配备了许多不同的模块和插件,涵盖了您项目所需的各种功能。通常情况下,您无需寻找其他外部库或依赖项并尝试自行实现它们。Qt 的抽象层使后端实现对用户不可见,并导致统一的编码风格和语法。如果您尝试自行组合一堆外部依赖项,您会发现每个库都有其独特的编码风格。在同一项目中混合所有不同的编码风格会非常混乱,除非您制作自己的抽象层,这是一项非常耗时的任务。由于 Qt 已经包含了大多数(如果不是全部)您需要创建功能丰富的应用程序的模块,因此您无需自行实现。

有关 Qt 附带的模块的更多信息,请访问:doc.qt.io/qt-5/qtmodules.html

也就是说,还有许多第三方库可以扩展 Qt,以实现 Qt 本身不支持的功能,例如专注于游戏开发或为特定用户群设计的其他功能的库。

下载和安装 Qt

不浪费任何时间,让我们开始安装吧!要获取开源 Qt 的免费安装程序,首先转到他们的网站www.qt.io。在那里,寻找一个名为 Download Qt 的按钮(如果他们已经更新了网站,网站可能看起来不同)。请注意,您可能正在下载商业 Qt 的免费试用版,在 30 天后将无法使用。确保您下载的是开源版本的 Qt。此外,您可能需要为您的平台选择正确的安装程序,因为 Qt 有许多不同的安装程序,适用于不同的操作系统 Windows、macOS 和 Linux。

您可能会想知道为什么安装程序的大小如此之小-只有大约 19 MB。这是因为统一的在线安装程序实际上不包含任何 Qt 软件包,而是一个下载客户端,它可以帮助您下载所有相关文件,并在下载完成后将它们安装到您的计算机上。一旦您下载了在线安装程序,请双击它,您将看到一个类似于这样的界面(以下示例在 Windows 系统上运行):

单击“下一步”按钮,将出现一个 DRM(数字版权管理)页面,并要求您使用 Qt 帐户登录。如果您没有帐户,您也可以在同一页面上创建您的帐户:

一旦您登录,您将看到一条消息,上面写着您的 Qt 帐户在此主机平台上没有有效的商业许可证。不用担心,只需单击“下一步”按钮即可继续。

接下来,您将被要求指定安装路径。默认路径通常就可以了,但您可以根据需要将其更改为任何其他路径。此外,您可以选择保留与 Qt Creator 关联这些常见文件类型的选项,或者如果不需要,也可以手动取消选中。

之后,您将看到一系列复选框,您可以使用这些复选框选择要安装到计算机上的 Qt 版本。通常,对于新用户,默认选项就足够了。如果您不需要某些选项,例如对 Android 上的 Qt 的支持,您可以在此处取消选择它们,以减小下载的大小。如果需要,您随时可以使用维护工具返回并添加或删除 Qt 组件:

接下来,您将看到许可协议。勾选第一个选项,即我已阅读并同意许可协议中包含的条款,然后单击“下一步”按钮。确保您确实阅读了许可协议中规定的条款和条件!

最后,安装程序将要求您输入一个名称,以创建 Qt 的开始菜单快捷方式。完成后,只需单击“下一步”,然后单击“安装”。下载过程将根据您的互联网速度花费几分钟到几个小时不等。一旦所有文件都已下载,安装程序将自动继续将文件安装到您刚刚在之前的步骤中设置的安装路径。

设置工作环境

既然您已经安装了最新版本的 Qt,让我们启动 Qt Creator,并开始通过创建我们的第一个项目来进行实验!您应该能够在桌面上或开始菜单的某个位置找到 Qt Creator 的快捷方式图标。

让我们看看设置环境的步骤:

  1. 当您首次启动 Qt Creator 时,您应该会看到以下界面:

  1. 在开始创建第一个项目之前,您可能需要调整一些设置。转到顶部菜单,选择“工具”|“选项”。屏幕上将弹出一个类似于此的窗口:

  1. 窗口左侧有许多不同的类别可供选择。每个类别代表一组选项,您可以设置以自定义 Qt Creator 的外观和操作方式。您可能不想触碰设置,但最好先了解它们。您可能想要更改的第一个设置之一是语言选项,该选项位于环境类别中。Qt Creator 为我们提供了在不同语言之间切换的选项。虽然它不支持所有语言,但大多数流行的语言都可用,例如英语、法语、德语、日语、中文、俄语等。选择所需的语言后,单击应用并重新启动 Qt Creator。您必须重新启动 Qt Creator 才能看到更改。

  2. 您可能需要的下一个设置是缩进设置。默认情况下,Qt 使用空格缩进,每当您在键盘上按“Tab”键时,将向您的脚本添加四个空格。像我这样的一些人更喜欢制表符缩进。您可以在 C++类别中更改缩进设置。

请注意,如果您要为 Qt 项目的源代码做出贡献,则需要使用空格缩进,而不是制表符,这是 Qt 项目的编码标准和样式。

  1. 在 C++类别下,您可以找到一个名为“复制”的按钮,位于右上方的“编辑”按钮旁边。单击它,将弹出一个新窗口。

  2. 输入您自己的代码样式名称,因为您无法编辑默认的内置编码样式。创建自己的设置后,单击“编辑”按钮。现在您可以在“常规”选项卡下看到实际的“制表符和缩进”设置:

  1. 请注意,即使在“文本编辑器”类别中有一个名为“制表符和缩进”的设置,我认为这是一个旧设置,在 Qt Creator 中已不再起作用。界面上还有一条注释,写着代码缩进是在 C++和 Qt Quick 设置中配置的。这可能的原因是,由于 Qt Creator 现在同时支持 C++项目和 QML 项目,Qt 开发人员可能觉得有必要将设置分开,因此旧设置不再有效。我相当肯定,文本编辑器中的这一部分将在不久的将来被弃用。

  2. 接下来,在“构建和运行”类别下,您将看到一个名为“工具包”的选项卡。

  3. 这是您可以为每个平台设置编译设置的地方。从下一个截图中可以看出,我的 Qt 不支持在 MSVC(Microsoft Visual Studio Compiler)下进行桌面构建,因为我从未在计算机上安装 Visual Studio。相反,我的 Qt 只支持在 MinGW(Minimal GNU for Windows)编译器下进行桌面构建。从此窗口,您可以检查并查看您的 Qt 是否支持您项目所需的平台和编译器,并在必要时进行更改。但是现在,我们将保持不变。要了解有关工具包是什么以及如何配置构建设置的更多信息,请转到第十五章,跨平台开发

  1. 最后,我们可以将我们的项目链接到版本控制类别中的版本控制服务器。

  2. 版本控制允许您或您的团队将代码更改提交到集中系统,以便每个团队成员都可以获取相同的代码,而无需手动传递文件。当您在一个大团队中工作时,手动跟踪代码更改非常困难,甚至更难合并不同程序员完成的代码。版本控制系统旨在解决这些问题。Qt 支持不同类型的版本控制系统,如 Git、SVN、Mercurial、Perforce 等。尽管这是一个非常有用的功能,特别是在团队中工作时,但我们现在不需要为其进行配置:

运行我们的第一个 Hello World Qt 程序

Hello World 程序是一个非常简单的程序,它只是显示一个输出,上面写着Hello, World!(或者其他内容,不一定是这个),以显示 SDK 正常工作。我们不需要编写很长的代码来生成Hello World程序,我们可以只使用最少和最基本的代码来完成。实际上,在 Qt 中我们不需要编写任何代码,因为它会在您第一次创建项目时生成代码!

让我们按照以下步骤开始我们的项目:

  1. 要在 Qt 中创建新项目,请单击 Qt Creator 欢迎屏幕上的“新项目”按钮。或者,您也可以转到顶部菜单,选择“文件”|“新文件或项目”。

  2. 之后,您将看到一个窗口,让您为项目或文件选择模板。在这个演示中,我们将选择 Qt Widgets Application:

  1. 之后,设置您的项目名称和项目目录。您还可以勾选“用作默认项目位置”,这样下次在 Qt 中创建新项目时就可以自动获得相同的路径。

  2. 接下来,Qt Creator 将要求您为项目选择一个或多个工具包。在这个演示中,我们将选择使用 MinGW 编译器的桌面 Qt。不用担心,因为您可以在开发过程中随时添加或删除项目中的工具包:

  1. 之后,您将看到一个页面,上面写着“类信息”。这基本上是您为基本窗口设置类名的地方,但我们不打算更改任何内容,所以只需点击“下一步”按钮继续:

  1. 最后,它会要求您将项目链接到您的版本控制服务器。如果您以前没有在 Qt 中添加过任何内容,可以单击“Configure”按钮,它将带您进入我在本章前一节中向您展示的设置对话框。

  2. 但是,在这个演示中,我们将保持设置为并按下“Finish”按钮。然后,Qt Creator 将继续生成项目所需的文件。一两秒后,Qt Creator 将自动切换到编辑模式,您应该能够在项目面板下看到它为您创建的文件。您可以通过在 Qt Creator 中双击它们来打开任何文件,并且它们将显示在右侧的编辑器中:

  1. 在开始编译项目之前,让我们在项目面板的Forms目录下打开mainwindow.ui文件。不要太担心用户界面,因为我们将在下一章中介绍它。我们需要做的是在右侧窗口的中心点击并拖动“Display Widgets”类别下的“Label”图标,如下面的屏幕截图所示:

  1. 之后,双击Text Label小部件并将文本更改为Hello World!。完成后,按下键盘上的Enter按钮:

  1. 最后一步是按下位于左下角的运行按钮,看起来像这样:

  1. 通常情况下,我们会先构建程序,然后运行程序,但是 Qt Creator 足够聪明,可以自行构建它。然而,构建和运行应用程序分开仍然是一个好习惯。经过几秒钟的编译,...哇!你已经使用 Qt 创建了你的第一个Hello World程序!

摘要

诸如 Qt Creator 之类的工具的存在使得为开发人员设计应用程序的用户界面成为一项简单而有趣的工作。我们不再需要编写大量的代码来创建单个按钮,或者更改一大堆代码来调整文本标签的位置,因为当我们设计我们的 GUI 时,Qt Designer 会为我们生成那些代码。Qt 已经将所见即所得的哲学应用到了工作流程中,并为我们提供了完成工作所需的所有便利和效率。

在下一章中,我们将学习 Qt Creator 的方方面面,并开始使用 Qt 设计我们的第一个 GUI!

第二章:Qt 小部件和样式表

使用 Qt 进行软件开发的一个优势是,使用 Qt 提供的工具非常容易设计程序的图形用户界面GUI)。在本书中,我们将尝试创建一个涉及 Qt 许多不同组件和模块的单一项目。我们将在每一章中逐步介绍项目的每个部分,这样您最终将能够掌握整个 Qt 框架,并同时完成演示项目,这对于您的作品集来说是一个非常有价值的项目。您可以在github.com/PacktPublishing/Hands-On-GUI-Programming-with-C-QT5找到所有源代码。

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

  • Qt Designer 简介

  • 基本 Qt 小部件

  • Qt 样式表

在本章中,我们将深入探讨 Qt 在设计时如何为我们提供优雅的 GUI。在本章开头,您将了解 Qt 提供的小部件类型及其功能。之后,我们将逐步进行一系列步骤,并使用 Qt 设计我们的第一个表单应用程序。

Qt Designer 简介

Qt 中有两种类型的 GUI 应用程序,即 Qt Quick 应用程序和 Qt Widgets 应用程序。在本书中,我们将主要涵盖后者,因为这是为桌面应用程序设计 GUI 的标准方式,而 Qt Quick 更广泛地用于移动和嵌入式系统:

  1. 我们需要做的第一件事是打开 Qt Creator 并创建一个新项目。您可以通过转到“文件”|“新文件或项目”,或者点击欢迎屏幕上的“新项目”按钮来完成:

  1. 在那之后,将弹出一个新窗口,询问您要创建的项目类型。在“应用程序”类别下选择“Qt Widgets 应用程序”,然后点击“选择...”,接着,为您的项目创建一个名称(我选择了Chapter2),并通过点击“浏览...”按钮选择项目目录:

  1. 接下来,您将被要求为您的项目选择一个工具包。如果您在 Windows 系统上运行,并且已安装了 Microsoft Visual Studio,则可以选择具有 MSVC 编译器的相关工具包;否则,选择运行 MinGW 编译器的工具包。Qt 通常预装了 MinGW 编译器,因此您无需单独下载它。如果您在 Linux 系统上运行,那么您将看到 GCC 工具包,如果您在 macOS 上运行,那么您将看到 Clang 工具包。要了解更多关于工具包和构建设置的信息,请查看第十五章,跨平台开发

  1. 在那之后,新项目向导将要求您命名主窗口类。我们将使用默认设置,然后点击“下一步”按钮继续:

  1. 最后,您将被要求将您的版本控制工具链接到您的项目。通过将版本控制工具链接到您的项目,您将能够将代码的每个修订版本保存在远程服务器上,并跟踪对项目所做的所有更改。如果您是在团队中工作,这将特别有用。然而,在本教程中,我们将不使用任何版本控制,所以让我们继续点击“完成”按钮:

  1. 完成后,Qt Creator 将打开您的新项目,您将能够在左上角看到您的项目目录显示,如下所示:

  1. 现在,通过双击项目目录面板上的mainwindow.ui来打开它。然后,Qt Creator 将切换到另一种模式,称为 Qt Designer,这实质上是一个用于为程序设计基于小部件的 GUI 的工具。一旦激活 Qt Designer,您将在左侧面板上看到可用的小部件列表,并且在右侧设计 GUI 的位置。在开始学习如何设计我们自己的 UI 之前,让我们花点时间熟悉一下 Qt Designer 的界面:

以下数字代表前面截图中显示的 UI:

  1. 菜单栏:菜单栏是您找到 Qt Creator 的所有基本功能的地方,例如创建新项目,保存文件,更改编译器设置等。

  2. 小部件框:小部件框有点像工具箱,其中显示了 Qt Designer 提供的所有不同小部件,并准备好供使用。您可以从小部件框直接将任何小部件拖放到表单编辑器的画布上,它们将出现在您的程序中。

  3. 模式选择器:模式选择器是您可以通过单击编辑或设计按钮快速轻松地在源代码编辑或 UI 设计之间切换的地方。您还可以通过单击位于模式选择器面板上的相应按钮轻松导航到调试器和分析器工具。

  4. 构建快捷键:这里显示了三个不同的快捷按钮——构建、运行和调试。您可以通过按下这里的按钮轻松构建和测试运行应用程序,而不是在菜单栏上这样做。

  5. 表单编辑器:这是您应用创意并设计应用程序 UI 的地方。您可以从小部件框中拖放任何小部件到表单编辑器的画布上,以使其出现在您的程序中。

  6. 表单工具栏:表单工具栏是您可以快速选择要编辑的不同表单的地方。您可以通过单击位于小部件框上方的下拉框并选择要在 Qt Designer 中打开的 UI 文件来切换到不同的表单。还有一些按钮,允许您在表单编辑器和 UI 布局之间切换不同的模式。

  7. 对象检查器:这是当前.ui文件中所有小部件以分层方式列出的地方。小部件按照其与其他小部件的父子关系在树状列表中排列。通过在表单编辑器中移动它来轻松重新排列小部件的层次结构。

  8. 属性编辑器:当您从对象检查器窗口(或表单编辑器窗口)中选择一个小部件时,该特定小部件的属性将显示在属性编辑器上。您可以在这里更改任何属性,结果将立即显示在表单编辑器上。

  9. 动作编辑器和信号与槽编辑器:动作编辑器和信号与槽编辑器都位于此窗口中。您可以使用动作编辑器创建与菜单栏和工具栏按钮相关联的动作。信号和槽编辑器是您

  10. 输出窗格:输出窗格是您在测试应用程序时查找问题或调试信息的地方。它由几个窗口组成,显示不同的信息,例如问题、搜索结果、应用程序输出等。

简而言之,Qt 提供了一个名为 Qt Creator 的多合一编辑器。Qt Creator 与 Qt 附带的几种不同工具紧密配合,例如脚本编辑器、编译器、调试器、分析器和 UI 编辑器。您在上面的截图中看到的 UI 编辑器称为 Qt Designer。Qt Designer 是设计师设计其程序 UI 的完美工具,而无需编写任何代码。这是因为 Qt Designer 采用了所见即所得的方法,通过提供最终结果的准确视觉表示,意味着您在 Qt Designer 中设计的任何内容在编译和运行程序时都会完全相同。请注意,Qt 附带的每个工具实际上都可以单独运行,但如果您是初学者或只是做一个简单的项目,建议只使用 Qt Creator,它将所有这些工具连接在一个界面中。

基本的 Qt 小部件

现在,我们将看一下 Qt Designer 中默认的小部件集。实际上,您可以自己创建自定义小部件,但这是本书范围之外的高级主题。让我们来看看小部件框中列出的第一和第二类别——布局和间隔:

布局和间隔实际上并不是您可以直接观察到的东西,但它们可以影响小部件的位置和方向:

  1. 垂直布局:垂直布局小部件以垂直列从上到下布置小部件。

  2. 水平布局:水平布局小部件以水平行从左到右(或从右到左的从右到左语言)布置小部件。

  3. 网格布局:网格布局小部件以二维网格布局放置小部件。每个小部件可以占据多个单元格。

  4. 表单布局:表单布局小部件以两列字段样式放置小部件。正如其名称所示,这种类型的布局最适合输入小部件的表单。

Qt 提供的布局对于创建高质量的应用程序非常重要,而且非常强大。Qt 程序通常不使用固定位置来布置元素,因为布局允许对话框和窗口以合理的方式动态调整大小,同时处理不同语言中本地化的文本长度。如果您在 Qt 程序中不使用布局,其 UI 在不同计算机或设备上可能会看起来非常不同,这在大多数情况下会导致不愉快的用户体验。

接下来,让我们看看间隔小部件。间隔是一个不可见的小部件,它沿特定方向推动小部件,直到达到布局容器的限制。间隔必须在布局内使用,否则它们将不会产生任何效果。

有两种类型的间隔,即水平间隔和垂直间隔:

  1. 水平间隔:水平间隔小部件是一个占据布局内空间并将布局内其他小部件推动沿水平空间移动的小部件。

  2. 垂直间隔:垂直间隔与水平间隔类似,只是它将小部件沿垂直空间推动。

在没有实际使用它们的情况下,很难想象布局和间隔是如何工作的。不用担心,我们马上就会尝试它。Qt Designer 最强大的功能之一是您可以在每次更改后无需更改和编译代码即可实验和测试布局。

除了布局和间隔之外,还有几个类别,包括按钮、项目视图、容器、输入小部件和显示小部件。我不会解释它们中的每一个,因为它们的名称基本上是不言自明的。您也可以将小部件拖放到表单编辑器中以查看其功能。让我们来试一试:

  1. 从小部件框中将“推按钮”小部件拖放到表单编辑器中,如下截图所示:

  1. 然后,选择新添加的“推送按钮”小部件,你会看到与该特定小部件相关的所有信息现在都显示在属性编辑器面板上:

  1. 你可以在 C++代码中以编程方式更改小部件的属性,如外观、焦点策略、工具提示等。有些属性也可以直接在表单编辑器中进行编辑。让我们双击“推送按钮”并更改按钮的文本,然后通过拖动其边缘来调整按钮的大小:

  1. 完成后,让我们在表单编辑器中拖放一个水平布局。然后,将“推送按钮”拖放到新添加的布局中。你会看到按钮自动适应到布局中:

  1. 默认情况下,主窗口不具有任何布局效果,因此小部件将保持在它们最初放置的位置,即使窗口被调整大小,这看起来并不好。要为主窗口添加布局效果,在表单编辑器中右键单击窗口,选择“布局”,最后选择“垂直布局”。现在你会看到我们之前添加的水平布局小部件现在自动扩展以适应整个窗口。这是 Qt 中布局的正确行为:

  1. 接下来,我们可以玩一下间隔器,看看它有什么效果。我们将在包含“推送按钮”的布局顶部拖放一个垂直间隔器,然后在其布局内的按钮两侧放置两个水平间隔器:

间隔器将推动它们两端的所有小部件并占据空间。在这个例子中,“提交”按钮将始终保持在窗口底部并保持其中间位置,无论窗口的大小如何。这使得 GUI 在不同的屏幕尺寸上看起来很好。

自从我们在窗口中添加了间隔器以后,我们的“推送按钮”被挤压到了最小尺寸。通过将其minimumSize属性设置为 120 x 40 来放大按钮,你会看到按钮现在显得更大了:

  1. 之后,让我们在“推送按钮”的布局上方添加一个表单布局,并在其下方添加一个垂直间隔器。现在你会看到表单布局非常窄,因为它被我们之前放置在主窗口上的垂直间隔器挤压,这可能会在你想要将小部件拖放到表单布局中时造成麻烦。为了解决这个问题,暂时将layoutTopMargin属性设置为20或更高:

  1. 然后,在表单布局的左侧拖放两个标签,右侧拖放两个行编辑。双击标签,将它们的显示文本分别更改为“用户名:”和“密码:”。完成后,将表单布局的layoutTopMargin属性设置回0

目前,GUI 看起来非常棒,但是表单布局现在占据了中间的所有空间,这在主窗口最大化时并不是很愉快。为了保持表单紧凑,我们将执行以下一些有点棘手的步骤:

  1. 首先,在表单上方拖放一个水平布局,并将其layoutTopMarginlayoutBottomMargin设置为20,以便稍后放置在其中的小部件不会离“提交”按钮太近。接下来,将之前放置在表单布局中的整个表单布局拖放到水平布局中。然后,在表单的两侧放置水平间隔器以使其保持居中。以下截图说明了这些步骤:

  1. 之后,我们可以在进入下一部分之前对 GUI 进行进一步调整,使其看起来整洁。首先,将两个行编辑小部件的minimumSize属性设置为 150 x 25。然后,将表单布局的layoutLeftMarginlayoutRightMarginlayoutTopMarginlayoutBottomMargin属性设置为25。我们这样做的原因是我们将在下一部分中为表单布局添加轮廓。

  2. 由于“提交”按钮现在与表单布局的距离太远,让我们将水平布局的layoutBottomMargin属性设置为0,以将表单布局设置为0。这将使“提交”按钮稍微上移并靠近表单布局。之后,我们将调整“提交”按钮的大小,使其与表单布局对齐。让我们将“提交”按钮的minimumSize属性设置为 260 x 35,然后我们完成了!:

您还可以通过转到“工具”|“表单编辑器”|“预览”来预览 GUI,而无需构建程序。Qt Designer 是一种非常方便的工具,可以在不陡峭的学习曲线的情况下为 Qt 程序设计时尚的 GUI。在接下来的部分中,我们将学习如何使用 Qt 样式表自定义小部件的外观。

Qt 样式表

Qt 的小部件应用程序使用了一个名为 Qt 样式表的样式系统,它类似于 Web 技术的样式系统——CSS层叠样式表)。您只需要编写小部件的样式描述,Qt 将相应地呈现它。Qt 样式表的语法与 CSS 几乎相同。

Qt 样式表受 CSS 的启发,因此它们非常相似:

  • Qt 样式表:
QLineEdit { color: blue; background-color: black; } 
  • CSS:
h1 { color: blue; background-color: black; } 

在上面的示例中,Qt 样式表和 CSS 都包含了一个声明块和一个选择器。每个声明由属性和值组成,它们之间用冒号分隔。

您可以通过两种方法更改小部件的样式表——直接使用 C++代码或使用属性编辑器。如果您使用 C++代码,可以调用QObject::setStyleSheet()函数,如下所示:

myButton->setStyleSheet("background-color: green"); 

上述代码将我们的按钮小部件的背景颜色更改为绿色。您也可以通过在 Qt Designer 中将相同的声明写入小部件的styleSheet属性中来实现相同的结果:

QPushButton#myButton { background-color: green } 

关于 Qt 样式表的语法和属性的更多信息,请参考以下链接:doc.qt.io/qt-5/stylesheet-reference.html

让我们继续我们的项目,并将自定义 Qt 样式表应用到我们的 GUI 上!

  1. 首先,右键单击“提交”按钮,然后选择“更改样式表...”将弹出一个窗口供您编辑小部件的样式表:

  1. 然后,将以下内容添加到样式表编辑器窗口中:
border: 1px solid rgb(24, 103, 155); 
border-radius: 5px; 
background-color: rgb(124, 203, 255); 
color: white;
  1. 完成后,单击“确定”按钮,您应该能够看到“提交”按钮的外观发生了变化:

我们之前使用的样式表基本上是不言自明的。它使按钮的边框变为深蓝色,并使用 RGB 值设置边框颜色。然后,它还将按钮应用了圆角效果,并将其背景颜色更改为浅蓝色。最后,“提交”文本也已更改为白色。

  1. 接下来,我们想要将自定义样式表应用到表单布局上。但是,您会注意到右键单击它时没有“更改样式表...”选项。这是因为布局不具备该属性。为了对表单布局应用样式,我们必须首先将其转换为 QWidget 或 QFrame 对象。为此,请右键单击表单布局,然后选择“转换为 | QFrame”:

  1. 完成后,您会注意到它现在具有styleSheet属性,因此我们现在可以自定义其外观。让我们右键单击它,然后选择“Change styleSheet...”打开样式表编辑器窗口。然后,插入以下脚本:
#formFrame { 
border: 1px solid rgb(24, 103, 155); 
border-radius: 5px; 
background-color: white; } 

单词formFrame指的是小部件的objectName属性,它必须与小部件的确切名称匹配,否则样式将不会应用于它。我们为这个例子定义小部件名称的原因(这是我们在上一个例子中没有做的)是因为如果我们不指定小部件名称,样式也将应用于其所有子级。您可以尝试从前面的脚本中删除#formFrame {},然后看看会发生什么——现在,即使标签和行编辑也有边框线,这不是我们打算做的。GUI 现在看起来像这样:

  1. 最后,我们想要一个漂亮的背景,我们可以通过附加背景图像来实现这一点。为此,我们首先需要将图像导入到 Qt 的资源系统中。转到“文件”|“新建文件或项目...”,然后在“文件和类别”类别下选择 Qt。之后,选择 Qt 资源文件并单击“选择...”按钮。Qt 资源系统是一种存储二进制文件的平台无关机制,这些文件存储在应用程序的可执行文件中。您可以基本上将所有这些重要文件存储在这里,例如图标图像或语言文件,直接通过使用 Qt 资源文件将这些重要文件直接嵌入到编译过程中的程序中。

  2. 然后,在按下“下一步”按钮之前,键入文件名并设置其位置,然后点击“完成”按钮。现在,您将看到一个新的资源文件被创建,我命名为resource.qrc

  1. 用 Qt Creator 打开resource.qrc,然后选择“添加”|“添加前缀”。之后,键入您喜欢的前缀,例如/images。完成后,再次选择“添加”,这次选择“添加文件”。添加样本项目提供的图像文件login_bg.png。然后,保存resource.qrc,右键单击图像,选择“复制资源路径到剪贴板”。之后,关闭resource.qrc,再次打开mainwindow.ui

  1. 我们需要做的下一件事是右键单击“Object Inspector”中的centralWidget对象,然后选择“Change styleSheet...”,然后插入以下脚本:
#centralWidget { 
border-image: url(:/images/login_bg.png); }
  1. url()中的文本可以通过按Ctrl + V(或粘贴)插入,因为在上一步中选择“复制资源路径到剪贴板”时已将其复制到剪贴板。最终结果如下:

请确保您还构建和运行应用程序,然后检查最终结果是否与预期相同。还有很多东西可以调整,以使其看起来真正专业,但到目前为止,它看起来相当不错!

摘要

Qt Designer 真正改变了我们设计程序 GUI 的方式。它不仅包括所有常见的小部件,还有像布局和间隔这样方便的东西,这使我们的程序在不同类型的监视器和屏幕尺寸上运行得非常好。还要注意,我们已成功创建了一个具有漂亮用户界面的工作应用程序,而没有编写一行 C++代码!

本章中我们学到的只是 Qt 的冰山一角,因为还有许多功能我们尚未涵盖!在下一章中加入我们,学习如何使我们的程序真正功能强大!

第三章:数据库连接

在上一章中,我们学习了如何从头开始创建一个登录页面。然而,它还没有功能,因为登录页面还没有连接到数据库。在本章中,您将学习如何将您的 Qt 应用程序连接到验证登录凭据的 MySQL(或 MariaDB)数据库。

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

  • 介绍 MySQL 数据库系统

  • 设置 MySQL 数据库

  • SQL 命令

  • Qt 中的数据库连接

  • 功能性登录页面

我们将逐步学习本章内容,以发现 Qt 提供的强大功能,使您的应用程序可以直接连接到数据库,而无需任何额外的第三方依赖。数据库查询本身是一个庞大的主题,但我们将能够通过示例和实际方法从头开始学习最基本的命令。

Qt 支持多种不同类型的数据库系统:

  • MySQL(或 MariaDB)

  • SQLite(版本 2 和 3)

  • IBM DB2

  • Oracle

  • ODBC

  • PostgreSQL

  • Sybase Adaptive Server

其中最受欢迎的两种是 MySQL 和 SQLite。SQLite 数据库通常用于离线,并且不需要任何设置,因为它使用磁盘文件格式来存储数据。因此,在本章中,我们将学习如何设置 MySQL 数据库系统,并同时学习如何将我们的 Qt 应用程序连接到 MySQL 数据库。用于连接到 MySQL 数据库的 C++代码可以在不进行太多修改的情况下重用于连接到其他数据库系统。

介绍 MySQL 数据库系统

MySQL是一种基于关系模型的开源数据库管理系统,这是现代数据库系统用于存储各种信息的最常用方法。

与一些其他传统模型(如对象数据库系统或分层数据库系统)不同,关系模型已被证明更加用户友好,并且在其他模型之外表现出色。这就是为什么我们今天看到的大多数现代数据库系统大多使用这种方法的原因。

MySQL 最初由一家名为MySQL AB的瑞典公司开发,其名称是公司联合创始人的女儿MyStructured Query Language的缩写SQL的组合。

与 Qt 类似,MySQL 在其历史上也曾被多个不同的人拥有。最引人注目的收购发生在 2008 年,Sun Microsystems以 10 亿美元收购了 MySQL AB。一年后的 2009 年,Oracle Corporation收购了 Sun Microsystems,因此 MySQL 直到今天仍归 Oracle 所有。尽管 MySQL 多次易手,但它仍然是一款开源软件,允许用户更改代码以适应其自身目的。

由于其开源性质,还有其他从 MySQL 项目派生/分叉出来的数据库系统,如MariaDBPercona Server等。然而,这些替代方案与 MySQL 并不完全兼容,因为它们已经修改了以适应自己的需求,因此在这些系统中有些命令可能会有所不同。

根据Stack Overflow在 2017 年进行的一项调查,MySQL 是 Web 开发人员中使用最广泛的数据库系统,如下图所示:

调查结果表明,您在本章中学到的内容不仅可以应用于 Qt 项目,还可以应用于 Web、移动应用程序和其他类型的应用程序。

此外,MySQL 及其变体被大公司和项目组使用,如 Facebook、YouTube、Twitter、NASA、Wordpress、Drupal、Airbnb、Spotify 等。这意味着在开发过程中遇到任何技术问题时,您可以轻松获得答案。

有关 MySQL 的更多信息,请访问:

www.mysql.com

设置 MySQL 数据库

设置 MySQL 数据库有许多不同的方法。这实际上取决于您正在运行的平台类型,无论是 Windows、Linux、Mac 还是其他类型的操作系统;它还将取决于您的数据库用途——无论是用于开发和测试,还是用于大规模生产服务器。

对于大规模服务(如社交媒体),最好的方法是从源代码编译 MySQL,因为这样的项目需要大量的优化、配置,有时需要定制,以处理大量用户和流量。

但是,如果您只是进行正常使用,可以直接下载预编译的二进制文件,因为默认配置对此非常足够。您可以从官方网站或下载安装包安装独立的 MySQL 安装程序,该安装程序还包括 MySQL 以外的几个其他软件。

在本章中,我们将使用一个名为XAMPP的软件包,这是一个由Apache Friends开发的 Web 服务器堆栈软件包。该软件包包括ApacheMariaDBPHP和其他可选服务,您可以在安装过程中添加。以前,MySQL 是该软件包的一部分,但从 5.5.30 和 5.6.14 版本开始,它已经被MariaDB替换。MariaDB 几乎与 MySQL 相同,除了涉及高级功能的命令,这些功能我们在本书中不会使用。

我们使用 XAMPP 的原因是它有一个控制面板,可以轻松启动和停止服务,而无需使用命令提示符,并且可以轻松访问配置文件,而无需自己深入安装目录。对于涉及频繁测试的应用程序开发来说,它非常快速和高效。但是,不建议在生产服务器上使用 XAMPP,因为一些安全功能已经被默认禁用。

或者,您也可以通过其他类似的软件包安装 MySQL,如AppServAMPPSLAMP(仅限 Linux),WAMP(仅限 Windows),Zend****Server等。

现在,让我们学习如何安装 XAMPP:

  1. 首先,访问他们的网站www.apachefriends.org,并点击屏幕底部的一个下载按钮,显示您当前操作系统的图标:

  1. 一旦您点击下载按钮,下载过程应该在几秒钟内自动开始,并且一旦完成,它应该继续安装程序。在安装过程开始之前,请确保包括 Apache 和 MySQL/MariaDB。

  2. 安装 XAMPP 后,从开始菜单或桌面快捷方式启动控制面板。之后,您可能会注意到没有发生任何事情。这是因为 XAMPP 控制面板默认隐藏在任务栏中。您可以通过右键单击它并在弹出菜单中选择显示/隐藏选项来显示控制面板窗口。以下屏幕截图显示了 Windows 机器上的情况。对于 Linux,菜单可能看起来略有不同,但总体上非常相似。对于 macOS,您必须从启动台或从 dock 启动 XAMPP:

  1. 一旦您点击显示/隐藏选项,您最终将在屏幕上看到控制面板窗口。如果再次点击显示/隐藏选项,窗口将被隐藏起来:

  1. 他们的控制面板乍一看就很容易理解。在左侧,您可以看到 XAMPP 中可用服务的名称,在右侧,您将看到指示启动、配置、日志等按钮。由于某种原因,XAMPP 显示 MySQL 作为模块名称,但实际上它正在运行 MariaDB。不用担心;由于 MariaDB 是 MySQL 的一个分支,两者基本上工作方式相同。

  2. 在本章中,我们只需要 Apache 和 MySQL(MariaDB),所以让我们点击这些服务的启动按钮。一两秒后,您会看到启动按钮现在标记为停止,这意味着服务已经启动!:

  1. 要验证这一点,让我们打开浏览器,输入localhost作为网站地址。如果您看到类似以下图像的东西,这意味着 Apache Web 服务器已成功启动!:

  1. Apache 在这里非常重要,因为我们将使用它来使用名为phpMyAdmin的基于 Web 的管理工具来配置数据库。phpMyAdmin 是用 PHP 脚本语言编写的 MySQL 管理工具,因此得名。尽管它最初是为 MySQL 设计的,但它对 MariaDB 也非常有效。

  2. 要访问 phpMyAdmin 控制面板,请在浏览器上输入localhost/phpmyadmin。之后,您应该会看到类似于这样的东西:

  1. 在页面的左侧,您将看到导航面板,它允许您访问 MariaDB 数据库中可用的不同数据库。页面的右侧是各种工具,让您查看表格,编辑表格,运行 SQL 命令,将数据导出到电子表格,设置权限等等。

  2. 默认情况下,您只能在右侧的设置面板上修改数据库的常规设置。在能够修改特定数据库的设置之前,您必须在左侧的导航面板上选择一个数据库。

  3. 数据库就像一个您可以在其中存储日志的文件柜。每本日志称为一个表,每个表包含数据,这些数据像电子表格一样排序。当您想从 MariaDB 获取数据时,您必须在获取数据之前指定要访问的文件柜(数据库)和日志(表)。希望这能让您更好地理解 MariaDB 和其他类似的数据库系统背后的概念。

  4. 现在,让我们开始创建我们的第一个数据库!要这样做,您可以点击导航面板上方的数据库名称上方的新建按钮,或者点击菜单顶部的数据库按钮。这两个按钮都会带您到数据库页面,您应该能够在菜单按钮下方看到这个:

  1. 之后,让我们创建我们的第一个数据库!输入您想要创建的数据库名称,然后点击创建按钮。数据库创建后,您将被重定向到结构页面,该页面将列出此数据库中包含的所有表。默认情况下,您新创建的数据库不包含任何表,因此您将看到一行文本,其中说没有在数据库中找到表:

  1. 猜猜我们接下来要做什么?正确,我们将创建我们的第一个表!首先,让我们插入您想要创建的表的名称。由于在本章后面我们将做一个登录页面,让我们将我们的表命名为user。我们将保留默认的列数,然后点击 Go。

  2. 之后,您将被重定向到另一个页面,其中包含许多列的输入字段供您填写。每一列代表一个数据结构,它将在创建后添加到您的表中。

  3. 第一件需要添加到表结构中的是一个 ID,它将在每次插入新数据时自动增加。然后,添加一个时间戳列来指示数据插入的日期和时间,这对于调试很有用。最后,我们将添加一个用户名列和密码列用于登录验证。如果您不确定如何操作,请参考以下图片。确保您遵循图片中被圈出的设置:

  1. 结构的类型非常重要,必须根据其预期目的进行设置。例如,id 列必须设置为 INT(整数),因为它必须是一个完整的数字,而用户名和密码必须设置为 VARCHAR 或其他类似的数据类型(CHAR、TEXT 等),以便正确保存数据。

  2. 另一方面,时间戳必须设置为时间戳类型,并且必须将默认值设置为 CURRENT_TIMESTAMP,这将通知 MariaDB 在数据插入时自动生成当前时间戳。

  3. 请注意,ID 列的索引设置必须设置为 PRIMARY,并确保 A_I(自动增量)复选框被选中。当您选中 A_I 复选框时,将出现一个添加索引窗口。您可以保持默认设置,然后点击 Go 按钮完成步骤并开始创建表:

  1. 创建新表后,您应该能够看到类似以下图片的内容。您仍然可以随时通过单击更改按钮来编辑结构设置;您还可以通过单击列右侧的删除按钮来删除任何列。请注意,删除列也将删除属于该列的所有现有数据,此操作无法撤消:

  1. 尽管我们通常会通过程序或网页向数据库添加数据,但我们也可以直接在 phpMyAdmin 上添加数据以进行测试。要使用 phpMyAdmin 添加数据,首先必须创建一个数据库和表,这是我们在前面的步骤中已经完成的。然后,点击菜单顶部的插入按钮:

  1. 之后,您会看到一个表单出现,它类似于我们之前创建的数据结构:

  1. 您可以简单地忽略 ID 和时间戳的值,因为当您保存数据时它们将自动生成。在这种情况下,只需要填写用户名和密码。为了测试,让我们将test作为用户名,123456作为密码。然后,点击 Go 按钮保存数据。

请注意,您不应该以人类可读的格式保存密码在您的实际生产服务器上。在将密码传递到数据库之前,您必须使用加密哈希函数(如 SHA-512、RIPEEMD-512、BLAKE2b 等)对密码进行加密。这将确保密码在数据库被攻破时不被黑客读取。我们将在本章末尾讨论这个话题。

现在我们已经完成了数据库的设置并插入了我们的第一个测试数据,让我们继续学习一些 SQL 命令!

SQL 命令

大多数流行的关系数据库管理系统,如 MySQL、MariaDB、Oracle SQL、Microsoft SQL 等,都使用一种称为 SQL(结构化查询语言)的声明性语言来与数据库交互。SQL 最初是由 IBM 工程师在 20 世纪 70 年代开发的,但后来又被 Oracle Corporation 和其他当时新兴的技术公司进一步增强。

如今,SQL 已成为美国国家标准学会ANSI)和国际标准化组织ISO)的标准。SQL 语言自那时起已被许多不同的数据库系统采用,并成为现代时代最流行的数据库语言之一。

在本节中,我们将学习一些基本的 SQL 命令,您可以使用这些命令与您的 MariaDB 数据库进行交互,特别是用于从数据库中获取、保存、修改和删除数据。这些基本命令也可以用于其他类型的基于 SQL 的数据库系统,以及在 ANSI 和 ISO 标准下。只是,一些更高级/定制的功能在不同系统中可能有所不同,因此在使用这些高级功能之前,请确保阅读系统手册。

好的,让我们开始吧!

SELECT

大多数 SQL 语句都是单词简短且不言自明的。例如,此语句用于从特定表中选择一个或多个列,并获取来自所述列的数据。让我们来看看一些使用SELECT语句的示例命令。

以下命令检索user表中所有列的所有数据:

SELECT * FROM user;

以下命令仅从用户表中检索username列:

SELECT username FROM user;

以下命令检索user表中id等于1usernamepassword列:

SELECT username, password FROM user WHERE id = 1;

您可以使用 phpMyAdmin 自行尝试这些命令。要执行此操作,请单击 phpMyAdmin 菜单顶部的 SQL 按钮。之后,您可以在下面的文本字段中输入命令,然后单击 Go 以执行查询:

要了解有关SELECT语句的更多信息,请参阅以下链接:

dev.mysql.com/doc/refman/5.7/en/select.html

INSERT

接下来,INSERT语句用于将新数据保存到数据库表中。例如:

INSERT INTO user (username, password) VALUES ("test2", "123456");

上述 SQL 命令将usernamepassword数据插入user表中。还有一些其他语句可以与INSERT一起使用,例如LOW_PRIORITYDELAYEDHIGH_PRIORITY等。

请参考以下链接以了解更多关于这些选项的信息:

dev.mysql.com/doc/refman/5.7/en/insert.html

UPDATE

UPDATE语句修改数据库中的现有数据。您必须为UPDATE命令指定条件,否则它将修改表中的每一条数据,这不是我们期望的行为。尝试以下命令,它将更改第一个用户的usernamepassword

UPDATE user SET username = "test1", password = "1234321" WHERE id = 1;

但是,如果 ID 为1的用户不存在,该命令将失败。如果您提供的usernamepassword数据与数据库中存储的数据完全匹配(没有变化),该命令还将返回状态0 行受影响。有关UPDATE语句的更多信息,请参阅以下链接:

dev.mysql.com/doc/refman/5.7/en/update.html

DELETE

DELETE语句从数据库的特定表中删除数据。例如,以下命令从user表中删除 ID 为1的数据:

DELETE FROM user WHERE id = 1;

尽管您可以使用此语句删除不需要的数据,但不建议从数据库中删除任何数据,因为该操作无法撤消。最好在表中添加另一列,称为状态,并使用该列指示数据是否应显示。例如,如果用户在前端应用程序中删除数据,请将该数据的状态设置为(假设)1而不是0。然后,当您想要在前端显示数据时,仅显示携带status0的数据:

这样,任何意外删除的数据都可以轻松恢复。如果您只计划使用 true 或 false,也可以使用 BOOLEAN 类型。我通常使用 TINYINT,以防将来需要第三或第四状态。有关DELETE语句的更多信息,您可以参考以下链接:

dev.mysql.com/doc/refman/5.7/en/delete.html

连接

使用关系数据库管理系统的优势在于,可以轻松地将来自不同表的数据连接在一起,并以单个批量返回给用户。这极大地提高了开发人员的生产力,因为它在设计复杂的数据库结构时提供了流动性和灵活性。

MariaDB/MySQL 中有许多类型的 JOIN 语句—INNER JOIN、FULL OUTER JOIN、LEFT JOIN 和 RIGHT JOIN。这些不同的 JOIN 语句在执行时表现不同,您可以在以下图像中看到:

大多数情况下,我们将使用 INNER JOIN 语句,因为它只返回两个表中具有匹配值的数据,因此只返回所需的少量数据。JOIN 命令比其他命令复杂得多,因为您需要首先设计可连接的表。在开始测试 JOIN 命令之前,让我们创建另一个表以实现这一点。我们将称这个新表为 department:

之后,添加两个部门,如下所示:

然后,转到用户表,在结构页面,滚动到底部,查找所示的表单,然后单击“Go”按钮:

添加一个名为 deptID(代表部门 ID)的新列,并将其数据类型设置为int(整数):

完成后,设置几个测试用户,并将他们的 deptID 分别设置为12

请注意,我在这里还添加了状态列,以检查用户是否已被删除。完成后,让我们尝试运行一个示例命令!:

SELECT my_user.username, department.name FROM (SELECT * FROM user WHERE deptID = 1) AS my_user INNER JOIN department ON department.id = my_user.deptID AND my_user.status = 0 

乍一看,这看起来相当复杂,但如果您将其分成几个部分,实际上并不复杂。我们将从()括号内的命令开始,其中我们要求 MariaDB/MySQL 选择deptID = 1user表中的所有列:

SELECT * FROM user WHERE deptID = 1 

之后,将其包含在()括号中,并将整个命令命名为my_user。之后,您可以开始使用INNER JOIN语句将用户表(现在称为my_user)与部门表进行连接。在这里,我们还添加了一些条件来查找数据,例如部门表的 ID 必须与my_userdeptID匹配,并且my_user的状态值必须为0,表示数据仍然有效,未标记为已移除:

(SELECT * FROM user WHERE deptID = 1) AS my_user INNER JOIN department ON department.id = my_user.deptID AND my_user.status = 0 

最后,在前面添加以下代码以完成 SQL 命令:

SELECT my_user.username, department.name FROM  

让我们尝试上述命令,看看结果是否符合您的预期。

只要表通过匹配列相互连接,您就可以使用此方法连接无限数量的表。

要了解有关JOIN语句的更多信息,请访问以下链接:

dev.mysql.com/doc/refman/5.7/en/join.html

在本章中,我们还没有涵盖的许多其他 SQL 语句,但我们已经涵盖的基本上就是您开始所需的全部内容。

在我们进入下一部分之前,我们必须为应用程序创建一个访问 MariaDB/MySQL 数据库的用户帐户。首先,转到 phpMyAdmin 的主页,然后单击顶部菜单上的用户帐户:

然后,转到底部,查找名为“添加用户帐户”的链接:

一旦您进入“添加用户帐户”页面,请在登录信息表单中输入用户名和密码信息。确保主机名设置为本地:

然后,向下滚动并设置用户的全局权限。在数据部分启用选项就足够了,但不要启用其他选项,因为一旦您的服务器被入侵,它可能会给黑客修改数据库结构的权限。

创建用户帐户后,请按照以下步骤允许新创建的用户访问名为 test 的数据库(或您选择的任何其他表名):

点击“Go”按钮后,您现在已经赋予了用户帐户访问数据库的权限!在下一节中,我们将学习如何将我们的 Qt 应用程序连接到数据库。

Qt 中的数据库连接

现在我们已经学会了如何设置一个功能齐全的 MySQL/MariaDB 数据库系统,让我们再进一步,了解 Qt 中的数据库连接模块!

在我们继续处理上一章的登录页面之前,让我们首先开始一个新的 Qt 项目,这样可以更容易地演示与数据库连接相关的功能,而不会被其他东西分散注意力。这次,我们将选择名为 Qt 控制台应用程序的终端样式应用程序,因为我们不真的需要任何 GUI 来进行演示:

创建新项目后,您应该只在项目中看到两个文件,即[project_name].pro 和 main.cpp:

您需要做的第一件事是打开您的项目文件(.pro),在我的情况下是 DatabaseConnection.pro,并在第一行的末尾添加sql关键字,如下所示:

QT += core sql 

就这么简单,我们已经成功地将sql模块导入到了我们的 Qt 项目中!然后,打开main.cpp,您应该看到一个非常简单的脚本,其中只包含八行代码。这基本上是您创建一个空控制台应用程序所需的全部内容:

#include <QCoreApplication> 
int main(int argc, char *argv[]) 
{ 
   QCoreApplication a(argc, argv); 
   return a.exec(); 
} 

为了连接到我们的数据库,我们必须首先将相关的头文件导入到main.cpp中,如下所示:

#include <QCoreApplication> 
#include <QtSql> 
#include <QSqlDatabase> 
#include <QSqlQuery> 
#include <QDebug> 
int main(int argc, char *argv[]) 
{ 
   QCoreApplication a(argc, argv); 
   return a.exec(); 
} 

没有这些头文件,我们将无法使用 Qt 的sql模块提供的函数,这些函数是我们之前导入的。此外,我们还添加了QDebug头文件,以便我们可以轻松地在控制台显示上打印出任何文本(类似于 C++标准库提供的std::cout函数)。

接下来,我们将向main.cpp文件添加一些代码。在return a.exec()之前添加以下突出显示的代码:

int main(int argc, char *argv[]) 
{ 
   QCoreApplication a(argc, argv); 
   QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); 
   db.setHostName("127.0.0.1"); 
   db.setPort(3306); 
   db.setDatabaseName("test"); 
   db.setUserName("testuser"); 
   db.setPassword("testpass"); 
   if (db.open()) 
   { 
         qDebug() << "Connected!"; 
   } 
   else 
   { 
         qDebug() << "Failed to connect."; 
         return 0; 
   } 
   return a.exec(); 
} 

请注意,数据库名称、用户名和密码可能与您在数据库中设置的不同,请在编译项目之前确保它们是正确的。

完成后,让我们点击“运行”按钮,看看会发生什么!:

如果您看到以下错误,请不要担心:

这只是因为您必须将 MariaDB Connector(或者如果您正在运行 MySQL,则是 MySQL Connector)安装到您的计算机上,并将 DLL 文件复制到 Qt 安装路径。请确保 DLL 文件与服务器的数据库库匹配。您可以打开 phpMyAdmin 的主页,查看它当前使用的库。

出于某种原因,尽管我正在运行带有 MariaDB 的 XAMPP,但这里的库名称显示为 libmysql 而不是 libmariadb,因此我不得不安装 MySQL Connector:

如果您使用的是 MariaDB,请在以下链接下载 MariaDB Connector:

downloads.mariadb.org/connector-c 如果您使用的是 MySQL(或者遇到了我遇到的相同问题),请访问另一个链接并下载 MySQL 连接器:

dev.mysql.com/downloads/connector/cpp/

在您下载了 MariaDB 连接器之后,请在您的计算机上安装它:

上面的截图显示了 Windows 机器的安装过程。如果您使用 Linux,您必须为您的 Linux 发行版下载正确的软件包。如果您使用 Debian、Ubuntu 或其变体之一,请下载 Debian 和 Ubuntu 软件包。如果您使用 Red Hat、Fedora、CentOS 或其变体之一,请下载 Red Hat、Fedora 和 CentOS 软件包。这些软件包的安装是自动的,所以您可以放心。但是,如果您没有使用这些系统之一,您将需要下载符合您系统要求的下载页面上列出的一个 gzipped tar 文件。

有关在 Linux 上安装 MariaDB 二进制 tarballs 的更多信息,请参阅以下链接:

mariadb.com/kb/en/library/installing-mariadb-binary-tarballs/

至于 macOS,您需要使用一个名为Homebrew的软件包管理器来安装 MariaDB 服务器。

有关更多信息,请查看以下链接:

mariadb.com/kb/en/library/installing-mariadb-on-macos-using-homebrew/

安装完成后,转到其安装目录并查找 DLL 文件(MariaDB 的libmariadb.dll或 MySQL 的libmysql.dll)。对于 Linux 和 macOS,而不是 DLL,它是libmariadb.solibmysql.so

然后,将文件复制到应用程序的构建目录(与应用程序的可执行文件相同的文件夹)。之后,尝试再次运行您的应用程序:

如果您仍然收到连接失败的消息,但没有QMYSQL driver not loaded的消息,请检查您的 XAMPP 控制面板,并确保您的数据库服务正在运行;还要确保您在代码中输入的数据库名称、用户名和密码都是正确的信息。

接下来,我们可以开始尝试使用 SQL 命令!在return a.exec()之前添加以下代码:

QString command = "SELECT name FROM department"; 
QSqlQuery query(db); 
if (query.exec(command)) 
{ 
   while(query.next()) 
   { 
         QString name = query.value("name").toString(); 
         qDebug() << name; 
   } 
} 

上述代码将命令文本发送到数据库,并同步等待来自服务器的结果返回。之后,使用while循环遍历每个结果并将其转换为字符串格式。然后,在控制台窗口上显示结果。如果一切顺利,您应该会看到类似这样的东西:

让我们尝试一些更复杂的东西:

QString command = "SELECT my_user.username, department.name AS deptname FROM (SELECT * FROM user WHERE status = 0) AS my_user INNER JOIN department ON department.id = my_user.deptID"; 
QSqlQuery query(db); 
if (query.exec(command)) 
{ 
   while(query.next()) 
   { 
         QString username = query.value("username").toString(); 
         QString department = query.value("deptname").toString(); 
         qDebug() << username << department; 
   } 
} 

这一次,我们使用INNER JOIN来合并两个表以选择usernamedepartment名称。为了避免关于名为name的变量的混淆,使用AS语句将其重命名为deptname。之后,在控制台窗口上显示usernamedepartment名称:

我们暂时完成了。让我们继续下一节,学习如何使我们的登录页面功能正常!

创建我们的功能性登录页面

既然我们已经学会了如何将我们的 Qt 应用程序连接到 MariaDB/MySQL 数据库系统,现在是时候继续在登录页面上继续工作了!在上一章中,我们学会了如何设置登录页面的 GUI。但是,它作为登录页面完全没有任何功能,因为它没有连接到数据库并验证登录凭据。因此,我们将学习如何通过赋予 Qt 的sql模块来实现这一点。

只是为了回顾一下——这就是登录界面的样子:

现在我们需要做的第一件事是为这个登录页面中重要的小部件命名,包括用户名输入、密码输入和提交按钮。您可以通过选择小部件并在属性编辑器中查找属性来设置这些属性:

然后,将密码输入的 echoMode 设置为 Password。这个设置将通过用点替换密码来在视觉上隐藏密码:

之后,右键单击提交按钮,选择转到槽... 一个窗口将弹出并询问您要使用哪个信号。选择 clicked(),然后点击确定:

一个名为on_loginButton_clicked()的新函数将自动添加到MainWindow类中。当用户按下提交按钮时,这个函数将被 Qt 触发,因此你只需要在这里编写代码来提交usernamepassword以进行登录验证。信号和槽机制是 Qt 提供的一项特殊功能,用于对象之间的通信。当一个小部件发出信号时,另一个小部件将收到通知,并将继续运行特定的函数,该函数旨在对特定信号做出反应。

让我们来看看代码。

首先,在项目(.pro)文件中添加sql关键字:

QT += core gui

sql

然后,继续在mainwindow.cpp中添加相关的头文件:

#ifndef MAINWINDOW_H 
#define MAINWINDOW_H 

#include <QMainWindow> 

#include <QtSql> 
#include <QSqlDatabase> 
#include <QSqlQuery> 
#include <QDebug> 
#include <QMessageBox> 

然后,回到mainwindow.cpp,在on_loginButton_clicked()函数中添加以下代码:

void MainWindow::on_loginButton_clicked() 
{ 
   QString username = ui->userInput->text(); 
   QString password = ui->passwordInput->text(); 
   qDebug() << username << password; 
} 

现在,点击运行按钮,等待应用程序启动。然后,输入任意随机的usernamepassword,然后点击提交按钮。您现在应该在 Qt Creator 的应用程序输出窗口中看到您的usernamepassword被显示出来。

接下来,我们将把之前编写的 SQL 集成代码复制到mainwindow.cpp中:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

   db = QSqlDatabase::addDatabase("QMYSQL"); 
   db.setHostName("127.0.0.1"); 
   db.setPort(3306); 
   db.setDatabaseName("test"); 
   db.setUserName("testuser"); 
   db.setPassword("testpass"); 

   if (db.open()) 
   { 
         qDebug() << "Connected!"; 
   } 
   else 
   { 
         qDebug() << "Failed to connect."; 
   } 
}

请注意,我在数据库名称、用户名和密码中使用了一些随机文本。请确保在这里输入正确的详细信息,并确保它们与您在数据库系统中设置的内容匹配。

我们对前面的代码做了一个小改动,就是我们只需要在mainwindow.cpp中调用db = QSqlDatabase::addDatabase("QMYSQL"),而不需要类名,因为声明QSqlDatabase db现在已经被移到了mainwindow.h中:

private: 
   Ui::MainWindow *ui; 
 QSqlDatabase db; 

最后,我们添加了将usernamepassword信息与 SQL 命令结合的代码,并将整个内容发送到数据库进行执行。如果有与登录信息匹配的结果,那么意味着登录成功,否则,意味着登录失败:

void MainWindow::on_loginButton_clicked() 
{ 
   QString username = ui->userInput->text(); 
   QString password = ui->passwordInput->text(); 

   qDebug() << username << password; 

   QString command = "SELECT * FROM user WHERE username = '" + username 
   + "' AND password = '" + password + "' AND status = 0"; 
   QSqlQuery query(db); 
   if (query.exec(command)) 
   { 
         if (query.size() > 0) 
         { 
               QMessageBox::information(this, "Login success.", "You 
               have successfully logged in!"); 
         } 
         else 
         { 
               QMessageBox::information(this, "Login failed.", "Login 
               failed. Please try again..."); 
         } 
   } 
} 

再次点击运行按钮,看看当您点击提交按钮时会发生什么:

万岁!登录页面现在已经完全可用!

摘要

在本章中,我们学习了如何设置数据库系统并使我们的 Qt 应用程序连接到它。在下一章中,我们将学习如何使用强大的 Qt 框架绘制图表和图表。

第四章:图表和图形

在上一章中,我们学习了如何使用 Qt 的sql模块从数据库中检索数据。有许多方法可以向用户呈现这些数据,例如以表格或图表的形式显示。在本章中,我们将学习如何进行后者——使用 Qt 的图表模块以不同类型的图表和图形呈现数据。

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

  • Qt 中的图表和图形类型

  • 图表和图形实现

  • 创建仪表板页面

自 Qt 5.7 以来,以前只有商业用户才能使用的几个模块已经免费提供给所有开源软件包用户,其中包括 Qt Charts 模块。因此,对于那些没有商业许可证的大多数 Qt 用户来说,这被认为是一个非常新的模块。

请注意,与大多数可在 LGPLv3 许可下使用的 Qt 模块不同,Qt Chart 模块是根据 GPLv3 许可提供的。与 LGPLv3 不同,GPLv3 许可要求您发布应用程序的源代码,同时您的应用程序也必须在 GPLv3 下获得许可。这意味着您不允许将 Qt Chart 与您的应用程序进行静态链接。它还阻止了该模块在专有软件中的使用。

要了解有关 GNU 许可的更多信息,请访问以下链接:www.gnu.org/licenses/gpl-faq.html.

让我们开始吧!

Qt 中的图表和图形类型

Qt 支持最常用的图表,并且甚至允许开发人员自定义它们的外观和感觉,以便可以用于许多不同的目的。Qt Charts 模块提供以下图表类型:

  • 线性和样条线图

  • 条形图

  • 饼图

  • 极坐标图

  • 区域和散点图

  • 箱形图

  • 蜡烛图

线性和样条线图

第一种类型的图表是线性和样条线图。这些图表通常呈现为一系列通过线连接的点/标记。在线图中,点通过直线连接以显示变量随时间变化的情况。另一方面,样条线图与线图非常相似,只是点是通过样条线/曲线连接而不是直线:

条形图

条形图是除线图和饼图之外最常用的图表之一。条形图与线图非常相似,只是它不沿轴连接数据。相反,条形图使用单独的矩形形状来显示其数据,其中其高度由数据的值决定。这意味着数值越高,矩形形状就会变得越高:

饼图

饼图,顾名思义,是一种看起来像饼的图表类型。饼图以饼片的形式呈现数据。每个饼片的大小将由其值的整体百分比决定,与其余数据相比。因此,饼图通常用于显示分数、比率、百分比或一组数据的份额:

有时,饼图也可以以甜甜圈形式显示(也称为甜甜圈图):

极坐标图

极坐标图以圆形图表的形式呈现数据,其中数据的放置基于角度和距离中心的距离,这意味着数据值越高,点距离图表中心就越远。您可以在极坐标图中显示多种类型的图表,如线性、样条线、区域和散点图来可视化数据:

如果您是游戏玩家,您应该已经注意到在一些视频游戏中使用了这种类型的图表来显示游戏角色的属性:

区域和散点图

面积图将数据显示为面积或形状,以指示体积。通常用于比较两个或多个数据集之间的差异。

散点图,另一方面,用于显示一组数据点,并显示两个或多个数据集之间的非线性关系。

箱线图

箱线图将数据呈现为四分位数,并延伸出显示值的变异性的须。箱子可能有垂直延伸的线,称为。这些线表示四分位数之外的变异性,任何超出这些线或须的点都被视为异常值。箱线图最常用于统计分析,比如股票市场分析:

蜡烛图

蜡烛图在视觉上与箱线图非常相似,只是用于表示开盘和收盘价之间的差异,同时通过不同的颜色显示值的方向(增加或减少)。如果特定数据的值保持不变,矩形形状将根本不会显示:

有关 Qt 支持的不同类型图表的更多信息,请访问以下链接:doc.qt.io/qt-5/qtcharts-overview.html.

Qt 支持大多数你项目中需要的图表类型。在 Qt 中实现这些图表也非常容易。让我们看看如何做到!

实现图表和图形

Qt 通过将复杂的绘图算法放在不同的抽象层后面,使得绘制不同类型的图表变得容易,并为我们提供了一组类和函数,可以用来轻松创建这些图表,而不需要知道绘图算法在幕后是如何工作的。这些类和函数都包含在 Qt 的图表模块中。

让我们创建一个新的 Qt Widgets 应用程序项目,并尝试在 Qt 中创建我们的第一个图表。

创建新项目后,打开项目文件(.pro)并将charts模块添加到项目中,如下所示:

QT += core gui charts 

然后,打开mainwindow.h并添加以下内容以包含使用charts模块所需的头文件:

#include <QtCharts> 
#include <QChartView> 
#include <QBarSet> 
#include <QBarSeries> 

QtChartsQtChartView头文件对于 Qt 的charts模块都是必不可少的。你必须包含它们两个才能让任何类型的图表正常工作。另外两个头文件,即QBarSetQBarSeries,在这里被使用是因为我们将创建一个条形图。根据你想创建的图表类型不同,项目中包含的头文件也会有所不同。

接下来,打开mainwindow.ui并将垂直布局或水平布局拖到中央窗口部件。然后,选择中央窗口部件,点击水平布局或垂直布局。布局方向并不是特别重要,因为我们这里只会创建一个图表:

之后,右键单击刚刚拖到中央窗口部件的布局部件,选择转换为 | QFrame。这将把布局部件更改为 QFrame 部件,同时保持其布局属性。如果从 Widget Box 创建 QFrame,它将没有我们需要的布局属性。这一步很重要,这样我们才能将其设置为稍后图表的父级:

现在打开mainwindow.cpp并添加以下代码:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

   QBarSet *set0 = new QBarSet("Jane"); 
   QBarSet *set1 = new QBarSet("John"); 
   QBarSet *set2 = new QBarSet("Axel"); 
   QBarSet *set3 = new QBarSet("Mary"); 
   QBarSet *set4 = new QBarSet("Samantha"); 

   *set0 << 10 << 20 << 30 << 40 << 50 << 60; 
   *set1 << 50 << 70 << 40 << 45 << 80 << 70; 
   *set2 << 30 << 50 << 80 << 13 << 80 << 50; 
   *set3 << 50 << 60 << 70 << 30 << 40 << 25; 
   *set4 << 90 << 70 << 50 << 30 << 16 << 42; 

   QBarSeries *series = new QBarSeries(); 
   series->append(set0); 
   series->append(set1); 
   series->append(set2); 
   series->append(set3); 
   series->append(set4); 
} 

上面的代码初始化了将显示在条形图中的所有类别。然后,我们还为每个类别添加了六个不同的数据项,这些数据项稍后将以条形/矩形形式表示。

QBarSet类表示条形图中的一组条形。它将几个条形组合成一个条形集,然后可以加标签。另一方面,QBarSeries表示按类别分组的一系列条形。换句话说,颜色相同的条形属于同一系列。

接下来,初始化QChart对象并将系列添加到其中。我们还设置了图表的标题并启用了动画:

QChart *chart = new QChart(); 
chart->addSeries(series); 
chart->setTitle("Student Performance"); 
chart->setAnimationOptions(QChart::SeriesAnimations); 

之后,我们创建了一个条形图类别轴,并将其应用于条形图的x轴。我们使用了一个QStringList变量,类似于数组,但专门用于存储字符串。然后,QBarCategoryAxis将获取字符串列表并填充到x轴上:

QStringList categories; 
categories << "Jan" << "Feb" << "Mar" << "Apr" << "May" << "Jun"; 
QBarCategoryAxis *axis = new QBarCategoryAxis(); 
axis->append(categories); 
chart->createDefaultAxes(); 
chart->setAxisX(axis, series); 

然后,我们为 Qt 创建一个图表视图来渲染条形图,并将其设置为主窗口中框架小部件的子级;否则,它将无法在主窗口上渲染:

QChartView *chartView = new QChartView(chart); 
chartView->setParent(ui->verticalFrame); 

在 Qt Creator 中点击运行按钮,你应该会看到类似这样的东西:

接下来,让我们做一个饼图;这真的很容易。首先,我们包括QPieSeriesQPieSlice,而不是QBarSetQBarSeries

#include <QPieSeries> 
#include <QPieSlice> 

然后,创建一个QPieSeries对象,并设置每个数据的名称和值。之后,将其中一个切片设置为不同的视觉样式,并使其脱颖而出。然后,创建一个QChart对象,并将其与我们创建的QPieSeries对象链接起来:

QPieSeries *series = new QPieSeries(); 
series->append("Jane", 10); 
series->append("Joe", 20); 
series->append("Andy", 30); 
series->append("Barbara", 40); 
series->append("Jason", 50); 

QPieSlice *slice = series->slices().at(1); 
slice->setExploded(); // Explode this chart 
slice->setLabelVisible(); // Make label visible 
slice->setPen(QPen(Qt::darkGreen, 2)); // Set line color 
slice->setBrush(Qt::green); // Set slice color 

QChart *chart = new QChart(); 
chart->addSeries(series); 
chart->setTitle("Students Performance"); 

最后,创建QChartView对象,并将其与我们刚刚创建的QChart对象链接起来。然后,将其设置为框架小部件的子级,我们就可以开始了!

QChartView *chartView = new QChartView(chart);
chartView->setParent(ui->verticalFrame);

现在按下运行按钮,你应该能看到类似这样的东西:

有关如何在 Qt 中创建不同图表的更多示例,请查看以下链接的示例代码:doc.qt.io/qt-5/qtcharts-examples.html

现在我们已经看到使用 Qt 创建图表和图形是很容易的,让我们扩展前几章开始的项目,并为其创建一个仪表板!

创建仪表板页面

在上一章中,我们创建了一个功能性的登录页面,允许用户使用他们的用户名和密码登录。接下来我们需要做的是创建仪表板页面,用户成功登录后将自动跳转到该页面。

仪表板页面通常用作用户快速了解其公司、业务、项目、资产和/或其他统计数据的概览。以下图片展示了仪表板页面可能的外观:

正如你所看到的,仪表板页面使用了相当多的图表和图形,因为这是在不让用户感到不知所措的情况下显示大量数据的最佳方式。此外,图表和图形可以让用户轻松了解整体情况,而无需深入细节。

让我们打开之前的项目并打开mainwindow.ui文件。用户界面应该看起来像这样:

正如你所看到的,我们现在已经有了登录页面,但我们还需要添加另一个页面作为仪表板。为了让多个页面在同一个程序中共存,并能够随时在不同页面之间切换,Qt 为我们提供了一种叫做QStackedWidget的东西。

堆叠窗口就像一本书,你可以不断添加更多页面,但一次只显示一页。每一页都是完全不同的 GUI,因此不会干扰堆叠窗口中的其他页面。

由于之前的登录页面并不是为堆叠窗口而设计的,我们需要对其进行一些调整。首先,从小部件框中将堆叠窗口拖放到应用程序的中央小部件下,然后,我们需要将之前在中央小部件下的所有内容移动到堆叠窗口的第一页中,我们将其重命名为 loginPage:

接下来,将中央窗口部件的所有布局设置为0,这样它就完全没有边距,就像这样:

在那之后,我们必须将中央窗口部件的样式表属性中的代码剪切,并粘贴到登录页面的样式表属性中。换句话说,背景图片、按钮样式和其他视觉设置现在只应用于登录页面。

完成后,切换页面时,你应该会得到两个完全不同的 GUI(仪表板页面目前为空):

接下来,将网格布局拖放到仪表板页面,并将布局垂直应用到仪表板页面:

在那之后,将六个垂直布局拖放到网格布局中,就像这样:

然后,选择我们刚刚添加到网格布局中的每个垂直布局,并将其转换为 QFrame:

就像我们在图表实现示例中所做的那样,我们必须将布局转换为QFrame(或QWidget),以便我们可以将图表附加到它作为子对象。如果你直接从部件框中拖动QFrame并且不使用变形,那么QFrame对象就没有布局属性,因此图表可能无法调整大小以适应QFrame的几何形状。此外,将这些QFrame对象命名为chart1chart6,因为我们将在接下来的步骤中需要它们。完成后,让我们继续编写代码。

首先,打开你的项目(.pro)文件,并添加charts模块,就像我们在本章的早期示例中所做的那样。然后,打开mainwindow.h并包含所有所需的头文件。这一次,我们还包括了用于创建折线图的QLineSeries头文件:

#include <QtCharts> 
#include <QChartView> 

#include <QBarSet> 
#include <QBarSeries> 

#include <QPieSeries> 
#include <QPieSlice> 

#include <QLineSeries> 

在那之后,声明图表的指针,就像这样:

QChartView *chartViewBar; 
QChartView *chartViewPie; 
QChartView *chartViewLine; 

然后,我们将添加创建柱状图的代码。这是我们之前在图表实现示例中创建的相同的柱状图,只是现在它附加到名为chart1QFrame对象上,并在渲染时设置为启用抗锯齿。抗锯齿功能可以消除所有图表的锯齿状边缘,从而使渲染看起来更加平滑:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

   ////////BAR CHART///////////// 
   QBarSet *set0 = new QBarSet("Jane"); 
   QBarSet *set1 = new QBarSet("John"); 
   QBarSet *set2 = new QBarSet("Axel"); 
   QBarSet *set3 = new QBarSet("Mary"); 
   QBarSet *set4 = new QBarSet("Samantha"); 

   *set0 << 10 << 20 << 30 << 40 << 50 << 60; 
   *set1 << 50 << 70 << 40 << 45 << 80 << 70; 
   *set2 << 30 << 50 << 80 << 13 << 80 << 50; 
   *set3 << 50 << 60 << 70 << 30 << 40 << 25; 
   *set4 << 90 << 70 << 50 << 30 << 16 << 42; 

   QBarSeries *seriesBar = new QBarSeries(); 
   seriesBar->append(set0); 
   seriesBar->append(set1); 
   seriesBar->append(set2); 
   seriesBar->append(set3); 
   seriesBar->append(set4); 

   QChart *chartBar = new QChart(); 
   chartBar->addSeries(seriesBar); 
   chartBar->setTitle("Students Performance"); 
   chartBar->setAnimationOptions(QChart::SeriesAnimations); 

   QStringList categories; 
   categories << "Jan" << "Feb" << "Mar" << "Apr" << "May" << "Jun"; 
   QBarCategoryAxis *axis = new QBarCategoryAxis(); 
   axis->append(categories); 
   chartBar->createDefaultAxes(); 
   chartBar->setAxisX(axis, seriesBar); 

   chartViewBar = new QChartView(chartBar); 
   chartViewBar->setRenderHint(QPainter::Antialiasing); 
   chartViewBar->setParent(ui->chart1); 
} 

接下来,我们还要添加饼图的代码。同样,这是来自先前示例的相同饼图:

QPieSeries *seriesPie = new QPieSeries(); 
seriesPie->append("Jane", 10); 
seriesPie->append("Joe", 20); 
seriesPie->append("Andy", 30); 
seriesPie->append("Barbara", 40); 
seriesPie->append("Jason", 50); 

QPieSlice *slice = seriesPie->slices().at(1); 
slice->setExploded(); 
slice->setLabelVisible(); 
slice->setPen(QPen(Qt::darkGreen, 2)); 
slice->setBrush(Qt::green); 

QChart *chartPie = new QChart(); 
chartPie->addSeries(seriesPie); 
chartPie->setTitle("Students Performance"); 

chartViewPie = new QChartView(chartPie); 
chartViewPie->setRenderHint(QPainter::Antialiasing); 
chartViewPie->setParent(ui->chart2); 

最后,我们还向仪表板添加了一个折线图,这是新的内容。代码非常简单,非常类似于饼图:

QLineSeries *seriesLine = new QLineSeries(); 
seriesLine->append(0, 6); 
seriesLine->append(2, 4); 
seriesLine->append(3, 8); 
seriesLine->append(7, 4); 
seriesLine->append(10, 5); 
seriesLine->append(11, 10); 
seriesLine->append(13, 3); 
seriesLine->append(17, 6); 
seriesLine->append(18, 3); 
seriesLine->append(20, 2); 

QChart *chartLine = new QChart(); 
chartLine->addSeries(seriesLine); 
chartLine->createDefaultAxes(); 
chartLine->setTitle("Students Performance"); 

chartViewLine = new QChartView(chartLine); 
chartViewLine->setRenderHint(QPainter::Antialiasing); 
chartViewLine->setParent(ui->chart3); 

完成后,我们必须为主窗口类添加一个 resize-event 槽,并在主窗口调整大小时使图表跟随其各自父级的大小。首先,进入mainwindow.h并添加事件处理程序声明:

protected: 
   void resizeEvent(QResizeEvent* event); 

然后,打开mainwindow.cpp并添加以下代码:

void MainWindow::resizeEvent(QResizeEvent* event) 
{ 
   QMainWindow::resizeEvent(event); 

   chartViewBar->resize(chartViewBar->parentWidget()->size()); 
   chartViewPie->resize(chartViewPie->parentWidget()->size()); 
   chartViewLine->resize(chartViewLine->parentWidget()->size()); 
} 

请注意,必须首先调用QMainWindow::resizeEvent(event),以便在调用自定义方法之前触发默认行为。resizeEvent()是 Qt 提供的许多事件处理程序之一,用于对其事件做出反应,例如鼠标事件、窗口事件、绘制事件等。与信号和槽机制不同,你需要替换事件处理程序的虚函数,以使其在调用事件时执行你想要的操作。

如果我们现在构建并运行项目,应该会得到类似这样的东西:

看起来相当整洁,不是吗!然而,为了简单起见,也为了不让读者感到困惑,图表都是硬编码的,并且没有使用来自数据库的任何数据。如果你打算使用来自数据库的数据,在程序启动时不要进行任何 SQL 查询,因为如果你加载的数据非常大,或者你的服务器非常慢,这将使你的程序冻结。

最好的方法是只在从登录页面切换到仪表板页面(或切换到任何其他页面时)加载数据,以便加载时间对用户不太明显。要做到这一点,右键单击堆叠窗口,然后选择转到槽。然后,选择 currentChanged(int)并单击确定。

之后,Qt 会自动创建一个新的槽函数。当堆叠窗口在页面之间切换时,此函数将自动调用。您可以通过检查arg1变量来查看它当前切换到的页面。如果目标页面是堆叠窗口中的第一页,则arg1的值将为0,如果目标是第二页,则为1,依此类推。

只有在堆叠窗口显示仪表板页面时,才能提交 SQL 查询,这是第二页(arg1等于1):

void MainWindow::on_stackedWidget_currentChanged(int arg1) 
{ 
   if (arg1 == 1) 
   { 
      // Do it here 
   } 
} 

哎呀!这一章内容真是太多了!希望这一章能帮助您了解如何为您的项目创建一个美丽而丰富的页面。

摘要

Qt 中的图表模块是功能和视觉美学的结合。它不仅易于实现,而且无需编写非常长的代码来显示图表,而且还可以根据您的视觉要求进行定制。我们真的需要感谢 Qt 开发人员开放了这个模块,并允许非商业用户免费使用它!

在本章中,我们学习了如何使用 Qt 图表模块创建一个真正漂亮的仪表板,并在其上显示不同类型的图表。在接下来的章节中,我们将学习如何使用视图部件、对话框和文件选择对话框。

第五章:项目视图和对话框

在上一章中,我们学习了如何使用不同类型的图表显示数据。图表是向用户在屏幕上呈现信息的许多方式之一。对于您的应用程序来说,向用户呈现重要信息非常重要,这样他们就可以准确地了解应用程序的情况——无论数据是否已成功保存,或者应用程序正在等待用户的输入,或者用户应该注意的警告/错误消息等等——这些都非常重要,以确保您的应用程序的用户友好性和可用性。

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

  • 使用项目视图部件

  • 使用对话框

  • 使用文件选择对话框

  • 图像缩放和裁剪

Qt 为我们提供了许多类型的部件和对话框,我们可以轻松使用它们来向用户显示重要信息。让我们看看这些部件是什么!

使用项目视图部件

除了使用不同类型的图表显示数据外,我们还可以使用不同类型的项目视图来显示这些数据。项目视图部件通过在垂直轴上呈现数据来将数据可视化呈现。

二维项目视图,通常称为表视图,在垂直和水平方向上显示数据。这使它能够在紧凑的空间内显示大量数据,并使用户能够快速轻松地搜索项目。

在项目视图中显示数据有两种方法。最常见的方法是使用模型-视图架构,它使用三个不同的组件,模型、视图和委托,从数据源检索数据并在项目视图中显示它。这些组件都利用 Qt 提供的信号-槽架构来相互通信:

  • 模型的信号通知视图有关数据源保存的数据的更改

  • 视图的信号提供有关用户与正在显示的项目的交互的信息

  • 委托的信号在编辑期间用于告诉模型和视图有关编辑器状态的信息

另一种方法是手动方式,程序员必须告诉 Qt 哪些数据放在哪一列和行。与模型-视图相比,这种方法要简单得多,但在性能上要慢得多。然而,对于少量数据,性能问题可以忽略不计,这是一个很好的方法。

如果您打开 Qt Designer,您将看到两种不同的项目视图部件类别,即项目视图(基于模型)和项目部件(基于项目):

尽管它们看起来可能相同,但实际上这两个类别中的部件工作方式非常不同。在本章中,我们将学习如何使用后一类别,因为它更直观、易于理解,并且可以作为前一类别的先决知识。

在项目部件(基于项目)类别下有三种不同的部件,称为列表部件、树部件和表部件。每个项目部件以不同的方式显示数据。选择适合您需求的部件:

正如您从前面的图表中所看到的,列表部件以一维列表显示其项目,而表部件以二维表格显示其项目。尽管树部件几乎与列表部件类似,但其项目以分层结构显示,其中每个项目下可以递归地有多个子项目。一个很好的例子是我们操作系统中的文件系统,它使用树部件显示目录结构。

为了说明这些区别,让我们创建一个新的 Qt Widgets 应用程序项目,并自己试一试。

创建我们的 Qt Widgets 应用程序

创建项目后,打开mainwindow.ui并将三种不同的项目小部件拖到主窗口中。之后,选择主窗口并点击位于顶部的垂直布局按钮:

然后,双击列表小部件,将弹出一个新窗口。在这里,您可以通过单击+图标向列表小部件添加一些虚拟项目,或者通过选择列表中的项目并单击-图标来删除它们。单击“确定”按钮将最终结果应用于小部件:

您可以对树形小部件执行相同的操作。它几乎与列表小部件相同,只是您可以向项目添加子项目,递归地。您还可以向树形小部件添加列并命名这些列:

最后,双击表格小部件以打开编辑表格小部件窗口。与其他两个项目视图不同,表格小部件是一个二维项目视图,这意味着您可以像电子表格一样向其添加列和行。可以通过在“列”或“行”选项卡中设置所需的名称来为每列和行加标签:

通过使用 Qt Designer,了解小部件的工作原理非常容易。只需将小部件拖放到窗口中并调整其设置,然后构建并运行项目以查看结果。

在这种情况下,我们已经演示了三种不同的项目视图小部件之间的区别,而不需要编写一行代码:

使我们的列表小部件功能化

然而,为了使小部件在应用程序中完全可用,仍然需要编写代码。让我们学习如何使用 C++代码向我们的项目视图小部件添加项目!

首先,打开mainwindow.cpp并在ui->setupui(this)之后的类构造函数中编写以下代码:

ui->listWidget->addItem("My Test Item"); 

就这么简单,您已成功向列表小部件添加了一个项目!

还有另一种方法可以向列表小部件添加项目。但在此之前,我们必须向mainwindow.h添加以下头文件:

#ifndef MAINWINDOW_H 
#define MAINWINDOW_H 

#include <QMainWindow> 
#include <QDebug> 
#include <QListWidgetItem> 

QDebug头文件用于打印调试消息,QListWidgetItem头文件用于声明列表小部件的项目对象。接下来,打开mainwindow.cpp并添加以下代码:

QListWidgetItem* listItem = new QListWidgetItem; 
listItem->setText("My Second Item"); 
listItem->setData(100, 1000); 
ui->listWidget->addItem(listItem); 

前面的代码与前一个一行代码相同。不同的是,这次我向项目添加了额外的数据。setData()函数接受两个输入变量——第一个变量是项目的数据角色,指示 Qt 应如何处理它。如果放入与Qt::ItemDataRole枚举器匹配的值,数据将影响显示、装饰、工具提示等,这可能会改变其外观。

在我的情况下,我只是简单地设置了一个与Qt::ItemDataRole中的任何枚举器都不匹配的数字,以便我可以将其存储为以后使用的隐藏数据。要检索数据,您只需调用data()并插入与您刚刚设置的数字匹配的数字:

qDebug() << listItem->data(100); 

构建并运行项目;您应该能够看到新项目现在已添加到列表小部件中:

有关Qt::ItemDataRole枚举器的更多信息,请查看以下链接:doc.qt.io/qt-5/qt.html#ItemDataRole-enum

如前所述,可以将隐藏数据附加到列表项目以供以后使用。例如,您可以使用列表小部件显示准备由用户购买的产品列表。每个项目都可以附加其产品 ID,以便当用户选择该项目并将其放入购物车时,您的系统可以自动识别已添加到购物车的产品 ID 作为数据角色存储。

在上面的例子中,我在我的列表项中存储了自定义数据1000,并将其数据角色设置为100,这与任何Qt::ItemDataRole枚举器都不匹配。这样,数据就不会显示给用户,因此只能通过 C++代码检索。

向树部件添加功能

接下来,让我们转到树部件。实际上,它与列表部件并没有太大的不同。让我们看一下以下代码:

QTreeWidgetItem* treeItem = new QTreeWidgetItem; 
treeItem->setText(0, "My Test Item"); 
ui->treeWidget->addTopLevelItem(treeItem); 

它与列表部件几乎相同,只是我们必须在setText()函数中设置列 ID。这是因为树部件介于列表部件和表部件之间——它可以有多个列,但不能有任何行。

树部件与其他视图部件最明显的区别是,所有的项都可以递归地包含子项。让我们看一下以下代码,看看我们如何向树部件中的现有项添加子项:

QTreeWidgetItem* treeItem2 = new QTreeWidgetItem; 
treeItem2->setText(0, "My Test Subitem"); 
treeItem->addChild(treeItem2); 

就是这么简单!最终结果看起来像这样:

最后,我们的表部件

接下来,让我们对表部件做同样的操作。从技术上讲,当列和行被创建时,表部件中的项已经存在并被保留。我们需要做的是创建一个新项,并用特定列和行的(当前为空的)项替换它,这就是为什么函数名叫做setItem(),而不是列表部件使用的addItem()

让我们看一下代码:

QTableWidgetItem* tableItem = new QTableWidgetItem; 
tableItem->setText("Testing1"); 
ui->tableWidget->setItem(0, 0, tableItem); 

QTableWidgetItem* tableItem2 = new QTableWidgetItem; 
tableItem2->setText("Testing2"); 
ui->tableWidget->setItem(1, 2, tableItem2); 

从代码中可以看出,我在两个不同的位置添加了两个数据部分,这将转化为以下结果:

就是这样!使用 Qt 中的项视图来显示数据是如此简单和容易。如果你正在寻找与项视图相关的更多示例,请访问以下链接:doc.qt.io/qt-5/examples-itemviews.html

使用对话框

创建用户友好的应用程序的一个非常重要的方面是,在发生某个事件(有意或无意)时,能够显示关于应用程序状态的重要信息。为了显示这样的信息,我们需要一个外部窗口,用户可以在确认信息后将其关闭。

Qt 具有这个功能,它全部驻留在QMessageBox类中。在 Qt 中,你可以使用几种类型的消息框;最基本的一种只需要一行代码,就像这样:

QMessageBox::information(this, "Alert", "Just to let you know, something happened!"); 

对于这个函数,你需要提供三个参数。第一个是消息框的父窗口,我们已经将其设置为主窗口。第二个参数是窗口标题,第三个参数是我们想要传递给用户的消息。上述代码将产生以下结果:

这里显示的外观是在 Windows 系统上运行的。在不同的操作系统(Linux、macOS 等)上,外观可能会有所不同。正如你所看到的,对话框甚至带有文本之前的图标。你可以使用几种类型的图标,比如信息、警告和严重。以下代码向你展示了调用带有图标的不同消息框的代码:

QMessageBox::question(this, "Alert", "Just to let you know, something happened!"); 
QMessageBox::warning(this, "Alert", "Just to let you know, something happened!"); 
QMessageBox::information(this, "Alert", "Just to let you know, something happened!"); 
QMessageBox::critical(this, "Alert", "Just to let you know, something happened!"); 

上述代码产生以下结果:

如果你不需要任何图标,只需调用QMessageBox::about()函数。你还可以通过从 Qt 提供的标准按钮列表中选择来设置你想要的按钮,例如:

QMessageBox::question(this, "Serious Question", "Am I an awesome guy?", QMessageBox::Ignore, QMessageBox::Yes); 

上述代码将产生以下结果:

由于这些是 Qt 提供的内置函数,用于轻松创建消息框,它不会给开发人员完全自定义消息框的自由。但是,Qt 允许您使用另一种方法手动创建消息框,这种方法比内置方法更可定制。这需要更多的代码行,但编写起来仍然相当简单:

QMessageBox msgBox; 
msgBox.setWindowTitle("Alert"); 
msgBox.setText("Just to let you know, something happened!"); 
msgBox.exec(); 

上述代码将产生以下结果:

“看起来完全一样”,你告诉我。那么添加我们自己的图标和自定义按钮呢?这没有问题:

QMessageBox msgBox; 
msgBox.setWindowTitle("Serious Question"); 
msgBox.setText("Am I an awesome guy?"); 
msgBox.addButton("Seriously Yes!", QMessageBox::YesRole); 
msgBox.addButton("Well no thanks", QMessageBox::NoRole); 
msgBox.setIcon(QMessageBox::Question); 
msgBox.exec(); 

上述代码产生以下结果:

在上面的代码示例中,我已经加载了 Qt 提供的问题图标,但如果您打算这样做,您也可以从资源文件中加载自己的图标:

QMessageBox msgBox; 
msgBox.setWindowTitle("Serious Question"); 
msgBox.setText("Am I an awesome guy?"); 
msgBox.addButton("Seriously Yes!", QMessageBox::YesRole); 
msgBox.addButton("Well no thanks", QMessageBox::NoRole); 
QPixmap myIcon(":/images/icon.png"); 
msgBox.setIconPixmap(myIcon); 
msgBox.exec(); 

现在构建并运行项目,您应该能够看到这个奇妙的消息框:

一旦您了解了如何创建自己的消息框,让我们继续学习消息框附带的事件系统。

当用户被呈现具有多个不同选择的消息框时,他/她会期望在按下不同按钮时应用程序有不同的反应。

例如,当消息框弹出并询问用户是否希望退出程序时,按钮“是”应该使程序终止,而“否”按钮将不起作用。

Qt 的QMessageBox类为我们提供了一个简单的解决方案来检查按钮事件。当消息框被创建时,Qt 将等待用户选择他们的选择;然后,它将返回被触发的按钮。通过检查哪个按钮被点击,开发人员可以继续触发相关事件。让我们看一下示例代码:

if (QMessageBox::question(this, "Question", "Some random question. Yes or no?") == QMessageBox::Yes) 
{ 
   QMessageBox::warning(this, "Yes", "You have pressed Yes!"); 
} 
else 
{ 
   QMessageBox::warning(this, "No", "You have pressed No!"); 
} 

上述代码将产生以下结果:

如果您更喜欢手动创建消息框,检查按钮事件的代码会稍微长一些:

QMessageBox msgBox; 
msgBox.setWindowTitle("Serious Question"); 
msgBox.setText("Am I an awesome guy?"); 
QPushButton* yesButton = msgBox.addButton("Seriously Yes!", QMessageBox::YesRole); 
QPushButton* noButton = msgBox.addButton("Well no thanks", QMessageBox::NoRole); 
msgBox.setIcon(QMessageBox::Question); 
msgBox.exec(); 

if (msgBox.clickedButton() == (QAbstractButton*) yesButton) 
{ 
   QMessageBox::warning(this, "Yes", "Oh thanks! :)"); 
} 
else if (msgBox.clickedButton() == (QAbstractButton*) noButton) 
{ 
   QMessageBox::warning(this, "No", "Oh why... :("); 
} 

尽管代码稍微长一些,但基本概念基本相同——被点击的按钮始终可以被开发人员检索以触发适当的操作。然而,这次,Qt 直接检查按钮指针,而不是检查枚举器,因为前面的代码没有使用QMessageBox类的内置标准按钮。

构建项目,您应该能够获得以下结果:

有关对话框的更多信息,请访问以下链接的 API 文档:doc.qt.io/qt-5/qdialog.html

创建文件选择对话框

既然我们已经讨论了消息框的主题,让我们也了解一下另一种类型的对话框——文件选择对话框。文件选择对话框也非常有用,特别是如果您的应用程序经常处理文件。要求用户输入他们想要打开的文件的绝对路径是非常不愉快的,因此文件选择对话框在这种情况下非常方便。

Qt 为我们提供了一个内置的文件选择对话框,看起来与我们在操作系统中看到的一样,因此,对用户来说并不陌生。文件选择对话框本质上只做一件事——让用户选择他们想要的文件或文件夹,并返回所选文件或文件夹的路径;就这些。实际上,它不负责打开文件和读取其内容。

让我们看看如何触发文件选择对话框。首先,打开mainwindow.h并添加以下头文件:

#ifndef MAINWINDOW_H 
#define MAINWINDOW_H 

#include <QMainWindow> 
#include <QFileDialog> 
#include <QDebug> 

接下来,打开mainwindow.cpp并插入以下代码:

QString fileName = QFileDialog::getOpenFileName(this); 
qDebug() << fileName; 

就是这么简单!现在构建并运行项目,您应该会得到这个:

如果用户选择了文件并按下打开,fileName 变量将填充为所选文件的绝对路径。如果用户单击取消按钮,fileName 变量将为空字符串。

文件选择对话框在初始化步骤中还包含几个可以设置的选项。例如:

QString fileName = QFileDialog::getOpenFileName(this, "Your title", QDir::currentPath(), "All files (*.*) ;; Document files (*.doc *.rtf);; PNG files (*.png)"); 
qDebug() << fileName; 

在前面的代码中,我们设置了三件事,它们如下:

  • 文件选择对话框的窗口标题

  • 对话框创建时用户看到的默认路径

  • 文件类型过滤

文件类型过滤在您只允许用户选择特定类型的文件时非常方便(例如,仅允许 JPEG 图像文件),并隐藏其他文件。除了 getOpenFileName(),您还可以使用 getSaveFileName(),它将允许用户指定尚不存在的文件名。

有关文件选择对话框的更多信息,请访问以下链接的 API 文档:doc.qt.io/qt-5/qfiledialog.html

图像缩放和裁剪

由于我们在上一节中学习了文件选择对话框,我想这次我们应该学习一些有趣的东西!

首先,让我们创建一个新的 Qt Widgets 应用程序。然后,打开 mainwindow.ui 并创建以下用户界面:

让我们将这个用户界面分解成三个部分:

  • 顶部—图像预览:

  • 首先,在窗口中添加一个水平布局。

  • 然后,将一个标签小部件添加到我们刚刚添加的水平布局中,然后将文本属性设置为 empty。将标签的 minimumSize 和 maximumSize 属性都设置为 150x150。最后,在 QFrame 类别下设置 frameShape 属性为 Box。

  • 在标签的两侧添加两个水平间隔器,使其居中。

  • 中部—用于调整的滑块:

  • 在窗口中添加一个表单布局,放在我们在步骤 1 中刚刚添加的水平布局下方。

  • 将三个标签添加到表单布局中,并将它们的文本属性分别设置为 比例:水平:垂直:

  • 将三个水平滑块添加到表单布局中。将最小属性设置为 1,最大属性设置为 100。然后,将 pageStep 属性设置为 1

  • 将比例滑块的值属性设置为 100

  • 底部—浏览按钮和保存按钮:

  • 在窗口中添加一个水平布局,放在我们在步骤 2 中添加的表单布局下方。

  • 将两个按钮添加到水平布局中,并将它们的文本属性分别设置为 浏览保存

    • 最后,从中央小部件中删除菜单栏、工具栏和状态栏。

现在我们已经创建了用户界面,让我们开始编码吧!首先,打开 mainwindow.h 并添加以下头文件:

#ifndef MAINWINDOW_H 
#define MAINWINDOW_H 

#include <QMainWindow> 
#include <QMessageBox> 
#include <QFileDialog> 
#include <QPainter> 

然后,将以下变量添加到 mainwindow.h

private: 
   Ui::MainWindow *ui; 
   bool canDraw; 
   QPixmap* pix; 
   QSize imageSize; 
   QSize drawSize; 
   QPoint drawPos; 

然后,返回到 mainwindow.ui,右键单击浏览按钮,然后选择转到槽。然后,一个窗口将弹出并要求您选择一个信号。选择位于列表顶部的 clicked() 信号,然后按下 OK 按钮:

在您的源文件中将自动添加一个新的 slot 函数。现在,添加以下代码以在单击浏览按钮时打开文件选择对话框。对话框仅列出 JPEG 图像并隐藏其他文件:

void MainWindow::on_browseButton_clicked() 
{ 
   QString fileName = QFileDialog::getOpenFileName(this, tr("Open   
   Image"), QDir::currentPath(), tr("Image Files (*.jpg *.jpeg)")); 

   if (!fileName.isEmpty()) 
   { 
         QPixmap* newPix = new QPixmap(fileName); 

         if (!newPix->isNull()) 
         { 
               if (newPix->width() < 150 || newPix->height() < 150) 
               { 
                     QMessageBox::warning(this, tr("Invalid Size"), 
                     tr("Image size too small. Please use an image  
                     larger than 150x150.")); 
                     return; 
               } 

               pix = newPix; 
               imageSize = pix->size(); 
               drawSize = pix->size(); 

               canDraw = true; 

         } 
         else 
         { 
               canDraw = false; 

               QMessageBox::warning(this, tr("Invalid Image"), 
               tr("Invalid or corrupted file. Please try again with  
               another image file.")); 
         } 
   } 
} 

如您所见,代码检查用户是否选择了任何图像。如果选择了图像,它会再次检查图像分辨率是否至少为 150 x 150。如果没有问题,我们将保存图像的像素映射到名为 pix 的指针中,然后将图像大小保存到 imageSize 变量中,并将初始绘图大小保存到 drawSize 变量中。最后,我们将 canDraw 变量设置为 true

之后,再次打开 mainwindow.h 并声明以下两个函数:

public: 
   explicit MainWindow(QWidget *parent = 0); 
   ~MainWindow(); 
   virtual void paintEvent(QPaintEvent *event); 
   void paintImage(QString fileName, int x, int y); 

第一个函数paintEvent()是一个虚函数,每当 Qt 需要刷新用户界面时(例如当主窗口被调整大小时),它就会自动调用。我们将重写这个函数,并将新加载的图像绘制到图像预览部件上。在这种情况下,我们将在paintEvent()虚函数中调用paintImage()函数:

void MainWindow::paintEvent(QPaintEvent *event) 
{ 
   if (canDraw) 
   { 
         paintImage("", ui->productImage->pos().x(), ui->productImage-
         >pos().y()); 
   } 
} 

之后,我们将在mainwindow.cpp中编写paintImage()函数:

void MainWindow::paintImage(QString fileName, int x, int y) 
{ 
   QPainter painter; 
   QImage saveImage(150, 150, QImage::Format_RGB16); 

   if (!fileName.isEmpty()) 
   { 
         painter.begin(&saveImage); 
   } 
   else 
   { 
         painter.begin(this); 
   } 

   if (!pix->isNull()) 
   { 
         painter.setClipRect(x, y, 150, 150); 
         painter.fillRect(QRect(x, y, 150, 150), Qt::SolidPattern); 
         painter.drawPixmap(x - drawPos.x(), y - drawPos.y(), 
         drawSize.width(), drawSize.height(), *pix); 
   } 

   painter.end(); 

   if (fileName != "") 
   { 
         saveImage.save(fileName); 
         QMessageBox::information(this, "Success", "Image has been 
         successfully saved!"); 
   } 
} 

此函数有两个作用——如果我们不设置fileName变量,它将继续在图像预览部件上绘制图像,否则,它将根据图像预览部件的尺寸裁剪图像,并根据fileName变量将其保存到磁盘上。

当单击保存按钮时,我们将再次调用此函数。这次,我们将设置fileName变量为所需的目录路径和文件名,以便QPainter类可以正确保存图像:

void MainWindow::on_saveButton_clicked() 
{ 
   if (canDraw) 
   { 
         if (!pix->isNull()) 
         { 
               // Save new pic from painter 
               paintImage(QCoreApplication::applicationDirPath() + 
               "/image.jpg", 0, 0); 
         } 
   } 
} 

最后,右键单击三个滑块中的每一个,然后选择“转到槽”。然后,选择valueChanged(int)并单击“确定”。

之后,我们将编写从上一步骤中得到的slot函数的代码:

void MainWindow::on_scaleSlider_valueChanged(int value) 
{ 
   drawSize = imageSize * value / 100; 
   update(); 
} 

void MainWindow::on_leftSlider_valueChanged(int value) 
{ 
   drawPos.setX(value * drawSize.width() / 100 * 0.5); 
   update(); 
} 

void MainWindow::on_topSlider_valueChanged(int value) 
{ 
   drawPos.setY(value * drawSize.height() / 100 * 0.5); 
   update(); 
} 

比例滑块基本上是供用户在图像预览部件内调整所需比例的。左侧滑块是供用户水平移动图像的,而顶部滑块是供用户垂直移动图像的。通过组合这三个不同的滑块,用户可以在将图像上传到服务器之前,或者用于其他目的之前,调整和裁剪图像以满足他们的喜好。

如果您现在构建并运行项目,您应该能够获得以下结果:

您可以单击“浏览”按钮选择要加载的 JPG 图像文件。之后,图像应该会出现在预览区域。然后,您可以移动滑块来调整裁剪大小。一旦您对结果满意,点击“保存”按钮将图像保存在当前目录中。

如果您想详细了解,请查看本书附带的示例代码。您可以在以下 GitHub 页面找到源代码:github.com/PacktPublishing/Hands-On-GUI-Programming-with-C-QT5

摘要

输入和输出(I/O)是现代计算机软件的本质。Qt 允许我们以许多直观和引人入胜的方式显示我们的数据给最终用户。除此之外,Qt 提供的事件系统使得作为程序员的我们的生活变得更加轻松,因为它倾向于通过强大的信号和槽机制自动捕获用户输入,并触发自定义行为。没有 Qt,我们将很难想出如何重新发明这个老生常谈的轮子,并最终可能会创建一个不太用户友好的产品。

在本章中,我们学习了如何利用 Qt 提供的出色功能——视图部件、对话框和文件选择对话框,用于向用户显示重要信息。此外,我们还通过一个有趣的小项目学习了如何使用 Qt 部件对用户输入进行缩放和裁剪图像。在下一章中,我们将尝试更高级(也更有趣)的内容,即使用 Qt 创建我们自己的网络浏览器!

第六章:集成网络内容

在上一章中,我们学习了如何在 Qt 中使用项目视图和对话框。在这一章中,我们将学习如何将网络内容集成到我们的 Qt 应用程序中。

从 90 年代末和 21 世纪初的互联网时代开始,我们的世界变得越来越被互联网连接。自然地,运行在我们计算机上的应用程序也朝着这个方向发展。如今,我们大多数——如果不是全部——的软件在某种程度上都与互联网连接,通常是为了检索有用的信息并将其显示给用户。最简单的方法是将网络浏览器显示(也称为网络视图)嵌入到应用程序的用户界面中。这样,用户不仅可以查看信息,而且可以以美观的方式进行查看。

通过使用网络视图,开发人员可以利用其渲染能力,并使用HTML(超文本标记语言)和CSS(层叠样式表)的强大组合来装饰他们的内容。在这一章中,我们将探索 Qt 的 web 引擎模块,并创建我们自己的网络浏览器。

在这一章中,我们将涵盖以下主题:

  • 创建你自己的网络浏览器

  • 会话、cookie 和缓存

  • 集成 JavaScript 和 C++

话不多说,让我们看看如何在 Qt 中创建我们自己的网络浏览器!

创建你自己的网络浏览器

从前,Qt 使用一个名为WebKit的不同模块在其用户界面上渲染网络内容。然而,自 5.5 版本以来,WebKit 模块已完全被弃用,并被一个名为WebEngine的新模块所取代。

新的 WebEngine 模块是基于谷歌构建的Chromium框架,它只能在 Windows 平台上的Visual C++编译器上运行。因此,如果你在运行 Windows,确保你已经在你的计算机上安装了Microsoft Visual Studio以及与你的计算机上安装的 Visual Studio 版本匹配的 Qt 的MSVC组件。除此之外,这个特定章节还需要 Qt WebEngine 组件。如果你在 Qt 的安装过程中跳过了这些组件,你只需要再次运行相同的安装程序并在那里安装它:

添加网络视图小部件

一旦你准备好了,让我们开始吧!首先,打开 Qt Creator 并创建一个新的 Qt Widgets 应用程序项目。之后,打开项目(.pro)文件并添加以下文本以启用模块:

QT += core gui webengine webenginewidgets 

如果你没有安装 MSVC 组件(在 Windows 上)或 Qt WebEngine 组件,如果你尝试构建项目,此时将会出现错误消息。如果是这种情况,请再次运行 Qt 安装程序。

接下来,打开mainwindow.h并添加以下头文件:

#ifndef MAINWINDOW_H 
#define MAINWINDOW_H 

#include <QMainWindow> 
#include <QWebEngineView> 

之后,打开mainwindow.h并添加以下代码:

private: 
   Ui::MainWindow *ui; 
 QWebEngineView* webview; 

然后,添加以下代码:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

   webview = new QWebEngineView(ui->centralWidget); 
   webview->load(QUrl("http://www.kloena.com")); 
} 

现在构建并运行程序,你应该看到以下结果:

就是这么简单。你现在已经成功地在你的应用程序上放置了一个网络视图!

我们使用 C++代码创建网络视图的原因是,Qt Creator 使用的默认 Qt Designer 在小部件框中没有网络视图。前面的代码简单地创建了QWebEngineView对象,设置了它的父对象(在这种情况下是中央小部件),并在显示网络视图小部件之前设置了网页的 URL。如果你想使用 Qt Designer 在你的 UI 上放置一个 web 引擎视图,你必须运行独立的 Qt Designer,它位于你的 Qt 安装目录中。例如,如果你在 Windows 上运行,它位于C:QtQt5.10.25.10.2msvc2017_64bin。请注意,它位于支持 web 引擎的编译器名称的目录中:

为网络浏览器创建用户界面

接下来,我们将把它变成一个合适的网络浏览器。首先,我们需要添加一些布局小部件,以便稍后可以放置其他小部件。将垂直布局(1)拖放到 centralWidget 上,并从对象列表中选择 centralWidget。然后,点击位于顶部的 Lay Out Vertically 按钮(2):

完成后,选择新添加的垂直布局,右键单击,选择 Morph into | QFrame。我们这样做的原因是,我们希望将 web 视图小部件放在这个 QFrame 对象下,而不是中心小部件下。我们必须将布局小部件转换为 QFrame(或任何继承自 QWidget 的)对象,以便它可以采用web 视图作为其子对象。最后,将 QFrame 对象重命名为webviewFrame

完成后,让我们将水平布局小部件拖放到 QFrame 对象上方。现在我们可以看到水平布局小部件和 QFrame 对象的大小是相同的,我们不希望这样。接下来,选择 QFrame 对象,并将其垂直策略设置为 Expanding:

然后,您会看到顶部布局小部件现在非常窄。让我们暂时将其高度设置为20,如下所示:

完成后,将三个按钮拖放到水平布局中,现在我们可以将其顶部边距设置回0

将按钮的标签分别设置为BackForwardRefresh。您也可以使用图标而不是文本显示在这些按钮上。如果您希望这样做,只需将文本属性设置为空,并从图标属性中选择一个图标。为了简单起见,我们将在本教程中只在按钮上显示文本。

接下来,在三个按钮的右侧放置一个行编辑小部件,然后再添加另一个带有Go标签的按钮:

完成后,右键单击每个按钮,然后选择转到插槽。窗口将弹出,选择 clicked(),然后按 OK。

这些按钮的信号函数将看起来像这样:

void MainWindow::on_backButton_clicked() 
{ 
   webview->back(); 
} 

void MainWindow::on_forwardButton_clicked() 
{ 
   webview->forward(); 
} 

void MainWindow::on_refreshButton_clicked() 
{ 
   webview->reload(); 
} 

void MainWindow::on_goButton_clicked() 
{ 
   loadPage(); 
} 

基本上,QWebEngineView类已经为我们提供了back()forward()reload()等函数,所以我们只需在按下相应按钮时调用这些函数。然而,loadPage()函数是我们将编写的自定义函数。

void MainWindow::loadPage() 
{ 
   QString url = ui->addressInput->text(); 
   if (!url.startsWith("http://") && !url.startsWith("https://")) 
   { 
         url = "http://" + url; 
   } 
   ui->addressInput->setText(url); 
   webview->load(QUrl(url)); 
} 

记得在mainwindow.h中添加loadPage()的声明。

我们不应该只调用load()函数,我认为我们应该做更多的事情。通常,用户在输入网页 URL 时不会包括http://(或https://)方案,但当我们将 URL 传递给 web 视图时,这是必需的。为了解决这个问题,我们会自动检查方案的存在。如果没有找到任何方案,我们将手动将http://方案添加到 URL 中。还要记得在开始时调用它来替换load()函数:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

 webview = new QWebEngineView(ui->webviewFrame); 
   loadPage(); 
} 

接下来,右键单击文本输入,然后选择转到插槽。然后,选择 returnPressed(),点击 OK 按钮:

用户在完成输入网页 URL 后,按键盘上的Return键时,将调用此插槽函数。从逻辑上讲,用户希望页面开始加载,而不必每次输入 URL 后都要按 Go 按钮。代码非常简单,我们只需调用前面步骤中创建的loadPage()函数:

void MainWindow::on_addressInput_returnPressed() 
{ 
   loadPage(); 
} 

现在我们已经完成了大量的代码,让我们构建并运行我们的项目,看看结果如何:

显示的结果看起来并不是很好。由于某种原因,新的 Web 视图似乎在扩展大小策略上也无法正确缩放,至少在编写本书时使用的 Qt 版本 5.10 上是如此。这个问题可能会在将来的版本中得到修复,但让我们找到解决这个问题的方法。我所做的是重写主窗口中继承的函数paintEvent()。在mainwindow.h中,只需添加函数声明,就像这样:

public: 
   explicit MainWindow(QWidget *parent = 0); 
   ~MainWindow(); 
 void paintEvent(QPaintEvent *event); 

然后,在mainwindow.cpp中编写其定义,就像这样:

void MainWindow::paintEvent(QPaintEvent *event) 
{ 
   QMainWindow::paintEvent(event); 
   webview->resize(ui->webviewFrame->size()); 
} 

当主窗口需要重新渲染其部件时(例如当窗口被调整大小时),Qt 会自动调用paintEvent()函数。由于这个函数在应用程序初始化时和窗口调整大小时都会被调用,我们将使用这个函数手动调整 Web 视图的大小以适应其父部件。

再次构建和运行程序,你应该能够让 Web 视图很好地适应,无论你如何调整主窗口的大小。此外,我还删除了菜单栏、工具栏和状态栏,以使整个界面看起来更整洁,因为我们在这个应用程序中没有使用这些功能:

接下来,我们需要一个进度条来显示用户当前页面加载的进度。为此,首先我们需要在 Web 视图下方放置一个进度条部件:

然后,在mainwindow.h中添加这两个槽函数:

private slots: 
   void on_backButton_clicked(); 
   void on_forwardButton_clicked(); 
   void on_refreshButton_clicked(); 
   void on_goButton_clicked(); 
   void on_addressInput_returnPressed(); 
   void webviewLoading(int progress); 
   void webviewLoaded(); 

它们在mainwindow.cpp中的函数定义如下:

void MainWindow::webviewLoading(int progress) 
{ 
   ui->progressBar->setValue(progress); 
} 

void MainWindow::webviewLoaded() 
{ 
   ui->addressInput->setText(webview->url().toString()); 
} 

第一个函数webviewLoading()简单地从 Web 视图中获取进度级别(以百分比值的形式)并直接提供给进度条部件。

第二个函数webviewLoaded()将用 Web 视图加载的网页的实际 URL 替换地址输入框上的 URL 文本。如果没有这个函数,地址输入框在你按下返回按钮或前进按钮后将不会显示正确的 URL。完成后,让我们再次编译和运行项目。结果看起来很棒:

你可能会问我,如果我不是使用 Qt 制作 Web 浏览器,这有什么实际用途?将 Web 视图嵌入到应用程序中还有许多其他用途,例如,通过精美装饰的 HTML 页面向用户展示产品的最新新闻和更新,这是游戏市场上大多数在线游戏使用的常见方法。例如,流媒体客户端也使用 Web 视图来向玩家展示最新的游戏和折扣。

这些通常被称为混合应用程序,它们将 Web 内容与本地 x 结合在一起,因此你可以利用来自 Web 的动态内容以及具有高性能和一致外观和感觉优势的本地运行的代码。

除此之外,你还可以使用它来以 HTML 格式显示可打印的报告。你可以通过调用webview->page()->print()webview->page()->printToPdf()轻松地将报告发送到打印机,或将其保存为 PDF 文件。

要了解更多关于从 Web 视图打印的信息,请查看以下链接:doc.Qt.io/Qt-5/qwebenginepage.html#print.

你可能还想使用 HTML 创建程序的整个用户界面,并将所有 HTML、CSS 和图像文件嵌入到 Qt 的资源包中,并从 Web 视图本地运行。可能性是无限的,唯一的限制是你的想象力!

要了解更多关于 Qt WebEngine 的信息,请查看这里的文档:doc.Qt.io/Qt-5/qtwebengine-overview.html.

管理浏览器历史记录

Qt 的 Web 引擎将用户访问过的所有链接存储在一个数组结构中以供以后使用。Web 视图部件使用这个结构通过调用back()forward()在历史记录中来回移动。

如果需要手动访问此浏览历史记录,请在mainwindow.h中添加以下头文件:

#include <QWebEnginePage> 

然后,使用以下代码以获取以QWebEngineHistory对象形式的浏览历史记录:

QWebEngineHistory* history = QWebEnginePage::history(); 

您可以从history->items()获取访问链接的完整列表,或者使用back()forward()等函数在历史记录之间导航。要清除浏览历史记录,请调用history->clear()。或者,您也可以这样做:

QWebEngineProfile::defaultProfile()->clearAllVisitedLinks();

要了解更多关于QWebEngineHistory类的信息,请访问以下链接:doc.Qt.io/Qt-5/qwebenginehistory.html.

会话、cookie 和缓存

与任何其他网络浏览器一样,WebEngine模块还支持用于存储临时数据和持久数据的机制,用于会话和缓存。会话和缓存非常重要,因为它们允许网站记住您的上次访问并将您与数据关联,例如购物车。会话、cookie 和缓存的定义如下所示:

  • 会话:通常,会话是包含用户信息和唯一标识符的服务器端文件,从客户端发送以将它们映射到特定用户。然而,在 Qt 中,会话只是指没有任何过期日期的 cookie,因此当程序关闭时它将消失。

  • Cookie:Cookie 是包含用户信息或任何您想要保存的其他信息的客户端文件。与会话不同,cookie 具有过期日期,这意味着它们将保持有效,并且可以在到达过期日期之前检索,即使程序已关闭并重新打开。

  • 缓存:缓存是一种用于加快页面加载速度的方法,通过在首次加载时将页面及其资源保存到本地磁盘。如果用户在下次访问时再次加载同一页面,Web 浏览器将重用缓存的资源,而不是等待下载完成,这可以显著加快页面加载时间。

管理会话和 cookie

默认情况下,WebEngine不保存任何 cookie,并将所有用户信息视为临时会话,这意味着当您关闭程序时,您在网页上的登录会话将自动失效。

要在 Qt 的WebEngine模块上启用 cookie,首先在mainwindow.h中添加以下头文件:

#include <QWebEngineProfile> 

然后,只需调用以下函数以强制使用持久性 cookie:

QWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(QWebEngineProfile::ForcePersistentCookies);

调用上述函数后,您的登录会话将在关闭程序后继续存在。要恢复为非持久性 cookie,我们只需调用:

QWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); 

除此之外,您还可以更改 Qt 程序存储 cookie 的目录。要做到这一点,请将以下代码添加到您的源文件中:

QWebEngineProfile::defaultProfile()->setPersistentStoragePath("your folder");  

如果出于某种原因,您想手动删除所有 cookie,请使用以下代码:

QWebEngineProfile::defaultProfile()->cookieStore()->deleteAllCookies(); 

管理缓存

接下来,让我们谈谈缓存。在 Web 引擎模块中,有两种类型的缓存,即内存缓存和磁盘缓存。内存缓存使用计算机的内存来存储缓存,一旦关闭程序就会消失。另一方面,磁盘缓存将所有文件保存在硬盘中,因此它们将在关闭计算机后仍然存在。

默认情况下,Web 引擎模块将所有缓存保存到磁盘,如果需要将它们更改为内存缓存,请调用以下函数:

QWebEngineProfile::defaultProfile()->setHttpCacheType(QWebEngineProfile::MemoryHttpCache); 

或者,您也可以通过调用完全禁用缓存:

QWebEngineProfile::defaultProfile()->setHttpCacheType(QWebEngineProfile::NoCache); 

要更改程序保存缓存文件的文件夹,请调用setCachePath()函数:

QWebEngineProfile::defaultProfile()->setCachePath("your folder"); 

最后,要删除所有缓存文件,请调用clearHttpCache()

QWebEngineProfile::defaultProfile()->clearHttpCache(); 

还有许多其他函数可用于更改与 cookie 和缓存相关的设置。

您可以在以下链接中了解更多信息:doc.Qt.io/Qt-5/qwebengineprofile.html

集成 JavaScript 和 C++

使用 Qt 的 Web 引擎模块的一个强大功能是它可以从 C++调用 JavaScript 函数,以及从 JavaScript 调用 C++函数。这使它不仅仅是一个 Web 浏览器。您可以使用它来访问 Web 浏览器标准不支持的功能,例如文件管理和硬件集成。这些功能在 W3C 标准中是不可能的;因此,无法在原生 JavaScript 中实现。但是,您可以使用 C++和 Qt 来实现这些功能,然后简单地从 JavaScript 中调用 C++函数。让我们看看如何在 Qt 中实现这一点。

从 C++调用 JavaScript 函数

之后,将以下代码添加到我们刚创建的 HTML 文件中:

<!DOCTYPE html><html> 
   <head> 
      <title>Page Title</title> 
   </head> 
   <body> 
      <p>Hello World!</p> 
   </body> 
</html> 

这些是基本的 HTML 标记,除了显示一行文字Hello World!之外,什么也不显示。您可以尝试使用 Web 浏览器加载它:

之后,让我们返回到我们的 Qt 项目中,然后转到文件|新建文件或项目,并创建一个 Qt 资源文件:

然后,打开我们刚创建的 Qt 资源文件,并在 HTML 文件中添加/html前缀,然后将 HTML 文件添加到资源文件中,就像这样:

在资源文件仍然打开的情况下,右键单击text.html,然后选择复制资源路径到剪贴板。然后,立即更改您的 Web 视图的 URL:

webview->load(QUrl("qrc:///html/test.html")); 

您可以使用刚从资源文件中复制的链接,但请确保在链接前面添加 URL 方案qrc://。现在构建并运行您的项目,您应该能够立即看到结果:

接下来,我们需要在 JavaScript 中设置一个函数,稍后将由 C++调用。我们将创建一个简单的函数,当调用时弹出一个简单的消息框并将Hello World!文本更改为其他内容:

<!DOCTYPE html> 
<html> 
   <head> 
         <title>Page Title</title> 
         <script> 
               function hello() 
               { 
                  document.getElementById("myText").innerHTML =       
                  "Something happened!"; 
                  alert("Good day sir, how are you?"); 
               } 
         </script> 
   </head> 
   <body> 
         <p id="myText">Hello World!</p> 
   </body> 
</html> 

请注意,我已经为Hello World!文本添加了一个 ID,以便我们能够找到它并更改其文本。完成后,让我们再次转到我们的 Qt 项目。

让我们继续向程序 UI 添加一个按钮,当按钮被按下时,我们希望我们的 Qt 程序调用我们刚刚在 JavaScript 中创建的hello()函数。在 Qt 中做到这一点实际上非常容易;您只需从QWebEnginePage类中调用runJavaScript()函数,就像这样:

void MainWindow::on_pushButton_clicked() 
{ 
   webview->page()->runJavaScript("hello();"); 
} 

结果非常惊人,您可以从以下截图中看到:

您可以做的远不止更改文本或调用消息框。例如,您可以在 HTML 画布中启动或停止动画,显示或隐藏 HTML 元素,触发 Ajax 事件以从 PHP 脚本中检索信息,等等...无限的可能性!

从 JavaScript 调用 C++函数

接下来,让我们看看如何从 JavaScript 中调用 C++函数。为了演示,我将在 Web 视图上方放置一个文本标签,并使用 JavaScript 函数更改其文本:

通常,JavaScript 只能在 HTML 环境中工作,因此只能更改 HTML 元素,而不能更改 Web 视图之外的内容。但是,Qt 允许我们通过使用 Web 通道模块来做到这一点。因此,让我们打开我们的项目(.pro)文件并将 Web 通道模块添加到项目中:

QT += core gui webengine webenginewidgets webchannel 

之后,打开mainwindow.h并添加QWebChannel头文件:

#include <QMainWindow> 
#include <QWebEngineView> 
#include <QWebChannel> 

同时,我们还声明一个名为doSomething()的函数,并在其前面加上Q_INVOKABLE宏:

Q_INVOKABLE void doSomething(); 

Q_INVOKABLE宏告诉 Qt 将函数暴露给 JavaScript 引擎,因此该函数可以从 JavaScript(以及 QML,因为 QML 也基于 JavaScript)中调用。

然后在mainwindow.cpp中,我们首先需要创建一个QWebChannel对象,并将我们的主窗口注册为 JavaScript 对象。只要从QObject类派生,就可以将任何 Qt 对象注册为 JavaScript 对象。

由于我们将从 JavaScript 中调用doSomething()函数,因此我们必须将主窗口注册到 JavaScript 引擎。之后,我们还需要将刚刚创建的QWebChannel对象设置为我们的 web 视图的 web 通道。代码如下所示:

QWebChannel* channel = new QWebChannel(this); 
channel->registerObject("mainwindow", this); 
webview->page()->setWebChannel(channel); 

完成后,让我们定义doSomething()函数。我们只是做一些简单的事情——改变我们的 Qt GUI 上的文本标签,就这样:

void MainWindow::doSomething() 
{ 
   ui->label->setText("This text has been changed by javascript!"); 
} 

我们已经完成了 C++代码,让我们打开 HTML 文件。我们需要做一些事情才能使其工作。首先,我们需要包含默认嵌入在 Qt 程序中的qwebchannel.js脚本,这样您就不必在 Qt 目录中搜索该文件。在head标签之间添加以下代码:

<script type="text/javascript" src="img/qwebchannel.js"></script> 

然后,在 JavaScript 中,当文档成功被 web 视图加载时,我们创建一个QWebChannel对象,并将mainwindow变量链接到之前在 C++中注册的实际主窗口对象。这一步必须在网页加载后才能完成(通过window.onload回调);否则,可能会出现创建 web 通道的问题:

var mainwindow; 
window.onload = function() 
{ 
   new QWebChannel(Qt.webChannelTransport,function(channel) 
   { 
         mainwindow = channel.objects.mainwindow; 
   }); 
} 

之后,我们创建一个调用doSomething()函数的 JavaScript 函数:

function myFunction() 
{ 
   mainwindow.doSomething(); 
} 

最后,在 HTML 主体中添加一个按钮,并确保在按下按钮时调用myFunction()

<body> 
   <p id="myText">Hello World!</p> 
   <button onclick="myFunction()">Do Something</button> 
</body> 

现在构建并运行程序,您应该能够获得以下结果:

除了更改 Qt 小部件的属性之外,您可以使用此方法做很多有用的事情。例如,将文件保存到本地硬盘,从条形码扫描仪获取扫描数据等。本地和 Web 技术之间不再有障碍。但是,请格外注意此技术可能带来的安全影响。正如古话所说:

“伟大的力量带来伟大的责任。”

摘要

在本章中,我们已经学会了如何创建自己的网络浏览器,并使其与本地代码交互。Qt 为我们提供了 Web 通道技术,使 Qt 成为软件开发的一个非常强大的平台。

它充分利用了 Qt 的强大功能和 Web 技术的美感,这意味着在开发时你可以有更多的选择,而不仅仅局限于 Qt 的方法。我非常兴奋,迫不及待地想看看你能用这个技术实现什么!

加入我们的下一章,学习如何创建一个类似 Google Maps 的地图查看器,使用 Qt!

第七章:地图查看器

用户位置和地图显示是如今变得更加常见的两个功能,已经被用于各种类型的应用程序。它们通常用于后端分析和前端显示目的。

地图查看器可用于导航、附近的兴趣点查找、基于位置的服务(如叫出租车)等等。你可以使用 Qt 来实现大部分功能,但如果你要做更复杂的东西,就需要一个先进的数据库系统。

在上一章中,我们学习了如何将 Web 浏览器嵌入到应用程序中。在本章中,我们将尝试一些更有趣的东西,涵盖以下主题:

  • 创建地图显示

  • 标记和形状显示

  • 获取用户位置

  • 地理路由请求

让我们继续创建我们自己的地图查看器!

地图显示

Qt 位置模块为开发者提供了地理编码和导航信息的访问权限。它还可以允许用户进行地点搜索,需要从服务器或用户设备中检索数据。

目前,Qt 的地图视图不支持 C++,只支持 QML。这意味着我们只能使用 QML 脚本来改变与可视化相关的任何内容——显示地图,添加标记等等;另一方面,我们可以使用模块提供的 C++类来从数据库或服务提供商获取信息,然后通过 QML 将其显示给用户。

简单来说,QMLQt 建模语言)是用于 Qt Quick 应用程序的用户界面标记语言。由于 QML 由 JavaScript 框架驱动,其编码语法几乎与 JavaScript 相似。如果你需要深入学习 QML 和 Qt Quick,请继续阅读第十四章,Qt Quick 和 QML,因为这是一个专门的章节。

有许多教程教你如何使用 Qt Quick 和 QML 语言创建一个完整的地图查看器,但并没有很多教你如何将 C++与 QML 结合使用。让我们开始吧!

设置 Qt 位置模块

  1. 首先,创建一个新的 Qt Widgets 应用程序项目。

  2. 之后,打开项目文件(.pro)并将以下模块添加到你的 Qt 项目中:

QT += core gui location qml quickwidgets 

除了location模块,我们还添加了qmlquickwidgets模块,这些模块是下一节地图显示小部件所需的。这就是我们在项目中启用Qt Location模块所需要做的。接下来,我们将继续向项目中添加地图显示小部件。

创建地图显示

准备好后,让我们打开mainwindow.ui,并移除 menuBar、toolBar 和 statusBar,因为在这个项目中我们不需要这些东西:

然后,从小部件框中拖动一个 QQuickWidget 到 UI 画布上。然后,点击画布顶部的水平布局按钮,为其添加布局属性:

然后,将中央小部件的所有边距属性设置为 0:

接下来,我们需要创建一个名为mapview.qml的新文件,方法是转到文件 | 新建文件或项目... 然后选择 Qt 类别并选择 QML 文件(Qt Quick 2):

一旦 QML 文件创建完成,打开它并添加以下代码以包含locationpositioning模块,以便稍后可以使用其功能:

import QtQuick 2.0 
import QtLocation 5.3 
import QtPositioning 5.0 

之后,我们创建一个Plugin对象并命名为osmOpen Street Map),然后创建一个 Map 对象并将插件应用到其plugin属性上。我们还将起始坐标设置为(40.7264175,-73.99735),这是纽约的某个地方。除此之外,默认的缩放级别设置为14,足以让我们有一个良好的城市视图:

Item 
{ 
    Plugin 
    { 
        id: mapPlugin 
        name: "osm" 
    } 

    Map 
    { 
        id: map 
        anchors.fill: parent 
        plugin: mapPlugin 
        center: QtPositioning.coordinate(40.7264175,-73.99735) 
        zoomLevel: 14 
    } 
} 

在我们能够在应用程序上显示地图之前,我们必须先创建一个资源文件并将 QML 文件添加到其中。这可以通过转到文件 | 创建新文件或项目...来完成。然后,选择 Qt 类别并选择 Qt 资源文件。

资源文件创建完成后,添加一个名为qml的前缀,并将 QML 文件添加到前缀中,如下所示:

现在我们可以打开mainwindow.ui并将 QQuickWidget 的source属性设置为qrc:/qml/mapview.qml。您还可以点击源属性后面的按钮,直接从资源中选择 QML 文件。

完成后,让我们编译并运行项目,看看我们得到了什么!您也可以尝试使用鼠标在地图上平移和放大缩小:

即使我们可以通过使用 web 视图小部件来实现相同的结果,但这将使我们编写大量的 JavaScript 代码来显示地图。通过使用 Qt Quick,我们只需要编写几行简单的 QML 代码就可以了。

标记和形状显示

在前面的部分中,我们成功创建了地图显示,但这只是这个项目的开始。我们需要能够以标记或形状的形式显示自定义数据,以便用户能够理解这些数据。

在地图上显示位置标记

如果我告诉你我的最喜欢的餐厅位于(40.7802655, -74.108644),你可能无法理解。然而,如果这些坐标以位置标记的形式显示在地图视图上,你会立刻知道它在哪里。让我们看看如何向地图视图添加位置标记!

首先,我们需要一个标记图像,应该看起来像这样,或者更好的是,设计你自己的标记:

之后,我们需要将这个图像注册到我们项目的资源文件中。用 Qt Creator 打开resource.qrc,创建一个名为images的新前缀。然后,将标记图像添加到新创建的前缀中。确保图像具有透明背景,以便在地图上显示良好。

接下来,打开mapview.qml并用以下代码替换原来的代码:

Item 
{ 
    id: window 

    Plugin 
    { 
        id: mapPlugin 
        name: "osm" 
    } 

    Image 
    { 
        id: icon 
        source: "qrc:///images/map-marker-icon.png" 
        sourceSize.width: 50 
        sourceSize.height: 50 
    } 

    MapQuickItem 
    { 
        id: marker 
        anchorPoint.x: marker.width / 4 
        anchorPoint.y: marker.height 
        coordinate: QtPositioning.coordinate(40.7274175,-73.99835) 

        sourceItem: icon 
    } 

    Map 
    { 
        id: map 
        anchors.fill: parent 
        plugin: mapPlugin 
        center: QtPositioning.coordinate(40.7264175,-73.99735) 
        zoomLevel: 14 

        Component.onCompleted: 
        { 
            map.addMapItem(marker) 
        } 
    } 
} 

在上面的代码中,我们首先添加了一个图像对象,它将用作标记的图像。由于原始图像非常庞大,我们必须通过将sourceSize属性设置为50x50来调整其大小。我们还必须将标记图像的锚点设置为图像的中心底部,因为那是标记的尖端所在的位置。

之后,我们创建一个MapQuickItem对象,它将作为标记本身。将标记图像设置为MapQuickItem对象的sourceItem,然后通过调用map.addMapItem()将标记添加到地图上。这个函数必须在地图创建并准备好显示之后调用,这意味着我们只能在Component.onCompleted事件触发后调用它。

现在我们完成了代码,让我们编译并查看结果:

尽管现在看起来一切都很好,但我们不想在 QML 中硬编码标记。想象一下向地图添加数百个标记,手动使用不同的代码添加每个标记是不可能的。

为了创建一个允许我们动态创建位置标记的函数,我们需要先将标记的 QML 代码从mapview.qml中分离出来,放到一个新的 QML 文件中。让我们创建一个名为marker.qml的新 QML 文件,并将其添加到资源文件中:

接下来,从mapview.qml中删除MapQuickItemImage对象,并将其移动到marker.qml中:

import QtQuick 2.0 
import QtLocation 5.3 

MapQuickItem 
{ 
    id: marker 
    anchorPoint.x: marker.width / 4 
    anchorPoint.y: marker.height 
    sourceItem: Image 
    { 
        id: icon 
        source: "qrc:///images/map-marker-icon.png" 
        sourceSize.width: 50 
        sourceSize.height: 50 
    } 
} 

从上述代码中,您可以看到我已经将Image对象与MapQuickItem对象合并。坐标属性也已被删除,因为我们只会在将标记放在地图上时设置它。

现在,再次打开mapview.qml,并将此函数添加到Item对象中:

Item 
{ 
    id: window 

    Plugin 
    { 
        id: mapPlugin 
        name: "osm" 
    } 

    function addMarker(latitude, longitude) 
    { 
        var component = Qt.createComponent("qrc:///qml/marker.qml") 
        var item = component.createObject(window, { coordinate: 
        QtPositioning.coordinate(latitude, longitude) }) 
        map.addMapItem(item) 
    } 

从上述代码中,我们首先通过加载marker.qml文件创建了一个组件。然后,我们通过调用createObject()从组件创建了一个对象/项。在createObject()函数中,我们将窗口对象设置为其父对象,并将其位置设置为addMarker()函数提供的坐标。最后,我们将项目添加到地图中以进行渲染。

每当我们想要创建一个新的位置标记时,我们只需调用这个addMarker()函数。为了演示这一点,让我们通过三次调用addMarker()来创建三个不同的标记:

Map 
{ 
    id: map 
    anchors.fill: parent 
    plugin: mapPlugin 
    center: QtPositioning.coordinate(40.7264175,-73.99735) 
    zoomLevel: 14 

    Component.onCompleted: 
    { 
        addMarker(40.7274175,-73.99835) 
        addMarker(40.7276432,-73.98602) 
        addMarker(40.7272175,-73.98935) 
    } 
} 

再次构建和运行项目,您应该能够看到类似于这样的东西:

我们甚至可以进一步为每个标记添加文本标签。要做到这一点,首先打开marker.qml,然后添加另一个名为QtQuick.Controls的模块:

import QtQuick 2.0 
import QtQuick.Controls 2.0 
import QtLocation 5.3 

之后,向MapQuickItem对象添加一个自定义属性称为labelText

MapQuickItem 
{ 
    id: marker 
    anchorPoint.x: marker.width / 4 
    anchorPoint.y: marker.height 
    property string labelText 

一旦完成,将其sourceItem属性更改为:

sourceItem: Item 
{ 
        Image 
        { 
            id: icon 
            source: "qrc:///images/map-marker-icon.png" 
            sourceSize.width: 50 
            sourceSize.height: 50 
        } 

        Rectangle 
        { 
            id: tag 
            anchors.centerIn: label 
            width: label.width + 4 
            height: label.height + 2 
            color: "black" 
        } 

        Label 
        { 
            id: label 
            anchors.centerIn: parent 
            anchors.horizontalCenterOffset: 20 
            anchors.verticalCenterOffset: -12 
            font.pixelSize: 16 
            text: labelText 
            color: "white" 
        } 
} 

从上述代码中,我们创建了一个Item对象来将多个对象组合在一起。然后,我们创建了一个Rectangle对象作为标签背景,以及一个文本的Label对象。Label对象的text属性将链接到MapQuickItem对象的labelText属性。我们可以为addMarker()函数添加另一个输入,用于设置labelText属性,如下所示:

function addMarker(name, latitude, longitude) 
{ 
        var component = Qt.createComponent("qrc:///qml/marker.qml") 
        var item = component.createObject(window, { coordinate: QtPositioning.coordinate(latitude, longitude), labelText: name }) 
        map.addMapItem(item) 
} 

因此,当我们创建标记时,我们可以像这样调用addMarker()函数:

Component.onCompleted: 
{ 
   addMarker("Restaurant", 40.7274175,-73.99835) 
   addMarker("My Home", 40.7276432,-73.98602) 
   addMarker("School", 40.7272175,-73.98935) 
} 

再次构建和运行项目,您应该会看到这个:

相当棒,不是吗?但是,我们还没有完成。由于我们很可能使用 C++通过 Qt 的 SQL 模块从数据库获取数据,我们需要找到一种方法从 C++调用 QML 函数。

为了实现这一点,让我们在mapview.qml中注释掉三个addMarker()函数,并打开mainwindow.h和以下头文件:

#include <QQuickItem> 
#include <QQuickView> 

之后,打开mainwindow.cpp并调用QMetaObject::invokeMethod()函数,如下所示:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

 QObject* target = qobject_cast<QObject*>(ui->quickWidget->rootObject()); 
   QString functionName = "addMarker"; 

   QMetaObject::invokeMethod(target, functionName, Qt::AutoConnection, Q_ARG(QVariant, "Testing"), Q_ARG(QVariant, 40.7274175), Q_ARG(QVariant, -73.99835)); 
} 

上述代码可能看起来复杂,但如果我们分解并分析每个参数,实际上非常简单。上述函数的第一个参数是我们要从中调用函数的对象,在这种情况下,它是地图视图小部件中的根对象(mapview.qml中的Item对象)。接下来,我们要告诉要调用的函数名称是什么,它是addMarker()函数。之后,第三个参数是信号和槽系统使用的连接类型来调用此方法。对于这一点,我们将让它保持默认设置,即Qt::AutoConnection。其余的是addMarker()函数所需的参数。我们使用Q_ARG宏来指示数据的类型和值。

最后,再次构建和运行应用程序。您将看到一个带有标签的标记已经添加到地图上,但这次是从我们的 C++代码而不是 QML 中调用的:

在地图上显示形状

除了在地图上添加标记,我们还可以在地图上绘制不同类型的形状,以指示感兴趣的区域或作为地理围栏,当目标进入或离开形状覆盖的区域时发出警告。地理围栏是在地图上定义感兴趣区域或虚拟地理边界的多边形形状,用于基于位置的服务。通常,地理围栏用于在设备进入和/或离开地理围栏时触发警报。使用地理围栏的一个很好的例子是当你需要购物提醒时,你可以在超市周围画一个地理围栏,并附上购物清单。当你(和你的手机)进入地理围栏区域时,你将收到一条提醒你要买什么的手机通知。那不是很棒吗?

有关地理围栏的更多信息,请访问:https://en.wikipedia.org/wiki/Geo-fence

在本章中,我们不会创建一个功能性的地理围栏,因为这是一个相当高级的话题,通常作为服务器端服务运行,用于检查和触发警报。我们只会使用 Qt 来绘制形状并在屏幕上显示它。

为了在地图视图小部件上绘制形状,我们将为每种类型的形状创建一些新的 QML 文件,并将它们添加到程序的资源中:

对于每个新创建的 QML 文件,我们将类似于位置标记的操作。对于circle.qml,它看起来像这样:

import QtQuick 2.0 
import QtLocation 5.3 

MapCircle 
{ 
    property int borderWidth 
    border.width: borderWidth 
} 

我们只在这个文件中声明borderWidth,因为当调用createCircle()函数时,我们可以直接设置其他属性。对于rectangle.qml也是一样的:

import QtQuick 2.0 
import QtLocation 5.3 

MapRectangle 
{ 
    property int borderWidth 
    border.width: borderWidth 
} 

对于polygon.qml,重复类似的步骤:

import QtQuick 2.0 
import QtLocation 5.3 

MapPolygon 
{ 
    property int borderWidth 
    border.width: borderWidth 
} 

如果你愿意,你可以设置其他属性,但为了演示,我们只改变了一些属性,比如颜色、形状和边框宽度。完成后,让我们打开mapview.qml并定义一些函数来添加形状:

Item 
{ 
    id: window 

    Plugin 
    { 
        id: mapPlugin 
        name: "osm" 
    } 

    function addCircle(latitude, longitude, radius, color, borderWidth) 
    { 
       var component = Qt.createComponent("qrc:///qml/circle.qml") 
       var item = component.createObject(window, { center: 
       QtPositioning.coordinate(latitude, longitude), radius: radius, 
       color: color, borderWidth: borderWidth }) 
       map.addMapItem(item) 
    } 

    function addRectangle(startLat, startLong, endLat, endLong, color, 
    borderWidth) 
    { 
        var component = Qt.createComponent("qrc:///qml/rectangle.qml") 
        var item = component.createObject(window, { topLeft: 
       QtPositioning.coordinate(startLat, startLong), bottomRight: 
       QtPositioning.coordinate(endLat, endLong), color: color, 
       borderWidth: borderWidth }) 
        map.addMapItem(item) 
    } 

    function addPolygon(path, color, borderWidth) 
    { 
        var component = Qt.createComponent("qrc:///qml/polygon.qml") 
        var item = component.createObject(window, { path: path, color: 
        color, borderWidth: borderWidth }) 
        map.addMapItem(item) 
    } 

这些函数与addMarker()函数非常相似,只是它接受稍有不同的参数,稍后传递给createObject()函数。之后,让我们尝试使用前面的函数创建形状:

addCircle(40.7274175,-73.99835, 250, "green", 3); 
addRectangle(40.7274175,-73.99835, 40.7376432, -73.98602, "red", 2) 
var path = [{ latitude: 40.7324281, longitude: -73.97602 }, 
            { latitude: 40.7396432, longitude: -73.98666 }, 
            { latitude: 40.7273266, longitude: -73.99835 }, 
            { latitude: 40.7264281, longitude: -73.98602 }]; 
addPolygon(path, "blue", 3); 

以下是使用我们刚刚定义的函数创建的形状。我分别调用了每个函数来演示其结果,因此有三个不同的窗口:

获取用户位置

Qt 为我们提供了一组函数来获取用户的位置信息,但只有在用户的设备支持地理定位时才能工作。这应该适用于所有现代智能手机,也可能适用于一些现代计算机。

要使用Qt Location模块获取用户位置,首先让我们打开mainwindow.h并添加以下头文件:

#include <QDebug> 
#include <QGeoPositionInfo> 
#include <QGeoPositionInfoSource> 

在同一个文件中声明以下的slot函数:

private slots: 
   void positionUpdated(const QGeoPositionInfo &info); 

就在那之后,打开mainwindow.cpp并将以下代码添加到你希望开始获取用户位置的地方。出于演示目的,我只是在MainWindow构造函数中调用它:

QGeoPositionInfoSource *source = QGeoPositionInfoSource::createDefaultSource(this); 
if (source) 
{ 
   connect(source, &QGeoPositionInfoSource::positionUpdated, 
         this, &MainWindow::positionUpdated); 
   source->startUpdates(); 
} 

然后,实现我们之前声明的positionUpdated()函数,就像这样:

void MainWindow::positionUpdated(const QGeoPositionInfo &info) 
{ 
   qDebug() << "Position updated:" << info; 
} 

如果现在构建并运行应用程序,根据你用于运行测试的设备,你可能会或者不会获得任何位置信息。如果你收到这样的调试消息:

serialnmea: No serial ports found
Failed to create Geoclue client interface. Geoclue error: org.freedesktop.DBus.Error.Disconnected

然后你可能需要找一些其他设备进行测试。否则,你可能会得到类似于这样的结果:

Position updated: QGeoPositionInfo(QDateTime(2018-02-22 19:13:05.000 EST Qt::TimeSpec(LocalTime)), QGeoCoordinate(45.3333, -75.9))

我在这里给你留下一个作业,你可以尝试使用我们迄今为止创建的函数来完成。由于你现在可以获取你的位置坐标,尝试通过在地图显示上添加一个标记来进一步增强你的应用程序。这应该很有趣!

地理路由请求

还有一个重要的功能叫做地理路由请求,它是一组函数,帮助你绘制从 A 点到 B 点的路线(通常是最短路线)。这个功能需要一个服务提供商;在这种情况下,我们将使用Open Street MapOSM),因为它是完全免费的。

请注意,OSM 是一个在线协作项目,这意味着如果你所在地区没有人向 OSM 服务器贡献路线数据,那么你将无法获得准确的结果。作为可选项,你也可以使用付费服务,如 Mapbox 或 ESRI。

让我们看看如何在 Qt 中实现地理路由请求!首先,将以下头文件包含到我们的mainwindow.h文件中:

#include <QGeoServiceProvider>
#include <QGeoRoutingManager>
#include <QGeoRouteRequest>
#include <QGeoRouteReply>

之后,向MainWindow类添加两个槽函数,分别是routeCalculated()routeError()

private slots:
    void positionUpdated(const QGeoPositionInfo &info);
    void routeCalculated(QGeoRouteReply *reply);
    void routeError(QGeoRouteReply *reply, QGeoRouteReply::Error error, const QString &errorString);

完成后,打开mainwindow.cpp并在MainWindow构造方法中创建一个服务提供商对象。我们将使用 OSM 服务,因此在初始化QGeoServiceProvider类时,我们将放置缩写"osm"

QGeoServiceProvider* serviceProvider = new QGeoServiceProvider("osm");

接着,我们将从刚刚创建的服务提供商对象中获取路由管理器的指针:

QGeoRoutingManager* routingManager = serviceProvider->routingManager();

然后,将路由管理器的finished()信号和error()信号与我们刚刚定义的slot函数连接起来:

connect(routingManager, &QGeoRoutingManager::finished, this, &MainWindow::routeCalculated);
connect(routingManager, &QGeoRoutingManager::error, this, &MainWindow::routeError);

当成功请求后,这些槽函数将在服务提供商回复时被触发,或者当请求失败并返回错误消息时被触发。routeCalculated()槽函数看起来像这样:

void MainWindow::routeCalculated(QGeoRouteReply *reply)
{
    qDebug() << "Route Calculated";
    if (reply->routes().size() != 0)
    {
        // There could be more than 1 path
        // But we only get the first route
        QGeoRoute route = reply->routes().at(0);
        qDebug() << route.path();
    }
    reply->deleteLater();
}

正如你所看到的,QGeoRouteReply指针包含了服务提供商在成功请求后发送的路线信息。有时它会有多条路线,所以在这个例子中,我们只获取第一条路线并通过 Qt 的应用程序输出窗口显示出来。或者,你也可以使用这些坐标来绘制路径或沿着路线动画移动你的标记。

至于routeError()槽函数,我们将只输出服务提供商发送的错误字符串:

void MainWindow::routeError(QGeoRouteReply *reply, QGeoRouteReply::Error error, const QString &errorString)
{
    qDebug() << "Route Error" << errorString;
    reply->deleteLater();
}

完成后,让我们在MainWindow构造方法中发起一个地理路由请求并将其发送给服务提供商:

QGeoRouteRequest request(QGeoCoordinate(40.675895,-73.9562151), QGeoCoordinate(40.6833154,-73.987715));
routingManager->calculateRoute(request);

现在构建并运行项目,你应该能看到以下结果:

这里有另一个具有挑战性的任务——尝试将所有这些坐标放入一个数组中,并创建一个addLine()函数,该函数接受数组并绘制一系列直线,代表地理路由服务描述的路线。

自从 GPS 导航系统发明以来,地理路由一直是最重要的功能之一。希望在完成本教程后,你能够创造出一些有用的东西!

摘要

在本章中,我们学习了如何创建类似于谷歌地图的自己的地图视图。我们学习了如何创建地图显示,将标记和形状放在地图上,最后找到用户的位置。请注意,你也可以使用 Web 视图并调用谷歌的 JavaScript 地图 API 来创建类似的地图显示。然而,使用 QML 更简单,轻量级(我们不必加载整个 Web 引擎模块来使用地图),在移动设备和触摸屏上运行得非常好,并且也可以轻松移植到其他地图服务上。希望你能利用这些知识创造出真正令人印象深刻和有用的东西。

在下一章中,我们将探讨如何使用图形项显示信息。让我们继续吧!

第八章:Graphics View

在上一章中,我们学习了通过在地图上显示坐标数据来为用户提供视觉呈现的重要性。在本章中,我们将进一步探索使用 Qt 的Graphics View框架来表示图形数据的可能性。

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

  • Graphics View 框架

  • 可移动的图形项

  • 创建一个组织图表

在本章结束时,你将能够使用 C++和 Qt 的 API 创建一个组织图表显示。让我们开始吧!

Graphics View 框架

Graphics View框架是 Qt 中的小部件模块的一部分,因此它已经默认支持,除非你运行的是 Qt 控制台应用程序,它不需要小部件模块。

在 Qt 中,Graphics View视图的工作方式基本上就像一个白板,你可以使用 C/C++代码在上面画任何东西,比如绘制形状、线条、文本,甚至图像。对于初学者来说,这一章可能有点难以理解,但肯定会是一个有趣的项目。让我们开始吧!

设置一个新项目

首先,创建一个新的 Qt Widgets 应用程序项目。之后,打开mainwindow.ui,将Graphics View小部件拖放到主窗口上,就像这样:

然后,通过点击画布顶部的垂直布局按钮为图形视图创建一个布局。之后,打开mainwindow.h并添加以下头文件和变量:

#include <QGraphicsScene> 
#include <QGraphicsRectItem> 
#include <QGraphicsEllipseItem> 
#include <QGraphicsTextItem> 
#include <QBrush> 
#include <QPen> 

private:
  Ui::MainWindow *ui;
  QGraphicsScene* scene;

之后,打开mainwindow.cpp。一旦打开,添加以下代码:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

   scene = new QGraphicsScene(this); 
   ui->graphicsView->setScene(scene); 

   QBrush greenBrush(Qt::green); 
   QBrush blueBrush(Qt::blue); 
   QPen pen(Qt::black); 
   pen.setWidth(2); 

   QGraphicsRectItem* rectangle = scene->addRect(80, 0, 80, 80, pen, greenBrush); 
   QGraphicsEllipseItem* ellipse = scene->addEllipse(0, -80, 200, 60, pen, blueBrush); 
   QGraphicsTextItem* text = scene->addText("Hello World!", QFont("Times", 25)); 
} 

现在构建并运行程序,你应该会看到类似这样的东西:

代码有点长,所以让我向你解释一下它的作用以及它如何将图形绘制到屏幕上。

正如我之前所说,Graphics View小部件就像一个画布或白板,允许你在上面画任何你想要的东西。然而,我们还需要一个叫做 Graphics Scene 的东西,它本质上是一个场景图,它在显示在Graphics View上之前以父子层次结构存储所有图形组件。场景图层次结构就像在之前的截图中出现的图像,每个对象都可以有一个链接在一起的父对象或子对象:

在上面的代码中,我们首先创建了一个QGraphicsScene对象,并将其设置为我们的Graphics View小部件的 Graphics Scene:

scene = new QGraphicsScene(this); 
ui->graphicsView->setScene(scene); 

然而,在这个例子中,我们不必将图形项链接在一起,所以我们只需独立创建它们,就像这样:

QBrush greenBrush(Qt::green); 
...
QGraphicsTextItem* text = scene->addText("Hello World!", QFont("Times", 25)); 

QPenQBrush类用于定义这些图形项的渲染样式。QBrush通常用于定义项目的背景颜色和图案,而QPen通常影响项目的轮廓。

Qt 提供了许多类型的图形项,用于最常见的形状,包括:

  • QGraphicsEllipseItem – 椭圆项

  • QGraphicsLineItem – 线条项

  • QGraphicsPathItem – 任意路径项

  • QGraphicsPixmapItem – 图像项

  • QGraphicsPolygonItem – 多边形项

  • QGraphicsRectItem – 矩形项

  • QGraphicsSimpleTextItem – 简单文本标签项

  • QGraphicsTextItem – 高级格式化文本项

更多信息,请访问此链接:doc.qt.io/archives/qt-5.8/qgraphicsitem.html#details.

可移动的图形项

在上一个例子中,我们成功地将一些简单的形状和文本绘制到了Graphics View小部件上。然而,这些图形项是不可交互的,因此不适合我们的目的。我们想要的是一个交互式的组织图表,用户可以使用鼠标移动项目。在 Qt 下,使这些项目可移动实际上非常容易;让我们看看我们如何通过继续我们之前的项目来做到这一点。

首先,确保不要更改我们的图形视图小部件的默认交互属性,即启用(复选框已选中):

在那之后,在之前的Hello World示例中创建的每个图形项下面添加以下代码:

QGraphicsRectItem* rectangle = scene->addRect(80, 0, 80, 80, pen, greenBrush); 
rectangle->setFlag(QGraphicsItem::ItemIsMovable); 
rectangle->setFlag(QGraphicsItem::ItemIsSelectable); 

QGraphicsEllipseItem* ellipse = scene->addEllipse(0, -80, 200, 60, pen, blueBrush); 
ellipse->setFlag(QGraphicsItem::ItemIsMovable); 
ellipse->setFlag(QGraphicsItem::ItemIsSelectable); 

QGraphicsTextItem* text = scene->addText("Hello World!", QFont("Times", 25)); 
text->setFlag(QGraphicsItem::ItemIsMovable); 
text->setFlag(QGraphicsItem::ItemIsSelectable); 

再次构建和运行程序,这次您应该能够在图形视图中选择和移动项目。请注意,ItemIsMovableItemIsSelectable都会给您不同的行为——前者标志将使项目可以通过鼠标移动,而后者使项目可选择,通常在选择时使用虚线轮廓进行视觉指示。每个标志都独立工作,不会影响其他标志。

我们可以通过使用 Qt 中的信号和槽机制来测试ItemIsSelectable标志的效果。让我们回到我们的代码并添加以下行:

ui->setupUi(this); 
scene = new QGraphicsScene(this); 
ui->graphicsView->setScene(scene); 
connect(scene, &QGraphicsScene::selectionChanged, this, &MainWindow::selectionChanged); 

selectionChanged()信号将在您在图形视图小部件上选择项目时触发,然后MainWindow类下的selectionChanged()槽函数将被调用(我们需要编写)。让我们打开mainwindow.h并添加另一个头文件以显示调试消息:

#include <QDebug> 

然后,我们声明槽函数,就像这样:

private: 
   Ui::MainWindow *ui; 

public slots: 
 void selectionChanged(); 

之后打开mainwindow.cpp并定义槽函数,就像这样:

void MainWindow::selectionChanged() 
{ 
   qDebug() << "Item selected"; 
} 

现在尝试再次运行程序;您应该看到一行调试消息,每当单击图形项时会出现“项目选择”。这真的很简单,不是吗?

至于ItemIsMovable标志,我们将无法使用信号和槽方法进行测试。这是因为所有从QGraphicsItem类继承的类都不是从QObject类继承的,因此信号和槽机制不适用于这些类。这是 Qt 开发人员有意为之,以使其轻量级,从而提高性能,特别是在屏幕上渲染数千个项目时。

尽管信号和槽对于这个选项不是一个选择,我们仍然可以使用事件系统,这需要对itemChange()虚函数进行重写,我将在下一节中演示。

创建组织图表

让我们继续学习如何使用 Graphics View 创建组织图表。组织图表是一种显示组织结构和员工职位关系层次结构的图表。通过使用图形表示来理解公司的结构是很容易的;因此最好使用 Graphics View 而不是表格。

这一次,我们需要为图形项创建自己的类,以便我们可以利用 Qt 的事件系统,并且更好地控制它的分组和显示方式。

首先,通过转到文件 | 新建文件或项目来创建一个 C/C++类:

接下来,在点击下一步和完成按钮之前,将我们的类命名为profileBox

之后,打开mainwindow.h并添加这些头文件:

#include <QWidget> 
#include <QDebug> 
#include <QBrush> 
#include <QPen> 
#include <QFont> 
#include <QGraphicsScene> 
#include <QGraphicsItemGroup> 
#include <QGraphicsItem> 
#include <QGraphicsRectItem> 
#include <QGraphicsTextItem> 
#include <QGraphicsPixmapItem> 

然后,打开profilebox.h并使我们的profileBox类继承QGraphicsItemGroup

class profileBox : public QGraphicsItemGroup 
{ 
public: 
   explicit profileBox(QGraphicsItem* parent = nullptr); 

在那之后,打开profilebox.cpp并在类的构造函数中设置QBrushQPenQFont,这将在稍后用于渲染:

profileBox::profileBox(QGraphicsItem *parent) : QGraphicsItemGroup(parent) 
{ 
   QBrush brush(Qt::white); 
   QPen pen(Qt::black); 
   QFont font; 
   font.setFamily("Arial"); 
   font.setPointSize(12); 
} 

之后,在构造函数中,创建一个QGraphicsRectItemQGraphicsTextItem和一个QGraphicsPixmapItem

QGraphicsRectItem* rectangle = new QGraphicsRectItem(); 
rectangle->setRect(0, 0, 90, 100); 
rectangle->setBrush(brush); 
rectangle->setPen(pen); 

nameTag = new QGraphicsTextItem(); 
nameTag->setPlainText(""); 
nameTag->setFont(font); 

QGraphicsPixmapItem* picture = new QGraphicsPixmapItem(); 
QPixmap pixmap(":/images/person-icon-blue.png"); 
picture->setPixmap(pixmap); 
picture->setPos(15, 30); 

然后,将这些项目添加到组中,这是当前类,因为这个类是从QGraphicsItemGroup类继承的:

this->addToGroup(rectangle); 
this->addToGroup(nameTag); 
this->addToGroup(picture); 

最后,为当前类设置三个标志,即ItemIsMovableItemIsSelectableItemSendsScenePositionChanges

this->setFlag(QGraphicsItem::ItemIsMovable); 
this->setFlag(QGraphicsItem::ItemIsSelectable); 
this->setFlag(QGraphicsItem::ItemSendsScenePositionChanges); 

这些标志非常重要,因为它们默认情况下都是禁用的,出于性能原因。我们在上一节中已经涵盖了ItemIsMovableItemIsSelectable,而ItemSendsPositionChanges是一些新的东西。此标志使图形项在用户移动时通知图形场景,因此得名。

接下来,创建另一个名为init()的函数,用于设置员工个人资料。为简单起见,我们只设置了员工姓名,但是如果您愿意,还可以进行更多操作,例如根据职级设置不同的背景颜色,或更改其个人资料图片:

void profileBox::init(QString name, MainWindow *window, QGraphicsScene* scene) 
{ 
   nameTag->setPlainText(name); 
   mainWindow = window; 
   scene->addItem(this); 
} 

请注意,我们还在这里设置了主窗口和图形场景指针,以便以后使用。在将其呈现在屏幕上之前,我们必须将QGraphicsItem添加到场景中。在这种情况下,我们将所有图形项分组到QGraphicsItemGroup中,因此我们只需要将组添加到场景中,而不是单个项。

请注意,您必须在profilebox.h中的#include "mainwindow.h"之后进行MainWindow类的前向声明,以避免递归头文件包含错误。同时,我们还在profilebox.h中放置了MainWindowQGraphicsTextItem指针,以便以后调用它们:

#include "mainwindow.h" 

class MainWindow; 

class profileBox : public QGraphicsItemGroup 
{ 
public: 
   explicit profileBox(QGraphicsItem* parent = nullptr); 
   void init(QString name, MainWindow* window, QGraphicsScene* scene); 

private: 
   MainWindow* mainWindow; 
   QGraphicsTextItem* nameTag; 

您还会注意到,我在QGraphicsPixmapItem中使用了一个图标作为装饰图标:

此图标是存储在资源文件中的 PNG 图像。您可以从我们在 GitHub 页面上的示例项目文件中获取此图像:github.com/PacktPublishing/Hands-On-GUI-Programming-with-C-QT5

为您的项目创建一个资源文件。转到文件|新建文件或项目,然后在 Qt 类别下选择 Qt 资源文件选项:

创建空的资源文件后,通过添加|添加前缀添加一个新前缀。我们将只称此前缀为images

然后,选择新创建的images前缀,单击添加|添加文件。将图标图像添加到资源文件并保存。您现在已成功将图像添加到项目中。

如果您的前缀名称或文件名与本书中的前缀名称或文件名不同,您可以右键单击资源文件中的图像,然后选择复制资源路径到剪贴板,并用您的路径替换代码中的路径。

之后,打开mainwindow.h并添加:

#include "profilebox.h"

然后,打开mainwindow.cpp并添加以下代码以手动创建个人资料框:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

   scene = new QGraphicsScene(this); 
   ui->graphicsView->setScene(scene); 

   connect(scene, &QGraphicsScene::selectionChanged, this, &MainWindow::selectionChanged); 

   profileBox* box = new profileBox(); 
   box->init("John Doe", this, scene); 
} 

现在构建和运行项目,您应该看到类似于这样的东西:

看起来整洁;但我们还远未完成。还有一些事情要做——我们必须允许用户通过用户界面添加或删除个人资料框,而不是使用代码。同时,我们还需要添加连接不同个人资料框的线条,以展示不同员工之间的关系以及他们在公司内的职位。

让我们从简单的部分开始。再次打开mainwindow.ui,并在图形视图小部件底部添加一个推送按钮,并将其命名为addButton

然后,右键单击推送按钮,选择转到插槽...之后,选择单击选项,然后单击确定。将自动为您创建一个新的插槽函数,名为on_addButton_clicked()。添加以下代码以允许用户在单击添加按钮时创建个人资料框:

void MainWindow::on_addButton_clicked() 
{ 
   bool ok; 
   QString name = QInputDialog::getText(this, tr("Employee Name"), 
   tr("Please insert employee's full name here:"), QLineEdit::Normal,  
   "John Doe", &ok); 
   if (ok && !name.isEmpty()) 
   { 
         profileBox* box = new profileBox(); 
         box->init(name, this, scene); 
   } 
} 

现在,用户不再需要使用代码创建每个个人资料框,他们可以通过单击添加按钮轻松创建任意数量的个人资料框。还将出现一个消息框,让用户在创建个人资料框之前输入员工姓名:

接下来,我们将创建另一个名为profileLine的类。这次,我们将使这个类继承QGraphicsLineItemprofileline.h基本上看起来像这样:

#include <QWidget> 
#include <QGraphicsItem> 
#include <QPen> 

class profileLine : public QGraphicsLineItem 
{ 
public: 
   profileLine(QGraphicsItem* parent = nullptr); 
   void initLine(QGraphicsItem* start, QGraphicsItem* end); 
   void updateLine(); 

   QGraphicsItem* startBox; 
   QGraphicsItem* endBox; 

private: 
}; 

profileBox类类似,我们还为profileLine类创建了一个init函数,称为initLine()函数。此函数接受两个QGraphicsItem对象作为渲染行的起点和终点。此外,我们还创建了一个updateLine()函数,以便在配置框移动时重新绘制行。

接下来,打开profileline.cpp并将以下代码添加到构造函数中:

profileLine::profileLine(QGraphicsItem *parent) : QGraphicsLineItem(parent) 
{ 
   QPen pen(Qt::black); 
   pen.setWidth(2); 
   this->setPen(pen); 

   this->setZValue(-999); 
} 

我们使用QPen将线的颜色设置为黑色,宽度设置为2。之后,我们还将线的Zvalue设置为-999,这样它将始终保持在配置框的后面。

之后,将以下代码添加到我们的initLine()函数中,使其看起来像这样:

void profileLine::initLine(QGraphicsItem* start, QGraphicsItem* end) 
{ 
   startBox = start; 
   endBox = end; 

   updateLine(); 
} 

它的作用基本上是设置框的起点和终点位置。之后,调用updateLine()函数来渲染行。

最后,updateLine()函数看起来像这样:

void profileLine::updateLine() 
{ 
   if (startBox != NULL && endBox != NULL) 
   { 
         this->setLine(startBox->pos().x() + startBox->boundingRect().width() / 2, startBox->pos().y() + startBox->boundingRect().height() / 2, endBox->pos().x() + endBox->boundingRect().width() / 2, endBox->pos().y() + endBox->boundingRect().height() / 2); 
   } 
} 

前面的代码看起来有点复杂,但如果我这样说,它就真的很简单:

this->setLine(x1, y1, x2, y2); 

x1y1基本上是第一个配置框的中心位置,而x2y2是第二个配置框的中心位置。由于从调用pos()获取的位置值从左上角开始,我们必须获取配置框的边界大小并除以二以获取其中心位置。然后,将该值添加到左上角位置以将其偏移至中心。

完成后,让我们再次打开mainwindow.cpp并将以下代码添加到on_addButton_clicked()函数中:

void MainWindow::on_addButton_clicked() 
{ 
   bool ok; 
   QString name = QInputDialog::getText(this, tr("Employee Name"), tr("Please insert employee's full name here:"), QLineEdit::Normal, "John Doe", &ok); 
   if (ok && !name.isEmpty()) 
   { 
         profileBox* box = new profileBox(); 
         box->init(name, this, scene); 

         if (scene->selectedItems().size() > 0) 
         { 
               profileLine* line = new profileLine(); 
               line->initLine(box, scene->selectedItems().at(0)); 
               scene->addItem(line); 

               lines.push_back(line); 
         } 
   } 
} 

在前面的代码中,我们检查用户是否选择了任何配置框。如果没有,我们就不必创建任何线。否则,创建一个新的profileLine对象,并将新创建的配置框和当前选择的配置框设置为startBoxendBox属性。

之后,将该profileLine对象添加到我们的图形场景中,以便它出现在屏幕上。最后,将此profileLine对象存储到QList数组中,以便我们以后使用。在mainwindow.h中,数组声明如下所示:

private: 
   Ui::MainWindow *ui; 
   QGraphicsScene* scene; 
   QList<profileLine*> lines; 

现在构建和运行项目。当您点击“添加”按钮创建第二个配置框时,您应该能够看到线出现,并在选择第一个框时保持选中。但是,您可能会注意到一个问题,即当您将配置框移出原始位置时,线根本不会更新自己!:

这是我们将行放入QList数组的主要原因,这样我们就可以在用户移动配置框时更新这些行。

为此,首先,我们需要重写profileBox类中的虚函数itemChanged()。让我们打开profilebox.h并添加以下代码行:

class profileBox : public QGraphicsItemGroup 
{ 
public: 
   explicit profileBox(QGraphicsItem* parent = nullptr); 
   void init(QString name, MainWindow* window, QGraphicsScene* scene); 
   QVariant itemChange(GraphicsItemChange change, const QVariant 
   &value) override; 

然后,打开profilebox.cpp并添加itemChanged()的代码:

QVariant profileBox::itemChange(GraphicsItemChange change, const QVariant &value) 
{ 
   if (change == QGraphicsItem::ItemPositionChange) 
   { 
         qDebug() << "Item moved"; 

         mainWindow->updateLines(); 
   } 

   return QGraphicsItem::itemChange(change, value); 
} 

itemChanged()函数是QGraphicsItem类中的虚函数,当图形项发生变化时,Qt 的事件系统将自动调用它,无论是位置变化、可见性变化、父级变化、选择变化等等。

因此,我们所需要做的就是重写该函数并向函数中添加我们自己的自定义行为。在前面的示例代码中,我们所做的就是在我们的主窗口类中调用updateLines()函数。

接下来,打开mainwindow.cpp并定义updateLines()函数。正如函数名所示,您要在此函数中做的是循环遍历存储在行数组中的所有配置行对象,并更新每一个,如下所示:

void MainWindow::updateLines() 
{ 
   if (lines.size() > 0) 
   { 
         for (int i = 0; i < lines.size(); i++) 
         { 
               lines.at(i)->updateLine(); 
         } 
   } 
} 

完成后,再次构建和运行项目。这次,您应该能够创建一个组织图表,如下所示:

这只是一个更简单的版本,向您展示了如何利用 Qt 强大的图形视图系统来显示一组数据的图形表示,这些数据可以被普通人轻松理解。

在完成之前还有一件事-我们还没有讲解如何删除配置档框。实际上很简单,让我们打开mainwindow.h并添加keyReleaseEvent()函数,看起来像这样:

public: 
   explicit MainWindow(QWidget *parent = 0); 
   ~MainWindow(); 

   void updateLines(); 
   void keyReleaseEvent(QKeyEvent* event); 

这个虚函数在键盘按钮被按下和释放时也会被 Qt 的事件系统自动调用。函数的内容在mainwindow.cpp中看起来像这样:

void MainWindow::keyReleaseEvent(QKeyEvent* event) 
{ 
   qDebug() << "Key pressed: " + event->text(); 

   if (event->key() == Qt::Key_Delete) 
   { 
         if (scene->selectedItems().size() > 0) 
         { 
               QGraphicsItem* item = scene->selectedItems().at(0); 
               scene->removeItem(item); 

               for (int i = lines.size() - 1; i >= 0; i--) 
               { 
                     profileLine* line = lines.at(i); 

                     if (line->startBox == item || line->endBox == 
                     item) 
                     { 
                           lines.removeAt(i); 
                           scene->removeItem(line); 
                           delete line; 
                     } 
               } 
               delete item; 
         } 
   } 
} 

在这个函数中,我们首先要检测用户按下的键盘按钮。如果按钮是Qt::Key_Delete (删除按钮),那么我们将检查用户是否选择了任何配置档框,通过检查scene->selectedItems().size()是否为空来判断。如果用户确实选择了一个配置档框,那么就从图形场景中移除该项。之后,循环遍历线数组,并检查是否有任何配置线连接到已删除的配置档框。从场景中移除连接到配置档框的任何线,然后我们就完成了:

这个截图显示了从组织结构图中删除Jane Smith配置档框的结果。请注意,连接配置框的线已经被正确移除。就是这样,本章到此结束;希望您觉得这很有趣,也许会继续创造比这更好的东西!

总结

在本章中,我们学习了如何使用 Qt 创建一个应用程序,允许用户轻松创建和编辑组织结构图。我们学习了诸如QGraphicsSceneQGrapicsItemQGraphicsTextItemQGraphicsPixmapItem等类,这些类帮助我们在短时间内创建一个交互式组织结构图。在接下来的章节中,我们将学习如何使用网络摄像头捕捉图像!

第九章:摄像头模块

在通过许多难度逐渐增加的章节后,让我们尝试一些更简单和更有趣的东西!我们将学习如何通过 Qt 的多媒体模块访问我们的摄像头并使用它拍照。

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

  • Qt 多媒体模块

  • 连接到摄像头

  • 将摄像头图像捕获到文件

  • 将摄像头视频录制到文件

您可以使用这个功能创建视频会议应用程序、安全摄像头系统等。让我们开始吧!

Qt 多媒体模块

Qt 中的多媒体模块处理平台的多媒体功能,如媒体播放和摄像头和收音机设备的使用。这个模块涵盖了很多主题,但是在本章中我们只会专注于摄像头。

设置一个新项目

首先,创建一个新的 Qt Widgets 应用程序项目。

首先,我们需要打开项目文件(.pro)并添加两个关键字——multimediamultimediawidgets

QT += core gui multimedia multimediawidgets 

通过在项目文件中检测这些关键字,Qt 在编译时将包含多媒体模块和所有与多媒体相关的部件到您的项目中。多媒体模块包括四个主要组件,列举如下:

  • 音频

  • 视频

  • 摄像头

  • 收音机

每个组件都包括一系列提供相应功能的类。通过使用这个模块,您不再需要自己实现低级别的平台特定代码。让 Qt 来为您完成这项工作。真的很简单。

在添加了多媒体模块后,让我们打开mainwindow.ui并将一个水平布局拖放到主窗口上,如下所示:

然后,在我们刚刚添加的水平布局中添加一个标签、下拉框(命名为deviceSelection)和一个按钮。之后,在下拉框和按钮之间添加一个水平间隔。完成后,选择中央窗口部件并点击工作区上方的垂直布局按钮。

然后,在上一个水平布局的底部添加另一个水平布局,右键单击它并选择转换为 | QFrame。然后,将其 sizePolicy(水平策略和垂直策略)设置为扩展。参考以下截图:

到目前为止,您的程序用户界面应该看起来像这样:

我们将布局转换为框架的原因是为了将 sizePolicy(水平策略和垂直策略)设置为扩展。但是,如果我们只是从部件框中添加一个框架部件(本质上是 QFrame),我们就无法得到所需的用于稍后附加取景器的布局组件。

接下来,再次右键单击 QFrame 并选择更改样式表。将弹出一个窗口来设置该部件的样式表。添加以下样式表代码以使背景变为黑色:

这一步是可选的;我们将其背景设置为黑色,只是为了指示取景器的位置。完成后,让我们在 QFrame 上方再添加一个水平布局,如下所示:

然后,在水平布局中添加两个按钮和一个水平间隔以使它们右对齐:

到此为止;我们已经完成了使用多媒体模块设置项目,并为下一节精心布置了用户界面。

连接到摄像头

最激动人心的部分来了。我们将学习如何使用 Qt 的多媒体模块访问我们的摄像头。首先,打开mainwindow.h并添加以下头文件:

#include <QMainWindow> 
#include <QDebug> 
#include <QCameraInfo> 
#include <QCamera> 
#include <QCameraViewfinder> 
#include <QCameraImageCapture> 
#include <QMediaRecorder> 
#include <QUrl> 

接下来,添加以下变量,如下所示:

private: 
   Ui::MainWindow *ui; 
   QCamera* camera; 
   QCameraViewfinder* viewfinder; 
   bool connected; 

然后,打开mainwindow.cpp并将以下代码添加到类构造函数中以初始化QCamera对象。然后,我们使用QCameraInfo类检索连接摄像头的列表,并将该信息填充到组合框小部件中:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

   connected = false; 
   camera = new QCamera(); 

   qDebug() << "Number of cameras found:" << QCameraInfo::availableCameras().count(); 

   QList<QCameraInfo> cameras = QCameraInfo::availableCameras(); 
   foreach (const QCameraInfo &cameraInfo, cameras) 
   { 
         qDebug() << "Camera info:" << cameraInfo.deviceName() << 
         cameraInfo.description() << cameraInfo.position(); 

         ui->deviceSelection->addItem(cameraInfo.description()); 
   } 
} 

现在构建并运行项目。之后,检查调试输出以查看计算机上检测到的摄像头。检测到的摄像头也应显示在下拉框中。如果您在支持摄像头的笔记本电脑上运行,您应该能够看到它在列表中。如果您在没有内置摄像头的系统上运行,则调试输出可能不会显示任何内容,下拉框也将保持为空。如果是这种情况,请尝试插入一个廉价的 USB 摄像头并重新运行程序:

之后,打开mainwindow.ui,右键单击连接按钮,然后选择转到槽.... 选择clicked()选项,然后单击确定。Qt Creator 将自动为您创建一个slot函数;将以下代码添加到函数中:

void MainWindow::on_connectButton_clicked() 
{ 
   if (!connected) 
   { 
         connectCamera(); 
   } 
   else 
   { 
         camera->stop(); 
         viewfinder->deleteLater(); 
         ui->connectButton->setText("Connect"); 
         connected = false; 
   } 
} 

当单击连接按钮时,我们首先检查camera是否已连接,方法是检查connect变量。如果尚未连接,我们运行connectCamera()函数,我们将在下一步中定义。如果摄像头已连接,我们停止摄像头,删除viewfinder并将连接按钮的文本设置为Connect。最后,将connected变量设置为false。请注意,这里我们使用deleteLater()而不是delete(),这是删除内存指针的推荐方法。如果在没有运行事件循环的线程中调用deleteLater(),则对象将在线程完成时被销毁。

接下来,我们将在MainWindow类中添加一个名为connectCamera()的新函数。该函数如下所示:

void MainWindow::connectCamera() 
{ 
   QList<QCameraInfo> cameras = QCameraInfo::availableCameras(); 
   foreach (const QCameraInfo &cameraInfo, cameras) 
   { 
         qDebug() << cameraInfo.description() << ui->deviceSelection-
         >currentText(); 

         if (cameraInfo.description() == ui->deviceSelection- 
         >currentText()) 
         { 
               camera = new QCamera(cameraInfo); 
               viewfinder = new QCameraViewfinder(this); 
               camera->setViewfinder(viewfinder); 
               ui->webcamLayout->addWidget(viewfinder); 

               connected = true; 
               ui->connectButton->setText("Disconnect"); 

               camera->start(); 

               return; 
         } 
   } 
} 

connectCamera()函数中,我们重复了构造中的操作,并获取当前连接摄像头的列表。然后,我们循环遍历列表,并将摄像头的名称(存储在description变量中)与组合框小部件上当前选择的设备名称进行比较。

如果有匹配的名称,这意味着用户打算连接到该特定摄像头,因此我们将通过初始化QCamera对象和新的QCameraViewFinder对象来连接到该摄像头。然后,我们将viewfinder链接到camera,并将viewfinder添加到具有黑色背景的布局中。然后,我们将connected变量设置为true,并将连接按钮的文本设置为Disconnect。最后,调用start()函数来启动摄像头运行。

现在构建并运行项目。选择要连接的摄像头,然后单击连接按钮。您应该能够连接到摄像头并在程序中看到自己:

如果您的摄像头无法连接,请执行以下步骤以显示操作系统返回的任何错误。首先,打开mainwindow.h并添加以下slot函数:

private slots: 
   void cameraError(QCamera::Error error); 

之后,打开mainwindow.cpp并将以下代码添加到connectCamera()函数中,将error()信号连接到cameraError()槽函数:

void MainWindow::connectCamera() 
{ 
   QList<QCameraInfo> cameras = QCameraInfo::availableCameras(); 
   foreach (const QCameraInfo &cameraInfo, cameras) 
   { 
         qDebug() << cameraInfo.description() << ui->deviceSelection-
         >currentText(); 

         if (cameraInfo.description() == ui->deviceSelection-
         >currentText()) 
         { 
               camera = new QCamera(cameraInfo); 
               viewfinder = new QCameraViewfinder(this); 
               camera->setViewfinder(viewfinder); 
               ui->webcamLayout->addWidget(viewfinder); 

               connect(camera, SIGNAL(error(QCamera::Error)), this, 
               SLOT(cameraError(QCamera::Error))); 

               connected = true; 
               ui->connectButton->setText("Disconnect"); 

               camera->start(); 

               return; 
         } 
   } 
} 

cameraError()槽函数如下所示:

void MainWindow::cameraError(QCamera::Error error) 
{ 
   qDebug() << "Camera error:" << error; 

   connected = false; 
   camera->stop(); 
   ui->connectButton->setText("Connect"); 
} 

在上述代码中,我们显示错误消息,并确保摄像头已完全停止,以防万一。通过查看错误消息,您应该能够更轻松地调试问题。

将摄像头图像捕获到文件

在上一节中,我们已经学习了如何使用 Qt 的多媒体模块连接到摄像头。现在,我们将尝试从摄像头中捕获静态图像并将其保存为 JPEG 文件。使用 Qt 实际上非常简单。

首先,打开mainwindow.h并添加以下变量:

private: 
   Ui::MainWindow *ui; 
   QCamera* camera; 
   QCameraViewfinder* viewfinder; QCameraImageCapture* imageCapture; bool connected; 

然后,在mainwindow.ui中右键单击 Capture 按钮,选择转到槽...。然后,选择clicked()并按 OK。现在,在mainwindow.cpp中为您创建了一个新的slot函数。添加以下代码以从摄像头捕获图像:

void MainWindow::on_captureButton_clicked() 
{ 
   if (connected) 
   { 
         imageCapture = new QCameraImageCapture(camera); 
         camera->setCaptureMode(QCamera::CaptureStillImage); 
         camera->searchAndLock(); 
         imageCapture->capture(qApp->applicationDirPath()); 
         camera->unlock(); 
   } 
} 

在前面的代码中,我们基本上创建了一个新的QCameraImageCapture对象,并将其媒体对象设置为活动摄像头。然后,将其捕获模式设置为静态图像。在要求QCameraImageCapture对象捕获图像之前,我们必须锁定摄像头,以便在捕获图像过程中设置保持不变。成功捕获图像后,您可以通过调用camera->unlock()来解锁它。

我们使用了qApp->applicationDirPath()来获取应用程序目录,以便图像将保存在可执行文件旁边。您可以将其更改为任何您想要的目录。您还可以将所需的文件名放在目录路径后面;否则,它将使用默认文件名格式按顺序保存图像,从IMG_00000001.jpg开始,依此类推。

将摄像头视频录制到文件

在学习了如何从我们的摄像头捕获静态图像之后,让我们继续学习如何录制视频。首先,打开mainwindow.h并添加以下变量:

private: 
   Ui::MainWindow *ui; 
   QCamera* camera; 
   QCameraViewfinder* viewfinder; 
   QCameraImageCapture* imageCapture; 
   QMediaRecorder* recorder; 

   bool connected; 
   bool recording; 

接下来,再次打开mainwindow.ui,右键单击 Record 按钮。从菜单中选择转到槽...,然后选择clicked()选项,然后单击 OK 按钮。将为您创建一个slot函数;然后继续将以下代码添加到slot函数中:

void MainWindow::on_recordButton_clicked() 
{ 
   if (connected) 
   { 
         if (!recording) 
         { 
               recorder = new QMediaRecorder(camera); 
               camera->setCaptureMode(QCamera::CaptureVideo); 
               recorder->setOutputLocation(QUrl(qApp-
               >applicationDirPath())); 
               recorder->record(); 
               recording = true; 
         } 
         else 
         { 
               recorder->stop(); 
               recording = false; 
         } 
   } 
} 

这次,我们使用QMediaRecorder来录制视频。在调用recorder->record()之前,我们还必须将摄像头的捕获模式设置为QCamera::CaptureVideo

要检查媒体录制器在录制阶段产生的错误消息,您可以将媒体录制器的error()信号连接到slot函数,如下所示:

void MainWindow::on_recordButton_clicked() 
{ 
   if (connected) 
   { 
         if (!recording) 
         { 
               recorder = new QMediaRecorder(camera); 
               connect(recorder, SIGNAL(error(QMediaRecorder::Error)), 
               this, SLOT(recordError(QMediaRecorder::Error))); 
               camera->setCaptureMode(QCamera::CaptureVideo); 
               recorder->setOutputLocation(QUrl(qApp-
               >applicationDirPath())); 
               recorder->record(); 
               recording = true; 
         } 
         else 
         { 
               recorder->stop(); 
               recording = false; 
         } 
   } 
} 

然后,只需在slot函数中显示错误消息:

void MainWindow::recordError(QMediaRecorder::Error error) 
{ 
   qDebug() << errorString(); 
} 

请注意,在撰写本章时,QMediaRecorder类仅支持 macOS、Linux、移动平台和 Windows XP 上的视频录制。目前在 Windows 8 和 Windows 10 上不起作用,但将在即将推出的版本之一中移植过去。主要原因是 Qt 在 Windows 平台上使用 Microsoft 的DirectShow API 来录制视频,但自那时起已经从 Windows 操作系统中停用。希望在您阅读本书时,这个功能已经完全在 Qt 中为 Windows 8 和 10 实现。

如果没有,您可以使用使用OpenCV API 进行视频录制的第三方插件,例如Qt 媒体编码库QtMEL)API,作为临时解决方案。请注意,QtMEL 中使用的代码与我们在本章中展示的代码完全不同。

有关 QtMEL 的更多信息,请查看以下链接:

kibsoft.ru

摘要

在本章中,我们学习了如何使用 Qt 连接到我们的摄像头。我们还学习了如何从摄像头捕获图像或录制视频。在下一章中,我们将学习有关网络模块,并尝试使用 Qt 制作即时通讯工具!

第十章:即时通讯

企业软件的一个重要特性是与员工进行通信的能力。因此,内部即时通讯系统是软件的一个关键部分。通过在 Qt 中整合网络模块,我们可以轻松地创建一个聊天系统。

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

  • Qt 网络模块

  • 创建即时通讯服务器

  • 创建即时通讯客户端

使用 Qt 创建即时通讯系统比你想象的要容易得多。让我们开始吧!

Qt 网络模块

在接下来的部分,我们将学习 Qt 的网络模块以及它如何帮助我们通过 TCP 或 UDP 连接协议实现服务器-客户端通信。

连接协议

Qt 的网络模块提供了低级网络功能,如 TCP 和 UDP 套接字,以及用于网络集成和网络通信的高级网络类。

在本章中,我们将使用 TCP(传输控制协议)互联网协议,而不是 UDP(用户数据报协议)协议。主要区别在于 TCP 是一种面向连接的协议,要求所有客户端在能够相互通信之前必须与服务器建立连接。

另一方面,UDP 是一种无连接的协议,不需要连接。客户端只需将需要发送到目的地的任何数据发送出去,而无需检查数据是否已被另一端接收。两种协议都有利弊,但 TCP 更适合我们的示例项目。我们希望确保每条聊天消息都被接收者接收到,不是吗?

两种协议之间的区别如下:

  • TCP:

  • 面向连接的协议

  • 适用于需要高可靠性的应用程序,对数据传输时间不太关键

  • TCP 的速度比 UDP 慢

  • 在发送下一个数据之前,需要接收客户端的确认收据

  • 绝对保证传输的数据保持完整,并按发送顺序到达目的地

  • UDP:

  • 无连接协议

  • 适用于需要快速、高效传输的应用程序,如游戏和 VOIP

  • UDP 比 TCP 轻量且更快,因为不会尝试错误恢复

  • 也适用于需要从大量客户端回答小查询的服务器

  • 没有保证发送的数据是否到达目的地,因为没有跟踪连接,也不需要接收客户端的任何确认

由于我们不打算采用点对点连接的方法,我们的聊天系统将需要两个不同的软件部分——服务器程序和客户端程序。服务器程序将充当中间人(就像邮递员一样),接收所有用户的消息并将它们发送给相应的接收者。服务器程序将被锁定在服务器房间的一台计算机中,普通用户无法接触。

另一方面,客户端程序是所有用户使用的即时通讯软件。这个程序将安装在用户的计算机上。用户可以使用这个客户端程序发送消息,并查看其他人发送的消息。我们的消息系统的整体架构看起来像这样:

让我们继续设置我们的项目并启用 Qt 的网络模块!对于这个项目,我们将先从服务器程序开始,然后再处理客户端程序。

设置新项目

首先,创建一个新的 Qt 控制台应用程序项目。然后,打开项目文件(.pro)并添加以下模块:

QT += core network 
Qt -= gui 

你应该已经注意到,这个项目没有任何gui模块(我们确保它被明确删除),因为服务器程序不需要任何用户界面。这也是为什么我们选择了 Qt 控制台应用程序而不是通常的 Qt 小部件应用程序的原因。

实际上,就是这样——你已经成功地将网络模块添加到了你的项目中。在下一节中,我们将学习如何为我们的聊天系统创建服务器程序。

创建即时通讯服务器

在接下来的部分,我们将学习如何创建一个即时通讯服务器,接收用户发送的消息并将其重新分发给各自的接收者。

创建 TCP 服务器

在这一部分,我们将学习如何创建一个 TCP 服务器,不断监听特定端口以接收传入的消息。为了简单起见,我们将创建一个全局聊天室,其中每个用户都可以看到聊天室内每个用户发送的消息,而不是一个一对一的消息系统带有好友列表。一旦你了解了聊天系统的运作方式,你可以很容易地将这个系统改进为后者。

首先,转到文件|新建文件或项目,并在 C++类别下选择 C++类。然后,将类命名为server,并选择 QObject 作为基类。在创建自定义类之前,确保选中包含 QObject 选项。你也应该注意到了mainwindow.uimainwindow.hmainwindow.cpp的缺失。这是因为在控制台应用程序项目中没有用户界面。

一旦服务器类被创建,让我们打开server.h并添加以下头文件、变量和函数:

#ifndef SERVER_H 
#define SERVER_H 

#include <QObject> 
#include <QTcpServer> 
#include <QTcpSocket> 
#include <QDebug> 
#include <QVector> 

private: 
   QTcpServer* chatServer; 
   QVector<QTcpSocket*>* allClients; 

public:
   explicit server(QObject *parent = nullptr);
 void startServer();
   void sendMessageToClients(QString message); public slots: void newClientConnection();
  void socketDisconnected();
  void socketReadyRead();
  void socketStateChanged(QAbstractSocket::SocketState state);

接下来,创建一个名为startServer()的函数,并将以下代码添加到server.cpp中的函数定义中:

void server::startServer() 
{ 
   allClients = new QVector<QTcpSocket*>; 

   chatServer = new QTcpServer(); 
   chatServer->setMaxPendingConnections(10); 
   connect(chatServer, SIGNAL(newConnection()), this, 
   SLOT(newClientConnection())); 

   if (chatServer->listen(QHostAddress::Any, 8001)) 
   { 
         qDebug() << "Server has started. Listening to port 8001."; 
   } 
   else 
   { 
         qDebug() << "Server failed to start. Error: " + chatServer-
         >errorString(); 
   } 
} 

我们创建了一个名为chatServerQTcpServer对象,并使其不断监听端口8001。你可以选择从102449151范围内的任何未使用的端口号。此范围之外的其他数字通常保留用于常见系统,如 HTTP 或 FTP 服务,因此最好不要使用它们以避免冲突。我们还创建了一个名为allClientsQVector数组,用于存储所有连接的客户端,以便我们以后可以利用它来将传入的消息重定向到所有用户。

我们还使用了setMaxPendingConnections()函数来限制最大挂起连接数为 10 个客户端。你可以使用这种方法来保持活动客户端的数量,以便服务器的带宽始终在其限制范围内。这可以确保良好的服务质量并保持积极的用户体验。

监听客户端

每当客户端连接到服务器时,chatServer将触发newConnection()信号,因此我们将该信号连接到我们的自定义槽函数newClientConnection()。槽函数如下所示:

void server::newClientConnection() 
{ 
   QTcpSocket* client = chatServer->nextPendingConnection(); 
   QString ipAddress = client->peerAddress().toString(); 
   int port = client->peerPort(); 

   connect(client, &QTcpSocket::disconnected, this, &server::socketDisconnected); 
   connect(client, &QTcpSocket::readyRead, this, &server::socketReadyRead); 
   connect(client, &QTcpSocket::stateChanged, this, &server::socketStateChanged); 

   allClients->push_back(client); 

   qDebug() << "Socket connected from " + ipAddress + ":" + QString::number(port); 
} 

每个连接到服务器的新客户端都是一个QTcpSocket对象,可以通过调用nextPendingConnection()QTcpServer对象中获取。你可以通过调用peerAddress()peerPort()分别获取有关客户端的信息,如其 IP 地址和端口号。然后我们将每个新客户端存储到allClients数组中以供将来使用。我们还将客户端的disconnected()readyRead()stateChanged()信号连接到其相应的槽函数。

当客户端从服务器断开连接时,将触发disconnected()信号,随后将调用socketDisconnected()槽函数。在这个函数中,我们只是在服务器控制台上显示消息,当它发生时,什么都不做。你可以在这里做任何你喜欢的事情,比如将用户的离线状态保存到数据库等。为了简单起见,我们将在控制台窗口上打印出消息:

void server::socketDisconnected() 
{ 
   QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender()); 
   QString socketIpAddress = client->peerAddress().toString(); 
   int port = client->peerPort(); 

   qDebug() << "Socket disconnected from " + socketIpAddress + ":" + 
   QString::number(port); 
} 

接下来,每当客户端向服务器发送消息时,readyRead()信号将被触发。我们已经将该信号连接到一个名为socketReadyRead()的槽函数,它看起来像这样:

void server::socketReadyRead() 
{ 
   QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender()); 
   QString socketIpAddress = client->peerAddress().toString(); 
   int port = client->peerPort(); 

   QString data = QString(client->readAll()); 

   qDebug() << "Message: " + data + " (" + socketIpAddress + ":" + 
   QString::number(port) + ")"; 

   sendMessageToClients(data); 
} 

在上述代码中,我们只是简单地将消息重定向到一个名为sendMessageToClients()的自定义函数中,该函数处理将消息传递给所有连接的客户端。我们将在一分钟内看看这个函数是如何工作的。我们使用QObject::sender()来获取发出readyRead信号的对象的指针,并将其转换为QTcpSocket类,以便我们可以访问其readAll()函数。

之后,我们还将另一个名为stateChanged()的信号连接到socketStateChanged()槽函数。慢函数看起来像这样:

void server::socketStateChanged(QAbstractSocket::SocketState state) 
{ 
   QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender()); 
   QString socketIpAddress = client->peerAddress().toString(); 
   int port = client->peerPort(); 

   QString desc; 

   if (state == QAbstractSocket::UnconnectedState) 
         desc = "The socket is not connected."; 
   else if (state == QAbstractSocket::HostLookupState) 
         desc = "The socket is performing a host name lookup."; 
   else if (state == QAbstractSocket::ConnectingState) 
         desc = "The socket has started establishing a connection."; 
   else if (state == QAbstractSocket::ConnectedState) 
         desc = "A connection is established."; 
   else if (state == QAbstractSocket::BoundState) 
         desc = "The socket is bound to an address and port."; 
   else if (state == QAbstractSocket::ClosingState) 
         desc = "The socket is about to close (data may still be 
         waiting to be written)."; 
   else if (state == QAbstractSocket::ListeningState) 
         desc = "For internal use only."; 

   qDebug() << "Socket state changed (" + socketIpAddress + ":" + 
   QString::number(port) + "): " + desc; 
} 

此函数在客户端的网络状态发生变化时触发,例如连接、断开连接、监听等。我们将根据其新状态简单地打印出相关消息,以便更轻松地调试我们的程序。

现在,让我们看看sendMessageToClients()函数的样子:

void server::sendMessageToClients(QString message) 
{ 
   if (allClients->size() > 0) 
   { 
         for (int i = 0; i < allClients->size(); i++) 
         { 
               if (allClients->at(i)->isOpen() && allClients->at(i)-
               >isWritable()) 
               { 
                     allClients->at(i)->write(message.toUtf8()); 
               } 
         } 
   } 
} 

在上述代码中,我们只是简单地循环遍历allClients数组,并将消息数据传递给所有连接的客户端。

最后,打开main.cpp并添加以下代码来启动我们的服务器:

#include <QCoreApplication> 
#include "server.h" 

int main(int argc, char *argv[]) 
{ 
   QCoreApplication a(argc, argv); 

   server* myServer = new server(); 
   myServer->startServer(); 

   return a.exec(); 
} 

现在构建并运行程序,你应该看到类似这样的东西:

除了显示服务器正在监听端口8001之外,似乎没有发生任何事情。别担心,因为我们还没有创建客户端程序。让我们继续!

创建即时通讯客户端

在接下来的部分中,我们将继续创建我们的即时通讯客户端,用户将使用它来发送和接收消息。

设计用户界面

在本节中,我们将学习如何为即时通讯客户端设计用户界面并为其创建功能:

  1. 首先,通过转到文件|新建文件或项目来创建另一个 Qt 项目。然后在应用程序类别下选择 Qt Widget 应用程序。

  2. 项目创建后,打开mainwindow.ui并将一个行编辑和文本浏览器拖放到窗口画布中。然后,选择中央窗口小部件并单击位于上方小部件栏上的“垂直布局”按钮,以将垂直布局效果应用到小部件上:

  1. 之后,在底部放置一个水平布局,并将行编辑放入布局中。然后,从小部件框中拖放一个按钮到水平布局中,并将其命名为sendButton;我们还将其标签设置为Send,就像这样:

  1. 完成后,将另一个水平布局拖放到文本浏览器顶部。然后,将标签、行编辑和一个按钮放入水平布局中,就像这样:

我们将行编辑小部件称为nameInput,并将其默认文本设置为John Doe,这样用户就有了默认名称。然后,我们将推按钮称为connectButton,并将其标签更改为Connect

我们已经完成了一个非常简单的即时通讯程序的用户界面设计,它将执行以下任务:

  1. 连接到服务器

  2. 让用户设置他们的名字

  3. 可以看到所有用户发送的消息

  4. 用户可以输入并发送他们的消息供所有人查看

现在编译并运行项目,你应该看到你的程序看起来类似这样:

请注意,我还将窗口标题更改为Chat Client,这样看起来稍微更专业一些。您可以通过在层次结构窗口中选择MainWindow对象并更改其windowTitle属性来实现。

在下一节中,我们将开始进行编程工作,并实现上面列表中提到的功能。

实现聊天功能

在我们开始编写任何代码之前,我们必须通过打开项目文件(.pro)并在那里添加 network 关键字来启用网络模块:

QT += core gui network 

接下来,打开 mainwindow.h 并添加以下头文件和变量:

#ifndef MAINWINDOW_H 
#define MAINWINDOW_H 

#include <QMainWindow> 
#include <QDebug> 
#include <QTcpSocket> 

private: 
   Ui::MainWindow *ui; 
   bool connectedToHost; 
   QTcpSocket* socket; 

我们在 mainwindow.cpp 中默认将 connectedToHost 变量设置为 false

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 
   connectedToHost = false; 
} 

完成此操作后,我们需要实现的第一个功能是服务器连接。打开 mainwindow.ui,右键单击连接按钮,然后选择转到槽...,然后选择 clicked()。之后,将自动为您创建一个槽函数。在 SLOT 函数中添加以下代码:

void MainWindow::on_connectButton_clicked() 
{ 
   if (!connectedToHost) 
   { 
         socket = new QTcpSocket(); 

         connect(socket, SIGNAL(connected()), this, 
         SLOT(socketConnected())); 
         connect(socket, SIGNAL(disconnected()), this, 
         SLOT(socketDisconnected())); 
         connect(socket, SIGNAL(readyRead()), this, 
         SLOT(socketReadyRead())); 

         socket->connectToHost("127.0.0.1", 8001); 
   } 
   else 
   { 
         QString name = ui->nameInput->text(); 
         socket->write("<font color="Orange">" + name.toUtf8() + " has 
         left the chat room.</font>"); 

         socket->disconnectFromHost(); 
   } 
} 

在前面的代码中,我们基本上是检查了 connectedToHost 变量。如果变量为 false(表示客户端未连接到服务器),则创建一个名为 socketQTcpSocket 对象,并使其连接到端口 8801 上的 127.0.0.1 主机。IP 地址 127.0.0.1 代表本地主机。由于这仅用于测试目的,我们将客户端连接到位于同一台计算机上的测试服务器。如果您在另一台计算机上运行服务器,则可以根据需要将 IP 地址更改为局域网或广域网地址。

connected()disconnected()readReady() 信号被触发时,我们还将 socket 对象连接到其相应的槽函数。这与我们之前所做的服务器代码完全相同。如果客户端已连接到服务器并且单击了连接(现在标记为 Disconnect)按钮,则向服务器发送断开连接消息并终止连接。

接下来,我们将看看槽函数,这些槽函数在上一步中连接到了 socket 对象。第一个是 socketConnected() 函数,当客户端成功连接到服务器时将被调用:

void MainWindow::socketConnected() 
{ 
   qDebug() << "Connected to server."; 

   printMessage("<font color="Green">Connected to server.</font>"); 

   QString name = ui->nameInput->text(); 
   socket->write("<font color="Purple">" + name.toUtf8() + " has joined 
   the chat room.</font>"); 

   ui->connectButton->setText("Disconnect"); 
   connectedToHost = true; 
} 

首先,客户端将在应用程序输出和文本浏览器小部件上显示 Connected to server. 消息。我们马上就会看到 printMessage() 函数是什么样子。然后,我们从输入字段中获取用户的名称,并将其合并到文本消息中,然后将其发送到服务器,以便通知所有用户。最后,将连接按钮的标签设置为 Disconnect,并将 connectedToHost 变量设置为 true

接下来,让我们看看 socketDisconnected(),正如其名称所示,每当客户端从服务器断开连接时都会被调用:

void MainWindow::socketDisconnected() 
{ 
   qDebug() << "Disconnected from server."; 

   printMessage("<font color="Red">Disconnected from server.</font>"); 

   ui->connectButton->setText("Connect"); 
   connectedToHost = false; 
} 

前面的代码非常简单。它只是在应用程序输出和文本浏览器小部件上显示断开连接的消息,然后将断开按钮的标签设置为 Connect,将 connectedToHost 变量设置为 false。请注意,由于此函数仅在客户端从服务器断开连接后才会被调用,因此我们无法在那时向服务器发送任何消息以通知它断开连接。您应该在服务器端检查断开连接并相应地通知所有用户。

然后是 socketReadyRead() 函数,每当服务器向客户端发送数据时都会触发该函数。这个函数比之前的函数更简单,因为它只是将传入的数据传递给 printMessage() 函数,什么都不做:

void MainWindow::socketReadyRead() 
{ 
   ui->chatDisplay->append(socket->readAll()); 
} 

最后,让我们看看 printMessage() 函数是什么样子。实际上,它就是这么简单。它只是将消息附加到文本浏览器中,然后完成:

void MainWindow::printMessage(QString message) 
{ 
   ui->chatDisplay->append(message); 
} 

最后但同样重要的是,让我们看看如何实现向服务器发送消息的功能。打开 mainwindow.ui,右键单击发送按钮,选择转到槽...,然后选择 clicked() 选项。一旦为您创建了槽函数,将以下代码添加到函数中:

void MainWindow::on_sendButton_clicked() 
{ 
   QString name = ui->nameInput->text(); 
   QString message = ui->messageInput->text(); 
   socket->write("<font color="Blue">" + name.toUtf8() + "</font>: " + 
   message.toUtf8()); 

   ui->messageInput->clear(); 
} 

首先,我们获取用户的名称并将其与消息组合在一起。然后,在将整个内容发送到服务器之前,我们将名称设置为蓝色,通过调用write()来发送。之后,清除消息输入字段,完成。由于文本浏览器默认接受富文本,我们可以使用<font>标签来为文本着色。

现在编译并运行项目;您应该能够在不同的客户端之间进行聊天!在连接客户端之前,不要忘记打开服务器。如果一切顺利,您应该会看到类似于这样的内容:

同时,您还应该在服务器端看到所有的活动:

到此为止!我们已经成功使用 Qt 创建了一个简单的聊天系统。欢迎您在此基础上进行改进,创建一个完整的消息传递系统!

总结

在本章中,我们学习了如何使用 Qt 的网络模块创建即时消息传递系统。在接下来的章节中,我们将深入探讨使用 Qt 进行图形渲染的奇妙之处。

第十一章:实现图形编辑器

Qt 为我们提供了使用QPainter类进行低级图形渲染的功能。Qt 能够渲染位图和矢量图像。在本章中,我们将学习如何使用 Qt 绘制形状,并最终创建我们自己的绘图程序。

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

  • 绘制矢量形状

  • 将矢量图像保存为 SVG 文件

  • 创建绘图程序

准备好了吗?让我们开始吧!

绘制矢量形状

在接下来的部分,我们将学习如何在我们的 Qt 应用程序中使用 QPainter 类渲染矢量图形。

矢量与位图

计算机图形中有两种格式——位图和矢量。位图图像(也称为光栅图像)是以一系列称为像素的微小点存储的图像。每个像素将被分配一种颜色,并且以存储的方式显示在屏幕上——像素与屏幕上显示的内容之间是一一对应的关系。

另一方面,矢量图像不是基于位图模式,而是使用数学公式来表示可以组合成几何形状的线条和曲线。

这里列出了两种格式的主要特点:

  • 位图:

  • 通常文件大小较大

  • 不能放大到更高分辨率,因为图像质量会受到影响

  • 用于显示颜色丰富的复杂图像,如照片

  • 矢量:

  • 文件大小非常小

  • 图形可以调整大小而不影响图像质量

  • 每个形状只能应用有限数量的颜色(单色、渐变或图案)

  • 复杂形状需要高处理能力才能生成

这里的图表比较了位图和矢量图形:

在本节中,我们将专注于学习如何使用 Qt 绘制矢量图形,但我们也将在本章后面介绍位图图形。

使用 QPainter 绘制矢量形状

首先,通过转到文件|新建文件或项目来创建另一个 Qt 项目。然后在应用程序类别下选择 Qt Widget 应用程序。创建项目后,打开mainwindow.h并添加QPainter头文件:

#include <QMainWindow> 
#include <QPainter> 

之后,我们还声明了一个名为paintEvent()的虚函数,这是 Qt 中的标准事件处理程序,每当需要绘制东西时都会调用它,无论是 GUI 更新、窗口调整大小,还是手动调用update()函数时:

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    virtual void paintEvent(QPaintEvent *event); 

然后,打开mainwindow.cpp并添加paintEvent()函数:

void MainWindow::paintEvent(QPaintEvent *event) 
{ 
   QPainter painter; 
   painter.begin(this); 

   // Draw Line 
   painter.drawLine(QPoint(50, 60), QPoint(100, 100)); 

   // Draw Rectangle 
   painter.setBrush(Qt::BDiagPattern); 
   painter.drawRect(QRect(40, 120, 80, 30)); 

   // Draw Ellipse 
   QPen ellipsePen; 
   ellipsePen.setColor(Qt::red); 
   ellipsePen.setStyle(Qt::DashDotLine); 
   painter.setPen(ellipsePen); 
   painter.drawEllipse(QPoint(80, 200), 50, 20); 

   // Draw Rectangle 
   QPainterPath rectPath; 
   rectPath.addRect(QRect(150, 20, 100, 50)); 
   painter.setPen(QPen(Qt::red, 1, Qt::DashDotLine, Qt::FlatCap, 
   Qt::MiterJoin)); 
   painter.setBrush(Qt::yellow); 
   painter.drawPath(rectPath); 

   // Draw Ellipse 
   QPainterPath ellipsePath; 
   ellipsePath.addEllipse(QPoint(200, 120), 50, 20); 
   painter.setPen(QPen(QColor(79, 106, 25), 5, Qt::SolidLine, 
   Qt::FlatCap, Qt::MiterJoin)); 
   painter.setBrush(QColor(122, 163, 39)); 
   painter.drawPath(ellipsePath); 

   painter.end(); 
} 

如果现在构建程序,你应该会看到以下内容:

上面的代码真的很长。让我们把它分解一下,这样你就更容易理解了。每当调用paintEvent()时(通常在窗口需要绘制时会调用一次),我们调用QPainter::begin()告诉 Qt 我们要开始绘制东西了,然后在完成时调用QPainter::end()。因此,绘制图形的代码将包含在QPainter::begin()QPainter::end()之间。

让我们看看以下步骤:

  1. 我们绘制的第一件事是一条直线,这很简单——只需调用QPainter::drawLine()并将起点和终点值插入函数中。请注意,Qt 使用的坐标系统是以像素格式的。它的原点从应用程序窗口的左上角开始,并向右和向下方向增加,取决于xy的值。x值的增加将位置移动到右方向,而y值的增加将位置移动到下方向。

  2. 接下来,绘制一个矩形,在形状内部有一种阴影图案。这次,我们调用了QPainter::setBrush()来设置图案,然后调用drawRect()

  3. 之后,我们用虚线轮廓和图案在形状内部绘制了一个椭圆形。由于我们已经在上一步中设置了图案,所以我们不必再次设置。相反,我们使用QPen类在调用drawEllipse()之前设置轮廓样式。只需记住,在 Qt 的术语中,刷子用于定义形状的内部颜色或图案,而笔用于定义轮廓。

  4. 接下来的两个形状基本上与之前的相似;我们只是改变了不同的颜色和图案,这样你就可以看到它们与之前的例子之间的区别。

绘制文本

此外,您还可以使用QPainter类来绘制文本。在调用QPainter::drawText()之前,您只需要调用QPainter::setFont()来设置字体属性,就像这样:

QPainter painter; 
painter.begin(this); 

// Draw Text 
painter.setFont(QFont("Times", 14, QFont::Bold)); 
painter.drawText(QPoint(20, 30), "Testing"); 

// Draw Line 
painter.drawLine(QPoint(50, 60), QPoint(100, 100)) 

setFont()函数是可选的,如果您不指定它,将获得默认字体。完成后,构建并运行程序。您应该在窗口中看到“Hello World!”这个词显示出来:

在这里你可以看到,矢量形状基本上是由 Qt 实时生成的,无论你如何重新调整窗口大小和改变它的纵横比,它看起来都很好。如果你渲染的是位图图像,当它与窗口一起重新调整大小或改变纵横比时,它的视觉质量可能会下降。

将矢量图像保存到 SVG 文件

除了绘制矢量图形,Qt 还允许我们将这些图形保存为矢量图像文件,称为SVG(可缩放矢量图形)文件格式。SVG 格式是许多软件使用的开放格式,包括 Web 浏览器用于显示矢量图形。实际上,Qt 也可以读取 SVG 文件并在屏幕上呈现它们,但我们暂时跳过这一点。让我们看看如何将我们的矢量图形保存为 SVG 文件!

这个例子继续了我们在上一节中留下的地方。因此,我们不必创建一个新的 Qt 项目,只需坚持之前的项目即可。

首先,如果主窗口还没有菜单栏,让我们为主窗口添加一个菜单栏。然后,打开mainwindow.ui,在表单编辑器中,右键单击层次结构窗口上的 MainWindow 对象,然后选择创建菜单栏:

完成后,将文件添加到菜单栏,然后在其下方添加“另存为 SVG”:

然后,转到底部的操作编辑器,右键单击我们刚刚添加的菜单选项,并选择转到槽...:

将弹出一个窗口询问您选择一个信号。选择triggered(),然后点击确定。这样就会在mainwindow.cpp中为您创建一个新的槽函数。在打开mainwindow.cpp之前,让我们打开我们的项目文件.pro)并添加以下svg模块:

QT += core gui svg 

svg关键字告诉 Qt 向您的项目添加相关类,可以帮助您处理 SVG 文件格式。然后,我们还需要在mainwindow.h中添加另外两个头文件:

#include <QtSvg/QSvgGenerator> 
#include <QFileDialog> 

之后,打开mainwindow.cpp并将以下代码添加到我们刚刚在上一步中添加的槽函数中:

void MainWindow::on_actionSave_as_SVG_triggered() 
{ 
    QString filePath = QFileDialog::getSaveFileName(this, "Save SVG", "", "SVG files (*.svg)"); 

    if (filePath == "") 
        return; 

    QSvgGenerator generator; 
    generator.setFileName(filePath); 
    generator.setSize(QSize(this->width(), this->height())); 
    generator.setViewBox(QRect(0, 0, this->width(), this->height())); 
    generator.setTitle("SVG Example"); 
    generator.setDescription("This SVG file is generated by Qt."); 

    paintAll(&generator); 
} 

在前面的代码中,我们使用QFileDialog让用户选择他们想要保存 SVG 文件的位置。然后,我们使用QSvgGenerator类将图形导出到 SVG 文件中。最后,我们调用paintAll()函数,这是我们将在下一步中定义的自定义函数。

实际上,我们需要修改现有的paintAll()方法并将我们的渲染代码放入其中。然后,将QSvgGenerator对象作为绘制设备传递到函数输入中:

void MainWindow::paintAll(QSvgGenerator *generator) 
{ 
    QPainter painter; 

    if (generator) 
        painter.begin(generator); 
    else 
        painter.begin(this); 

   // Draw Text 
    painter.setFont(QFont("Times", 14, QFont::Bold)); 
   painter.drawText(QPoint(20, 30), "Hello World!"); 

因此,我们的paintEvent()现在在mainwindow.cpp中看起来像这样:

void MainWindow::paintEvent(QPaintEvent *event) 
{ 
   paintAll(); 
} 

这里的过程可能看起来有点混乱,但它的基本作用是在创建窗口时调用paintAll()函数一次绘制所有图形,然后当您想要将图形保存到 SVG 文件时再次调用paintAll()

唯一的区别是绘图设备——一个是主窗口本身,我们将其用作绘图画布,对于后者,我们将QSvgGenerator对象传递为绘图设备,它将把图形保存到 SVG 文件中。

现在构建并运行程序,单击文件|保存 SVG 文件,您应该能够将图形保存到 SVG 文件中。尝试用网络浏览器打开文件,看看它是什么样子的:

看起来我的网络浏览器(Firefox)不支持填充图案,但其他东西都很好。由于矢量图形是由程序生成的,形状不存储在 SVG 文件中(只存储数学公式及其变量),您可能需要确保用户平台支持您使用的功能。

在下一节中,我们将学习如何创建我们自己的绘画程序,并使用它绘制位图图像!

创建绘画程序

在接下来的部分,我们将转向像素领域,并学习如何使用 Qt 创建绘画程序。用户将能够通过使用不同大小和颜色的画笔来表达他们的创造力,绘制像素图像!

设置用户界面

同样,对于这个例子,我们将创建一个新的 Qt Widget 应用程序。之后,打开mainwindow.ui并在主窗口上添加一个菜单栏。然后,在菜单栏中添加以下选项:

我们的菜单栏上有三个菜单项——文件、画笔大小和画笔颜色。在文件菜单下有将画布保存为位图文件的功能,以及清除整个画布的功能。画笔大小类别包含不同的画笔大小选项;最后,画笔颜色类别包含设置画笔颜色的几个选项。

您可以选择更像绘画Photoshop的 GUI 设计,但出于简单起见,我们现在将使用这个。

完成所有这些后,打开mainwindow.h并在顶部添加以下头文件:

#include <QMainWindow> 
#include <QPainter> 
#include <QMouseEvent> 
#include <QFileDialog> 

之后,我们还声明了一些虚拟函数,如下所示:

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    virtual void mousePressEvent(QMouseEvent *event); 
    virtual void mouseMoveEvent(QMouseEvent *event); 
    virtual void mouseReleaseEvent(QMouseEvent *event); 
    virtual void paintEvent(QPaintEvent *event); 
    virtual void resizeEvent(QResizeEvent *event); 

除了我们在上一个示例中使用的paintEvent()函数之外,我们还可以添加一些用于处理鼠标事件和窗口调整事件的函数。然后,我们还向我们的MainWindow类添加以下变量:

private: 
    Ui::MainWindow *ui; 
 QImage image; 
    bool drawing; 
    QPoint lastPoint; 
    int brushSize; 
    QColor brushColor; 

之后,让我们打开mainwindow.cpp并从类构造函数开始:

MainWindow::MainWindow(QWidget *parent) : 
    QMainWindow(parent), 
    ui(new Ui::MainWindow) 
{ 
    ui->setupUi(this); 

 image = QImage(this->size(), QImage::Format_RGB32); 
    image.fill(Qt::white); 

    drawing = false; 
    brushColor = Qt::black; 
    brushSize = 2; 
} 

我们需要首先创建一个QImage对象,它充当画布,并将其大小设置为与我们的窗口大小相匹配。然后,我们将默认画笔颜色设置为黑色,其默认大小设置为2。之后,我们将看一下每个事件处理程序及其工作原理。

首先,让我们看一下paintEvent()函数,这也是我们在矢量图形示例中使用的。这一次,它所做的就是调用QPainter::drawImage()并在我们的主窗口上渲染QImage对象(我们的图像缓冲区):

void MainWindow::paintEvent(QPaintEvent *event)
{
    QPainter canvasPainter(this);
    canvasPainter.drawImage(this->rect(), image, image.rect());
}

接下来,我们将看一下resizeEvent()函数,每当用户调整主窗口大小时都会触发该函数。为了避免图像拉伸,我们必须调整图像缓冲区的大小以匹配新的窗口大小。这可以通过创建一个新的QImage对象并设置其大小与调整后的主窗口相同来实现,然后复制先前的 QImage 的像素信息,并将其放置在新图像缓冲区的完全相同的位置。

这意味着如果窗口大小小于绘图,您的图像将被裁剪,但至少画布不会被拉伸和扭曲图像,当窗口调整大小时。让我们看一下代码:

void MainWindow::resizeEvent(QResizeEvent *event) 
{ 
    QImage newImage(event->size(), QImage::Format_RGB32); 
    newImage.fill(qRgb(255, 255, 255)); 

    QPainter painter(&newImage); 
    painter.drawImage(QPoint(0, 0), image); 
    image = newImage; 
} 

接下来,我们将看一下鼠标事件处理程序,我们将使用它来在画布上应用颜色。首先是mousePressEvent()函数,当我们开始按下鼠标按钮(在这种情况下是左鼠标按钮)时将触发该函数。在这一点上我们仍然没有画任何东西,但是将绘图布尔值设置为true并将我们的光标位置保存到lastPoint变量中。

void MainWindow::mousePressEvent(QMouseEvent *event) 
{ 
    if (event->button() == Qt::LeftButton) 
    { 
        drawing = true; 
        lastPoint = event->pos(); 
    } 
} 

然后,这是mouseMoveEvent()函数,当鼠标光标移动时将被调用:

void MainWindow::mouseMoveEvent(QMouseEvent *event) 
{ 
    if ((event->buttons() & Qt::LeftButton) && drawing) 
    { 
        QPainter painter(&image); 
        painter.setPen(QPen(brushColor, brushSize, Qt::SolidLine, 
        Qt::RoundCap, Qt::RoundJoin)); 
        painter.drawLine(lastPoint, event->pos()); 

        lastPoint = event->pos(); 
        this->update(); 
    } 
} 

在前面的代码中,我们检查是否确实在按住鼠标左键移动鼠标。如果是,那么我们就从上一个光标位置画一条线到当前光标位置。然后,我们保存当前光标位置到lastPoint变量,并调用update()通知 Qt 触发paintEvent()函数。

最后,当我们释放鼠标左键时,将调用mouseReleaseEvent()。我们只需将绘图变量设置为false,然后完成:

void MainWindow::mouseReleaseEvent(QMouseEvent *event) 
{ 
    if (event->button() == Qt::LeftButton) 
    { 
        drawing = false; 
    } 
} 

如果我们现在构建并运行程序,我们应该能够在我们的小绘画程序上开始绘制一些东西:

尽管现在我们可以绘制一些东西,但都是相同的笔刷大小和相同的颜色。这有点无聊!让我们在主菜单的“笔刷大小”类别上右键单击每个选项,然后选择“转到槽...”,然后选择“触发()”选项,然后按“确定”。然后 Qt 将为我们创建相应的槽函数,我们需要在这些函数中做的就是基本上改变 brushSize 变量,就像这样:

void MainWindow::on_action2px_triggered() 
{ 
    brushSize = 2; 
} 

void MainWindow::on_action5px_triggered() 
{ 
    brushSize = 5; 
} 

void MainWindow::on_action10px_triggered() 
{ 
    brushSize = 10; 
} 

在“笔刷颜色”类别下的所有选项也是一样的。这次,我们相应地设置了brushColor变量:

void MainWindow::on_actionBlack_triggered() 
{ 
    brushColor = Qt::black; 
} 

void MainWindow::on_actionWhite_triggered() 
{ 
    brushColor = Qt::white; 
} 

void MainWindow::on_actionRed_triggered() 
{ 
    brushColor = Qt::red; 
} 

void MainWindow::on_actionGreen_triggered() 
{ 
    brushColor = Qt::green; 
} 

void MainWindow::on_actionBlue_triggered() 
{ 
    brushColor = Qt::blue; 
} 

如果您再次构建和运行程序,您将能够使用各种笔刷设置绘制图像:

除此之外,我们还可以将现有的位图图像添加到我们的画布上,以便我们可以在其上绘制。假设我有一个企鹅图像,以 PNG 图像的形式存在(名为tux.png),我们可以在类构造函数中添加以下代码:

MainWindow::MainWindow(QWidget *parent) : 
    QMainWindow(parent), 
    ui(new Ui::MainWindow) 
{ 
    ui->setupUi(this); 

    image = QImage(this->size(), QImage::Format_RGB32); 
    image.fill(Qt::white); 

    QImage tux; 
    tux.load(qApp->applicationDirPath() + "/tux.png"); 
    QPainter painter(&image); 
    painter.drawImage(QPoint(100, 100), tux); 

    drawing = false; 
    brushColor = Qt::black; 
    brushSize = 2; 
} 

前面的代码基本上打开图像文件并将其移动到位置 100 x 100,然后将图像绘制到我们的图像缓冲区上。现在,每当我们启动程序时,我们就可以在画布上看到一个企鹅:

接下来,我们将看一下“文件”下的“清除”选项。当用户在菜单栏上点击此选项时,我们使用以下代码清除整个画布(包括企鹅)并重新开始:

void MainWindow::on_actionClear_triggered() 
{ 
    image.fill(Qt::white); 
    this->update(); 
} 

最后,当用户点击“文件”下的“保存”选项时,我们打开一个文件对话框,让用户将他们的作品保存为位图文件。在以下代码中,我们过滤图像格式,只允许用户保存 PNG 和 JPEG 格式:

void MainWindow::on_actionSave_triggered() 
{ 
    QString filePath = QFileDialog::getSaveFileName(this, "Save Image", "", "PNG (*.png);;JPEG (*.jpg *.jpeg);;All files (*.*)"); 

    if (filePath == "") 
        return; 

    image.save(filePath); 
} 

就是这样,我们成功地使用 Qt 从头开始创建了一个简单的绘画程序!您甚至可以将从本章学到的知识与上一章结合起来,创建一个在线协作白板!唯一的限制就是您的创造力。最后,我要感谢所有读者使用我们新创建的绘画程序创建了以下杰作:

总结

在这一章中,我们学习了如何绘制矢量和位图图形,随后我们使用 Qt 创建了自己的绘画程序。在接下来的章节中,我们将研究创建一个将数据传输并存储到云端的程序的方面。

第十二章:云存储

在上一章中,我们学习了如何使用 Qt 在屏幕上绘制图像。然而,在本章中,我们将学习完全不同的东西,即设置我们自己的文件服务器并将其链接到我们的 Qt 应用程序。

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

  • 设置 FTP 服务器

  • 在列表视图上显示文件列表

  • 将文件上传到 FTP 服务器

  • 从 FTP 服务器下载文件

让我们开始吧!

设置 FTP 服务器

在接下来的部分,我们将学习如何设置 FTP 服务器,该服务器存储用户上传的所有文件,并允许他们随时下载。这一部分与 Qt 无关,因此如果您已经运行了 FTP 服务器,请跳过此部分并继续本章的下一部分。

介绍 FTP

FTP文件传输协议的缩写。FTP 用于在网络上从一台计算机传输文件到另一台计算机,通常是通过互联网。FTP 只是云存储技术的众多形式之一,但它也是一种简单的形式,您可以轻松地在自己的计算机上设置。

有许多不同的 FTP 服务器是由不同的人群为特定操作系统开发的。在本章的这一部分,我们将学习如何设置运行在 Windows 操作系统上的 FileZilla 服务器。如果您运行其他操作系统,如 GNU、Linux 或 macOS,还有许多其他 FTP 服务器程序可供使用,如 VSFTP 和 Pure-FTPd。

在 Debian、Ubuntu 或其他类似的 Linux 变体上,在终端上运行sudo apt-get install vsftpd将安装和配置 FTP 服务器。在 macOS 上,从苹果菜单中打开“系统偏好设置”,然后选择“共享”。然后,点击“服务”选项卡,选择 FTP 访问。最后,点击“启动”按钮启动 FTP 服务器。

如果您已经运行了 FTP 服务器,请跳过到下一节,我们将开始学习 C++编程。

下载 FileZilla

FileZilla 真的很容易设置和配置。它提供了一个完全功能的、易于使用的用户界面,不需要任何先前的操作经验。我们需要做的第一件事是下载 FileZilla。我们将按照以下步骤进行:

  1. 打开浏览器,跳转到filezilla-project.org。您将在主页上看到两个下载按钮。

  2. 点击“下载 FileZilla 服务器”,它将带我们到下载页面:

  1. 一旦您到达下载页面,点击“下载 FileZilla 服务器”按钮并开始下载软件。我们不会使用 FileZilla 客户端,所以您不需要下载它。一切准备就绪后,让我们继续安装软件。

  2. 像大多数 Windows 软件一样,安装过程非常简单。保持一切默认,然后一直点击下一步,直到安装过程开始。安装过程最多只需要几分钟。

  3. 完成后,点击“关闭”按钮,我们完成了!:

设置 FileZilla

安装完 FileZilla 后,控制面板很可能会自动打开。

  1. 由于这是您第一次启动 FileZilla,它将要求您设置服务器。将服务器 IP 地址保持为127.0.0.1(即localhost),将管理员端口设置为14147

  2. 输入您想要的服务器管理密码,并勾选“始终连接到此服务器”选项。点击连接,FTP 服务器现在将启动!如下截图所示:

  1. FTP 服务器启动后,我们需要创建一个用户帐户。点击左侧的第四个图标打开“用户”对话框:

  1. 然后,在常规页面下,单击窗口右侧的添加按钮。通过设置用户名创建一个帐户,然后单击确定。

  2. 我们现在不必为用户设置任何组,因为用户组仅在您有许多具有相同特权设置的用户时才有用,因为这样可以更容易地一次更改所有用户的设置或将用户移动到不同的组中。创建用户后,选中密码选项并输入所需的密码。将密码放在您的 FTP 帐户上始终是一个好习惯:

  1. 之后,我们将继续到共享文件夹页面,并为我们新创建的用户添加一个共享目录。

  2. 确保删除和追加选项已选中,以便可以替换具有相同名称的文件。我们将在稍后使用它来更新我们的文件列表:

  1. 如果单击从左起的第三个图标,将出现 FileZilla 服务器选项对话框。您基本上可以在这里配置一切以满足您的需求。例如,如果您不想使用默认端口号21,您可以在选项窗口中简单地更改它,在常规设置页面下:

  1. 您还可以在速度限制页面为所有用户或特定用户设置速度限制。这可以防止您的服务器在许多用户同时下载大文件时性能下降:

接下来,让我们继续创建我们的 Qt 项目!

在列表视图上显示文件列表

在上一节中,我们成功地设置了一个 FTP 服务器并使其保持运行。在接下来的部分中,我们将学习如何创建一个 FTP 客户端程序,该程序显示文件列表,将文件上传到 FTP 服务器,最后从中下载文件。

设置项目

像往常一样,让我们使用Qt Creator创建一个新项目。以下步骤将有所帮助:

  1. 我们可以通过转到文件|新文件或项目并选择 Qt 小部件应用程序来创建一个新项目。

  2. 创建项目后,打开您的项目(.pro)文件,并添加network关键字,以便 Qt 知道您的项目需要网络模块:

QT += core gui network

设置用户界面

之后,打开mainwindow.ui并执行以下步骤来设计用户界面的上半部分以上传文件:

  1. 放置一个标签,上面写着上传文件:放在其他小部件的顶部。

  2. 在标签下方放置一个水平布局和两个按钮,分别写着打开和上传。

  3. 在水平布局下放置一个进度条。

  4. 在底部放置一个水平线,然后是垂直间隔器:

接下来,我们将构建用户界面的底部部分,用于下载文件:

这次,我们的用户界面与上半部分非常相似,只是我们在第二个进度条之前添加了一个列表视图来显示文件列表。我们将所有内容放在同一页上,以便更简单和不易混淆地解释这个示例程序。

显示文件列表

接下来,我们将学习如何保存并显示 FTP 服务器上的文件列表。实际上,FTP 服务器默认提供文件列表,并且 Qt 能够在旧版本中使用qtftp模块显示它。但是,自从版本 5 以来,Qt 已经完全放弃了qtftp模块,这个功能不再存在。

如果您仍然对旧的qtftp模块感兴趣,您仍然可以通过访问以下链接在 GitHub 上获取其源代码:github.com/qt/qtftp

在 Qt 中,我们使用QNetworkAccessManager类与我们的 FTP 服务器通信,因此不再使用专门为 FTP 设计的功能。但是,不用担心,我们将研究一些其他替代方法来实现相同的结果。

在我看来,最好的方法是使用在线数据库来存储文件列表及其信息(文件大小、格式、状态等)。如果您有兴趣学习如何将 Qt 应用程序连接到数据库,请参阅第三章,数据库连接。然而,为了简单起见,我们将使用另一种方法,它可以正常工作,但不够安全——直接将文件名保存在文本文件中,并将其存储在 FTP 服务器上。

如果您正在为客户或公司做一个严肃的项目,请不要使用这种方法。查看第三章,数据库连接,并学习使用实际数据库。

好吧,假设除了使用文本文件之外没有其他办法;我们该怎么做呢?很简单:创建一个名为files.txt的文本文件,并将其放入我们在本章开头创建的 FTP 目录中。

编写代码

接下来,打开mainwindow.h并添加以下头文件:

#include <QMainWindow> 
#include <QDebug> 
#include <QNetworkAccessManager> 
#include <QNetworkRequest> 
#include <QNetworkReply> 
#include <QFile> 
#include <QFileInfo> 
#include <QFileDialog> 
#include <QListWidgetItem> 
#include <QMessageBox> 

之后,添加以下变量和函数:

private: 
   Ui::MainWindow *ui; 
 QNetworkAccessManager* manager; 

   QString ftpAddress; 
   int ftpPort; 
   QString username; 
   QString password; 

   QNetworkReply* downloadFileListReply; 
   QNetworkReply* uploadFileListReply; 

   QNetworkReply* uploadFileReply; 
   QNetworkReply* downloadFileReply; 

   QStringList fileList; 
   QString uploadFileName; 
   QString downloadFileName; 

public:
   void getFileList();

完成上一步后,打开mainwindow.cpp并将以下代码添加到类构造函数中:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

 manager = new QNetworkAccessManager(this); 

   ftpAddress = "ftp://127.0.0.1/"; 
   ftpPort = 21; 
   username = "tester"; // Put your FTP user name here
   password = "123456"; // Put your FTP user password here 
   getFileList(); 
} 

我们所做的基本上是初始化QNetworkAccessManager对象并设置存储我们的 FTP 服务器信息的变量,因为我们将在后续步骤中多次使用它。之后,我们将调用getFileList()函数开始从 FTP 服务器下载files.txtgetFileList()函数如下所示:

void MainWindow::getFileList() 
{ 
   QUrl ftpPath; 
   ftpPath.setUrl(ftpAddress + "files.txt"); 
   ftpPath.setUserName(username); 
   ftpPath.setPassword(password); 
   ftpPath.setPort(ftpPort); 

   QNetworkRequest request; 
   request.setUrl(ftpPath); 

   downloadFileListReply = manager->get(request); 
   connect(downloadFileListReply, &QNetworkReply::finished, this, 
   &MainWindow::downloadFileListFinished); 
} 

我们使用QUrl对象来存储有关我们的服务器和我们试图下载的文件位置的信息,然后将其提供给QNetworkRequest对象,然后通过调用QNetworkAccessManager::get()将其发送出去。由于我们不知道何时所有文件将完全下载,因此我们利用了 Qt 的SIGNALSLOT机制。

我们连接了来自downloadFileListReply指针(指向mainwindow.h中的QNetworkReply对象)的finished()信号,并将其链接到slot函数downloadFileListFinished(),如下所示:

void MainWindow::downloadFileListFinished() 
{ 
   if(downloadFileListReply->error() != QNetworkReply::NoError) 
   { 
         QMessageBox::warning(this, "Failed", "Failed to load file 
         list: " + downloadFileListReply->errorString()); 
   } 
   else 
   { 
         QByteArray responseData; 
         if (downloadFileListReply->isReadable()) 
         { 
               responseData = downloadFileListReply->readAll(); 
         } 

         // Display file list 
         ui->fileList->clear(); 
         fileList = QString(responseData).split(","); 

         if (fileList.size() > 0) 
         { 
               for (int i = 0; i < fileList.size(); i++) 
               { 
                     if (fileList.at(i) != "") 
                     { 
                           ui->fileList->addItem(fileList.at(i)); 
                     } 
               } 
         } 
   } 
} 

代码有点长,所以我将函数分解为以下步骤:

  1. 如果在下载过程中出现任何问题,请显示一个消息框,告诉我们问题的性质。

  2. 如果一切顺利并且下载已经完成,我们将尝试通过调用downloadFileListReply | readAll()来读取数据。

  3. 然后,清空列表窗口并开始解析文本文件的内容。我们在这里使用的格式非常简单;我们只使用逗号符号来分隔每个文件名:filename1,filename2,filename,...。重要的是我们不要在实际项目中这样做。

  4. 一旦我们调用split(",")将字符串拆分为字符串列表,就进行for循环并在列表窗口中显示每个文件名。

测试前面的代码是否有效,创建一个名为files.txt的文本文件,并将以下文本添加到文件中:

filename1,filename2,filename3 

然后,将文本文件放到 FTP 目录中并运行项目。您应该能够在应用程序中看到它出现如下:

一旦它工作正常,我们可以清空文本文件的内容并继续下一节。

将文件上传到 FTP 服务器

由于我们的 FTP 目录中还没有任何文件(除了文件列表),让我们编写代码以允许我们上传我们的第一个文件。

  1. 首先,打开mainwindow.ui,右键单击“打开”按钮。然后,选择“转到槽”并选择“clicked()”选项:

  1. 将自动为您创建一个slot函数。然后,将以下代码添加到函数中,以打开文件选择器窗口,让用户选择要上传的文件:
void MainWindow::on_openButton_clicked() 
{ 
   QString fileName = QFileDialog::getOpenFileName(this, "Select 
   File", qApp->applicationDirPath()); 
   ui->uploadFileInput->setText(fileName); 
}
  1. 之后,重复此步骤,并对“上传”按钮执行相同操作。这次,其slot函数的代码看起来像下面这样:
void MainWindow::on_uploadButton_clicked() 
{ 
   QFile* file = new QFile(ui->uploadFileInput->text()); 
   QFileInfo fileInfo(*file); 
   uploadFileName = fileInfo.fileName(); 

   QUrl ftpPath; 
   ftpPath.setUrl(ftpAddress + uploadFileName); 
   ftpPath.setUserName(username); 
   ftpPath.setPassword(password); 
   ftpPath.setPort(ftpPort); 

   if (file->open(QIODevice::ReadOnly)) 
   { 
         ui->uploadProgress->setEnabled(true); 
         ui->uploadProgress->setValue(0); 

         QNetworkRequest request; 
         request.setUrl(ftpPath); 

         uploadFileReply = manager->put(request, file); 
         connect(uploadFileReply, 
         SIGNAL(uploadProgress(qint64,qint64)), this, 
         SLOT(uploadFileProgress(qint64,qint64))); 
         connect(uploadFileReply, SIGNAL(finished()), this,  
         SLOT(uploadFileFinished())); 
   } 
   else 
   { 
         QMessageBox::warning(this, "Invalid File", "Failed to open 
         file for upload."); 
   } 
} 

代码看起来有点长,所以让我们分解一下:

  1. 我们使用QFile类打开我们要上传的文件(文件路径取自ui->uploadFileInput->text())。如果文件不存在,显示一个消息框通知用户。

  2. 然后,我们将 FTP 服务器和上传目的地的信息填入一个QUrl对象中,然后将其提供给QNetworkRequest对象。

  3. 之后,我们开始读取文件的内容,并将其提供给QNetworkAccessManager::put()函数。

  4. 由于我们不知道文件何时会完全上传,我们使用了 Qt 提供的SIGNALSLOT机制。我们将uploadProgress()finished()信号链接到我们的两个自定义slot函数uploadFileProgress()uploadFileFinised()

slot函数uploadFileProgress()将告诉我们上传的当前进度,因此我们可以用它来设置进度条:

void MainWindow::uploadFileProgress(qint64 bytesSent, qint64 bytesTotal) 
{ 
   qint64 percentage = 100 * bytesSent / bytesTotal; 
   ui->uploadProgress->setValue((int) percentage); 
} 

与此同时,当文件完全上传时,uploadFileFinished()函数将被触发:

void MainWindow::uploadFileFinished() 
{ 
   if(uploadFileReply->error() != QNetworkReply::NoError) 
   { 
         QMessageBox::warning(this, "Failed", "Failed to upload file: " 
         + uploadFileReply->errorString()); 
   } 
   else 
   { 
         QMessageBox::information(this, "Success", "File successfully 
         uploaded."); 
   } 
} 

我们还没有完成前面的函数。由于已向 FTP 服务器添加了新文件,我们必须更新现有文件列表,并替换存储在 FTP 目录中的files.txt文件。由于代码稍微长一些,我们将把代码分成几个部分,这些部分都发生在显示文件成功上传消息框之前。

  1. 首先,让我们检查新上传的文件是否已经存在于我们的文件列表中(替换 FTP 服务器上的旧文件)。如果存在,我们可以跳过整个过程;否则,将文件名追加到我们的fileList字符串列表中,如下所示:
// Add new file to file list array if not exist yet 
bool exists = false; 
if (fileList.size() > 0) 
{ 
   for (int i = 0; i < fileList.size(); i++) 
   { 
         if (fileList.at(i) == uploadFileName) 
         { 
               exists = true; 
         } 
   } 
} 

if (!exists) 
{ 
   fileList.append(uploadFileName); 
} 
  1. 之后,在我们应用程序的目录中创建一个临时文本文件(files.txt),并将新文件列表保存在文本文件中:
// Create new files.txt 
QString fileName = "files.txt"; 
QFile* file = new QFile(qApp->applicationDirPath() + "/" + fileName); 
file->open(QIODevice::ReadWrite); 
if (fileList.size() > 0) 
{ 
   for (int j = 0; j < fileList.size(); j++) 
   { 
         if (fileList.at(j) != "") 
         { 
               file->write(QString(fileList.at(j) + ",").toUtf8()); 
         } 
   } 
} 
file->close(); 
  1. 最后,我们使用QFile类打开我们刚创建的文本文件,并将其再次上传到 FTP 服务器以替换旧的文件列表:
// Re-open the file 
QFile* newFile = new QFile(qApp->applicationDirPath() + "/" + fileName); 
if (newFile->open(QIODevice::ReadOnly)) 
{ 
   // Update file list to server 
   QUrl ftpPath; 
   ftpPath.setUrl(ftpAddress + fileName); 
   ftpPath.setUserName(username); 
   ftpPath.setPassword(password); 
   ftpPath.setPort(ftpPort); 

   QNetworkRequest request; 
   request.setUrl(ftpPath); 
   uploadFileListReply = manager->put(request, newFile); 
   connect(uploadFileListReply, SIGNAL(finished()), this, SLOT(uploadFileListFinished())); 
   file->close(); 
} 
  1. 再次使用SIGNALSLOT机制,以便在文件列表上传完成时得到通知。slot函数uploadFileListFinished()看起来像下面这样:
void MainWindow::uploadFileListFinished() 
{ 
   if(uploadFileListReply->error() != QNetworkReply::NoError) 
   { 
         QMessageBox::warning(this, "Failed", "Failed to update file list: " + uploadFileListReply->errorString()); 
   } 
   else 
   { 
         getFileList(); 
   } 
} 

  1. 我们基本上只是在更新文件列表到 FTP 服务器后再次调用getFileList()。如果现在构建和运行项目,您应该能够将第一个文件上传到本地 FTP 服务器,万岁!

从 FTP 服务器下载文件

现在我们已经成功将第一个文件上传到 FTP 服务器,让我们创建一个功能,将文件下载回我们的计算机!

  1. 首先,再次打开mainwindow.ui,右键单击“设置文件夹”按钮。选择转到槽... 并选择 clicked()信号以创建一个slot函数。slot函数非常简单;它只会打开一个文件选择对话框,但这次它只允许用户选择一个文件夹,因为我们为其提供了一个QFileDialog::ShowDirsOnly标志:
void MainWindow::on_setFolderButton_clicked() 
{ 
   QString folder = QFileDialog::getExistingDirectory(this, tr("Open Directory"), qApp->applicationDirPath(), QFileDialog::ShowDirsOnly); 
   ui->downloadPath->setText(folder); 
} 
  1. 然后,在列表窗口上右键单击并选择转到槽... 这一次,我们将选择itemDoubleClicked(QListWidgetItem*)选项:

  1. 当用户在列表窗口中双击项目时,将触发以下函数,启动下载。文件名可以通过调用item->text()QListWidgetItem对象中获取:
void MainWindow::on_fileList_itemDoubleClicked(QListWidgetItem *item) 
{ 
   downloadFileName = item->text(); 

   // Check folder 
   QString folder = ui->downloadPath->text(); 
   if (folder != "" && QDir(folder).exists()) 
   { 
         QUrl ftpPath; 
         ftpPath.setUrl(ftpAddress + downloadFileName); 
         ftpPath.setUserName(username); 
         ftpPath.setPassword(password); 
         ftpPath.setPort(ftpPort); 

         QNetworkRequest request; 
         request.setUrl(ftpPath); 

         downloadFileReply = manager->get(request); 
         connect(downloadFileReply, 
         SIGNAL(downloadProgress(qint64,qint64)), this, 
         SLOT(downloadFileProgress(qint64,qint64))); 
         connect(downloadFileReply, SIGNAL(finished()), this, 
         SLOT(downloadFileFinished())); 
   } 
   else 
   { 
         QMessageBox::warning(this, "Invalid Path", "Please set the 
         download path before download."); 
   } 
} 
  1. 就像我们在upload函数中所做的那样,我们在这里也使用了SIGNALSLOT机制来获取下载过程的进展以及完成信号。slot函数downloadFileProgress()将在下载过程中被调用,我们用它来设置第二个进度条的值:
void MainWindow::downloadFileProgress(qint64 byteReceived,qint64 bytesTotal) 
{ 
   qint64 percentage = 100 * byteReceived / bytesTotal; 
   ui->downloadProgress->setValue((int) percentage); 
} 
  1. 然后,当文件完全下载时,slot函数downloadFileFinished()将被调用。之后,我们将读取文件的所有数据并将其保存到我们想要的目录中:
void MainWindow::downloadFileFinished() 
{ 
   if(downloadFileReply->error() != QNetworkReply::NoError) 
   { 
         QMessageBox::warning(this, "Failed", "Failed to download 
         file: " + downloadFileReply->errorString()); 
   } 
   else 
   { 
         QByteArray responseData; 
         if (downloadFileReply->isReadable()) 
         { 
               responseData = downloadFileReply->readAll(); 
         } 

         if (!responseData.isEmpty()) 
         { 
               // Download finished 
               QString folder = ui->downloadPath->text(); 
               QFile file(folder + "/" + downloadFileName); 
               file.open(QIODevice::WriteOnly); 
               file.write((responseData)); 
               file.close(); 

               QMessageBox::information(this, "Success", "File 
               successfully downloaded."); 
         } 
   } 
}
  1. 现在构建程序,你应该能够下载文件列表上列出的任何文件!

总结

在本章中,我们学习了如何使用 Qt 的网络模块创建自己的云存储客户端。在接下来的章节中,我们将学习更多关于多媒体模块,并使用 Qt 从头开始创建自己的多媒体播放器。

第十三章:多媒体查看器

在上一章中,我们学习了如何通过云存储上传和下载文件。现在,在本章中,我们将学习如何使用 Qt 的多媒体模块打开这些文件,特别是媒体文件,如图像、音乐和视频。

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

  • 重新访问多媒体模块

  • 图像查看器

  • 音乐播放器

  • 视频播放器

让我们开始!

重新访问多媒体模块

在本章中,我们将再次使用多媒体模块,这在第九章中已经介绍过,相机模块。但是,这一次我们将使用模块的其他部分,所以我认为剖析模块并看看里面有什么是个好主意。

剖析模块

多媒体模块是一个非常庞大的模块,包含许多不同的部分,提供非常不同的功能和功能。主要类别如下:

  • 音频

  • 视频

  • 相机

  • 收音机

请注意,处理图像格式的类,如QImageQPixmap等,不是多媒体模块的一部分,而是 GUI 模块的一部分。这是因为它们是 GUI 的重要组成部分,不能分开。尽管如此,我们仍将在本章中介绍QImage类。

在每个类别下都有一些子类别,看起来像下面这样:

  • 音频:

  • 音频输出

  • 音频录制器

  • 视频:

  • 视频录制器

  • 视频播放器

  • 视频播放列表

  • 相机:

  • 相机取景器

  • 相机图像捕获

  • 相机视频录制器

  • 收音机:

  • 收音机调谐器(适用于支持模拟收音机的设备)

每个类都设计用于实现不同的目的。例如,QSoundEffect用于播放低延迟音频文件(如 WAV 文件)。另一方面,QAudioOutput将原始音频数据输出到特定的音频设备,这使您可以对音频输出进行低级控制。最后,QMediaPlayer是一个高级音频(和视频)播放器,支持许多不同的高延迟音频格式。在选择项目的正确类之前,您必须了解所有类之间的区别。

Qt 中的多媒体模块是一个庞大的怪兽,经常会让新手感到困惑,但如果您知道该选择哪个,它可能会带来好处。多媒体模块的另一个问题是,它可能会或可能不会在您的目标平台上工作。这是因为在所有这些类的底层都有特定平台的本机实现。如果特定平台不支持某个功能,或者尚未对其进行实现,那么您将无法使用这些功能。

有关 Qt 多媒体模块提供的不同类的更多信息,请访问以下链接:

doc.qt.io/qt-5.10/qtmultimedia-index.html

图像查看器

数字图像已经成为我们日常生活中的重要组成部分。无论是自拍、毕业晚会照片还是有趣的表情包,我们花费大量时间查看数字图像。在接下来的部分中,我们将学习如何使用 Qt 和 C++创建我们自己的图像查看器。

为图像查看器设计用户界面

让我们开始创建我们的第一个多媒体程序。在本节中,我们将创建一个图像查看器,正如其名称所示,它会打开一个图像文件并在窗口上显示它:

  1. 让我们打开 Qt Creator 并创建一个新的 Qt Widgets 应用程序项目。

  2. 之后,打开mainwindow.ui并向中央窗口添加一个Label(命名为imageDisplay),它将用作渲染图像的画布。然后,通过选择中央窗口并按下位于画布顶部的垂直布局按钮,向 centralWidget 添加一个布局:

  1. 您可以删除工具栏和状态栏以给Label腾出空间。此外,将中央窗口的布局边距设置为0

  2. 之后,双击菜单栏,添加一个文件操作,然后在其下方添加打开文件:

  1. 然后,在操作编辑器下,右键单击打开文件操作,选择转到槽...:

  1. 将弹出一个窗口,询问您选择一个信号,因此选择triggered(),然后点击确定:

一个slot函数将自动为您创建,但我们将在下一部分保留它。我们已经完成了用户界面,而且真的很简单。接下来,让我们继续并开始编写我们的代码!

为图像查看器编写 C++代码

让我们通过以下步骤开始:

  1. 首先,打开mainwindow.h并添加以下头文件:
#include <QMainWindow> 
#include <QFileDialog> 
#include <QPixmap> 
#include <QPainter>
  1. 然后,添加以下变量,称为imageBuffer,它将作为指向重新缩放之前的实际图像数据的指针。然后,也添加函数:
private: 
   Ui::MainWindow *ui; 
 QPixmap* imageBuffer; 

public:
   void resizeImage();
 void paintEvent(QPaintEvent *event);

public slots:
   void on_actionOpen_triggered();
  1. 接下来,打开mainwindow.cpp并在类构造函数中初始化imageBuffer变量:
MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 
   imageBuffer = nullptr; 
} 
  1. 之后,在上一部分中 Qt 为我们创建的slot函数中添加以下代码:
void MainWindow::on_actionOpen_triggered() 
{ 
   QString fileName = QFileDialog::getOpenFileName(this, "Open Image File", qApp->applicationDirPath(), "JPG (*.jpg *.jpeg);;PNG (*.png)"); 

   if (!fileName.isEmpty()) 
   { 
         imageBuffer = new QPixmap(fileName); 
         resizeImage(); 
   } 
}
  1. 上述代码基本上打开了文件选择对话框,并创建了一个QPixmap对象,其中包含所选的图像文件。完成所有这些后,它将调用resizeImage()函数,代码如下所示:
void MainWindow::resizeImage() 
{ 
   if (imageBuffer != nullptr) 
   { 
         QSize size = ui->imageDisplay->size(); 
         QPixmap pixmap = imageBuffer->scaled(size, 
            Qt::KeepAspectRatio); 

         // Adjust the position of the image to the center 
         QRect rect = ui->imageDisplay->rect(); 
         rect.setX((this->size().width() - pixmap.width()) / 2); 
         rect.setY((this->size().height() - pixmap.height()) / 2); 

         QPainter painter; 
         painter.begin(this); 
         painter.drawPixmap(rect, pixmap, ui->imageDisplay->rect()); 
         painter.end(); 
   } 
} 

resizeImage()函数的作用是简单地从imageBuffer变量中复制图像数据,并将图像调整大小以适应窗口大小,然后显示在窗口的画布上。您可能打开的图像比屏幕分辨率大得多,我们不希望在打开这样一个大图像文件时裁剪图像。

我们使用imageBuffer变量的原因是,这样我们可以保留原始数据的副本,并且不会通过多次调整大小来影响图像质量。

最后,我们还在paintEvent()函数中调用resizeImage()函数。每当主窗口被调整大小或从最小化状态恢复时,paintEvent()将自动被调用,resizeImage()函数也将被调用,如下所示:

void MainWindow::paintEvent(QPaintEvent *event) 
{ 
   resizeImage(); 
} 

就是这样。如果现在构建并运行项目,您应该会得到一个看起来像下面这样的漂亮的图像查看器:

音乐播放器

在接下来的部分中,我们将学习如何使用 Qt 和 C++构建自定义音乐播放器。

为音乐播放器设计用户界面

让我们继续下一个项目。在这个项目中,我们将使用 Qt 构建一个音频播放器。执行以下步骤:

  1. 与上一个项目一样,我们将创建一个Qt Widgets 应用程序项目。

  2. 打开项目文件(.pro),并添加multimedia模块:

QT += core gui multimedia 
  1. 我们添加了multimedia文本,以便 Qt 在我们的项目中包含与多媒体模块相关的类。接下来,打开mainwindow.ui,并参考以下截图构建用户界面:

我们基本上在顶部添加了一个标签,然后添加了一个水平滑块和另一个标签来显示音频的当前时间。之后,我们在底部添加了三个按钮,分别是播放按钮、暂停按钮和停止按钮。这些按钮的右侧是另一个水平布局,用于控制音频音量。

如您所见,所有按钮目前都没有图标,很难分辨每个按钮的用途。

  1. 要为按钮添加图标,让我们转到文件 | 新建文件或项目,并在 Qt 类别下选择 Qt 资源文件。然后,创建一个名为icons的前缀,并将图标图像添加到前缀中:

  2. 之后,通过设置其图标属性并选择选择资源...,将这些图标添加到推按钮。然后,将位于音量滑块旁边的标签的pixmap属性设置为音量图标:

  3. 在您将图标添加到推按钮和标签之后,用户界面应该看起来更好了!

我们已经完成了用户界面,让我们继续进行编程部分!

为音乐播放器编写 C++代码

要为音乐播放器编写 C++代码,请执行以下步骤:

  1. 首先,打开mainwindow.h并添加以下标头:
#include <QMainWindow> 
#include <QDebug> 
#include <QFileDialog> 
#include <QMediaPlayer> 
#include <QMediaMetaData> 
#include <QTime> 
  1. 之后,添加player变量,它是一个QMediaPlayer指针。然后,声明我们将稍后定义的函数:
private: 
   Ui::MainWindow *ui; 
   QMediaPlayer* player; 

public:
 void stateChanged(QMediaPlayer::State state);
 void positionChanged(qint64 position);
  1. 接下来,打开mainwindow.cpp并初始化播放器变量:
MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

   player = new QMediaPlayer(this); 
   player->setVolume(ui->volume->value()); 
   connect(player, &QMediaPlayer::stateChanged, this, &MainWindow::stateChanged); 
   connect(player, &QMediaPlayer::positionChanged, this, &MainWindow::positionChanged); 
} 

QMediaPlayer类是我们的应用程序用来播放由其加载的任何音频文件的主要类。因此,我们需要知道音频播放的状态及其当前位置。我们可以通过将其stateChanged()positionChanged()信号连接到我们的自定义slot函数来获取这些信息。

  1. stateChanged()信号允许我们获取有关音频播放的当前状态的信息。然后,我们相应地启用和禁用推按钮:
void MainWindow::stateChanged(QMediaPlayer::State state) 
{ 
   if (state == QMediaPlayer::PlayingState) 
   { 
         ui->playButton->setEnabled(false); 
         ui->pauseButton->setEnabled(true); 
         ui->stopButton->setEnabled(true); 
   } 
   else if (state == QMediaPlayer::PausedState) 
   { 
         ui->playButton->setEnabled(true); 
         ui->pauseButton->setEnabled(false); 
         ui->stopButton->setEnabled(true); 
   } 
   else if (state == QMediaPlayer::StoppedState) 
   { 
         ui->playButton->setEnabled(true); 
         ui->pauseButton->setEnabled(false); 
         ui->stopButton->setEnabled(false); 
   } 
} 

  1. 至于positionChanged()slot函数,我们使用它们来设置时间轴滑块以及计时器显示:
 void MainWindow::positionChanged(qint64 position) 
{ 
   if (ui->progressbar->maximum() != player->duration()) 
         ui->progressbar->setMaximum(player->duration()); 

   ui->progressbar->setValue(position); 

   int seconds = (position/1000) % 60; 
   int minutes = (position/60000) % 60; 
   int hours = (position/3600000) % 24; 
   QTime time(hours, minutes,seconds); 
   ui->durationDisplay->setText(time.toString()); 
} 

  1. 完成后,打开mainwindow.ui,右键单击每个推按钮,然后选择转到槽...然后选择clicked()信号。这将为每个推按钮生成一个slot函数。这些slot函数的代码非常简单:
void MainWindow::on_playButton_clicked() 
{  
   player->play(); 
} 

void MainWindow::on_pauseButton_clicked() 
{ 
   player->pause(); 
} 

void MainWindow::on_stopButton_clicked() 
{ 
   player->stop(); 
} 
  1. 之后,在两个水平滑块上右键单击,并选择转到槽...然后选择sliderMoved()信号,然后单击确定:

  2. 每当用户拖动滑块更改其位置时,都会调用sliderMoved()信号。我们需要将此位置发送到媒体播放器,并告诉它调整音频音量或更改当前音频位置。请注意不要将音量滑块的默认位置设置为零。考虑以下代码:

void MainWindow::on_volume_sliderMoved(int position) 
{ 
   player->setVolume(position); 
} 

void MainWindow::on_progressbar_sliderMoved(int position) 
{ 
   player->setPosition(position); 
} 
  1. 然后,我们需要向菜单栏添加文件和打开文件操作,就像我们在上一个示例项目中所做的那样。

  2. 然后,在操作编辑器中右键单击打开文件操作,选择转到槽...之后,选择triggered(),让 Qt 为您生成一个slot函数。将以下代码添加到用于选择音频文件的slot函数中:

 void MainWindow::on_actionOpen_File_triggered() 
{ 
   QString fileName = QFileDialog::getOpenFileName(this,
      "Select Audio File", qApp->applicationDirPath(), 
       "MP3 (*.mp3);;WAV (*.wav)"); 
   QFileInfo fileInfo(fileName); 

   player->setMedia(QUrl::fromLocalFile(fileName)); 

   if (player->isMetaDataAvailable()) 
   { 
         QString albumTitle = player-
         >metaData(QMediaMetaData::AlbumTitle).toString(); 
         ui->songNameDisplay->setText("Playing " + albumTitle); 
   } 
   else 
   { 
         ui->songNameDisplay->setText("Playing " + 
           fileInfo.fileName()); 
   } 

   ui->playButton->setEnabled(true); 
   ui->playButton->click(); 
} 

上述简单地打开一个文件选择对话框,只接受 MP3 和 WAV 文件。如果您愿意,也可以添加其他格式,但支持的格式因平台而异;因此,您应该测试以确保您想要使用的格式受支持。

之后,它将选定的音频文件发送到媒体播放器进行预加载。然后,我们尝试从元数据中获取音乐的标题,并在Labelwidget上显示它。但是,此功能(获取元数据)可能会或可能不会受到您的平台支持,因此,以防它不会显示,我们将其替换为音频文件名。最后,我们启用播放按钮并自动开始播放音乐。

就是这样。如果您现在构建并运行项目,您应该能够获得一个简单但完全功能的音乐播放器!

视频播放器

在上一节中,我们已经学习了如何创建音频播放器。在本章中,我们将进一步改进我们的程序,并使用 Qt 和 C++创建视频播放器。

为视频播放器设计用户界面

下一个示例是视频播放器。由于QMediaPlayer还支持视频输出,我们可以使用上一个音频播放器示例中的相同用户界面和 C++代码,只需对其进行一些小的更改。

  1. 首先,打开项目文件(.pro)并添加另一个关键字,称为multimediawidgets
QT += core gui multimedia multimediawidgets 
  1. 然后,打开mainwindow.ui,在时间轴滑块上方添加一个水平布局(将其命名为movieLayout)。之后,右键单击布局,选择转换为 | QFrame。然后将其 sizePolicy 属性设置为 Expanding, Expanding:

  1. 之后,我们通过设置其styleSheet属性将 QFrame 的背景设置为黑色:
background-color: rgb(0, 0, 0); 
  1. 用户界面应该看起来像下面这样,然后我们就完成了:

为视频播放器编写 C++代码

要为视频播放器编写 C++代码,我们执行以下步骤:

  1. 对于mainwindow.h,对它的更改并不多。我们只需要在头文件中包含QVideoWidget
#include <QMainWindow> 
#include <QDebug> 
#include <QFileDialog> 
#include <QMediaPlayer> 
#include <QMediaMetaData> 
#include <QTime> 
#include <QVideoWidget> 
  1. 然后,打开mainwindow.cpp。在将其添加到我们在上一步中添加的QFrame对象的布局之前,我们必须定义一个QVideoWidget对象并将其设置为视频输出目标:
MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 

   player = new QMediaPlayer(this); 

   QVideoWidget* videoWidget = new QVideoWidget(this); 
   player->setVideoOutput(videoWidget); 
   ui->movieLayout->addWidget(videoWidget); 

   player->setVolume(ui->volume->value()); 
   connect(player, &QMediaPlayer::stateChanged, this, &MainWindow::stateChanged); 
   connect(player, &QMediaPlayer::positionChanged, this, &MainWindow::positionChanged); 
} 
  1. slot函数中,当“打开文件”操作被触发时,我们只需将文件选择对话框更改为仅接受MP4MOV格式。如果您愿意,也可以添加其他视频格式:
QString fileName = QFileDialog::getOpenFileName(this, "Select Movie File", qApp->applicationDirPath(), "MP4 (*.mp4);;MOV (*.mov)"); 

就是这样。代码的其余部分与音频播放器示例几乎相同。这个示例的主要区别在于我们定义了视频输出小部件,Qt 会为我们处理其余部分。

如果我们现在构建和运行项目,应该会得到一个非常流畅的视频播放器,就像您在这里看到的那样:

在 Windows 系统上,有一个情况是视频播放器会抛出错误。这个问题类似于这里报告的问题:stackoverflow.com/questions/32436138/video-play-returns-directshowplayerservicedoseturlsource-unresolved-error-cod

要解决此错误,只需下载并安装 K-Lite_Codec_Pack,您可以在此处找到:www.codecguide.com/download_k-lite_codec_pack_basic.htm。之后,视频应该可以正常播放!

总结

在本章中,我们已经学会了如何使用 Qt 创建自己的多媒体播放器。接下来的内容与我们通常的主题有些不同。在接下来的章节中,我们将学习如何使用 QtQuick 和 QML 创建触摸友好、移动友好和图形导向的应用程序。

第十四章:Qt Quick 和 QML

在这一章中,我们将学习与本书其他章节非常不同的内容。Qt 包括两种不同的应用开发方法。第一种方法是 Qt Widgets 和 C++,这是我们在之前所有章节中都涵盖过的内容。第二种方法是使用 Qt Quick 控件和 QML 脚本语言,这将在本章中介绍。

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

  • 介绍 Qt Quick 和 QML

  • Qt Quick 控件和控制

  • Qt Quick 设计师

  • Qt Quick 布局

  • 基本的 QML 脚本

准备好了吗?让我们开始吧!

介绍 Qt Quick 和 QML

在接下来的部分,我们将学习 Qt Quick 和 QML 是什么,以及如何利用它们来开发 Qt 应用程序,而无需编写 C++代码。

介绍 Qt Quick

Qt Quick是 Qt 中的一个模块,为开发面向触摸和视觉的应用程序提供了一整套用户界面引擎和语言基础设施。开发人员选择 Qt Quick 后,将使用 Qt Quick 对象和控件,而不是通常的 Qt Widgets 进行用户界面设计。

此外,开发人员将使用类似于JavaScript的 QML 语言编写代码,而不是使用 C++代码。但是,您可以使用 Qt 提供的 C++ API 来扩展 QML 应用程序,通过相互调用每种语言的函数(在 QML 中调用 C++函数,反之亦然)。

开发人员可以通过在创建项目时选择正确的选项来选择他们喜欢的开发应用程序的方法。开发人员可以选择 Qt Quick 应用程序而不是通常的 Qt Widgets 应用程序选项,这将告诉 Qt Creator 为您的项目创建不同的起始文件和设置,从而增强 Qt Quick 模块:

当您创建 Qt Quick 应用程序项目时,Qt Creator 将要求您选择项目的最低要求 Qt 版本:

选择了 Qt 版本后,Qt Quick 设计师将确定要启用哪些功能,并在 QML 类型窗口上显示哪些小部件。我们将在后面的部分中更多地讨论这些内容。

介绍 QML

QMLQt 建模语言)是一种用于设计触摸友好用户界面的用户界面标记语言,类似于 CSS 在 HTML 上的工作方式。与 C++或 JavaScript 不同,它们都是命令式语言,QML 是一种声明式语言。在声明式编程中,您只需在脚本中表达逻辑,而不描述其控制流。它只是告诉计算机要做什么,而不是如何做。然而,命令式编程需要语句来指定操作。

当您打开新创建的 Qt Quick 项目时,您将在项目中看到main.qmlMainForm.ui.qml,而不是通常的mainwindow.hmainwindow.cpp文件。您可以在以下截图中的项目目录中看到这一点:

这是因为整个项目主要将在 QML 上运行,而不是在 C++上。您将看到的唯一 C++文件是main.cpp,它在应用程序启动时只是加载main.qml文件。main.cpp中执行此操作的代码如下所示:

int main(int argc, char *argv[]) 
{ 
   QGuiApplication app(argc, argv); 

   QQmlApplicationEngine engine; 
   engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); 
   if (engine.rootObjects().isEmpty()) 
         return -1; 

   return app.exec(); 
} 

您应该已经意识到有两种类型的 QML 文件,一种是扩展名为.qml,另一种是扩展名为.ui.qml。尽管它们都使用相同的语法等,但它们在项目中的作用是非常不同的。

首先,.ui.qml文件(在开头多了一个.ui)用作基于 Qt Quick 的用户界面设计的声明文件。您可以使用 Qt Quick Designer 可视化编辑器编辑.ui.qml文件,并轻松设计应用程序的 GUI。您也可以向文件添加自己的代码,但对文件中可以包含的代码有一些限制,特别是与逻辑代码相关的限制。当运行 Qt Quick 应用程序时,Qt Quick 引擎将阅读.ui.qml文件中存储的所有信息,并相应地构建用户界面,这与 Qt Widgets 应用程序中使用的.ui文件非常相似。

然后,我们有另一个只有.qml扩展名的文件。这个文件仅用于构建 Qt Quick 应用程序中的逻辑和功能,就像 Qt Widget 应用程序中使用的.h.cpp文件一样。这两种不同的格式将应用程序的视觉定义与其逻辑块分开。这使开发人员能够将相同的逻辑代码应用于不同的用户界面模板。您不能使用 Qt Quick Designer 打开.qml文件,因为它不用于 GUI 声明。.qml文件是由开发人员手动编写的,对他们使用的 QML 语言特性没有限制。

让我们首先打开MainForm.ui.qml,看看这两个 QML 文件的区别。默认情况下,Qt Creator 将打开用户界面设计师(Qt Quick Designer);然而,让我们通过按左侧面板上的编辑按钮切换到代码编辑模式:

然后,您将能够看到形成您在设计模式中看到的用户界面的 QML 脚本。让我们分析这段代码,看看 QML 与 C++相比是如何工作的。在MainForm.ui.qml中,您首先看到的是这行代码:

import QtQuick 2.6 

这非常简单明了;我们需要导入带有适当版本号的Qt Quick模块。不同的 Qt Quick 版本可能具有不同的功能,并支持不同的部件控件。有时,甚至语法可能略有不同。请确保为您的项目选择正确的版本,并确保它支持您需要的功能。如果不知道要使用哪个版本,请考虑使用最新版本。

接下来,我们将看到在两个大括号之间声明的不同 GUI 对象(我们称之为 QML 类型)。我们首先看到的是Rectangle类型:

    Rectangle { 
       property alias mouseArea: mouseArea 
       property alias textEdit: textEdit 

       width: 360 
       height: 360 
       ... 

在这种情况下,Rectangle类型是窗口背景,类似于 Qt Widget 应用程序项目中使用的中央窗口部件。让我们看看Rectangle下面的其他 QML 类型:

    MouseArea { 
        id: mouseArea 
        anchors.fill: parent 
    } 

    TextEdit { 
        id: textEdit 
        text: qsTr("Enter some text...") 
        verticalAlignment: Text.AlignVCenter 
        anchors.top: parent.top 
        anchors.horizontalCenter: parent.horizontalCenter 
        anchors.topMargin: 20 
        Rectangle { 
            anchors.fill: parent 
            anchors.margins: -10 
            color: "transparent" 
            border.width: 1 
        } 
    } 

MousArea类型,顾名思义,是一个检测鼠标点击和触摸事件的无形形状。您基本上可以通过在其上放置MouseArea将任何东西变成按钮。之后,我们还有一个TextEdit类型,其行为与 Qt Widget 应用程序中的Line Edit部件完全相同。

您可能已经注意到,在Rectangle声明中有两个带有alias关键字的属性。这两个属性公开了MouseAreaTextEdit类型,并允许其他 QML 脚本与它们交互,接下来我们将学习如何做到这一点。

现在,打开main.qml并查看其代码:

import QtQuick 2.6 
import QtQuick.Window 2.2 

Window { 
    visible: true 
    width: 640 
    height: 480 
    title: qsTr("Hello World") 

    MainForm { 
        anchors.fill: parent 
        mouseArea.onClicked: { 
            console.log(qsTr('Clicked on background. Text: "' + 
            textEdit.text + '"')) 
        } 
    } 
} 

在上面的代码中,有一个Window类型,只能通过导入QtQuick.Window模块才能使用。设置了Window类型的属性后,声明了MainForm类型。这个MainForm类型实际上就是我们之前在MainForm.ui.qml中看到的整个用户界面。由于MouseAreaTextEdit类型已在MainForm.ui.qml中公开,我们现在可以在main.qml中访问并使用它们。

QML 还使用 Qt 提供的信号和槽机制,但写法略有不同,因为我们不再编写 C++代码。例如,我们可以在上面的代码中看到onClicked的使用,这是一个内置信号,相当于 Qt Widgets 应用程序中的clicked()。由于.qml文件是我们定义应用程序逻辑的地方,我们可以定义onClicked被调用时发生的事情。另一方面,我们不能在.ui.qml中做同样的事情,因为它只允许与视觉相关的代码。如果你尝试在.ui.qml文件中编写逻辑相关的代码,Qt Creator 会发出警告。

就像 Qt Widgets 应用程序一样,您也可以像以前一样构建和运行项目。默认示例应用程序看起来像这样:

您可能会意识到构建过程非常快。这是因为 QML 代码默认不会被编译成二进制代码。QML 是一种解释性语言,就像 JavaScript 一样,因此不需要编译就可以执行。在构建过程中,所有 QML 文件将被打包到应用程序的资源系统中。然后,在应用程序启动时,Qt Quick 引擎将加载和解释 QML 文件。

但是,您仍然可以选择使用包含在 Qt 中的Qt Quick Compiler程序将您的 QML 脚本编译成二进制代码,以使代码执行速度略快于通常情况。这是一个可选步骤,除非您要在资源非常有限的嵌入式系统上运行应用程序,否则不需要。

现在我们已经了解了Qt QuickQML语言是什么,让我们来看看 Qt 提供的所有不同的 QML 类型。

Qt Quick 小部件和控件

在 Qt Quick 的领域中,小部件和控件被称为QML 类型。默认情况下,Qt Quick Designer为我们提供了一组基本的 QML 类型。您还可以导入随不同模块提供的其他 QML 类型。此外,如果没有现有的类型符合您的需求,甚至可以创建自定义的 QML 类型。

让我们来看看 Qt Quick Designer 默认提供的 QML 类型。首先,这是基本类别下的 QML 类型:

让我们看看不同的选项:

  • Border Image:Border Image 是一个设计用来创建可维持其角形状和边框的可伸缩矩形形状的 QML 类型。

  • Flickable:Flickable 是一个包含所有子类型的 QML 类型,并在其裁剪区域内显示它们。Flickable 还被ListViewGridView类型扩展和用于滚动长内容。它也可以通过触摸屏轻扫手势移动。

  • Focus Scope:Focus Scope 是一个低级别的 QML 类型,用于促进其他 QML 类型的构建,这些类型在被按下或释放时可以获得键盘焦点。我们通常不直接使用这种 QML 类型,而是使用直接从它继承的其他类型,如GroupBoxScrollViewStatusBar等。

  • ImageImage类型基本上是不言自明的。它可以加载本地或网络上的图像。

  • ItemItem类型是 Qt Quick 中所有可视项的最基本的 QML 类型。Qt Quick 中的所有可视项都继承自这个Item类型。

  • MouseArea:我们已经在默认的 Qt Quick 应用程序项目中看到了MouseArea类型的示例用法。它在预定义区域内检测鼠标点击和触摸事件,并在检测到时调用 clicked 信号。

  • RectangleRectangle QML 类型与Item类型非常相似,只是它有一个可以填充纯色或渐变的背景。您还可以选择使用自己的颜色和厚度添加边框。

  • 文本Text QML 类型也很容易理解。它只是在窗口上显示一行文本。您可以使用它来显示特定字体系列和字体大小的纯文本和富文本。

  • 文本编辑:文本编辑 QML 类型相当于 Qt Widgets 应用程序中的文本编辑小部件。当焦点在它上面时,允许用户输入文本。它可以显示纯文本和格式化文本,这与文本输入类型非常不同。

  • 文本输入:文本输入 QML 类型相当于 Qt Widgets 应用程序中的行编辑小部件,因为它只能显示单行可编辑的纯文本,这与文本编辑类型不同。您还可以通过验证器或输入掩码对其应用输入约束。通过将echoMode设置为PasswordPasswordEchoOnEdit,它也可以用于密码输入字段。

我们在这里讨论的 QML 类型是 Qt Quick Designer 默认提供的最基本的类型。这些也是用于构建其他更复杂的 QML 类型的基本构建块。Qt Quick 还提供了许多额外的模块,我们可以将其导入到我们的项目中,例如,如果我们在MainForm.ui.qml文件中添加以下行:

import QtQuick.Controls 2.2

当您切换到设计模式时,Qt Quick Designer 将在您的 Qt Quick Designer 上显示一堆额外的 QML 类型:

我们不会逐一介绍所有这些 QML 类型,因为它们太多了。如果您有兴趣了解更多关于这些 QML 类型的信息,请访问以下链接:doc.qt.io/qt-5.10/qtquick-controls-qmlmodule.html

Qt Quick Designer

接下来,我们将看一下 Qt Quick Designer 对 Qt Quick 应用程序项目的布局。当您打开一个.ui.qml文件时,Qt Quick Designer,即包含在 Qt Creator 工具集中的设计工具,将自动为您启动。

自从本书第一章以来一直跟随所有示例项目的读者可能会意识到,Qt Quick Designer 看起来与我们一直在使用的设计工具有些不同。这是因为 Qt Quick 项目与 Qt Widgets 项目非常不同,因此设计工具自然也应该有所不同以适应其需求。

让我们看看 Qt Quick 项目中的 Qt Quick Designer 是什么样子的:

  1. 库:库窗口显示当前项目可用的所有 QML 类型。您可以单击并将其拖动到画布窗口中以将其添加到您的 UI 中。您还可以创建自己的自定义 QML 类型并在此处显示。

  2. 资源:资源窗口以列表形式显示所有资源,然后可以在 UI 设计中使用。

  3. 导入:导入窗口允许您将不同的 Qt Quick 模块导入到当前项目中。

  4. 导航器:导航器窗口以树形结构显示当前 QML 文件中的项目。它类似于 Qt Widgets 应用程序项目中的对象操作器窗口。

  5. 连接:连接窗口由几个不同的选项卡组成:连接、绑定、属性和后端。这些选项卡允许您在不切换到编辑模式的情况下向您的 QML 文件添加连接(信号和槽)、绑定和属性。

  6. 状态窗格:状态窗格显示 QML 项目中的不同状态,通常描述 UI 配置,例如 UI 控件、它们的属性和行为以及可用操作。

  7. 画布:画布是您设计应用程序 UI 的工作区。

  8. 属性窗格:与我们在 Qt Widgets 应用程序项目中使用的属性编辑器类似,QML 设计师中的属性窗格显示所选项目的属性。在更改这里的值后,您可以立即在 UI 中看到结果。

Qt Quick 布局

与 Qt Widget 应用程序一样,Qt Quick 应用程序中也存在布局系统。唯一的区别是在 Qt Quick 中称为定位器:

最显著的相似之处是列和行定位器。这两者与 Qt Widgets 应用程序中的垂直布局和水平布局完全相同。除此之外,网格定位器也与网格布局相同。

在 Qt Quick 中唯一额外的是 Flow 定位器。Flow 定位器中包含的项目会像页面上的单词一样排列,项目沿一个轴排成一行,然后沿另一个轴放置项目行。

基本的 QML 脚本

在接下来的部分中,我们将学习如何使用 Qt Quick Designer 和 QML 创建我们的第一个 Qt Quick 应用程序!

设置项目

话不多说,让我们动手使用 QML 创建一个 Qt Quick 应用程序吧!在这个示例项目中,我们将使用 Qt Quick Designer 和一个 QML 脚本创建一个虚拟登录界面。首先,让我们打开 Qt Creator,并通过转到文件|新建文件或项目...来创建一个新项目。

在那之后,选择 Qt Quick 应用程序并按“选择”....之后,一直按“下一步”直到项目创建完成。我们将在这个示例项目中使用所有默认设置,包括最小所需的 Qt 版本:

项目创建完成后,我们需要向项目中添加一些图像文件,以便稍后使用它们:

您可以在我们的 GitHub 页面上获取源文件(包括这些图像):github.com/PacktPublishing/Hands-On-GUI-Programming-with-C-QT5

我们可以通过右键单击项目窗格中的qml.qrc文件并选择在编辑器中打开来将这些图像添加到我们的项目中。添加一个名为images的新前缀,并将所有图像文件添加到该前缀中:

在那之后,打开MainForm.ui.qml,并删除 QML 文件中的所有内容。我们通过向画布添加一个 Item 类型,将其大小设置为 400 x 400,并将其命名为loginForm来重新开始。之后,在其下方添加一个Image类型,并将其命名为background。然后将背景图像应用到Image类型上,画布现在看起来像这样:

然后,在Image类型(背景)下添加一个Rectangle类型,并在属性窗格中打开布局选项卡。启用垂直和水平锚定选项。之后,将width设置为402height设置为210,将vertical anchor margin设置为50

接着,我们将矩形的颜色设置为#fcf9f4,边框颜色设置为#efedeb,然后将边框值设置为1。到目前为止,用户界面看起来像这样:

接下来,在矩形下添加一个 Image QML 类型,并将其锚定设置为顶部锚定和水平锚定。然后将其顶部锚定边距设置为-110,并将 logo 图像应用到其image source属性上。您可以通过单击位于画布顶部的小按钮来打开和关闭 QML 类型的边界矩形和条纹,这样在画布上充满内容时更容易查看结果:

然后,我们在loginRect矩形下的画布中添加了三个Rectangle类型,并将它们命名为emailRectpasswordRectloginButton。矩形的锚定设置如下所示:

然后,将emailRectpasswordRectborder值设置为1color设置为#ffffffbordercolor设置为#efedeb。至于loginButton,我们将border设置为0radius设置为2color设置为#27ae61。登录屏幕现在看起来像这样:

看起来不错。接下来,我们将在emailRectpasswordRect中添加TextInputImageMouseAreaText QML 类型。由于这里有许多 QML 类型,我将列出需要设置的属性:

  • TextInput:

  • 选择颜色设置为#4f0080

  • 启用左锚点、右锚点和垂直锚点

  • 左锚点边距20,右锚点边距40,垂直边距3

  • 为密码输入设置 echoMode 为 Password

  • Image:

  • 启用右锚点和垂直锚点

  • 右锚点边距设置为10

  • 将图像源设置为电子邮件图标或密码图标

  • 将图像填充模式设置为 PreserveAspectFit

  • MouseArea:

  • 启用填充父项

  • Text:

  • 将文本属性分别设置为E-MailPassword

  • 文本颜色设置为#cbbdbd

  • 将文本对齐设置为左对齐和顶部对齐

  • 启用左锚点、右锚点和垂直锚点

  • 左锚点边距20,右锚点边距40,垂直边距-1

完成后,还要为loginButton添加MouseAreaText。为MouseArea启用fill parent item,为Text QML 类型启用verticalhorizontal anchors。然后,将其text属性设置为LOGIN

您不必完全按照我的步骤进行,它们只是指导您实现与上面截图类似的结果的指南。但是,最好您应用自己的设计并创建独特的东西!

哦!经过上面漫长的过程,我们的登录屏幕现在应该看起来像这样:

在转到main.qml之前,我们还需要做一件事,那就是公开我们登录屏幕中的一些 QML 类型,以便我们可以将其链接到我们的main.qml文件进行逻辑编程。实际上,我们可以直接在设计工具上做到这一点。您只需点击对象名称旁边的小矩形图标,并确保图标上的三条线穿过矩形框,就像这样:

我们需要公开/导出的 QML 类型是emailInput(TextInput)、emailTouch(MouseArea)、emailDisplay(Text)、passwordInput(TextInput)、passwordTouch(MouseArea)、passwordDisplay(Text)和loginMouseArea(MouseArea)。完成所有这些后,让我们打开main.qml

首先,我们的main.qml应该看起来像这样,它只会打开一个空窗口:

import QtQuick 2.6 
import QtQuick.Window 2.2 

Window { 
    id: window 
    visible: true 
    width: 800 
    height: 600 
    title: qsTr("My App") 
} 

之后,添加MainForm对象,并将其锚点设置为anchors.fill: parent。然后,当点击(或触摸,如果在触摸设备上运行)loginButton时,在控制台窗口上打印一行文本Login pressed

Window { 
    id: window 
    visible: true 
    width: 800 
    height: 600 
    title: qsTr("My App") 

    MainForm 
    { 
        anchors.fill: parent 

        loginMouseArea.onClicked: 
        { 
            console.log("Login pressed"); 
        } 
    } 
} 

之后,我们将编写MouseArea在电子邮件输入上被点击/触摸时的行为。由于我们手动创建自己的文本字段,而不是使用QtQuick.Controls模块提供的TextField QML 类型,我们必须手动隐藏和显示E-MailPassword文本显示,并在用户点击/触摸MouseArea时更改输入焦点。

我选择不使用TextField类型的原因是,我几乎无法自定义TextField的视觉呈现,那么为什么不创建自己的呢?手动为电子邮件输入设置焦点的代码如下:

emailTouch.onClicked: 
{ 
    emailDisplay.visible = false;      // Hide emailDisplay 
    emailInput.forceActiveFocus();     // Focus emailInput 
    Qt.inputMethod.show();       // Activate virtual keyboard 
} 

emailInput.onFocusChanged: 
{ 
    if (emailInput.focus == false && emailInput.text == "") 
    { 
        emailDisplay.visible = true;   // Show emailDisplay if 
        emailInput is empty when loses focus 
    } 
} 

之后,对密码字段执行相同操作:

passwordTouch.onClicked: 
{ 
    passwordDisplay.visible = false;   // Hide passwordDisplay 
    passwordInput.forceActiveFocus();  // Focus passwordInput 
    Qt.inputMethod.show();       // Activate virtual keyboard 
} 

passwordInput.onFocusChanged: 
{ 
    if (passwordInput.focus == false && passwordInput.text == "") 
    { 
        passwordDisplay.visible = true;      // Show passwordDisplay if  
        passwordInput is empty when loses focus 
    } 
} 

就是这样,我们完成了!现在您可以编译和运行程序。您应该会得到类似这样的结果:

如果您没有看到图片,并且收到错误消息说 Qt 无法打开图片,请返回到您的MainForm.ui.qml,并在源属性的前面添加前缀image/。这是因为 Qt Quick Designer 加载图片时没有前缀,而您的最终程序需要前缀。添加了前缀后,您可能会意识到在 Qt Quick Designer 中不再看到图片显示,但在最终程序中将正常工作。

我不确定这是一个错误还是他们有意这样做的。希望 Qt 的开发人员可以解决这个问题,这样我们就不必再做额外的步骤了。就是这样,希望您已经理解了 Qt Widgets 应用程序和 Qt Quick 应用程序之间的相似之处和不同之处。现在您可以从这两者中选择最适合您项目需求的选项了!

总结

在本章中,我们学习了 Qt Quick 是什么,以及如何使用 QML 语言创建程序。在接下来的章节中,我们将学习如何将我们的 Qt 项目轻松导出到不同的平台。让我们开始吧!

第十五章:跨平台开发

自从第一次发布以来,Qt 就以其跨平台能力而闻名。这也是创始人在决定创建这个框架时的主要目标之一,早在它被诺基亚和后来的Qt 公司接管之前。

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

  • 编译器

  • 构建设置

  • 部署到 PC 平台

  • 部署到移动平台

让我们开始吧。

了解编译器

在本章中,我们将学习从 Qt 项目生成可执行文件的过程。这个过程就是我们所谓的编译构建。用于此目的的工具称为编译器。在接下来的部分中,我们将学习编译器是什么,以及如何使用它为我们的 Qt 项目生成可执行文件。

什么是编译器?

当我们开发一个应用程序时,无论是使用 Qt 还是其他任何软件开发工具包,我们经常需要将项目编译成可执行文件,但实际上在我们编译项目时到底发生了什么呢?

编译器是一种软件,它将用高级编程语言编写的计算机代码或计算机指令转换为计算机可以读取和执行的机器代码或较低级别形式。这种低级机器代码在操作系统和计算机处理器上都有很大的不同,但你不必担心,因为编译器会为你转换它。

这意味着你只需要担心用人类可读的编程语言编写逻辑代码,让编译器为你完成工作。理论上,通过使用不同的编译器,你应该能够将代码编译成可在不同操作系统和硬件上运行的可执行程序。我在这里使用“理论上”这个词是因为实际上要比使用不同的编译器更困难,你可能还需要实现支持目标平台的库。然而,Qt 已经为你处理了所有这些,所以你不必做额外的工作。

在当前版本中,Qt 支持以下编译器:

  • GNU 编译器集合(GCC):GCC 是用于 Linux 和 macOS 的编译器

  • MinGW(Windows 的最小 GNU):MinGW 是 GCC 和 GNU Binutils(二进制工具)的本地软件端口,用于在 Windows 上开发应用程序

  • Microsoft Visual C++(MSVC):Qt 支持 MSVC 2013、2015 和 2017 用于构建 Windows 应用程序

  • XCode:XCode 是开发者为 macOS 和 iOS 开发应用程序时使用的主要编译器

  • Linux ICC(英特尔 C++编译器):Linux ICC 是英特尔为 Linux 应用程序开发开发的一组 C 和 C++编译器

  • Clang:Clang 是 LLVM 编译器的 C、C++、Objective C 和 Objective C++前端,适用于 Windows、Linux 和 macOS

  • Nim:Nim 是适用于 Windows、Linux 和 macOS 的 Nim 编译器

  • QCC:QCC 是用于在 QNX 操作系统上编译 C++应用程序的接口

使用 Make 进行构建自动化

在软件开发中,Make是一种构建自动化工具,它通过读取名为Makefiles的配置文件自动从源代码构建可执行程序和库,这些配置文件指定如何生成目标平台。简而言之,Make 程序生成构建配置文件,并使用它们告诉编译器在生成最终可执行程序之前要做什么。

Qt 支持两种类型的 Make 程序:

  • qmake:它是 Qt 团队开发的本地 Make 程序。它在 Qt Creator 上效果最好,我强烈建议在所有 Qt 项目中使用它。

  • CMake:另一方面,尽管这是一个非常强大的构建系统,但它并不像 qmake 那样专门为 Qt 项目做所有事情,比如:

  • 运行元对象编译器MOC

  • 告诉编译器在哪里查找 Qt 头文件

  • 告诉链接器在哪里查找 Qt 库

在 CMake 上手动执行上述步骤,以便成功编译 Qt 项目。只有在以下情况下才应使用 CMake:

  • 您正在处理一个非 Qt 项目,但希望使用 Qt Creator 编写代码

  • 您正在处理一个需要复杂配置的大型项目,而 qmake 无法处理

  • 您真的很喜欢使用 CMake,并且您确切地知道自己在做什么

在选择适合项目的正确工具时,Qt 真的非常灵活。它不仅限于自己的构建系统和编译器。它给开发人员自由选择最适合其项目的工具。

构建设置

在项目编译或构建之前,编译器需要在继续之前了解一些细节。这些细节被称为构建设置,是编译过程中非常重要的一个方面。在接下来的部分中,我们将学习构建设置是什么,以及如何以准确的方式配置它们。

Qt 项目(.pro)文件

我相信您已经了解Qt 项目文件,因为我们在整本书中已经提到了无数次。.pro文件实际上是qmake用来构建应用程序、库或插件的项目文件。它包含了所有信息,例如链接到头文件和源文件,项目所需的库,不同平台/环境的自定义构建过程等。一个简单的项目文件可能如下所示:

QT += core gui widgets 

TARGET = MyApp 
TEMPLATE = app 

SOURCES +=  
        main.cpp  
        mainwindow.cpp 

HEADERS +=  
        mainwindow.h 

FORMS +=  
        mainwindow.ui 

RESOURCES +=  
    resource.qrc 

它只是告诉 qmake 应该在项目中包含哪些 Qt 模块,可执行程序的名称是什么,应用程序的类型是什么,最后是需要包含在项目中的头文件、源文件、表单声明文件和资源文件的链接。所有这些信息对于 qmake 生成配置文件并成功构建应用程序至关重要。对于更复杂的项目,您可能希望为不同的操作系统不同地配置项目。在 Qt 项目文件中也可以轻松实现这一点。

要了解如何为不同的操作系统配置项目,请参阅以下链接:doc.qt.io/qt-5/qmake-language.html#scopes-and-conditions.

评论

您可以在项目文件中添加自己的注释,以提醒自己添加特定配置行的目的,这样您在一段时间不接触后就不会忘记为什么添加了一行。注释以井号(#)开头,之后您可以写任何内容,因为构建系统将简单地忽略整行文本。例如:

# The following define makes your compiler emit warnings if you use 
# any feature of Qt which has been marked as deprecated (the exact warnings 
# depend on your compiler). Please consult the documentation of the 
# deprecated API in order to know how to port your code away from it. 
DEFINES += QT_DEPRECATED_WARNINGS 

您还可以添加虚线或使用空格使您的评论脱颖而出:

#------------------------------------------------- 
# 
# Project created by QtCreator 2018-02-18T01:59:44 
# 
#------------------------------------------------- 

模块、配置和定义

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

QT += core gui sql printsupport charts multimedia 

或者您还可以在前面添加条件来确定何时向项目添加特定模块:

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 

您还可以向项目添加配置设置。例如,我们希望明确要求编译器在编译我们的项目时遵循 C++规范的 2011 版本(称为 C++11),并使其成为多线程应用程序:

CONFIG += qt c++11 thread

您必须使用+=,而不是=,否则 qmake 将无法使用 Qt 的配置来确定项目所需的设置。或者,您也可以使用-=来从项目中删除模块、配置和定义。

至于向编译器添加定义(或变量),我们使用DEFINES关键字,如下所示:

DEFINES += QT_DEPRECATED_WARNINGS 

在编译项目之前,qmake 将此变量的值作为编译器 C 预处理宏(-D选项)添加到项目中。前面的定义告诉 Qt 编译器,如果您使用了已标记为弃用的 Qt 功能,则会发出警告。

特定于平台的设置

您可以为不同的平台设置不同的配置或设置,因为并非每个设置都适用于所有用例。例如,如果我们想为不同的操作系统包含不同的头文件路径,可以执行以下操作:

win32:INCLUDEPATH += "C:/mylibs/extra headers" 
unix:INCLUDEPATH += "/home/user/extra headers" 

或者,您还可以将设置放在花括号中,这类似于编程语言中的if语句:

win32 { 
    SOURCES += extra_code.cpp 
} 

您可以通过访问以下链接查看项目文件中可以使用的所有设置:doc.qt.io/qt-5/qmake-variable-reference.html.

部署到 PC 平台

让我们继续学习如何在 Windows、Linux 和 macOS 等平台上部署我们的应用程序。

Windows

在本节中,我们将学习如何将我们的应用程序部署到不同的操作系统。尽管 Qt 默认支持所有主要平台,但可能需要设置一些配置,以便使您的应用程序能够轻松部署到所有平台。

我们将要介绍的第一个操作系统是最常见的Microsoft Windows

从 Qt 5.6 开始,Qt 不再支持Windows XP

在您尝试部署的 Windows 版本上可能有某些插件无法正常工作,因此在决定处理项目之前,请查看文档。但可以肯定的是,大多数功能在 Qt 上都可以直接使用。

默认情况下,当您将 Qt 安装到 Windows PC 时,MinGW 32 位编译器会一起安装。不幸的是,除非您从源代码编译 Qt,否则默认情况下不支持 64 位。如果您需要构建 64 位应用程序,可以考虑在Microsoft Visual Studio旁边安装 MSVC 版本的 Qt。可以从以下链接免费获取 Microsoft Visual Studio:www.visualstudio.com/vs

您可以通过转到 Tools | Options,然后转到 Build & Run 类别并选择 Kits 选项卡,在 Qt Creator 中设置编译器设置:

如您所见,有多个运行在不同编译器上的工具包,您可以进行配置。默认情况下,Qt 已经配备了五个工具包——一个用于 Android,一个用于 MinGW,三个用于 MSVC(版本 2013、2015 和 2017)。Qt 将自动检测这些编译器的存在,并相应地为您配置这些设置。

如果您尚未安装Visual StudioAndroid SDK,则在工具包选项前会出现带有感叹号的红色图标。安装所需的编译器后,请尝试重新启动 Qt Creator。它现在将检测到新安装的编译器。您应该可以毫无问题地为 Windows 平台编译 Qt 将为您处理其余部分。我们将在另一节中更多地讨论 Android 平台。

编译应用程序后,打开安装 Qt 的文件夹。将相关的 DLL 文件复制到应用程序文件夹中,并在分发给用户之前将其打包在一起。没有这些 DLL 文件,用户可能无法运行 Qt 应用程序。

有关更多信息,请访问以下链接:doc.qt.io/qt-5/windows-deployment.html.

要为应用程序设置自定义图标,必须将以下代码添加到项目(.pro)文件中:

win32:RC_ICONS = myappico.ico 

前面的代码仅适用于 Windows 平台,因此我们必须在其前面添加win32关键字。

Linux

Linux(或 GNU/Linux)通常被认为是主导云/服务器市场的主要操作系统。由于 Linux 不是单一操作系统(Linux 以不完全兼容的不同 Linux 发行版的形式由不同供应商提供),就像 Windows 或 macOS 一样,开发人员很难构建他们的应用程序并期望它们在不同的 Linux 发行版(distros)上无缝运行。但是,如果您在 Qt 上开发 Linux 应用程序,只要目标系统上存在 Qt 库,它就有很高的机会在大多数发行版上运行,如果不是所有主要发行版。

在 Linux 上的默认套件选择比 Windows 简单得多。由于 64 位应用程序已经成为大多数 Linux 发行版的主流和标准已经有一段时间了,我们在安装 Qt 时只需要包括GCC 64 位编译器。还有一个 Android 选项,但我们稍后会详细讨论:

如果您是第一次在 Qt Creator 上编译 Linux 应用程序,我相当肯定您会收到以下错误:

这是因为您尚未安装构建 Linux 应用程序所需的相关工具,例如 Make、GCC 和其他程序。

不同的 Linux 发行版安装程序的方法略有不同,但我不会在这里解释每一个。在我的情况下,我使用的是 Ubuntu 发行版,所以我首先打开终端并键入以下命令来安装包含 Make 和 GCC 的build-essential软件包:

sudo apt-get install build-essential 

前面的命令仅适用于继承自DebianUbuntu的发行版,可能不适用于其他发行版,如FedoraGentooSlackware等。您应该搜索您的 Linux 发行版使用的适当命令来安装这些软件包,如下图所示:

一旦安装了适当的软件包,请重新启动 Qt Creator 并转到工具|选项。然后,转到“构建和运行”类别,打开“套件”选项卡。现在,您应该能够为您的桌面套件选择 C 和 C ++选项的编译器:

但是,当您再次尝试编译时,可能会遇到另一个错误,即找不到-lGL:

这是因为 Qt 试图寻找OpenGL库,但在您的系统上找不到它们。通过使用以下命令安装Mesa 开发库软件包,可以轻松解决这个问题:

sudo apt-get install libgl1-mesa-dev 

同样,前面的命令仅适用于 Debian 和 Ubuntu 变体。如果您没有运行 Debian 或 Ubuntu 分支之一,请寻找适合您的 Linux 发行版的命令:

安装了软件包后,您应该能够编译和运行 Qt 应用程序而无任何问题:

至于使用其他不太流行的编译器,如Linux ICCNimQCC,您必须通过单击位于 Kits 界面右侧的“添加”按钮来手动设置,然后输入所有适当的设置以使其正常工作。大多数人不使用这些编译器,所以我们暂时跳过它们。

在分发 Linux 应用程序时,比 Windows 或 macOS 要复杂得多。这是因为 Linux 不是单一操作系统,而是一堆具有自己依赖项和配置的不同发行版,这使得分发程序非常困难。

最安全的方法是静态编译程序,这有其优缺点。您的程序将变得非常庞大,这使得对于互联网连接速度较慢的用户来说,更新软件将成为一个巨大的负担。除此之外,如果您不是在进行开源项目并且没有 Qt 商业许可证,Qt 许可证也禁止您进行静态构建。要了解有关 Qt 许可选项的更多信息,请访问以下链接:www1.qt.io/licensing-comparison

另一种方法是要求用户在运行应用程序之前安装正确版本的 Qt,但这将在用户端产生大量问题,因为并非每个用户都非常精通技术,并且有耐心去避免依赖地狱。

因此,最好的方法是将 Qt 库与应用程序一起分发,就像我们在 Windows 平台上所做的那样。该库可能在某些 Linux 发行版上无法工作(很少见,但有一点可能性),但可以通过为不同的发行版创建不同的安装程序来轻松克服这个问题,现在每个人都很满意。

然而,出于安全原因,Linux 应用程序通常不会默认在其本地目录中查找其依赖项。您必须在您的 qmake 项目(.pro)文件中使用可执行文件的rpath设置中的$ORIGIN关键字:

unix:!mac{ 
QMAKE_LFLAGS += -Wl,--rpath=$$ORIGIN 
QMAKE_RPATH= 
} 

设置QMAKE_RPATH会清除 Qt 库的默认rpath设置。这允许将 Qt 库与应用程序捆绑在一起。如果要将rpath包括在 Qt 库的路径中,就不要设置QMAKE_RPATH

之后,只需将 Qt 安装文件夹中的所有库文件复制到应用程序的文件夹中,并从文件名中删除其次版本号。例如,将libQtCore.so.5.8.1重命名为libQtCore.so.5,现在应该能够被您的 Linux 应用程序检测到。

至于应用程序图标,默认情况下无法为 Linux 应用程序应用任何图标,因为不受支持。尽管某些桌面环境(如 KDE 和 GNOME)支持应用程序图标,但必须手动安装和配置图标,这对用户来说并不是很方便。它甚至可能在某些用户的 PC 上无法工作,因为每个发行版的工作方式都有些不同。为应用程序设置图标的最佳方法是在安装过程中创建桌面快捷方式(符号链接)并将图标应用于快捷方式。

macOS

在我看来,macOS是软件世界中最集中的操作系统。它不仅设计为仅在 Macintosh 机器上运行,您还需要从 Apple 应用商店下载或购买软件。

毫无疑问,这对一些关心选择自由的人造成了不安,但另一方面,这也意味着开发人员在构建和分发应用程序时遇到的问题更少。

除此之外,macOS 应用程序的行为与 ZIP 存档非常相似,每个应用程序都有自己的目录,其中包含适当的库。因此,用户无需预先在其操作系统上安装 Qt 库,一切都可以直接使用。

至于 Kit Selection,Qt for macOS 支持 Android、clang 64 位、iOS 和 iOS 模拟器的工具包:

从 Qt 5.10 及更高版本开始,Qt 不再支持 macOS 的 32 位构建。此外,Qt 不支持 PowerPC 上的 OS X;由于 Qt 在内部使用 Cocoa,因此也不可能构建 Carbon,请注意这一点。

在编译您的 macOS 应用程序之前,请先从 App Store 安装 Xcode。Xcode 是 macOS 的集成开发环境,包含了由苹果开发的一套用于开发 macOS 和 iOS 软件的软件开发工具。一旦安装了 Xcode,Qt Creator 将检测到其存在,并自动为您设置编译器设置,这非常棒:

编译项目后,生成的可执行程序是一个单个的应用程序包,可以轻松地分发给用户。由于所有库文件都打包在应用程序包中,因此它应该可以在用户的 PC 上直接运行。

为 Mac 设置应用程序图标是一项非常简单的任务。只需将以下代码添加到您的项目(.pro)文件中,我们就可以开始了:

ICON = myapp.icns 

请注意,图标格式为.icns,而不是我们通常用于 Windows 的.ico

在移动平台上部署

除了 Windows、Linux 和 macOS 等平台外,移动平台同样重要。许多开发人员希望将他们的应用程序部署到移动平台。让我们看看如何做到这一点。我们将涵盖两个主要平台,即 iOS 和 Android。

iOS

在 iOS 上部署 Qt 应用程序非常简单。就像我们之前为 macOS 所做的那样,您需要首先在开发 PC 上安装 Xcode:

然后,重新启动 Qt Creator。它现在应该能够检测到 Xcode 的存在,并且会自动为您设置编译器设置:

之后,只需将 iPhone 连接并点击运行按钮!

在 Qt 上构建 iOS 应用程序确实很容易。然而,分发它们并不容易。这是因为 iOS 就像一个有围墙的花园一样,是一个非常封闭的生态系统。您不仅需要在 Apple 注册为应用程序开发人员,还需要在能够将其分发给用户之前对 iOS 应用程序进行代码签名。如果您想为 iOS 构建应用程序,您无法避开这些步骤。

您可以通过访问以下链接了解更多信息:developer.apple.com/app-store/submissions.

Android

尽管 Android 是基于 Linux 的操作系统,但与您在 PC 上运行的 Linux 平台相比,它非常不同。要在 Qt 上构建 Android 应用程序,无论您是在 Windows、Linux 还是 macOS 上运行,都必须先将Android SDKAndroid NDKApache ANT安装到开发 PC 上:

这三个软件包在构建 Qt 上的 Android 应用程序时至关重要。一旦它们都安装好了,重新启动 Qt Creator,它应该已经检测到它们的存在,并且构建设置现在应该已经自动设置好了:

最后,您可以通过使用 Qt Creator 打开AndroidManifect.xml文件来配置您的 Android 应用程序:

您可以在这里设置一切,如包名称、版本代码、SDK 版本、应用程序图标、权限等。

与 iOS 相比,Android 是一个开放的系统,因此在将应用程序分发给用户之前,您无需做任何事情。但是,如果您希望在 Google Play 商店上分发您的应用程序,可以选择注册为 Google Play 开发人员。

总结

在本章中,我们已经学习了如何为不同平台(如 Windows、Linux、macOS、Android 和 iOS)编译和分发我们的 Qt 应用程序。在下一章中,我们将学习不同的调试方法,这可以节省开发时间。让我们来看看吧!

第十六章:测试和调试

在阅读与编程相关的教程或文章时,我们经常看到调试这个词。但是您知道调试是什么意思吗?在编程术语中,bug表示计算机程序中的错误或缺陷,导致软件无法正常运行,通常会导致不正确的输出甚至崩溃。

在本章中,我们将涵盖以下主题,并学习如何调试我们的 Qt 项目:

  • 调试技术

  • Qt 支持的调试器

  • 单元测试

让我们开始吧。

调试技术

在开发过程中经常会出现技术问题。为了解决这些问题,我们需要在将应用程序发布给用户之前找出所有这些问题并解决它们,以免影响公司/团队的声誉。用于查找技术问题的方法称为调试。在本节中,我们将介绍专业人士常用的常见调试技术,以确保他们的程序可靠且质量高。

识别问题

在调试程序时,无论编程语言或平台如何,最重要的是知道代码的哪一部分导致了问题。您可以通过几种方式来识别问题代码:

  • 询问用户出现错误的位置;例如,按下了哪个按钮,导致崩溃的步骤是什么,等等。

  • 注释掉代码的一部分,然后重新构建和运行程序,以检查问题是否仍然存在。如果问题仍然存在,继续注释更多的代码,直到找到问题所在的代码行。

  • 使用内置调试器通过设置数据断点来检查目标函数中的变量更改。您可以轻松地发现您的变量是否已更改为意外值,或者对象指针是否已变为未定义指针。

  • 确保您为用户安装程序中包含的所有库与项目中使用的库具有匹配的版本号。

使用 QDebug 打印变量

您还可以使用QDebug类将变量的值打印到应用程序输出窗口。QDebug与标准库中的std::cout非常相似,但使用QDebug的优势在于,由于它是 Qt 的一部分,它支持 Qt 类,而且能够在不需要任何转换的情况下输出其值。

要启用QDebug,我们必须首先包含其头文件:

#include <QDebug> 

之后,我们可以调用qDebug()将变量打印到应用程序输出窗口:

int amount = 100; 
qDebug() << "You have obtained" << amount << "apples!"; 

结果将如下所示:

通过使用QDebug,我们将能够检查我们的函数是否正常运行。在检查完问题后,您可以注释掉包含qDebug()的特定代码行。

设置断点

设置断点是调试程序的另一种好方法。当您在 Qt Creator 中右键单击脚本的行号时,将会弹出一个包含三个选项的菜单,您可以在下面的截图中看到:

第一个选项称为在行处设置断点...,允许您在脚本的特定行上设置断点。一旦创建了断点,该行号旁边将出现一个红色圆点图标:

第二个选项称为在行处设置消息跟踪点...,当程序到达特定代码行时打印消息。一旦创建了断点,该行号旁边将出现一个眼睛图标:

第三个选项是切换书签,允许您为自己设置书签。让我们创建一个名为test()的函数来尝试断点:

void MainWindow::test() 
{ 
   int amount = 100; 
   amount -= 10; 
   qDebug() << "You have obtained" << amount << "apples!"; 
} 

之后,我们在MainWindow构造函数中调用test()函数:

MainWindow::MainWindow(QWidget *parent) : 
   QMainWindow(parent), 
   ui(new Ui::MainWindow) 
{ 
   ui->setupUi(this); 
   test(); 
} 

然后,按下位于 Qt Creator 窗口左下角的开始调试按钮:

您可能会收到类似于这样的错误消息:

在这种情况下,请确保您的项目工具包已连接到调试器。如果仍然出现此错误,请关闭 Qt Creator,转到您的项目文件夹并删除.pro.user文件。然后,用 Qt Creator 打开您的项目。Qt Creator 将重新配置您的项目,并且调试模式现在应该可以工作了。

让我们给我们的代码添加两个断点并运行它。一旦我们的程序启动,我们将看到一个黄色箭头出现在第一个红点的顶部:

这意味着调试器已经停在了第一个断点处。现在,位于 Qt Creator 右侧的本地和表达式窗口将显示变量及其值和类型:

在上图中,您可以看到值仍然为 100,因为此时减法操作尚未运行。接下来,我们需要做的是单击位于 Qt Creator 底部的堆栈窗口顶部的“步入”按钮:

之后,调试器将移动到下一个断点,这里我们可以看到值已经减少到了 90,正如预期的那样:

您可以使用这种方法轻松检查您的应用程序。要删除断点,只需再次单击红点图标。

请注意,您必须在调试模式下运行此操作。这是因为在调试模式下编译时,将额外的调试符号嵌入到您的应用程序或库中,使您的调试器能够访问来自二进制源代码的信息,例如标识符、变量和例程的名称。这也是为什么在调试模式下编译的应用程序或库的文件大小会更大的原因。

Qt 支持的调试器

Qt 支持不同类型的调试器。根据您的项目运行的平台和编译器,使用的调试器也会有所不同。以下是 Qt 通常支持的调试器列表:

  • Windows (MinGW): GDB (GNU 调试器)

  • Windows (MSVC): CDB (Windows 调试工具)

  • macOS: LLDB (LLVM 调试器), FSF GDB (实验性)

  • Linux: GDB, LLDB (实验性)

  • Unix (FreeBSD, OpenBSD, 等): GDB

  • Android: GDB

  • iOS: LLDB

PC 的调试

对于GDB (GNU 调试器),如果您在 Windows 上使用 MinGW 编译器,则无需进行任何手动设置,因为它通常与您的 Qt 安装一起提供。如果您运行其他操作系统,如 Linux,则可能需要在将其与 Qt Creator 链接之前手动安装它。Qt Creator 会自动检测 GDB 的存在并将其与您的项目链接起来。如果没有,您可以轻松地在 Qt 目录中找到 GDB 可执行文件并自行链接。

另一方面,需要在 Windows 机器上手动安装CDB (Windows 调试工具)。请注意,Qt 不支持 Visual Studio 的内置调试器。因此,您需要通过在安装 Windows SDK 时选择一个名为“调试工具”的可选组件来单独安装 CDB 调试器。Qt Creator 通常会识别 CDB 的存在,并将其放在调试器选项页面下的调试器列表中。您可以转到“工具”|“选项”|“构建和运行”|“调试器”查找设置,如下面的屏幕截图所示:

针对 Android 设备的调试

针对 Android 设备的调试比 PC 稍微复杂一些。您必须安装所有必要的 Android 开发包,如 JDK(6 或更高版本)、Android SDK 和 Android NDK。然后,您还需要在 Windows 平台上安装 Android 调试桥(ADB)驱动程序,以启用 USB 调试,因为 Windows 上的默认 USB 驱动程序不允许调试。

macOS 和 iOS 的调试

至于 macOS 和 iOS,使用的调试器是LLDB(LLVM 调试器),它默认随 Xcode 一起提供。Qt Creator 也会自动识别其存在并将其与您的项目链接起来。

每个调试器都与另一个略有不同,并且在 Qt Creator 上可能表现不同。如果您熟悉这些工具并知道自己在做什么,还可以在其各自的 IDE(Visual Studio、XCode 等)上运行非 GDB 调试器。

如果您需要向项目添加其他调试器,可以转到“工具”|“选项”|“构建和运行”|“工具包”,然后单击“克隆”以复制现有工具包。然后,在“调试器”选项卡下,单击“添加”按钮以添加新的调试器选择:

在“名称”字段中,输入调试器的描述性名称,以便您可以轻松记住其目的。然后,在“路径”字段中指定调试器二进制文件的路径,以便 Qt Creator 知道在启动调试过程时要运行哪个可执行文件。除此之外,“类型”和“版本”字段由 Qt Creator 用于识别调试器的类型和版本。此外,Qt Creator 还在“ABIs”字段中显示将在嵌入式设备上使用的 ABI 版本。

要了解如何在 Qt 中设置不同调试器的详细信息,请访问以下链接:

doc.qt.io/qtcreator/creator-debugger-engines.html.

单元测试

单元测试是一个自动化的过程,用于测试应用程序中的单个模块、类或方法。单元测试可以在开发周期的早期发现问题。这包括程序员实现中的错误和单元规范中的缺陷或缺失部分。

Qt 中的单元测试

Qt 带有一个内置的单元测试模块,我们可以通过在项目文件(.pro)中添加testlib关键字来使用它:

QT += core gui testlib 

之后,将以下标题添加到我们的源代码中:

#include <QtTest/QtTest> 

然后,我们可以开始测试我们的代码。我们必须将测试函数声明为私有槽。除此之外,该类还必须继承自QOBject类。例如,我创建了两个文本函数,分别称为testString()testGui(),如下所示:

private slots: 
   void testString(); 
   void testGui(); 

函数定义看起来像这样:

void MainWindow::testString() 
{ 
   QString text = "Testing"; 
   QVERIFY(text.toUpper() == "TESTING"); 
} 

void MainWindow::testGui() 
{ 
   QTest::keyClicks(ui->lineEdit, "testing gui"); 
   QCOMPARE(ui->lineEdit->text(), QString("testing gui")); 
} 

我们使用QTest类提供的一些宏,如QVERIFYQCOMPARE等,来评估作为其参数传递的表达式。如果表达式求值为true,则测试函数的执行将继续。否则,将向测试日志附加描述失败的消息,并且测试函数停止执行。

我们还使用了QTest::keyClicks()来模拟鼠标在我们的应用程序中的点击。在前面的示例中,我们模拟了在主窗口小部件上的行编辑小部件上的点击。然后,我们输入一行文本到行编辑中,并使用QCOMPARE宏来测试文本是否已正确插入到行编辑小部件中。如果出现任何问题,Qt 将在应用程序输出窗口中显示问题。

之后,注释掉我们的main()函数,而是使用QTEST_MAIN()函数来开始测试我们的MainWindow类:

/*int main(int argc, char *argv[]) 
{ 
   QApplication a(argc, argv); 
   MainWindow w; 
   w.show(); 

   return a.exec(); 
}*/ 
QTEST_MAIN(MainWindow) 

如果我们现在构建和运行我们的项目,我们应该会得到类似以下的结果:

********* Start testing of MainWindow ********* 
Config: Using QtTest library 5.9.1, Qt 5.9.1 (i386-little_endian-ilp32 shared (dynamic) debug build; by GCC 5.3.0) 
PASS   : MainWindow::initTestCase() 
PASS   : MainWindow::_q_showIfNotHidden() 
PASS   : MainWindow::testString() 
PASS   : MainWindow::testGui() 
PASS   : MainWindow::cleanupTestCase() 
Totals: 5 passed, 0 failed, 0 skipped, 0 blacklisted, 880ms 
********* Finished testing of MainWindow ********* 

还有许多宏可以用来测试应用程序。

有关更多信息,请访问以下链接:

doc.qt.io/qt-5/qtest.html#macros

总结

在这一章中,我们学习了如何使用多种调试技术来识别 Qt 项目中的技术问题。除此之外,我们还了解了 Qt 在不同操作系统上支持的不同调试器。最后,我们还学会了如何通过单元测试自动化一些调试步骤。

就是这样!我们已经到达了本书的结尾。希望你在学习如何使用 Qt 从头开始构建自己的应用程序时找到了这本书的用处。你可以在 GitHub 上找到所有的源代码。祝你一切顺利!

posted @ 2024-05-04 22:43  绝不原创的飞龙  阅读(165)  评论(0编辑  收藏  举报