QT5-移动和嵌入式开发实用指南-全-
QT5 移动和嵌入式开发实用指南(全)
原文:
zh.annas-archive.org/md5/af60e363dff13d7591431ef9db482003译者:飞龙
前言
Qt 现在无处不在。从您典型的家用电脑到云服务器、移动电话、机器自动化、咖啡机、医疗设备和各种嵌入式设备,甚至在一些最豪华的汽车中,甚至在太空中某个地方!现在的手表也在运行 Qt。
物联网(IoT)和家庭自动化是热门词汇,Qt 也是其中的一部分。正如我经常说的,没有传感器就没有物联网!在写关于 Qt 开发的书时,我会提到传感器吗?不会。因此,我们还将深入研究 Qt 传感器。
当前的目标设备并不缺乏,这是肯定的。但在这本书中,我们将专门针对移动电话平台和树莓派来演示 Qt 的一些嵌入式特性。因为 Qt 是跨平台的,所以你仍然可以学到如何针对咖啡机进行开发!
需要考虑的一个因素是,将要使用哪个 UI 框架。对于移动设备来说,使用 OpenGL 是一个可行的选项,特别是如果存在硬件支持的话。我故意跳过了 OpenGL 的讨论,因为那相当复杂,几乎可以写成一本完整的书。然而,这是一个选项,并且可以通过使用 Qt 框架来获得。
本书面向的对象
这本书的目标读者是那些对使用 Qt 在移动和嵌入式设备上开发跨平台应用程序感兴趣的开发者。读者应具备 C++ 的先验知识,并且熟悉从命令行界面运行命令。
本书涵盖的内容
第一章,标准 Qt 小部件,涵盖了标准 UI 元素和动态布局,以教读者如何处理方向变化。
第二章,使用 Qt Quick 的流体 UI,概述了标准 QML 元素、图表、数据可视化和动画。
第三章,图形和特殊效果,探讨了 QML 粒子和图形效果。
第四章,输入和触摸,教读者如何创建和使用虚拟键盘,以及触摸和语音输入。
第五章,Qt 网络通信,向读者介绍了网络操作、套接字和会话。
第六章,使用 Qt Bluetooth LE 进行连接,介绍了蓝牙 LE 设备发现、设置服务和操作特性。
第七章,机器对话,讨论了传感器、WebSockets 和 MQTT 自动化。
第八章,我在哪里?定位和定位,探讨了 GPS 定位、定位和地图。
第九章,声音与视觉 - Qt 多媒体,涵盖了 3D 音频、FM 收音机和视频的录制与播放。
第十章,使用 Qt SQL 的远程数据库,概述了远程使用 SQLite 和 MySQL 数据库。
第十一章,使用 Qt 购买功能启用应用内购买,讨论了如何将应用内购买添加到您的应用中。
第十二章,交叉编译和远程调试,探讨了交叉编译,以及连接到远程设备并进行调试。
第十三章,部署到移动和嵌入式系统,探讨了设置应用程序和完成应用商店流程。
第十四章,适用于移动和嵌入式设备的通用平台,探讨了在网页浏览器中运行 Qt 应用程序。
第十五章,构建 Linux 系统,涵盖了设置和构建完整的 Linux 嵌入式系统。
为了充分利用这本书
本书假设您已经使用过 C++,熟悉 QML,可以使用 Git 下载源代码,并且可以在命令行界面中输入命令。您还应该习惯使用 GDB 调试器。
它还假设您拥有一个移动或嵌入式设备,例如手机或树莓派。
下载示例代码文件
您可以从 www.packt.com 的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packt.com/support 并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在 www.packt.com 登录或注册。
-
选择 SUPPORT 选项卡。
-
点击 Code Downloads & Errata。
-
在搜索框中输入本书的名称,并遵循屏幕上的说明。
文件下载完成后,请确保使用最新版本的软件解压缩或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Hands-On-Mobile-and-Embedded-Development-with-Qt-5/tree/master。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包可供选择,请访问 github.com/PacktPublishing/。查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789614817_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText: 表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“在 iOS 上,你需要编辑 plist.info 文件。”
代码块设置如下:
if (!QTouchScreen::devices().isEmpty()) {
qApp->setStyleSheet("QButton {padding: 10px;}");
}
粗体: 表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的文字会以这种方式显示。以下是一个示例:“在 Qt Creator 的左侧选择“项目”图标,然后选择你想要的靶平台,例如 iOS 的 Qt 5.12.0”。
警告或重要提示会以这种方式显示。
小贴士和技巧会以这种方式显示。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过 customercare@packtpub.com 发送邮件给我们。
勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将非常感谢。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果您在互联网上发现我们作品的任何非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将非常感谢。请通过 copyright@packt.com 联系我们,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为什么不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需更多关于 Packt 的信息,请访问 packt.com。
第一部分:制作出色的 UI
Qt 为开发者提供了多种创建外观出色的应用程序的方法,从更标准的 Qt Widgets 到 Qt Quick,它拥有无限创意的流畅动画和闪耀效果,再到 Qt OpenGL,它将沉浸式游戏带入 Qt 移动世界。在本节中,读者将学习如何使用 Qt 的各种 UI 框架为移动和嵌入式设备创建动态且灵活的用户界面,针对的平台包括 iOS、Android 移动平台以及树莓派。
本节包括以下章节:
-
第一章,标准 Qt Widgets
-
第二章,使用 Qt Quick 的流畅 UI
-
第三章,图形和特效
-
第四章,输入和触摸
第一章:标准 Qt Widgets
Qt Widgets 不是新出现的,但它们在针对移动和嵌入式设备的应用程序中仍然有其位置。它们结构良好、可预测,并且具有标准 UI 元素。
可识别的 UI 元素可以在 Qt Widgets 中找到,并且在笔记本电脑上运行得很好,笔记本电脑只是移动的桌面。在本章中,你将学习如何设计看起来标准的应用程序。基本控件,如菜单、图标和列表,将进行讨论,重点是如何将用户界面限制在中型和小型显示屏上。我们将讨论的主题包括如何使用 Qt 的动态布局来处理方向变化。将使用 QGraphicsScene、QGraphicsView 和 QGraphicsItem 等类。还将讨论 QVBoxLayout、QGridLayout 和 QStackedLayout 等布局 API。
在本章中,我们将涵盖:
-
使用 Qt Creator 和 Qt Widgets 创建移动应用程序并在设备上运行
-
桌面应用程序和移动应用程序之间的差异,包括屏幕尺寸、内存、手势
-
使用 Qt Widgets 在动态布局中实现屏幕尺寸和方向变化的便捷性
-
使用
QGraphicsView进行图形应用程序开发
嗨,移动世界!
你想使用 Qt 开发移动和嵌入式设备的应用程序,这是一个非常好的选择,因为 Qt 是为了跨平台开发而设计的。为了帮助你入门,我们将简要介绍使用 Qt Creator 创建、构建和运行应用程序的基本步骤。我们将简要探讨在创建移动和嵌入式应用程序时需要考虑的不同方面,例如如何使用 Qt Creator 添加菜单。在设计师中添加 QWidget 并不困难,我将向你展示如何操作。
Qt 在移动设备上运行有着悠久的历史,始于 2000 年首次发布的 Qt Embedded。Qt Embedded 是 UI Qtopia 的基础框架,最初在 Sharp Zaurus 的 SL-5000D 开发版上发布。
现在,你可以使用 Qt 开发应用程序,并在 iOS App Store、Android Google Play 商店或其他 Linux 移动手机上销售。Qt 应用程序可以在电视上运行,你甚至可以看到它们在汽车和飞机的娱乐系统中运行。它还可以在医疗设备和工厂地面的工业自动化机器上运行。
在移动和嵌入式设备上使用 Qt 时需要考虑一些因素,例如内存限制和显示尺寸限制。手机有触摸屏,而嵌入式设备可能根本就没有屏幕。
当你安装 Qt 时,你可以使用 Qt Creator IDE 来编辑、构建和运行你的代码。它是免费和开源的,因此你可以对其进行自定义。我曾经有一个补丁,它以允许我打印出 Qt Creator 所使用的所有键盘命令的方式自定义了 Qt Creator,这样我就可以有一个快速参考表。让我们快速了解一下 Qt Creator,它曾经被称为 Workbench。
Qt Creator
我们不会深入探讨 Qt Creator 的细节,但我认为应该提一下,以展示我们如何使用它来开发一个跨平台的基于 QWidget 的应用程序,该应用程序可以在桌面和移动平台上运行。我们将讨论两者之间的差异。然后,我们将演示如何使用动态布局来帮助您针对多种不同的屏幕尺寸并处理设备方向变化。您可能已经熟悉 Qt Creator,所以我们将刷新您的记忆。
基本 Qt Creator 步骤
在设置好环境后,跨编译和构建在移动设备上运行的应用程序的基本步骤是直接的。我们理论上会遵循以下步骤:
-
文件 | 新建文件或项目... | Qt Widgets 应用程序,点击选择...按钮
-
编写一些令人惊叹的代码
-
在 Qt Creator 左侧选择“项目”图标,然后选择你想要的目标平台,例如为 iOS 选择 Qt 5.12.0。
-
按 Ctrl + B,或 Command + B 构建
-
按 Ctrl + R,或 Command + R 运行
-
按 F5,或 Command + Y 调试
对于本章,我们将使用 Qt Widgets,这些是更接近传统桌面计算机应用程序的 UI 元素。它们对移动和嵌入式设备仍然很有用。
Qt Designer
Qt Creator 随附一个名为 Qt Designer 的设计工具。当您创建一个新的模板应用程序时,您将在左侧看到一个文件列表。当您点击任何 .ui 文件时,它将在 Qt Designer 中打开您的应用程序表单。
源代码可以在 Git 仓库的 Chapter01-a 目录下的 cp1 分支中找到。
导航到表单 | mainwindow.ui 并双击。这将打开 UI 文件在 Qt Creator 的 Designer 中。UI 文件只是一个 XML 格式的文本文件,如果您选择,可以直接编辑该文件。以下图片显示了在 Qt Designer 中打开时的样子:

让我们从几乎每个桌面应用程序都有的菜单开始。您的移动或嵌入式应用程序可能也需要一个菜单。如您所见,Qt 应用程序向导已经为我们生成了一个模板菜单。我们需要对其进行自定义以使其可用。我们可以添加一些子菜单项来演示基本的 Qt Creator 功能。
添加 QMenu
点击应用程序表单中标记为菜单的位置,添加菜单项。输入类似 Item1 的内容,按 Enter。添加另一个菜单项,如下图所示:

如果您现在构建它,您将得到一个带有菜单的空应用程序,所以让我们添加更多内容来演示如何从 Qt Creator 左侧的控件列表中添加控件。
添加 QListView
我们的 UI 表单需要一些内容。我们将为桌面构建和运行它,然后为移动模拟器构建和运行它,以比较两者。这里的步骤很简单,就像拖放一样。
在 Qt Creator 的左侧是一个 Widget、布局和间隔的列表,你可以简单地拖放它们到模板表单上,创建你的杰作 Qt 应用程序。让我们开始吧:
- 拖动 ListView 并将其放置在表单上。

- 选择桌面工具包,然后通过点击“运行”按钮来构建和运行它。如果你对表单或源代码进行了任何更改,Qt Creator 可以在同一步骤中构建和运行你的应用程序。当你运行它时,应用程序应该看起来类似于以下图片:

这一切看起来都很完美,但它并没有在像手机这样的小设备上运行。
Qt Creator 自带 iOS 和 Android 模拟器,你可以使用它们来查看你的应用程序在小型屏幕设备上的运行情况。它不是一个仿真器,也就是说,它并不试图模拟设备硬件,而只是模拟机器。实际上,Qt Creator 正在构建和运行目标架构。
- 现在选择 iOS 模拟器工具包,或在绿色项目工具中选择
Android,如图所示:

- 构建并运行它,这将启动它在模拟器中。
这是这个应用在 iOS 模拟器上的运行情况:

好了!你已经制作了一个移动应用!感觉不错,不是吗?正如你所见,它在模拟器中看起来略有不同。
调整屏幕尺寸
将为桌面开发的程序移植到较小的移动设备上可能是一项艰巨的任务,这取决于应用程序。即使是为移动设备创建新应用,也需要考虑一些因素,例如屏幕分辨率、内存限制以及处理方向变化。触摸屏增加了提供触摸手势的另一种奇妙方式,但由于手指点与鼠标指针大小的差异,可能会带来挑战!然后还有传感器、GPS 和网络需要考虑!
屏幕分辨率
如你在“添加 QListView”部分之前看到的图片所示,桌面和移动电话之间的应用程序范式相当不同。当你移动到一个更小的显示时,关于如何在屏幕上放置所有内容的问题开始变得棘手。
幸运的是,Qt Widgets 可以帮助。C++类QScrollArea、QStackedWidget和QTabbedWidget可以更合适地显示内容。将你的屏幕小部件委托到不同的页面上,将允许你的用户享受到与桌面应用程序相同的导航便捷性。
在使用QMenu时,移动设备可能会有问题。它们可能很长、难以控制,并且菜单树对于小屏幕来说钻得太深。以下是一个在桌面上运行良好的菜单:

在移动设备上,这个菜单的最后几项变得无法触及和无法使用。我们需要重新设计它!

可以通过消除菜单或重构以减少其深度,或者使用类似 QStackedWidget 的东西来展示菜单选项来固定菜单。
Qt 支持高(每英寸点数)DPI 显示器。Qt 的新版本自动补偿 iOS 和 Wayland 显示服务器协议中高 DPI 和低 DPI 显示器之间的差异。对于 Android,需要将环境变量 QT_AUTO_SCALE_FACTOR 设置为 true。要测试不同的缩放因子,设置 QT_SCALE_FACTOR,通常使用整数,通常是 1 或 2。
让我们通过一些小部件的例子和它们如何在不同的屏幕上更好地使用来运行一遍:
-
类似
QScrollBar的小部件可以增大尺寸,以便更好地适应手指作为指针,或者更好的是隐藏并使用小部件本身来滚动。UI 通常需要简化。 -
长的
QListViews可能会带来一些挑战。你可以尝试为这样的长列表添加筛选或搜索功能,以便在较小的显示上使数据更易于访问且更美观。 -
即使
QStackedWidget或QTabbedWidget也可能太大。不要让用户左右翻页超过几页。更多内容可能会让用户感到繁琐和烦恼,不断翻页以获取内容。 -
QStyleSheets是针对较小显示器的缩放的好方法,允许开发者对任何小部件进行自定义设置。你可以增加填充和边距,使其更容易进行手指触摸输入。你可以为特定的小部件设置样式,或者将其应用于整个QApplication的某个类的小部件。
qApp->setStyleSheet("QButton {padding: 10px;}");
或者对于特定的小部件,可以这样:
myButton->setStyleSheet("padding: 10px;");
让我们只在设备上有触摸屏时应用这个样式。这将使按钮稍微大一些,更容易用手指点击:
if (!QTouchScreen::devices().isEmpty()) {
qApp->setStyleSheet("QButton {padding: 10px;}");
}
如果你使用样式表设置了一个样式,你很可能还需要自定义其他属性和子控件。应用一个样式表会移除默认样式。
当然,在 Qt Designer 中设置样式表也很容易,只需在目标小部件上右键单击,并在上下文菜单中选择“更改样式表”。如下所示,在苹果 Mac 上:

手机和嵌入式设备具有较小的显示屏,它们的 RAM 和存储空间也较少。
内存和存储
手机和嵌入式设备通常比桌面机器的内存要少。特别是对于嵌入式设备,RAM 和存储空间都有限。
通过优化图像,如果需要则压缩,可以降低使用的存储空间。如果不使用不同的屏幕尺寸,可以手动调整图像大小,而不是在运行时缩放。
还有一些堆栈考虑因素,通常总是通过使用 &(和号)运算符将参数按引用传递给函数。你将在大多数 Qt 代码中注意到这一点。
编译器优化可以极大地影响性能和可执行文件的大小。一般来说,Qt 的qmake mkspec构建文件在正确使用优化方面做得相当不错。
如果存储空间是一个关键考虑因素,那么自己构建 Qt 是一个好主意。使用-no-feature-*配置来排除可能不需要的任何 Qt 功能,这是一种减少其占用空间的好方法。例如,如果一个设备只有一个静态以太网连接并且不需要网络承载管理,只需使用-no-feature-bearermanagement配置 Qt 即可。如果你知道你不会使用 SQL,为什么还要提供那些存储库?使用--list-features参数运行 configure 将列出所有可用的功能。
方向
移动设备可以移动(谁会想到?)有时在横屏模式下查看特定应用程序比在竖屏模式下更好。在 Android 和 iOS 上,响应方向变化是内置的,并且默认根据用户的配置发生。你可能需要做的一件事,实际上是禁用方向变化。
在iOS上,你需要编辑plist.info文件。对于UISupportedInterfaceOrientations键,你需要添加以下内容:
<array><string>UIInterfaceOrientationLandscapeLeft</string></array>
在Android上,编辑AndroidManifest.xml文件,将android:screenOrientation="landscape"设置为横向。
如果一个图片框架设备有一个定制的操作系统,它可能需要其照片查看应用程序在用户切换方向时做出响应。这就是 Qt 传感器能提供帮助的地方。关于这一点,稍后将在第七章的第一部分机器对话中详细介绍。
手势
触屏手势是移动设备与桌面设备不同的另一种方式。多点触控屏幕彻底改变了设备世界。QPanGesture、QPinchGesture和QSwipeGesture可以在这类设备上发挥巨大作用,Qt Quick 也为此类功能提供了组件——Flickable、SwipeView、PinchArea等。关于 Qt Quick 的更多内容将在稍后介绍。
要使用QGestures,首先创建一个包含你想要处理的动作手势的QList,然后为目标小部件调用grabGesture函数。
QList<Qt::GestureType> gestures;
gestures << Qt::PanGesture;
gestures << Qt::PinchGesture;
gestures << Qt::SwipeGesture;
for (Qt::GestureType gesture : gestures)
someWidget->grabGesture(gesture);
你需要从并覆盖小部件的事件循环来处理事件发生时的情况。
bool SomeWidget::event(QEvent *event)
{
if (event->type() == QEvent::Gesture)
return handleGesture(static_cast<QGestureEvent *>(event));
return QWidget::event(event);
}
要对手势进行有用的处理,我们可以这样处理:
if (QGesture *swipe = event->gesture(Qt::SwipeGesture)) {
if (swipe->state() == Qt::GestureFinished) {
switch (gesture->horizontalDirection()) {
case QSwipeGesture::Left:
break;
case QSwipeGesture::Right:
break;
case QSwipeGesture::Up:
break;
case QSwipeGesture::Down:
break;
}
}
}
搭载传感器的设备还可以访问QSensorGesture,这允许实现如摇动等动作手势。关于这一点,稍后将在第七章机器对话中详细介绍。
动态布局
考虑到手机有各种各样的形状和大小,需要为每个不同的屏幕分辨率提供不同的包是荒谬的。因此,我们将使用动态布局。
源代码可以在 Git 仓库的Chapter01-layouts目录下的cp1分支中找到。
Qt 小部件支持使用QVBoxLayout和QGridLayout等类来实现这一功能。
Qt Creator 的设计器是开发动态布局的最简单方式。让我们看看我们如何做到这一点。
要设置布局,我们在应用程序表单上放置一个小部件,然后在键盘上按住Command或Control键,同时选择我们想要放入布局中的小部件。这里有两个选定的QPushButtons用于使用:

接下来,点击此处突出显示的水平布局图标:

您将看到两个被薄红色框包围的小部件,如下面的截图所示:

现在为剩余的小部件重复此操作。
要使小部件随着表单扩展和调整大小,请点击背景并选择网格布局:

保存并构建此应用程序,现在它将能够根据方向变化调整大小,并且能够在不同尺寸的屏幕上工作。注意在纵向(垂直)方向上的外观:

还要注意此应用程序在横向(水平)方向上的外观:

如您所见,此应用程序可以随着方向的变化而变化,但所有小部件都可见且可使用。使用QSpacer可以帮助引导小部件和布局定位。它们可以将小部件推到一起,分开,或将一些小部件保持在一边或另一边。
当然,可以在不接触 Qt Designer 的情况下使用布局。例如以下代码:
QPushButton *button = new QPushButton(this);
QPushButton *button2 = new QPushButton(this);
QBoxLayout *boxLayout = new QVBoxLayout;
boxLayout->addWidget(button);
boxLayout->addWidget(button2);
QHBoxLayout *horizontalLayout = new QHBoxLayout;
horizontalLayout->setLayout(boxLayout);
QLayout及其相关内容是编写能够适应目标设备众多屏幕分辨率和动态变化方向的跨平台应用程序的关键。
图形视图
QGraphicsView、QGraphicScene和QGraphicsItem为基于 Qt Widgets 的应用程序显示 2D 图形提供了一种方式。
源代码可以在 Git 仓库的Chapter01-graphicsview目录下的cp1分支中找到。
每个QGraphicsView都需要一个QGraphicsScene。每个QGraphicsScene都需要一个或多个QGraphicsItem。
QGraphicsItem可以是以下任何一种:
-
QGraphicsEllipseItem -
QGraphicsLineItem -
QGraphicsLineItem -
QGraphicsPathItem -
QGraphicsPixmapItem -
QGraphicsPolygonItem -
QGraphicsRectItem -
QGraphicsSimpleTextItem -
QGraphicsTextItem
Qt Designer 支持添加QGraphicsView。您可以按照以下步骤进行操作:
- 将
QGraphicsView拖动到新的应用程序表单中,并使用与之前相同的QGridLayout填充表单。

- 在源代码中实现
QGraphicsScene并将其添加到QGraphicsView
QGraphicsScene *gScene = new QGraphicsScene(this);
ui->graphicsView->setScene(gScene);
- 定义一个矩形,它将是
Scene的范围。这里它比图形视图的大小小,因此我们可以继续定义一些碰撞检测。
gScene->setSceneRect(-50, -50, 120, 120);
- 创建一个红色矩形来显示边界矩形。为了使其呈现红色,创建一个
QPen,它将被用来绘制矩形,然后将矩形添加到Scene中。
QPen pen = QPen(Qt::red);
gScene->addRect(gScene->sceneRect(), pen);
- 构建并运行应用程序。你会注意到一个带有红色边框正方形的程序。
如前所述,QGraphicsView 显示 QGraphicsItems。如果我们想添加一些碰撞检测,我们需要从 QGraphicsSimpleTextItem 中派生一个子类。
该头文件的格式如下:
#include <QGraphicsScene>
#include <QGraphicsSimpleTextItem>
#include <QGraphicsItem>
#include <QPainter>
class TextGraphic :public QGraphicsSimpleTextItem
{
public:
TextGraphic(const QString &text);
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
QString simpleText;
};
这个从 QGraphicsSimpleTextItem 派生的自定义类将重新实现 paint(..) 函数,并使用 scene 的 collidingItems(...) 函数来检测何时有东西与我们的文本对象发生碰撞。通常,collidingItems 会返回一个 QList 的 QGraphicsItems,但在这里它只是用来检测是否有任何项目发生碰撞。
由于这个类只包含一个项目,因此我们知道它是哪个项目。如果检测到碰撞,文本会改变。在我们更改文本之前,我们不需要检查项目文本是否不同,因为父类的 setText(...) 方法已经为我们做了这件事。
TextGraphic::TextGraphic(const QString &text)
: QGraphicsSimpleTextItem(text),
simpleText(text)
{
}
void TextGraphic::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
if (scene()->collidingItems(this).isEmpty())
QGraphicsSimpleTextItem::setText("BOOM!");
else
QGraphicsSimpleTextItem::setText(simpleText);
QGraphicsSimpleTextItem::paint(painter, option, widget);
}
现在创建我们的 TextGraphic 对象,并将其添加到 Scene 中以使用。
TextGraphic *text = new TextGraphic(QStringLiteral("Qt Mobile!"));
gScene->addItem(text);
如果你构建并运行此应用程序,请注意,如果我们尝试拖动它,text 对象将不会移动。QGraphicsItems 有一个名为 QGraphicsItem::ItemIsMovable 的 flag 属性,可以设置为允许它被移动,无论是通过用户还是通过程序:
text->setFlag(QGraphicsItem::ItemIsMovable);
当我们构建并运行此应用程序时,你可以抓住 text 对象并四处移动。如果它超出我们的边界矩形,文本将改变,只有在它再次移动到红色框内时才会返回原始文本。
如果你想对它进行动画处理,只需添加一个计时器,并在计时器触发时更改 text 对象的位置。
即使使用 Qt Quick 的软件渲染器,QGraphicsView 仍然是一个可行的图形动画解决方案。如果目标设备的存储空间非常紧张,可能没有足够的空间来添加 Qt Quick 库。或者,一个遗留应用程序可能难以导入到 Qt Quick 中。
概述
在本章中,我们讨论了移动和嵌入式开发者在尝试为较小显示设备开发时面临的一些问题,以及如何使用 QStyleSheets 在运行时更改界面以适应触摸屏输入。
我们讨论了存储和内存空间需求,以及配置 Qt 中不必要的功能以使其具有更小占位符的需求。
我们讨论了处理方向变化,并讨论了使用屏幕手势,如 Pinch 和 Swipe。
我们学习了如何使用 Qt Designer 添加 QLayouts 来创建动态调整大小的应用程序。
最后,我们讨论了如何使用 QGraphicsView 来利用图形元素,例如图形文本和图像。
接下来,我们将探讨移动和嵌入式开发中继切片面包之后最好的东西——Qt Quick 和 QML。然后我们将深入探讨有关图形效果的真实精彩内容,以增强任何界面!
第二章:使用 Qt Quick 的流畅 UI
我的电视使用 Qt。我的手机使用 Qt。我可以买一辆使用 Qt 的汽车。我可以在使用 Qt 的信息娱乐中心飞行的飞机上。所有这些事情都使用 Qt Quick 作为它们的 UI。为什么?因为它提供了更快的开发——无需等待编译——语法易于使用,但复杂到足以超越你的想象。
Qt Quick 最初是在 Trolltech 的布里斯班开发办公室作为一位开发者的研究项目而开发的。我的一个工作是将早期版本的演示应用程序安装到诺基亚 N800 平板电脑上,我已将其定制为运行 Qtopia 而不是诺基亚的 Maemo 界面。在那之前,诺基亚已经收购了 Trolltech 公司。在我看来,它将成为下一代 Qtopia,Qtopia 已更名为 Qt Extended。到 2006 年,Qtopia 已经在数百万部手机上销售,包括 11 款手机和 30 种不同的手持设备。Qtopia 的某些部分被融合到 Qt 本身中——我最喜欢的,Qt Sensors 和 Qt Bearer Management 就是这些例子。这个新的类似 XML 的框架变成了 QML 和 Qt Quick。
Qt Quick 是一项真正令人兴奋的技术,它似乎正在接管世界。它被用于笔记本电脑、如 Jolla Sailfish 之类的手机以及医疗设备等。
它允许快速开发、流畅的转换、动画和特殊效果。Qt Quick 允许开发者设计定制的动画用户界面(UI)。结合相关的 Qt Quick Controls 2 和 Qt Charts API,任何人都可以创建炫酷的移动和嵌入式应用程序。
在本章中,我们将设计和构建一个动画用户界面。我们还将介绍基本组件,如 项目、矩形,以及更高级的元素,如 GraphicsView。我们将探讨使用锚点、状态、动画和过渡来定位项目,并还将介绍传统功能,如按钮、滑块和滚动条。还将展示显示数据的先进组件,如柱状图和饼图。
我们在本章中将涵盖以下主题:
-
学习 Qt Quick 基础
-
Qt Quick Controls 中的高级 QML 元素
-
显示数据的元素——Qt 数据可视化和 Qt Charts
-
使用 Qt Quick 进行基本动画
Qt Quick 基础 – 任何事都可行
Qt Quick 是超现实的。你应该知道,在其核心,它只有几个基本构建块,称为组件。你无疑会经常使用这些组件:
-
项目 -
矩形 -
文本 -
图像 -
文本输入 -
鼠标区域
虽然可能有一百多个组件和类型,但这些项目是最重要的。还有几个用于文本、定位、状态、动画、过渡和转换的元素类别。视图、路径和数据处理都有自己的元素。
使用这些构建块,你可以创建充满动画的精彩用户界面(UI)。
编写 Qt Quick 应用程序的语言相当容易上手。让我们开始吧。
QML
Qt 模型语言(QML)是 Qt Quick 使用的声明性编程语言。与 JavaScript 密切相关,它是 Qt Quick 的核心语言。你可以在 QML 文档中使用 JavaScript 函数,Qt Quick 将运行它。
我们在这本书中使用 Qt Quick 2,因为 Qt Quick 1.0 已被弃用。
所有 QML 文档都需要有一个或多个 import 语句。
这与 C 和 C++ 的 #include 语句大致相同。
最基本的 QML 至少有一个导入语句,例如这个:
import QtQuick 2.12
.12 与 Qt 的次要版本相对应,这是应用程序将支持的最低版本。
如果你正在使用在某个 Qt 版本中添加的属性或组件,你需要指定该版本。
Qt Quick 应用程序是用称为元素或组件的构建块构建的。一些基本类型是 Rectangle、Item 和 Text。
输入交互通过 MouseArea 和其他项目,如 Flickable 来支持。
开始开发 Qt Quick 应用的一种方式是使用 Qt Creator 中的 Qt Quick 应用向导。你也可以抓取你喜欢的文本编辑器并开始编码!
让我们通过以下一些重要概念,作为构成 QML 语言的术语来了解:
-
组件、类型和元素
-
动态绑定
-
信号连接
组件
组件,也称为类型或元素,是代码的对象,可以包含 UI 和非 UI 方面。
一个 UI 组件的例子是 Text 对象:
Text {
// this is a component
}
组件属性可以绑定到变量、其他属性和值。
动态绑定
动态绑定是一种设置属性值的方式,该值可以是硬编码的静态值,也可以绑定到其他动态属性值。在这里,我们将 Text 组件的 id 属性绑定到 textLabel。然后我们可以通过使用它的 id 来引用这个元素:
Text {
id: textLabel
}
一个组件可以有零个、一个或几个可以使用的信号。
信号连接
处理信号有两种方式。最简单的方式是在前面加上 on 并将特定信号的第一个字母大写。例如,MouseArea 有一个名为 clicked 的信号,可以通过声明 onClicked 并将其绑定到带有花括号 { } 或单行的函数来连接:
MouseArea {
onClicked: console.log("mouse area clicked!")
}
你还可以使用 Connections 类型来针对其他组件的信号:
Connections {
target: mouseArea
onClicked: console.log("mouse area clicked!")
}
模型-视图范式在 Qt Quick 中并未过时。有一些元素可以显示数据模型视图。
模型-视图编程
Qt Quick 的视图基于一个模型,该模型可以通过 model 属性或组件内的元素列表来定义。视图由一个代理控制,该代理是任何能够显示数据的 UI 元素。
你可以在代理中引用模型数据的属性。
例如,让我们声明一个 ListModel,并用两组数据填充它。Component 是一个可以声明的通用对象,在这里,我使用它来包含一个将作为代理的 Text 组件。具有 carModel ID 的模型数据可以在代理中引用。在这里,有一个绑定到 Text 元素的 text 属性:
源代码可以在 Git 仓库的 Chapter02-1b 目录下的 cp2 分支中找到。
ListModel {
id: myListModel
ListElement { carModel: "Tesla" }
ListElement { carModel: "Ford Sync 3" }
}
Component {
id: theDelegate
Text {
text: carModel
}
}
我们可以使用这个模型及其代理在不同视图中。Qt Quick 提供了一些不同的视图供选择:
-
GridView -
ListView -
PathView -
TreeView
让我们看看我们如何使用这些中的每一个。
GridView
GridView 类型在网格中显示模型数据,类似于 GridLayout。
网格的布局可以通过以下属性包含:
-
flow-
GridView.FlowLeftToRight -
GridView.FlowTopToBottom
-
-
layoutDirection-
Qt.LeftToRight -
Qt.RightToLeft
-
-
verticalLayoutDirection:-
GridView.TopToBottom -
GridView.BottomToTop
-
flow 属性包含数据展示的方式,当数据适合时,它会自动换行到下一行或列。它控制数据如何溢出到下一行或列。
下一个示例的图标来自 icons8.com。
FlowLeftToRight 意味着流是水平的。以下是 FlowLeftToRight 的图示:

对于 FlowTopToBottom,流是垂直的;以下是 FlowTopToBottom 的表示:

当这个示例构建并运行时,你可以通过用鼠标抓住角落来调整窗口大小。这将更好地帮助你理解流的工作方式。
layoutDirection 属性指示数据将如何布局的方向。在以下情况下,这是 RightToLeft:

verticalLayoutDirection 也指示数据将如何布局的方向,但这次将是垂直的。以下是 GridView.BottomToTop 的表示:

ListView
QML 的 Listview 是一种 Flickable 元素类型,这意味着用户可以通过左右滑动或轻扫来浏览不同的视图。与桌面上的 QListView 不同,ListView 中的项目以自己的页面呈现,可以通过左右轻扫来访问。
布局由以下属性处理:
-
orientation:-
Qt.horizontal -
Qt.vertical
-
-
layoutDirection-
Qt.LeftToRight -
Qt.RightToLeft
-
-
verticalLayoutDirection:-
ListView.TopToBottom -
ListView.BottonToTop
-
PathView
PathView 在 Path 中显示模型数据。其代理是一个用于显示模型数据的视图。它可以是简单的线条绘制,也可以是带有文本的图像。这可以产生流动的轮盘式数据展示。Path 可以通过以下一个或多个 path 段落构建:
-
PathAngleArc: 带有半径和中心的弧 -
PathArc: 带有半径的圆弧 -
PathCurve: 通过一系列点绘制路径 -
PathCubic: 贝塞尔曲线上的路径 -
PathLine: 一条直线 -
PathQuad: 二次贝塞尔曲线
在这里,我们使用PathArc来显示一个类似轮子的项目模型,使用我们的carModel:
源代码可以在 Git 仓库的Chapter02-1c目录下的cp2分支中找到。
PathView {
id: pathView
anchors.fill: parent
anchors.margins: 30
model: myListModel
delegate: Rectangle {
id: theDelegate
Text {
text: carModel
}
Image {
source: "/icons8-sedan-64.png"
}
}
path: Path {
startX: 0; startY: 40
PathArc { x: 0; y: 400; radiusX:5; radiusY: 5 }
}
}
你现在应该能看到类似这样的内容:

有几个特殊的path段可以增强和改变path的属性:
-
PathAttribute: 允许在路径的某些点上指定属性 -
PathMove: 将路径移动到新位置
TreeView
TreeView可能是这些视图中最容易被识别的。它看起来非常类似于桌面版本。它显示其模型数据的树结构。TreeView有标题,称为TableViewColumn,你可以用它来添加标题以及指定其宽度。还可以使用headerDelegate、itemDelegate和rowDelegate进行进一步定制。
默认情况下没有实现排序,但可以通过几个属性来控制:
-
sortIndicatorColumn:Int,表示要排序的列 -
sortIndicatorVisible:Bool用于启用排序 -
sortIndicatorOrder:Enum可以是Qt.AscendingOrder或Qt.DescendingOrder
手势和触摸
触摸手势可以是与你的应用程序交互的创新方式。要在 Qt 中使用QtGesture类,你需要通过重写QGestureEvent类并处理内置的Qt::GestureType在 C++中实现处理程序。这样,以下手势可以被处理:
-
Qt::TapGesture -
Qt::TapAndHoldGesture -
Qt::PanGesture -
Qt::PinchGesture -
Qt::SwipeGesture -
Qt::CustomGesture
Qt::CustomGesture标志是一个特殊标志,可以用来发明你自己的自定义手势。
Qt Quick 中有一个内置的手势项目——PinchArea。
PinchArea
PinchArea处理捏合手势,这在 Qt Quick 中常用于从手机上放大图像,因此你可以使用简单的 QML 为任何基于Item的元素实现它。
你可以使用onPinchFinished、onPinchStarted和onPinchUpdated信号,或将pinch.target属性设置为要处理的手势的目标项。
MultiPointTouchArea
MultiPointTouchArea不是一个手势,而是一种跟踪触摸屏多个接触点的途径。并非所有触摸屏都支持多点触摸。手机通常支持多点触摸,一些嵌入式设备也是如此。
要在 QML 中使用多点触摸屏,有MultiPointTouchArea组件,它的工作方式有点像MouseArea。通过将其mouseEnabled属性设置为true,它可以与MouseArea一起操作。这使得MultiPointTouchArea组件忽略鼠标事件,只响应触摸事件。
每个 MultiPointTouchArea 都接受一个 TouchPoints 数组。注意方括号的使用,[ ]——这表示它是一个数组。你可以定义一个或多个这些来处理一定数量的 TouchPoints 或手指。在这里,我们定义并处理了三个 TouchPoints。
如果你在一个非触摸屏上尝试这个,只有一个绿色点会追踪触摸点:
源代码可以在 Git 仓库的 Chapter02-2a 目录下的 cp2 分支中找到。
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 640
height: 480
color: "black"
title: "You can touch this!"
MultiPointTouchArea {
anchors.fill: parent
touchPoints: [
TouchPoint { id: touch1 },
TouchPoint { id: touch2 },
TouchPoint { id: touch3 }
]
Rectangle {
width: 45; height: 45
color: "#80c342"
x: touch1.x
y: touch1.y
radius: 50
Behavior on x {
PropertyAnimation {easing.type: Easing.OutBounce; duration: 500 }
}
Behavior on y {
PropertyAnimation {easing.type: Easing.OutBounce; duration: 500 }
}
}
Rectangle {
width: 45; height: 45
color: "#b40000"
x: touch2.x
y: touch2.y
radius: 50
Behavior on x {
PropertyAnimation {easing.type: Easing.OutBounce; duration: 500 }
}
Behavior on y {
PropertyAnimation {easing.type: Easing.OutBounce; duration: 500 }
}
}
Rectangle {
width: 45; height: 45
color: "#6b11d8"
x: touch2.x
y: touch2.y
radius: 50
Behavior on x {
PropertyAnimation {easing.type: Easing.OutBounce; duration: 500 }
}
Behavior on y {
PropertyAnimation {easing.type: Easing.OutBounce; duration: 500 }
}
}
}
}
当你在非触摸屏上运行它时,你应该看到这个:

注意到 PropertyAnimation 吗?我们很快就会涉及到它;继续阅读。
定位
目前可用的各种不同尺寸的手机和嵌入式设备使得元素的动态定位变得更加重要。你可能不希望事物随机地放置在屏幕上。如果你在具有高 DPI 的 iPhone 上有一个看起来很棒的布局,它可能在小型的 Android 设备上看起来完全不同,图像覆盖了屏幕的一半。QML 中的自动布局被称为定位器。
移动和嵌入式设备具有各种屏幕尺寸。我们可以通过使用动态布局来更好地针对尺寸变化。
布局
这些是用于排列你可能想要使用的不同项目的定位元素:
-
Grid:在网格中定位项目 -
Column:垂直定位项目 -
Row:水平定位项目 -
Flow:以换行方式横向定位项目
此外,还有以下项目:
-
GridLayout -
ColumnLayout -
RowLayout -
StackLayout
Grid 和 GridLayout 元素之间的区别在于,布局在调整大小方面更加动态。布局有附加属性,因此你可以轻松指定布局的各个方面,例如 minimumWidth、列数或行数。项目可以被设置为填充到网格或固定宽度。
你也可以使用更像表格的“刚性”布局。让我们看看使用稍微不那么动态的布局和使用静态尺寸。
刚性布局
我使用“刚性”这个词,因为它们比所有布局元素都缺乏动态性。单元格大小是固定的,并且基于它们所包含的空间的百分比。它们不能跨越行或列来填充下一个列或行。以这个代码为例。
它没有任何布局,当你运行它时,所有元素都会挤在一起:
源代码可以在 Git 仓库的 Chapter02-3 目录下的 cp2 分支中找到。
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
width: 35
height: 35
gradient: Gradient {
GradientStop { position: 0.0; color: "green"; }
GradientStop { position: 0.25; color: "purple"; }
GradientStop { position: 0.5; color: "yellow"; }
GradientStop { position: 1.0; color: "black"; }
}
}
Text {
text: "Hands-On"
color: "purple"
font.pointSize: 20
}
Text {
text: "Mobile"
color: "red"
font.pointSize: 20
}
Text {
text: "and Embedded"
color: "blue"
font.pointSize: 20
}
}
正如你在下面的屏幕截图中所看到的,所有元素都堆叠在一起,没有进行定位:

这可能不是设计团队所梦想的。除非,当然,他们确实这样做了,并且想要使用一个 PropertyAnimation 值来动画化元素移动到它们正确的布局位置。
当我们添加一个 Column QML 元素时会发生什么?检查以下代码:
源代码可以在 Git 仓库的 Chapter02-3a 目录下的 cp2 分支中找到。
Rectangle {
width: 500
height: 500
Column {
Rectangle {
width: 35
height: 35
gradient: Gradient {
GradientStop { position: 0.0; color: "green"; }
GradientStop { position: 0.25; color: "purple"; }
GradientStop { position: 0.5; color: "yellow"; }
GradientStop { position: 1.0; color: "black"; }
}
}
Text {
text: "Hands-On"
color: "purple"
font.pointSize: 20
}
Text {
text: "Mobile"
color: "red"
font.pointSize: 20
}
Text {
text: "and Embedded"
color: "blue"
font.pointSize: 20
}
}
}
当你构建此示例时,布局看起来像这样:

这更像是设计师的草图的样子!(我知道;便宜的设计师。)
Flow 是我们可以使用的另一个布局项。
源代码可以在 Git 仓库的 Chapter02-3b 目录下的 cp2 分支中找到。
Flow {
anchors.fill: parent
anchors.margins: 4
spacing: 10
现在,从我们前面的代码中,将 Column 改为 Flow,添加一些锚定项目,然后在模拟器上构建并运行,以了解 Flow 项目在小屏幕上的工作方式:

Flow 类型在需要时将围绕其内容进行包装,实际上,它已经在最后一个 Text 元素上进行了包装。如果将其重新调整为横向或平板电脑方向,则不需要包装,所有这些元素都将位于顶部的一行中。
动态布局
除了使用 Grid 元素来布局项目外,还有 GridLayout,它可以用来自定义布局。在针对具有不同屏幕尺寸和设备方向的移动和嵌入式设备时,可能最好使用 GridLayout、RowLayout 和 ColumnLayout。使用这些布局,你将能够使用其附加属性。以下是可以使用的附加属性列表:
Layout.alignment |
一个 Qt.Alignment 值,指定单元格内项目的对齐方式 |
|---|---|
Layout.bottomMargin |
空间底部边距 |
Layout.column |
指定列位置 |
Layout.columnSpan |
展开到多少列 |
Layout.fillHeight |
如果为 true,则项目填充到高度 |
Layout.fillWidth |
如果为 true,则项目填充到宽度 |
Layout.leftMargin |
空间左侧边距 |
Layout.margins |
空间的所有边距 |
Layout.maximumHeight |
项目最大高度 |
Layout.maximumWidth |
项目最大宽度 |
Layout.minimumHeight |
项目最小高度 |
Layout.minimumWidth |
项目最小宽度 |
Layout.preferredHeight |
项目首选高度 |
Layout.preferredWidth |
项目首选宽度 |
Layout.rightMargin |
空间右侧边距 |
Layout.row |
指定行位置 |
Layout.rowSpan |
展开到多少行 |
Layout.topMargin |
空间顶部边距 |
在此代码中,我们使用 GridLayout 来定位三个 Text 元素。第一个 Text 元素将跨越或填充两行,以便第二个 Text 元素位于第二行:
源代码可以在 Git 仓库的 Chapter02-3c 目录下的 cp2 分支中找到。
GridLayout {
rows: 3
columns: 2
Text {
text: "Hands-On"
color: "purple"
font.pointSize: 20
}
Text {
text: "Mobile"
color: "red"
font.pointSize: 20
}
Text {
text: "and Embedded"
color: "blue"
font.pointSize: 20
Layout.fillHeight: true
}
}
定位是一种获取动态变化的应用程序并允许它们在各种设备上工作而不必更改代码的方法。GridLayout 工作方式类似于布局,但具有更强大的功能。
让我们看看如何使用锚点动态定位这些组件。
锚点
锚点与定位相关,是相对于彼此定位元素的一种方式。它们是动态定位 UI 元素和布局的一种方法。
它们使用以下接触点:
-
left -
right -
top -
bottom -
horizontalCenter -
verticalCenter
以两个图像为例;你可以通过指定锚点位置将它们组合在一起,就像拼图一样:
Image{ id: image1; source: "image1.png"; }
Image{ id: image2; source: "image2.png; anchors.left: image1.right; }
这将使image2的左侧与image1的右侧对齐。如果你给image1添加anchors.top: parent.top,这两个项目就会相对于父元素的顶部位置进行定位。如果父元素是顶级元素,它们就会被放置在屏幕顶部。
锚点是一种实现相对于其他组件的列、行和网格组件的方式。你可以将项目对角锚定,也可以将它们彼此分开,等等。
例如,Rectangle的anchor属性,称为fill,是一个特殊术语,表示顶部、底部、左侧和右侧,并且绑定到其父元素上。这意味着它将填充到父元素的大小。
使用anchors.top表示元素的顶部锚点,这意味着它将绑定到父组件的顶部位置。例如,一个Text组件将位于Rectangle组件之上。
要使Text组件水平居中,我们使用anchor.horizontal属性并将其绑定到parent.horizontalCenter位置属性。
在这里,我们将Text标签锚定到Rectangle标签的顶部中心,而Rectangle标签本身锚定到fill其父元素,即Window:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 500
height: 500
Rectangle {
anchors.fill: parent
Text {
id: textLabel
text: "Hands-On Mobile and Embedded"
color: "purple"
font.pointSize: 20
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
源代码可以在 Git 仓库的Chapter02目录下的cp2分支中找到。
Window组件是由 Qt Quick 应用程序向导提供的,默认情况下不可见,因此向导将visible属性设置为true,因为我们需要看到它。我们将使用Window作为Rectangle组件的父元素。我们的Rectangle组件将为Text组件提供一个区域,这是一个简单的标签类型。
每个组件都有自己的属性可以进行绑定。这里的绑定指的是将属性绑定到元素上。例如,color: "purple"这一行将颜色"purple"绑定到了Text元素的color属性上。这些绑定不必是静态的;它们可以动态更改,并且它们所绑定的属性值也会随之改变。这种值绑定将一直持续到属性被赋予另一个值。
这个应用程序的背景很无聊。我们何不在那里添加一个渐变效果?在 Text 组件的关闭括号下,但仍在 Rectangle 内部,添加这个渐变。GradientStop 是在渐变中指定某个点颜色的方式。position 属性是从零到一的百分比分数点,对应于颜色应该开始的位置。渐变将填充中间的空白:
gradient: Gradient {
GradientStop { position: 0.0; color: "green"; }
GradientStop { position: 0.25; color: "purple"; }
GradientStop { position: 0.75; color: "yellow"; }
GradientStop { position: 1.0; color: "black"; }
}
源代码可以在 Git 仓库的 Chapter02-1 目录下的 cp2 分支中找到。
如您所见,渐变从顶部的绿色开始,平滑地过渡到紫色,然后是黄色,最后结束于黑色:

简单易行,轻松愉快!
布局和锚点对于能够控制 UI 非常重要。它们提供了一种简单的方法来处理显示尺寸的差异和在不同屏幕尺寸的数百种不同设备上的方向变化。您可以让一个 QML 文件在所有显示设备上工作,尽管建议为极端不同的设备使用不同的布局。一个应用程序可以在平板电脑上运行良好,甚至可以在手机上运行,但尝试将其放置在手表或其他嵌入式设备上,您将遇到许多用户可以使用但无法访问的细节。
Qt Quick 有许多构建块,可以在任何设备上创建有用的应用程序。当您不想自己创建所有 UI 元素时会发生什么?这就是 Qt Quick Controls 发挥作用的地方。
Qt Quick Controls 2 按钮,按钮,谁有按钮?
在 Qt Quick 生命周期的某个阶段,只有一些基本组件,如 Rectangle 和 Text。开发者必须创建自己的按钮、旋钮以及几乎所有常见 UI 元素的实现。随着其成熟,它还增加了 Window 和甚至 Sensor 元素。一直有关于提供一组常见 UI 元素的讨论。最终,常见的 UI 元素被发布了。
关注 Qt Quick Controls。不再需要自己创建按钮和其他组件,太好了!开发者们也为此欢呼!
然后,他们找到了更好的做事方式,并发布了 Qt Quick Controls 2!
Qt Quick Controls 有两个版本,Qt Quick Controls 和 Qt Quick Controls 2。Qt Quick Controls(原始版本)已被 Qt Quick Controls 2 弃用。任何新使用这些组件的情况都应该使用 Qt Quick Controls 2。
您可以访问各种常见的 UI 元素,包括以下内容:
-
按钮 -
容器 -
输入 -
菜单 -
单选按钮 -
进度条 -
弹出窗口
让我们检查一个简单的 Qt Quick Controls 2 示例。
ApplicationWindow 有附加的 menuBar、header 和 footer 属性,您可以使用它们添加所需的内容。由于 ApplicationWindow 默认不可见,我们几乎总是需要添加 visible: true。
在这里,我们将在页眉中添加一个带有 TextField 的传统菜单。
菜单有一个 onTriggered 信号,在这里用于运行 MessageDialog 的 open() 函数:
源代码可以在 Git 仓库的 Chapter02-4 目录下找到,位于 cp2 分支。
import QtQuick 2.12
import QtQuick.Controls 2.3
import QtQuick.Dialogs 1.1
ApplicationWindow {
visible: true
title: "Mobile and Embedded"
menuBar: MenuBar {
Menu { title: "File"
MenuItem { text: "Open "
onTriggered: helloDialog.open()
}
}
}
header: TextField {
placeholderText: "Remember the Qt 4 Dance video?"
}
MessageDialog {
id: helloDialog
title: "Hello Mobile!"
text: "Qt for Embedded devices to rule the world!"
}
}
下面是我们的代码将产生的结果:

哇哦 – 真是太棒了!
Qt Quick Controls 2 提供了多种样式供选择 – 默认, 融合, 想象, 材料, 和 通用。这可以在 C++ 后端通过 QQuickStyle::setStyle("Fusion"); 来设置。我猜你确实有一个 C++ 后端,对吧?
以下是一些在移动和嵌入式设备上可能会很有用的视图:
-
ScrollView -
StackView -
SwipeView
这些在小屏幕上可能很有帮助,因为它们提供了一种轻松查看和访问多个页面的方式,而无需太多麻烦。Drawer 元素也很方便,可以提供一种实现侧边菜单或工具栏的方式。
按钮很棒,Qt Quick Controls 2 也有按钮。它甚至有 RoundButton 组件,以及按钮的图标!在 Qt Quick Controls 之前,我们不得不自己实现这些功能。同时,很棒的是我们可以用很少的努力来实现这些功能,现在甚至更少了!
让我们测试一些这些功能,并在此基础上扩展我们的上一个示例。
我喜欢 SwipeView,所以让我们使用它,将两个 Page 元素作为 SwipeView 的子元素:
源代码可以在 Git 仓库的 Chapter02-5 目录下找到,位于 cp2 分支。
SwipeView {
id: swipeView
anchors.fill: parent
Page {
id: page1
anchors.fill: parent.fill
header: Label {
text: "Working"
font.pixelSize: Qt.application.font.pixelSize * 2
padding: 10
}
BusyIndicator {
id: busyId
anchors.centerIn: parent
running: true;
}
Label {
text: "Busy Working"
anchors.top: busyId.bottom
anchors.horizontalCenter: parent.horizontalCenter
}
}
Page {
id: page2
anchors.fill: parent.fill
header: Label {
text: "Go Back"
font.pixelSize: Qt.application.font.pixelSize * 2
padding: 10
}
Label {
text: "Nothing here to see. Move along, move along."
anchors.centerIn: parent
}
}
}
PageIndicator {
id: indicator
count: swipeView.count
currentIndex: swipeView.currentIndex
anchors.bottom: swipeView.bottom
anchors.horizontalCenter: parent.horizontalCenter
}
我认为在底部添加一个 PageIndicator 来指示我们当前所在的页面,可以为用户导航提供一些视觉反馈。我们通过将 SwipeView 的 count 和 currentIndex 属性绑定到同名属性上来整合 PageIndicator。多么方便啊!
我们可以像使用 PageIndicator 一样轻松地使用 TabBar。
自定义
你几乎可以自定义每个 Qt Quick Control 2 组件的外观和感觉。你可以覆盖控件的不同属性,例如 background。在前面的示例代码中,我们自定义了 Page 标题。在这里,我们覆盖背景为按钮,添加我们自己的 Rectangle,上色,用对比色添加边框,并通过 radius 属性使其两端圆润。下面是如何工作的:
源代码可以在 Git 仓库的 Chapter02-5 目录下找到,位于 cp2 分支。
Button {
text: "Click to go back"
background: Rectangle {
color: "#673AB7"
radius: 50
border.color: "#4CAF50"
border.width: 2
}
onClicked: swipeView.currentIndex = 0
}

使用 Qt Quick 进行自定义非常简单。它是为了自定义而构建的。方法多种多样。几乎所有的 Qt Quick Controls 2 元素都有可自定义的视觉元素,包括大多数背景和内容项,尽管并非全部。
这些控件在桌面电脑上似乎效果最好,但它们可以被自定义以在移动设备和嵌入式设备上良好工作。ScrollView 的 ScrollBar 属性可以在触摸屏上增加宽度。
展示你的数据 – Qt 数据可视化和 Qt 图表
Qt Quick 提供了一种方便的方式来展示各种类型的数据。两个模块,Qt 数据可视化和 Qt Charts,都可以提供完整的 UI 元素。它们很相似,但 Qt 数据可视化以 3D 形式展示数据。
Qt Charts
Qt Charts 展示二维图表并使用图形视图框架。
它添加了以下图表类型:
-
面积图
-
柱状图
-
箱线图
-
K 线图
-
线形图:简单的线形图
-
饼图:饼图切片
-
极坐标:圆形线
-
散点图:一组点集
-
样条图:带有曲线点的线形图
以下是从 Qt 中提供的示例,展示了几个可用的不同图表:

每个图表或图形至少有一个轴,可以有以下类型:
-
柱状图轴
-
类别
-
日期时间
-
对数值
-
值
Qt Charts 需要一个 QApplication 实例。如果您使用 Qt Creator 向导创建应用程序,它默认使用 QGuiApplication 实例。您需要将 main.cpp 中的 QGuiApplication 实例替换为 QApplication,并更改 includes 文件。
您可以在轴上使用网格线、阴影和刻度标记,这些也可以在这些图表中显示。
让我们看看如何创建一个简单的柱状图。
源代码可以在 Git 仓库的 Chapter02-6 目录下的 cp2 分支中找到。
import QtCharts 2.0
ChartView {
title: "Australian Rain"
anchors.fill: parent
legend.alignment: Qt.AlignBottom
antialiasing: true
BarSeries {
id: mySeries
axisX: BarCategoryAxis {
categories: ["2015", "2016", "2017" ]
}
BarSet { label: "Adelaide"; values: [536, 821, 395] }
BarSet { label: "Brisbane"; values: [1076, 759, 1263] }
BarSet { label: "Darwin"; values: [2201, 1363, 1744] }
BarSet { label: "Melbourne"; values: [526, 601, 401] }
BarSet { label: "Perth"; values: [729, 674, 578] }
BarSet { label: "Sydney"; values: [1076, 1386, 1338] }
}
}
看看这些图表看起来有多棒?来看看:

Qt 数据可视化
Qt 数据可视化类似于 Qt Charts,但以 3D 形式展示数据。它可以通过 Qt Creator 的维护工具应用程序下载。它与 Qt Widget 和 Qt Quick 兼容。我们将使用 Qt Quick 版本。它使用 OpenGL 来展示数据的 3D 图形。
由于我们针对的是移动电话和嵌入式设备,我们讨论使用 OpenGL ES2。Qt 数据可视化的一些功能与 OpenGl ES2 不兼容,这是您在移动电话上会发现的情况:
-
抗锯齿
-
平滑着色
-
阴影
-
使用 3D 纹理的体积对象
让我们尝试使用来自之前示例中使用的澳大利亚某些城市总降雨量的 Bars3D 数据。
我将主题设置为 Theme3D.ThemeQt,这是一个基于绿色的主题。添加一些自定义,如字体大小,以便在小型移动显示屏上更好地查看内容。
Bar3DSeries 将管理诸如行、列和数据(此处为该年的总降雨量)的标签等视觉元素。ItemModelBarDataProxy 是显示数据的代理。此处模型数据是一个包含前三年城市降雨数据的 ListModel。我们将使用与之前 Qt Charts 示例中相同的数据,以便您可以比较柱状图显示数据的方式的差异:
源代码可以在 Git 仓库的 Chapter02-7 目录下的 cp2 分支中找到。
import QtQuick 2.12
import QtQuick.Window 2.12
import QtDataVisualization 1.2
Window {
visible: true
width: 640
height: 480
title: qsTr("Australian Rain")
Bars3D {
width: parent.width
height: parent.height
theme: Theme3D {
type: Theme3D.ThemeQt
labelBorderEnabled: true
font.pointSize: 75
labelBackgroundEnabled: true
}
Bar3DSeries {
itemLabelFormat: "@colLabel, @rowLabel: @valueLabel"
ItemModelBarDataProxy {
itemModel: dataModel
rowRole: "year"
columnRole: "city"
valueRole: "total"
}
}
}
ListModel {
id: dataModel
ListElement{ year: "2017"; city: "Adelaide"; total: "536"; }
ListElement{ year: "2016"; city: "Adelaide"; total: "821"; }
ListElement{ year: "2015"; city: "Adelaide"; total: "395"; }
ListElement{ year: "2017"; city: "Brisbane"; total: "1076"; }
ListElement{ year: "2016"; city: "Brisbane"; total: "759"; }
ListElement{ year: "2015"; city: "Brisbane"; total: "1263"; }
ListElement{ year: "2017"; city: "Darwin"; total: "2201"; }
ListElement{ year: "2016"; city: "Darwin"; total: "1363"; }
ListElement{ year: "2015"; city: "Darwin"; total: "1744"; }
ListElement{ year: "2017"; city: "Melbourne"; total: "526"; }
ListElement{ year: "2016"; city: "Melbourne"; total: "601"; }
ListElement{ year: "2015"; city: "Melbourne"; total: "401"; }
ListElement{ year: "2017"; city: "Perth"; total: "729"; }
ListElement{ year: "2016"; city: "Perth"; total: "674"; }
ListElement{ year: "2015"; city: "Perth"; total: "578"; }
ListElement{ year: "2017"; city: "Sydney"; total: "1076"; }
ListElement{ year: "2016"; city: "Sydney"; total: "1386"; }
ListElement{ year: "2015"; city: "Sydney"; total: "1338"; }
}
}
您可以在触摸屏设备上运行此代码,然后可以在 3D 中移动图表:

你可以抓取图表并旋转它以从不同的角度查看数据。你还可以放大和缩小。
QtDataVisualization模块还具有显示 3D 数据的散点图和表面图。
让它动起来!
这就是它变得复杂的地方。有各种类型的动画:
-
ParallelAnimation -
SmoothedAnimation -
PauseAnimation -
SequentialAnimation
此外,还可以使用PropertyAction和ScriptAction。PropertyAction是指不涉及动画的任何属性的变化。我们在上一节关于状态的部分学习了ScriptAction。
还有其他类型的动画,它们操作各种值:
-
AnchorAnimation -
ColorAnimation -
NumberAnimation -
OpacityAnimator -
PathAnimation -
ParentAnimation -
PropertyAnimation -
RotationAnimation -
SpringAnimation -
Vector3DAnimation
可以使用Behavior来指定属性变化时的动画。
让我们看看这些是如何被使用的。
转换
转换和状态被明确地联系在一起。当状态发生变化时,会发生Transition动画。
状态变化可以处理不同类型的更改:
-
AnchorChanges: 锚定布局的变化 -
ParentChanges: 父亲关系的变化(例如重新分配) -
PropertyChanges: 目标属性的变化
你甚至可以使用StateChangeScript和ScriptAction在状态变化上运行 JavaScript。
要定义不同的states,一个元素有一个states数组,其中可以定义State元素。我们将添加一个PropertyChanges:
states : [
State {
name: "phase1"
PropertyChanges { target: someTarget; someproperty: "some value";}
},
State {
name: "phase2"
PropertyChanges { target: someTarget; someproperty: "some other value";}
}
]
目标属性可以是几乎任何东西——opacity、position、color、width或height。如果一个元素具有可变属性,那么你很可能可以在状态变化中对其动画化。
如我之前提到的,要在状态变化中运行脚本,你可以在你想要运行脚本的State元素中定义一个StateChangeScript。在这里,我们只是输出一些日志文本:
function phase3Script() {
console.log("demonstrate a state running a script");
}
State {
name: "phase3"
StateChangeScript {
name: "phase3Action"
script: phase3Script()
}
}
想象一下可能性!我们甚至还没有介绍动画!我们将在下一节介绍。
动画
动画可以用奇妙的方式为你的应用增添色彩。Qt Quick 使得几乎可以轻松地动画化应用的不同方面。同时,它允许你将它们定制为独特且更复杂的动画。
PropertyAnimation
PropertyAnimation动画化一个项目的可变属性。通常,这是 x 或 y 颜色,或者可以是任何项目的其他属性:
Behavior on activeFocus { PropertyAnimation { target: myItem; property: color; to: "green"; } }
Behavior指定器意味着当activeFocus在myItem上时,颜色将变为绿色。
NumberAnimation
NumberAnimation从PropertyAnimation派生,但仅适用于具有可变qreal值的属性:
NumberAnimation { target: myOtherItem; property: "y"; to: 65; duration: 250 }
这将在 250 微秒的时间内将myOtherItem元素的y位置移动到 65。
其中一些动画元素控制其他动画的播放方式,包括SequentialAnimation和ParallelAnimation。
SequentialAnimation
SequentialAnimation是一种连续运行其他动画类型的动画,一个接一个,就像编号的程序:
SequentialAnimation {
NumberAnimation { target: myOtherItem; property: "x"; to: 35; duration: 1500 }
NumberAnimation { target: myOtherItem; property: "y"; to: 65; duration: 1500 }
}
在这种情况下,首先播放的动画是ColorAnimation,一旦完成,就会播放NumberAnimation。将myOtherItem元素的x属性移动到位置35,然后将其y属性移动到位置65,分两步进行:

您可以使用on <属性>或properties来定位一个属性。
此外,还有when关键字,表示何时可以发生某事。如果它评估为true或false,则可以与任何属性一起使用,例如when: y > 50。例如,您可以在running属性上使用它。
ParallelAnimation
ParallelAnimation同时异步播放所有定义的动画:
ParallelAnimation {
NumberAnimation { target: myOtherItem; property: "x"; to: 35; duration: 1500 }
NumberAnimation { target: myOtherItem; property: "y"; to: 65; duration: 1500 }
}
这些是相同的动画,但它们会同时执行。
有趣的是,这个动画会直接将myOtherItem移动到位置35和65,就像它是一步一样:

SpringAnimation
SpringAnimation通过弹簧运动来动画化项目。有两个需要注意的属性——spring和damping:
-
spring:一个控制弹跳能量的qreal值 -
damping:弹跳停止的速度 -
mass:为弹跳添加重量,使其表现得像有重力和重量 -
velocity:指定最大速度 -
modulus:值将环绕到零的值 -
epsilon:四舍五入到零的量
源代码可以在 Git 仓库的Chapter02-8目录下的cp2分支中找到。
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 640
height: 480
color: "black"
title: qsTr("Red Bouncy Box")
Rectangle {
id: redBox
width: 50; height: 50
color: "black"
border.width: 4
border.color: "red"
Behavior on x { SpringAnimation { spring: 10; damping: 10; } }
Behavior on y { SpringAnimation { spring: 10; damping: .1; mass: 10 } }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: animation.start()
onPositionChanged: {
redBox.x = mouse.x - redBox.width/2
redBox.y = mouse.y - redBox.height/2
}
}
ParallelAnimation {
id: animation
NumberAnimation { target: redBox; property: "x"; to: 35; duration: 1500 }
NumberAnimation { target: redBox; property: "y"; to: 65; duration: 1500 }
}
}
在这个例子中,一个红色方块跟随手指或鼠标光标移动,上下弹跳。当用户点击应用时,红色方块会移动到位置35和65。spring值为10使其非常弹跳,但y轴上的mass值为10会使它像有更多重量一样弹跳。damping值越低,它越快停下来。在这里,x轴上的damping值要大得多,所以它倾向于比侧向弹跳更多上下弹跳。
缓动
我应该在这里提到缓动。每个 Qt Quick 动画都有一个easing属性。缓动是一种指定动画进度速度的方式。默认的easing值是Easing.Linear。有 40 种不同的easing属性,这些属性最好在示例中运行,而不是在这里用图表展示。
您可以通过 Qt for WebAssembly 的魔法在我的 GitHub 网络服务器上看到这个演示。
lpotter.github.io/easing/easing.html.
Qt for WebAssembly 将 Qt 应用程序带到了网页上。在撰写本书时,Firefox 拥有最快的 WebAssembly 实现。我们将在 第十四章,通用移动和嵌入式设备平台中讨论 Qt for WebAssembly。
SceneGraph
场景图基于 OpenGL 构建 Qt Quick。在移动和嵌入式设备上,通常是 OpenGL ES2。如前所述,场景图旨在管理大量的图形。OpenGL 是一个庞大的主题,值得有它自己的书籍——实际上,有成吨的关于 OpenGL ES2 编程的书籍。在这里我不会过多地详细介绍它,但只是提到 OpenGL 可用于移动电话和嵌入式设备,具体取决于硬件。
如果你打算使用场景图,大部分繁重的工作将在 C++ 中完成。你应该已经熟悉如何结合使用 C++ 和 QML,以及 OpenGL ES2。如果不熟悉,Qt 有关于它的优秀文档。
摘要
Qt Quick 为在移动和嵌入式设备上使用而预先准备。从基本 Qt Quick 项的简单构建块到 3D 数据图表,你可以使用各种数据集和 QML 中的展示来编写复杂的动画应用程序。
你现在应该能够使用基本组件,如 Rectangle 或 Text,来创建使用动态变量绑定和信号的 Qt Quick 应用程序。
我们还介绍了如何使用 anchors 来视觉定位组件,并能够接受目标设备的改变方向和各种屏幕尺寸。
你现在可以使用看起来更传统的组件,例如现成的 Button、Menu 和 ProgressBar 实例,以及更高级的图形元素,如 PieChart 和 BarChart。
我们还检查了 Qt Quick 中可用的不同动画方法,例如 ProperyAnimation 和 NumberAnimation。
在下一章中,我们将学习如何使用粒子和特殊图形效果。
第三章:图形和特效
Qt Quick 通过使用粒子扩展了动画和特效。粒子和 Qt 图形特效可以使用户界面(UI)生动活泼,并在众多界面中脱颖而出。
Qt Quick 中的粒子系统允许大量图像或其他图形对象模拟高度活跃和混乱的动画和效果。使用粒子系统模拟下雪或爆炸着火变得更容易。这些元素的动态属性使动画更加生动。
使用 Qt 图形特效可以帮助使 UI 在视觉上更具吸引力,并使用户更容易区分图形组件。阴影、发光和模糊使二维对象看起来更像三维对象。
在本章中,我们将涵盖以下主题:
-
粒子宇宙
-
粒子
画家、发射器和影响器 -
Qt Quick 的图形特效
粒子宇宙
最后!我们终于到达了书中最有趣的部分,魔法就在这里发生。使用矩形、文本和按钮已经很好了,但粒子增加了活力和动感,同时为游戏增添了光影。它们还可以用来突出和强调感兴趣的项目。
粒子是一种由众多图形元素组成的动画,所有元素都以模糊的方式移动。有四个主要的 QML 组件可以使用:
-
ParticleSystem:维护粒子动画时间线 -
发射器:将粒子辐射到系统中 -
画家:这些组件绘制粒子。以下是各种组件:-
ImageParticle:使用图像的粒子 -
ItemParticle:使用 QML 项目作为代理的粒子 -
CustomParticle:使用着色器的粒子
-
-
影响器:改变粒子的属性
要了解我们如何管理所有这些项目,让我们看看主要的粒子管理器,即 ParticleSystem。
ParticleSystem
ParticleSystem 组件维护粒子动画时间线。它是将所有其他元素连接在一起并作为操作中心的元素。您可以暂停、恢复、重启、重置、开始和停止粒子动画。
画家、发射器 和 影响器 都通过 ParticleSystem 相互交互。
在您的应用程序中可以存在多个 ParticleSystem 组件,每个组件都有一个 Emitter 组件。
让我们更深入地探讨一下关于粒子 画家、发射器 和 `影响器的细节。
粒子画家、发射器和影响器
Qt Quick 中的粒子是图形元素,如图像、QML 项目和 OpenGL 着色器。
它们可以被制作成以无数种方式移动和流动。
每个粒子都是 ParticleGroup 的一部分,默认情况下,它有一个空名称。ParticleGroup 是一组粒子画家,允许对分组粒子画家进行定时动画转换。
粒子发射的方向由 Direction 项目控制,这些项目由以下组件组成:AngleDirection、PointDirection 和 TargetDirection。
你可以使用的粒子画家类型只有几种,但它们几乎涵盖了你想用它们做的所有事情。Qt Quick 中可用的粒子类型如下:
-
CustomParticle:基于 OpenGL 着色器的粒子 -
ImageParticle:基于图像文件的粒子 -
ItemParticle:基于 QML 项目的粒子
ImageParticle 可能是最常见且最容易使用的,可以从 QML 支持的任何图像中创建。如果将要有很多粒子,最好使用小而优化的图像。
让我们检查一个简单的 ItemParticle 动画。我们将首先定义一个 ParticleSystem 组件,它有一个作为透明 Rectangle 元素定义的子 ItemParticle 动画,该元素有一个小的绿色边框和半径 65,这意味着它看起来像一个绿色圆圈。
实际上有两种类型的发射器——标准的 Emitter 类型,还有一种特殊的 TrailEmitter 类型,它从 Emitter 项目派生出来,但它的粒子是从其他粒子而不是其边界区域发射的。
使用 SystemParticle 组件将其 system 属性绑定到一个 Emitter 项目上,定义了一个 Emitter 项目。对于 Emitter 项目的 velocity 属性,我们使用 AngleDirection。AngleDirection 将发射的粒子指向一定角度。
QML 元素中的角度是顺时针方向的,从元素的右侧开始。以下是它的表示:

例如,设置 AngleDirection 为 90 将使粒子向下移动。
让我们深入一个粒子示例:
源代码可以在 Git 仓库的 Chapter03-1 目录下的 cp3 分支中找到。
- 我们首先定义一个
ParticleSystem:
ParticleSystem {
id: particelSystem
anchors.fill: parent
- 我们添加一个
ItemParticle并将delegate定义为透明的Rectangle。我们定义一个radius,使其具有圆角,并指定它有一个小的绿色边框:
ItemParticle {
delegate: Rectangle {
height: 30; width: 30
id: particleSquare
color: "transparent"
radius: 65
border.color: "green"
border.width: 4
}
}
}
- 我们定义一个
Emitter并将其分配给ParticleSystem:
Emitter {
id: particles
system: particleSystem
anchors { horizontalCenter: parent.horizontalCenter; }
y: parent.height / 2
width: 10
height: 10
lifeSpan: 5000
velocityFromMovement: 60
sizeVariation: 15
emitRate: 50
enabled: false
- 我们给
Emitter一个AngleDirectionvelocity以增加方向上的变化:
velocity: AngleDirection {
angle: 90
magnitude: 150
angleVariation: 25
magnitudeVariation: 50
}
}
到目前为止,应用程序看起来是这样的:

让我们看看当发射器未居中时的样子:
-
我们将
Emitter属性,称为enabled,绑定到false的值,以停止粒子持续发射。 -
然后,我们将
burst属性绑定到动画,通过鼠标点击来产生25个粒子的脉冲,如下所示:
MouseArea {
id: mousey
anchors.fill: parent
onClicked: {particles.burst(25) }
hoverEnabled: true
}
Emitter 组件的属性是动画开始时粒子的属性。
- 我们将
Emitter属性的x和y属性绑定到鼠标位置:
y: mousey.mouseY
x: mousey.mouseX
- 我们也可以移除
horizontalCenter锚点,除非你希望粒子爆发始终在水平方向上居中。
这张图片显示了Emitter在水平方向未居中的情况:

要影响粒子在场景中发射时的行为,你需要一个Affector。让我们看看如何在下一节中如何使用Affector。
影响器
影响器是一种属性,它影响粒子流的模式。有几种类型的affectors可供选择:
-
Age:将提前终止粒子 -
Attractor:吸引粒子向一个点 -
Friction:根据粒子的速度减慢粒子 -
Gravity:以角度应用加速度 -
Turbulence:以流体方式应用噪声 -
Wander:随机粒子轨迹
还有GroupGoal和SpriteGoal``affectors。
Affectors是可选的,但在粒子发射后会增加其效果。
让我们考察一种使用这些项目的方法。
- 我们将一个
Turbulence项目作为子组件添加到ParticleSystem组件中。现在,粒子将随机飞舞,就像被风吹散的落叶:
Turbulence {
anchors.fill: parent
strength: 32
}
- 你可以有多个影响器。让我们添加一些
Gravity,也!我们将使这个Gravity向上。Gravity有点像给一个项目在某个方向上施加重量:
Gravity {
anchors.fill: parent
angle: 270
magnitude: 4
}
这里是我们的Turbulence圆形示例的样子:

你可以在这里尝试 Qt for WebAssembly 版本:lpotter.github.io/particles/ch3-1.html。
我们也可以使粒子以特定方向流动,或以特定形状行动。
形状和方向
形状是一种可以用来影响影响器如何作用于特定区域的方法。
-
EllipseShape:作用于椭圆形状区域 -
LineShape:作用于一条线 -
MaskShape:作用于图像形状区域 -
RectangleShape:作用于矩形区域
粒子可以在某个方向上具有速度。有三种方法可以引导粒子:
-
AngleDirection -
PointDirection -
TargetDirection
从发射点开始,AngleDirection有四个属性——angle、angleVariation、magnitude和magnitudeVariation。正如我之前提到的,角度是以顺时针方向测量的,从Emitter项的右侧开始。magnitude属性指定每秒移动的速度(以像素为单位)。
PointDirection将velocity属性指向场景中的某个点,或者如果你喜欢,可以指向屏幕外。它需要x、y、xVariation和yVariation属性。
使用TargetDirection,你可以指示粒子向目标项发射,或者向目标x、y点发射。TargetDirection有一个新属性称为proportionalMagnitude,这使得magnitude和magnitudeVariation属性作为起始点和目标点之间距离的每秒倍数来操作。
粒子可以非常有趣,并为应用程序增添科幻元素。要使它们表现出你心中所想,需要进行一些实验,因为它们具有很大的随机性。
现在,让我们看看如何添加其他类型的图形效果。
Qt Quick 的图形效果
当你通常想到模糊、对比度和发光等效果时,你可能会想到图像编辑软件,因为它们倾向于将这些效果应用于图像。Qt 图形效果可以将这些相同类型的效应应用于 QML UI 组件。
如果你使用 Qt Quick Scene Graph 软件渲染器,这些效果将不可用或不可用,因为该软件不支持这些效果。
Qt 图形效果有多种类型,每种类型都有各种子效果:
-
Blend -
Color:-
BrightnessContrast -
`ColorOverlay` -
Colorize -
Desaturate -
GammaAdjust -
HueSaturation -
LevelAdjust
-
-
Gradients:-
ConicalGradient -
LinearGradient -
RadialGradient
-
-
Displace -
DropShadows:-
DropShadow -
InnerShadow
-
-
Blurs:-
FastBlur -
GaussianBlur -
MaskedBlur -
RecursiveBlur
-
-
MotionBlurs:-
DirectionalBlur -
RadialBlur -
ZoomBlur
-
-
Glows:-
Glow -
RectangularGlow
-
-
Masks:-
OpacityMask -
ThresholdMask
-
现在,让我们继续探讨 DropShadow 作为最有用的效果之一是如何工作的。
DropShadow
DropShadow 效果是一种可以使事物突出并看起来更有生命力的效果。它的用途在于,它将为原本平面的物体增加深度。
我们可以在上一个例子中的 Text 项上添加一个 DropShadow 效果。horizontalOffset 和 verticalOffset 属性描述了阴影在场景中的感知位置。radius 属性描述了阴影的焦点,而 samples 属性决定了模糊时每像素的样本数。
使用以下代码添加 DropShadow 并将其应用于 Text 组件:
Text {
id: textLabel
text: "Hands-On Mobile and Embedded"
color: "purple"
font.pointSize: 20
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
}
DropShadow {
anchors.fill: textLabel
horizontalOffset: 2
verticalOffset: 2
radius: 10
samples: 25
color: "white"
source: textLabel
}
源代码可以在 Git 仓库的 Chapter03-2 目录下的 cp3 分支中找到。
在这里,你可以看到字母现在下面有一个白色阴影:

它还有一个控制阴影锐度的 spread 属性。这仍然有点难以阅读,所以让我们试试别的。怎么样,一个 Glow 效果?
Glow
Glow 是一种通过以下代码产生物体周围扩散颜色的效果:
Glow {
anchors.fill: textLabel
radius: 10
samples: 25
color: "lightblue"
source: textLabel
}
效果在下面的屏幕截图中显示。注意漂亮的浅蓝色发光效果:

现在更像了!我们甚至可以给 Glow 效果添加自己的阴影!将 DropShadow、anchors.fill 和 source 属性更改为 glow:
DropShadow {
anchors.fill: glow
horizontalOffset: 5
verticalOffset: 5
radius: 10
samples: 25
color: "black"
source: glow
}
让我们把 horizontalOffset 和 verticalOffset 属性也稍微调大一些。
我们现在的横幅看起来是这样的:

DropShadows 对于使某物从场景中脱颖而出非常出色。渐变是另一种要使用的效果。
渐变
梯度可以吸引用户的注意力,将他们引入 UI 界面,并与他们的情感相连接。Qt 图形效果内置了对三种类型的梯度支持——锥形、线性和径向。
径向梯度,或者更确切地说,任何 QML 梯度,由一系列GradientStop项目组成,这些项目指定了颜色以及在梯度周期中从哪里开始,数字零代表开始点,而一代表终点。
下面是表示径向梯度的代码:
Item {
width: 250; height: 250
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
RadialGradient {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "red" }
GradientStop { position: 0.3; color: "green" }
GradientStop { position: 0.6; color: "purple" }
}
}
}
源代码可以在 Git 仓库的Chapter03-4目录下的cp3分支中找到。
以下是我们径向梯度的图示:

这个径向梯度使用三个GradientStop项目来告诉梯度某种颜色应该从哪里开始。position属性是一个从0.0到1.0的qreal值;尽管大于1.0的数字不会产生错误,但它将简单地不会绘制在边界项中。
使用与径向梯度相同的颜色停止方案,我们看到线性梯度和锥形梯度的外观。
以下是对线性梯度的表示:

以下是对锥形梯度的表示:

您可以看到这些梯度之间的差异。
模糊
模糊效果可以帮助降低或强调静态图像。最快的模糊效果是名为FastBlur的效果,但高斯模糊效果质量最高,因此渲染速度最慢。
所有的模糊效果都有radius、samples和source属性。Radius代表影响模糊效果的像素距离,数值越大,模糊效果越强。samples属性代表应用效果时每个像素使用的样本数。数值越大意味着质量越好,但渲染时间会变慢。Source是应用模糊效果的目标项。
Displace是一种模糊效果,但具有更多可能的水印效果。displacementSource属性是放置在源项上的项。displacement属性是一个介于-1.0 和 1.0 之间的qreal值,其中 0 表示没有像素位移。
摘要
Qt Quick 提供了非常容易开始使用的图形和特殊效果。特别是粒子,非常适合游戏应用。您现在知道如何使用ParticleSystem通过AngleDirection在特定角度发射ImageParticle。我们探讨了Affectors(如Turbulence)如何通过向粒子流添加变化来影响Emitter。
梯度、发光和阴影对于强调某些项目非常有用。模糊效果用于模拟运动动作或向图像添加水印。
在下一章中,我们将深入探讨在手机上现在无处不在的功能——触摸输入。我还会涉及到(字面意义上的“触及”)使用其他形式的输入,例如当没有硬件键盘时,你的应用需要启动的情况。
第四章:输入和触摸
并非所有设备都配备了现成的键盘。使用触摸屏设备,用户可以轻松使用按钮和其他 用户界面 (UI) 功能。当没有键盘或鼠标时,例如在展台或交互式标牌上,您该怎么办?虚拟键盘和触摸交互定义了当今的移动和嵌入式应用程序。
在本章中,我们将涵盖以下主题:
-
我们将发现 Qt 的图形解决方案来整合用户输入。
-
将检查参考 Qt 虚拟键盘。
-
我们将演示触摸组件,例如
TouchPoints、Flickable和PinchArea。
没有键盘时怎么办
嘿,我的键盘在哪里?
计算机信息亭和汽车通常不配备键盘输入。它们使用虚拟输入,例如虚拟键盘、语音输入,甚至手势识别。
Qt 公司的人创建了一个名为 Qt 虚拟键盘 (QtVK) 的虚拟输入法。它不仅仅是一个屏幕键盘,因为它还具有手写识别功能。它既提供商业许可证,也提供开源的 GPL 版本 3。
有其他虚拟键盘可以与 Qt 应用程序一起使用。在具有触摸屏的台式计算机上,例如二合一笔记本电脑,系统可能已经内置了虚拟键盘。这些应该作为 Qt 应用的输入方法,尽管它们可能在用户想要在文本区域输入时自动弹出或不弹出。
有两种方式集成 Qt 的虚拟键盘:
| 桌面系统 | 完全集成到应用程序中 |
|---|---|
| 应用程序 | Qt Widget 应用程序:设置环境变量 QT_IM_MODULE=qtvirtualkeyboardQt Quick:在您的应用程序中使用 InputPanel |
我在这里有一个用于 Boot to Qt 的 Raspberry Pi 设置,它完全集成到 Qt Creator 中,因此我可以在 Qt Creator 中构建和运行 Raspberry Pi 上的 Qt 应用程序。您也可以从 git://code.qt.io/qt/qtvirtualkeyboard.git 获取源代码并自行构建。
要构建 QtVK,请下载以下源代码:
git clone git://code.qt.io/qt/qtvirtualkeyboard.git
可以通过 qmake 配置 QtVK 构建,使用 CONFIG+=<配置> 和以下配置选项:
-
lang <code>-
language_country 的形式-
语言为小写,为两个字母的语言代码
-
国家名称为大写,为两个字母的国家代码
-
-
-
lang-all -
handwriting- 处理自定义引擎
-
箭头键导航
例如,要仅配置澳大利亚英语并添加手写支持,您将运行 qmake CONFIG+=lang-en_AU CONFIG+=handwriting 然后执行 make && make install。
有许多其他配置可用。您可以通过禁用布局来创建自定义布局和桌面集成,以及其他配置。
QtVK 可以在 C++ 或 QML 中使用。让我们从使用 Qt Quick 开始,在创建新的 Qt Quick 应用程序模板时,在 Qt Creator 项目向导中勾选“使用 Qt 虚拟键盘”:

这是当你使用 Boot to Qt for Device Creation 时得到的样板代码:
InputPanel {
id: inputPanel
z: 99
x: 0
y: window.height
width: window.width
states: State {
name: "visible"
when: inputPanel.active
PropertyChanges {
target: inputPanel
y: window.height - inputPanel.height
}
}
transitions: Transition {
from: ""
to: "visible"
reversible: true
ParallelAnimation {
NumberAnimation {
properties: "y"
duration: 250
easing.type: Easing.InOutQuad
}
}
}
}
源代码可以在 Git 仓库的Chapter04-1目录下的cp4分支中找到。
让我们添加一些接受文本输入的东西,比如一个TextField元素:
TextField {
anchors {
bottom: inputPanel.top
top: parent.top
right: parent.right
left: parent.left
}
placeholderText: "Enter something"
}
在这里,anchors用于在用户点击TextField时自动打开 QtVK 时调整此TextField元素的大小。
这应该是这样的:

实现触摸屏可以带来许多好处,我们可以通过使用 Qt 事件循环来实现。让我们详细看看如何使用触摸屏作为输入。
使用触摸输入
触摸屏如今无处不在。它们无处不在。虽然在手机或平板电脑中是必不可少的,但你也可以得到带有触摸屏的笔记本电脑或台式电脑。冰箱和汽车也通常配备触摸屏。知道如何在 Qt 应用程序中利用这些也是必不可少的。
在移动手机和平板电脑平台上,触摸屏支持来自系统,通常是内置的。如果你正在创建自己的嵌入式设备,你很可能需要告诉 Qt 如何使用触摸屏。Qt 支持嵌入式设备上的各种触摸屏系统。
QEvent
QEvent是获取 C++中触摸输入事件的方式。它通过你可以添加到应用程序的事件过滤器来实现。有几种不同的方式来访问这些数据。
我们可以使用事件过滤器或事件循环。我们将首先查看事件过滤器。
事件过滤器
你可以通过使用事件过滤器来访问事件循环。首先需要调用以下函数:
qApp->installEventFilter(this);
源代码可以在 Git 仓库的Chapter04-2目录下的cp4分支中找到。
然后你需要重写名为eventFilter(QObject* obj, QEvent* event)的函数,它返回一个bool值:
bool MainWindow::eventFilter(QObject* obj, QEvent* event);
你将接收到任何和所有的事件。你也可以通过以下方式处理这些触摸事件:
-
QEvent::TouchBegin -
QEvent::TouchCancel -
QEvent::TouchEnd -
QEvent::TouchUpdate
在eventFilter中使用switch语句是遍历不同选项的有效方式:
bool MainWindow::eventFilter(QObject* obj, QEvent* event)
{
switch(event->type()) {
case QEvent::TouchBegin:
case QEvent::TouchCancel:
case QEvent::TouchEnd:
case QEvent::TouchUpdate:
qWarning("Touch event %d", event->type());
break;
default:
break;
};
return false;
}
除非你需要拦截它们,否则请确保将这些事件传递给父类。要阻止传递,请返回true。使用事件循环是访问事件的另一种方式。让我们看看。
事件循环
要使用事件循环,你需要重写event(QEvent *ev):
bool MainWindow::event(QEvent *ev)
{
switch (ev->type()) {
case QEvent::TouchBegin:
qWarning("TouchBegin event %d", ev->type());
break;
case QEvent::TouchEnd:
qWarning("TouchEnd event %d", ev->type());
break;
case QEvent::TouchUpdate:
qWarning("TouchUpdate event %d", ev->type());
break;
};}
你还需要在类构造函数中添加setAttribute(Qt::WA_AcceptTouchEvents, true);,否则你的应用程序将不会接收到触摸事件。
让我们看看 Qt 如何处理触摸屏支持以及如何使用 Qt 访问触摸屏输入堆栈的更低级别。
触摸屏支持
Qt 对触摸屏的支持是通过Qt 平台抽象(QPA)平台插件实现的。
Qt 配置将自动检测正确的平台并确定是否已安装开发文件。如果找到开发文件,它将使用它们。
让我们看看各种操作系统中的触摸屏是如何工作的,从移动电话开始。
Windows、iOS 和 Android
在 Windows、iOS 和 Android 上,触摸屏通过 Qt 事件系统得到支持。
使用 Qt 事件系统并允许平台插件进行扫描和读取,如果我们需要访问这些事件,我们可以使用 QEvent。
让我们看看如何使用 Qt 在嵌入式 Linux 上访问输入系统的底层。
Linux
在 Linux 操作系统中,有各种可以使用 Qt 的输入系统。
Qt 内置了对这些类型的触摸屏接口的支持:
-
evdev: 一个事件设备接口 -
libinput: 一个用于处理输入设备的库 -
tslib: 一个 TypeScript 运行时库
我们将首先学习关于 Linux evdev 系统的知识,以便直接读取设备文件。
evdev
Qt 内置了对 Linux 和嵌入式 Linux 的 evdev 标准事件处理系统的支持。如果没有配置或检测到其他系统,您将默认获得这个系统。它处理键盘、鼠标和触摸。然后您可以使用 Qt 正常处理键盘、触摸和鼠标事件。
您可以分配启动参数,例如设备文件路径和屏幕默认旋转,如下所示:
QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/input2:rotate=90
可用的其他参数包括 invertx 和 inverty。当然,您不需要依赖于 Qt 来处理这些输入事件,可以直接在 Qt 以下的堆栈中访问它们。我称它们为原始事件,但实际上它们只是读取特殊的 Linux 内核设备文件。
让我们看看在使用 Qt 时如何自己处理这些 evdev 输入事件。这是低级系统文件访问,因此您可能需要 root 或管理员权限来运行使用这种方式的应用程序。
在 Linux 上,可以通过内核的 dev 节点访问输入事件,通常位于 /dev/input,但它们可能位于 /dev 目录树下的任何位置,具体取决于驱动程序。QFile 不应用于实际读取这些特殊的设备节点文件。
QFile 不适合读取 Unix 设备节点文件。这是因为 QFile 没有信号,而设备节点文件报告的大小为零,并且只有当您读取它们时才有数据。
读取输入节点的主体 include 文件如下:
#include <linux/input.h>
源代码可以在 Git 仓库的 cp4 分支下的 Chapter04-3 目录中找到。
您将想要扫描设备文件以检测触摸屏产生的哪个文件。在 Linux 中,这些设备节点是动态命名的,因此您需要使用其他方法来识别正确的文件,而不仅仅是文件名。因此,您必须打开文件并要求它告诉您其名称。
我们可以使用 QDir 和其过滤器至少过滤掉一些我们知道不是我们想要的文件:
QDir inputDir = QDir("/dev/input");
QStringList filters;
filters << "event*";
QStringList eventFiles = inputDir.entryList(filters,
QDir::System);
int fd = -1;
char name[256];
for (QString file : eventFiles) {
file.prepend(inputDir.absolutePath());
fd = ::open(file.toLocal8Bit().constData(), O_RDONLY|O_NONBLOCK);
if (fd >= 0) {
ioctl(fd, EVIOCGNAME(sizeof(name)), name);
::close(fd);
}
}
在 open 时务必包含 O_NONBLOCK 参数。
到目前为止,我们已经有了一个不同输入设备名称的列表。你可能只需要猜测使用哪个名称,然后进行 String 比较,以找到正确的设备。有时,驱动程序将具有正确的 id 信息,可以使用 EVIOCGID 获取,如下所示:
unsigned short id[4];
ioctl(fd, EVIOCGID, &id);
有时,你可以使用 EVIOCGBIT 检测某些功能。这将告诉我们硬件驱动程序支持哪些按钮或键。当触摸触摸屏时,触摸屏驱动程序输出键码 0x14a (BTN_TOUCH),因此我们可以使用这个来检测哪个输入事件将是我们的触摸屏:
bool MainWindow::isTouchDevice(int fd)
{
unsigned short id[4];
long bitsKey[LONG_FIELD_SIZE(KEY_CNT)];
memset(bitsKey, 0, sizeof(bitsKey));
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(bitsKey)), bitsKey);
if (testBit(BTN_TOUCH, bitsKey)) {
return true;
}
return false;
}
现在,我们可以相当确信我们已经有了正确的设备文件。现在,我们可以设置一个 QSocketNotifier 对象来通知我们该文件何时被激活,然后我们可以读取它以获取触摸的 X 和 Y 值。我们使用 QSocketNotifier 类,因为我们不能使用 QFile,因为它没有任何信号来告诉我们 Linux 设备文件何时更改,这使得它更容易:
int MainWindow::doScan(int fd)
{
QSocketNotifier *notifier
= new QSocketNotifier(fd, QSocketNotifier::Read,
this);
auto c = connect(notifier, &QSocketNotifier::activated,
= {
struct input_event ev;
unsigned int size;
size = read(fd, &ev, sizeof(struct input_event));
if (size < sizeof(struct input_event)) {
qWarning("expected %u bytes, got %u\n", sizeof(struct
input_event), size);
perror("\nerror reading");
return EXIT_FAILURE;
}
if (ev.type == EV_KEY && ev.code == BTN_TOUCH)
qWarning("Touchscreen value: %i\n", ev.value);
if (ev.type == EV_ABS && ev.code == ABS_MT_POSITION_X)
qWarning("X value: %i\n", ev.value);
if (ev.type == EV_ABS && ev.code == ABS_MT_POSITION_Y)
qWarning("Y value: %i\n", ev.value);
return 0;
});
return true;
}
我们也使用标准的 read() 函数而不是 QFile 来读取这个。
BTN_TOUCH 事件值告诉我们触摸屏何时被按下或释放。
ABS_MT_POSITION_X 值将是触摸屏的 X 位置,而 ABS_MT_POSITION_Y 值将是 Y 位置。
有一个库可以用来做同样的事情,可能稍微容易一些。
libevdev
当你使用库 libevdev 时,你不需要访问像 QSocketNotifier 这样的低级文件系统函数,也不需要自己读取文件。
要使用 libevdev,我们首先在我们的项目 .pro 文件中添加到 LIBS 条目。
LIBS += -levdev
源代码可以在 Git 仓库的 cp4 分支下的 Chapter04-4 目录中找到。
这允许 qmake 设置正确的链接器参数。include 头文件如下所示:
#include <libevdev-1.0/libevdev/libevdev.h>
我们可以从前面的代码中借用扫描目录以查找设备文件的初始代码,但 isTouchDevice 函数的代码会更简洁:
bool MainWindow::isTouchDevice(int fd)
{
int rc = 1;
rc = libevdev_new_from_fd(fd, &dev);
if (rc < 0) {
qWarning("Failed to init libevdev (%s)\n", strerror(-rc));
return false;
}
if (libevdev_has_event_code(dev, EV_KEY, BTN_TOUCH)) {
qWarning("Device: %s\n", libevdev_get_name(dev));
return true;
}
libevdev_free(dev);
return false;
}
Libevdev 有一个很好的 libevdev_has_event_code 函数,可以用来轻松检测设备是否具有某个事件代码。这正是我们需要用来识别触摸屏的!注意 libevdev_free 函数,它将释放我们不需要使用的内存。
doScan 函数去掉了读取的调用,而是用 libevdev_next_event 的调用代替。它还可以通过调用 libevdev_event_code_get_name 输出有关实际事件代码的漂亮消息:
int MainWindow::doScan(int fd)
{
QSocketNotifier *notifier
= new QSocketNotifier(fd, QSocketNotifier::Read,
this);
auto c = connect(notifier, &QSocketNotifier::activated,
= {
int rc = -1;
do { struct input_event ev;
rc = libevdev_next_event(dev,
LIBEVDEV_READ_FLAG_NORMAL, &ev);
if (rc == LIBEVDEV_READ_STATUS_SYNC) {
while (rc == LIBEVDEV_READ_STATUS_SYNC) {
rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_SYNC, &ev);
}
} else if (rc == LIBEVDEV_READ_STATUS_SUCCESS) {
if ((ev.type == EV_KEY && ev.code == BTN_TOUCH) ||
(ev.type == EV_ABS && ev.code ==
ABS_MT_POSITION_X) ||
(ev.type == EV_ABS && ev.code ==
ABS_MT_POSITION_Y)) {
qWarning("%s value: %i\n",
libevdev_event_code_get_name(ev.type, ev.code), ev.value);
}
}
} while (rc == 1 || rc == 0 || rc == -EAGAIN);
return 0;
});
return 0;
}
库 libinput 也使用 evdev,并且比其他库更新一些。
libinput
libinput 库是 Wayland 合成器和 X.Org 窗口系统的输入处理。Wayland 是一种显示服务器协议,类似于古老 Unix 标准的 X11 的新版本。Libinput 依赖于 libudev 并支持以下输入类型:
-
Keyboard: 标准硬件键盘 -
Gesture: 触摸手势 -
Pointer: 鼠标事件 -
Touch: 触摸屏事件 -
Switch: 笔记本电脑盖子开关事件 -
Tablet: 平板工具事件 -
Tablet pad:平板垫事件
libinput 库在构建时依赖于 libudev;因此,为了配置 Qt,你需要安装 libudev 以及 libinput 的开发文件或包。如果你需要硬件键盘支持,还需要安装 xcbcommon 包。
另一个触摸屏库是 tslib,它专门用于嵌入式设备,因为它具有小的文件系统占用和最小的依赖。
Tslib
Tslib 是一个用于访问和过滤 Linux 设备上触摸屏事件的库;它支持多点触控,Qt 也支持使用它。你需要安装 tslib 的开发文件。Qt 会自动检测这一点,或者你可以使用以下方式显式配置 Qt:
configure -qt-mouse-tslib
可以通过将环境变量 QT_QPA_EGLFS_TSLIB 或 QT_QPA_FB_TSLIB 设置为 1 来启用它。你可以通过将环境变量 TSLIB_TSDEVICE 设置为设备节点路径来更改实际的设备文件路径,如下所示:
export TSLIB_TSDEVICE=/dev/input/event4
现在我们来探讨如何使用 Qt 的高级 API 来利用触摸屏。
使用触摸屏
Qt 后端使用触摸屏有两种方式。事件以鼠标的形式传入,使用 click 和 drag 事件,或者作为多点触摸到处理的手势,例如 pinch 和 swipe。让我们更好地了解多点触摸。
MultiPointTouchArea
如我之前所述,要在 QML 中使用多点触摸屏,有 MultiPointTouchArea 类型。如果你想在 QML 中使用手势,你或者必须使用 MultiPointTouchArea 并自己处理,或者在你的 C++ 中使用 QGesture 并在 QML 组件中处理自定义信号。
源代码可以在 Git 仓库的 cp4 分支下的 /Chapter04-5 目录中找到。
MultiPointTouchArea {
anchors.fill: parent
touchPoints: [
TouchPoint { id: finger1 },
TouchPoint { id: finger2 },
TouchPoint { id: finger3 },
TouchPoint { id: finger4 },
TouchPoint { id: finger5 }
]
}
你使用 TouchPoint 元素声明 MultiPointTouchArea 的 touchPoints 属性,为每个你想要处理的手指声明一个元素。在这里,我们使用五指点。
你可以使用 x 和 y 属性来移动对象:
Rectangle {
width: 30; height: 30
color: "green"
radius: 50
x: finger1.x
y: finger1.y
}
你也可以在你的应用程序中使用触摸屏手势。
Qt 手势
手势是利用用户输入的绝佳方式。正如我在 第二章 中提到的,使用 Qt Quick 的流畅 UI,我将在下面提到 C++ API,它比 QML 中的手势功能更丰富。请记住,这些是触摸屏手势,而不是设备或传感器手势,这些我将在后面的章节中探讨。QGesture 支持以下内置手势:
-
QPanGesture -
QPinchGesture -
QSwipeGesture -
QTapGesture -
QTapAndHoldGesture
QGesture 是一个基于事件的 API,所以它将通过事件过滤器传递,这意味着你需要重新实现你的 event(QEvent *event) 小部件,因为手势将针对你的小部件。它还支持通过子类化 QGestureRecognizer 并重新实现 recognize 来自定义手势。
要在你的应用中使用手势,你首先需要告诉 Qt 你想要接收触摸事件。如果你使用的是内置手势,这是由 Qt 内部完成的,但如果你有自定义手势,你需要这样做:
setAttribute(Qt::WA_AcceptTouchEvents);
为了接受触摸事件,你需要调用 QGraphicsItem::setAcceptTouchEvent(bool) 并将参数设置为 true。
如果你想要使用未处理的鼠标事件作为触摸事件,你也可以设置 Qt::WA_SynthesizeTouchEventsForUnhandledMouseEvents 属性。
然后,你需要通过调用你的 QWidget 或 QGraphicsObject 类的 grabGesture 函数来告诉 Qt 你想要使用某些手势:
grabGesture(Qt::SwipeGesture);
QGesture 事件被发送到特定的 QWidget 类,而不是像鼠标事件那样发送到当前拥有焦点的 QWidget 类。
在你的 QWidget 派生类中,你需要重新实现 event 函数,并在发生手势事件时处理 gesture 事件:
bool MyWidget::event(QEvent *event)
{
if (event->type() == QEvent::Gesture)
handleSwipe();
return QWidget::event(event);
}
由于我们只处理一种 QGesture 类型,我们知道这是我们目标的手势。你可以通过检查其 pointer 来确定这个事件是否由某种手势引起,该 pointer 是通过以下定义的 gesture 函数来检查的:
QGesture * QGestureEvent::gesture(Qt::GestureType type) const
这可以通过以下方式实现:
if (QGesture *swipe = event->gesture(Qt::SwipeGesture))
如果调用 swipe 的 QGesture 对象是 nullptr,那么这个事件不是我们的目标手势。
还是一个好主意去检查手势的 state(),它可以是指以下任何一个:
-
Qt::NoGesture -
Qt::GestureStarted -
Qt::GestureUpdated -
Qt::GestureFinished -
Qt::GestureCanceled
你可以使用 QGestureRecognizer 通过派生 QGestureRecognizer 并重新实现 recognize() 来创建自己的手势。大部分的工作将在这里进行,因为你需要检测你的手势,更有可能检测到不是你的手势。你的 recognize() 函数需要返回 enum 值 QGestureRecognizer::Result 中的一个,它可以是指以下任何一个:
-
QGestureRecognizer::Ignore -
QGestureRecognizer::MayBeGesture -
QGestureRecognizer::TriggerGesture -
QGestureRecognizer::FinishGesture -
QGestureRecognizer::CancelGesture -
QGestureRecognizer::ConsumeEventHint
在这里你需要处理大量的边缘情况,以准确判断哪些是你的手势,哪些不是。如果这个函数复杂或长,不要害怕。
另一种越来越受欢迎的输入形式是使用你的声音。让我们接下来看看这个。
语音作为输入
语音识别和 Qt 已经存在了一段时间。在 2003 年我成为 Qtopia 社区联络员的时候,IBM 的 ViaVoice 被移植到了 KDE,并且正在被移植到 Trolltech 的手机软件套件 Qtopia。因此,它是同一个开发者后来梦想成真成为 Qt Quick 的东西。虽然概念本质上保持不变,但技术已经得到了改进,现在你可以在许多不同的设备中找到语音控制,包括汽车。
有许多竞争系统,例如 Alexa、Google Voice、Cortana 和 Siri,以及一些开源 API。结合语音搜索,语音输入是一种无价工具。
在撰写本文时,Qt 公司及其合作伙伴之一 Integrated Computer Solutions(ICS)已宣布他们正在合作将亚马逊的 Alexa 系统集成到 Qt 中。我被告知这将被称为 QtAlexaAuto 并在 lgpl v3 许可下发布。由于在撰写本文时尚未发布,我无法详细介绍如何使用此实现。我相信,如果或当它发布时,API 将非常易于使用。
亚马逊的 Alexa Voice Service(AVS)软件开发工具包(SDK)支持 Windows、Linux、Android 和 MacOS。您甚至可以使用麦克风阵列组件,例如与 Raspberry Pi 配合使用的 MATRIX creator。 Siri 在 iOS 和 MacOS 上运行。Cortana 在 Windows 上运行。
虽然这些语音系统都没有集成到 Qt 中,但它们可以通过自定义集成来使用。根据您的应用程序将执行的操作和它将在哪种设备上运行,值得对其进行研究。
Alexa、Google Assistant 和 Cortana 提供了 C++ API,Siri 也可以使用其 Objective-C API:
QtAlexaAuto
QtAlexaAuto 是由 Integrated Computer Solutions(ICS)和 Qt 公司共同创建的一个模块,用于在 Qt 和 QtQuick 应用程序中启用亚马逊的 Alexa Voice Service(AVS)的使用。您可以使用 Raspberry Pi、Linux、Android 或其他机器来原型化一个使用语音作为输入的应用程序。
在撰写本书时,QtAlexaAuto 尚未发布,因此您需要在网上搜索下载源代码的 URL。官方发布的内容可能与本书中所述的内容有所不同。
您需要从亚马逊下载、构建和安装以下 SDK:
-
AVS Device SDK: git clone -b v1.9
github.com/alexa/avs-device-sdk -
Alexa Auto SDK: git clone -b 1.2.0
github.com/alexa/aac-sdk
构建这些时,您应遵循此 URL 上的平台说明:
github.com/alexa/avs-device-sdk/wiki
构建 QtAlexaAuto 的基本步骤如下:
-
注册亚马逊开发者账户,并注册一个产品。
developer.amazon.com/docs/alexa-voice-service/register-a-product.html -
添加你的 clientID 和 productID
-
安装 Alexa wiki 中提到的需求
-
按照 install_aac_sdk.md 中的详细说明应用补丁。
-
构建并安装 AVS 设备 SDK
-
构建并安装 Alexa Auto SDK
-
编辑 AlexaSamplerConfig.json
-
构建 QtAlexaAuto
你需要应用几个补丁。幸运的是,install_aac_sdk.md 中有说明,指导你如何将补丁从 aac-sdk 应用到 avs-device-sdk。
需要编辑并重命名文件 AlexaSamplerConfig.json 为 AlexaClientSDKConfig_new_version_linux.json
然后将文件放入你从其中运行示例的目录中。
QtAlexaAuto 的主要 QML 组件名为 alexaApp,它对应于 QtAlexaAuto 类。
当你运行示例应用程序时,你需要登录到你的亚马逊开发者账户,并通过输入应用程序在首次启动时给出的代码来关联此应用程序。这些可以通过调用 acctLinkUrl() 和 acctLinkCode() 或在 QML 中通过名为 accountLinkCode 和 accountLinkUrl 的 alexaApp 属性提供给用户。
一旦此功能与账户关联,你就可以通过点击按钮使用语音输入和 Alexa 语音服务。
当用户按下说话按钮时运行的函数是 tapToTalk(),并且会发出 startTapToTalk 信号。

AVS 有一个 RenderTemplate 的概念,它从服务传递过来,这样应用程序就能向用户展示关于响应的视觉信息。QtAlexaAuto 处理天气、媒体播放器模板以及一些通用多用途模板。RenderTemplate 以 JSON 文档的形式发出,并在示例应用程序中使用 QML 组件显示,然后解析并显示数据。
这只是对 QtAlexaAuto 的快速浏览,因为我没有足够的时间在本书出版前真正深入研究这个新 API。
摘要
用户输入很重要,用户与应用程序交互的方式有很多。如果你正在创建自己的嵌入式设备,你需要决定使用哪些输入方法。触摸屏可以提高可用性,因为触摸东西是非常自然的事情。婴儿甚至猫都可以使用触摸屏设备!手势是使用触摸输入的绝佳方式,你甚至可以为你的应用程序开发自定义手势。语音输入现在正在兴起。虽然添加对其的支持可能需要一点工作,但在一些需要免提使用的设备上,这可能是一件正确的事情。
在下一章中,我们将学习网络及其功能。
第二部分:网络、连接、传感器和自动化
在本节中,你将了解如何使用额外功能和移动 API 来扩展应用程序的功能,使其超越其设备。本节将讨论从远程传感器读取数据以及使用 API 与机器进行通信。将涵盖QNetworkReply、QNetworkRequest、QDnsLookup、QHostInfo、QLocalServer和QTcpSocket API。我们还将讨论QNetworkSession和QNetworkConfiguration,它们用于查看附近的可用 Wi-Fi 网络。
本节包括以下章节:
-
第五章,Qt 网络通信
-
第六章,使用 Qt 蓝牙 LE 进行连接
-
第七章,机器对话
-
第八章,我在哪里?定位与定位
第五章:Qt 网络用于通信
网络对于移动设备来说几乎和移动设备本身一样重要。没有网络,数据就必须从物理上一个地方移动到另一个地方。幸运的是,Qt 在QNetwork中提供了广泛的网络功能。在本章中,我们将讨论以下 API:
-
QNetworkReply -
QNetworkRequest -
QDnsLookup -
QHostInfo -
QLocalServer -
QTcpSocket
要显示附近的可用 Wi-Fi 网络,我们还将介绍以下内容:
-
QNetworkSession -
QNetworkConfiguration
你还将学习如何使用 Qt API 进行标准网络任务,例如域名服务(DNS)查找、下载和上传文件,以及如何使用 Qt 的套接字类进行通信。
高级 – 请求、回复和访问
Qt 中的网络功能非常丰富。Qt Quick 中的网络比 Qt 更隐蔽。在Qt 建模语言(QML)中,你可以下载远程组件并在你的应用程序中使用它们,但任何其他任意下载或网络功能你将不得不在 C++后端中实现或使用 JavaScript。
尽管QNetworkRequest、QNetworkReply和QNetworkAccessManager都用于制作网络请求,但让我们分开来看如何使用它们。
QNetworkRequest
QNetworkRequest是访问功能的一部分。它构建一个request,可以是以下动词之一:
-
GET:get(...) -
POST:post(...) -
PUT:put(...) -
DELETE:deleteResource(...) -
HEAD:head(...)
你还可以使用sendCustomRequest发送自定义动词,它接受自定义动词作为QByteArray参数。
可以使用setHeader将头设置为已知头,可以是以下之一:
-
ContentDispositionHeader -
ContentTypeHeader -
ContentLengthHeader -
LocationHeader -
LastModifiedHeader -
CookieHeader -
SetCookieHeader -
UserAgentHeader -
ServerHeader
可以使用setRawHeader设置原始或自定义头。HTTP 属性可以帮助控制请求缓存、重定向和 cookies。它们可以用setAttribute设置。
让我们把这段代码放到以下代码中。
源代码可以在Chapter05-1目录下的cp5分支中的 Git 仓库中找到。
要使用网络模块,在.pro项目中,将network添加到QT变量中,如下所示:
QT += network
我们现在可以使用 Qt 网络。
QNetworkRequest是需要用于从网络请求操作的部分,例如get和put。
一个简单的实现如下:
QNetworkRequest request;
request.setUrl(QUrl("http://www.example.com"));
QNetworkRequest也可以将QUrl作为其参数。QNetworkRequest不是基于QObject的,因此它没有父对象,也没有任何自己的信号。所有通信都是通过QNetworkAccessManager完成的。
你想要连接的一个信号是finished信号。
假设我有一些需要传输的表单数据;我需要使用setHeader添加一个标准头。我还可以添加以下自定义头,我称之为X-UUID:
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setRawHeader(QByteArray("X-UUID"), QUuid::createUuid().toByteArray());
现在我们有一个有效的QNetworkRequest,我们需要将其发送到QNetworkAccessManager。让我们看看我们如何做到这一点。
QNetworkAccessManager
引入管理器——QNetworkAccessManager(QNAM)。它用于通过网络发送和接收异步请求。通常,一个应用程序中只有一个 QNAM 实例,如下所示:
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
在最简单的情况下,你可以使用get、put、post、deleteResource或head函数来创建一个 QNAM 请求。
QNAM 使用信号来传输数据和请求信息,而finished()信号用于表示请求已完成。
让我们为它添加一个信号处理程序,如下所示:
connect(manager, &QNetworkAccessManager::finished,
this, &MainWindow::replyFinished);
这将调用你的replyFinished槽,其中包含QNetworkReply参数中的数据和头信息,如下所示:
void MainWindow::replyFinished(QNetworkReply *reply)
{
if (reply->error())
ui->textEdit->insertPlainText( reply->errorString());
else {
QList<QByteArray> headerList = reply->rawHeaderList();
ui->textEdit->insertPlainText(headerList.join("\n") +"\n");
QByteArray responsData = reply->readAll();
ui->textEdit->insertHtml(responsData);
}
}
然后,按照以下方式在QNetworkAccessManager上调用get方法:
manager->get(request);
下载东西就这么简单!QNAM 将施展其魔法并下载 URL。
创建文件上传也是同样简单的方法。当然,你的 Web 服务器需要支持put方法,如下所示:
QFileDialog dialog(this);
dialog.setFileMode(QFileDialog::AnyFile);
QString filename = QFileDialog::getOpenFileName(this, tr("Open File"), QDir::homePath());
if (!filename.isEmpty()) {
QFile file(filename);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QByteArray fileBytes = file.readAll();
manager->put(request, fileBytes);
}
}
源代码可以在 Git 仓库的Chapter05-2目录下的cp5分支中找到。
如果你需要在 URL 中发送一些查询参数,你可以使用QUrlQuery来构建form查询数据,然后按照以下方式发送request:
QNetworkRequest request;
QUrl url("http://www.example.com");
QUrlQuery formData;
formData.addQueryItem("login", "me");
formData.addQueryItem("password", "123");
formData.addQueryItem("submit", "Send");
url.setQuery(formData);
request.setUrl(url);
manager->get(request);
可以使用post函数将表单数据作为QByteArray上传,如下所示:
QByteArray postData;
postData.append("?login=me&password=123&submit=Send");
manager->post(request, postData);
要发送多部分表单数据,例如表单数据和图片,你可以使用QHttpMultiPart如下所示:
QFile *file = new QFile(filename);
if (file->open(QIODevice::ReadOnly)) {
QByteArray fileBytes = file->readAll();
QHttpMultiPart *multiPart =
new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart textPart;
textPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"filename\""));
textPart.setBody(filename.toLocal8Bit());
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\""));
filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/zip"));
filePart.setBodyDevice(file);
file->setParent(multiPart);
multiPart->append(textPart);
multiPart->append(filePart);
manager->put(request, multiPart);
}
当然,这些示例都没有跟踪回复。QNetworkReply是QNetworkAccessManager的get、post和put方法的返回值,可以用来跟踪下载或上传进度或是否有错误。
QNetworkReply
对 QNAM 的get、post等所有调用都将返回QNetworkReply。
你需要删除这个指针,否则它将导致内存泄漏,但不要在finished信号处理程序中删除它。你可以使用deleteLater()。
QNetworkReply有一个有趣的信号,我们很可能需要处理。让我们从两个最重要的信号开始——error和readyRead。
因此,让我们正确地处理那个QNetworkReply。由于我们事先没有有效的对象,我们需要在网络请求操作之后连接信号。这对我来说似乎有点反直觉,但这是必须这样做的方式,并且它有效。代码如下:
QNetworkReply *networkReply = manager->get(request);
connect(networkReply, SIGNAL(finished()), this, SLOT(requestFinished()));
connect(networkReply, SIGNAL(error(QNetworkReply::NetworkError)),
this,SLOT(networkReplyError(QNetworkReply::NetworkError)));
connect(networkReply, SIGNAL(readyRead()), this, SLOT(readyRead()));
我正在使用传统的信号连接方式,但你也可以并应该编写如下所示的连接,因为它允许编译时检查语法和其他错误:
connect(networkReply, &QNetworkReply::error, this, &MyClass::networkReplyError);
connect(networkReply, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error),this, &MyClass::networkReplyError);
connect(networkReply, &QNetworkReply::finished, this, &MyClass::requestFinished);
connect(networkReply, &QNetworkReply::readyRead, this, &MyClass::readyRead);
因此,我们现在已经发送了一个请求,正在等待服务器的回复。让我们逐一查看信号处理程序。
当出现错误并带有错误代码作为参数时,会发出 error(QNetworkReply::NetworkError)。如果您需要一个用户友好的字符串,可以使用 QNetworkReply::errorString() 获取。当请求完成时,会发出 finished()。回复仍然打开,因此您可以在这里读取它:readyRead()。由于回复是从 QIODevice 派生的,它具有 readyRead 信号,该信号在可以读取更多数据时发出。
在大文件下载时,您可能想要监控下载进度,这是一件常见的事情。通常,每个下载都有一个进度条。QNetworkReply 会发出 downloadProgress(qint64 bytesReceived, qint64 bytesTotal) 信号,如下所示:
connect(networkReply, &QNetworkReply::downloadProgress, this, &MyClass::onDownloadProgress);
对于上传,存在相应的 uploadProgress。
当下载需要身份验证时,会发出 preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator)。QSslPreSharedKeyAuthenticator 对象应加载预共享密钥和其他详细信息以验证用户。
当 安全套接字层 (SSL) 遇到问题时,会调用 sslErrors(const QList<QSslError> &errors) 信号,包括证书验证错误。
QNetworkManager 也可以执行简单的 文件传输协议 (FTP) 转发。
QFtp
使用 Qt 进行 FTP 有两种方式。QNetworkAccessManager 提供简单的 FTP get 和 put 支持,我们可以轻松使用它。
FTP 服务器通常需要某种类型的用户名和密码。我们使用 QUrl 的 setUserName() 和 setPassword() 来设置这些,如下所示:
QUrl url("ftp://llornkcor.com/");
url.setUserName("guest@llornkcor.com");
url.setPassword("handsonmobileandembedded");
源代码可以在 Git 仓库的 Chapter05-5 目录下的 cp5 分支中找到。
一旦我们知道文件名,我们需要将其添加到 url 中,因为它将使用此信息写入失败,如下所示:
url.setPath(QFileInfo(file).fileName());
然后,设置请求 url,如下所示:
request.setUrl(url);
一旦我们在 QNAM 上调用 put,我们就可以将槽连接到 QNetworkReply 信号。
QNetworkReply *networkReply = manager->put(request, fileBytes);
connect(networkReply, &QNetworkReply::downloadProgress,
this, &MainWindow::onDownloadProgress);
connect(networkReply, &QNetworkReply::downloadProgress,
this, &MainWindow::onUploadProgress);
不要忘记 error 信号需要 QOverload,如下所示:
connect(networkReply, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), ={
qDebug() << Q_FUNC_INFO << code << networkReply->errorString(); });
connect(networkReply, &QNetworkReply::finished,
this, &MainWindow::requestFinished);
如果您需要执行除 get 和 put 之外更复杂的事情,您将需要使用除 QNetworkAccessManager 之外的其他东西。
QFtp 不包含在 Qt 中,但您可以使用从 Qt 4 移植的独立 QFtp 模块,如下运行与 Qt 5:
git clone -b 5.12 git://code.qt.io/qt/qtftp.git
我们需要构建 QFtp,因此可以在 Qt Creator 中打开 qtftp.pro。运行构建并安装它。
使用命令行,命令如下:
cd qtftp
qmake
make
make install
我们需要将其安装到 Qt 5.12 中,因此请在 Qt Creator 中导航到 Projects | Build | Build Steps 并选择 Add Build Step | Make。在参数字段中,键入 install。
构建此文件,它也会安装。
在项目的 .pro 文件中,为了告诉 qmake 使用 network 和 ftp 模块,请添加以下内容:
QT += network ftp
QFtp 的工作方式非常典型;登录,执行操作,然后登出,如下所示:
connect(ftp, SIGNAL(commandFinished(int,bool)),
this, SLOT(qftpCommandFinished(int,bool)));
connect(ftp, SIGNAL(stateChanged(int)),
this, SLOT(stateChanged(int)));
connect(ftp, SIGNAL(dataTransferProgress(qint64,qint64)),
this, SLOT(qftpDataTransferProgress(qint64,qint64)));
QUrl url(URL);
ftp->connectToHost(url.host(), 21);
ftp->login(USER, PASS);
我们连接到 commandFinished 信号,它可以告诉我们是否发生了错误。
stateChanged 信号将告诉我们何时登录,而 dataTransferProgress 信号将告诉我们何时正在传输字节。
QFtp 支持其他操作,包括以下:
-
list -
cd -
remove -
mkdir -
rmdir -
rename
QNAM 还触及了我最喜欢的 Qt 网络部分——承载管理。让我们继续学习承载管理。
带来好消息的承载管理
承载管理旨在方便用户对网络连接的控制。有 open 和 close 函数用于找到的连接。它不做的一件事是实际配置这些连接。它们必须已经由系统配置。
它还旨在能够将连接分组,以便更容易地在连接之间平滑切换,例如从 Wi-Fi 迁移到移动蜂窝数据,有点像 媒体无关切换(MIH)或 未授权移动接入(UMA)规范。如果您对帮助切换的开源库感兴趣,请查看 SourceForge 上的 Open MIH。
在 Qt 的承载管理最初开发时,Symbian 是最常用的,可以说是最重要的移动操作系统。Symbian 有能力在不停机或丢失数据的情况下,在技术之间无缝迁移连接,有点像手机连接从基站迁移到基站的方式。
苹果似乎称之为 Wi-Fi 助手;三星有自动网络切换。
几年前,移动数据连接非常昂贵,因此一旦发生特定的上传或下载,连接通常会关闭。连接的开启和关闭更加动态,需要自动控制。
无论如何,QtConfigurationManager 将使用系统支持的功能;它不会实现自己的连接数据迁移。
Qt 有以下三个主要类构成了承载管理:
-
QNetworkConfiguration -
QNetworkConfigurationManager -
QNetworkSession
此外,还有 QBearerEngine,它是承载插件的基类。
QNetworkConfiguration
QNetworkConfiguration 表示一个网络连接配置,例如连接到特定接入点的 Wi-Fi,其 服务集标识符(SSID)作为配置名称。
网络配置可以是以下类型之一:
-
QNetworkConfiguration::InternetAccessPoint:- 这种类型是一个典型的接入点,例如 Wi-Fi 接入点(AP)或它可能代表以太网或移动网络。
-
QNetworkConfiguration::ServiceNetwork:ServiceNetwork类型是一组称为 服务网络接入点(SNAP)的接入点。系统将根据成本、速度和可用性等标准确定连接到哪个服务网络最好。QNetworkConfiguration::ServiceNetwork类型的配置也可能在其子QNetworkConfiguration::InternetAccessPoint之间漫游。
-
QNetworkConfiguration::UserChoice:- 此类型可以表示用户首选的配置。它曾被诺基亚的 Maemo 和 Symbian 平台使用,其中系统可以弹出一个对话框询问用户哪个 AP 最好。当前的所有承载后端都不使用这种类型的
QNetworkConfiguration。
- 此类型可以表示用户首选的配置。它曾被诺基亚的 Maemo 和 Symbian 平台使用,其中系统可以弹出一个对话框询问用户哪个 AP 最好。当前的所有承载后端都不使用这种类型的
通常,我们需要知道承载类型,也就是说,连接使用的是哪种通信协议。让我们了解一下BearerType。
QNetworkConfiguration::BearerType
这是一个enum,指定了QNetworkConfiguration的底层技术,可以是以下之一:
-
BearerEthernet -
BearerWLAN -
Bearer2G -
BearerCDMA2000 -
BearerWCDMA -
BearerHSPA -
BearerBluetooth -
BearerWiMAX -
BearerEVDO -
BearerLTE -
Bearer3G -
Bearer4G
这可以通过调用QNetworkConfiguration对象的bearerType()函数来确定,如下所示:
QNetworkConfiguration config;
if (config.bearerType() == QNetworkConfiguration::Bearer4G)
qWarning() << "Config is using 4G";
你可以打开或连接。
QNetworkConfiguration::StateFlags
StateFlags是StateFlag值的 OR'd ||组合,如下所示:
-
Defined: 已知于系统但尚未配置 -
Discovered: 已知并配置,可用于open() -
Active: 当前在线
一个具有Active标志的QNetworkConfiguration也将具有Discovered和Defined标志。你可以通过以下方式检查配置是否处于活动状态:
QNetworkConfiguration config;
if (config.testFlag(QNetworkConfiguration::Active))
qWarning() << "Config is active";
QNetworkConfigurationManager
QNetworkConfigurationManager允许你获取系统的QNetworkConfigurations,如下所示:
QNetworkConfigurationManager manager;
QNetworkConfiguration default = manager.defaultConfiguration();
在使用它之前,总是明智的等待QNetworkConfigurationManager的updateCompleted信号,以确保配置已正确设置。
默认配置是系统定义的默认配置。它可能处于Active状态或只是Discovered状态。
如果你只需要确定系统当前是否在线,manager->isOnline();如果系统被认为是在线的,将返回true。在线是指通过网络连接到另一个设备,这可能或可能不是互联网,可能或可能不是正确路由。因此,它可能是在线的,但不能访问互联网。
你可能需要调用updateConfigurations(),这将要求系统更新配置列表,然后你需要监听updateCompleted信号才能继续。
你可以通过调用allConfigurations()获取系统已知的所有配置,或者通过allConfigurations(QNetworkConfiguration::Discovered);过滤到具有特定状态的配置。
在这种情况下,它返回一个Discovered配置的列表。
你可以通过调用capabilities()来检查系统的能力,它可以是以下之一:
-
CanStartAndStopInterfaces: 系统允许用户启动和停止连接 -
DirectConnectionRouting: 连接路由直接绑定到指定的设备接口 -
SystemSessionSupport: 系统保持连接打开,直到所有会话都关闭 -
ApplicationLevelRoaming: 应用程序可以控制漫游/迁移 -
ForcedRoaming:系统在漫游/迁移时将重新连接 -
DataStatics:系统提供有关已传输和接收的数据的信息 -
NetworkSessionRequired:系统需要会话
QNetworkSession
QNetworkSession提供了一种启动和停止连接以及管理连接会话的方法。在用QNetworkConfiguration实例化QNetworkSession且该配置为ServiceNetwork类型时,它可以提供漫游功能。在大多数系统中,漫游将涉及实际断开连接然后连接新的接口和/或连接。在其他系统中,漫游可以是无缝的,不会干扰用户的流量。
如果QNetworkConfigurationManager的能力报告它支持CanStartAndStopInterfaces,那么您可以使用QNetworkSession来open()(连接)和stop()(关闭)QNetworkConfigurations。
QNAM 在幕后进行网络请求时会使用QNetworkSession。您可以使用QNetworkSession如下监控连接:
源代码可以在 Git 仓库的Chapter05-3目录下的cp5分支中找到。
QNetworkAccessManager manager;
QNetworkConfiguration config = manager.configuration();
QNetworkSession *networkSession = new QNetworkSession(config, this);
connect(networkSession, &QNetworkSession::opened, this, &SomeClass::sessionOpened);
networkSession->open();
要监控从 QNAM 请求接收和发送的字节,连接到bytesReceived和bytesWritten信号,如下所示:
connect(networkSession, &QNetworkSession::bytesReceived, this, &SomeClass::bytesReceived);
connect(networkSession, &QNetworkSession::bytesWritten, this, &SomeClass::bytesWritten);
QNetworkRequest request(QUrl("http://example.com"));
manager->get(request);
漫游
当我提到漫游时,我指的是在 Wi-Fi 和移动数据之间的漫游,而不是在家庭网络之外漫游,这可能是一个非常昂贵的移动数据使用。
为了方便漫游,客户端应用可以连接到preferredConfigurationChanged信号,然后通过调用migrate()开始漫游过程,或者通过调用ignore()取消漫游。迁移连接可能就像暂停下载、断开连接并重新连接到新连接,然后继续下载一样简单。这种方法被称为强制漫游。在某些平台上,它可以将数据流无缝迁移到新连接,类似于手机在通话迁移到另一个基站时的操作。
目前没有支持迁移会话的后端。系统集成商可以实现一个支持真正连接迁移和切换的后端。如果系统允许这样做,那也会有所帮助。
话虽如此,三星的 Android 和 iOS 的漫游功能似乎已经赶上了几年前诺基亚的水平。三星称之为自适应 Wi-Fi,之前被称为智能网络切换。iOS 称之为 Wi-Fi Assist。这些都是在系统级别发生的,允许在 Wi-Fi 和移动数据连接之间漫游。这两个平台都不允许应用程序控制切换。
QBearerEngine
Qt 基于QBearerEngine类提供了以下承载后端插件:
-
Android:Android -
Connman:Linux 桌面和嵌入式,SailfishOS -
Corewlan:Mac OS 和 iOS -
Generic:所有 -
NativeWifi:Windows -
NetworkManager:Linux -
NLA:Windows
根据平台的不同,其中一些与通用后端协同工作。
低级 – 网络套接字和服务器
QTcpSocket 和 QTcpServer 是 Qt 中用于套接字的两个类。它们的工作方式与你的网络浏览器和 WWW 服务器非常相似。它们连接到网络地址主机,而 QLocalSocket 和 QLocalServer 连接到本地文件描述符。
让我们先看看 QLocalServer 和 QLocalSocket。
在套接字服务器编程中,基本步骤如下:
-
创建套接字
-
设置套接字选项
-
绑定套接字地址
-
监听连接
-
接受新连接
Qt 将这些步骤简化为以下步骤:
-
创建套接字
-
监听连接
-
接受新连接
QLocalServer
如果你需要在同一台机器上进行通信,那么 QLocalServer 将比使用基于 TCP 的套接字服务器稍微高效一些。它可以用于 进程间通信 (IPC)。
首先,我们创建服务器,然后使用客户端用于连接的字符串名称调用 listen 函数。我们将连接到 newConnection 信号,这样我们就能知道何时有新的客户端连接。
源代码可以在 Git 仓库的 Chapter05-5a 目录下的 cp5 分支中找到。
当客户端尝试连接时,我们使用 write 函数发送一条小消息,最后使用 flush 发送消息,如下所示:
QLocalServer *localServer = new QLocalServer(this);
localServer->listen("localSocketName");
connect(localServer, &QLocalServer::newConnection, this,
&SomeClass::newLocalConnection);
void SomeClass::newLocalConnection()
{
QLocalSocket *local = localServer->nextPendingConnection();
local->write("Client OK\r\n");
local->flush();
}
这很简单!任何时候你需要向客户端写入,只需使用 nextPendingConnection() 获取下一个 QLocalSocket 对象,并使用 write 发送数据。确保在所有需要发送的行中添加 \r\n,包括最后一行。调用 flush() 不是必需的,但它会立即发送数据。
你可以保留这个对象,以便在需要时发送更多消息。
我们的应用程序现在正在等待并监听连接。让我们接下来做这件事。
QLocalSocket
QLocalSocket 用于与 QLocalServer 通信。你将想要连接到 readyRead 信号。其他信号包括 connected()、disconnected()、error(...) 和 stateChanged(...),如下所示:
源代码可以在 Git 仓库的 Chapter05-5b 目录下的 cp5 分支中找到。
QLocalSocket *lSocket = new QLocalSocket(this);
connect(lSocket, &QLocalSocket::connected, this, &SomeClass::connected);
connect(lSocket, &QLocalSocket::disconnected, this,
&SomeClass::disconnected);
connect(lSocket, &QLocalSocket::error, this, &SomeClass::error);
connect(lSocket, &QLocalSocket::readyRead, this, &SomeClass::readMessage);
void SomeClass::readMessage()
{
if (lSocket->bytesAvailable())
QByteArray msg = lSocket->readAll();
}
如果你需要状态变化,你将连接到 stateChanged,并且会在以下状态变化时收到通知:
-
UnconnectedState -
ConnectingState -
ConnectedState -
ClosingState
现在,我们需要实际连接到服务器,如下所示:
lSocket->connectToHost("localSocketName");
与 QLocalServer 类似,QLocalSocket 使用 write 函数向服务器发送消息,如下所示:
lSocket->write("local socket OK\r\n");
记得添加 行结束符 (EOL) \r\n 来标记数据馈送行的结束。
这就是一个简单的基于本地套接字的通信。现在,让我们看看基于网络的 TCP 套接字。
QTcpServer
QTcpServer API 与QLocalServer非常相似,几乎可以无缝替换,只需进行一些小的更改。最值得注意的是,监听调用的参数略有不同,你需要为QTcpServer指定QHostAddress而不是QString名称和一个端口号。在这里,我使用QHostAddress::Any,这意味着它将在所有网络接口上监听。如果你不关心使用哪个端口,将其设置为0,如下所示:
QTcpServer *tcpServer = new QTcpServer(this);
tcpServer->listen(QHostAddress::Any, 8888);
connect(tcpServer, &QTcpServer::newConnection, this,
&SomeClass::newLocalConnection);
void SomeClass::newLocalConnection()
{
QTcpSocket *tSocket = tcpServer->nextPendingConnection();
tSocket->write("Client OK\r\n");
tSocket->flush();
}
这看起来熟悉吗?QHostAddress可以是 IPv4 或 IPv6 地址。你也可以通过使用QHostAddress::SpecialAddress 枚举来指定不同的地址范围,就像我这样做的一样,它可以有以下之一:
-
LocalHost:127.0.0.1 -
LocalHostIPv6:::1 -
Broadcast:255.255.255.255 -
AnyIPv4:0.0.0.0 -
AnyIPv6::: -
Any:所有 IPv4 和 IPv6 地址
QTcpServer有一个额外的信号acceptError,当新连接的接受阶段发生错误时会被触发。你还可以pauseAccepting()和resumeAccepting()待处理连接队列中的连接接受。
QTcpSocket
QTcpSocket与QLocalSocket类似。除了其他方面之外,QTcpSocket有connectToHost作为连接到服务器的方式,如下所示:
QTcpSocket *tSocket = new QTcpSocket(this);
connect(tSocket, &QTcpSocket::connected, this, &SomeClass::connected);
connect(tSocket, &QTcpSocket::disconnected, this,
&SomeClass::disconnected);
connect(tSocket, &QTcpSocket::error, this, &SomeClass::error);
connect(tSocket, &QTcpSocket::readyRead, this, &SomeClass::readData);
要发送一个简单的HTTP请求,我们可以在连接后向套接字写入,如下所示:
void SomeClass:connected()
{
QString requestLine = QStringLiteral("GET \index.html HTTP/1.1\r\nhost: www.example.com\r\n\r\n");
QByteArray ba;
ba.append(requestLine);
tSocket->write(ba);
tSocket->flush();
}
这将请求服务器上的index.html文件。数据可以在readyRead信号处理程序中读取,如下所示:
void SomeClass::readData()
{
if (tSocket->bytesAvailable())
QByteArray msg = tSocket->readAll();
}
如果你不想使用更同步的方式,你也可以使用waitForConnected、waitForBytesWritten和waitForReadyRead函数,如下所示:
QTcpSocket *tSocket = new QTcpSocket(this);
if (!tSocket->waitForConnected(3000)) {
qWarning() << "Not connected";
return;
}
tSocket->write("GET \index.html HTTP/1.1\r\nhost: www.example.com\r\n\r\n");
tSocket->waitForBytesWritten(1000);
tSocket->waitForReadyRead(3000);
if (tSocket->bytesAvailable())
QByteArray msg = tSocket->readAll();
然后,使用以下命令关闭连接:
tSocket->close();
QSctpServer
SCTP代表流控制传输协议。QSctpServer将消息作为字节数组组发送,就像 UDP 一样,而不是像 TCP 套接字一样发送字节数据流。它还确保数据包的可靠传输,就像 TCP 一样。它可以并行或同时发送多个消息。它是通过使用多个连接来做到这一点的。
QSctpServer也可以通过将setMaximumChannelCount设置为-1来发送字节流,就像 TCP 一样。在创建QSctpServer对象后,你首先想要做的是setMaximumChannelCount。将其设置为0将允许它使用客户端使用的通道数,如下所示:
QSctpServer *sctpServer = new QSctpServer(this);
sctpServer->setMaximumChannelCount(8);
如果你打算使用 TCP 字节流,你可以像QTcpServer一样使用nextPendingConnection()函数来获取一个QTcpSocket对象进行通信。QSctpServer有额外的nextPendingDatagramConnection()来与QSctpSocket通信。
要在newConnection信号处理程序中接收字节,请使用以下代码:
QSctpSocket *sSocket = sctpServer->nextPendingDatagramConnection();
QSctpSocket
QSctpSocket也有对通道数的控制,并且与QSctpServer一样,如果你将最大通道数设置为-1,它将表现得更像 TCP 套接字,发送数据流而不是消息包。消息块被称为datagram。
要读取和写入这些数据报,请使用 readDatagram() 和 writeDatagram()。让我们来检查 QNetworkDatagram。
要构建 QNetworkDatagram,您需要一个包含数据消息的 QByteArray,一个目标 QHostAddress,以及可选的端口号。它可以像以下这样简单:
QNetworkDatagram datagram("Hello Mobile!", QHostAddress("10.0.0.50"), 8888);
sSocket->writeDatagram(datagram);
这将发送 "Hello Mobile!" 消息到相应的服务器。
QUdpSocket
QUdpSocket 发送数据报,例如 QSctpSocket,但它们是不可靠的,这意味着它不会重试发送任何数据报。它也是无连接的,并且对数据长度有 65,536 字节的限制。
设置 QUdpSocket 有两种方式——bind(...) 和 connectToHost(...)。
如果您使用 connectToHost,您可以使用 QIODevice 的 read()、write()、readAll() 方法来发送和接收数据报。使用 bind(...) 方法,您需要使用 readDatagram 和 writeDatagram,如下所示:
QUdpSocket *uSocket = new QUdpSocket(this);
uSocket->bind(QHostAddress::LocalHost, 8888);
connect(uSocket, &QUdpSocket::readyRead, this, &SomeClass::readMessage);
void SomeClass::readMessage()
{
while (udpSocket->hasPendingDatagrams()) {
QNetworkDatagram datagram = uSocket->receiveDatagram();
qWarning() << datagram.data();
}
}
QSslSocket
加密套接字通信可以通过 QSslSocket 处理,它使用 SSL 加密 TCP 连接。当连接安全时,将发出加密信号,如下所示:
QSslSocket *sslSocket = new QSslSocket(this);
connect(sslSocket, &QSslSocket::encrypted, this, SomeClass::socketEncrypted);
sslSocket->connectToHostEncrypted("example.com", 943);
源代码可以在 Git 仓库的 Chapter05-6a 目录下的 cp5 分支中找到。
这将启动连接并立即开始安全的握手过程。一旦握手完成且没有错误,将发出加密信号,连接将准备就绪。
您需要将密钥/证书对添加到 QSslSocket 以利用加密功能。您可以通过使用此网站轻松生成密钥证书失败对进行测试:www.selfsignedcertificate.com/。
由于我们使用的是自签名证书,因此我们需要在错误处理槽中添加 ignoreSslErrors:
sslSocket->ignoreSslErrors();
要添加加密密钥和证书,您需要打开并读取这两个文件,并使用生成的 QByteArrays 创建 QSslKey 和 QSslCertificate:
void MainWindow::initCerts()
{
QByteArray key;
QByteArray cert;
QString keyPath =
QFileDialog::getOpenFileName(0, tr("Open Key File"),
QDir::homePath(),
"Key file (*.key)");
if (!keyPath.isEmpty()) {
QFile keyFile(keyPath);
if (keyFile.open(QIODevice::ReadOnly)) {
key = keyFile.readAll();
keyFile.close();
}
}
QString certPath =
QFileDialog::getOpenFileName(0, tr("Open cert File"),
QDir::homePath(),
"Cert file (*.cert)");
if (!certPath.isEmpty()) {
QFile certFile(certPath);
if (certFile.open(QIODevice::ReadOnly)) {
cert = certFile.readAll();
certFile.close();
}
}
QSslKey sslKey(key, QSsl::Rsa, QSsl::Pem,QSsl::PrivateKey,"localhost");
sslSocket->setPrivateKey(sslKey);
QSslCertificate sslCert(cert);
sslSocket->addCaCertificate(sslCert);
sslSocket->setLocalCertificate(sslCert);
}
当您运行此代码时,您需要使用 QFileDialog 导航并找到源目录中的 localhost.key 和 localhost.cert 文件。
然后,我们使用 setPrivateKey 来设置密钥文件,并使用 addCaCertificate 和 setLocalCertificate 来添加证书。
要从套接字读取,您可以连接到 readReady 信号,就像在 QTcpSocket 中一样。
要向服务器发送的套接字写入,只需使用 write 函数:
sslSocket->write(ui->lineEdit->text().toUtf8() +"\r\n");
然后,您可以使用 QSslSocket 连接到打开 QSslSocket 的 QTcpServer。这带我们到下一步。
QSslServer
好的,没有 QSslServer 类,但由于 QSslSocket 类只是从 QTcpSocket 派生而来,并在其顶部添加了一些额外的 SSL 功能,您可以使用 QSslSocket 的函数创建自己的 SSL 服务器。
您需要生成 SSL 密钥和证书。如果它们是自签名的,同样适用以下规则,我们需要设置以下内容:
server->ignoreSslErrors()
您可以通过继承 QTcpServer 并重写 incomingConnection() 方法来创建一个 SSL 服务器,如下所示。
源代码可以在 Git 仓库的 Chapter05-6 目录下的 cp5 分支中找到。
我们使用 override 函数实现 header 文件,以及一个在服务器进入加密模式时连接的槽:
class MySslServer : public QTcpServer
{
public:
MySslServer();
protected:
void incomingConnection(qintptr handle) override;
private slots:
void socketEncrypted();
};
在 SSL 服务器类的实现中,请注意对 startServerEncryption() 的调用。这将启动 server 通道的加密并创建一个 Server,如下所示:
MySslServer::MySslServer()
{
server = new QSslSocket(this);
initCerts();
}
我们还需要添加加密密钥和证书,因为这与上一节中的 QSslSocket 类似,QSslSocket:
void MySslServer::incomingConnection(qintptr sd)
{
if (server->setSocketDescriptor(sd)) {
addPendingConnection(server);
connect(server, &QSslSocket::encrypted, this, &MySslServer::socketEncrypted);
server->startServerEncryption();
} else {
delete server;
}
}
void MySslServer::socketEncrypted()
{
// entered encrypted mode, time to write secure transmissions
}
在这里,我们连接到 QSslSocket 的 encrypted 信号,该信号在 QSslSocket 进入加密模式时发出。从那时起,所有发送或接收的字节都将被加密。
错误通过连接到 sslErrors(const QList<QSslError> &errors) 信号来处理:
connect(server, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors),
={
for (QSslError error : errors) {
emit messageOutput(error.errorString());
}
});
我们还需要连接到 QAbstractSocket::socketError 信号来处理这些错误:
connect(server, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(error(QAbstractSocket::SocketError)));
你还希望连接的其他信号如下:
-
QSslSocket::connected -
QSslSocket::disconnected -
QSslSocket::encrypted -
QSslSocket::modeChanged -
QSslSocket::stateChanged
到目前为止,我们一直在使用本地 IP 地址,但当服务器是远程的,我们不仅需要服务器名,还需要它的 IP 地址时会发生什么?让我们探索我们如何使用 Qt 来执行域名查找。
查找——查找我
计算机网络,如互联网,依赖于 域名服务 (DNS) 查找。这通常在远程中央服务器上完成,但也可以在本地使用。
有两个类用于执行网络查找——QDnsLookup 和 QHostInfo。QHostInfo 将为主机名提供简单的 IP 地址查找。这实际上只是使用主机名查找 IP 地址。让我们看看我们如何使用它。
QHostInfo
QHostInfo 是平台系统提供的一个简单的用于地址查找的类。它有同步、阻塞的查找方法,或者你可以使用信号/槽,如下所示:
QHostInfo hInfo = QHostInfo::fromName("www.packtpub.com");
此方法会阻塞,直到收到响应。
lookupHost 函数执行异步查找,并接受一个槽作为参数,如下所示:
QHostInfo::lookupHost("www.packtpub.com", this, SLOT(lookupResult(QHostInfo)));
我们需要实现的槽接收 QHostInfo 作为参数,如下所示:
void SomeClass::lookupResult(QHostInfo info)
{
if (!hInfo.addresses().isEmpty()) {
QHostAddress address = info.addresses().first();
qWarning() << address.toString();
}
}
要从这些响应中的任何一个获取地址,可以执行如下操作:
if (!hInfo.addresses().isEmpty()) {
QHostAddress address = info.addresses().first();
qWarning() << address.toString();
}
现在我们继续到 QDnsLookup。
QDnsLookup
QDnsLookup 可以查找不同类型的记录,而不仅仅是 IP 地址。你可以使用的值来设置查找类型如下:
-
A: IPv4 地址,通过hostAddressRecords()访问 -
AAAA: IPv6 地址,通过hostAddressRecords()访问 -
ANY: 任何记录 -
CNAME: 规范名称,通过canonicalNameRecords()访问 -
MX: 邮件交换,通过mailExchangeRecords()访问 -
NS: 名称服务器,通过nameServerRecords()访问 -
PTR: 指针,通过pointerRecords()访问 -
SRV: 服务,通过serviceRecords()访问 -
TXT: 文本,通过textRecords()访问
让我们看看如何实现这一点。我们将名为finished的QDnsLookup信号连接到我们的lookupFinished槽。我们将类型设置为TXT以访问文本记录:
QDnsLookup *lookup = new QDnsLookup(this);
connect(lookup, &QDnsLookup::finished, this, &SomeClass::lookupFinished);
lookup->setType(QDnsLookup::TXT);
lookup->setName("example.com");
lookup->lookup();
对lookup()的调用将开始对设置的名称example.com的文本记录进行查找。我们仍然需要处理响应,如下所示:
void SomeClass:: lookupFinished()
{
QDnsLookup *lookup = qobject_cast<QDnsLookup *>(sender());
if (!lookup)
return;
if (lookup->error() != QDnsLookup::NoError) {
lookup->deleteLater();
return;
}
const QList<QDnsTextRecord> txtRecords = lookup->textRecords();
for (const QDnsTextRecord &record: txtRecords) {
const QString recordName = record->name();
const QList <QByteArray> recordValues = record->values();
...
}
}
然后,你可以按需使用这些记录。
摘要
QNetwork的功能非常广泛。我已经提到了一些特性,例如QNetworkRequest、QNetworkAccessManager和QNetworkReply,它们用于发起网络请求,如get和put。你可以使用 Qt 的承载管理功能来控制在线状态,以及使用QNetworkSession将连接分组在一起,以便在连接之间漫游。我们讨论了使用QLocalSocket、QLocalServer、QTcpSocket和QTcpServer进行套接字开发。你可以使用QHostInfo和QDnsLookup进行主机和 DNS 查找。
连接性可以意味着几件事情,在下一章中,我们将探讨使用蓝牙低功耗(LE)进行连接性。
第六章:使用 Qt 蓝牙 LE 进行连接
您将学习如何使用 Qt 蓝牙低功耗(LE)来建立与具有 LE 蓝牙无线电的设备的连接。蓝牙不仅仅是鼠标、键盘和音频。设备发现、数据交换以及涉及蓝牙低功耗的其他任务将被检查。我们将使用QBluetoothUuid、QBluetoothCharacteristic、QLowEnergyController和QLowEnergyService类。
本章将涵盖以下主题:
-
什么是蓝牙低功耗
-
发现和连接设备
-
广播服务
-
从远程设备检索传感器数据
什么是低功耗蓝牙?
蓝牙低功耗(BLE),也称为蓝牙智能,最初由诺基亚在 Wibree 的名称下开发,并于 2006 年首次发布。它集成到蓝牙 4.0 规范中,并于 2010 年发布。
蓝牙是一种无线连接技术,它工作在 2.4 GHz 频段的 2,400-2,483.5 MHz 范围内。它可以选择 79 个数据通道来传输数据包。BLE 将数据通道限制为 40 个。
BLE 针对的是需要低功耗的移动和嵌入式设备。与蓝牙不同,BLE 是为定期交换少量数据的设备设计的,而常规蓝牙是为连续数据流设计的。最重要的是,BLE 有一个睡眠模式,用于节省电力。
Qt 在 Qt Connectivity 模块中支持 BLE,同时还有近场通信(NFC)。BLE 有许多配置文件和服务:
-
提醒
-
电池
-
健身
-
健康
-
HID
-
互联网
-
网状网络
-
传感器
通用属性(GATT)用于存储配置文件、服务、特性和其他数据。每个条目都有一个唯一的 16 位 ID。BLE 连接是专一的,它一次只能连接到一台计算机。BLE 外围设备被称为 GATT 服务器,与之连接的计算机是 GATT 客户端。
每个配置文件可以有多个服务。每个服务可以有多个特性。配置文件只是规范中预定义服务的集合。
服务是一组由唯一的 16 或 128 位 ID 定义的特性。特性是一个单一的数据点,可能包含一个数据数组,例如加速度计。
现在您已经了解了一些背景知识,让我们开始吧。
实现 BLE GATT 服务器
我想我们真的需要一个 BLE 服务器了。
假设您有一个带有几个环境传感器(如湿度和温度)的嵌入式设备。您需要偶尔通过蓝牙将此数据发送到另一台手持设备。在嵌入式传感器设备上,您需要设置设备。设置 BLE 服务器的基本步骤如下:
-
提供广告数据(
QLowEnergyAdvertisingData) -
提供特性数据(
QLowEnergyCharacteristicData) -
设置服务数据(
QLowEnergyServiceData) -
开始广告并监听连接
QLowEnergyAdvertisingData
QLowEnergyAdvertisingData是用于告诉服务器数据和数据如何呈现的类。
这是我们如何使用QLowEnergyAdvertisingData的方式。
构造一个QLowEnergyAdvertisingData对象:
QLowEnergyAdvertisingData *leAdd = new QLowEnergyAdvertisingData;
设置Discoverability选项:
leAdd->setDiscoverability(
QLowEnergyAdvertisingData::DiscoverabilityGeneral);
为我们的服务设置一个Name:
leAdd->setLocalName("SensorServer");
添加我们感兴趣的服务的列表:
QList<QBluetoothUuid> servicesList
<< QBluetoothUuid::EnvironmentalSensing;
leAdd->setServices(servicesList);
源代码可以在 Git 仓库的Chapter06-1目录下的cp6分支中找到。
现在我们需要创建一些特征数据。让我们创建一个处理温度的Characteristic,因此我们将它的uuid设置为TemperatureMearurement。我们还需要让它能够配置通知。
QLowEnergyCharacteristicData
QLowEnergyCharacteristicData代表一个通用属性配置文件(GATT)特征,它定义了蓝牙传输中的单个数据点。你用它来设置服务数据:
QLowEnergyCharacteristicData chData;
chData.setUuid(QBluetoothUuid::TemperatureMeasurement);
chData.setValue(QByteArray(2,0));
chData.setProperties(QLowEnergyCharacteristic::Notify);
const QLowEnergyDescriptorData descriptorData(QBluetoothUuid::ClientCharacteristicConfiguration, QByteArray(2, 0));
chData.addDescriptor(descriptorData);
QLowEnergyServiceData
在这里,我们将Temperature服务数据设置为Primary服务,并将Characteristic添加到service中:
QLowEnergyServiceData serviceData;
serviceData.setUuid(QBluetoothUuid::Temperature);
serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceData.addCharacteristic(chData);
现在,让我们提供温度数据。我们使用TemperatureMeasurement类型构造QLowEnergyCharacteristic,并向它提供一些数据。第一个位指定我们提供的是摄氏度的temperature单位:
QLowEnergyCharacteristic characteristic = service->characteristic(QLowEnergyCharacteristic::TemperatureMeasurement);
quint8 temperature = 35;
QByteArray currentTempValue;
value.append(char(0));
value.append(char(temperature));
service->writeCharacteristic(characteristic, currentTempValue);
我们现在已经设置好了,现在我们只需要开始Advertising来监听连接:
controller->startAdvertising(QLowEnergyAdvertisingParameters(), leAdd, leAdd);
发现和配对性 - 搜索和连接 BLE 设备
你需要做的第一件事是搜索设备,这被称为发现。它包括将蓝牙设备置于搜索或发现模式。然后你会收到一个设备地址列表,你可以通过连接或配对来访问和共享数据。
让我们看看在 Qt 中使用QBluetoothDeviceDiscoveryAgent是如何实现的。
QBluetoothDeviceDiscoveryAgent
QBluetoothDeviceDiscoveryAgent类负责设备发现搜索。当找到任何蓝牙设备时,它将发出deviceDiscovered信号:
QBluetoothDeviceDiscoveryAgent *discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); connect(discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)),
this, SLOT(newDevice(QBluetoothDeviceInfo)));
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod));
源代码可以在 Git 仓库的Chapter06-1a目录下的cp6分支中找到。
调用start()将启动发现过程。QBluetoothDeviceDiscoveryAgent::LowEnergyMethod参数将设置一个过滤器,仅发现LowEnergy设备。一旦找到你想要的设备,你可以调用stop()来停止设备搜索。
你可以通过连接到错误信号(QBluetoothDeviceDiscoveryAgent::Error error)来等待错误。
QBluetoothDeviceDiscoveryAgent类中的error信号是重载的,因此需要特别注意才能连接到该信号。Qt 提供了QOverload,可以像这样实现:
connect(discoveryAgent, QOverload<QBluetoothDeviceDiscoveryAgent::Error>::of(&QBluetoothDeviceDiscoveryAgent::error), this, &SomeClass::deviceDiscoveryError);
如果你希望一次性获取所有设备的列表,请连接到Finished信号并使用discoveryDevices()调用,该调用返回QList <QBluetoothDeviceInfo>:

你可能想要检查远程设备的配对状态,因此调用QLocalBluetoothDevice的pairingStatus。
你可以通过调用QBluetoothLocalDevice的requestPairing函数,并传入远程蓝牙设备的QBluetoothAddress来与设备配对:
SomeClass::newDevice(const QBluetoothDeviceInfo &info)
{
QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(info.address());
if (pairingStatus == QBluetoothLocalDevice::Unpaired) {
QMessageBox msgBox;
msgBox.setText("Bluetooth Pairing.");
msgBox.setInformativeText("Do you want to pair with device: " + item->data(Qt::UserRole).toString());
msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Cancel);
int ret = msgBox.exec();
if (ret == QMessageBox::Ok) {
qDebug() << Q_FUNC_INFO << "Pairing...";
localDevice->requestPairing(address, QBluetoothLocalDevice::Paired);
}
}
}
我们的示例应用在执行requestPairing过程之前要求配对设备:

你可以调用QBluetoothLocalDevice上的requestPairing,并传入你想要配对的设备的QBluetoothAddress。让我们看看QBluetoothLocalDevice。
QBluetoothLocalDevice
QBluetoothLocalDevice代表设备上的蓝牙。你使用这个类来启动与其他设备的配对,同时也用于处理来自远程蓝牙设备的配对请求。它有几个信号来帮助完成这些操作:
-
pairingDisplayConfirmation:这是一个远程设备请求显示 PIN 并询问两个设备上是否相同的信号。你必须在使用QBluetoothLocalDevice时调用pairingConfirmation并传入true或false。 -
pairingDisplayPinCode:这是一个请求输入 PIB。 -
pairingFinished:配对成功完成。
我们然后连接到这些信号,如果用户在点击 OK 按钮时允许这样做:
if (ret == QMessageBox::Ok) {
connect(localDevice, &QBluetoothLocalDevice::pairingDisplayPinCode, this, &MainWindow::displayPin);
connect(localDevice, &QBluetoothLocalDevice::pairingDisplayConfirmation, this, &MainWindow::displayConfirmation);
connect(localDevice, &QBluetoothLocalDevice::pairingFinished, this, &MainWindow::pairingFinished);
connect(localDevice, &QBluetoothLocalDevice::error, this, &MainWindow::pairingError);
localDevice->requestPairing(address, QBluetoothLocalDevice::Paired);
}
当远程设备只需要 PIN 确认时,会调用pairingDisplayConfirmation信号:
SomeClass::displayConfirmation(const QBluetoothAddress &address, const QString &pin)
{
QMessageBox msgBox;
msgBox.setText("Confirm pin");
msgBox.setInformativeText("Confirm the pin is the same as on the device.");
msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Cancel);
int ret = msgBox.exec();
if (ret == QMessageBox::Ok) {
localDevice->pairingConfirmed(true);
} else {
localDevice->pairingConfirmed(false);
}
}
当远程设备需要用户输入 PIN 时,会调用pairingDisplayPinCode信号,并带上要在远程设备上显示和输入的 PIN:
SomeClass::displayPin(const QBluetoothAddress &address, const QString &pin) {
{
QMessageBox msgBox;
msgBox.setText(pin);
msgBox.setInformativeText("Enter pin on remote device");
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.exec();
}
在另一边,为了接收配对,你需要将QBluetoothLocalDevice置于Discoverable模式:
localDevice->setHostMode(QBluetoothLocalDevice::HostDiscoverable);
设备可以被处于蓝牙Discovery模式的其他设备看到。
指定和获取客户端数据
一旦你连接到 BLE 设备的外围设备,你需要发现其特性才能读取和写入它们。你可以通过使用QLowEnergyController来完成这个操作。让我们看看QLowEnergyController是什么。
QLowEnergyController
QLowEnergyController是访问本地和远程 BLE 设备的中心位置。
可以通过使用静态函数QLowEnergyController::createPeripheral(QObject *parent)来创建本地的QLowEnergyController。
通过调用静态类QLowEnergyController::createCentral并使用你在发现远程设备时收到的QBluetoothDeviceInfo对象来创建表示远程设备的QLowEnergyController对象:
QLowEnergyController对象有几个信号:
-
discoveryFinished -
serviceDiscovered -
connected -
disconnected
连接到connected信号,并通过调用connectToDevice()开始连接:
SomeClass::newDevice(const QBluetoothDeviceInfo &device)
{
QLowEnergyController *controller = new QLowEnergyController(device.address());
connect(controller, &QLowEnergyController::connected, this, &SomeClass::controllerConnected);
controller->connectToDevice();
}
SomeClass::controllerConnected()
{
QLowEnergyController *controller = qobject_cast<QLowEnergyController *>(sender());
if (controller) {
connect(controller, &QLowEnergyController::serviceDiscovered, this, &SomeClass::newServiceFound);
controller->discoverServices();
}
一旦设备连接成功,就是时候发现它的服务了,因此我们连接到serviceDiscovered信号,并通过调用discoverServices()来启动服务发现。
QLowEnergyService
您还可以连接到 discoveryFinished() 信号,该信号通过调用 services() 返回发现的服务的列表。使用这两种方法中的任何一种,您都将获得属于该服务的 QBluetoothUuid,然后您可以使用它创建一个 QLowEnergyService 对象:
SomeClass::newServiceFound(const QBluetoothUuid &gatt)
{
QLowEnergyController *controller = qobject_cast<QLowEnergyController *>(sender());
QLowEnergyService *myLEService = controller->createServiceObject(gatt, this);
}
我们现在有一个 QLowEnergyService 对象,它提供了关于它的详细信息。我们只能在它的状态变为 ServiceDiscovered 时读取其服务详情,因此现在调用服务的 discoverDetails() 函数以启动发现过程:
QLowEnergyService *myLEService = controller->createServiceObject(gatt, this);
connect(myLEService, &QLowEnergyService::stateChanged, this, &SomeClass::serviceStateChanged);
myLEService->discoverDetails();
让我们看看 QLowEnergyCharacteristic。
QLowEnergyCharacteristic
一旦发现服务详情或 characteristics,我们就可以使用 QLowEnergyCharacteristic 来执行操作,例如启用通知:
void SomeClass::serviceStateChanged(QLowEnergyService::ServiceState state))
{
if (state != QLowEnergyService::ServiceDiscovered)
return;
QLowEnergyService *myLEService = qobject_cast<QLowEnergyService *>(sender());
QList <QLowEnergyCharacteristic> characteristics = myLEService->characteristics();
}
使用 QLowEnergyCharacteristic,我们可以获取一个 QLowEnergyDescriptor,我们用它来启用或禁用通知。
有时,远程设备上的 characteristic 也需要被写入,例如启用特定的传感器。在这种情况下,您需要使用服务的 writeCharacteristic 函数,其中 characteristic 作为第一个参数,要写入的值作为第二个参数:
QLowEnergyCharacteristic *movementCharacteristic = myLEService->characteristic(someUuid);
myLEService->writeCharacteristic(movementCharacteristic, QLowEnergyCharacteristic::Read);
写入 QLowEnergyDescriptor 与此类似;让我们看看。
QLowEnergyDescriptor
根据蓝牙规范,描述符被定义为描述特征值的属性。它包含有关特征的一些附加信息。QLowEnergyDescriptor 封装了一个 GATT 描述符。当发生变化时,描述符和特征可以具有通知。
要启用通知,我们可能需要向描述符写入一个值。这里有一些可能的值:
| GATT 术语 | 描述 | 值 | Qt 常量 |
|---|---|---|---|
| 广播 | 允许广播 | 0x01 |
QLowEnergyCharacteristic::Broadcasting |
| 读取 | 允许读取 | 0x02 |
QLowEnergyCharacteristic::Read |
| 无响应写入 | 允许带任何响应的写入 | 0x04 |
QLowEnergyCharacteristic::WriteNoResponse |
| 写入 | 允许带响应的写入 | 0x08 |
QLowEnergyCharacteristic::Write |
| 通知 | 允许通知 | 0x10 |
QLowEnergyCharacteristic::Notify |
| 指示 | 允许带客户端确认的通知 | 0x20 |
QLowEnergyCharacteristic::Indicate |
| 认证签名写入 | 允许签名写入 | 0x40 |
QLowEnergyCharacteristic::WriteSigned |
| 扩展属性 | 排队写入和可写辅助设备 | 0x80 |
QLowEnergyCharacteristic::ExtendedProperty |
通知和指示之间的区别在于,使用指示时,服务器要求客户端确认它已收到消息,而使用通知时,服务器不关心客户端是否收到。
Qt 目前不支持使用带认证的签名写入 (0x40) 与 Qt 一起使用,也不支持使用指示 (0x20)。
我们希望在特征值改变时得到通知。为了启用此功能,我们需要将 0x10 或 QLowEnergyCharacteristic::Notify 的值写入 descriptor:
for ( const QLowEnergyCharacteristic character : characteristics) {
QLowEnergyDescriptor descriptor = character.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);
connect(myLEService, &QLowEnergyService::characteristicChanged, this, &SomeClass::characteristicUpdated);
myLEService->writeDescriptor(descriptor, QByteArrayLiteral("\x01\x00"));
}
或者我们可以使用预定义的 QLowEnergyCharacteristic::Notify,如下所示:
myLEService->writeDescriptor(descriptor, QLowEnergyCharacteristic::Notify));
现在,我们终于可以从我们的蓝牙低功耗设备中获取值了:
void SomeClass::characteristicUpdated(const QLowEnergyCharacteristic &ch, const QByteArray &value)
{
qWarning() << ch.name() << "value changed!" << value;
}
Bluetooth QML
有一些蓝牙 QML 组件可以作为客户端使用,用于扫描和连接到蓝牙设备。它们简单但功能性强。
源代码可以在 Git 仓库的 Chapter06-2 目录下的 cp6 分支中找到。
- 将
bluetooth模块添加到你的pro文件中:
QT += bluetooth
- 在你的
qml文件中,使用QtBluetooth导入:
import QtBluetooth 5.12
最重要的元素是 BluetoothDiscoveryModel。
BluetoothDiscoveryModel
BluetoothDiscoveryModel 提供了附近可用蓝牙设备的数据模型。您可以在各种基于模型的 Qt Quick 组件中使用它,例如 GridView、ListView 和 PathView。设置 discoveryMode 属性告诉本地蓝牙设备服务发现的级别,这以下是以下之一:
-
FullServiceDiscovery: 发现所有设备的所有服务 -
MinimalServiceDiscovery: 最小发现仅包括设备和 UUID 信息 -
DeviceDiscovery: 仅发现设备,不发现服务
根据需要发现的服务的数量,发现过程将需要各种不同的时间。为了加快特定设备的发现速度,你可以将 discoveryMode 属性设置为 BluetoothDiscoveryModel.DeviceDiscovery,这将允许你发现目标设备地址。在下面的示例中,我已经注释掉了设备的蓝牙地址,这样当你运行它时至少会显示一些设备:
BluetoothDiscoveryModel {
id: discoveryModel
discoveryMode: BluetoothDiscoveryModel.DeviceDiscovery
onDeviceDiscovered: {
if (/*device == "01:01:01:01:01:01" && */ discoveryMode == BluetoothDiscoveryModel.DeviceDiscovery) {
discoveryModel.running = false
discoveryModel.discoveryMode = BluetoothDiscoveryModel.FullServiceDiscovery
discoveryModel.remoteAddress = device
discoveryModel.running = true
}
}
}
要发现所有附近设备的所有服务,将 discoveryMode 设置为 BluetoothDiscoveryModel.FullServiceDiscovery。如果你使用设备地址设置了 remoteAddress 属性,你可以针对该特定设备。然后你必须切换 running 属性从关闭到打开以启动新的扫描。
我们有一个基本的数据模型,但我们需要一个地方来显示它。Qt Quick 有几个选项用于查看模型数据:
-
GridView -
ListView -
PathView
PathView 最好使用 Qt Creator QML 设计器编写,因为你可以直观地调整其路径。
为了简单起见,我们选择 ListView,尽管我真的很想使用 PathView:
ListView {
id: mainList
anchors.top: busy.bottom
anchors.fill: parent
model: discoveryModel
}
如果没有定义 delegate,它将不会显示任何内容:
delegate: Rectangle {
id: btDelegate
width: parent.width
height: column.height + 10
focus: true
Column {
id: column
anchors.horizontalCenter: parent.horizontalCenter
Text {
id: btText
text: deviceName ? deviceName : name
font.pointSize: 14
}
}
}
扫描设备有时可能需要一段时间才能完成,所以我想要添加一个忙碌指示器。Qt Quick Control 2 有 BusyIndicator:
BusyIndicator {
id: busy
width: mainWindow.width *.6
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: mainWindow.top
height: mainWindow.height / 8
running: discoveryModel.running
}
当你发现远程服务时,你会得到一个 BluetoothService 对象。
BluetoothService
当你指定 BluetoothDiscoveryModel.FullServiceDiscovery 作为发现扫描,并且当 BluetoothDiscoveryModel 定位到一个新服务时,serviceDiscovered 信号将被发出。当我们连接到该信号时,我们将在槽中接收到 BluetoothService 对象。
我们可以获取 通用唯一标识符(uuid)、设备和服务名称、服务描述以及其他详细信息。您可以使用此 BluetoothService 连接到 BluetoothSocket。
BluetoothSocket
BluetoothSocket 组件可用于发送和接收 String 消息。
要实现此组件,最简单的方法如下:
BluetoothSocket {
id: btSocket
}
BluetoothSocket 不处理二进制数据。为此,您将不得不使用 C++ 的 QBluetoothSocket 类。
在 BluetoothDiscoveryModel 中处理 serviceDiscovered 信号。您将获得一个名为 service 的 BluetoothService 对象。然后您可以使用 setService 方法设置 Socket 以使用该服务:
onServiceDiscovered {
if (service.serviceName == "Magical Service")
btSocket.setService(service)
}
首先,您可能想要处理 stateChanged 信号:
onSocketStateChanged: {
switch (socketState) {
case BluetoothSocket.Unconnected:
case BluetoothSocket.NoServiceSet:
break;
case BluetoothSocket.Connected:
console.log("Connected");
break;
case BluetoothSocket.Connecting:
console.log("Connecting...");
break;
case BluetoothSocket.ServiceLookup:
console.log("Looking up Service");
break;
case BluetoothSocket.Closing:
console.log("Closing connection");
break;
case BluetoothSocket.Listening:
console.log("Listening for incoming connections");
break;
case BluetoothSocket.Bound:
console.log("Bound to local address")
break;
}
}
要连接到服务,将 true 写入 connected 属性:
btSocket.connected = true
一旦 socketState 属性为 Connected,您可以使用 stringData 属性发送消息或字符串数据:
btSocket.stringData = "Message Ok"
Qt Quick 提供了一种简单的方法来通过蓝牙发送字符串消息。
概述
低功耗蓝牙旨在为移动和嵌入式设备降低能耗。Qt 提供了 C++ 和 QML 类和组件来使用它。现在您应该能够发现并连接到蓝牙低能耗设备。
广告 GATT 服务,以便用户和客户端可以接收和发送数据,也已被涵盖。
在下一章中,我们将介绍物联网(IoT)的一些主要组件,例如传感器和自动化通信协议。
第七章:机器对话
机器自动化和物联网使用各种 API 进行相互通信。
我喜欢说,没有传感器就无法实现物联网。它们真正定义了物联网。如今,传感器无处不在。汽车、灯光和移动电话都有无数的传感器。笔记本电脑有亮度、光线、触摸和接近传感器。
MQTT 和 WebSockets 是通信和消息协议。它们的一个用途是将传感器发送到远程位置。
你将学习如何使用 Qt API 进行机器到机器的自动化和通信,使用QWebSocket和QWebSocketServer类与 Web 应用程序进行通信。
MQTT 是基于发布和订阅的 TCP/IP 协议,用于通过QMqttMessage将传感器数据发送到有限带宽网络中的QMqttClient和QMqttSubscription。
我们将涵盖以下主题:
-
感官控制 – QtSensor 数据
-
WebSockets - 双向 Web 通信
-
QMqtt - 机器对话的代理
感官控制 – QtSensor 数据
Qt Sensors API 始于 Qt Mobility,Qt Mobility 本身又源于 Qtopia,后来更名为 Qt Extended。
Qt Mobility 是一组适用于移动和嵌入式设备的 API 集合。它专门用于诺基亚手机。一些 Mobility API 已集成到 Qt 4,后来集成到 Qt 5。
与此同时,当 Qt 5 拆分为模块时,Qt Sensors 被放入了自己的仓库。Qt Sensors 最初针对的是移动电话平台,但随着笔记本电脑和树莓派等计算机获得了传感器,后端得到了扩展。你可以找到 iOS、Android、WinRT、通用 Linux、Sensorfw 以及德州仪器的 SensorTag 的后端。在我的 GitHub 仓库中,你可以找到 Raspberry Pi Sense HAT 和 Raspberry Pi 的 MATRIX Creator 的附加传感器后端。
传感器框架(SensorFW)是一个用于以多种方式配置和读取传感器数据的框架和后端。它已在一些最好的替代移动设备上经过测试和验证。它支持 Hybris(用于 Sailfish OS),Linux IIO 传感器,以及直接从 Linux 文件系统中读取。如果你正在集成新设备并需要读取各种传感器,我建议使用 Sensor Framework,可在git.merproject.org/mer-core/sensorfw/找到.
有数十种不同的传感器用于监控环境。Qt Sensors 处理在移动电话和平板电脑中最常见的传感器,并提供工具以帮助实现可能开发并变得流行的新的传感器类型。
不仅用于监控环境的传感器可以用作系统的输入。Qt Sensor API 包括一个临时的QSensorGestures,这是一个用于各种设备手势的 API,如摇晃、自由落体、悬停、覆盖、翻转和拾取。
Qt Sensors 具有 C++和 QML API。让我们从 C++ API 开始。
实际上,有三种使用此 API 的方法。第一种是通用方式。所有传感器类都是 QSensor 的派生类。使用它们的一个更通用的方式是直接使用 QSensor。
QSensor
QSensor 有两个静态函数我们可以使用。QSensor::sensorTypes() 返回一个传感器类型的 QList;例如,它可能是 QLightSensor 或 QOrientationSensor。然后你可以使用 QSensor::sensorForType 或 defaultSensorForType。通常一个类型只有一个传感器,所以,使用后者就足够了。
但首先,我们需要告诉 qmake 我们想要使用 sensors 模块,因此,在 .pro 文件中,执行以下操作:
源代码可以在 Git 仓库的 Chapter07-1 目录下的 cp7 分支中找到。
QT += sensors
要包含所有 QSensors 头文件,包含文件行是 #include <QtSensors>,因此让我们将其添加到我们的文件中。
通过使用 QSensor::sensorTypes() 获取系统已知的所有传感器类型列表:
for (const QByteArray &type : QSensor::sensorTypes()) {
const QByteArray &identifier = QSensor::defaultSensorForType(type);
QSensor 是通过提供一个 QSensor::type 参数创建的,然后你调用 setIdentifier 函数,使用一个 String 指示你想要使用的传感器。
QSensor* sensor = new QSensor(type, this);
sensor->setIdentifier(identifier);
我们现在有一个 QSensor。如果你直接使用 QSensor,那么你必须调用 connectToBackend():
if (!sensor->connectToBackend())
qWarning() << "Could not connect to sensor backend";
然后,你可以连接到 readingChanged() 信号并从那里读取值。要获取 QSensor,你可以在任何槽中使用 sender() 函数,然后使用 qobject_cast 来转换到 QSensor:
connect(sensor, &QSensor::readingChanged, this, &SomeClass::readingChanged);
readingChanged() 槽看起来像这样:
void SomeClass::readingChanged()
{
QSensor *sensor = qobject_cast<QSensor *>(sender());
QSensorReading *reading = sensor->reading()
QString values;
for (int i = 0; i < reading->valueCount(); i++) {
values += QString::number(reading->value(i).toReal()) + " ";
}
ui->textEdit->insertPlainText(sensor->type() +" " + sensor->identifier() + " "+ values + "\n");
}
我们使用 sender() 函数将 QSensor 进行转换,该函数返回槽连接到的对象。然后我们使用它通过 reading() 函数获取 QSensorReading。从读取中,我们可以获取传感器向我们发出的值。
我们仍然需要在传感器上调用 start(),因此我们将在连接到 readingChanged() 信号之后将其添加到某个地方。这将激活传感器的后端并开始从设备读取数据。
if (!sensor->isActive())
sensor->start();
访问传感器的另一种方式是使用 QSensor 子类。让我们看看我们将如何使用 QSensor 作为子类:
QSensor 子类
使用 Qt Sensors 的更流行的方式是使用标准 QSensors 派生类,例如 QLightSensor 或 QAccelerometer。如果你确切知道你的设备有哪些传感器或你打算使用什么,这很有用。它还减少了类型转换的需要。以这种方式,它也更容易使用类的特定于传感器的属性:
QLightSensor *lightSensor = new QLightSensor(this);
if (!lightSensor->connectToBackend()) {
qWarning() << "Could not connect to light sensor backend";
return;
}
connect(lightSensor, &QLightSensor::readingChanged, &SomeClass::lightSensorChanged);
与通用的 QSensorReading 不同,我们得到一个特定于传感器的读取,例如 QLightReading,以及一个特定于传感器的值访问器:
SomeClass::lightSensorChanged(const QLightReading *reading)
{
qWarning() << reading->lux();
}
访问传感器数据的另一种方式是使用 QSensorFilter。让我们看看那里。
QSensorFilter
在 C++中访问传感器数据还有第三种方法,即使用传感器特定的过滤器类。当信号和槽可能太慢时,这提供了一种有效的回调,例如在QAccelerometer和其他可能以每秒 200 次周期运行的动态传感器的情况下。它还提供了一种在传感器读取信号发出之前应用一个或多个影响值的过滤器的方式。您可以提供额外的平滑和噪声减少,或者放大信号到一个更大的范围。
在我们的案例中,我们的类将继承自QLightFilter。
class LightFilter : public QLightFilter
{
public:
我们接着实现过滤器覆盖。
如果filter函数返回true,它将存储QLightSensor的QLightReading,并且新值将由我们的QLightSensor类发出。让我们对我们的光传感器数据应用一个简单的移动平均滤波器:
bool filter(QLightReading *reading)
{
int lux = 0;
int averageLux = 0;
if (averagingList.count() <= 4) {
averagingList.append(reading->lux());
} else {
for (int i = 0; i < averagingList.count(); i++) {
lux += averagingList.at(i);
}
averageLux = lux / (averagingList.count());
reading->setLux(averageLux);
averagingList.append(averageLux);
return true; // store the reading in the sensor
}
return false;
};
QList<int> averagingList;
};
然后,您可以创建一个新的LightFilter对象,并将QLightSensor设置为使用它。在调用start()之前添加此代码:
if (type == QByteArray("QLightSensor")) {
LightFilter *filter = new LightFilter();
sensor->addFilter(filter);
}
现在我们来了解一下QSensor数据以及如何访问它。
QSensor 数据
QSensor具有各自传感器特有的值。您可以通过QSensor以通用方式访问它们,或者通过传感器值访问。
QSensorReading
如果您使用更通用的QSensor类,则有一个相应的QSensorReading类,您可以使用它来检索通用数据。对于获取任何特定传感器数据,您需要使用相应的传感器QSensorReading子类,例如QAccelerometerReading。例如,如果我们使用QSensor来获取加速度计数据,我们可以这样做:
QSensorReading reading;
QList <qreal> data;
qreal x = reading.at(0);
qreal y = reading.at(1);
if (reading.valueCount() == 3)
qreal z = reading.at(2);
qreal timestamp = reading.timestamp;
然而,使用QAccelerometer和QAccelerometerReading类做同样的事情,看起来是这样的。
QAccelerometer accel;
QAccelerometerReading accelReading = accel.reading();
qreal x = accelReading.x();
qreal y = accelReading.y();
qreal z = accelReading.z();
这里是一些常见传感器的数据解释:
| 传感器读取 | 值 | 单位 | 描述 |
|---|---|---|---|
QAccelerometerReading |
x, y, z |
m²/s², 米每平方秒 | 沿 x、y、z 轴的线性加速度 |
QAltimeterReading |
altitude |
米 | 海拔高度 |
QAmbientLightReading |
lightLevel |
Dark, Twilight, Light, Bright, Sunny | 一般光级 |
QAmbientTemperatureReading |
temperature |
摄氏度 | 摄氏度 |
QCompassReading |
azimuth |
度 | 从磁北方向的角度 |
QGyroscopeReading |
x, y, z |
每秒度 | 绕轴的角速度 |
QHumidityReading |
absoluteHumidity,relativeHumidty |
g/m³, 克每立方米 | 空气中的水蒸气 |
QIrProximityReading |
reflectance |
十进制分数 0 - 1 | 反射回来的红外光量 |
QLidReading |
backLidClosed,frontLidClosed |
布尔值 | 笔记本电脑盖 |
QLightReading |
lux |
Lux | 以勒克斯为单位测量的光 |
QMagnetometerReading |
x, y, z |
磁通密度 | 原始磁通 |
QOrientationReading |
orientation |
TopUp, TopDown, LeftUp, RightUp, FaceUp, FaceDown | 枚举设备方向 |
QPressureReading |
pressure, temperature |
帕斯卡,摄氏度 | 大气压力 |
QProximityReading |
close |
布尔值 | 靠近或远离 |
QRotationReading |
x, y, z |
度 | 绕轴旋转的度数 |
一些这些传感器具有特定的读数,例如QCompass和QMagnetometer——两者都包含校准级别。
当然,C++不是实现 Qt 传感器的唯一方式;你同样可以在 QML 中使用它们。让我们来看看如何实现。
QML 传感器
当然,你也可以从 QML 中使用 Qt Sensors。在许多方面,这种方式实现起来更简单,因为 Qt Quick API 已经被优化和简化,因此启动传感器所需的时间更少。继我们之前对光传感器的使用之后,我们将继续这样做。首先是一个始终存在的import语句:不是通过调用一个start()函数来启动,而是一个active属性。不是Lux值,属性是illuminance。不确定为什么有这种差异,但就是这样:
import QtSensors 5.12
LightSensor {
id: lightSensor
active: true
onReadingChanged {
console.log("Lux "+ illuminance);
}
这已经非常简单了。QtSensors QML 没有过滤器,所以如果你需要过滤任何内容,你将不得不使用 C++。
自定义 QSensor 和后端引擎
我想简要地谈谈如何创建自定义传感器和引擎后端。如果你在一个嵌入式设备上,Qt Sensors 可能不支持你的传感器,如果它是一个湿度或空气质量传感器。你需要实现自己的QSensor和QSensorBackend引擎。
在该目录中有一个脚本src/sensors/make_sensor.pl,你可以运行它来生成一个简单的QSensor派生类,但此脚本还会生成从QmlSensor派生的 Qt Quick 类。make_sensor.pl脚本需要从src/sensors目录中运行。在这个练习中,我们将创建一个用于监测盐水游泳池中盐浓度的传感器,因此其名称将是QSaltSensor。
你可以打开这些文件在一个编辑器中,例如 Qt Creator,并添加你需要的内容。拥有一个新的QSensor类型也将需要一个从传感器读取的新后端。
自定义 QSensor
有一个名为QtSensors/src/sensors/make_sensor.pl的辅助脚本,它将为新的QSensor和QSensorReading创建一个基本模板。它生成一个简单的QSensor派生类,同时也为QmlSensor派生类生成类。
如果在你的源目录中没有,它可以在 Git 仓库中找到,网址为code.qt.io/cgit/qt/qtsensors.git/tree/src/sensors/make_sensor.pl。
make_sensor.pl脚本需要从src/sensors目录中运行。
你将需要编辑生成的文件并填写一些内容。在这个例子中,我选择了QSaltSensor作为类名。使用类名作为第一个参数执行脚本:make_sensor.pl QSaltSensor。
它会创建以下文件:
-
<sensorname>.h -
<sensorname>.cpp -
<sensorname>_p.h -
imports/sensors/<sensorname>.h -
imports/sensors/<sensorname>.cpp
使用make_sensor.pl命令的输出将如下所示:
cd src/sensors
$perl ./make_sensor.pl QSaltSensor
Creating ../imports/sensors/qmlsaltsensor.h
Creating ../imports/sensors/qmlsaltsensor.cpp
Creating qsaltsensor_p.h
Creating qsaltsensor.h
Creating qsaltsensor.cpp
You will need to add qsaltsensor to the src/sensors.pro file to the SENSORS and the qmlsaltsensor files to src/imports/sensors/sensors.pro
正如输出所示,你需要将qsaltsensor添加到src/sensors.pro文件中的SENSORS变量中。在文件src/imports/sensors/sensors.pro中添加qmlsaltsensor的文件路径。
首先,编辑qsaltsensor.cpp,这是我们用作QSensorBackend的类。我们用来创建模板的perl脚本已经添加了注释,你应该在这里编辑以进行自定义。你还需要添加任何属性。
自定义 QSensorBackend
你可能有很多原因想要实现自己的传感器后端。其中一个可能是因为你有一种新的传感器类型。
你需要开始实现你新的QSensor类型的后端引擎。首先,从QSensorBackend派生:
#ifndef LINUXSALT_H
#define LINUXSALT_H
#include <qsensorbackend.h>
#include <qsaltsensor.h>
class LinuxSaltSensor : public QSensorBackend
{
类QSensorBackend有两个纯虚函数需要实现:start()和stop():
public:
static char const * const id;
LinuxSaltSensor(QSensor *sensor);
~LinuxSaltSensor();
void start() override;
void stop() override;
private:
QSaltReading m_reading;
};
#endif // LINUXSALT_H
源代码可以在 Git 仓库的Chapter07-2目录下的cp7分支中找到。
实现后端功能取决于你是否有想要使用的盐传感器设备。当然,当你这样做时,你将不得不编译和部署自己的 Qt Sensors。
关于自定义QSensors和后端的信息,请查看 Qt 传感器的 Grue 传感器示例。有关如何实现自定义传感器的文档可以在doc.qt.io/qt-5/qtsensors-grue-example.html找到。
有时一个系统上可能会有多个传感器插件,针对任何传感器都可能有多个。在这种情况下,我们需要告诉系统使用哪个传感器。让我们看看如何配置QSensors。
Sensors.conf
如果一个特定传感器有多个后端,你可能需要指定哪个是默认的。
你可以将qsaltsensor添加到Sensors.conf配置文件中,以便系统可以确定某个传感器的默认传感器类型。当然,开发者可以自由选择系统上注册的任何传感器。配置文件的格式是SensorType = sensor.id,其中SensorType是基本传感器类名,例如QLightSensor,而sensor.id是特定传感器后端的String标识符。以下代码使用 Linux 后端的saltsensor和sensorfw后端中的列表传感器:
[Default]
QSaltSensor = linux.saltsensor
QLightSensor = sensorfw.lightsensor
QSensorGesture
QSensorGesture是一个用于设备手势的传感器 API。正如我在介绍中提到的,它们使用临时的手势,也就是说没有涉及机器学习。Qt Sensors 提供了以下预定义的手势:
-
cover -
doubletap -
freefall -
hover -
pickup -
slam -
shake -
turnover -
twist -
whip
在 Qt Sensors 中执行特定手势的说明可以在doc.qt.io/qt-5/sensorgesture-plugins-topics.html中找到。
值得注意的是,QSensorGesture使用识别器特定的信号。slam手势有slam()信号,当检测到slam手势时发出。它还有标准的detected("<gesture>")信号。shake2手势有shakeLeft、shakeRight、shakeUp和shakeDown信号,但还有相应的detected信号。
QSensorGesture类没有Q_OBJECT宏,并且直接在meta对象上在运行时创建其信号。因此,在使用Q_OBJECT的同时使用qobject_cast和子类化QSensorGesture将不会工作。
QSensorGestureManager有recognizerSignals函数,它接受一个gestureId,这样你就可以发现特定于手势的信号,如果你需要的话。
源代码可以在 Git 仓库的Chapter07-3目录下的cp7分支中找到。
要使用QSensorGestures,创建一个QSensorGesture对象,它接受一个包含你想要使用的手势 ID 列表的QStringList参数。你可以使用这样的QStringList直接指定你想要的手势:
QSensorGesture *gesture = new QSensorGesture(QStringList() << "QtSensors.slam", this);
connect(gesture, SIGNAL(detected(QString)), this, SLOT(detectedGesture(QString)));
或者,你也可以使用QSensorGestureManager来获取所有已注册手势的列表,调用gestureIds()。
由于QSensorGesture的实现不典型(因为信号在运行时动态创建),使用新的连接语法connect(gesture, &QSensorGesture::detected, this, &SomeClass::detectedGesture);将导致编译器错误,因为新的语法有编译时检查。
一旦正确连接了这些信号,你就可以为QSensorGesture调用startDetection():
gesture->startDetection();
QSensorGestureManager
你可以通过使用QSensorGestureManager来获取系统上注册的所有传感器手势的列表:
QSensorGestureManager manager;
for (const QString gestureId : manager.gestureIds()) {
qDebug() << gestureId;
QStringList recognizerSignals = manager.recognizerSignals(gestureId);
for (const QString signalId : recognizerSignals ) {
qDebug() << " Has signal " << signalId;
}
}
你可以使用前一段代码中的gestureId来创建一个新的QSensorGesture对象,并将其连接到检测到的信号:
QSensorGesture *gesture = new QSensorGesture(QStringList() << gestureId, this);
connect(gesture,SIGNAL(detected(QString)), this,SLOT(detectedGesture(QString)));
SensorGesture
当然,传感器手势也可以在 QML 中使用。API 略有不同,因为只有一个类型,即SensorGesture,所以它就像使用通用的QSensor类一样,除了SensorGesture可以表示一个或多个手势,而不是每个对象一个手势。
SensorGesture没有自己的导入,而是包含在QtSensors中,因此我们需要使用它来表示我们正在使用QtSensors模块:
import QtSensors 5.12
你可以通过写入gestures属性来指定你想要的手势,该属性接受一个字符串列表的id识别器:
SensorGesture {
id: sensorGesture
gestures : [ "QtSensors.slam", "QtSensors.pickup" ]
}
由于只有一个通用的SensorGesture,因此没有特定于手势的信号。手势信号是onDetected,在gesture属性中设置检测到的手势字符串。如果你使用该组件进行多个手势,你将不得不使用一些逻辑来过滤特定手势:
onDetected: {
if (gesture == "slam") {
console.log("slam gesture detected!")
}
}
要开始检测,将true写入SensorGesture的启用属性:
sensor.gesture.enabled
您可以拿起您的设备,按照 Qt 文档中概述的方式执行 slam 手势doc.qt.io/qt-5/sensorgesture-plugins-topics.html。根据您的设备,它将检测到 slam。
WebSockets – 双向 Web 通信
现在我们开始进入网络和互联网的领域。WebSockets 是一种协议,允许在 Web 浏览器或客户端和服务器之间进行双向数据交换,而不需要轮询。您可以流式传输数据或随时发送数据。Qt 通过使用QWebSocket API 支持 WebSockets。像正常的 TCP 套接字一样,QWebSockets需要一个服务器。
QWebSocketServer
QWebSocketServer可以在两种模式下工作:非安全和 SSL。我们首先将websockets添加到.pro文件中,这样qmake就会设置正确的库和头文件路径:
QT += websockets
然后包含QWebSocketServer头文件:
#include <QtWebSockets/QWebSocketServer>
源代码可以在 Git 仓库的Chapter07-3目录下的cp7分支中找到。
要创建一个QWebSocketServer,需要一个字符串形式的服务器名称、一个模式和父对象。模式可以是SecureMode或NonSecureMode。
SecureMode类似于 HTTPS,使用 SSL,协议是 wss。NonSecureMode类似于使用 ws 协议的 HTTPS:
const QWebSocketServer *socketServer = new QWebSocketServer("MobileSocketServer",
QWebSocketServer::NonSecureMode,this);
connect(sockerServer, &QWebSocketServer::newConnection, this, &SomeClass::newConnection);
connect(sockerServer, &QWebSocketServer::closed, this, &SomeClass::closed);
与QSocket类似,有一个newConnection信号,当客户端尝试连接到该服务器时会发出。如果您使用SecureMode,您将想要连接到sslErrors(const QList<QSslError> &errors)信号。一旦连接了您想要使用的信号,调用listen来启动服务器,并指定QHostAddress和端口号。QHostAddress::Any将监听所有网络接口。您可以指定一个接口的地址。端口号是可选的,端口号为 0 将自动分配端口号:
socketServer->listen(QHostAddress::Any, 7532);
现在我们有一个监听传入连接的QWebSocketServer对象。我们可以像处理QSocketServer一样处理它,使用nextPendingConnection在相应的槽中连接到newConnection信号。这将给我们一个代表连接客户端的QWebSocket对象。
QWebSocket
当一个新的连接到来时,QWebSocketServer会发出newConnection信号,在这里,它调用newConnection槽。我们使用服务器对象的nextPendingConnection来获取QWebSocket。有了这个,我们就连接到QWebSocket的信号:
QWebSocket *socket = socketServer->nextPendingConnection();
我最喜欢连接的第一个信号是错误信号,因为它可以帮助调试。像QBluetooth类一样,错误函数是重载的,因此需要特殊的语法来使用此信号。
QWebSocket的error信号是重载的,因此需要独特的处理才能编译。QOverload是您需要使用的。
connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
this, &SomeClass::socketError);
可以发送和接收两种类型的消息:text和binary。我们必须分别处理这些,因此每种类型都有自己的信号。当客户端发送text或binary消息时,服务器会发出这些信号:
connect(socket, &QWebSocket::textMessageReceived,
this, &SomeClass::textMessageReceived);
connect(socket, &QWebSocket::binaryMessageReceived,
this, &SomeClass::binaryReceived);
WebSocket 中 binary 和 text 消息之间的一个区别是,text 消息以 0xFF 字符结尾。
textMessageReceived 信号发送 QString,而 binaryMessageReceived 发送 QByteArray:
SomeClass:binaryMessageReceived(const QByteArray &message) {
}
SomeClass:textMessageReceived(const QString &message) {
}
它们也在帧级别上工作,但我们只是处理整个消息。如果您有某种类型的连续流数据,您可能会选择 textFrameReceived 或 binaryFrameReceived 信号。
客户端
WebSocket 客户端会简单地使用 QWebSocket 并连接到一个支持 WebSocket 的服务器。一个用例是一个网页(客户端),它显示通过 QWebSocketServer 发送的传感器数据。
QtQuick
当然,QWebSockets API 提供了 QML 组件——确切地说,是 WebSocket 和 WebSocketServer。像往常一样,使用它比使用 C++ 更快。
WebSocketServer
在您的 qml 文件中添加以下导入行以使用 WebSocket:
源代码可以在 Git 仓库的 Chapter07-4 目录下的 cp7 分支中找到。
import QtWebSockets 1.0
要使用 WebSocketServer 开始监听,将 listen 属性设置为 true。url 属性接受一个字符串,可以设置为客户端将要连接的地址:
WebSocketServer {
id: socketServer
url : "ws://127.0.0.1:33343"
listen: true
}
当客户端连接时,会发出 onClientConnected 信号,其 webSocket 属性表示传入的 WebSocket 客户端。您还希望能够进行错误检查,因此 WebSocketServer 有 onErrorStringChanged 信号,带有 errorString 属性。为此,在 WebSocketServer 组件中实现如下。
onClientConnected {
...
}
onErrorStringChanged {
console.log(errorString)
}
让我们看看如何处理服务器和客户端的 WebSocket。
WebSocket
客户端和服务器都使用 WebSocket 元素。在服务器端,正如我在 WebSocketServer 部分概述的那样,可以通过 onClientConnect 信号获取客户端的 WebSocket 对象。
检查 WebSocketServer 组件中的工作方式,与客户端相比:
WebSocketServer {
id: socketServer
host : "127.0.0.1"
port: 33343
listen: false
onClientConnected {
webSocket.onTextMessageReceived.connect(function(message) {
console.log(message)
});
}
}
客户端需要填充 url 属性,以便知道它将连接到哪个服务器:
WebSocket {
id: webSocket
url: "ws://localhost"
onTextMessageReceived {
console.log(message)
}
}
收到的消息会出现在 onTextMessageReceived 信号中,带有 message 属性。
要向服务器或客户端发送消息,WebSocket 有 sendMessage 函数。如果是服务器,可以使用 webSocket 发送类似文本的消息。
webSocket.sendTextMessage("socket connected ok!")
对于 Qt Quick 的 WebSocket,并不真正处理二进制消息。它确实有一个 onBinaryMessageReceived 信号,但接收到的 message 对象是一个 String。我建议如果您的二进制消息在转换为 UTF16 编码的 QString 时会出错,您可能需要考虑使用 C++ API。
QMqtt – 机器对话的代理
MQTT 是一种发布和订阅的消息传输。Qt Mobility 栈中有一个类似的框架,称为发布和订阅,现在它是官方不支持的 QSystems API 框架的一部分,该框架还包括 QSystemInfo 和 QSystemFramework。
QMqtt 是编写 MQTT 客户端的框架。您需要安装并运行一个 MQTT 代理,如 Mosquitto 或 HiveMQ,或者使用基于互联网的服务。出于我的开发和测试目的,我选择了 HiveMQ。您可以从 www.hivemq.com/ 下载它。
他们还有一个公共代理在 www.mqtt-dashboard.com/index.html。
MQTT 有一个代理,或服务器,一个或多个客户端连接到。然后客户端可以发布和/或订阅不同的主题。
您可以使用 QWebSockets 访问代理,Qt 中有一个示例,它使用 examples/mqtt/websocketsubscription 目录中的 WebSocketIODevice 类。
QMqttClient
要开始开发 QMqttClient,您必须自己构建它,因为它本身并不与 Qt 一起分发,除非您获得商业 Qt for Automation。
您可以从 git://code.qt.io/qt/qtmqtt.git 下载开源许可版本。
幸运的是,这是一个简单且容易构建的过程。一旦您运行 qmake; make && make install;,您就可以使用它了。
在您的 pro 文件中,我们需要添加 mqtt 模块。
QT += mqtt
头文件命名为 QtMqtt/QMqttClient,所以让我们包含它:
#include <QtMqtt/QMqttClient>
源代码可以在 Git 仓库的 Chapter07-5 目录下的 cp7 分支中找到。
我们用来访问代理的主要类名为 QMqttClient。它可以被认为是管理者。它有一个简单的构造。您需要使用 setHostname 和 setPort 函数指定主机和端口。我们将使用 hivemq 公共代理和端口 1883:
mqttClient = new QMqttClient(this);
mqttClient->setHostname(broker.hivemq.com);
mqttClient->setPort(1883);
当事情出错时,连接到任何错误信号以帮助调试是个好主意;让我们先这么做:
connect(mqttClient, &QMqttClient::errorChanged, this, &SomeClass::errorChanged);
connect(mqttClient, &QMqttClient::stateChanged, this, &SomeClass::stateChanged);
connect(mqttClient, &QMqttClient::messageReceived, this, &SomeClass::messageReceived);
要连接到 mqtt 代理,请调用 connectToHost();:
mqttClient->connectToHost();
由于我们连接到了 stateChanged 信号,我们可以等待我们连接到代理后再订阅任何主题:
void SomeClass::stateChanged(QMqttClient::ClientState state)
{
switch(state) {
case QMqttClient::Connecting:
qDebug() << "Connecting...";
break;
case QMqttClient::Connected:
qDebug() << "Connected.";
subscribe();
break;
case QMqttClient::Disconnected:
qDebug() << "Disconnected."
break;
}
}
QMqttClient::subscribe 函数接收一个以 QMqttTopicFilter 形式的主题。在这里,我将其分配给 "Qt" 字符串。
它返回一个 QMqttSubscription 指针,我们可以用它来连接到 stateChanged 信号。然后我们将简单地订阅我们刚刚发布的主题。
我们的 subscribe 函数看起来像这样:
void MainWindow::subscribe()
{
QMqttTopicFilter topicName("Qt");
subscription = mqttClient->subscribe(topicName, 0);
connect(subscription, &QMqttSubscription::stateChanged,this,
&SomeClass::subscriptionStateChanged);
publish();
}
我们只是调用我们的函数,然后就会向该主题发布一些内容。
QMqttClient::publish 接收一个以 QMqttTopicName 形式的主题名称,消息只是一个标准的 QByteArray。
publish 函数看起来像这样:
void MainWindow::publish()
{
QMqttTopicName topicName("Qt");
QByteArray topicMessage("Everywhere!");
mqttClient->publish(topicName, topicMessage);
}
您应该会看到我们在 messageReceived 插槽中发布的消息:

将所有这些放在一起
我有一个 Raspberry Pi 和一个 Sense HAT 板,可以用它来收集传感器数据。幸运的是,我之前为 Sense HAT 编写了一个 Qt Sensors 插件。它恰好在一个独立的 Git 仓库中,而不是任何 Qt Sensors 版本中,与 TI SensorTag 后端插件不同。
如果你不想编写自己的 Sense HAT 传感器插件,你可以从 github.com/lpotter/qsensors-sensehatplugin.git 获取我的独立 Sense HAT 插件。
Raspbian 分发版上的 Qt Sensors 版本为 5.7,它没有 Sense HAT 所具有的压力和湿度传感器。这些传感器是在后续的 Qt Sensors 版本中添加的。
在桌面上的交叉编译比在设备上编译要快得多——在 Raspberry Pi(rpi) 上需要几天时间,而在一个好的开发机上只需要几分钟。我在设置交叉编译 toolchain 时遇到了一些麻烦,所以我不得不选择在板载上使用原生编译,这在 Raspberry Pi 上当然需要非常长的时间。最简单的方法是获取 Qt 的商业 Boot2Qt 和 Automation 软件包,因为它们打包得很好,并提供二进制文件和支持。
由于本书使用 Qt 5.12,我们需要使用以下 Git 命令获取以下 Qt 模块仓库的显式版本:
-
Qt Base:
git clone http://code.qt.io/qt/qtbase.git -b 5.12 -
Qt WebSockets:
git clone http://code.qt.io/qt/qtwebsockets.git -b 5.12 -
Qt MQTT:
git clone http://code.qt.io/qt/qtmqtt -b 5.12 -
Qt Sensors:
git clone http://code.qt.io/qt/qtsensors -b 5.12
我们将创建一个适用于 Raspberry Pi 的应用程序,该应用程序抓取 Sense HAT 的温度和压力数据,并通过 QMqtt 和 QWebSockets 将其分发到运行在 HiveMQ 上的代理。
源代码可以在 Git 仓库的 Chapter07-6 目录下的 cp7 分支中找到。
首先实现一个 SensorServer 类,这通常是一个从 QObject 派生的类。
SensorServer::SensorServer(QObject *parent)
: QObject(parent),
humiditySensor(new QHumiditySensor(this)),
temperatureSensor(new QAmbientTemperatureSensor(this))
{
initSensors();
initWebsocket();
}
然后,我们实现我们声明的 QWebSockeIODevice 并连接到其 socketConnected 信号。
void SensorServer::initWebsocket()
{
mDevice.setUrl("broker.hivemq.com:8000");
mDevice.setProtocol("mqttv3.1");
connect(&mDevice, &WebSocketIODevice::socketConnected, this, &SensorServer::websocketConnected);
}
接下来,我们调用我们想要使用的传感器的 connectToBackend() 函数。
void SensorServer::initSensors()
{
if (!humiditySensor->connectToBackend()) {
qWarning() << "Could not connect to humidity backend";
} else {
humiditySensor->setProperty("alwaysOn",true);
connect(humiditySensor,SIGNAL(readingChanged()),
this, SLOT(humidityReadingChanged()));
}
if (!temperatureSensor->connectToBackend()) {
qWarning() << "Could not connect to humidity backend";
} else {
temperatureSensor->setProperty("alwaysOn",true);
connect(temperatureSensor,SIGNAL(readingChanged()),
this, SLOT(temperatureReadingChanged()));
}
}
调用 initSensors() 连接到传感器的后端并设置 readingChanged 信号连接。
要使用 QWebSockets 为 QMqtt 客户端,我们需要创建一个使用 QWebSockets 的 QIODevice。幸运的是,在 QMqtt 的 examples/mqtt/websocketssubscription 目录中已经有一个名为 websocketsiodevice 的现成代码,所以我们将将其导入到项目中:
SOURCES += websocketiodevice.cpp
HEADERS += websocketiodevice.h
在我们的头文件中,我们包含 websocketdevice.h。
#include "websocketiodevice.h"
在类声明中,我们将 WebSocketIODevice 实例化为一个类成员。
WebSocketIODevice mDevice;
要实际使用 WebSocketIODevice,我们需要将其设置为 QMqttClient 传输。
我们首先设置我们的 WebSocketIODevice 并连接到其 socketConnected 信号以设置 QMqtt。
hivemq 上的 mqtt 代理使用不同的端口号,所以我们将其设置在 URL 中:
void SensorServer::initWebsocket()
{
mDevice.setUrl(QUrl("broker.hivemq.com:8000"));
connect(&mDevice, &WebSocketIODevice::socketConnected, this, &SensorServer::websocketConnected);
mDevice.open(QIODevice::ReadWrite);
}
现在我们设置 QMqtt 并将其传输设置为使用 WebSocketIODevice。我们使用一个具有自己连接的传输,因此我们不需要为 QMqtt 对象设置 URL,而是依赖于 websocket 进行连接。然后我们像往常一样设置 mqttClient:
void SensorServer::websocketConnected()
{
mqttClient = new QMqttClient(this);
mqttClient->setProtocolVersion(QMqttClient::MQTT_3_1);
mqttClient->setTransport(&mDevice, QMqttClient::IODevice);
connect(mqttClient, &QMqttClient::errorChanged,
this, &SensorServer::errorChanged);
connect(mqttClient, &QMqttClient::stateChanged,
this, &SensorServer::stateChanged);
connect(mqttClient, &QMqttClient::messageReceived,
this, &SensorServer::messageReceived);
mqttClient->connectToHost();
}
我们监控状态的变化,并在它变为 Connected 时采取行动。我们将启动 humidity 和 temperature 传感器,然后调用订阅以监控 mqtt 代理发布消息:
void SensorServer::stateChanged(QMqttClient::ClientState state)
{
switch(state) {
case QMqttClient::Connecting:
qDebug() << "Connecting...";
break;
case QMqttClient::Connected:
qDebug() << "Connected.";
humiditySensor->start();
temperatureSensor->start();
subscribe();
break;
case QMqttClient::Disconnected:
qDebug() << "Disconnected.";
break;
}
}
在我们传感器的 readingChanged 插槽中,我们将数据发布到 mqtt 代理:
void SensorServer::humidityReadingChanged()
{
qDebug() << Q_FUNC_INFO << __LINE__;
QHumidityReading *humidityReading = humiditySensor->reading();
QByteArray data;
data.setNum(humidityReading->relativeHumidity());
QMqttTopicName topicName("Humidity");
QByteArray topicMessage(data);
mqttClient->publish(topicName, topicMessage);
}
void SensorServer::temperatureReadingChanged()
{
qDebug() << Q_FUNC_INFO << __LINE__;
QAmbientTemperatureReading *tempReading = temperatureSensor
>reading();
QByteArray data;
data.setNum(tempReading->temperature());
QMqttTopicName topicName("Temperature");
QByteArray topicMessage(data);
mqttClient->publish(topicName, topicMessage);
}
最后,让我们看看任何已订阅的消息:
void SensorServer::messageReceived(const QByteArray &message, const QMqttTopicName &topic)
{
qDebug() << Q_FUNC_INFO << topic << message;
}
摘要
在本章中,我们探讨了使用 QSensors 读取设备传感器数据的多种方式。Qt 传感器支持许多平台:Android、iOS、WinRT、SensorTag、Sensorfw、Linux 通用和 Linux iio-sensor-proxy。Sensorfw 还支持 Linux 的 IIO 传感器。
我描述了如何实现自定义的 QSensor 和 QSensorBackend 以添加对 Qt 传感器当前不支持传感器的支持。
我们回顾了使用 QtMqtt 与 mqtt 代理通信的步骤,并探讨了如何使用 QWebsockets 与网页服务器进行通信。
然后,我将它们全部组合起来,从 Sense HAT 获取传感器数据,并通过 WebSocket 将它们发布到 mqtt 代理。
在下一章中,我们将讨论使用包含位置和位置的 GPS 数据以及地图绘制。
第八章:我在哪里?定位和定位
带有 GPS 芯片的设备无处不在。你甚至可以追踪你的猫或鸡!在本章中,你将学习如何使用 Qt 进行定位和定位服务。
Qt 定位包含来自各种来源的地理坐标,包括卫星、Wi-Fi 和日志文件。Qt 位置则是关于本地地点,例如服务,如餐厅或公共公园,以及路线信息。
在本章中,我们将涵盖以下主题:
-
使用卫星进行定位
-
映射位置
-
兴趣点
使用卫星进行定位
一部手机通常内置了 GPS 调制解调器,但也拥有其他定位信息来源,因此我将使用 Android 作为示例。我们将关注的 Qt 主要类如下:
这里是 Qt 定位 API:
-
QGeoSatelliteInfo -
QGeoLocation -
QGeoPositionInfoSource -
QGeoCoordinate
这里是 Qt 位置 API:
-
QPlaceSearchResult -
QPlaceContent -
QGeoRoute
首先,我们需要编辑.pro文件并添加QT += positioning。
QGeoSatelliteInfoSource
你可以使用QGeoSatelliteInfoSource来向用户展示卫星信息,它有一个static方法来获取QGeoSatelliteInfoSource。
源代码可以在 Git 仓库的Chapter08-1目录下的cp8分支中找到。
我们将首先调用QGeoSatelliteInfoSource::createDefaultSource。
QGeoSatelliteInfoSource *source = QGeoSatelliteInfoSource::createDefaultSource(this);
在某些系统,如 iOS 上,卫星信息不会公开到公共 API,因此QGeoSatelliteInfoSource在该平台上将不起作用。
这将在系统上构建一个QGeoSatelliteInfoSource对象,该对象是最高优先级的插件,这大致等同于以下操作:
QStringList geoSources = QGeoSatelliteInfoSource::availableSources();
QGeoSatelliteInfoSource *source = QGeoSatelliteInfoSource::createSource(geoSources.at(0),this);
有两个特别感兴趣的信号:satellitesInUseUpdated和satellitesInViewUpdated。此外,还有一个重载的error信号,因此我们需要使用特殊的QOverload语法:
connect(source, QOverload<QGeoSatelliteInfoSource::Error>::
of(&QGeoSatelliteInfoSource::error),
this, &SomeClass::error);
当系统使用的卫星数量发生变化时,会发出satellitesInUseUpdated信号。当系统可以看到的卫星数量发生变化时,会发出satellitesInViewUpdated信号。我们将接收到一个QGeoSatelliteInfo对象列表。
QGeoSatelliteInfo
让我们连接satellitesInViewUpdated信号,以便我们可以检测到卫星被找到:
connect(source, SIGNAL(satellitesInViewUpdated(QList<QGeoSatelliteInfo>)),
this, SLOT(satellitesInViewUpdated(QList<QGeoSatelliteInfo>)));
我们可以通过这种方式接收单个卫星的信息。包括卫星标识符、信号强度、仰角和方位角等信息:
void SomeClass::satellitesInViewUpdated(const QList<QGeoSatelliteInfo> &infos)
{
if (infos.count() > 0)
qWarning() << "Number of satellites in view:" << infos.count();
foreach (const QGeoSatelliteInfo &info, infos) {
qWarning() << " "
<< "satelliteIdentifier" << info.satelliteIdentifier()
<< "signalStrength" << info.signalStrength()
<< (info.hasAttribute(QGeoSatelliteInfo::Elevation) ? "Elevation "
+ QString::number(info.attribute(QGeoSatelliteInfo::Elevation)) : "")
<< (info.hasAttribute(QGeoSatelliteInfo::Elevation) ? "Azimuth " +
QString::number(info.attribute(QGeoSatelliteInfo::Azimuth)) : "");
}
}
在小屏幕上这里有很多东西可以看。每次更新都是新的一行,你可以看到它定位并添加不同的卫星,当它们进入视野时:

下一步是使用这些卫星来在全球上定位我们的位置。我们首先使用QGeoPositionInfoSource。
QGeoPositionInfoSource
我们可以通过使用QGeoPositionInfoSource来获取设备的纬度和经度位置,它封装了位置数据。类似于QGeoSatelliteInfoSource,它有两个static方法来创建source对象:
源代码可以在 Git 仓库的Chapter08-2目录下的cp8分支中找到。
QGeoPositionInfoSource *geoSource = QGeoPositionInfoSource::createDefaultSource(this);
我们感兴趣的QGeoPositionInfoSource信号是positionUpdated(const QGeoPositionInfo &update):
connect(geoSource, &QGeoPositionInfoSource::positionUpdated,
this, &MainWindow::positionUpdated);
要开始接收更新,请调用startUpdates();:
geoSource->startUpdates();
positionUpdated信号接收一个QGeoPositionInfo。
QGeoPositionInfo
QGeoPositionInfo包含一个QGeoCoordinate,其中包含我们的纬度和经度坐标,以及位置数据的时间戳。
它还可以包含以下可选属性:
-
Direction -
GroundSpeed -
VerticalSpeed -
MagneticVariation -
HorizontalAccuracy -
VerticalAccuracy
可以使用hasAttribute(QGeoPositionInfo::Attribute)检查属性,并使用attribute(QGeoPositionInfo::Attribute)函数检索:
if (positionInfo.hasAttribute(QGeoPositionInfo::MagneticVariation)
qreal magneticVariation = positionInfo.attribute(QGeoPositionInfo::MagneticVariation);
要获取纬度和经度信息,请调用QGeoPositionInfo类中的coordinate()函数,它返回一个QGeoCoordinate。
QGeoCoordinate
QGeoCoordinate包含纬度和经度坐标,可以通过调用相应的latitude()和longitude()函数找到。它可以由不同类型的数据组成,可以通过调用type()函数来发现,该函数返回一个QGeoCoordinate::CoordinateType的enum,它可以有以下值之一:
-
InvalidCoordinate:无效坐标 -
Coordinate2D:包含纬度和经度坐标 -
Coordinate3D:包含纬度、经度和高度坐标
我们可以通过调用QGeoPositionInfo对象的coordinate()函数从QGeoPositionInfo对象中获取QGeoCoordinate,该函数反过来具有latitude和longitude值:
QGeoCoordinate coords = positionInfo.coordinate();
QString("Latitude %1\n").arg(coords.latitude());
QString("Longitude %1\n").arg(coords.longitude());
if (coords.type() == QGeoCoordinate::Coordinate3D)
QString("Altitude %1\n").arg(coords.altitude())
让我们看看我们如何使用 Qt Quick 和 QML 来完成这个操作。
Qt Quick
有相应的 QML 元素可用于定位。
import语句将是import QtPositioning 5.12。
让我们用 QML 做同样简单的事情,并显示我们的纬度和经度值。
这里是之前提到的类的 Qt Quick 项目等效:
-
PositionSource:QGeoPositionInfoSource -
Position:QGeoPositionInfo -
Coordinate:QGeoCoordinate
Qt Quick 通常要简单得多,并且快速实现这些功能。
源代码可以在 Git 仓库的Chapter08-3目录下的cp8分支中找到。
我们使用 1,000 毫秒的updateInterval实现了PositionSource,这意味着设备的位置将每 1,000 毫秒更新一次。我们将其设置为active以开始更新:
PositionSource {
id: positionSource
updateInterval: 1000
active: true
此组件有一个名为onPositionChanged的信号,当位置改变时会被调用。我们接收改变后的坐标,然后可以使用它们:
onPositionChanged: {
var coord = positionSource.position.coordinate;
console.log("Coordinate:", coord.longitude, coord.latitude);
latitudeLabel.text = "Latitude: " + coord.latitude;
longitudeLabel.text = "Longitude: " + coord.longitude;
if (positionSource.position.altitudeValid)
altitudeLabel.text = "Altitude: " + coord.altitude;
}
}
现在我们知道了我们的位置,我们可以使用这些位置细节来获取坐标周围的一些详细信息,比如地图和位置详情。
映射位置
我们现在需要一个某种地图来显示我们的位置发现。
QML 的 Map 组件是 Qt 提供的唯一地图功能,因此您必须使用 Qt Quick。
Map 组件可以由各种后端插件支持。实际上,您需要指定您正在使用哪个插件。Map 内置支持以下插件:
| Provider | Key | Notes | Url |
|---|---|---|---|
| Esri | esri | 需要订阅 | www.esri.com |
| HERE | here | 需要访问令牌 | developer.here.com/terms-and-conditions |
| Mapbox | mapbox | 需要访问令牌 | www.mapbox.com/tos |
| Mapbox GL | mapboxgl | 需要访问令牌 | www.mapbox.com/tos |
| Open Street Map (OSM) | osm | 免费访问 | openstreetmap.org/ |
我将使用 OSM 和 HERE 提供商。
HERE 插件需要在 developer.here.com 上有一个账户。注册很容易,并且有一个免费版本。您需要应用程序 ID 和应用程序代码才能访问他们的地图和 API。
地图
要开始使用 Map 组件,在您选择的 .qml 文件中,在 import 行中添加 QtLocation 和 QtPositioning:
import QtLocation 5.12
import QtPositioning 5.12
源代码可以在 Git 仓库的 Chapter08-4 目录下的 cp8 分支中找到。
Map 组件需要一个 plugin 对象,其 name 属性是前面表格中的一个键。您可以通过设置 center 属性为一个坐标来设置地图的中心位置。
我正在使用 OSM 后端,并且它以澳大利亚的黄金海岸为中心:
Map {
anchors.fill: parent
plugin: Plugin {
name: "osm"
}
center: QtPositioning.coordinate(-28.0, 153.4)
zoomLevel: 10
}
地图以 center 属性中我们指定的坐标为中心,该属性用于将地图定位到用户的当前位置。
我们将地图的 plugin 属性定义为 "osm" 插件,这是 Open Street Map 插件的标识符。
显示地图就这么简单。
MapCircle
您可以通过在 Map 中放置一个 MapCircle 来突出显示一个区域。再次以黄金海岸为中心。
MapCircle 有一个 center 属性,我们可以通过使用一个带有符号十进制值的 latitude 和 longitude 位置来定义它。
这里 radius 属性的单位是米,根据地图。所以在我们这个例子中,MapCircle 的半径将是 5,000 米。
MapCircle {
center {
latitude: -28.0
longitude: 153.4
}
radius: 5000.0
border.color: 'red'
border.width: 3
opacity: 0.5
}
每个地图后端都有自己的参数,可以使用 Map 组件中的 PluginParameter 组件来设置。
PluginParameter
默认情况下,OSM 后端下载的是低分辨率的瓦片。如果您想要高分辨率的地图,您可以指定 'osm.mapping.highdpi_tiles' 参数:
PluginParameter {
name: 'osm.mapping.highdpi_tiles'
value: true
}
每个 PluginParameter 元素只包含一个 name/value 参数对。如果您需要设置多个参数,您将需要一个 PluginParameter 元素来设置每个:
PluginParameter { name: "osm.useragent"; value: "Mobile and Embedded Development with Qt5"; }
您可以考虑的其他 PluginParameters 包括各种地图提供商的令牌和应用程序 ID,例如 HERE 地图。
这是我们的地图在 Android 上运行的样子:

我们还可以使用地址在地图上使用其他 Qt Quick 元素。让我们看看路线规划。
RouteModel
要在地图上显示路线,您需要使用RouteModel,它是Map项的一个属性,RouteQuery用于添加航点,以及MapItemView用于显示它。
RouteModel需要一个插件,所以我们只是重用了Map项的插件。它还需要一个RouteQuery来设置其query属性:
RouteQuery {
id: routeQuery
}
RouteModel {
id: routeModel
plugin : map.plugin
query: routeQuery
}
MapItemView用于在地图上显示模型数据。它还需要一个MapRoute的代理。在我们的案例中,这是一条描述路线的线:
MapItemView {
id: mapView
model: routeModel
delegate: routeDelegate
}
Component {
id: routeDelegate
MapRoute {
id: route
route: routeData
line.color: "#46a2da"
line.width: 5
smooth: true
opacity: 0.8
}
}
现在我们需要的是一个起点、一个终点以及任何中间点。在这个例子中,我保持简单,只指定起点和终点。您可以通过使用QtPositioning.coordinate来指定 GPS 坐标,它接受纬度和经度值作为参数:
property variant startCoordinate: QtPositioning.coordinate(-28.0, 153.4)
property variant endCoordinate: QtPositioning.coordinate(-27.579744, 153.100175)
起始点坐标位于澳大利亚黄金海岸的某个随机区域;终点是南半球最后一个 Trolltech 办公室的位置。RouteQuery travelModes属性决定了路线是如何计算的,是开车、步行还是公共交通。它可以有以下几种值:
-
CarTravel -
PedestrianTravel -
BicycleTravel -
PublicTransit -
TruckTravel
RouteQuery属性routeOptimzations限制了查询到以下不同的值:
-
ShortestRoute -
FastestRoute -
MostEconomicRoute -
MostScenicRoute
在这个例子中,我在Component.onCompleted信号中触发了routeQuery。通常,这种操作会在用户配置查询后触发:
Component.onCompleted: {
routeQuery.clearWaypoints();
routeQuery.addWaypoint(startCoordinate)
routeQuery.addWaypoint(endCoordinate)
routeQuery.travelModes = RouteQuery.CarTravel
routeQuery.routeOptimizations = RouteQuery.FastestRoute
routeModel.update();
}
这是路线的显示方式。这条由蓝色线条指示的路线从大红色圆圈开始:

您可以添加更多Waypoints来建立不同的路线或通过将routeModel设置为ListView或类似的内容来获取逐段方向指示。
不仅 Qt Location 可以显示地图和路线,而且Places API 还支持显示兴趣点,如餐厅、加油站和国家公园。
兴趣点
在这一点上,我将切换到 HERE 地图插件。我尝试让 OpenStreetMaps 地点工作,但它找不到任何东西。
在我们地图构建的下一步中,我们使用PlaceSearchModel来搜索地点。与之前的RouteModel一样,MapItemView可以在我们的地图上显示此模型。
就像RouteModel一样,PlaceSearchModel需要一种显示数据的方式;我们可以选择ListView,这在某些用途上很有用,但让我们选择MapItemView以获得视觉效果。
我们需要使用searchArea和searchTerm来声明我们正在使用哪个插件:
PlaceSearchModel {
id: searchModel
plugin: mapPlugin
searchTerm: "coffee"
searchArea: QtPositioning.circle(startCoordinate)
Component.onCompleted: update()
}
我们的MapItemView和delegate代码如下。searchView代理将以图标的形式显示,其标题文本来自结果地点:
MapItemView {
id: searchView
model: searchModel
delegate: MapQuickItem {
coordinate: place.location.coordinate
anchorPoint.x: image.width * 0.5
anchorPoint.y: image.height
sourceItem: Column {
Image { id: image; source: "map-pin.png" }
Text { text: title; font.bold: true; color: "red"}
}
}
}
如您所见,地点点有些难以阅读,并且叠加在周围的点上。这表明附近有太多地点,对于缩放级别来说太近了,地图在放置名称时遇到了困难。您可以通过使用不同的缩放级别或使用一些碰撞检测和布局算法来解决这个问题,这里我不会深入讨论。

map-pin.png 图标来自 feathericons.com/,并采用开源 MIT 许可协议发布。
摘要
在本章中,我们使用 Qt Location 和 Qt Positioning 覆盖了映射的许多方面。我们可以使用 QGeoSatelliteInfo 获取卫星信息,并使用 QGeoPositionInfo 定位精确的当前位置坐标。我们学习了如何使用 Qt Quick Map 和不同的地图提供商来显示当前位置。我们介绍了如何使用 RouteModel 提供路线,使用 PlaceSearchModel 在附近搜索地点,并使用 MapItemView 显示它们。
在下一章中,我们将讨论使用 Qt Multimedia 的音频和视频。
第三部分:其他 API Qt SQL、Qt 多媒体和 Qt 购买
我们将从这个章节开始讨论一些适用于移动设备的实用 API,例如游戏中的音频和视频。然后,我们将转向设备中数据库带来的挑战,并学习如何通过网络远程利用数据库。我们还将讨论如何使用 Qt 购买功能启用应用内购买。
本节包含以下章节:
-
第九章, 声音与视觉 - Qt 多媒体
-
第十章, Qt SQL 的远程数据库
-
第十一章, 使用 Qt 购买功能启用应用内购买
第九章:声音与视觉 - Qt Multimedia
需要播放声音或显示视频的应用程序通常是游戏,而其他则是完整的多媒体应用程序。Qt Multimedia 可以处理这两者。
Qt Multimedia 可以与 Qt Widgets 和 Qt Quick 一起使用,甚至在没有 GUI 界面的情况下使用。它具有 C++ 和 QML API,但 QML API 有一些特殊的特性和技巧。一个鲜为人知的事实是,Qt 还可以在 Qt Quick 中播放 3D 位置音频。你可以用三个维度来控制增益和音调。
本章我们将涵盖以下主题:
-
声音振动 - 音频
-
图像传感器 - 相机
-
视觉媒体 - 播放视频
-
调谐它 - FM 收音机调谐器
声音振动 - 音频
我与音频的关系可以追溯到很久以前——在计算机成为家庭用品之前,当 Mylar 磁带和磁铁统治着声音领域的时候。从那时起,事物已经进步了。现在,移动电话可以放入我们的口袋,灯泡可以播放音乐。
Qt 中的 3D 音频通过 OpenAL API 支持。如果你使用的是 Linux,Qt 公司提供的默认 Qt 二进制文件不包含所需的 Qt 音频引擎 API。你必须安装 OpenAL 开发包,然后自行编译 Qt Multimedia。OpenAL 在 Android 上不受支持,所以在这方面没有乐趣。幸运的是,它在 Apple Mac 和 iOS 上默认支持。所以,我将在下一个部分进行开发。让我们拿起最近的 MacBook,前往那里。
3D 音频是三维空间中的音频,就像 3D 图形一样——不仅仅是左右,还有上下、前后位置。术语 位置音频 可能能更好地解释这一点。
在 Qt 中,3D 音频仅通过 Qt Quick 支持。
本章的源代码可以在 Git 仓库的 Chapter09-3dAudio 目录下的 cp9 分支中找到。
要使用 Qt Multimedia,你需要编辑项目的 .pro 文件并添加以下行:
QT += multimedia
编辑你想要在 3D 音频中使用的 qml 文件,并添加 import 行:
import QtAudioEngine 1.0
3D 空间由三个轴组成,分别命名为 x、y 和 z,它们对应于三维空间中的水平/垂直和上下方向。
AudioEngine 和其他相关类使用 Qt.vector3d 值类型。理解这个元素对于使用 3D 音频至关重要。
Qt.vector3d
Qt.vector3d 是一个表示 x、y 和 z 轴的值数组——x 表示水平,y 表示垂直,z 表示向上或向下。每个值都是一个单精度 qreal。
它可以使用如 Qt.vector3d(15, -5, 0) 或 "15, -5, 0" 作为 String。
音频的位置是通过使用 vector3d 属性值来控制的。
Qt.vector3d 用于在三维空间中定位音频。
在 QML 中使用 3D 音频的主要组件称为 AudioEngine。我们将使用的其他组件可以是此组件的子组件。
音频引擎
AudioEngine 是你将使用的其他 3D 音频项的中心容器。
我们可以轻松设置组件:
AudioEngine {
id: audioEngine
dopplerFactor: 1
speedOfSound: 343.33
}
dopplerFactor 属性创建多普勒频移效果。speedOfSound 值反映了计算多普勒效应时声音的速度。
您可以通过 listener 属性分配一个 listener。我们将在 AudioListener 部分中稍后讨论。
我们有一个想要加载并使用的音频样本,因此至少声明一个 AudioSample。
AudioSample
AudioSample 可以定义为 AudioEngine 组件的子组件:
AudioEngine {
id: audioEngine
dopplerFactor: 1
speedOfSound: 343.33
AudioSample {
name:"plink"
source: "thunder.wav"
preloaded: true
}
}
它也可以使用 AudioEngine.addAudioSample() 方法添加:
AudioEngine {
id: audioEngine
dopplerFactor: 1
speedOfSound: 343.33
addAudioSample(plinkSound)
}
AudioSample {
id: plinkSound
name:"plink"
source: "thunder.wav"
preloaded: true
}
source 属性包含样本的文件名和一个用于引用它的名称。
现在,我们准备好使用 Sound 组件播放声音。
Sound
Sound 元素是一个容器,可以包含一个或多个样本,这些样本将以不同的参数和变化播放。换句话说,您可以定义一个 PlayVariation 项目,它定义了 Sound 如何播放 AudioSample,包括音调和增益的最大值和最小值。您还可以声明样本为 looping,这意味着它会反复播放:
Sound {
name: "thunderengine"
attenuationModel: "thunderModel"
PlayVariation {
looping: true
sample: "plink"
maxGain: 0.5
minGain: 0.3
}
}
attenuationModel 属性控制声音音量水平下降的方式,或者随时间淡出。它可以取以下值之一:
-
线性是直线下降
-
反向是一个更自然、非线性的曲线
您可以使用 start、end 和 rolloff 属性来控制这一点。
AudioListener
AudioListener 组件代表 listener 以及其在 3D 空间的位置。只有一个 listener。它可以构建为 AudioEngine 组件的 listener 属性,或者作为一个可定义的元素:
AudioListener {
engine: audioEngine
position: Qt.vector3d(0, 0, 0)
}
SoundInstance 是 Sound 用于播放样本的组件。
SoundInstance
SoundInstance 有一些属性,您可以使用它们来调整声音:
-
direction -
gain -
pitch -
position
这些属性接受一个 vector3d 值。
SoundInstance 元素的 sound 属性接受一个字符串,表示 Sound 组件的名称:
SoundInstance {
id: plinkSound
engine: audioEngine
sound: "thunderengine"
position: Qt.vector3d(leftRightValue, forwardBacktValue,
upDownValue)
Component.onCompleted: plinkSound.play()
}
在这里,我在组件完成时开始播放声音。
现在,我们只需要一些机制来移动声音位置。如果我们设备上有加速度计,我们可以使用加速度计的值。我只会使用鼠标。记住,在触摸屏上,MouseArea 也包括触摸输入。
我们必须启用 hover 才能跟踪鼠标而不点击:
MouseArea {
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onPositionChanged: {
leftRightValue = -((window.width / 2) - mouse.x)
forwardBacktValue = (window.height / 2) - mouse.y
}
当使用 MouseArea 时,为了将鼠标点击传播到按钮或其他项目,请将 MouseArea 放在文件顶部,因为 Qt Quick 会按照从文件顶部到底部的组件顺序设置 z 轴顺序。您也可以设置按钮的 z 属性,并将 MouseArea 的 z 属性设置为最低值。
我之前在 Window 组件中声明了三个值,用于音频的位置:
property real leftRightValue: 0;
property real forwardBacktValue: 0;
property real upDownValue: 0;
现在,当您移动鼠标时,音频将看起来在移动。
但手机上没有鼠标。有一个触摸点,但没有滚动。我可以使用加速度计,因为它有 z 轴,或者使用PinchArea来控制上下位置。
让我们看看处理音频的几种其他方法。
音频
Audio元素可能是播放音频最简单的方法。它只需要几行代码。它非常适合播放音效。
源代码也可以在本书的 Git 仓库中找到,位于Chapter09-1目录下的cp9分支。
我们将使用以下import语句:
import QtMultimedia 5.12
这是一个简单的段落,将播放名为sample.mp3的.mp3文件:
Audio {
id: audioPlayer
source: "sample.mp3"
}
source属性是声明要播放哪个文件的地方。现在,你只需要调用play()方法来播放这个sample.wav文件:
Component.onCompleted: audioPlayer.play()
你也可以将autoPlay属性设置为true,而不是调用play,这样组件完成后就会播放文件。
设置音量就像声明volume属性并设置一个介于 0 和 1 之间的十进制值一样——1 表示全音量,0 表示静音:
volume: .75
从文件中获取元数据或 ID 标签并不明显,因为它们只有在metaDataChanged信号发出后才会可用。这个信号只由Audio元素的metaData对象发出。
有时,你可能需要显示文件的元数据,或者可以包含在音频文件头部的额外数据。Audio组件有一个metaData属性,可以像这样使用:
metaData {
onMetaDataChanged: {
titleLabel.text = "Title: " + metaData.title
artistLabel.text = "Artist: " + metaData.contributingArtist
albumLabel.text = "Album: " + metaData.albumTitle
}
}
如果你需要访问麦克风并录制音频,你需要深入到 C++,让我们看看QAudioRecorder。
QAudioRecorder
录制音频是我的一项爱好。录制音频,或者更具体地说使用麦克风,在某些平台上可能需要用户权限。
音频的录制,在我那个时代被称为录音,可以通过使用QAudioRecorder类来实现。录制属性由QAudioEncoderSettings类控制,你可以从中控制使用的编解码器、通道数、比特率和采样率。你可以显式设置比特率和采样率,或者使用更通用的setQuality函数。
源代码可以在本书的 Git 仓库中找到,位于Chapter09-2目录下的cp9分支。
你可能想查询输入设备并查看哪些设置可用。为此,你会使用QAudioDeviceInfo进行查询,遍历QAudioDeviceInfo::availableDevices(QAudio::AudioInput):
void MainWindow::listAudioDevices()
{
for (const QAudioDeviceInfo &deviceInfo :
QAudioDeviceInfo::availableDevices(QAudio::AudioInput)) {
ui->textEdit->insertPlainText(
QString("Device name: %1\n")
.arg(deviceInfo.deviceName()));
ui->textEdit->insertPlainText(
" Supported Codecs: "
+ deviceInfo.supportedCodecs()
.join(", ") + "\n");
ui->textEdit->insertPlainText(
QString(" Supported channel count: %1\n")
.arg(stringifyIntList(deviceInfo.supportedChannelCounts())));
ui->textEdit->insertPlainText(
QString(" Supported bit depth b/s: %1\n")
.arg(stringifyIntList(deviceInfo.supportedSampleSizes())));
ui->textEdit->insertPlainText(
QString(" Supported sample rates Hz: %1\n")
.arg(stringifyIntList(deviceInfo.supportedSampleRates())));
}
}
Qt 多媒体使用“样本大小”这个术语来指代更常见的“位深度”。
如从我笔记本电脑上所见,我有几个不同的音频输入设备。笔记本电脑的内置音频芯片因电涌而损坏,这就是为什么它在这里没有显示:

对于 iPhone,情况不同。它只有一个音频设备,名为default:

我的 Linux 桌面因为 ALSA 驱动程序报告了大量的音频输入设备,这里不包括在内。
我们需要设置录音编码设置,包括我们想要录制的音频文件的类型、通道数、编码、采样率和比特率:
QAudioEncoderSettings audioSettings;
audioSettings.setCodec("audio/pcm");
audioSettings.setChannelCount(2);
audioSettings.setBitRate(16);
audioSettings.setSampleRate(44100);
如果您想让系统决定各种设置,使用setQuality函数会更快捷,代码也更少,该函数可以接受以下值之一:
-
QMultimedia::VeryLowQuality -
QMultimedia::LowQuality -
QMultimedia::NormalQuality -
QMultimedia::HighQuality -
QMultimedia::VeryHighQuality
让我们选择NormalQuality,这将给出相同的结果:
audioSettings.setQuality(QMultimedia::NormalQuality);
QAudioRecorder类用于录音,所以让我们构建一个QAudioRecorder并设置编码设置:
QAudioRecorder *audioRecorder = new QAudioRecorder(this);
audioRecorder->setEncodingSettings(audioSettings);
您还可以指定要使用的音频输入,但首先您需要获取可用音频输入的列表:
QStringList inputs = audioRecorder->audioInputs();
如果您不想麻烦选择哪个音频设备,您可以使用defaultAudioInput()函数指定默认设备:
audioRecorder->setAudioInput(audioRecorder->defaultAudioInput());
我们可以将它保存到文件,甚至网络位置,因为setOutputLocation函数接受一个QUrl。我们只需指定一个本地文件来保存:
audioRecorder->setOutputLocation(QUrl::fromLocalFile("record1.wav"));
如果文件是相对的,就像这里一样,一旦开始录音,您可以使用outputLocation()获取实际输出位置。
最后,我们可以开始录音过程:
audioRecorder->record();
还有一些方法来控制录音操作,比如stop()和pause()。
当然,您会想连接到错误信号,因为错误有时会发生。再次注意在错误报告信号中使用的QOverload语法:
connect(audioRecorder, QOverload<QMediaRecorder::Error>::of(&QMediaRecorder::error),
={
ui->textEdit->insertPlainText("QAudioRecorder Error: " + audioRecorder->errorString());
on_stopButton_clicked();
});
因此,现在我们已经录制了一些音频,我们可能想听听它。这就是QMediaPlayer发挥作用的地方。
QMediaPlayer
QMediaPlayer相当简单。它可以播放音频和视频,但在这里我们只会播放音频。首先,我们需要通过调用setMedia来设置要播放的媒体。
我们可以使用QAudioRecorder获取输出文件并使用它来播放:
player = new QMediaPlayer(this);
player->setMedia(audioRecorder->outputLocation());
我们将不得不监控当前播放位置,因此我们将positionChanged信号连接到一个进度条:
connect(player, &QMediaPlayer::positionChanged,
this, &MainWindow::positionChanged);
连接错误信号及其QOverload语法:
connect(player, QOverload<QMediaPlayer::Error>::of(&QMediaPlayer::error),
={
ui->textEdit->insertPlainText("QMediaPlayer Error: " + player->errorString());
on_stopButton_clicked();
});
然后,只需在QMediaPlayer对象上调用play()即可:
player->play();
您甚至可以设置播放音量:
player->setVolume(75);
如果您需要访问媒体数据,比如说获取播放时的音量级别,您可能需要使用除了QMediaPlayer之外的其他方式来播放您的文件。
QAudioOutput
QAudioOutput提供了一种将音频发送到音频输出设备的方式:
QAudioOutput *audio;
使用QAudioOutput,您需要设置文件的精确格式。要获取文件的格式,您可以使用QMediaResource。
抱歉,QMediaResource在 Qt 6.0 中被弃用,并且它并没有按照文档所说的那样工作,也没有像预期的那样工作。我们需要硬编码数据格式,因此我们将使用基本的优质立体声格式。QAudioFormat是这样做的方式:
QAudioFormat format;
format.setSampleRate(44100);
format.setChannelCount(2);
format.setSampleSize(16);
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::UnSignedInt);
我们将遍历音频设备并检查QAudioDeviceInfo是否支持此格式:
for (const QAudioDeviceInfo &deviceInfo : QAudioDeviceInfo::availableDevices(QAudio::AudioOutput)) {
if (deviceInfo.isFormatSupported(format)) {
audio = new QAudioOutput(deviceInfo, format, this);
connect(audio, &QAudioOutput::stateChanged, [=] (QAudio::State
state) {
qDebug() << Q_FUNC_INFO << "state" << state;
if (state == QAudio::StoppedState) {
if (audio->error() != QAudio::NoError) {
qDebug() << Q_FUNC_INFO << audio->error();
}
}
});
}
在这里,我连接到了stateChanged信号并测试了状态是否为StoppedState;我们知道可能存在错误,因此我们检查QAudioOutput对象的error()。否则,我们可以播放文件:
QFile sourceFile;
sourceFile.setFileName(file);
sourceFile.open(QIODevice::ReadOnly);
audio->start(&sourceFile);
现在,我们看到 Qt 多媒体有各种播放音频的方式。现在,让我们来看看相机和录制视频。
图像传感器 - 相机
首先,我们应该确定设备是否有任何相机。这有助于我们确定相机使用的具体细节以及其他相机规格,例如设备上的方向或位置。
对于此操作,我们将使用QCameraInfo。
QCameraInfo
我们可以使用QCameraInfo::availableCameras()函数获取相机列表:
源代码可以在本书的 Git 仓库的Chapter09-4目录下的cp9分支中找到。
QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
foreach (const QCameraInfo &cameraInfo, cameras)
ui->textEdit->insertPlainText(cameraInfo.deviceName() + "\n");
在我的 Android 设备上,我看到了两个相机,分别命名为back和front。您也可以使用QCameraInfo::position()检查front和back相机,它将返回以下之一:
-
QCamera::UnspecifiedPosition -
QCamera::BackFace -
QCamera::FrontFace
FrontFace表示相机镜头与屏幕在同一侧。然后您可以使用QCameraInfo来构建QCamera对象:
QCamera *camera;
if (cameraInfo.position() == QCamera::BackFace) {
camera = new QCamera(cameraInfo);
}
现在,检查相机支持的捕获模式,这可以是以下之一:
-
QCamera::CaptureViewfinder -
QCamera::CaptureStillImage -
QCamera::CaptureVideo
首先,让我们先进行一次快速静态图像拍摄。我们需要告诉相机使用QCamera::CaptureStillImage模式:
camera->setCaptureMode(QCamera::CaptureStillImage);
statusChanged信号用于监控状态,可以是以下值之一:
-
QCamera::UnavailableStatus -
QCamera::UnloadedStatus -
QCamera::UnloadingStatus -
QCamera::LoadingStatus -
QCamera::LoadedStatus -
QCamera::StandbyStatus -
QCamera::StartingStatus -
QCamera::StoppingStatus -
QCamera::ActiveStatus
让我们连接到statusChanged信号,这样我们就可以看到状态变化:
connect(camera, &QCamera::statusChanged, [=] (QCamera::Status status) {
ui->textEdit->insertPlainText(QString("Status changed %1").arg(status) + "\n");
});
如果您需要调整任何相机设置,您必须在获取对QCameraImageProcessing对象的访问权限之前先load()它:
camera->load();
QCameraImageProcessing *imageProcessor = camera->imageProcessing();
使用QCameraImageProcessing类,您可以设置配置,例如亮度、对比度、饱和度和锐化。
在我们对相机调用start之前,我们需要为相机设置一个QMediaRecorder对象。由于QCamera是从QMediaObject继承的,我们可以将其馈送到QMediaRecorder对象。
Qt 多媒体小部件在 Android 上不受支持。
我在 Mac 和 iOS 上尝试了QCamera版本 5.12,但在尝试start()相机时它总是崩溃。在 Linux 桌面上我成功了。在 Android 上,由于多媒体小部件不受支持,相机取景器小部件无法工作,但我仍然可以从图像传感器捕获图像。
也许你在 QML 方面会有更好的运气。QML API 通常针对易于使用进行了优化。
Camera
是的,QML 的Camera实现起来要容易得多。实际上,您只需要两个组件来拍照:Camera和VideoOutput。
VideoOutput是用于取景器的元素。在您录制视频时也会使用它:
Camera {
id: camera
position: Camera.BackFace
onCameraStateChanged: console.log(cameraState)
imageCapture {
onImageCaptured: {
console.log("Image captured")
}
}
}
position属性控制使用哪个相机,尤其是在可能具有前置和后置相机的移动设备上。在这里,我不仅使用后置相机。您可以使用FrontFace位置来拍摄自拍。
imageCaptured与CameraCapture子元素相关。我们可以处理onImageCaptured信号来预览图像或提醒用户已拍照。
Camera对象的其余属性可以通过它们相应的组件进行控制:
-
focus : CameraFocus -
flash : CameraFlash -
曝光 : CameraExposure -
imageProcessing : CameraImageProcessing -
imageCapture : CameraCapture -
videoRecorder: CameraRecorder
CameraRecorder是您用来控制饱和度、亮度、颜色滤镜、对比度和其他设置的元素。
CameraExposure控制诸如光圈、曝光补偿和快门速度等事项。
CameraFlash可以打开、关闭闪光灯或使用自动模式。它还可以设置红眼消除和视频(恒定)模式。
我们需要一个取景器来查看我们试图捕捉的是什么,让我们看看VideoOutput元素。
视频输出
VideoOutput是我们用来查看相机所感知内容的组件。
源代码可以在 Git 仓库的Chapter09-5目录下的cp9分支中找到。
要实现VideoOutput组件,您需要定义source属性。在这里,我们使用的是相机:
VideoOutput {
id: viewfinder
source: camera
autoOrientation: true
}
autoOrientation属性用于允许VideoOutput组件补偿图像传感器的设备方向。如果没有这个属性为真,图像可能会以错误的方向显示在取景器中,从而混淆用户,使得拍摄好照片或视频变得更加困难。
让我们通过添加一个MouseArea来使这个VideoOutput可点击,我将使用onClicked和onPressAndHold信号来聚焦并实际捕捉图像:
MouseArea {
anchors.fill: parent
onPressAndHold: {
captureMode: captureSwitch.position === 0 ?Camera.CaptureStillImage : Camera.CaptureVideo
camera.imageCapture.capture()
}
onClicked: {
if (camera.lockStatus == Camera.Unlocked)
camera.unlock();
camera.searchAndLock();
}
}
我还添加了来自 Qt Quick Controls 的Switch组件来控制用户是否想要记录静态照片或视频。
要聚焦相机,请调用searchAndLock()方法,它将启动聚焦、白平衡和曝光计算。
让我们添加录制视频的支持。我们将向Camera组件添加一个CameraRecorder容器:
VideoRecorder {
audioEncodingMode: CameraRecorder.ConstantBitrateEncoding;
audioBitRate: 128000
mediaContainer: "mp4"
}
我们可以为视频设置某些方面,例如比特率、帧率、音频通道数以及要使用的容器。
我们还需要更改onPressAndHold信号的工作方式,以确保当用户通过开关指定时,我们记录视频。
onPressAndHold: {
captureMode: captureSwitch.position === 0 ? Camera.CaptureStillImage : Camera.CaptureVideo
if (captureSwitch.position === 0)
camera.imageCapture.capture()
else
camera.videoRecorder.record()
}
我们需要一种停止录制的方法,所以让我们修改onClicked信号处理程序,以便在RecordingState时停止录制。
onClicked: {
if (camera.videoRecorder.recorderState === CameraRecorder.RecordingState) {
camera.videoRecorder.stop()
} else {
if (camera.lockStatus == Camera.Unlocked)
camera.unlock();
camera.searchAndLock();
}
}
现在,我们需要真正看到我们刚刚录制的视频。让我们继续并看看如何播放视频。
视觉媒体 - 播放视频
使用 QML 播放视频与使用MediaPlayer播放音频类似,只是使用VideoOutput组件代替AudioOutput组件。
源代码可以在 Git 仓库的Chapter09-6目录下的cp9分支中找到。
我们首先实现一个MediaPlayer组件:
MediaPlayer {
id: player
命名为autoPlay的属性将控制组件完成后自动开始视频。
在这里,source属性设置为我们的视频文件名:
autoPlay: true
source: "hellowindow.m4v"
onStatusChanged: console.log("Status " + status)
onError: console.log("Error: " + errorString)
}
然后我们创建一个VideoOutput组件,源为我们的MediaPlayer:
VideoOutput {
source: player
anchors.fill : parent
}
MouseArea {
id: playArea
anchors.fill: parent
onPressed: player.play();
}
这里使用的MouseArea,即整个应用程序,用于在您点击应用程序的任何地方时开始播放视频。
使用 C++,您会使用QMediaPlayer类与QGraphicsVideoItem、QVideoWidget或其他类似的东西。
由于QMultimediaWidgets在移动设备上的支持有限,我将把这留作读者的练习。
Qt 多媒体也支持 FM、AM 以及一些其他收音机,前提是您的设备中也有收音机。
调谐它 – FM 收音机调谐器
一些安卓手机有 FM 收音机接收器。我的手机就有!它需要插入有线耳机才能作为天线工作。
我们首先实现一个Radio组件:
Radio {
id: radio
Radio元素有一个band属性,您可以使用它来配置收音机的频率波段使用。它们是以下之一:
-
Radio.AM: 520 - 1610 kHz -
Radio.FM: 87.5 - 108 MHz,日本 76 - 90 MHz -
Radio.SW: 1.711 到 30 MHz -
Radio.LW: 148.5 到 283.5 kHz -
Radio.FM2: 范围未定义
band: Radio.FM
Component.onCompleted {
if (radio.availability == Radio.Available)
console.log("Good to go!")
else
console.log("Sad face. No radio found. :(")
}
}
availability属性可以返回以下不同的值:
-
Radio.Available -
Radio.Busy -
Radio.Unavailable -
Radio.ResourceMissing
用户首先会用收音机搜索电台,这可以通过使用searchAllStations方法完成,该方法接受以下值之一:
-
Radio.SearchFast -
Radio.SearchGetStationId: 与SearchFast类似,它发出stationFound信号
stationsFound信号为每个调谐的电台返回一个int类型的frequency和一个stationId字符串。您可以在基于模型的组件中收集这些信息,例如ListView,使用ListModel。ListView将使用ListModel作为其模型。
您可以通过调用cancelScan()方法取消扫描。scanUp()和scanDown()方法与searchAllStations类似,但不会记住它找到的电台。tuneUp和tuneDown方法将根据frequencyStep调整频率上下一个步长。
这里有一些其他有趣的属性:
-
antennaConnected: 如果连接了天线则为 True -
signalStrength: 信号强度,百分比% -
frequency: 保存和设置收音机调谐到的频率
摘要
在本章中,我们讨论了 Qt 多媒体大 API 的不同方面。你现在应该能够以三维方式定位声音,用于三维游戏。我们学习了如何录制和播放音频和视频,以及如何控制和使用摄像头来拍摄自拍。我们还简要介绍了如何使用 QML 来收听电台。
在下一章中,我们将深入探讨如何使用 QSqlDatabase 访问数据库。
第十章:使用 Qt SQL 的远程数据库
Qt SQL 不依赖于任何特定的数据库驱动程序。相同的 API 可以与各种流行的数据库后端一起使用。数据库可以拥有巨大的存储空间,而移动和嵌入式设备存储量有限,嵌入式设备比手机更有限。您将学习如何使用 Qt 通过网络远程访问数据库。
我们将在本章中介绍以下主题:
-
驱动程序
-
连接到数据库
-
创建数据库
-
向数据库添加
技术要求
您可以在 cp10 分支中获取本章的源代码,地址为 git clone -b cp10 https://github.com/PacktPublishing/Hands-On-Mobile-and-Embedded-Development-with-Qt-5。
您还应该为您的系统安装了 sqlite 或 mysql 软件包。
驱动程序是数据库后端
Qt 支持各种数据库驱动程序或后端,这些后端封装了各种系统数据库,并允许 Qt 拥有一个统一的 API 前端。Qt 支持以下数据库类型:
| 数据库类型 | 软件 |
|---|---|
| QDB2 | IBM Db2 |
| QIBASE | Borland InterBase |
| QMYSQL | MySQL |
| QOCI | Oracle Call Interface |
| QODBC | ODBC |
| QPSQL | PostgreSQL |
| QSQLITE | SQLite 版本 3 或以上 |
| QSQLITE2 | SQLite 版本 2 |
| QTDS | Sybase Adaptive Server |
我们将探讨 QMYSQL 类型,因为它支持远程访问。MySQL 可以安装在树莓派上。 QSQLITE3 可以在网络资源上共享并支持远程访问,iOS 和 Android 也支持 SQLite。
设置
MySQL 数据库需要配置以允许您远程访问它。让我们看看我们如何做到这一点:
-
您需要安装服务器和/或客户端。
-
然后,我们将创建数据库并在需要时使其可通过网络访问。这将通过命令行和终端应用程序来完成。
MySQL 服务器
我使用的是 Ubuntu,因此这些命令将主要针对基于 Debian 的 Linux。如果您使用的是不同的 Linux 发行版,只有安装命令会有所不同。您应根据您的发行版安装 MySQL 服务器和客户端。创建数据库的命令将是相同的。
这是我们将如何设置服务器的方式:
- 您需要安装 MySQL 服务器和客户端:
sudo apt-get install mysql-server mysql-client
- 运行
sudo mysql_secure_installation,这将允许您设置 root 账户。然后,登录到mysqlroot 账户:
sudo mysql -u root -p
-
创建新的数据库
username和password:GRANT ALL PRIVILEGES ON *.* TO 'username'@'localhost' IDENTIFIED BY 'password';。将username替换为您的数据库用户,将password替换为您想要用于访问此数据库的密码。 -
要使服务器可以从除了 localhost 之外的主机访问,请编辑
/etc/mysql/mysql.conf.d/mysqld.cnf。 -
将
bind-address = localhost行更改为bind-address = <your ip>,其中<your ip>是数据库所在机器的 IP 地址。然后,重启mysql服务器:
sudo service mysql restart
在你的 MySQL 控制台中,允许远程用户访问数据库:
GRANT ALL ON *.* TO 'username'@'<your ip>' IDENTIFIED BY 'password';
将<your ip>更改为客户端设备的 IP 地址或主机名,username更改为你在 MySQL 服务器上使用的用户名,password更改为他们将要使用的密码。
SQLite
SQLite 是一个基于文件的数据库,因此没有服务器这样的东西。我们仍然可以通过网络文件系统远程连接到它,例如 Windows 文件共享/Samba、网络文件系统(NFS)或 Linux 上的安全外壳文件系统(SSHFS)。SSHFS 允许你像本地文件系统一样挂载和访问远程文件系统。
除非你需要,否则没有必要手动使用晦涩的命令来创建数据库,因为我们将会使用 Qt 来创建它!
在 Android 上,有 Samba 客户端,可以将 Windows 网络共享挂载,这样我们就可以使用它。如果你使用 Raspberry Pi 或其他开发板,你可能能够使用 SSHFS 通过 SSH 挂载远程目录。
连接到本地或远程数据库
一旦我们配置并启动了数据库,现在我们可以使用相同的函数连接到它,无论它是本地数据库还是远程数据库。现在,让我们看看如何编写代码来连接到数据库,无论是本地还是远程。
数据库要么是本地可用的,这通常意味着在同一台机器上,要么通过网络远程访问。使用 Qt 连接到这些不同的数据库基本上是相同的。并非所有数据库都支持远程访问。
首先,让我们使用本地数据库。
要使用sql模块,我们需要将sql添加到配置文件中:
QT += sql
要在 Qt 中连接到数据库,我们需要使用QSqlDatabase类。
QSqlDatabase
尽管名字叫QSqlDatabase,但它代表的是对数据库的连接,而不是数据库本身。
要创建数据库连接,你首先需要指定你正在使用的数据库类型。它以支持数据库的字符串表示形式引用。让我们首先选择 MySQL 的QMYSQL数据库。
源代码可以在 Git 仓库的Chapter10-1目录下的cp10分支中找到。
要使用QSqlDatabase,我们首先需要添加数据库以创建其实例。
静态的QSqlDatbase::addDatabase函数接受一个参数,即数据库类型,并将数据库实例添加到数据库连接列表中。
在这里,我们添加一个 MySQL 数据库,所以使用QMYSQL类型:
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
如果你正在连接到 SQLite 数据库,请使用MSQLITE数据库类型:
QSqlDatabase db = QSqlDatabase::addDatabase("MSQLITE");
大多数数据库都需要用户名和密码。要设置username和password,请使用以下命令:
db.setUserName("username");
db.setPassword("password");
由于我们正在连接到远程 MySQL 数据库,因此我们需要指定主机名。它也可以是一个 IP 地址:
db.setHostName("10.0.0.243");
要开始连接,请调用 open() 函数:
bool ok = db.open()
open 返回一个 bool,如果成功则为 true,如果失败则为 false,在这种情况下我们可以检查错误:
if (!db.open()) {
qWarning() << dq.lastError.text();
}
如果这成功打开,我们就连接到了数据库。
让我们实际上创建远程数据库,因为我们有需要的权限。
创建和打开数据库
对于 SQLite 数据库,一旦我们打开它,它就会在文件系统中创建数据库。对于 MySQL,我们必须发送 MySQL 命令来创建数据库。我们使用 QSqlQuery 构建 SQL 查询来完成这一点。QSqlQuery 将数据库对象作为参数:
QSqlQuery query(db);
要发送查询,我们在 QSqlQuery 对象上调用 exec() 函数。它需要一个 String 作为典型的 query 语法:
QString dbname = "MAEPQTdb";
if (!query.exec("CREATE DATABASE IF NOT EXISTS " + dbname)) {
qWarning() << "Database query error" << query.lastError().text();
}
dbname 这里是任何我们希望数据库名称为 String;我正在使用 MAEPQT db。
如果这个命令失败,我们发出警告消息。如果成功,我们就继续发出命令来 USE 它,所以我们调用另一个 query 命令:
query.exec("USE " + dbname);
从这里开始,我们需要创建一些表格。我会让它保持简单,并填充一些数据。
我们开始另一个查询,但使用空命令,并将 db 对象作为第二个参数,这将创建指定数据库上的 QSqlQuery 对象,但在我们准备好之前不会执行任何命令:
QSqlQuery q("", db);
q.exec("drop table Mobile");
q.exec("drop table Embedded");
q.exec("create table Mobile (id integer primary key, Device varchar,
Model varchar, Version number)");
q.exec("create table Embedded (id integer primary key, Device varchar,Model varchar, Version number)");
数据库已准备就绪,因此现在我们可以添加一些数据。
向数据库添加数据
Qt 文档指出,保留 QSqlDatabase 对象不是一个最佳实践。
这里有一些不同的方法我们可以这样做:
- 我们可以使用
QSqlDatabase::database来获取已打开数据库的实例:
QSqlDatabase db = QSqlDatabase::database("MAEPQTdb");
QSqlQuery q("", db);
q.exec("insert into Mobile values (0, 'iPhone', '6SE', '12.1.2')");
q.exec("insert into Mobile values (1, 'Moto', 'XT1710-09', '2')");
q.exec("insert into Mobile values (1, 'rpi', '1', '1')");
q.exec("insert into Mobile values (1, 'rpi', '2', '2')");
q.exec("insert into Mobile values (1, 'rpi', '3', '3')");
- 我们还可以使用
QSqlQuery的另一个函数,名为prepare(),它使用代理变量准备查询字符串以执行。
然后,我们可以使用 bindValue 将值绑定到其标识符:
q.prepare("insert into Mobile values (id, device, model, version)"
"values ( :id, :device, :model, :version)");
q.bindValue(":id", 0);
q.bindValue(":device", "iPhone");
q.bindValue(":model", "6SE");
q.bindValue(":version", "12.1.2");
q.exec();
- 作为一种替代方案,你可以使用
bindValue函数,并将第一个参数设置为标识符的位置索引,从数字 0 开始,向上通过值进行操作:
q.bindValue(1, "iPhone");
q.bindValue(3, "12.1.2");
q.bindValue(2, "6SE");
- 你也可以按值的顺序使用
bindValue:
q.bindValue(0);
q.bindValue("iPhone");
q.bindValue("6SE");
q.bindValue("12.1.2");
接下来,让我们看看如何从数据库中检索数据。
执行查询
到目前为止,我们一直在运行查询,但没有返回任何数据。数据库的一个要点是查询数据,而不仅仅是输入数据。如果我们只能输入数据,那会有什么乐趣呢?Qt API 有一种方法来适应不同的语法和查询的数百万种方式。大多数情况下,它特定于需要返回的数据类型,但也特定于数据库数据本身。幸运的是,QSqlQuery 足够通用,查询参数是一个字符串。
QSqlQuery
要检索数据,请使用 QsqlQuery 执行查询,然后使用以下函数对记录进行操作:
-
first() -
last() -
next() -
previous() -
seek(int)
first() 和 last() 函数分别用于检索第一条和最后一条记录。要反向遍历记录,请使用 previous()。seek (int) 函数接受一个整数作为参数,以确定要检索的记录。
我们将使用 next(),它将遍历查询中找到的记录:
QSqlDatabase db = QSqlDatabase::database("MAEPQTdb");
QSqlQuery query("SELECT * FROM Mobile", db);
int rowCount = 0;
while (query.next()) {
QString id = query.value(0).toString();
QString device = query.value(1).toString();
QString model = query.value(2).toString();
QString version = query.value(3).toString();
ui->tableWidget->setRowCount(rowCount + 1);
ui->tableWidget->setItem(rowCount, 0, new QTableWidgetItem(id));
ui->tableWidget->setItem(rowCount, 1, new
QTableWidgetItem(device));
ui->tableWidget->setItem(rowCount, 2, new
QTableWidgetItem(model));
ui->tableWidget->setItem(rowCount, 3, new
QTableWidgetItem(version));
rowCount++;
}
我们还使用 value 来检索每个字段的数据,它需要一个 int,表示从 0 开始的记录位置。
您还可以使用 QSqlRecord 和 QSqlField 来做同样的事情,但可以更清晰地了解实际发生的情况:
QSqlField idField = record.field("id");
QSqlField deviceField = record.field("device");
QSqlField modelField = record.field("model");
QSqlField versionField = record.field("version");
qDebug() << Q_FUNC_INFO
<< modelField.name()
<< modelField.tableName()
<< modelField.value();
要获取记录数据,请使用 value(),它将返回一个 QVariant,表示该记录字段的值。
我们本可以使用基于模型的控件,然后使用 QsqlQueryModel 来执行查询。
QSqlQueryModel
QSqlQueryModel 继承自 QSqlQuery,并返回一个可以用于基于模型的控件和其他类的模型对象。如果我们将我们的 QTableWidget 改为 QTableView,我们可以使用 QSqlQueryModel 作为其数据模型:
QSqlQueryModel *model = new QSqlQueryModel;
model->setQuery("SELECT * FROM Mobile", db);
tableView->setModel(model);
这里是我的运行数据库示例的 Raspberry Pi(带有乐高支架!)使用 MySQL 插件远程运行:

摘要
在本章中,我们了解到 QSqlDatabase 代表了对任何受支持的数据库的连接。您可以使用它来登录远程 MySQL 数据库或网络共享上的 SQLite 数据库。要执行数据检索,请使用 QSqlQuery。您使用相同的类来设置数据、表和其他数据库命令。如果您正在使用模型-视图应用程序,则可以使用 QSqlDatabaseModel。
在下一章中,我们将探讨使用 Qt Purchasing 模块进行应用内购买。
第十一章:使用 Qt 购买启用应用内购买
在手机上应用内购买对于生成更多收入至关重要。Qt 利用系统 API 将应用内购买引入 Qt 应用。Android 和 iOS 都有自己的应用商店,每个商店都有自己的产品注册方法。这就是 Qt 购买发挥作用的地方!
在本章中,我们将涵盖以下主题:
-
在 Android 和 iOS 商店注册
-
创建应用内产品
-
恢复购买
在 Android Google Play 上注册
销售移动应用程序往往是不稳定的;只有少数可供购买的应用程序实际上能赚钱。目前赚钱的最好方法之一是让您的应用程序免费下载,但包含应用内购买。这样,人们可以尝试您的应用程序,同时也有机会在需要增强体验时进行购买。本节假设您已经将应用程序注册到相关的移动商店。要激活应用内购买,您首先需要注册您打算出售的商品。这有其自身的优势,因为它允许测试人员 购买 并安装您打算出售的商品。
首先,让我们看看如何在 Android 设备上完成这项操作:
-
您首先需要注册一个 Google 开发者账户,以便创建可在 Google Play 商店中使用的应用程序,
developer.android.com。 -
您还需要添加和编辑
AndroidManifest.xml文件。
在 Qt Creator 中,导航到:
项目 | 构建 | 构建设置 | 构建 Android APK | 创建模板。在这里,您需要编辑包名,理想情况下使用约定,com.<公司>.<应用程序名称>。当然,还有其他命名约定可供选择,您可以将其命名为任何您想要的名称。
-
当您在 Google Play 商店更新您的包时,版本号必须递增。最简单的方法是勾选名为“包含 Qt 模块默认权限”的复选框。如果不勾选,您需要确保添加
uses-permission android:name="com.android.vending.BILLING"权限。 -
您还需要使用您的证书签名此包,因此如果您还没有这样做,请创建一个密钥库。
-
在 Google 的应用内购买被称为 Google Play Billing,而 Google Play Console 是您发布应用到 Google Play 商店时访问的网站名称。您需要注册为开发者并支付注册费。(对我来说,是 25 澳元。)一旦支付了费用,您就可以设置一个商户账户。
-
之后,是时候提供有关您应用程序的信息并上传商店图形,如截图和宣传视频。这也是您需要提供客户联系详情的地方。
-
您可以通过仅向组织内的开发者提供内部测试来开发应用内购买。一旦解决了问题,并且您的应用进入 alpha 阶段,您可以扩大测试并执行封闭测试。之后,在 beta 开发期间,您可以进行公开测试。
-
在 Google Play Console 网站上,点击您的应用并导航到商店展示 | 应用内产品 | 管理产品。
-
然后,点击蓝色按钮创建管理产品,如下截图所示:

这将打开一个名为新管理产品的新表单,如下截图所示:

- 在此表单中,完成以下字段:
-
产品 ID:这将用于 Qt 应用程序标识符
-
标题
-
描述
-
状态:活动或非活动
-
价格:此价格限制在$0.99 和$550.00 之间
- 然后,点击保存。您将在 Google Play 上注册。
如果您为 Android 和 iOS 使用相同的产品 ID,将使开发应用内购买的过程更容易。
注册 iOS 应用商店
您应该已经注册在 Apple 开发者计划中,并已接受所有与税务、银行和其他数据相关的必要协议。
本节假设您已经注册了应用 ID,已签署相关协议等。在 iOS 上注册应用内购买相对简单:
-
导航到您的 Apple App Store Connect 账户并登录。点击应用,因为我们将要注册应用的内置产品。
-
点击您的应用,然后选择功能。在页面顶部,点击包含加号标记的蓝色圆圈,标记为应用内购买(0),如下截图所示:

您可以从以下选项中选择:
| 可消耗 | 应用一次使用后需要重新购买的项目 |
|---|---|
| 非消耗性 | 一次性购买但不失效的项目 |
| 自动续订订阅 | 自动续订的订阅内容 |
| 非续订订阅 | 不再续订的订阅内容 |
- 您将需要填写此过程的部分表格,因此事先决定以下标记项的值:
-
参考名称
-
产品 ID
-
价格
-
分级价格(起价$1.49)
-
开始日期
-
结束日期
-
显示名称
-
描述
-
截图
-
审查备注
一旦您有了产品 ID,请记住此信息以备后用。您在 Qt Creator 创建应用内购买产品时将需要它。
创建应用内产品
现在,真正的乐趣开始了!假设您设计了一个寻宝游戏,用户在地图上移动并寻找宝藏。在这种情况下,您可能希望提供加速游戏玩法,用户可以购买提示来帮助他们找到游戏的隐藏宝藏。
在我们的例子中,我们将出售颜色。颜色真的很好,因为它们是可收集的,并且用户可以相互之间出售和交易。
当你根据上一节中提到的在iOS App Store和Android Google Play上注册的说明开发并注册了你的应用后,你现在可以开发并测试 Qt 购买。我们将从使用 QML 开始。
在你的 QML 应用中使用 Qt 购买时的导入行如下所示:
import QtPurchasing 1.0
将以下行添加到配置文件中:
QT += purchasing
现在,决定你的应用内购买将是什么。请注意,Qt 购买有以下两种产品类型:
-
消耗品
-
可解锁
消耗品购买是一些一次性使用且可以多次购买的东西,例如游戏代币。一个例子是游戏货币。
可解锁购买是诸如额外角色、广告移除和关卡解锁等特性,这些特性可以被重新下载、恢复,甚至转移。
我们的颜色产品是一种消耗品购买,使用户能够购买他们想要的任意数量的颜色。
在 QtPurchasing 中,有以下三个 QML 组件:
-
产品 -
存储 -
交易
存储
存储组件代表平台默认的市场;在 Android 上,它是 Google Play 商店,而在 iOS 上则是 Apple App Store。一个存储元素有一个方法,restorePurchases(),当用户想要恢复他们的购买时使用。
你可以将产品作为存储的子组件,或者作为独立组件,其中存储对象由 ID 指定。
产品
产品组件代表应用内购买产品。identifier属性对应于你在相关商店注册应用内购买产品时使用的产品 ID。
关于产品组件,有以下几点需要注意:
-
产品可以是存储的子组件,或者可以使用存储的id属性来引用 -
产品可以有两种类型之一:Product.Consumable或Product.Unlockable -
一个
Product.Consumable产品可以购买多次,前提是购买已经完成 -
一个
Product.Unlockable产品一旦购买就可以恢复
以下代码演示了一个具有Product.Consumable类型的产品组件:
Store {
id: marketPlace
}
Product {
id: colorProduct
store:marketPlace
identifier: "some.store.id"
type: Product.Consumable
}
Button {
text: "Buy this color"
onClicked: {
colorProduct.purchase()
}
}
现在,是时候继续我们的购买选项了。看看下面的截图:

要开始购买流程,请使用purchase()方法,OK 按钮调用该方法以从 Google Play 商店弹出以下对话框:

注意到前面截图中的付款不是真实的,而是使用 Google 测试卡进行的。没有进行货币交换。
你现在将想要处理onPurchaseSucceeded和onPurchaseFailed信号。如果你有可恢复的产品,请在onPurchaseRestored信号中恢复,如下所示:
onPurchaseSucceeded: {
console.log("sale succeeeded " + transaction.orderId)
// do something like fill a model view
transaction.finalize()
}
您还应该保存交易。当应用启动时,它会查询商店中的任何购买。如果用户已购买产品,onPurchaseSucceeded 信号将被调用,并带有每个购买的过渡 ID,这样应用就知道已经完成了哪些购买,并可以相应地采取行动。
以下截图说明了 Google Play 商店上的成功购买:

如果购买因任何原因失败,将调用 onPurchaseFailed,如下所示:
onPurchaseFailed: {
console.log("product purchase failed " + transaction.errorString)
transaction.finalize()
}
您可能希望为这里查看的任何事件提供用户通知,仅为了提供清晰并避免用户混淆。
交易
Transaction 代表市场商店中购买的产品,并包含有关购买的信息,包括 status、orderId 以及描述可能发生的任何错误的字符串。以下表格解释了这些属性:
errorString |
描述错误的特定平台字符串 |
|---|---|
failureReason |
可以是 NoFailure、CanceledByUser 或 ErrorOccurred |
orderId |
由平台商店发出的唯一 ID |
product |
product 对象 |
status |
可以是 PurchaseApproved、PurchaseFailed 或 PurchaseRestored |
timestamp |
交易发生的时间 |
Transaction 有一个方法:finalize()。所有交易无论成功与否都需要最终化。
一旦用户成功购买了一种颜色,他们应该会看到如下截图所示的内容:

注意可解锁产品可以恢复。让我们继续前进,看看如何处理这一点。
恢复购买
用户可能出于多种原因想要恢复购买。也许他们重新安装了应用,切换到了新手机,或者甚至重置了现有的手机。
只有可解锁产品才能恢复。
恢复购买通过 restorePurchases() 方法初始化,然后将为每个恢复的购买调用 onPurchaseRestored 信号,如下所示:
Button {
text: "Restore Purchases"
onClicked: {
colorProduct.restorePurchases()
}
}
在 product 组件中,它看起来如下所示:
onPurchaseRestored: {
// handle product
console.log("Product restored "+ transaction.orderId)
}
如您所见,QML 使得添加内购以及必要时恢复它们变得非常简单。
摘要
Qt 使得实现内购相当简单。大部分工作将涉及整理您的应用,并在您平台的应用商店中注册内购产品。
您现在应该能够将内购产品注册到相关的应用商店。您还应该知道如何使用 QML 实现内购产品并执行商店转换。我们还探讨了如何恢复任何可解锁产品的购买。
这章全部关于手机应用和购买。在下一章中,我们将探讨各种交叉编译方法以及如何使用嵌入式设备进行远程调试。
第四部分:移动部署和设备创建
为移动和嵌入式设备编译以及部署可能会很具挑战性。在嵌入式设备的情况下,读者可能需要部署到一个裸机——包括操作系统在内。本节将涵盖将跨平台应用程序部署到移动和嵌入式设备的不同方法,以及如何使用 Raspberry Pi 为设备创建一个启动到 Qt 的过程。
本节包括以下章节:
-
第十二章,交叉编译和远程调试
-
第十三章,部署到移动和嵌入式
-
第十四章,移动和嵌入式设备的通用平台
-
第十五章,构建 Linux 系统
第十二章:交叉编译和远程调试
由于在嵌入式设备上使用 Linux 系统的可能性很大,我们将介绍在 Linux 上设置交叉编译器的步骤。手机平台有自己的开发方式,这也会被讨论。你将学习如何为不同设备编译跨平台应用,并通过网络或 USB 连接进行远程调试。我们将探讨各种移动平台。
在本节中,我们将涵盖以下主题:
-
交叉编译
-
连接到远程设备
-
远程调试
交叉编译
交叉编译是一种在宿主机上为不同于宿主机运行架构的应用程序和库构建的方法。当你使用 Android 或 iOS SDKs 为手机构建时,你正在进行交叉编译。
做这件事的一个简单方法就是使用 Qt for Device Creation,或者 Qt 的 Boot to Qt 商业工具。它可用于评估或购买。
你不必自己构建任何工具和设备系统镜像。我为我使用的树莓派使用了 Boot to Qt。这使得设置变得更快、更简单。还有更多传统的为不同设备构建的方法,除了目标机器之外,它们大致相同。
如果你使用的是 Windows,交叉编译可能会有些棘手。你可以安装 MinGW 或 Cygwin 来构建自己的交叉编译器,安装 Windows Subsystem for Linux,或者安装预构建的交叉toolchain,例如来自 Sysprogs。
传统交叉工具
获取交叉编译器有许多方法。设备制造商可以与其软件栈一起发布交叉toolchain。当然,如果你正在构建自己的硬件,或者只是想创建自己的交叉toolchain,还有其他选择。你可以为你的设备架构下载预构建的交叉toolchain,或者自己构建它。如果你最终决定编译toolchain,你需要一台快速且健壮的机器,并且有大量的磁盘空间,因为它将花费相当长的时间来完成,并且会使用大量的文件系统——如果你构建整个系统,可能需要 50 GB。
DIY 工具链
对于某些项目,你可能需要或者必须(如果没有提供toolchain)自己构建自己的toolchain。以下是一些较为知名的交叉工具:
-
Buildroot:
buildroot.org/ -
Crosstool-NG:
crosstool-ng.github.io/ -
OpenEmbedded:
www.openembedded.org -
Yocto:
www.yoctoproject.org/ -
Ångström:
wp.angstrom-distribution.org/
BitBake 被 OpenEmbedded、Yocto 和 Ångström(以及 Boot to Qt)使用,因此从其中之一开始可能最容易。您可以说它是 Buildroot 2.0,因为它是原始 Buildroot 的第二次版本。尽管如此,它是一个完全不同的构建。Buildroot 更简单,没有包的概念,因此升级系统可能更困难。
我将在第十五章 构建 Linux 系统中描述使用 BitBake 构建 toolchain,本质上它与构建系统镜像非常相似;事实上,它必须在构建系统镜像之前构建 toolchain。
Buildroot
Buildroot 是一个帮助构建完整系统的工具。它可以构建交叉 toolchain 或使用外部的一个。它传统上使用 ncurses 接口进行配置,就像 Linux 内核一样。它还有一个新的 ncurses 配置器,但还有一个基于 Qt 的配置器。让我们使用那个吧!
在您解压缩 Buildroot 的目录中,运行以下命令:
make xconfig
哎!它使用 Qt 4。如果您不想安装 Qt 4,您始终可以使用 make menuconfig 或 make nconfig。
这是 Qt 接口的外观:

默认情况下,Buildroot 将创建一个基于 BusyBox 的系统,而不是 glibc。
一旦您已配置好系统,保存配置并关闭配置器。然后运行 make,坐下来,让它构建。它将文件放入一个名为 output/ 的目录中,其中您的系统镜像位于一个名为 image 的目录中。
Crosstool-NG
Crosstool-NG 是用于构建工具链的,而不是系统镜像。您可以使用用 crosstools 构建的 toolchain 来构建系统,尽管您可能需要手动完成。
Crosstool-NG 与 Buildroot 类似,因为它使用 ncurses 配置要构建的 toolchain。一旦您解压缩它,您需要运行以下 bootstrap 脚本:
./bootstrap
要安装它,您需要使用以下 --prefix 参数调用配置器:
./configure --prefix=/path/to/output
您也可以按照以下方式本地运行它:
./configure --enable-local
它将告诉您需要安装的任何缺失的包。在我的 Ubuntu Linux 上,我必须安装 flex、lzip、help2man、libtool-bin 和 ncurses-dev。
然后运行 make 和 make install,如果您配置了前缀。
您需要将 /path/to/output/bin 添加到您的 $PATH 中。
export PATH=$PATH:/path/to/output/bin。
现在您可以运行以下配置:
./ct-ng menuconfig

然后运行 make,这将构建交叉 toolchain。
预构建工具
有公司为各种设备和架构提供了以下预构建的交叉工具链:
-
Code Sourcery:
www.codesourcery.com/ -
Linaro, Debian, Fedora:从软件包管理器下载
-
Boot to Qt:
doc.qt.io/QtForDeviceCreation/qtb2-index.html -
Sysprogs:
gnutoolchains.com/
这些是一些较好的选择。我大多数都体验过,并且一度使用过。每个都附带安装和使用说明。Linaro、Debian 和 Fedora 都制作 ARM 交叉编译器。这是一本关于 Qt 开发的书籍,所以我将描述 Qt 公司的产品——Boot to Qt。
Boot to Qt
Qt 公司的 Boot to Qt 产品包含开发工具和预构建的操作系统镜像,你可以将其写入微 SD 卡或烧录到设备上运行。除了 Raspberry Pi 之外,他们还支持以下其他设备:
-
Boundary Devices i.MX6 Boards
-
Intel NUC
-
NVIDIA Jetson TX2
-
NXP i.MX 8QMax LPDDR4
-
Raspberry Pi 3
-
Toradex Apalis iMX6 和 iMX8
-
Toradex Colibri iMX6, iMX6ULL, 和 iMX7
-
WaRP7
我选择了 RPI,因为我已经有一个带有触摸屏的 3 型模型在身边。
当你运行系统镜像时,你会启动一个 Qt 应用程序,该应用程序充当示例应用程序的启动器。它还设置 Qt Creator 以便能够在设备上运行交叉编译的应用程序。你可以在 Qt Creator 中点击运行按钮来在设备上运行它。
Boot to Qt 是一种快速且简单的方法,可以在相对较小的系统上快速将原型运行在触摸屏上。Qt 公司目前正在努力让 Qt 在更小的设备上运行良好,例如微控制器。
你可以直接运行 Boot to Qt 的toolchain;你只需要源环境文件。在 Raspberry Pi 和 Boot to Qt 的情况下,它被称为environment-setup-cortexa7hf-neon-vfpv4-poky-linux-gnueabi。你也可以直接调用toolchain的 qmake 并在你的配置文件/path/to/x86_64-pokysdk-linux/usr/bin/qmake myApp.pro上运行它。
这里还有一个选择是直接使用 Qt Creator 并选择 Raspberry Pi 作为目标。
如果你使用 Windows,有几个选项你可以使用来获取交叉编译toolchain。
Windows 上的交叉工具链
你可以在 Windows 上以几种方式交叉编译,我们可以简要地介绍一下。它们如下,但无疑还有其他未涵盖的:
-
Sysrogs 为 Windows 提供了预构建的交叉
toolchain。 -
Windows Subsystem for Linux.
Sysprogs
Sysprogs 是一家为在 Windows 上运行针对 Linux 设备的交叉工具链的公司。他们的toolchain可以从gnutoolchains.com/下载
-
安装完成后,启动一个 Qt 5.12.1(MinGW 7.3.0 64 位)控制台终端
-
你需要按照以下方式将
toolchain添加到你的路径中:
set PATH=C:\SysGCC\raspberry\bin;%PATH%
- 按照以下方式将
PATH添加到 Qt 的mingw中:
set PATH=C:\Qt\Tools\mingw730_64\bin;%PATH%
你还必须构建 OpenGL 和其他 Qt 的要求。
按照以下方式配置 Qt 以交叉编译:
..\qtbase/configure -opengl es2 -device linux-rasp-pi-g++ -device-option CROSS_COMPILE=C:\SysGCC\raspberry\bin\arm-linux-gnueabihf- -sysroot C:\SysGCC\raspberry\arm-linux-gnueabihf\sysroot -prefix /usr/local/qt5pi -opensource -confirm-license -nomake examples -make libs -v -platform win32-g++
Windows Subsystem for Linux
您可以安装 Windows Subsystem for Linux 来安装交叉编译器,您可以从 docs.microsoft.com/en-us/windows/wsl/install-win10 下载。
然后,您可以选择所需的 Linux 发行版——Ubuntu、OpenSUSE 或 Debian。一旦安装完成,您就可以使用内置的包管理器来安装 Linux 的 toolchain。
移动平台特定工具
iOS 和 Android 都提供了预构建的交叉工具和 SDK,可供下载。如果您打算在移动平台上使用 Qt,则需要其中之一,因为 Qt Creator 依赖于原生平台构建工具。
iOS
Xcode 是您想要下载的 IDE 巨兽,它只能在 macOS X 上运行。如果您还没有,可以从桌面上的 App Store 下载它。您需要注册为 iOS 开发者。从那里,您可以选择要下载和设置的 iOS 构建工具。一旦开始下载,这个过程就相当自动化了。
您还可以从命令行使用这些工具,但您需要从 Xcode 内安装命令行工具。对于 Sierra,您只需在终端中输入 gcc 命令即可。在这种情况下,系统将打开一个对话框询问您是否想要安装命令行工具。或者,您可以通过运行 xcode-select --install 来安装它。
我不知道有任何嵌入式系统工具可以与 Xcode 一起使用,除非您将 iWatch 或 iTV SDKs 计算在内。这两个 SDK 您都可以通过 Xcode 下载。
您当然可以使用 Darwin,因为它开源且基于 伯克利软件发行版(BSD)。您也可以使用 BSD。这远远达不到在任意嵌入式硬件上运行苹果操作系统的能力,因此您的选择有限。
Android
Android 为其 IDE 开发包提供了 Android Studio,并且适用于 macOS X、Windows 和 Linux 系统。
与 Xcode 一样,Android Studio 也提供了命令行工具,您可以通过 SDK 管理器或 sdkmanager 命令进行安装。
~/Android/Sdk/tools/bin/sdkmanager --list 将列出所有可用的包。如果您想下载 adb 和 fastboot 命令,可以执行以下操作:
~/Android/Sdk/tools/bin/sdkmanager install "platform-tools"
Android 为其不同版本提供了吸引人的代码名称,这与它们的 API 级别完全不同。在安装 Android SDKs 时,您应该坚持使用 API 级别。我有一部运行 Android 版本 8.0.0 的手机,其代码名称为 Oreo。我需要安装 API 级别 26 或 27 的 SDK。如果我想安装 SDK,我可能会执行以下操作:
~/Android/Sdk/tools/bin/sdkmanager install "platforms;android-26"
在使用 Qt 进行开发时,您还需要安装 Android NDK。我使用的是 NDK 版本 10.4.0,或者称为 r10e,Qt Creator 与之配合工作得很好。我在运行较新版本的 NDK 时遇到了问题。正如他们所说,您的体验可能会有所不同。
QNX
QNX 是一个商业化的类 UNIX 操作系统,目前由 Blackberry 拥有。它不是开源的,但我认为在这里提一下是合适的,因为 Qt 在 QNX 上运行,并且在市场上被商业使用。
连接到远程设备
这是一本关于 Qt 开发的书,我将坚持使用 Qt Creator。连接到任何设备的方法几乎相同,只有一些细微的差别。你也可以通过终端使用 Secure Shell (SSH) 和其他相关工具进行连接。我经常使用这两种方法,因为每种方法都有其自身的优缺点。
Qt Creator
我记得当现在被称为 Qt Creator 的版本首次在诺基亚内部进行测试时,它被称为 Workbench。当时,它基本上是一个好的文本编辑器。从那时起,它获得了大量的优秀功能,并且它是我 Qt 项目的首选 IDE。
Qt Creator 是一个多平台 IDE,它可以在 macOS X、Windows 和 Linux 上运行。它可以连接到 Android、iOS 或通用的 Linux 设备。你甚至可以获得 UBports(开源 Ubuntu 手机)或 Jolla 手机等设备的 SDK。
要配置您的设备,在 Qt Creator 中导航到 工具 | 选项... | 设备 | 设备。
通用的 Linux
一个通用的 Linux 设备可能是一个定制的嵌入式 Linux 设备,甚至是一个树莓派。它应该运行一个 SSH 服务器。由于我使用了一个 RPI,我将使用它进行演示。
以下是在设备选项卡中显示的连接细节,用于树莓派:

如您所见,最重要的可能就是主机名。请确保主机名配置中的 IP 地址与设备的实际 IP 地址相匹配。其他设备可能使用常规网络而不是直接 USB 连接。
Android
您需要安装 Android SDK 和 NDK。
Android 是一个使用直接 USB 连接的设备,因此当运行应用程序时,复制应用程序二进制文件将更容易:

Qt Creator 大概会自动配置这个连接。
iOS
确保您的设备首先被 Xcode 发现,然后 Qt Creator 将自动识别并使用它。
它看起来可能像这张图片:

注意到那个类似绿色 LED 的图标吗?是的,一切正常!
硬件裸机
如果您的设备没有运行 SSH 服务器,您可以使用 gdb/gdbserver 或硬件服务器通过它进行连接。您首先需要启用插件。在 Qt Creator 中,导航到 帮助 | 关于插件 | 设备支持,然后选择裸机。裸机连接使用您可以从 http://openocd.org 获取的 OpenOCD。OpenOCD 不是一个新的焦虑症,而是一个通过 JTAG 接口运行的片上调试器。Qt Creator 还支持 ST-LINK 调试器。两者都使用 JTAG 接口。有 USB JTAG 接口以及传统的 JTAG 接口,它们不需要任何设备驱动程序即可连接。
写这部分内容让我想起了当 Trolltech 推出 Trolltech Greenphone 并使其运行,以及在其他设备上工作,比如 OpenMoko 手机时的情景。美好的时光!
现在我们已经连接了设备,我们可以开始调试。
远程调试
开发软件很困难。所有软件都有 bug。有些 bug 比其他 bug 更痛苦。最糟糕的情况可能是当你遇到一个随机崩溃,需要特定的触发事件序列,这些事件序列位于一个只读文件系统的远程设备上,而这个设备是在发布模式下构建的。我经历过。做过。甚至得到了一件 T 恤。(我还有许多来自过去的 Trolltech 和诺基亚 T 恤。)
传统上,远程调试涉及在设备上运行gdbserver命令。在非常小的机器上,由于没有足够的 RAM 直接运行 gdb,所以在远程设备上运行gdbserver可能是使用 gdb 的唯一方法。让我们播放一些 groove salad,开始工作吧!
gdbserver
你可能想体验没有 UI 的远程调试,或者类似的东西。这将帮助你开始。gdbserver命令需要在远程设备上运行,并且需要有一个串行或 TCP 连接。
在远程设备上,运行以下命令:
gdbserver host:1234 <target> <app args>
使用host参数将在端口1234上启动gdbserver。你也可以通过运行以下命令将调试器附加到正在运行的应用程序:
gdbserver host:1234 --attach <pid>
pid是你试图调试的已运行应用程序的进程 ID,你可以通过运行ps、top 或类似的命令来获取。
在主机设备上,运行以下命令:
gdb target remote <host ip/name>:1234
然后,你将通过运行gdb的控制台在主机设备上issue命令。
如果你遇到崩溃,崩溃发生后,你可以输入bt来获取一个回溯列表。如果你在远程设备上有崩溃内存转储,或者称为核心转储,gdbserver不支持远程调试核心内存转储。你必须远程运行gdb本身才能完成这项工作。
通过命令行使用gdb可能对某些人来说很有趣,但我更喜欢 UI,因为它更容易记住要完成的事情。拥有一个可以进行远程调试的 GUI 可以帮助你,如果你不太熟悉运行gdb命令,因为这可能是一项艰巨的任务。Qt Creator 可以进行远程调试,所以让我们继续使用 Qt Creator 进行调试。
Qt Creator
Qt Creator 在设备上使用gdbserver,所以它本质上只是一个 UI 界面。你需要为设备上的gdbserver提供 Python 脚本支持;否则,你会看到一条消息“Selected build of GDB does not support Python scripting”,并且它将无法工作。
对于大多数情况,使用 Qt Creator 进行调试对于 Android、iOS 和任何支持的 Boot to Qt 设备来说都是即插即用的。
在 Qt Creator 中加载任何项目,它都可以处理 C++ 调试,以及调试 Qt Quick 项目。请确保在 Qt Creator 的运行设置页面中正确配置了设置,在下面的调试器设置中,以启用所需的 qml 调试和/或 C++ 调试。
将以下内容添加到您的项目中并重新构建:
CONFIG+=debug qml_debug
将以下内容添加到应用程序启动参数 -qmljsdebugger=port:<port>, host:<ip>。
要中断应用程序的执行,请单击工具栏上提示为 '中断 GDB for "yourapp"' 的图标。然后您可以检查变量的值并逐行执行代码。
在某处设置一个断点——在相关的行上右键单击并选择在行上设置断点。
按 F5 开始应用程序构建(如果需要)。一旦成功构建,它将被传输并在设备上执行,同时远程调试服务启动。当然,如果设置了断点,它将在断点处停止执行。要继续正常执行,请按 F5 直到遇到那个痛苦的崩溃,然后您可以检查那个美妙的回溯!从这里,您可能希望收集足够的线索来修复它。
Qt Creator 默认支持的其他键命令如下:
-
F5:开始/继续执行
-
F9:切换断点
-
F10:跳过
-
Ctrl + F10:运行到当前行
-
F11:进入
-
Shift + F11:跳出
让我们试试。加载本章的源代码。
要在 Qt Creator 编辑器中的当前行切换断点,请按 Linux 和 Windows 上的 F9,或在 macOS 上按 F8,如下所示:

现在通过按 F5 运行调试器中的应用程序来启动调试器。它将在我们的行上停止执行,如下所示:

看到那个小黄色箭头吗?它告诉我们执行在执行语句之前停止了。
您将能够看到变量的以下值:

如您所见,断点在 QString b 被初始化之前就停止了执行,所以其值是 ""。如果您按 F10 或跳过,QString b 将被初始化,您可以看到新的值如下:

您可以从下面的屏幕截图注意到,执行行也会移动到下一行:

您还可以通过在编辑器中右键单击断点并选择编辑断点来编辑断点。让我们在 for 循环的第 20 行设置一个断点,如下所示:

右键单击并选择编辑断点以打开以下编辑断点属性对话框:

编辑条件字段并添加 i == 15,然后点击确定:

通过点击 F5 运行调试器中的应用程序。点击字符串按钮。当它遇到断点时,你可以看到它停止时 i 包含的值是 15:

你可以接着进入或跳过。
让我们现在看看一个崩溃错误,当你点击崩溃按钮时,它是一个除以零的崩溃。
在第 31 行设置断点。运行调试器,它将在崩溃前停止。现在执行下一步。你应该会看到一个如下所示的对话框弹出:

哇,现在看起来真丑。
在以下屏幕截图所示的堆栈视图中,你可以看到程序崩溃的位置:

是的,它就在我放的地方!在 C++ 中除以零会发生糟糕的事情。
摘要
调试是一个强大的过程,通常需要修复错误。你可以从命令行运行调试器,例如 gdb,或者你应该能够将 gdb 连接到在远程设备上运行的调试服务器。使用基于 GUI 的调试器会更有趣。你应该能够通过远程连接从 Qt Creator 调试运行在您的移动或嵌入式设备上的 Qt 应用程序。
下一步是部署你的应用程序。我们将探讨在几个不同的移动和嵌入式平台上部署你的应用程序的多种方法。
第十三章:部署到移动和嵌入式
手机、平板电脑和手表都有它们自己的平台方式来部署应用程序——通常是通过应用商店。部署插件和其他库需要特别注意。在本章中,我们将讨论其他操作系统,例如 Jolla 的 Sailfish OS,因为嵌入式设备有几种选择。我使用 Raspberry Pi 作为嵌入式 Linux 设备的示例。
对于主要的移动手机应用商店,您需要使用安全证书对您的包进行数字签名,系统使用该证书作为识别作者并足以信任应用程序的方式。
证书涉及一个公钥私钥对。私钥就是那个。您只需将其保密。公开证书可以公开分发。我不会深入讲解这里涉及的加密。Qt Creator 将这些证书称为密钥库,您可以使用 Qt Creator 生成这些自签名的证书。
我们将检查以下部署目标:
-
对于 Android
-
对于 iOS
-
对于其他操作系统
-
对于嵌入式 Linux
在备份您的数字证书时,因为如果您丢失它们,您将无法在商店中更新您的应用程序。
部署到 Android
Android 不需要 Google Play Store 来安装应用程序;这只是一个最方便的方式。还有其他市场可供选择,例如 Aptoid、Yandex、F-Droid 和 Amazon。
您还可以侧载应用程序。侧载是通过通过 USB、内存卡或通过互联网传输包来安装应用程序,而不使用官方商店。
从技术上讲,Qt Creator 可以侧载您正在工作的应用程序的包。它可以安装该包,或者在不安装的情况下简单地运行设备上的可执行文件。
实际上,您可以将包文件放在您的 web 服务器上,让人们将其下载到他们的手机或电脑上,并让他们手动安装。
您还可以通过官方发布使其在 Google Play Store 上可用。您需要能够使用从您的开发者账户获得的证书对其进行签名。这个 Android 证书不需要由证书颁发机构签名,但可以是自签名的。
包
在开发和测试您的应用程序之后,您需要制作一个包,以便人们可以安装它。制作包有几种方式,可以通过 Qt Creator 或通过命令行进行。
androiddeployqt
命令行工具 androiddeployqt 随 Qt Creator 一起提供,用于 Android SDK。这是一个命令行工具,用于帮助构建和签名 Android 包。要查看其帮助输出,请运行 /path/to/androiddeployqt --help。除了说明它可用之外,我不会深入讲解命令行部署。
Qt Creator
在 Qt Creator 中,数字证书在项目的构建设置页面上进行处理。现在,让我们构建:
- 项目 | 构建 将带您进入构建设置页面。如果您正在制作发布版本,请确保您的项目在此页面的顶部处于发布模式:

- 导航到构建步骤 | 构建 Android APK:

- 您需要点击创建模板,这将创建商店所需的
manifest.xml文件。两个主要条目是包名和应用程序图标。我使用 Android Studio 创建不同尺寸的图标,因为它在同时创建多个图标时效率最高。我从一个大 PNG 开始。务必选择正确的 SDK 版本,否则以后可能会出现问题:

- 您还需要生成证书。在构建步骤页面中,找到签名包部分。密钥库条目最右侧的按钮,上面写着创建...,将弹出此对话框:

-
您需要为密钥库和证书提供密码,它们是私钥和公钥。
-
您还需要提供您的姓名、组织、城市、州和两位字母的国家代码。
进行另一个构建,它将创建一个已签名的 Android 包,您可以安装或上传到商店。现在您已经准备好在封闭的、内部的或公开的测试轨道中测试您的应用了。Qt Creator 在那里不会帮到您。
测试轨道
您可以为您的组织中的测试者设置一个内部测试轨道。如果您是一家一站式商店,那么您就是测试者!
内部测试
您最多可以有 100 名内部测试者。要在 Play Console 上创建测试者列表,请导航到:
设置 | 管理测试者 | 创建列表
您需要提供以下信息:
-
列表名称
-
电子邮件地址(您也可以上传 csv 电子邮件列表!)
您可以添加测试者到应用(您可能之前已经添加或尚未添加)。现在选择您的应用,导航到应用发布,并选择以下选项之一:
-
内部:一个内部封闭轨道
-
首次发布:一个封闭轨道
-
测试版:一个公开轨道
-
生产:发布
点击管理(内部)| 创建发布
您需要确保您的账户已设置好,提供以下项目:
-
商店列表—截图
-
高分辨率图标(512 x 512 png)
-
特色图形(1,024 x 500 jpg, png)
-
-
内容评级
-
定价和分发
在添加测试者之前,您需要上传一个应用包。一旦您上传了包应用,您将需要选择“审查”。
当您返回到应用发布页面时,您将被要求选择您的测试者列表。您应该会收到一个 URL,可以与您的测试者分享,以便他们可以下载并安装您内部测试的应用程序包。
页面顶部的那个红色图标意味着需要检查某些内容:

在我的情况下,我实际上还没有为这次发布选择任何测试者。
当您向测试者列表添加内容时,它应该看起来像这样:

您的测试者将在 Google Play 商店看到以下内容:

在 iOS 上的部署非常相似,所以让我们看看那个。
iOS 部署
iOS 商店可能是所有移动应用商店中最具限制性和复杂的,要提交应用。它也有更详尽的提交指南,例如:不应复制原生应用的功能。到达提交应用这一点的过程也更加复杂。
包
Qt Creator 支持创建和签名 iOS 包。与 Android 一样,您需要从您的开发者账户获取证书。这是您在 Apple 开发者账户登录时看到的:

一旦进入您的开发者账户,点击标有“证书、标识符和配置文件”的图标以添加证书。注意左侧的证书列表:

有两种类型的证书:开发证书和生产证书。生产证书用于发布分发。如果您没有生产证书,现在可以通过点击加号图标添加一个。

这将打开以下对话框:

选择“App Store”和“Ad Hoc”。Ad Hoc 意味着您只能在少数测试设备上安装。
接下来,在“标识符”下,选择App IDs:

有两种类型的 App ID:
-
通配符,如果应用不需要 iCloud 或应用内购买,则可用于多个应用。
-
显式,用于每个应用的应用内购买。如果您有使用应用内购买的应用,您将需要其中之一。
您还需要配置文件:

如您所见,这里有不同类型的配置文件。在“分发”下选择“App Store”,然后点击页面底部的“继续”按钮。选择您之前创建的 App ID。选择也之前创建的证书。您需要为此配置文件命名,并且它必须与您的包的 Bundle Identifier 匹配。下载此文件并将其保存在您记得的地方;您将需要此文件通过 XCode 对应用进行签名。
您还需要 App ID,因此从左侧选择 App IDs 并创建一个新的。
现在我们可以对发布模式的包进行签名。
Qt Creator
Qt Creator 本身无法创建 iOS 包。我们需要使用 Qt Creator 生成的 Xcode 项目文件来使用 Xcode 创建包。
选择发布构建,然后运行 qmake 以创建我们将使用的 Xcode 项目文件。
Xcode
从构建目录中,在 Xcode 中打开生成的 <target>.xcodeproj 文件。从左侧选择项目以打开项目设置。
点击“通用”,然后取消选择“自动管理签名”,以便能够手动选择包签名:

您可以通过从标记为“配置文件”的下拉列表中选择“导入配置文件...”来导入配置文件,然后选择“导入配置文件...”。一旦文件对话框打开,您可以导航到您放置<profilename>.mobileprovision文件的位置:

您可以为签名(调试)和签名(发布)都这样做。
您的 Bundle 标识符必须与配置文件名称匹配。如果不匹配,它会提醒您:

证书已经申请:

测试并构建包。在签名包时,它应该会要求您输入管理员密码。
要构建发布模式的包,请导航到产品 | 方案 | 编辑方案 | 信息 | 构建配置。
选择发布然后构建包。
从这里,您需要再次打开您的网络浏览器并导航到 App Store Connect,选择我的应用,然后点击+创建新应用:

从这里,填写应用信息、定价和可用性。
接下来,为应用页面和 App Store 添加截图、图标、商店文本和其他项目。
要从 Xcode 上传您的应用,请选择产品 | 归档。
这将创建包并打开存档窗口。
在将包上传到 App Store 之前,您应该验证包,因此请选择“验证应用”,然后选择配置文件和证书:

修复您可能遇到的所有问题。这还包括为应用商店的店面添加截图、应用图标等。然后您可以点击“分发应用”按钮。
替代操作系统
有其他可用的移动和嵌入式操作系统,您可能听说过,也可能没有听说过,例如我最喜欢的替代移动操作系统:Jolla 的 Sailfish。另一个操作系统是 UBports,它是 Canonical 现在已停用的移动手机操作系统 Ubuntu Touch 的开源版本。
Sailfish OS
Sailfish OS 是诺基亚 MeeGo 的延续,而 MeeGo 又是 Maemo 的延续。
用户界面由 Jolla 开发,基础操作系统是开源的 Mer,由 Jolla 和社区开发。
Jolla 有一个名为 Harbour 的应用商店(harbour.jolla.com)。目前,您无法通过这个应用商店销售应用:

这就是我的开发者页面看起来像:

是的,自从我上次更新它以来已经过去了五年。
您可以将 Jolla 安装到某些 Android 手机上——如果您足够幸运拥有实际的 Jolla 硬件,或者如果您有一部预装了 Jolla 的手机,您可以通过商店应用访问 Harbour。以下是顶级应用页面的视图:

在那个商店里,我有一个名为 Compass 的老应用,我需要将其更新到新的 Sailfish OS 版本 3:

我需要从 releases.sailfishos.org/sdk/installers/1.24/ 下载应用程序 SDK。
我将其安装在以下 Linux 开发机上:
chmod +x SailfishOSSDK-Beta-1.24-Qt5-linux-64-offline.run
./SailfishOSSDK-Beta-1.24-Qt5-linux-64-offline.run
Sailfish OS SDK 已安装!

在 Qt Creator 打开并带有 Sailfish SDK 后,点击左侧的 Sailfish OS 图标:

您应该会看到一个消息说构建引擎没有运行,因此我们需要启动它。点击“启动构建引擎!”:

完成上述操作后,您将进入 Sailfish 控制中心,在那里您可以向 SDK 添加组件,并在需要时应用更新。
如果您有设备,请设置您的设备,并导航到“工具”|“选项”|“设备”。
到达那里后,点击“工具”|“选项”|“套件”,并选择 armv7hl 套件。
在“设备选项”部分,请确保选择您之前设置的 Jolla 设备而不是模拟器,除非您想将应用运行在模拟器上。
Jolla SDK 构建引擎在虚拟机中运行,因此可以从任何平台使用。它使用的 IDE 是 Qt Creator。Jolla 的独特之处在于您不仅可以运行 Jolla OS 的原生应用,还可以运行 Android 应用。关于 Android 支持的注意事项是,没有 Google Play API。
现在,构建引擎(交叉编译器)正在运行,我们可以构建我们的应用,测试,然后制作一个包上传到 Harbour。
从“项目”|“运行设置”中,确保选择了“通过复制二进制文件部署”或“作为 RPM 包部署”作为方法。如果您在没有安装包的情况下运行它,请选择“复制方法”。以下是我在 Jolla 手机上运行的更新后的 Compass 应用程序:

一旦构建了发布包,导航到 Sailfish OS | 发布,点击 RPM 验证器,并选择您的包文件进行验证。
Jolla 商店界面是这里移动应用商店中最简单的,部分原因是因为这里没有应用销售。
点击“添加应用”。您需要填写以下内容:
-
标题
-
详细信息:
-
描述
-
摘要
-
近期更改
-
-
分类
-
兼容性
-
视觉辅助(截图、图标)
-
联系方式
-
任何给 QA 的信息
一旦您将应用程序提交到 Jolla 的 Harbour 商店,您将看到类似以下图片的内容:

QA 将检查包,要么拒绝要么接受它。
UBports
UBports 是基于 Canonical 现已停用的移动产品 Ubuntu Touch 的融合操作系统。融合意味着它被设计为可以在桌面和移动设备上运行。它运行在多种手机和平板电脑上,UI 基于 Qt 和 QML。我在 Nexus 4 上运行了我的系统。我不会详细介绍,但我想提一下。更多信息可以在:ubports.com/ 上找到。
在ubuntu-touch.io/get-ut有一个简单的安装程序,可以将 UBports 安装到支持的设备上。
你可以在docs.ubports.com/en/latest/appdev/index.html获取 SDK。
可点击的
UBports SDK 可以从命令行生成 click 包。Ubuntu Touch SDK IDE 不再由 Canonical 或 UBports 支持。商店被称为 OpenStore。你需要一个账户,提交应用的 URL 是open-store.io/submit。
嵌入式 Linux
嵌入式 Linux 设备有多种不同的尺寸和种类。一些可能有应用商店,但大多数没有。有各种方法将操作系统和应用程序安装到设备上。
操作系统部署
操作系统的部署将直接在你的设备上进行,因为一些嵌入式设备有非常特定的方法来部署操作系统到设备上。以树莓派为例,将镜像复制到 SD 卡并放入 RPI 中启动非常简单。
我有一个名为writeIso的脚本,我使用它;它由两行组成:
#!/bin/bash
sudo dd if=$1 of=$2 bs=4M status=progress
我运行它的方式如下:
./writeIso /path/to/deviceImage.img /dev/sdc
其他设备可能有一种flash方法,即镜像直接复制到设备上。这可能需要使用 JTAG 这样的低级方法,或者可能是使用 Android 的adb命令这样的高级方法。有时,你必须将镜像写入 SD 卡,将其放入设备中,然后通过一些按键或按钮的组合将镜像闪存到机器的 ROM 中。
应用部署
使用带有包管理器的发行版,如 Raspbian 或 Yocto,你可以轻松地分发你的应用程序,无论是直接在设备上安装还是添加到包仓库。在 Yocto 的情况下,你可以有一个本地仓库来分发。
要将包文件安装到设备上,你可以使用 Qt Creator 并设置一个通用的 Linux 设备。这需要在设备上运行一个 SSH 服务器,并建立某种类型的网络连接。
你还可以使用scp命令将包和/或二进制文件复制到设备上。这也需要一个 SSH 服务器。
摘要
操作系统和应用程序的部署有几种不同的方法。手机有应用商店,它们也有各种提交应用程序的方法。通常,这些操作是通过网页浏览器完成的。你应该能够将应用程序发布到 Android、iOS 和替代操作系统,如 Jolla 的 Sailfish 应用商店。
你也应该能够将你的应用程序分发到嵌入式设备上,例如树莓派。
在下一章中,我们将探讨 Qt for WebAssembly 这项全新的技术,它允许 Qt 应用程序在网页浏览器中运行。
第十四章:移动和嵌入式设备的通用平台
部署应用程序并针对所有不同的平台可能需要大量时间和数千美元的成本。有一个新的 Qt 应用程序目标平台,称为 Qt for WebAssembly,它允许 Qt 和 Qt Quick 应用程序通过网络从浏览器中运行。你将学习如何设置、交叉构建、部署和运行在任何具有现代浏览器的设备上运行的 Qt 应用程序。可以说,Qt for WebAssembly 是一个通用平台。
我们将详细介绍以下材料:
-
技术要求
-
入门
-
使用命令行构建
-
使用 Qt Creator 构建
-
针对移动和嵌入式设备进行部署
-
小贴士、技巧和建议
什么是 WebAssembly?
WebAssembly 既不是严格意义上的 Web,也不是 Assembly。同时,它两者都有一点。
在技术层面上,根据 WebAssembly 网站 webassembly.org 的描述,它是一个基于栈的虚拟机的新二进制指令格式。它在现代浏览器中运行,但人们自然地在进行实验,现在它可以像任何其他应用程序一样独立和实验性地运行,同时正在编写支持 Linux 内核的代码。
通过使用 Emscripten 工具,它可以编译 C 和 C++。Emscripten 是一个用 Python 编写的工具,它使用 LLVM 将 C++ 代码转换为可以被浏览器加载的 WebAssembly 字节码。
WebAssembly 字节码在同一个沙盒中运行与 JavaScript,因此它对本地文件系统的访问以及生活在单个线程中的限制与 JavaScript 相同。它也具有相同的安全优势。尽管正在进行工作以完全支持 pthreads,但在撰写本文时,它仍然是实验性的。
技术要求
从以下 Git 仓库轻松安装二进制 SDK:
- Emscripten sdk
github.com/emscripten-core/emscripten.git
或者,手动编译 SDK。您可以从以下 Git 网址下载源代码:
-
Emscripten
github.com/emscripten-core/emscripten.git -
Binaryen
github.com/WebAssembly/binaryen.git
入门
根据 Emscripten 网站 emscripten.org/:
Emscripten 是一个使用 LLVM 将代码转换为 WebAssembly 以在浏览器中以接近原生速度运行的工具链。
安装 Emscripten 有两种方式:
-
克隆仓库,安装预编译的二进制文件
-
克隆仓库,构建它们
我推荐第一个,因为构建 LLVM 非常耗时。也建议使用 Linux 或 macOS。如果您使用的是 Windows,您可以安装 Linux 子系统并使用它,或者使用 MinGW 编译器。Visual Studio 编译器似乎不支持 Emscripten 输出的四字母扩展名目标,即 .wasm 和 .html。
下载 Emscripten
您需要安装 Git 和 Python 才能进行此操作——只需克隆 emscripten sdk:
git clone https://github.com/emscripten-core/emscripten.git.
在其中,有一些 Python 脚本可以帮助您,其中最重要的是 emsdk。
首先运行 ./emsdk --help 以打印有关如何运行的文档。
然后您需要安装并激活 SDK,如下所示:
./emsdk install latest
./emsdk activate latest
您可以针对特定的 SDK;您可以通过运行以下命令查看可用的选项:
./emsdk list
然后通过运行以下命令安装特定版本的 SDK:
./emsdk install sdk-1.38.16-64bit
./emsdk activate sdk-1.38.16-64bit
activate 命令会设置包含 Emscripten 所需环境设置的 ~/.emscripten 文件。
要能够使用它进行构建,您需要按照以下方式源码 emsdk_env.sh 文件:
source ~/emsdk/emsdk_env.sh
Qt 针对某个已知对该版本有良好支持的 Emscripten 版本。对于 Qt 5.11,Qt for WebAssembly 有自己的分支——wip/webassembly。它已集成到 5.12 作为技术预览,并在 5.13 中提供官方支持。截至本文撰写时,它计划作为二进制安装包含在 Qt Creator 中。
手动构建 Emscripten SDK
如果您想手动构建 Emscripten,例如编译支持直接转换为 WebAssembly 二进制文件而不是首先写入 JavaScript 然后转换为 WebAssembly 的上游 LLVM。这可以加快编译时间,但截至本文撰写时,这仍然是实验性的。这通过向链接器添加一个参数 -s WASM_OBJECT_FILES=1 来实现。
关于使用 WASM_OBJECT_FILES 的更多信息,请参阅 github.com/emscripten-core/emscripten/issues/6830。
技术要求
您需要从您的操作系统安装 node.js 和 cmake 软件包。克隆以下资源:
mkdir emsdks
cd emsdks
git clone -b 1.38.27 https://github.com/kripken/emscripten.git
git clone -b 1.38.27 https://github.com/WebAssembly/binaryen.git
git clone https://github.com/llvm/llvm-project.git
Emscripten 不需要构建,因为它是用 Python 编写的。
要构建 binaryen,请输入以下代码:
cd binaryen
cmake .
make
要构建 LLVM,请输入以下代码:
mkdir llvm
cmake ../llvm-project/llvm -DLLVM_ENABLE_PROJECTS="clang;libcxx;libcxxabi;lld" -DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD=WebAssembly -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly
make
运行 emscripten 以编写配置文件,如下所示:
cd emscripten
./emcc --help
这将创建一个 ~/.emscripten 文件。按照以下方式将此文件复制到您的 emsdks 目录:
cp ~/.emscripten /path/to/emsdks/.emscripten-vanillallvm
设置环境时,请编写以下脚本:
#!/bin/bash
SET EMSDK=/path/to/emscripten
SET LLVM=/path/to/llvm/bin
SET BINARYEN=/path/to/binaryen
SET PATH=%EMSDK%;%PATH%
SET EM_CONFIG=/path/to/emsdks/.emscripten-vanillallvm
SET EM_CACHE=/path/to/esdks/.emscripten-vanillallvm_cache
将其保存为 emsdk-env.sh。
您需要使其可执行,因此运行 chmod +x emsdk-env.sh。
每次您需要设置构建环境时,只需运行此脚本并使用相同的控制台进行构建。
现在我们已经准备好了,让我们看看如何配置和构建 Qt。
配置和编译 Qt
您可以在以下 URL 找到有关 Qt for WebAssembly 的信息:wiki.qt.io/Qt_for_WebAssembly
我猜我们需要源代码。您可以通过 Qt Creator 获取它们,或者您可以通过git clone克隆仓库。使用 Git,您可以对版本和任何需要的分支有更多的控制。
对于 5.12 和 5.13,您可以简单地克隆以下标签:
git clone http://code.qt.io/qt/qtbase.git -b v5.12.1
或者,您可以克隆此标签:
git clone http://code.qt.io/qt/qtbase.git -b v5.13.0
就像任何新技术一样,它发展迅速,所以请获取最新版本。对于这本书,我们使用 Qt 5.12,但我提到了其他版本,因为它们有很多错误修复和优化。
现在我们可以配置和编译 Qt 了!
对于 5.12 和 5.13,它简化为以下内容:
configure -xplatform wasm-emscripten -nomake examples -nomake tests
如果您需要线程,5.13 支持多线程 WebAssembly,但您还需要配置浏览器以支持它。
一旦配置完成,您只需运行 make!
然后,为了构建在网页浏览器中运行的 Qt 应用程序,只需从构建目录使用qmake命令并在您的 app.pro 文件上运行即可。并非所有 Qt 功能都受支持——例如本地文件系统访问和线程。QOpenGLWidget也不受支持,尽管QOpenGLWindow运行良好。让我们看看如何使用命令行进行构建。
使用命令行构建
构建用于 WebAssembly 应用程序的 Qt 需要您源码 Emscripten 环境文件,因此请在控制台命令中运行以下内容:
source /path/to/emsdk/emsdk_env.sh
您需要添加 Qt WebAssembly 的路径到qmake,如下所示:
export PATH=/path/to/QtForWebAssembly/bin:$PATH.
当然,您必须将/path/to替换为实际的文件系统路径。
您现在可以开始操作了!您只需像其他任何 Qt 应用程序一样运行qmake即可,如下所示:
qmake mycoolapp.pro && make.
如果需要调试,请按照以下方式重新运行带有CONFIG+=debug的qmake:
qmake CONFIG+=debug mycoolapp.pro && make.
这将为编译器和链接器添加各种 Emscripten 特定参数。
一旦构建完成,您可以使用 Emscripten 的emrun命令运行它,这将启动一个简单的 Web 服务器并服务<target>.html文件。这将反过来加载qtloader.js,然后加载<target>.js文件,然后加载<target>.wasm二进制文件:
emrun --browser firefox --hostname 10.0.0.4 <target>.html.
您也可以给emrun指定目录,例如:
emrun --browser firefox --hostname 10.0.0.4 ..
这给您时间打开浏览器控制台进行调试。现在,让我们看看如何使用 Qt Creator 进行构建。
使用 Qt Creator 构建
一旦您从命令行编译了 Qt 本身,您就可以使用 Qt Creator 构建和运行您的 Qt 应用程序。
构建环境
在 Qt Creator 中,导航到工具 | 选项... | 套件
然后转到编译器选项卡。您需要添加emcc作为 C 编译器,并将em++作为 C++编译器,因此点击添加按钮并从下拉列表中选择自定义。
首先选择 C 并添加以下详细信息:
-
名称:
emcc (1.38.16) -
编译器路径:
/home/user/emsdk/emscripten/1.38.16/emcc -
Make 路径:
/usr/bin/make -
ABI:
x86 linux unknown elf 64bit -
Qt mkspecs:
wasm-emscripten
选择 C++并添加以下详细信息:
-
名称:
emc++(1.38.16) -
编译器路径:
/home/user/emsdk/emscripten/1.38.16/em++ -
Make 路径:
/usr/bin/make -
ABI:
x86 linux unknown elf 64bit -
Qt mkspecs:
wasm-emscripten
点击应用。
前往标签页“Qt 版本”并点击添加按钮。导航到您构建 Qt for WebAssembly 的位置,然后在 bin 目录中选择 qmake。点击应用。
前往标签页“工具包”,然后点击添加按钮。添加以下详细信息:
-
名称:
Qt %{Qt:Version} (qt5-wasm) -
编译器:
C: emcc (1.38.16) -
编译:
C++: em++ (1.38.16) -
Qt 版本:
Qt (qt5-wasm)
运行环境
您需要使应用程序的项目处于活动状态,以构建 Qt for WebAssembly。从 Qt Creator 左侧的按钮中选择“项目”,然后选择您的 Qt for WebAssembly 工具包。
在 Qt Creator 中运行 WebAssembly 应用目前有些棘手,因为你需要将 emrun 指定为一个自定义的可执行文件,然后将其构建目录或 <target>.html 文件作为其参数。你也可以指定要运行的浏览器。你可以使用 --browser chrome 参数运行 Chrome。
要获取已找到的浏览器列表,请运行命令 emrun --list_browsers。
您甚至可以使用 --android 参数在连接到 USB 的 Android 设备上运行应用。您需要安装并运行 Android Debug Bridge (adb) 命令。
无论如何,现在我们知道了如何运行应用,我们需要告诉 Qt Creator 项目运行它。
前往“项目”|“运行”。在“运行”部分,选择“添加”|“自定义可执行文件”,并添加以下详细信息:
-
可执行文件:
/home/user/emsdk/emrun <target>.html -
工作目录:
%{buildDir}
现在我们已经准备好构建和运行。以下是它应该看起来的样子:

我们甚至可以运行 OpenGL 应用!以下是从 Android Firefox 浏览器中运行的 Qt 的 hellogles3 示例:

我们还可以运行声明性应用!以下是从 Qt Quick 的 qopenglunderqml 示例应用:

为移动和嵌入式设备部署
实际上,为移动和嵌入式设备部署只需将 Emscripten 构建的结果文件复制到支持 CORS 的 Web 服务器上。
任何支持 WebAssembly 的网络浏览器都能运行它。
当然,屏幕尺寸也是一个需要考虑的因素。
对于测试,您可以使用 Emscripten SDK 中的 emrun 命令运行您的应用程序。如果您计划从除 localhost 之外的其他设备进行测试,您需要使用 --hostname 参数设置它使用的 IP 地址。
还有用于测试 CORS 启用型 Web 服务器的 Python 脚本。Apache Web 服务器也可以配置为支持 CORS。
目前需要部署的文件有五个——qtloader.js、qtlogo.svg、<target>.html、<target>.js 和 <target>.wasm。.wasm 文件是大的 WebAssembly 二进制文件,静态链接。以下是一些建议,以帮助您完成此过程。
小贴士、技巧和建议
Qt for WebAssembly 被视为 Qt 的跨平台构建。它是一种新兴技术,因此,可能需要一些特殊设置配置来更改或启用所需的一些功能。在使用它作为目标时,你需要注意以下几点。
在这里,我提供了一些关于 Qt for WebAssembly 的技巧。
浏览器
所有主流浏览器现在都支持加载 WebAssembly。Firefox 似乎加载速度最快,尽管 Chrome 有一个可以设置为加速的配置(查看chrome://flags for #enable-webassembly-baseline)。随 Android 和 iOS 一起提供的移动浏览器也适用,尽管这些浏览器可能会遇到内存不足错误,这取决于正在运行的应用程序。
Qt 5.13 for WebAssembly 添加了对线程的实验性支持,这些线程依赖于浏览器中的onSharedArrayBuffer支持。由于 Spectre 漏洞,默认情况下已关闭,需要在浏览器中启用。
在 Chrome 中,导航到chrome://flags并启用#enable-webassembly-threads。
在 Firefox 中,导航到about://config并启用javascript.options.shared.memory。
调试
调试是通过在 Web 浏览器中使用调试控制台来完成的。可以通过调用带有CONFIG+=debug的qmake来启用额外的调试功能,即使是在发布模式下编译的 Qt。以下是崩溃可能看起来像什么:

你还可以从你的手机进行远程调试,并在你的桌面上查看远程浏览器的 JavaScript 控制台输出。请参阅以下链接:
developer.mozilla.org/en-US/docs/Tools/Remote_Debugging
网络
可以使用常规的QNetworkAccessManager进行简单的下载请求。这些将通过XMLNetworkRequest进行,并需要启用 CORS 的服务器才能下载。典型的QTCPSocket和QUdpSockets会被转换成 WebSockets。你的 Web 服务器需要支持 WebSockets,或者你可以使用 Websockify 工具,该工具可以从以下链接获取:
字体和文件系统访问
系统字体无法访问,必须包含并嵌入到应用程序中。Qt 嵌入了一种字体。
文件系统访问目前也不受支持,但将来将通过使用 Qt WebAssembly 特定 API 来实现。
OpenGL
OpenGL 以 OpenGL ES2 的形式得到支持,并将其转换成 WebGL。
如果你计划在 WebAssembly 应用程序中使用 OpenGL,你应该注意 OpenGL ES2 和 WebGL 之间的一些差异。WebGL 通常更严格。
这里是一些关于 WebGL 的差异:
-
缓冲区在其生命周期内只能绑定到一个
ARRAY_BUFFER或ELEMENT_ARRAY_BUFFER -
没有客户端
Arrays -
没有二进制着色器,
ShaderBinary -
对
drawElements强制执行offset;vertexAttribPointer和vertexAttribPointer的stride参数是数据类型大小的倍数 -
drawArrays和drawElements被限制在缓冲区边界之外请求数据 -
添加
DEPTH_STENCIL_ATTACHMENT和DEPTH_STENCIL -
texImage2D和texSubImage2D的大小基于TexImageSource对象 -
copyTexImage2D、copyTexSubImage2D和readPixels不能触及framebuffer之外的像素 -
模板测试和绑定的
framebuffer限制了绘制 -
vertexAttribPointer的值不得超过 255 -
zNear不能大于zFar -
常量颜色和常量 alpha 不能与
blendFunc一起使用 -
不支持
GL_FIXED -
compressedTexImage2D和compressedTexSubImage2D不受支持 -
GLSL 令牌大小限制为 256 个字符
-
GLSL 只支持 ASCII 字符
-
GLSL 限制为 1 级嵌套结构
-
通用和属性位置长度限制为 256 个字符
-
INFO_LOG_LENGTH、SHADER_SOURCE_LENGTH、ACTIVE_UNIFORM_MAX_LENGTH和ACTIVE_ATTRIBUTE_MAX_LENGTH已被移除。 -
传递给
texSubImage2D的纹理类型必须与texImage2D匹配 -
不允许对同一纹理(反馈循环)进行
read和write调用 -
从缺失的附件中读取数据是不允许的
-
属性别名不允许
-
gl_Position的初始值定义为 (0,0,0,0)
更多信息,请参阅以下网页:
WebGL 1.0 www.khronos.org/registry/webgl/specs/latest/1.0/#6
WebGL 2.0 www.khronos.org/registry/webgl/specs/latest/2.0/#5
支持的 Qt 模块
Qt for WebAssembly 支持以下 Qt 模块:
-
qtbase -
qtdeclarative -
qtquickcontrols2 -
qtwebsockets -
qtmqtt -
qtsvg -
qtcharts -
qtgraphicaleffects
其他注意事项
二级事件循环在 Qt for Webassembly 中不起作用。这是因为它需要连接的 Emscripten 事件循环不会返回。如果您需要弹出对话框,不要调用 exec(),而是调用 show(),并使用信号获取返回值。
在移动平台如 Android 和 iOS 上的虚拟键盘不会自动弹出。您可以直接在项目中使用 Qt 虚拟键盘。
摘要
Qt for WebAssembly 是 Qt 的新兴平台,可以在网络浏览器中运行 Qt 应用程序。
您现在应该能够下载或构建 Emscripten SDK,并用于构建 WebAssembly 的 Qt。您现在可以在网络浏览器中运行 Qt 应用程序,包括移动和嵌入式设备,只要浏览器支持 WebAssembly。
在最后一章,我们探讨了构建一个完整的 Linux 嵌入式操作系统。
第十五章:构建 Linux 系统
在嵌入式设备上构建自己的 Linux 系统可能是一项令人望而生畏的任务。了解需要哪些软件才能使堆栈启动并运行;了解软件依赖项;找到要下载的软件并下载它;配置、构建和打包所有这些软件——这实际上可能需要几周的时间。在过去的好日子里是这样的。现在,有一些优秀的工具可以简化构建自定义 Linux 文件系统的过程。如果您有一台足够强大的机器,您可以在一天之内启动并运行嵌入式设备。
原型设计始终是设备创建的第一步。拥有正确的工具将使此过程更加流畅。嵌入式系统需要快速且直接地启动到 Qt 应用程序,例如汽车仪表盘。在本章中,您将学习如何使用 Yocto 和 Boot to Qt for Device Creation 创建嵌入式 Linux 系统的完整软件栈。将使用 Raspberry Pi 设备作为目标来演示如何构建操作系统。
我们将探讨以下内容:
-
Bootcamp – Boot to Qt
-
DIY – 定制嵌入式 Linux
-
部署到嵌入式系统
Bootcamp – Boot to Qt
我们已经在第十二章中讨论了 Qt 公司的 Boot to Qt 系统,交叉编译和远程调试。Boot to Qt 提供了配置文件,供您使用以创建自定义操作系统。它需要 BitBake 软件 和 Yocto 项目,这是一个开源项目,旨在帮助构建基于 Linux 的自定义系统,该项目本身基于我的老朋友 OpenEmbedded。
在本书的 /path/to/install/dir/<Qtversion>/Boot2Qt/sources/meta-boot2qt/b2qt-init-build-env 文件中有一个名为 b2qt-init-build-env 的脚本,该脚本将为 Raspberry Pi 初始化构建。您可以从您选择的构建目录中运行该命令。
要获取支持设备的列表,请使用 list-devices 参数。我的系统输出如下:

您需要初始化构建系统和构建环境,因此请运行名为 b2qt-init-build-env 的脚本,该脚本位于 Boot to Qt 安装目录中:
/path/to/install/dir/<Qtversion>/Boot2Qt/sources/meta-boot2qt/b2qt-init-build-env init --device raspberrypi3
将 /path/to/install/dir 替换为 Boot2Qt 所在的目录路径,通常是 ~/Qt。同时,将 <Qtversion> 替换为安装的 Qt 版本。如果您使用的是不同的设备,将 raspberrypi3 更改为 Boot2Qt 支持的设备列表中的一个。
Yocto 提供了脚本和配置,以便您可以构建自己的系统并对其进行自定义,也许还可以添加 MySQL 数据库支持。B2Q 脚本 setup-environment.sh 将帮助设置开发环境。
您需要将设备类型导出到 MACHINE 环境变量中,并源 environment 设置脚本:
export MACHINE=raspberrypi3
source ./setup-environment.sh
现在,您可以使用以下命令构建默认镜像:
bitbake b2qt-embedded-qt5-image
您可以先通过添加默认情况下不存在的所需软件包来自定义它——比如说添加mysql插件,这样我们就可以远程访问数据库了!让我们看看如何做到这一点。
滚动自己的——定制嵌入式 Linux
Yocto 有一个历史,它起源于 OpenEmbedded 项目。OpenEmbedded 项目在编程世界中的名字来源于 OpenZaurus 项目。当时,我参与了 OpenZaurus 及其相关项目,最初的焦点是运行 Trolltech 的 Qtopia 的 Sharp Zaurus,使用的是不同的操作系统。OpenZaurus 是一个开源的替代操作系统,用户可以将它刷到他们的设备上。构建系统的演变从基于 Makefile 的 Buildroot 到被 BitBake 取代。
您当然可以为此部分构建 Poky 或 Yocto。我将使用Boot2Qt配置。
要开始使用 Yocto 并进行定制,请使用以下命令创建基本映像:
bitbake core-image-minimal
这将需要相当多的时间。
基本定制过程将与定制 Boot to Qt 相同,包括添加层和配方,以及定制现有配方。
系统定制
默认情况下,Boot2Qt rpi映像不包含 MySQL Qt 插件,所以我之前提到的 MySQL 示例将无法工作。我通过自定义映像构建添加了它。
Yocto 和所有基于 BitBake 的系统使用conf/local.conf文件,以便您可以自定义映像构建。如果您还没有,在运行setup-environment.sh file之后,创建一个local.conf文件并添加以下代码行:
PACKAGECONFIG_append_pn-qtbase = " sql-mysql"
sql-mysql部分来自 Qt 的配置参数,因此这是告诉bitbake将-sql-mysql参数添加到配置参数中,这将构建 MySQL 插件并将其包含在系统映像中。还有其他选项,但您需要查看meta-qt5/recipes-qt/qt5/qtbase_git.bb文件中以PACKAGECONFIG开头的行。
我还需要进行一项其他定制,这与 Qt 无关。OpenEmbedded 使用www.example.com URL 来测试连通性。由于某种原因,我的 ISP 的 DNS 没有https://www.example.com的条目,所以我最初无法访问它,构建立即失败。我可以在计算机的网络配置中添加一个新的 DNS,但告诉bitbake使用另一个服务器进行在线检查更快,所以我将以下行添加到我的conf/local.conf文件中:
CONNECTIVITY_CHECK_URIS ?= "https://www.google.com/
如果您需要进行更广泛的定制,您可以创建自己的bitbake层,这是一个配方集合。
local.conf 文件
conf/local.conf文件是您可以进行本地更改以构建映像的地方。就像我们在上一节中提到的PACKAGECONFIG_append_pn-一样,还有其他方法可以添加软件包和发布其他配置命令。模板化的local.conf包含大量注释,以指导您完成这个过程。
IMAGE_INSTALL_append 允许您将包添加到镜像中。
PACKAGECONFIG_append_pn-<package> 允许您将特定于包的配置追加到包中。在qtbase的情况下,它允许您向配置过程添加参数。每个配方都将具有特定的配置。
meta- 目录
层是一种向包添加功能或向现有包添加功能的方式。要创建自己的层,您需要在sources/目录中创建一个模板目录结构,在那里您初始化了bitbake构建。将<layer>更改为您打算使用的名称:
sources/meta-<layer>/
sources/meta-<layer>/licenses/
sources/meta-<layer>/recipes/
sources/meta-<layer>/conf/layer.conf
sources/meta-<layer>/README
licenses/目录是放置任何许可证文件的位置。
您可能添加的任何配方都直接放入recipes/中。关于这一点稍后会有更多说明。
layer.conf文件是层的控制配置。使用此文件的起点可以是以下内容,其中包含通用条目:
BBPATH .= ":${LAYERDIR}"
BBFILES += "${LAYERDIR}/recipes-*/*/*.bb \
${LAYERDIR}/recipes-*/*/*.bbappend"
BBFILE_COLLECTIONS += "meta-custom"
BBFILE_PATTERN_meta-custom = "^${LAYERDIR}/"
BBFILE_PRIORITY_meta-custom = "5"
LAYERVERSION_meta-custom = "1"
LICENSE_PATH += "${LAYERDIR}/licenses"
将meta-custom更改为您想要的任何名称。
一旦创建了层,您需要将其添加到conf/bblayers.conf文件中,该文件位于您在Boot2Qt构建中初始化的目录中。在我的情况下,这是~/development/b2qt/build-raspberrypi3/。
我们现在可以向我们的自定义层添加一个或多个包。
<recipe>.bb文件
如果您有现有的代码,或者如果某个软件项目需要包含在系统镜像中,您也可以创建自己的配方。
在自定义层中,我们创建了一个recipes/目录,我们的新配方可以在这里存在。
为了了解如何编写配方,请查看 Boot2Qt 或 Yocto 中包含的一些配方。
有一些脚本可以帮助创建配方文件,即devtool和recipetool。devtool和recipetool命令在执行功能上相当相似。Devtool在需要应用补丁和修改代码时会使事情变得更容易。有时,您的软件需要在实际设备上进行开发或调试,例如,如果您正在开发使用任何传感器的产品。Devtool还可以构建配方,这样您就可以解决其中的问题。
devtool 命令
devtool --help的输出如下:

最重要的参数将是add、modify和upgrade。
对于devtool,我将使用 Git 仓库 URL 来添加我的sensors-examples仓库:
devtool add sensors-examples https://github.com/lpotter/sensors-examples.git
运行前面的命令将输出类似以下内容:

我们需要尝试构建包以查看它是否成功或失败,这可以通过运行以下命令来完成:
devtool build sensors-examples
如果构建失败,我们可能需要编辑这个.bb文件来使其构建成功。
在sensors-examples的情况下,我们将得到以下输出:

我们已经构建完成了!
您可以在tmp/work/cortexa7hf-neon-vfpv4-poky-linux-gnueabi/sensors-examples/1.0+git999-r0中找到这个构建。
如果你想要编辑一个食谱,你可以使用 devtool 并创建一个补丁,这样你就可以使用了:
devtool modify qtsensors
运行前面的命令后,我们得到以下输出:

这将在你的本地工作区中复制食谱,这样你就可以编辑它,而不用担心在更新 bitbake 时丢失。现在,你可以编辑源代码,在这个例子中,是 qtsensors。我有一个补丁,用于添加 Raspberry Pi Sense HAT 的 qtsensors 插件,所以我现在将手动应用它:

我的补丁已经过时了,我需要修复它。你可以通过运行以下命令单独构建它:
devtool build qtsensors
这最初无法找到 rtimulib.h,因此我们需要添加对该库的依赖。
在 OpenEmbedded 层索引中,有一个 python-rtimu 食谱,但它没有导出头文件或构建库,所以我将基于 Git 仓库创建一个新的食谱,如下所示:
devtool add rtimulib https://github.com/RPi-Distro/RTIMULib.git
这是一个基于 cmake 的项目,我需要修改食谱来添加一些 cmake 参数。为了编辑这个,我可以简单地运行以下命令:
devtool edit-recipe rtimulib
我添加了以下行,这些行使用 EXTRA_OECMAKE 来禁用一些依赖于 Qt 4 的演示。我认为曾经有一个补丁将其移植到 Qt 5,但我找不到它。最后的 EXTRA_OEMAKE 告诉 cmake 在 Linux 目录中构建。然后,我们告诉 bitbake 它需要继承 cmake 相关的内容:
EXTRA_OECMAKE = "-DBUILD_GL=OFF"
EXTRA_OECMAKE += "-DBUILD_DRIVE=OFF"
EXTRA_OECMAKE += "-DBUILD_DRIVE10=OFF"
EXTRA_OECMAKE += "-DBUILD_DEMO=OFF"
EXTRA_OECMAKE += "-DBUILD_DEMOGL=OFF"
EXTRA_OECMAKE += "Linux"
inherit cmake
然后,我们需要编辑我们的 qtsensors_git.bb 文件,以便我们可以添加对这个新包的依赖。这将允许它找到头文件:
DEPENDS += "rtimulib"
当我运行构建命令 bitbake qtsensors 时,它将确保我的 rtimulib 包被构建,然后应用我的 sensehat 补丁到 qtsensors,然后构建并打包它!
Recipetool 是另一种创建新食谱的方法。它的设计和使用比 devtool 更简单。
recipetool 命令
使用 recipetool create --help 命令的输出如下:

例如,我运行了 recipetool -d create -o rotationtray_1.bb https://github.com/lpotter/rotationtray.git:

使用 -d 参数意味着它将更加详细,所以我排除了部分输出:

现在,你可能想要编辑生成的文件。了解 bitbake 的一个好方法是查看其他食谱,看看它们是如何做的。
bitbake-layers
OpenEmbedded 随带一个名为 bitbake-layers 的脚本,你可以用它来获取有关可用层的详细信息。你还可以使用它来添加新层或从配置文件中删除一个层。
运行 bblayers --help 将给出以下输出:

运行 bitbake-layers show-recipes 将输出所有可用的食谱。这个列表可能相当长。
yocto-layer
Yocto 有一个名为yocto-layer的脚本,它将创建一个空的层目录结构,然后你可以使用bitbake-layers将其添加。你还可以添加一个示例配方和一个bbappend文件。
要创建一个新的层,使用create参数运行yocto-layer:
yocto-layer create mylayer
这将交互式运行并询问你几个问题。我告诉它两个问题都回答“是”以创建示例:

你将看到一个新的名为meta-mylayer的目录树。然后你可以使用bitbake-layers将新层提供给bitbake,如下所示:
bitbake-layers add-layer meta-mylayer
使用以下命令查看新层运行情况:
bitbake-layers show-layers
bbappend 文件
当我将qtsensors配方导入我的工作区时,我可以用一个bbappend文件。当你将配方导入你的工作区时,它实际上是被复制的。请注意,然而,你将无法使用devtool构建它。
我还提到,yocto-layer脚本可以创建一个包含补丁的示例bbappend文件,这样我们可以看到它是如何工作的。你选择的文件名必须与你要修改的配方相匹配。名称的唯一区别将是扩展名,对于bbappend文件来说是.bbappend。
在workspace/conf/local.conf文件中,有一行关于 BBFILES 的说明,告诉我它在寻找bbappend文件的位置。当然,你可以将它们放在任何地方,只要告诉bitbake它们在哪里。我的配置是它们位于${LAYERDIR}/appends/*.bbappend。
我们的系统很简单——它只应用补丁。只需在bbappend文件中包含以下几行,我们就可以让它启动并运行:
-
SUMMARY:简单字符串解释补丁 -
FILESEXTRAPATHS_prepend:补丁文件路径的字符串 -
SRC_URI:补丁文件的 URL 字符串
如果我们想要创建一个bbappend文件来使用sensehat补丁修补qtsensors,这将是一个四行编辑,包括实际的补丁。简单的bbappends文件看起来像这样:
SUMMARY = "Sensehat plugin for qtsensors"
DEPENDS += "rtimulib"
FILESEXTRAPATHS_prepend := "${THISDIR}:"
SRC_URI += "file://0001-Add-sensehat-plugin.patch"
将补丁放置在目录中是一种良好的实践,但这个补丁与bbappend文件处于同一级别。
在我们能够构建附加的配方之前,我们需要从工作区中移除导入的qtsensors配方:
devtool reset qtsensors
将qtsensors_git.bbappend和补丁文件放入appends目录。要构建它,只需运行以下命令:
bitbake qtsensors
现在我们可以自定义我们的 OpenEmbedded/Yocto 镜像,我们可以将设备部署到设备上。
将部署到嵌入式系统
我们已经构建了一个自定义的系统镜像,系统可以以几种不同的方式部署到设备上。通常,嵌入式设备有特定的部署方式。可以使用 dd 或类似工具将系统镜像文件直接写入存储磁盘来部署到 Raspberry Pi。其他设备可能需要将文件系统写入格式化的磁盘,甚至使用 JTAG 进行低级部署。
OpenEmbedded
如果您计划使用 Qt 与 OpenEmbedded,您应该了解meta-qt5-extra层,它包含 LXQt 和甚至 KDE5 等桌面环境。我个人使用这两个环境,并在我的桌面上在这两个之间来回切换,但大多数时候我更喜欢 LXQt,因为它轻量级。
使用 LXQt 构建 OpenEmbedded 镜像相当直接,与构建 Boot to Qt 镜像类似。
要查看可用的镜像目标,您可以运行以下命令:
bitbake-layers show-recipes | grep image
如果您有 Boot to Qt,您应该看到b2qt-embedded-qt5-image层,我们将使用它来为 Raspberry Pi 创建镜像。您还应该看到 OpenEmbedded 的core-image-base和core-image-x11,这可能也很有趣。
还有其他可用的层,您可以在layers.openembedded.org/layerindex/branch/master/layers/中搜索并下载。
部署方法实际上取决于您的目标设备。让我们看看如何将系统镜像部署到 Raspberry Pi。
Raspberry Pi
本节中的示例针对 Raspberry Pi。您可能有不同的设备,这里的流程可能类似。
如果您打算只创建一个可以在 Qt Creator 中使用的交叉toolchain,您可以运行以下命令:
bitbake meta-toolchain-b2qt-embedded-qt5-sdk
要创建要复制到 SD 卡上的系统镜像,请运行以下命令:
bitbake b2qt-embedded-qt5-image
b2qt-embedded-qt5-image目标还会在需要时创建 SDK。当您让它运行一天左右时,您将有一个新鲜出炉的 Qt 镜像!我建议使用您拥有的最快机器,并具有最多的内存和存储空间,因为完整的发行版构建可能需要数小时,即使在快速机器上也是如此。
然后,您可以将系统镜像用于设备的闪存程序或它使用的任何方法来制作文件系统。对于 RPI,您将 micro SD 卡放入 USB 读卡器中,然后运行dd命令来写入镜像文件。
我需要写入 SD 卡的文件位于以下位置:
tmp/deploy/images/raspberrypi3/b2qt-embedded-qt5-image-raspberrypi3-20190224202855.rootfs.rpi-sdimg
要将其写入 SD 卡,请使用以下命令:
sudo dd if=/path/to/sdimg of=/dev/path/to/usb/drive bs=4M status=progress
我的确切命令如下:
sudo dd if=tmp/deploy/images/raspberrypi3/b2qt-embedded-qt5-image-raspberrypi3-20190224202855.rootfs.rpi-sdimg of=/dev/sde bs=4M status=progress
现在,等待直到所有内容都已写入磁盘。将其插入 Raspberry Pi 的 SD 卡槽中,打开电源,然后您就可以开始了!
摘要
在本章中,我们学习了如何使用bitbake构建自定义系统镜像,从 Qt 的 Boot to Qt 配置文件开始。这个过程与构建 Yocto、Poky 或Ångström 类似。我们还学习了如何使用devtool自定义 Qt 的构建以添加更多功能。然后,我们讨论了如何使用recipetool将您自己的配方添加到镜像中。通过这样做,您还可以将此配方添加到一个新层中。最后,我们将镜像部署到 SD 卡上,以便在 Raspberry Pi 上运行。


浙公网安备 33010602011771号