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

Qt5 C++ GUI 编程秘籍(全)

原文:annas-archive.org/md5/9BC2D959B55E8629DCD159B600A4BD90

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

计算机软件市场的持续增长导致了一个竞争激烈和具有挑战性的时代。你的软件不仅需要功能强大且易于使用,还必须对用户具有吸引力和专业性。为了在市场上获得竞争优势,产品的外观和感觉至关重要,并且应该在生产阶段早期予以关注。在本书中,我们将教你如何使用 Qt5 开发平台创建功能强大、吸引人且用户友好的软件。

本书涵盖了什么

第一章 外观和感觉定制,展示了如何使用 Qt Designer 和 Qt Quick Designer 设计程序的用户界面。

第二章 状态和动画,解释了如何通过使用状态机框架和动画框架来为用户界面小部件添加动画效果。

第三章 QPainter 和 2D 图形,介绍了如何使用 Qt 的内置类在屏幕上绘制矢量形状和位图图像。

第四章 OpenGL 实现,演示了如何通过在 Qt 项目中集成 OpenGL 来渲染程序中的 3D 图形。

第五章 使用 Qt5 构建触摸屏应用程序,解释了如何创建适用于触摸屏设备的程序。

第六章 简化 XML 解析,展示了如何处理 XML 格式的数据,并与 Google 地理编码 API 一起使用,以创建一个简单的地址查找器。

第七章 转换库,介绍了如何使用 Qt 的内置类以及第三方程序在不同变量类型、图像格式和视频格式之间进行转换。

第八章 访问数据库,解释了如何使用 Qt 将程序连接到 SQL 数据库。

第九章 使用 Qt Web 引擎开发 Web 应用程序,介绍了如何使用 Qt 提供的 Web 渲染引擎,并开发利用 Web 技术的程序。

本书需要什么

以下是本书的先决条件:

  1. Qt5(适用于所有章节)

  2. FFmpeg(用于第七章 转换库

  3. XAMPP(用于第八章 访问数据库

本书适合谁

本书旨在为那些想使用 Qt5 开发软件的人提供帮助。如果你想提高软件应用的视觉质量和内容呈现,这本书将最适合你。

部分

在本书中,你会经常看到几个标题(准备工作,如何做,它是如何工作的,还有更多,另请参阅)。

为了清晰地说明如何完成一个配方,我们使用以下这些部分:

准备工作

本节告诉你在配方中可以期待什么,并描述了为配方设置任何软件或任何预备设置所需的步骤。

如何做...

本节包含了遵循配方所需的步骤。

它是如何工作的...

本节通常包括对上一节内容的详细解释。

还有更多...

本节包含有关配方的附加信息,以使读者更加了解配方。

另请参阅

本节为配方提供了其他有用信息的链接。

约定

在本书中,你会发现一些区分不同信息类型的文本样式。以下是一些样式的示例及其含义解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"在mylabel.cpp源文件中,定义一个名为SetMyObject()的函数来保存对象指针。"

代码块设置如下:

QSpinBox::down-button
{
  image: url(:/images/spindown.png);
  subcontrol-origin: padding;
  subcontrol-position: right bottom;
}

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

QSpinBox::down-button
{
 image: url(:/images/spindown.png);
  subcontrol-origin: padding;
  subcontrol-position: right bottom;
}

新术语重要单词以粗体显示。例如,屏幕上看到的单词,比如菜单或对话框中的单词,会以这样的方式出现在文本中:"转到Library窗口中的Imports标签,并向您的项目添加一个名为QtQuick.Controls的 Qt Quick 模块。"

注意

警告或重要提示会显示在这样的框中。

提示

提示和技巧会显示为这样。

第一章:外观和感觉定制

在本章中,我们将涵盖以下内容:

  • 在 Qt Designer 中使用样式表

  • 基本样式表定制

  • 使用样式表创建登录界面

  • 在样式表中使用资源

  • 自定义属性和子控件

  • QML 中的样式

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

介绍

Qt 允许我们通过大多数人熟悉的方法轻松设计程序的用户界面。Qt 不仅为我们提供了一个强大的用户界面工具包 Qt Designer,使我们能够在不写一行代码的情况下设计用户界面,而且还允许高级用户通过一种简单的脚本语言 Qt 样式表来自定义他们的用户界面组件。

在 Qt Designer 中使用样式表

在这个例子中,我们将学习如何通过使用样式表和资源来改变程序的外观和感觉,使其看起来更专业。Qt 允许你使用一种名为 Qt 样式表的样式表语言来装饰你的图形用户界面GUI),这与网页设计师使用的层叠样式表CSS)非常相似,用于装饰他们的网站。

如何做…

  1. 我们需要做的第一件事是打开 Qt Creator 并创建一个新项目。如果这是你第一次使用 Qt Creator,你可以点击上面写着New Project和一个+号的大按钮,或者简单地转到File | New File or New Project

  2. 然后,在项目窗口下选择Application,并选择Qt Widgets Application

  3. 之后,点击底部的Choose按钮。然后会弹出一个窗口,要求你输入项目名称和位置。

  4. 完成后,点击Next几次,然后点击Finish按钮创建项目。现在我们将坚持使用所有默认设置。项目创建完成后,你会看到窗口左侧有一个名为Mode Selector的面板,上面有很多大图标;我们稍后将在How it works...部分详细讨论这一点。

  5. 然后,你还会看到所有源文件都列在位于Mode Selector面板旁边的Side Bar面板上。这是你可以选择要编辑的文件的地方,在这种情况下是mainwindow.ui,因为我们即将开始设计程序的 UI!

  6. 双击 mainwindow.ui,你会看到一个完全不同的界面突然出现。Qt Creator 实际上帮助你从脚本编辑器切换到 UI 编辑器(Qt Designer),因为它检测到你要打开的文件具有.ui扩展名。

  7. 你还会注意到Mode Selector面板上高亮显示的按钮已经从Edit按钮变成了Design按钮。你可以通过点击Mode Selector面板上半部分的按钮之一,切换回脚本编辑器或切换到其他工具。

  8. 让我们回到 Qt Designer,看看mainwindow.ui文件。这基本上是我们程序的主窗口(如文件名所示),默认情况下是空的,没有任何小部件。你可以尝试通过点击Mode Selector面板底部的Run按钮(绿色箭头按钮)来编译和运行程序,一旦编译完成,你会看到一个空窗口弹出来:如何做…

  9. 现在,让我们通过单击小部件框中的Push Button项目(在按钮类别下)并将其拖动到表单编辑器中的主窗口,向我们的程序 UI 添加一个按钮。然后,保持按钮选定状态,现在您将在窗口右侧的属性编辑器中看到此按钮的所有属性。向下滚动到中间左右某处,查找名为styleSheet的属性。这是您向小部件应用样式的地方,这些样式可能会根据您设置样式表的方式递归地继承到其子代或孙代。或者,您还可以右键单击表单编辑器中的任何小部件,并从弹出菜单中选择更改样式表

  10. 您可以单击styleSheet属性的输入字段,直接编写样式表代码,或单击输入字段旁边的...按钮,打开编辑样式表窗口,该窗口具有更大的空间,用于编写更长的样式表代码。在窗口顶部,您可以找到几个按钮,例如添加资源添加渐变添加颜色添加字体,这些按钮可以帮助您启动编码,如果您记不住属性的名称。

让我们尝试使用编辑样式表窗口进行一些简单的样式设置。

  1. 单击添加颜色并选择颜色。

  2. 从颜色选择器窗口中选择一个随机颜色,比如纯红色。然后单击确定

  3. 现在,您将看到一行代码已添加到编辑样式表窗口上的文本字段中,例如:

color: rgb(255, 0, 0);

  1. 单击确定按钮,现在您将看到按钮上的文本已更改为红色。

它是如何工作的...

在开始学习如何设计自己的 UI 之前,让我们花点时间熟悉 Qt Designer 的界面:

它是如何工作的...

  1. 菜单栏:菜单栏包含特定于应用程序的菜单,可轻松访问诸如创建新项目、保存文件、撤消、重做、复制、粘贴等基本功能。它还允许您访问随 Qt Creator 一起提供的开发工具,例如编译器、调试器、分析器等。

  2. 小部件框:这是您可以找到 Qt Designer 提供的所有不同类型的小部件的地方。您可以通过单击小部件框中的一个小部件并将其拖动到表单编辑器中,向程序 UI 添加一个小部件。

  3. 模式选择器:模式选择器是一个侧面面板,其中放置了用于轻松访问不同工具的快捷按钮。您可以通过单击模式选择器面板上的编辑设计按钮快速在脚本编辑器和表单编辑器之间切换,这对于多任务处理非常有用。您还可以以相同的速度和方式轻松导航到调试器和分析器工具。

  4. 构建快捷键:构建快捷键位于模式选择器面板的底部。您可以通过按快捷按钮轻松构建、运行和调试项目。

  5. 表单编辑器:表单编辑器是您编辑程序 UI 的地方。您可以从小部件框中选择一个小部件,并将其拖动到表单编辑器中,从而向程序添加不同的小部件。

  6. 表单工具栏:从这里,您可以快速选择要编辑的不同表单,单击位于小部件框上方的下拉框,并选择要在 Qt Designer 中打开的文件。在下拉框旁边是用于在表单编辑器的不同模式之间切换的按钮,还有用于更改 UI 布局的按钮。

  7. 对象检视器:对象检视器列出了当前.ui文件中的所有小部件。所有小部件按照它们在层次结构中的父子关系进行排列。您可以从对象检视器中选择一个小部件,以在属性编辑器中显示其属性。

  8. 属性编辑器:属性编辑器将显示您从对象检视器窗口或表单编辑器窗口中选择的小部件的所有属性。

  9. 操作编辑器和信号与槽编辑器: 此窗口包含两个编辑器,操作编辑器信号与槽编辑器,可以从窗口下方的选项卡中访问。操作编辑器是您创建可以添加到程序 UI 的菜单栏或工具栏中的操作的地方。

  10. 输出窗格: 输出窗格由几个不同的窗口组成,显示与脚本编译和调试相关的信息和输出消息。您可以通过按带有数字的按钮(例如1-Issues2-Search Results3-Application Output等)来在不同的输出窗格之间切换。

还有更多...

在前一节中,我们讨论了如何通过 C++编码将样式表应用到 Qt 小部件。虽然这种方法非常有效,但大多数时候负责设计程序 UI 的人不是程序员,而是专门设计用户友好 UI 的 UI 设计师。在这种情况下,最好让 UI 设计师使用不同的工具设计程序的布局和样式表,而不要在代码中乱搞。

Qt 提供了一个名为 Qt Creator 的多合一编辑器。Qt Creator 包括几种不同的工具,如脚本编辑器、编译器、调试器、分析器和 UI 编辑器。UI 编辑器,也称为 Qt Designer,是设计师设计其程序 UI 而无需编写任何代码的完美工具。这是因为 Qt Designer 采用了所见即所得的方法,通过提供最终结果的准确视觉表示,意味着您在 Qt Designer 中设计的任何内容在编译和运行程序时都会完全相同。

Qt 样式表和 CSS 之间的相似之处如下:

  • CSSh1 { color: red; background-color: white;}

  • Qt 样式表QLineEdit { color: red; background-color: white;}

  • 如您所见,它们都包含选择器和声明块。每个声明包含一个属性和一个值,由冒号分隔。

  • 在 Qt 中,可以通过在 C++代码中调用QObject::setStyleSheet()函数将样式表应用于单个小部件,例如:

myPushButton->setStyleSheet("color : blue");
  • 上述代码将将变量名为myPushButton的按钮的文本颜色更改为蓝色。您也可以通过在 Qt Designer 的样式表属性字段中编写声明来实现相同的结果。我们将在下一节中更多地讨论 Qt Designer。

  • Qt 样式表还支持 CSS2 标准中定义的所有不同类型的选择器,包括通用选择器、类型选择器、类选择器、ID 选择器等,这使我们能够将样式应用于非常具体的单个或一组小部件。例如,如果我们想要更改具有对象名称usernameEdit的特定行编辑小部件的背景颜色,我们可以使用 ID 选择器来引用它:

QLineEdit#usernameEdit { background-color: blue }

注意

要了解 CSS2 中所有选择器的详细信息(这些选择器也被 Qt 样式表支持),请参考此文档:www.w3.org/TR/REC-CSS2/selector.html

基本样式表定制

在前面的示例中,您学会了如何在 Qt Designer 中将样式表应用于小部件。让我们疯狂一下,进一步推动事情,创建一些其他类型的小部件,并将它们的样式属性更改为一些奇怪的东西以便学习。但是这一次,我们不会逐个将样式应用于每个小部件,而是学会将样式表应用于主窗口,并让它在整个层次结构中继承到所有其他小部件,以便更容易地管理和维护样式表。

如何做...

  1. 首先,让我们通过选择它并单击styleSheet属性旁边的小箭头按钮来从按钮中删除样式表。这个按钮将将属性恢复到默认值,在这种情况下是空的样式表。

  2. 然后,通过将它们一个接一个地从小部件框拖动到表单编辑器中,向 UI 添加几个小部件。我添加了一个行编辑、组合框、水平滑块、单选按钮和复选框。

  3. 为了简单起见,通过从对象检查器中选择它们,右键单击并选择删除,从您的 UI 中删除菜单栏、主工具栏和状态栏。现在您的 UI 应该看起来类似于这样:如何做...

  4. 从表单编辑器或对象检查器中选择主窗口,然后右键单击并选择更改样式表以打开编辑样式表

插入以下样式表:

border: 2px solid gray;
border-radius: 10px;
padding: 0 8px;
background: yellow;
  1. 现在您将看到一个完全奇异的 UI,所有内容都被涂成黄色,带有厚厚的边框。这是因为前面的样式表没有选择器,这意味着样式将应用于主窗口的所有子小部件,一直到层次结构的底部。为了改变这一点,让我们尝试一些不同的东西:
QPushButton
{
  border: 2px solid gray;
  border-radius: 10px;
  padding: 0 8px;
  background: yellow;
}
  1. 这一次,只有按钮将获得前面代码中描述的样式,所有其他小部件将返回到默认样式。您可以尝试向您的 UI 添加几个按钮,它们将看起来都一样:如何做...

  2. 这是因为我们明确告诉选择器将样式应用于所有具有名为QPushButton的类的小部件。我们还可以通过在样式表中提及其名称来仅将样式应用于其中一个按钮,如下所示:

QPushButton#pushButton_3
{
  border: 2px solid gray;
  border-radius: 10px;
  padding: 0 8px;
  background: yellow;
}
  1. 一旦您理解了这种方法,我们可以将以下代码添加到样式表中:
QPushButton
{
 color: red;
 border: 0px;
 padding: 0 8px;
 background: white;
}

QPushButton#pushButton_2
{
 border: 1px solid red;
 border-radius: 10px;
}

QPushButton#pushButton_3
{
  border: 2px solid gray;
  border-radius: 10px;
  padding: 0 8px;
  background: yellow;
}
  1. 它的作用基本上是更改所有按钮的样式,以及更改名为pushButton_2的特定按钮的一些属性。我们保留pushButton_3的样式表。现在按钮将看起来像这样:如何做...

  2. 第一组样式表将把所有QPushButton类型的小部件更改为白色的矩形按钮,没有边框,红色文本。然后第二组样式表仅更改名为pushButton_2的特定QPushButton小部件的边框。请注意,pushButton_2的背景颜色和文本颜色仍然分别为白色和红色,因为我们没有在第二组样式表中覆盖它们,因此它将返回到第一组样式表中描述的样式,因为它适用于所有QPushButton小部件。请注意,第三个按钮的文本也变为了红色,因为我们没有在第三组样式表中描述颜色属性。

  3. 之后,使用通用选择器创建另一组样式,如下所示:

*
{
  background: qradialgradient(cx: 0.3, cy: -0.4, fx: 0.3, fy: -0.4, radius: 1.35, stop: 0 #fff, stop: 1 #888);
  color: rgb(255, 255, 255);
  border: 1px solid #ffffff;
}
  1. 通用选择器将影响所有小部件,而不考虑它们的类型。因此,前面的样式表将为所有小部件的背景应用漂亮的渐变颜色,并将它们的文本设置为白色,并给它们一个白色的一像素实线轮廓。我们可以使用rgb函数(rgb(255, 255, 255))或十六进制代码(#ffffff)来描述颜色值,而不是写颜色的名称(即白色)。

  2. 就像以前一样,前面的样式表不会影响按钮,因为我们已经为它们提供了自己的样式,这将覆盖通用选择器中描述的一般样式。请记住,在 Qt 中,更具体的样式最终将在对小部件产生影响的多个样式中使用。这是现在 UI 的外观:如何做...

它是如何工作的...

如果您曾经参与使用 HTML 和 CSS 进行 Web 开发,Qt 的样式表的工作方式与 CSS 完全相同。样式表提供了描述小部件呈现方式的定义 - 小部件组中每个元素的颜色是什么,边框应该有多厚等等。

如果你将小部件的名称指定给样式表,它将更改具有你提供的名称的特定推按钮小部件的样式。其他小部件都不会受到影响,仍将保持默认样式。

要更改小部件的名称,从表单编辑器或对象检查器中选择小部件,并在属性窗口中更改名为objectName的属性。如果之前使用了 ID 选择器来更改小部件的样式,更改其对象名称将破坏样式表并丢失样式。要解决这个问题,只需在样式表中也更改对象名称。

使用样式表创建登录界面

接下来,我们将学习如何将我们在之前示例中学到的所有知识结合起来,为一个想象中的操作系统创建一个虚假的图形登录界面。样式表并不是你需要掌握的唯一东西,以设计良好的 UI。你还需要学会如何使用 Qt Designer 中的布局系统整齐地排列小部件。

操作步骤…

  1. 我们需要做的第一件事是在开始任何操作之前设计图形登录界面的布局。规划对于制作良好的软件非常重要。以下是我制作的一个示例布局设计,以展示我想象中的登录界面将会是什么样子。只要能清晰地传达信息,像这样的简单线条图就足够了:操作步骤…

  2. 现在我们知道了登录界面应该是什么样子,让我们再次回到 Qt Designer。

  3. 我们将首先放置顶部面板上的小部件,然后放置标志和登录表单。

  4. 选择主窗口,并将其宽度和高度从 400 和 300 分别更改为 800 和 600,因为我们将需要更大的空间来放置所有的小部件。

  5. 点击并从小部件框中的显示小部件类别下拖动一个标签到表单编辑器中。

  6. 将标签的objectName属性更改为currentDateTime,并将其Text属性更改为当前日期和时间,仅用于显示目的,例如星期一,2015 年 10 月 25 日 下午 3:14

  7. 点击并从按钮类别下拖动一个推按钮到表单编辑器中。重复此过程一次,因为顶部面板上有两个按钮。将这两个按钮分别重命名为restartButtonshutdownButton

  8. 接下来,选择主窗口并单击表单工具栏上的小图标按钮,当鼠标悬停在上面时,它会显示垂直布局。现在你会看到小部件被自动排列在主窗口上,但还不是我们想要的。

  9. 点击并从布局类别下拖动一个水平布局小部件到主窗口中。

  10. 点击并将两个按钮和文本标签拖放到水平布局中。现在你会看到这三个小部件被水平排列,但在垂直方向上它们位于屏幕中间。水平排列几乎是正确的,但垂直位置完全不对。

  11. 点击并从间隔器类别下拖动一个垂直间隔器,并将其放置在我们之前创建的水平布局下方(红色矩形轮廓下方)。现在你会看到所有的小部件都被间隔器推到了顶部。

  12. 现在,在文本标签和两个按钮之间放置一个水平间隔器,使它们保持分开。这将使文本标签始终保持在左侧,按钮对齐到右侧。

  13. 将两个按钮的水平策略垂直策略属性都设置为固定,并将minimumSize属性设置为55x55。然后,将按钮的text属性设置为空,因为我们将使用图标而不是文本。我们将在下一节学习如何在按钮小部件中放置图标。

  14. 现在你的 UI 应该看起来类似于这样:操作步骤…

接下来,我们将通过以下步骤添加标志:

  1. 在顶部面板和垂直间隔器之间添加一个水平布局,作为标志的容器。

  2. 添加水平布局后,您会发现布局的高度太瘦,无法添加任何小部件。这是因为布局是空的,并且被下方的垂直间隔推到零高度。为解决这个问题,我们可以将其垂直边距(layoutTopMarginlayoutBottomMargin)临时设置得更大,直到向布局添加小部件为止。

  3. 接下来,在您刚刚创建的水平布局中添加一个标签,并将其重命名为logo。我们将在下一节中学习如何将图像插入标签以将其用作徽标。目前,只需清空text属性,并将其Horizontal PolicyVertical Policy属性都设置为Fixed。然后,将minimumSize属性设置为150x150

  4. 如果尚未这样做,请将布局的垂直边距设置回零。

  5. 现在徽标看起来是不可见的,因此我们将添加一个临时样式表使其可见,直到在下一节中为其添加图像。样式表非常简单:

border: 1px solid;
  1. 现在您的 UI 应该看起来类似于这样:操作步骤…

现在让我们按照以下步骤创建登录表单:

  1. 在徽标的布局和垂直间隔之间添加一个水平布局。就像之前一样,将layoutTopMargin属性设置为更大的数字(例如 100),以便更轻松地向其中添加小部件。

  2. 之后,在您刚刚创建的水平布局中添加一个垂直布局。该布局将用作登录表单的容器。将其layoutTopMargin设置为比水平布局低的数字(例如 20),以便我们可以在其中放置小部件。

  3. 接下来,右键单击您刚刚创建的垂直布局,然后选择Morph into -> QWidget。垂直布局现在被转换为一个空小部件。这一步是必不可少的,因为我们将调整登录表单的容器的宽度和高度。布局小部件不包含宽度和高度的任何属性,而只包含边距,因为布局将向其周围的空白空间扩展,这是有道理的,考虑到它没有任何大小属性。将布局转换为QWidget对象后,它将自动继承小部件类的所有属性,因此我们现在可以调整其大小以满足我们的需求。

  4. 将刚刚从布局转换的QWidget对象重命名为loginForm,并将其Horizontal PolicyVertical Policy属性都设置为Fixed。然后,将minimumSize设置为350x200

  5. 由于我们已经将loginForm小部件放入了水平布局中,现在可以将其layoutTopMargin属性设置回零。

  6. 将与徽标相同的样式表添加到loginForm小部件中,以使其暂时可见,但这次我们需要在前面添加一个 ID 选择器,以便仅将样式应用于loginForm,而不是其子小部件:

#loginForm { border: 1px solid; }
  1. 现在您的 UI 应该看起来类似于这样:操作步骤…

我们还没有完成登录表单。现在我们已经为登录表单创建了容器,是时候向表单中添加更多小部件了:

  1. 将两个水平布局放入登录表单容器中。我们需要两个布局,一个用于用户名字段,另一个用于密码字段。

  2. 在您刚刚添加的每个布局中添加一个标签和一个行编辑。将上方标签的text属性更改为“用户名:”,下方标签更改为“密码:”。然后,分别将两个行编辑重命名为usernamepassword

  3. 在密码布局下方添加一个按钮,并将其text属性更改为“登录”。然后,将其重命名为loginButton

  4. 您可以在密码布局和登录按钮之间添加一个垂直间隔以稍微拉开它们之间的距离。放置垂直间隔后,将其sizeType属性更改为Fixed,并将Height更改为5

  5. 现在,选择loginForm容器,并将其所有边距设置为 35。这是为了通过在所有边缘添加一些空间来使登录表单看起来更好。

  6. 您还可以将usernamepasswordloginButton小部件的Height属性设置为 25,以使它们看起来不那么拥挤。

  7. 现在您的用户界面应该看起来像这样:操作步骤…

我们还没有完成!正如您所看到的,由于它们下方的垂直间隔器,登录表单和标志都紧贴主窗口顶部。标志和登录表单应该放在主窗口的中心,而不是顶部。要解决这个问题,请按照以下步骤操作:

  1. 在顶部面板和标志布局之间添加另一个垂直间隔器。这样它将抵消底部的间隔器,从而平衡对齐。

  2. 如果您认为标志与登录表单太过紧密,还可以在标志布局和登录表单布局之间添加一个垂直间隔器。将其sizeType属性设置为Fixed,将Height属性设置为10

  3. 右键单击顶部面板的布局,然后选择Morph into -> QWidget。然后,将其重命名为topPanel。布局必须转换为QWidget的原因是,我们无法对布局应用样式表,因为它除了边距之外没有任何属性。

  4. 目前,您可以看到主窗口的边缘周围有一点边距 - 我们不希望出现这种情况。要删除边距,请从对象检查器窗口中选择centralWidget对象,该对象位于MainWindow面板的正下方,并将所有边距值设置为零。

  5. 此时,您可以通过单击Run按钮(带有绿色箭头图标)来运行项目,以查看您的程序现在的外观。如果一切顺利,您应该看到类似于这样的东西:操作步骤…

  6. 布局完成后,是时候为用户界面添加一些花哨的样式表了!由于所有重要的小部件都被赋予了对象名称,因此我们可以更容易地从主窗口为其应用样式表,因为我们只会将样式表写到主窗口,然后让它们在层次树中继承下来。

  7. 从对象检查器窗口中右键单击MainWindow,然后选择Change Stylesheet

  8. 将以下代码添加到样式表中:

#centralWidget { background: rgba(32, 80, 96, 100); }
  1. 现在您会看到主窗口的背景颜色发生了变化。我们将在下一节中学习如何使用图像作为背景,因此颜色只是临时的。

  2. 在 Qt 中,如果要对主窗口本身应用样式,必须将其应用到其中央小部件,而不是主窗口本身,因为窗口只是一个容器。

  3. 然后,我们将为顶部面板添加一个漂亮的渐变颜色:

#topPanel { background-color: qlineargradient(spread:reflect, x1:0.5, y1:0, x2:0, y2:0, stop:0 rgba(91, 204, 233, 100), stop:1 rgba(32, 80, 96, 100)); }
  1. 之后,我们将为登录表单应用黑色,并使其看起来半透明。之后,我们还将通过设置border-radius属性使登录表单容器的角略微圆润:
#loginForm
{
  background: rgba(0, 0, 0, 80);
  border-radius: 8px;
}
  1. 在我们完成对特定小部件应用样式之后,我们将对一般类型的小部件应用样式:
QLabel { color: white; }
QLineEdit { border-radius: 3px; }
  1. 上述样式表将把所有标签的文本更改为白色,这包括小部件上的文本,因为在内部,Qt 使用相同类型的标签来标记带有文本的小部件。此外,我们使线编辑小部件的角稍微圆润。

  2. 接下来,我们将为用户界面上的所有推按钮应用样式表:

QPushButton
{
  color: white;
  background-color: #27a9e3;
  border-width: 0px;
  border-radius: 3px;
}
  1. 上述样式表将把所有按钮的文本更改为白色,然后将其背景颜色设置为蓝色,并且还使其角稍微圆润。

  2. 为了更进一步推动事情,我们将使用关键字hover来在鼠标悬停时更改推按钮的颜色。

QPushButton:hover { background-color: #66c011; }
  1. 上述样式表将在鼠标悬停时将推按钮的背景颜色更改为绿色。我们将在下一节中详细讨论这个问题。

  2. 您可以进一步调整小部件的大小和边距,使它们看起来更好。记得通过删除我们之前直接应用到登录表单的样式表来删除登录表单的边框线。

  3. 现在您的登录屏幕应该看起来像这样:如何做…

它是如何工作的…

这个示例更多地关注 Qt 的布局系统。Qt 布局系统提供了一种简单而强大的方式,自动安排小部件在一个小部件内,以确保它们充分利用可用的空间。

在前面的示例中使用的间隔项有助于推动布局中包含的小部件向外推动,以创建间距。要将小部件定位到布局的中间,请将两个间隔项放到布局中,一个放在小部件的左侧,另一个放在小部件的右侧。然后,这两个间隔器将把小部件推到布局的中间。

在样式表中使用资源

Qt 为我们提供了一个平台无关的资源系统,允许我们将任何类型的文件存储在程序的可执行文件中以供以后使用。我们可以在可执行文件中存储任何类型的文件,如图像、音频、视频、HTML、XML、文本文件、二进制文件等。如果您的应用程序始终需要一定的文件集(图标、翻译文件等),并且您不希望丢失这些文件,这将非常有用。为了实现这一点,我们必须告诉 Qt 我们想要将哪些文件添加到其资源系统中的.qrc文件,并且 Qt 将在构建过程中处理其余部分。

如何做

要向项目添加新的.qrc文件,请转到文件 | 新建文件或项目。然后,在文件和类别类别下选择Qt,然后选择Qt 资源文件。之后,给它取一个名字(即resources),然后单击下一步,接着单击完成.qrc文件将被创建并由 Qt Creator 自动打开。

您不必直接在 XML 格式中编辑.qrc文件,因为 Qt Creator 为您提供了用户界面来管理资源。要向项目添加图像和图标,首先需要确保图像和图标被放置在项目的目录中。

在 Qt Creator 中打开.qrc文件后,单击添加按钮,然后单击添加前缀按钮。前缀用于对资源进行分类,以便在项目中有大量资源时更好地进行管理:

  1. 重命名您刚创建的前缀为/icons

  2. 然后,通过单击添加,然后单击添加前缀来创建另一个前缀。

  3. 重命名新的前缀为/images

  4. 之后,选择/icon前缀,然后单击添加,接着单击添加文件

  5. 将出现文件选择窗口;使用它来选择所有图标文件。您可以通过在键盘上按住Ctrl键并单击文件来选择多个文件。完成后,单击打开

  6. 然后,选择/images前缀,然后单击添加按钮,接着单击添加文件按钮。文件选择窗口将再次弹出,这次我们将选择背景图像。

  7. 重复上述步骤,但这次我们将把徽标图像添加到/images前缀。

完成后不要忘记按Ctrl + S保存。您的.qrc文件现在应该是这样的:

如何做

  1. 之后,打开我们的mainwindow.ui文件;我们现在将使用刚刚添加到项目中的资源。首先,我们将选择位于顶部面板上的重新启动按钮。然后,向下滚动属性编辑器,直到看到icon属性。单击带有下拉箭头图标的小按钮,然后从其菜单中单击选择资源

  2. 然后将弹出选择资源窗口。在左侧面板上单击icons前缀,然后在右侧面板上选择重新启动图标。之后,按确定

  3. 现在您会看到一个小图标出现在按钮上。图标看起来非常小,因为默认图标尺寸设置为16x16。将iconSize属性更改为50x50,您会看到图标现在变大了。

对于关闭按钮,重复上述步骤,只是这次我们将选择关闭图标。

  1. 完成后,两个按钮现在应该看起来像这样:如何做

  2. 接下来,我们将使用添加到资源文件中的图像作为我们的标志。首先,选择标志小部件,并删除我们先前添加的样式表,以渲染其轮廓。

  3. 向下滚动属性编辑器,直到看到pixmap属性。

  4. 单击pixmap属性后面的小下拉按钮,并从菜单中选择选择资源。之后,选择标志图像并单击确定。现在,您会看到标志的大小不再遵循您先前设置的尺寸,而是遵循图像的实际尺寸。我们无法更改其尺寸,因为这就是pixmap的工作原理。

  5. 如果您想对标志的尺寸有更多控制,可以从pixmap属性中删除图像,并改用样式表。您可以使用以下代码将图像应用到图标容器:

border-image: url(:/images/logo.png);
  1. 要获取图像的路径,请右键单击文件列表窗口上的图像名称,然后选择复制路径。路径将保存到您的操作系统剪贴板中,现在您可以将其粘贴到前面的样式表中。使用这种方法将确保图像完全适合您应用样式的小部件的尺寸。您的标志现在应该看起来像这样:如何做

  2. 最后,我们将使用样式表将壁纸图像应用到背景上。由于背景尺寸会根据窗口大小而改变,所以在这种情况下我们不能使用pixmap。相反,我们将使用样式表中的border-image属性来实现这一点。右键单击主窗口,选择更改样式表以打开编辑样式表窗口。我们将在中央小部件的样式表下添加一行新的样式表:

#centralWidget
{
  background: rgba(32, 80, 96, 100);
 border-image: url(:/images/login_bg.png);
}
  1. 这真的很简单和容易!您的登录界面现在应该看起来像这样:如何做

它的工作原理…

Qt 中的资源系统将二进制文件(如图像、翻译文件等)存储在编译后的可执行文件中。它读取项目中的资源集合文件(.qrc)来定位需要存储在可执行文件中的文件,并将它们包含到构建过程中。.qrc文件看起来像这样:

<!DOCTYPE RCC><RCC version="1.0">
  <qresource>
    <file>images/copy.png</file>
    <file>images/cut.png</file>
    <file>images/new.png</file>
    <file>images/open.png</file>
    <file>images/paste.png</file>
    <file>images/save.png</file>
  </qresource>
</RCC>

它使用 XML 格式存储资源文件的路径,这些路径是相对于包含它的目录的。请注意,列出的资源文件必须位于与.qrc文件相同的目录中,或者其子目录之一。

自定义属性和子控件

Qt 的样式表系统使我们能够轻松创建令人惊叹和专业的 UI。在这个例子中,我们将学习如何为我们的小部件设置自定义属性,并使用它们在不同样式之间切换。

如何做…

  1. 让我们尝试一下上述段落中描述的情景,创建一个新的 Qt 项目。我已经为此准备了 UI。UI 在左侧包含三个按钮,在右侧有一个包含三个页面的选项卡小部件,如下截图所示:如何做…

  2. 三个按钮是蓝色的,因为我已将以下样式表添加到主窗口(而不是单独的按钮):

QPushButton
{
  color: white;
  background-color: #27a9e3;
  border-width: 0px;
  border-radius: 3px;
}
  1. 接下来,我将通过向主窗口添加以下样式表来向您解释 Qt 中的伪状态,您可能已经熟悉:
QPushButton:hover
{
  color: white;
  background-color: #66c011;
  border-width: 0px;
  border-radius: 3px;
}
  1. 我们在上一个教程中使用了前面的样式表,使按钮在鼠标悬停时更改颜色。这是由 Qt 样式表的伪状态实现的,在这种情况下,是单词hoverQPushButton类之间用冒号分隔。每个小部件都有一组通用伪状态,例如activedisabledenabled等,还有一组适用于其小部件类型的伪状态。例如,QPushButton可用openflat等状态,但QLineEdit不行。让我们添加pressed伪状态以在用户单击时将按钮的颜色更改为黄色:
QPushButton:pressed
{
  color: white;
  background-color: yellow;
  border-width: 0px;
  border-radius: 3px;
}
  1. 伪状态允许用户根据适用于它的条件加载不同的样式表。Qt 通过在 Qt 样式表中实现动态属性进一步推动了这一概念。这使我们能够在满足自定义条件时更改小部件的样式表。我们可以利用此功能根据 Qt 中的自定义属性设置来更改按钮的样式表。

首先,我们将向我们的主窗口添加此样式表:

QPushButton[pagematches=true]
{
  color: white;
  background-color: red;
  border-width: 0px;
  border-radius: 3px;
}
  1. 它的基本作用是,如果名为pagematches的属性返回true,则将推按钮的背景颜色更改为红色。显然,QPushButton类中不存在此属性。但是,我们可以通过使用QObject::setProperty()将其添加到我们的按钮中:
  • 在您的MainWindow.cpp源代码中,在ui->setupUi(this)之后添加以下代码:
ui->button1->setProperty("pagematches", true);
  • 前面的代码将向第一个按钮添加一个名为pagematches的自定义属性,并将其值设置为true。这将使第一个按钮默认变为红色。

  • 然后,在选项卡小部件上右键单击,选择转到槽。然后会弹出一个窗口;从列表中选择currentChanged(int)选项,然后单击确定。Qt 将为您生成一个slot函数,看起来像这样:

private slots:
void on_tabWidget_currentChanged(int index);
  • 每当我们更改选项卡小部件的页面时,将调用slot函数。然后,我们可以通过将代码添加到slot函数中来决定我们希望它执行的操作。要做到这一点,请打开mainwindow.cpp,您将在那里看到函数的声明。让我们向函数添加一些代码:
void MainWindow::on_tabWidget_currentChanged(int index)
{
  // Set all buttons to false
  ui->button1->setProperty("pagematches", false);
  ui->button2->setProperty("pagematches", false);
  ui->button3->setProperty("pagematches", false);

  // Set one of the buttons to true
  if (index == 0)
    ui->button1->setProperty("pagematches", true);
  else if (index == 1)
    ui->button2->setProperty("pagematches", true);
  else
    ui->button3->setProperty("pagematches", true);

  // Update buttons style
  ui->button1->style()->polish(ui->button1);
  ui->button2->style()->polish(ui->button2);
  ui->button3->style()->polish(ui->button3);
}
  1. 前面的代码基本上是这样的:当选项卡小部件切换到当前页面时,它将所有三个按钮的pagematches属性设置为false。在我们决定哪个按钮应该变为红色之前,请确保重置所有内容。

  2. 然后,检查事件信号提供的index变量,它将告诉您当前页面的索引号。根据索引号将一个按钮的pagematches属性设置为true

  3. 最后,通过调用polish()来刷新所有三个按钮的样式。

然后,构建并运行项目。现在,每当您将选项卡小部件切换到不同页面时,您应该会看到三个按钮的颜色变为红色。此外,当鼠标悬停时,按钮将变为绿色,当您单击它们时,它们的颜色将变为黄色:

如何做…

它是如何工作的…

Qt 为用户提供了向任何类型的小部件添加自定义属性的自由。如果您想在满足特殊条件时更改特定小部件,而 Qt 默认情况下不提供这样的上下文,那么自定义属性非常有用。这使用户能够扩展 Qt 的可用性,并使其成为定制解决方案的灵活工具。

例如,如果我们在主窗口上有一排按钮,并且我们需要其中一个根据选项卡小部件当前显示的页面而更改其颜色,则按钮不会知道它们何时应更改其颜色,因为 Qt 本身没有针对这种情况的内置上下文。为了解决这个问题,Qt 为我们提供了一种方法来向小部件添加自己的属性,即使用一个名为QObject::setProperty()的通用函数。要读取自定义属性,我们可以使用另一个名为QObject::property()的函数。

接下来,我们将讨论 Qt 样式表中的子控件。通过观察子控件这个术语,实际上是相当不言自明的。通常,一个小部件不仅仅是一个单一的对象,而是由多个对象或控件组合而成,以形成一个更复杂的小部件,这些对象被称为子控件。

例如,一个微调框小部件包含一个输入字段、一个向下按钮、一个向上按钮、一个向上箭头和一个向下箭头,与其他一些小部件相比相当复杂。在这种情况下,Qt 允许我们通过样式表更灵活地改变每个子控件,如果我们想的话。我们可以通过在小部件类名后面指定子控件的名称,用双冒号分隔来实现。例如,如果我想改变微调框中向下按钮的图像,我可以这样写我的样式表:

QSpinBox::down-button
{
  image: url(:/images/spindown.png);
  subcontrol-origin: padding;
  subcontrol-position: right bottom;
}

这将只将图像应用于我的微调框的向下按钮,而不是小部件的任何其他部分。

通过结合自定义属性、伪状态和子控件,Qt 为我们提供了一种非常灵活的方法来自定义我们的用户界面。

注意

访问以下链接了解更多关于 Qt 中伪状态和子控件的信息:

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

在 QML 中进行样式设置

Qt Meta LanguageQt Modeling LanguageQML)是 Qt 使用的一种类似于 Javascript 的用户界面标记语言,用于设计用户界面。Qt 为您提供了 Qt Quick 组件(由 QML 技术提供支持的小部件),可以轻松设计触摸友好的 UI,无需 C++编程。我们将通过按照以下部分给出的步骤来学习如何使用 QML 和 Qt Quick 组件来设计我们程序的 UI。

如何做…

  1. 通过转到文件|新建文件或项目来创建一个新项目。在项目类别下选择应用程序,然后选择Qt Quick 应用程序

  2. 点击选择按钮,这将带您到下一个窗口。输入项目名称,然后再次点击下一步按钮。

  3. 现在将出现另一个窗口,询问您选择所需的最低 Qt 版本。选择计算机上安装的最新版本,然后点击下一步

  4. 之后,再次点击下一步,然后点击完成。Qt Creator 现在将为您创建一个新项目。

  5. 项目创建后,您会发现与 C++ Qt 项目相比有一些不同。您会在项目资源中看到两个.qml文件,分别是main.qmlMainForm.ui.qml。这两个文件是使用 QML 标记语言的 UI 描述文件。如果您双击main.qml文件,Qt Creator 将打开脚本编辑器,您会看到类似于这样的内容:

import QtQuick 2.5
import QtQuick.Window 2.2

Window {
  visible: true
  MainForm {
    anchors.fill: parent
    mouseArea.onClicked: {
      Qt.quit();
    }
  }
}
  1. 这个文件基本上告诉 Qt 创建一个窗口,并插入一个名为MainForm的 UI 集,实际上是来自另一个名为MainForm.ui.qml.qml文件。它还告诉 Qt,当用户点击mouseArea小部件时,整个程序应该被终止。

  2. 现在,尝试双击打开MainForm.ui.qml文件。这次,Qt Designer(UI 编辑器)将被打开,您会看到一个与我们之前做的 C++项目完全不同的 UI 编辑器。这个编辑器也被称为 Qt Quick Designer,专门用于编辑基于 QML 的 UI。

  3. 如果你在项目中打开main.cpp文件,你会看到这行代码:

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
  1. 前面的代码基本上告诉 Qt 的 QML 引擎在程序启动时加载main.qml文件。如果你想加载其他.qml文件而不是main.qml,你知道在哪里找代码了。

  2. main.qml被 QML 引擎加载时,它还将MainForm.ui.qml导入到 UI 中,因为MainFormmain.qml文件中被调用。Qt 将通过搜索其.qml文件来检查MainForm是否是有效的 UI,这是基于命名约定的。基本上,这个概念类似于我们在上一节中做的 C++项目,其中main.qml文件就像main.cpp文件,MainForm.ui.qml就像MainWindow类。您还可以创建其他 UI 模板并在main.qml中使用它们。希望这种比较能让您更容易理解 QML 的工作原理。

  3. 现在让我们打开MainForm.ui.qml。您应该在导航窗口上看到列出了三个项目:RectanglemouseAreaText。当 QML 引擎解释这些项目时,它会在画布上产生以下结果:操作步骤…

  4. Rectangle项目基本上是窗口的基本布局,无法删除。它类似于我们在上一节中使用的centralWidgetmouseArea项目是一个无形项目,当鼠标点击它或手指触摸它(适用于移动平台)时会触发它。鼠标区域也用于按钮组件,我们一会儿会用到。Text组件是不言自明的:它是一个在应用程序上显示文本块的标签。

  5. 导航窗口上,我们可以通过单击类似于眼睛图标的项目旁边的图标来隐藏或显示项目。当项目被隐藏时,它将不会显示在画布上,也不会显示在编译后的应用程序中。就像 C++ Qt 项目中的小部件一样,Qt Quick 组件是根据父子关系的层次结构排列的。所有子项目将放置在具有缩进位置的父项目下方。在我们的情况下,您可以看到mouseAreaText项目都相对于Rectangle项目略微向右放置,因为它们都是Rectangle项目的子项目。我们可以通过从导航窗口使用单击和拖动的方法重新排列父子关系以及它们在层次结构中的位置。您可以尝试单击Text项目并将其拖动到mouseArea上方。然后,您将看到Text项目改变了位置,现在位于mouseArea下方,并且缩进更宽:操作步骤…

  6. 我们还可以通过使用位于导航窗口顶部的箭头按钮来重新排列它们,如前面的屏幕截图所示。发生在父项上的任何事情也会影响到其所有子项,例如移动父项、隐藏和显示父项等。

提示

您可以通过按住鼠标中键(或鼠标滚轮)并移动鼠标来在画布视图中移动。您还可以在按住键盘上的Ctrl键的同时滚动鼠标来放大和缩小。默认情况下,滚动鼠标会上下移动画布视图。但是,如果您的鼠标光标位于画布的水平滚动条上方,滚动鼠标将使视图向左和向右移动。

  1. 接下来,删除mouseAreaText项目,因为我们将学习如何使用 QML 和 Qt Quick 从头开始创建用户界面。

  2. 完成后,让我们将Rectangle项目的大小设置为800x600,因为我们将需要更大的空间来放置小部件。

  3. 打开main.qml并删除以下代码:

mouseArea.onClicked: {
  Qt.quit();
}

这是因为mouseArea项目不再存在,当编译时会导致错误。

  1. 之后,从MainForm.ui.qml中删除以下代码:
property alias mouseArea: mousearea
  1. 由于mouseArea项目不再存在,因此出于相同的原因,此内容已被删除。

  2. 然后,将我们在之前的 C++项目中使用的图像复制到 QML 项目的文件夹中,因为我们要使用 QML 重新创建相同的登录界面!

  3. 将图像添加到资源文件中,以便我们可以在 UI 中使用它们。

  4. 完成后,再次打开 Qt Quick Designer,并切换到资源窗口。直接点击并拖动背景图像到画布上。然后,切换到属性窗格上的布局选项卡,并点击红色圆圈标记的填充锚点按钮。这将使背景图像始终固定在窗口大小上:操作步骤…

  5. 接下来,从库窗口中点击并拖动一个矩形组件到画布上。我们将使用这个作为程序的顶部面板。

  6. 对于顶部面板,启用顶部锚点、左锚点和右锚点,使其固定在窗口顶部并跟随其宽度。确保所有边距都设置为零。

  7. 然后,转到顶部面板的Color属性,并选择Gradient模式。将第一种颜色设置为#805bcce9,第二种颜色设置为#80000000。这将创建一个半透明的面板,带有蓝色的渐变。

  8. 之后,将一个文本小部件添加到画布上,并将其设置为顶部面板的子级。将其文本属性设置为当前日期和时间(例如,星期一,2015 年 10 月 26 日下午 3:14),以供显示目的。然后,将文本颜色设置为白色。

  9. 切换到布局选项卡,启用顶部锚点和左锚点,以便文本小部件始终固定在屏幕的左上角。

  10. 接下来,在屏幕上添加一个鼠标区域,并将其大小设置为50x50。然后,通过将其拖动到导航窗口中的顶部面板上,使其成为顶部面板的子级。

  11. 将鼠标区域的颜色设置为蓝色(#27a9e3),并将其半径设置为2,使其角落略微圆润。然后,启用顶部锚点和右锚点,使其固定在窗口的右上角。将顶部锚点的边距设置为8,右锚点的边距设置为10,以留出一些空间。

  12. 之后,打开资源窗口,并将关闭图标拖动到画布上。然后,将其设置为我们刚刚创建的鼠标区域项的子级。然后,启用填充锚点,使其适应鼠标区域的大小。

  13. 哦,这是很多步骤!现在你的项目应该在导航窗口上排列如下:操作步骤…

  14. 当主窗口改变大小时,父子关系和布局锚点都非常重要,以保持小部件在正确的位置。

  15. 此时,你的顶部面板应该看起来像这样:操作步骤…

  16. 接下来,我们将开始处理登录表单。首先,通过从窗口中拖动矩形到画布上,添加一个新的矩形。将矩形的大小调整为360x200,并将其半径设置为15

  17. 然后,将其颜色设置为#80000000,这将使其变为黑色,透明度为50%

  18. 之后,启用垂直中心锚点和水平中心锚点,使其始终与窗口中心对齐。然后,将垂直中心锚点的边距设置为100,使其稍微向下移动到底部,为标志留出空间。以下截图展示了锚点的设置:操作步骤…

  19. 将文本小部件添加到画布上。将它们设置为登录表单(矩形小部件)的子级,并将它们的文本属性分别设置为用户名:密码:。然后,将它们的文本颜色设置为白色,并相应地定位它们。这次我们不需要设置边距,因为它们将跟随矩形的位置。

  20. 然后,在画布上添加两个文本输入小部件,并将它们放置在我们刚刚创建的文本小部件旁边。确保文本输入也是登录表单的子级。由于文本输入不包含任何背景颜色属性,我们需要在画布上添加两个矩形作为它们的背景。

  21. 在画布上添加两个矩形,并将它们分别设置为我们刚刚创建的文本输入的子级。然后,将半径属性设置为5,使它们具有一些圆角。之后,在两个矩形上启用填充锚点,以便它们将跟随文本输入小部件的大小。

  22. 之后,我们将在密码字段下方创建登录按钮。首先,在画布上添加一个鼠标区域,并将其设置为登录表单的子级。然后,将其调整为所需的尺寸并移动到指定位置。

  23. 由于鼠标区域也不包含任何背景颜色属性,我们需要添加一个矩形小部件,并将其设置为鼠标区域的子级。将矩形的颜色设置为蓝色(#27a9e3),并启用填充锚点,使其与鼠标区域完美匹配。

  24. 接下来,在画布上添加一个文本小部件,并将其设置为登录按钮的子级。将其文本颜色设置为白色,并将其文本属性设置为登录。最后,启用水平居中锚点和垂直居中锚点,使其与按钮居中对齐。

  25. 现在,您将获得一个看起来与我们在 C++项目中制作的登录表单非常相似的登录表单:如何做...

  26. 完成登录表单后,是时候添加标志了。实际上非常简单。首先,打开资源窗口,然后将标志图像拖放到画布中。

  27. 将其设置为登录表单的子级,并将其大小设置为512x200

  28. 将其定位在登录表单上方,然后完成!

  29. 这是编译后整个 UI 的样子。我们已经成功地用 QML 和 Qt Quick 重新创建了 C++项目中的登录界面!如何做...

它是如何工作的...

Qt Quick 编辑器在将小部件放置在应用程序中的方法上与表单编辑器有很大不同。用户可以自行决定哪种方法最适合他/她。

以下截图显示了 Qt Quick Designer 的外观:

它是如何工作的...

现在我们将看一下编辑器 UI 的各个元素:

  1. 导航器导航器窗口以树形结构显示当前 QML 文件中的项目。它类似于我们在上一节中使用的其他 Qt Designer 中的对象操作器窗口。

  2. 窗口显示所有可用的 QML 组件或 Qt Quick 控件。您可以单击并将其拖放到画布窗口中以添加到您的 UI 中。您还可以创建自定义的 QML 组件并在此处显示。

  3. 资源资源窗口以列表形式显示所有资源,然后可以在 UI 设计中使用。

  4. 导入导入窗口允许您将不同的 QML 模块导入到当前的 QML 文件中,例如蓝牙模块、webkit 模块、定位模块等,以为您的 QML 项目添加额外的功能。

  5. 状态窗格:状态窗格显示 QML 项目中的不同状态,通常描述 UI 配置,例如 UI 控件、其属性和行为以及可用的操作。

  6. 属性窗格:与我们在上一节中使用的属性编辑器类似,QML Designer 中的属性窗格显示所选项目的属性。您也可以在代码编辑器中更改项目的属性。

  7. 画布:画布是您创建 QML 组件和设计应用程序的工作区。

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

有时,我们希望通过 C++脚本修改 QML 对象的属性,例如更改标签的文本、隐藏/显示小部件、更改其大小等。Qt 的 QML 引擎允许您将 QML 对象注册为 C++类型,从而自动公开其所有属性。

如何做...

我们想在 QML 中创建一个标签,并偶尔更改其文本。为了将标签对象暴露给 C++,我们可以执行以下步骤。首先,创建一个名为MyLabel的 C++类,它继承自QObject类:

mylabel.h:
class MyLabel : public QObject
{
  Q_OBJECT
  public:
    // Object pointer
    QObject* myObject;

    explicit MyLabel(QObject *parent = 0);

  // Must call Q_INVOKABLE so that this function can be used in QML
  Q_INVOKABLE void SetMyObject(QObject* obj);
}

mylabel.cpp源文件中,定义一个名为SetMyObject()的函数来保存对象指针。稍后将在 QML 中调用此函数:

mylabel.cpp:
void MyLabel::SetMyObject(QObject* obj)
{
  // Set the object pointer
  myObject = obj;
}

之后,在main.cpp中,包括MyLabel头文件,并使用qmlRegisterType()函数将其注册到 QML 引擎中:

#include "mylabel.h"
int main(int argc, char *argv[])
{
  // Register your class to QML
  qmlRegisterType<MyClass>("MyLabelLib", 1, 0, "MyLabel");
}

请注意,在qmlRegisterType()中需要声明四个参数。除了声明您的类名(MyLabel)之外,还需要声明您的库名称(MyLabelLib)和其版本(1.0),这将用于以后将您的类导入到 QML 中。

现在 QML 引擎已经完全了解我们的自定义标签类,我们可以将其映射到 QML 中的标签对象,并通过在我们的 QML 文件中调用import MyLabelLib 1.0来导入我们之前定义的类库。请注意,库名称及其版本号必须与您在main.cpp中声明的相匹配,否则将会抛出错误。

在 QML 中声明MyLabel并将其 ID 设置为mylabels后,立即调用mylabel.SetMyObject(myLabel)将其指针暴露给 C/C++,在标签初始化后:

import MyLabelLib 1.0

ApplicationWindow
{
  id: mainWindow
  width: 480
  height: 640

  MyLabel
  {
    id: mylabel
  }

  Label
  {
    id: helloWorldLabel
    text: qsTr("Hello World!")
    Component.onCompleted:
    {
      mylabel.SetMyObject(hellowWorldLabel);
    }
  }
}

请注意,您需要等待标签完全初始化后,才能将其指针暴露给 C/C++,否则可能会导致程序崩溃。为了确保它完全初始化,调用SetMyObject()Component.onCompleted中,而不是其他任何地方。

现在 QML 标签已经暴露给 C/C++,我们可以通过调用setProperty()函数来更改其任何属性。例如,我们可以将其可见性设置为true,并将其文本更改为再见世界!

// QVariant automatically detects your data type
myObject->setProperty("visible", QVariant(true));
myObject->setProperty("text", QVariant("Bye bye world!"));

除了更改属性,我们还可以通过调用QMetaObject::invokeMethod()来调用其函数:

QVariant returnedValue;
QVariant message = "Hello world!";

QMetaObject::invokeMethod(myObject, "myQMLFunction",
Q_RETURN_ARG(QVariant, returnedValue),
Q_ARG(QVariant, message));

qDebug() << "QML function returned:" << returnedValue.toString();

或者简单地,如果我们不希望从中返回任何值,我们可以只使用两个参数调用invokedMethod()函数:

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

它是如何工作的...

QML 旨在通过 C++代码轻松扩展。Qt QML 模块中的类使得可以从 C++加载和操作 QML 对象,而 QML 引擎与 Qt 的元对象系统的集成性质使得可以直接从 QML 调用 C++功能。要将一些 C++数据或功能提供给 QML,必须从 QObject 派生类中提供。

QML 对象类型可以从 C++中实例化和检查,以便访问它们的属性,调用它们的方法,并接收它们的信号通知。这是可能的,因为所有 QML 对象类型都是使用 QObject 派生类实现的,使得 QML 引擎能够通过 Qt 元对象系统动态加载和内省对象。

第二章:状态和动画

在本章中,我们将涵盖以下内容:

  • Qt 中的属性动画

  • 使用缓动曲线控制属性动画

  • 创建动画组

  • 创建嵌套动画组

  • Qt 中的状态机

  • QML 中的状态、转换和动画

  • 使用动画器动画小部件属性

  • 精灵动画

介绍

Qt 提供了一种简单的方法来为继承QObject类的小部件或其他对象进行动画处理,通过其强大的动画框架。动画可以单独使用,也可以与状态机框架一起使用,该框架允许根据小部件的当前活动状态播放不同的动画。Qt 的动画框架还支持分组动画,允许您同时移动多个图形项,或者按顺序移动它们。

Qt 中的属性动画

在这个例子中,我们将学习如何使用 Qt 的属性动画类来为我们的图形用户界面GUI)元素添加动画效果,这是其强大的动画框架的一部分,它允许我们以最小的努力创建流畅的动画效果。

如何做到…

  1. 首先,让我们创建一个新的 Qt 小部件应用程序项目。之后,用 Qt Designer 打开mainwindow.ui并在主窗口上放置一个按钮,如下所示:如何做到…

  2. 接下来,打开mainwindow.cpp并在源代码开头添加以下行代码:

#include <QPropertyAnimation>
  1. 之后,打开mainwindow.cpp并在构造函数中添加以下代码:
QPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, "geometry");
animation->setDuration(10000);
animation->setStartValue(ui->pushButton->geometry());
animation->setEndValue(QRect(200, 200, 100, 50));
animation->start();

它是如何工作的...

通过 Qt 提供的属性动画类QPropertyAnimation类是通过 Qt 提供的一种常见方法来为 GUI 元素添加动画效果。这个类是动画框架的一部分,它利用 Qt 的定时器系统在给定的持续时间内更改 GUI 元素的属性。

我们在这里要做的是将按钮从一个位置动画到另一个位置,同时也随着动画过程放大按钮的大小。

通过在步骤 2 中在源代码中包含QPropertyAnimation头文件,我们将能够访问 Qt 提供的QPropertyAnimation类并利用其功能。

步骤 3 中的代码基本上创建了一个新的属性动画,并将其应用于我们刚刚在 Qt Designer 中创建的按钮。我们明确要求属性动画类更改按钮的geometry属性,并将其持续时间设置为 3,000 毫秒(3 秒)。

然后,动画的起始值设置为按钮的初始几何形状,因为显然我们希望它从我们最初在 Qt Designer 中放置按钮的位置开始。然后,结束值设置为我们希望它变成的值;在这种情况下,我们将按钮移动到 x:200,y:200的新位置,同时沿途改变其大小为宽度:100,高度:50

之后,调用animation->start()来启动动画。

编译并运行项目,现在您应该看到按钮开始在主窗口上缓慢移动,同时逐渐扩大大小,直到达到目的地。您可以通过修改前面代码中的值来更改动画持续时间和目标位置和比例。使用 Qt 的属性动画系统来为 GUI 元素添加动画效果真的是如此简单!

还有更多...

Qt 为我们提供了几种不同的子系统来为我们的 GUI 创建动画,包括定时器、时间轴、动画框架、状态机框架和图形视图框架:

  • 定时器:Qt 为我们提供了重复和单次定时器。当达到超时值时,将通过 Qt 的信号和槽机制触发事件回调函数。您可以利用定时器在给定的时间间隔内更改 GUI 元素的属性(颜色、位置、比例等),以创建动画效果。

  • 时间轴:时间轴定期调用插槽以对 GUI 元素进行动画处理。它与重复定时器非常相似,但是当触发插槽时,时间轴会向插槽提供一个值来指示当前帧索引,以便您可以根据给定的值执行不同的操作(例如偏移到精灵表的不同位置)。

  • 动画框架:动画框架通过允许对 GUI 元素的属性进行动画处理,使动画变得简单。动画是通过使用缓动曲线来控制的。缓动曲线描述了控制动画速度的函数,从而产生不同的加速和减速模式。Qt 支持的缓动曲线类型包括:线性、二次、三次、四次、正弦、指数、圆形和弹性。

  • 状态机框架:Qt 为我们提供了用于创建和执行状态图的类,允许每个 GUI 元素在触发信号时从一个状态移动到另一个状态。状态机框架中的状态图是分层的,这意味着每个状态也可以嵌套在其他状态内部。

  • 图形视图框架:图形视图框架是一个强大的图形引擎,用于可视化和与大量自定义的 2D 图形项进行交互。如果您是一名经验丰富的程序员,您可以使用图形视图框架手动绘制 GUI,并对其进行动画处理。

通过利用这里提到的所有强大功能,我们能够轻松创建直观现代的 GUI。在本章中,我们将探讨使用 Qt 对 GUI 元素进行动画处理的实际方法。

使用缓动曲线控制属性动画

在这个示例中,我们将学习如何通过利用缓动曲线使我们的动画更加有趣。我们仍然会使用先前的源代码,该源代码使用属性动画来对推按钮进行动画处理。

如何做...

  1. 在调用start()函数之前,定义一个缓动曲线并将其添加到属性动画中:
QPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, "geometry");
animation->setDuration(3000);
animation->setStartValue(ui->pushButton->geometry());
animation->setEndValue(QRect(200, 200, 100, 50));
QEasingCurve curve;
curve.setType(QEasingCurve::OutBounce);
animation->setEasingCurve(curve);
animation->start();
  1. 调用setLoopCount()函数来设置要重复多少次循环:
QPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, "geometry");
animation->setDuration(3000);
animation->setStartValue(ui->pushButton->geometry());
animation->setEndValue(QRect(200, 200, 100, 50));
QEasingCurve curve;
Curve.setType(EasingCurve::OutBounce);
animation->setEasingCurve(curve);
animation->setLoopCount(2);
animation->start();
  1. 在应用缓动曲线到动画之前,调用setAmplitude()setOvershoot()setPeriod()
QEasingCurve curve;
curve.setType(QEasingCurve::OutBounce);
curve.setAmplitude(1.00);
curve.setOvershoot(1.70);
curve.setPeriod(0.30);
animation->setEasingCurve(curve);
animation->start();

它是如何工作的...

为了让缓动曲线控制动画,您只需要在调用start()函数之前定义一个缓动曲线并将其添加到属性动画中。您还可以尝试几种其他类型的缓动曲线,看看哪一种最适合您。以下是一个示例:

animation->setEasingCurve(QEasingCurve::OutBounce);

如果您希望动画在播放完成后循环播放,可以调用setLoopCount()函数来设置要重复多少次循环,或者将值设置为-1以进行无限循环:

animation->setLoopCount(-1);

在将缓动曲线应用到属性动画之前,您可以设置几个参数来完善缓动曲线。这些参数包括振幅、超调和周期:

  • 振幅:振幅越高,动画的弹跳或弹簧效果就越强。

  • 超调:由于阻尼效应,某些曲线函数将产生超调(超过其最终值)曲线。通过调整超调值,我们能够增加或减少这种效果。

  • 周期:设置较小的周期值将使曲线具有较高的频率。较大的周期将使其具有较低的频率。

然而,这些参数并不适用于所有曲线类型。请参考 Qt 文档,了解哪个参数适用于哪种曲线类型。

还有更多...

虽然属性动画运行良好,但有时看到 GUI 元素以恒定速度进行动画处理会感到有些无聊。我们可以通过添加缓动曲线来控制运动使动画看起来更有趣。在 Qt 中有许多类型的缓动曲线可供使用,以下是其中一些:

还有更多...

正如您从上图中看到的,每个缓动曲线产生不同的缓入和缓出效果。

注意

有关 Qt 中可用的缓动曲线的完整列表,请参阅 Qt 文档doc.qt.io/qt-5/qeasingcurve.html#Type-enum

创建动画组

在这个例子中,我们将学习如何使用动画组来管理组中包含的动画的状态。

操作方法...

  1. 我们将使用之前的例子,但这次,我们将在主窗口中添加两个更多的按钮,如下所示:操作方法...

  2. 接下来,在主窗口的构造函数中为每个按钮定义动画:

QPropertyAnimation *animation1 = new QPropertyAnimation(ui->pushButton, "geometry");
animation1->setDuration(3000);
animation1->setStartValue(ui->pushButton->geometry());
animation1->setEndValue(QRect(50, 200, 100, 50));

QPropertyAnimation *animation2 = new QPropertyAnimation(ui->pushButton_2, "geometry");
animation2->setDuration(3000);
animation2->setStartValue(ui->pushButton_2->geometry());
animation2->setEndValue(QRect(150, 200, 100, 50));

QPropertyAnimation *animation3 = new QPropertyAnimation(ui->pushButton_3, "geometry");
animation3->setDuration(3000);
animation3->setStartValue(ui->pushButton_3->geometry());
animation3->setEndValue(QRect(250, 200, 100, 50));
  1. 之后,创建一个缓动曲线并将相同的曲线应用于所有三个动画:
QEasingCurve curve;
curve.setType(QEasingCurve::OutBounce);
curve.setAmplitude(1.00);
curve.setOvershoot(1.70);
curve.setPeriod(0.30);

animation1->setEasingCurve(curve);
animation2->setEasingCurve(curve);
animation3->setEasingCurve(curve);
  1. 一旦您将缓动曲线应用于所有三个动画,我们将创建一个动画组并将所有三个动画添加到组中:
QParallelAnimationGroup *group = new QParallelAnimationGroup;group->addAnimation(animation1);
group->addAnimation(animation2);
group->addAnimation(animation3);
  1. 从刚刚创建的动画组中调用start()函数:
group->start();

工作原理...

由于我们现在使用动画组,我们不再从单独的动画中调用start()函数,而是从刚刚创建的动画组中调用start()函数。

如果现在编译并运行示例,您将看到所有三个按钮同时播放。这是因为我们使用了并行动画组。您可以将其替换为顺序动画组并再次运行示例:

QSequentialAnimationGroup *group = new QSequentialAnimationGroup;

这次,一次只有一个按钮会播放其动画,而其他按钮将耐心等待他们的轮到。

优先级是根据首先添加到动画组中的动画来设置的。您可以通过简单地重新排列添加到组中的动画的顺序来更改动画顺序。例如,如果我们希望按钮 3 首先开始动画,然后是按钮 2,然后是按钮 1,代码将如下所示:

group->addAnimation(animation3);
group->addAnimation(animation2);
group->addAnimation(animation1);

由于属性动画和动画组都是从QAbstractAnimator类继承的,这意味着您也可以将一个动画组添加到另一个动画组中,以形成一个更复杂的嵌套动画组。

还有更多...

Qt 允许我们创建多个动画并将它们分组成一个动画组。一个组通常负责管理其动画的状态(即,它决定何时开始、停止、恢复和暂停它们)。目前,Qt 提供了两种动画组类,QParallelAnimationGroupQSequentialAnimationGroup

  • QParallelAnimationGroup:顾名思义,并行动画组同时运行其组中的所有动画。当最持久的动画完成运行时,组被视为已完成。

  • QSequentialAnimationGroup:顺序动画组按顺序运行其动画,这意味着一次只运行一个动画,并且只有当前动画完成后才会播放下一个动画。

创建嵌套动画组

使用嵌套动画组的一个很好的例子是当您有几个并行动画组并且希望按顺序播放这些组时。

操作方法...

  1. 我们将使用之前的示例中的 UI,并在主窗口中添加几个更多的按钮,如下所示:操作方法...

  2. 首先,为所有按钮创建动画,然后创建一个缓动曲线并将其应用于所有动画:

QPropertyAnimation *animation1 = new QPropertyAnimation(ui->pushButton, "geometry");
animation1->setDuration(3000);
animation1->setStartValue(ui->pushButton->geometry());
animation1->setEndValue(QRect(50, 50, 100, 50));

QPropertyAnimation *animation2 = new QPropertyAnimation(ui->pushButton_2, "geometry");
animation2->setDuration(3000);
animation2->setStartValue(ui->pushButton_2->geometry());
animation2->setEndValue(QRect(150, 50, 100, 50));

QPropertyAnimation *animation3 = new QPropertyAnimation(ui->pushButton_3, "geometry");
animation3->setDuration(3000);
animation3->setStartValue(ui->pushButton_3->geometry());
animation3->setEndValue(QRect(250, 50, 100, 50));

QPropertyAnimation *animation4 = new QPropertyAnimation(ui->pushButton_4, "geometry");
animation4->setDuration(3000);
animation4->setStartValue(ui->pushButton_4->geometry());
animation4->setEndValue(QRect(50, 200, 100, 50));

QPropertyAnimation *animation5 = new QPropertyAnimation(ui->pushButton_5, "geometry");
animation5->setDuration(3000);
animation5->setStartValue(ui->pushButton_5->geometry());
animation5->setEndValue(QRect(150, 200, 100, 50));

QPropertyAnimation *animation6 = new QPropertyAnimation(ui->pushButton_6, "geometry");
animation6->setDuration(3000);
animation6->setStartValue(ui->pushButton_6->geometry());
animation6->setEndValue(QRect(250, 200, 100, 50));

QEasingCurve curve;
curve.setType(QEasingCurve::OutBounce);
curve.setAmplitude(1.00);
curve.setOvershoot(1.70);
curve.setPeriod(0.30);

animation1->setEasingCurve(curve);
animation2->setEasingCurve(curve);
animation3->setEasingCurve(curve);
animation4->setEasingCurve(curve);
animation5->setEasingCurve(curve);
animation6->setEasingCurve(curve);
  1. 创建两个动画组,一个用于上列按钮,另一个用于下列按钮:
QParallelAnimationGroup *group1 = new QParallelAnimationGroup;
group1->addAnimation(animation1);
group1->addAnimation(animation2);
group1->addAnimation(animation3);

QParallelAnimationGroup *group2 = new QParallelAnimationGroup;
group2->addAnimation(animation4);
group2->addAnimation(animation5);
group2->addAnimation(animation6);
  1. 我们将创建另一个动画组,用于存储我们之前创建的两个动画组:
QSequentialAnimationGroup *groupAll = new QSequentialAnimationGroup;
groupAll->addAnimation(group1);
groupAll->addAnimation(group2);
groupAll->start();

工作原理...

我们在这里要做的是先播放上列按钮的动画,然后是下列按钮的动画。

由于两个动画组都是并行动画组,当调用start()函数时,属于各自组的按钮将同时进行动画。

这一次,然而,这个组是一个顺序动画组,这意味着一次只有一个并行动画组会被播放,当第一个完成时,另一个会接着播放。

动画组是一个非常方便的系统,它允许我们用简单的编码创建非常复杂的 GUI 动画。Qt 会为我们处理困难的部分,所以我们不必自己处理。

Qt 中的状态机

状态机可以用于许多目的,但在本章中,我们只会涵盖与动画相关的主题。

如何做...

  1. 首先,我们将为我们的示例程序设置一个新的用户界面,它看起来像这样:如何做...

  2. 接下来,我们将在源代码中包含一些头文件:

#include <QStateMachine>
#include <QPropertyAnimation>
#include <QEventTransition>
  1. 在我们的主窗口构造函数中,添加以下代码来创建一个新的状态机和两个状态,我们稍后会使用它们:
QStateMachine *machine = new QStateMachine(this);
QState *s1 = new QState();
QState *s2 = new QState();
  1. 然后,我们将定义在每个状态中应该做什么,这种情况下将是更改标签的文本,以及按钮的位置和大小:
QState *s1 = new QState();
s1->assignProperty(ui->stateLabel, "text", "Current state: 1");
s1->assignProperty(ui->pushButton, "geometry", QRect(50, 200, 100, 50));

QState *s2 = new QState();
s2->assignProperty(ui->stateLabel, "text", "Current state: 2");
s2->assignProperty(ui->pushButton, "geometry", QRect(200, 50, 140, 100));

  1. 完成后,让我们通过向源代码添加事件转换类来继续:
QEventTransition *t1 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);
t1->setTargetState(s2);
s1->addTransition(t1);

QEventTransition *t2 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);
T2->setTargetState(s1);
s2->addTransition(t2);
  1. 接下来,将我们刚刚创建的所有状态添加到状态机中,并将状态 1 定义为初始状态。然后,调用machine->start()来启动状态机运行:
machine->addState(s1);
machine->addState(s2);

machine->setInitialState(s1);
machine->start();
  1. 如果你现在运行示例程序,你会注意到一切都运行正常,除了按钮没有经历平滑的过渡,它只是立即跳到我们之前设置的位置和大小。这是因为我们没有使用属性动画来创建平滑的过渡。

  2. 返回到事件转换步骤,添加以下代码行:

QEventTransition *t1 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);
t1->setTargetState(s2);
t1->addAnimation(new QPropertyAnimation(ui->pushButton, "geometry"));
s1->addTransition(t1);

QEventTransition *t2 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);
t2->setTargetState(s1);
t2->addAnimation(new QPropertyAnimation(ui->pushButton, "geometry"));
s2->addTransition(t2);
  1. 你也可以为动画添加一个缓动曲线,使其看起来更有趣:
QPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, "geometry");
animation->setEasingCurve(QEasingCurve::OutBounce);
QEventTransition *t1 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);
t1->setTargetState(s2);
t1->addAnimation(animation);
s1->addTransition(t1);

QEventTransition *t2 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);
t2->setTargetState(s1);
t2->addAnimation(animation);
s2->addTransition(t2);

它是如何工作的...

主窗口布局上有两个按钮和一个标签。左上角的按钮在按下时将触发状态更改,而右上角的标签将更改其文本以显示我们当前处于哪个状态,并且下面的按钮将根据当前状态进行动画。

QEventTransition类定义了触发一个状态到另一个状态的转换。

在我们的例子中,当点击ui->changeState按钮(左上角的按钮)时,我们希望状态从状态 1 变为状态 2。之后,当再次按下相同的按钮时,我们还希望从状态 2 变回状态 1。这可以通过创建另一个事件转换类并将目标状态设置回状态 1 来实现。然后,将这些转换添加到它们各自的状态中。

我们告诉 Qt 使用属性动画类来平滑地插值属性到目标值,而不是直接将属性直接分配给小部件。就是这么简单!

不需要设置起始值和结束值,因为我们已经调用了assignProperty()函数,它已经自动分配了结束值。

还有更多...

Qt 中的状态机框架提供了用于创建和执行状态图的类。Qt 的事件系统用于驱动状态机,状态之间的转换可以通过使用信号来触发,然后在另一端的槽将被信号调用来执行一个动作,比如播放一个动画。

一旦你理解了状态机的基础知识,你也可以用它们来做其他事情。状态机框架中的状态图是分层的。就像前一节中的动画组一样,状态也可以嵌套在其他状态中:

还有更多...

QML 中的状态、转换和动画

如果你更喜欢使用 QML 而不是 C++,Qt 还提供了类似的功能在 Qt Quick 中,允许你用最少的代码轻松地为 GUI 元素添加动画。在这个例子中,我们将学习如何用 QML 实现这一点。

如何做...

  1. 首先,我们将创建一个新的Qt Quick Application项目,并设置我们的用户界面如下:如何做…

  2. 这是我的main.qml文件的样子:

import QtQuick 2.3
import QtQuick.Window 2.2

Window {
  visible: true
  width: 480;
  height: 320;

  Rectangle {
    id: background;
    anchors.fill: parent;
    color: "blue";
  }

  Text {
    text: qsTr("Hello World");
    anchors.centerIn: parent;
    color: "white";
    font.pointSize: 15;
  }
}
  1. 将颜色动画添加到Rectangle对象中:
Rectangle {
  id: background;
  anchors.fill: parent;
  color: "blue";
  SequentialAnimation on color
  {
    ColorAnimation { to: "yellow"; duration: 1000 }
    ColorAnimation { to: "red"; duration: 1000 }
    ColorAnimation { to: "blue"; duration: 1000 }
    loops: Animation.Infinite;
  }
}
  1. 然后,将数字动画添加到文本对象中:
Text {
  text: qsTr("Hello World");
  anchors.centerIn: parent;
  color: "white";
  font.pointSize: 15;
  SequentialAnimation on opacity {
 NumberAnimation { to: 0.0; duration: 200}
 NumberAnimation { to: 1.0; duration: 200}
 loops: Animation.Infinite;
 }
}
  1. 接下来,为其添加另一个数字动画:
Text {
  text: qsTr("Hello World");
  anchors.centerIn: parent;
  color: "white";
  font.pointSize: 15;
  SequentialAnimation on opacity {
    NumberAnimation { to: 0.0; duration: 200}
    NumberAnimation { to: 1.0; duration: 200}
    loops: Animation.Infinite;
  }
 NumberAnimation on rotation {
 from: 0;
 to: 360;
 duration: 2000;
 loops: Animation.Infinite;
 }
}
  1. 定义两种状态,一种称为PRESSED状态,另一种称为RELEASED状态。然后,将默认状态设置为RELEASED
Rectangle {
  id: background;
  anchors.fill: parent;

 state: "RELEASED";
 states: [
 State {
 name: "PRESSED"
 PropertyChanges { target: background; color: "blue"}
 },
 State {
 name: "RELEASED"
 PropertyChanges { target: background; color: "red"}
 }
 ]
}
  1. 之后,在Rectangle对象内创建一个鼠标区域,以便我们可以单击它:
MouseArea {
  anchors.fill: parent;
  onPressed: background.state = "PRESSED";
  onReleased: background.state = "RELEASED";
}
  1. Rectangle对象添加一些过渡效果:
transitions: [
  Transition {
    from: "PRESSED"
    to: "RELEASED"
    ColorAnimation { target: background; duration: 200}
  },
  Transition {
    from: "RELEASED"
    to: "PRESSED"
    ColorAnimation { target: background; duration: 200}
  }
]

它是如何工作的...

主窗口由一个蓝色矩形和静态文本组成,上面写着Hello World

我们希望背景颜色在循环中从蓝色变为黄色,然后变为红色,最后再变回蓝色。这可以通过在 QML 中使用颜色动画类型轻松实现。

在步骤 3 中,我们基本上是在Rectangle对象内创建了一个顺序动画组,然后在组内创建了三个不同的颜色动画,这些动画将每 1000 毫秒(1 秒)改变对象的颜色。我们还将动画设置为无限循环。

在步骤 4 中,我们希望使用数字动画来动画化静态文本的 alpha 值。我们在Text对象内创建了另一个顺序动画组,并创建了两个数字动画,以将 alpha 值从 0 动画到 1,然后再返回。然后,我们将动画设置为无限循环。

然后在第 5 步中,我们通过向其添加另一个数字动画来旋转Hello World文本。

在第 6 步中,我们希望在单击时使Rectangle对象从一种颜色变为另一种颜色。当鼠标释放时,Rectangle对象将恢复到其初始颜色。为了实现这一点,首先我们需要定义两种状态,一种称为PRESSED状态,另一种称为RELEASED状态。然后,我们将默认状态设置为RELEASED

现在,当您编译并运行示例时,背景在按下时会立即变为蓝色,当释放鼠标时会变回红色。这很好用,我们可以通过给颜色切换时添加一些过渡效果来进一步增强它。这可以通过向Rectangle对象添加过渡效果轻松实现。

还有更多…

在 QML 中,有八种不同类型的属性动画可供使用:

  • 锚点动画:动画化锚点值的变化

  • 颜色动画:动画化颜色值的变化

  • 数字动画:动画化 qreal 类型值的变化

  • 父动画:动画化父级值的变化

  • 路径动画:沿路径动画化项目

  • 属性动画:动画化属性值的变化

  • 旋转动画:动画化旋转值的变化

  • Vector3d 动画:动画化 QVector3d 值的变化

就像 C++版本一样,这些动画也可以在动画组中组合在一起,以便按顺序或并行播放动画。您还可以使用缓动曲线来控制动画,并使用状态机确定何时播放这些动画,就像我们在上一节中所做的那样。

使用动画器动画化小部件属性

在本教程中,我们将学习如何使用 QML 提供的动画器功能来动画化 GUI 小部件的属性。

如何做…

  1. 创建一个矩形对象,并为其添加一个比例动画器:
Rectangle {
  id: myBox;
  width: 50;
  height: 50;
  anchors.horizontalCenter: parent.horizontalCenter;
  anchors.verticalCenter: parent.verticalCenter;
  color: "blue";

  ScaleAnimator {
    target: myBox;
    from: 5;
    to: 1;
    duration: 2000;
    running: true;
  }
}
  1. 添加一个旋转动画器,并将运行值设置为并行动画组中,但不是任何单独的动画器中。
ParallelAnimation {
  ScaleAnimator {
    target: myBox;
    from: 5;
    to: 1;
    duration: 2000;
  }
  RotationAnimator {
    target: myBox;
    from: 0;
    to: 360;
    duration: 1000;
  }
  running: true;
}
  1. 为比例动画器添加一个缓动曲线:
ScaleAnimator {
  target: myBox;
  from: 5;
  to: 1;
  duration: 2000;
  easing.type: Easing.InOutElastic;
 easing.amplitude: 2.0;
 asing.period: 1.5;
  running: true;
}

它是如何工作的...

动画器类型可以像任何其他动画类型一样使用。我们希望在 2000 毫秒(2 秒)内将矩形从大小5缩放到大小1

我们创建了一个蓝色的Rectangle对象,并为其添加了一个比例动画器。我们将初始值设置为5,最终值设置为1。然后,我们将动画持续时间设置为2000,并将运行值设置为true,这样程序启动时就会播放它。

就像动画类型一样,动画器也可以放入组中(即并行动画组或顺序动画组)。动画组也将被 QtQuick 视为动画器,并在可能的情况下在场景图的渲染线程上运行。

在第 2 步中,我们想将两个不同的动画器分组到一个并行动画组中,以便它们同时运行。

我们将保留之前创建的缩放动画器,并向Rectangle对象添加另一个旋转动画器。这次,在并行动画组中设置运行值,而不是在任何单独的动画器中设置。

就像 C++版本一样,QML 也支持缓动曲线,它们可以轻松应用于任何动画或动画器类型。

QML 中有一种叫做动画器的东西,它与普通动画类型类似但又不同。动画器类型是一种特殊类型的动画,它直接在 Qt Quick 的场景图上运行,而不是像常规动画类型那样在 QML 对象和它们的属性上运行。

QML 属性的值将在动画完成后更新。然而,在动画运行时,属性不会被更新。使用动画器类型的好处是性能稍微更好,因为它不在 UI 线程上运行,而是直接在场景图的渲染线程上运行。

精灵动画

在这个例子中,我们将学习如何在 QML 中创建精灵动画。

如何做…

  1. 首先,我们需要将精灵表添加到 Qt 的资源系统中,以便在程序中使用。打开qml.qrc,点击添加 | 添加文件按钮。选择精灵表图像并按下Ctrl + S保存资源文件。

  2. 之后,在main.qml中创建一个新的空窗口:

import QtQuick 2.3
import QtQuick.Window 2.2

Window {
  visible: true
  width: 420
  height: 380
  Rectangle {
    anchors.fill: parent
    color: "white"
  }
}
  1. 完成后,我们将在 QML 中开始创建一个AnimatedSprite对象:
import QtQuick 2.3
import QtQuick.Window 2.2

Window {
  visible: true;
  width: 420;
  height: 380;
  Rectangle {
    anchors.fill: parent;
    color: "white";
  }

 AnimatedSprite {
 id: sprite;
 width: 128;
 height: 128;
 anchors.centerIn: parent;
 source: "qrc:///horse_1.png";
 frameCount: 11;
 frameWidth: 128;
 frameHeight: 128;
 frameRate: 25;
 loops: Animation.Infinite;
 running: true;
 }
}
  1. 在窗口中添加一个鼠标区域并检查onClicked事件:
MouseArea {
  anchors.fill: parent;
  onClicked: {
    if (sprite.paused)
      sprite.resume();
    else
      sprite.pause();
  }
}
  1. 如果现在编译和运行示例程序,你会看到一个小马在窗口中间奔跑。多有趣啊!如何做…

  2. 接下来,我们想尝试做一些酷炫的事情。我们将让马在窗口中奔跑,并在播放奔跑动画的同时无限循环!

首先,我们需要从 QML 中删除anchors.centerIn: parent,并用xy值替换它:

AnimatedSprite {
  id: sprite;
  width: 128;
  height: 128;
  x: -128;
  y: parent.height / 2;
  source: "qrc:///horse_1.png";
  frameCount: 11;
  frameWidth: 128;
  frameHeight: 128;
  frameRate: 25;
  loops: Animation.Infinite;
  running: true;
}
  1. 之后,向精灵对象添加一个数字动画,并设置其属性如下:
NumberAnimation {
  target: sprite;
  property: "x";
  from: -128;
  to: 512;
  duration: 3000;
  loops: Animation.Infinite;
  running: true;
}
  1. 现在编译和运行示例程序,你会看到小马疯狂地在窗口中奔跑!

工作原理…

在这个示例中,我们将动画精灵对象放在窗口中间,并将其图像源设置为刚刚添加到项目资源中的精灵表。

然后,我们数了一下属于奔跑动画的精灵表中有多少帧,这里是 11 帧。我们还告诉 Qt 每一帧动画的尺寸,这里是 128 x 128。之后,我们将帧速率设置为25以获得合理的速度,然后将其设置为无限循环。然后我们将奔跑值设置为true,这样动画在程序开始运行时将默认播放。

然后在第 4 步,我们希望能够通过点击窗口来暂停动画并恢复它。当点击鼠标区域时,我们只需检查精灵当前是否暂停。如果精灵动画已经暂停,那么恢复动画;否则,暂停动画。

在第 6 步,我们用xy值替换了anchors.centerIn,这样动画精灵对象就不会锚定在窗口中心,这样就可以移动了。

然后,我们在动画精灵中创建了一个数字动画,以动画化其x属性。我们将起始值设置为窗口左侧的某个位置,将结束值设置为窗口右侧的某个位置。之后,我们将持续时间设置为 3,000 毫秒(3 秒),并使其无限循环。

最后,我们还将运行值设置为true,这样当程序开始运行时,它将默认播放动画。

还有更多...

精灵动画被广泛应用,尤其在游戏开发中。精灵用于角色动画、粒子动画,甚至 GUI 动画。精灵表包含许多图像组合成一个,然后可以被切割并逐一显示在屏幕上。从精灵表中不同图像(或精灵)之间的过渡创造了动画的错觉,我们通常称之为精灵动画。在 QML 中,可以很容易地使用AnimatedSprite类型实现精灵动画。

注意

在这个示例程序中,我使用了由 bluecarrot16 在 CC-BY 3.0 / GPL 3.0 / GPL 2.0 / OGA-BY 3.0 许可下创建的免费开源图像。该图像可以在opengameart.org/content/lpc-horse上合法获取。

第三章:QPainter 和 2D 图形

在本章中,我们将涵盖以下内容:

  • 在屏幕上绘制基本形状

  • 将形状导出为 SVG 文件

  • 坐标变换

  • 在屏幕上显示图像

  • 将图像效果应用于图形

  • 创建基本绘画程序

  • QML 中的 2D 画布

简介

在本章中,我们将学习如何使用 Qt 在屏幕上渲染 2D 图形。在内部,Qt 使用一个低级别的类称为QPainter来在主窗口上渲染其小部件。Qt 允许我们访问和使用QPainter类来绘制矢量图形、文本、2D 图像,甚至 3D 图形。您可以利用QPainter类来创建自定义小部件,或者创建依赖于计算机图形渲染的程序,如视频游戏、照片编辑器、3D 建模工具等。

在屏幕上绘制基本形状

在本节中,我们将学习如何使用QPainter类在主窗口上绘制简单的矢量形状(线条、矩形、圆形等)并显示文本。我们还将学习如何使用QPen类更改矢量形状的绘制样式。

如何做...

首先,让我们创建一个新的Qt Widgets 应用程序项目:

  1. 打开mainwindow.ui并删除菜单栏、主工具栏和状态栏,以便获得一个干净的空白主窗口。右键单击栏小部件,从弹出菜单中选择删除菜单栏操作步骤...

  2. 然后,打开mainwindow.h并添加以下代码以包含QPainter头文件:

#include <QMainWindow>
#include <QPainter>

  1. 然后,在类析构函数下面声明paintEvent()事件处理程序:
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
virtual void paintEvent(QPaintEvent *event);

  1. 接下来,打开mainwindow.cpp并定义paintEvent()事件处理程序:
void MainWindow::paintEvent(QPaintEvent *event)
{
}
  1. 之后,我们将使用paintEvent()事件处理程序内的QPainter类向屏幕添加文本。我们在屏幕上的位置(20, 30)绘制文本之前设置文本字体设置:
QPainter textPainter(this);
textPainter.setFont(QFont("Times", 14, QFont::Bold));
textPainter.drawText(QPoint(20, 30), "Testing");
  1. 然后,我们将绘制一条从(50, 60)开始到(100, 100)结束的直线:
QPainter linePainter(this);
linePainter.drawLine(QPoint(50, 60), QPoint(100, 100));
  1. 我们还可以通过使用QPainter类调用drawRect()函数轻松绘制一个矩形形状。不过这次,在绘制之前我们还会为形状应用一个背景图案:
QPainter rectPainter(this);
rectPainter.setBrush(Qt::BDiagPattern);
rectPainter.drawRect(QRect(40, 120, 80, 30));
  1. 接下来,声明一个QPen类,将其颜色设置为red,将其绘制样式设置为Qt::DashDotLine。然后,将QPen类应用于QPainter并在(80, 200)处绘制一个水平半径为50,垂直半径为20的椭圆形:
QPen ellipsePen;
ellipsePen.setColor(Qt::red);
ellipsePen.setStyle(Qt::DashDotLine);

QPainter ellipsePainter(this);
ellipsePainter.setPen(ellipsePen);
ellipsePainter.drawEllipse(QPoint(80, 200), 50, 20);
  1. 我们还可以使用QPainterPath类来定义形状,然后将其传递给QPainter类进行渲染:
QPainterPath rectPath;
rectPath.addRect(QRect(150, 20, 100, 50));

QPainter pathPainter(this);
pathPainter.setPen(QPen(Qt::red, 1, Qt::DashDotLine, Qt::FlatCap, Qt::MiterJoin));
pathPainter.setBrush(Qt::yellow);
pathPainter.drawPath(rectPath);
  1. 您还可以使用QPainterPath绘制任何其他形状,比如椭圆:
QPainterPath ellipsePath;
ellipsePath.addEllipse(QPoint(200, 120), 50, 20);

QPainter ellipsePathPainter(this);
ellipsePathPainter.setPen(QPen(QColor(79, 106, 25), 5, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
ellipsePathPainter.setBrush(QColor(122, 163, 39));
ellipsePathPainter.drawPath(ellipsePath);
  1. QPainter也可以用来将图像文件绘制到屏幕上。在下面的示例中,我们加载一个名为tux.png的图像文件,并在屏幕上的位置(100, 150)绘制它:
QImage image;
image.load("tux.png");

QPainter imagePainter(this);
imagePainter.drawImage(QPoint(100, 150), image);
  1. 最终结果应该看起来像这样:操作步骤...

工作原理...

如果您想使用QPainter在屏幕上绘制一些东西,基本上您只需要告诉它应该绘制什么类型的图形(文本、矢量形状、图像、多边形等),以及其位置和大小。

QPen 确定了图形的轮廓样式,如颜色、线宽、线型(实线、虚线、点线等)、端点样式、连接样式等。

另一方面,QBrush设置了图形的背景样式,如背景颜色、图案(纯色、渐变、密集刷、交叉对角线等)和像素图。

在调用绘制函数(drawLine()drawRect()drawEllipse()等)之前应设置图形的选项。

如果你的图形不显示在屏幕上,并且在 Qt Creator 的应用程序输出窗口中看到警告,比如QPainter::setPen: Painter not activeQPainter::setBrush: Painter not active,这意味着QPainter类当前不活动,你的程序不会触发它的绘制事件。要解决这个问题,将主窗口设置为QPainter类的父类。通常,如果你在mainwindow.cpp文件中编写代码,初始化QPainter时只需要在括号中放入this。例如:

QPainter linePainter(this);

QImage可以从计算机目录和程序资源中加载图像。

还有更多…

QPainter想象成一个带着笔和空画布的机器人。你只需要告诉机器人应该画什么类型的形状以及它在画布上的位置,然后机器人会根据你的描述完成工作。为了让你的生活更轻松,QPainter类还提供了许多函数,比如drawArc()drawEllipse()drawLine()drawRect()drawPie()等,让你可以轻松地渲染预定义的形状。

在 Qt 中,所有的窗口部件类(包括主窗口)都有一个名为QWidget::paintEvent()的事件处理程序。每当操作系统认为主窗口应该重新绘制其窗口部件时,这个事件处理程序就会被触发。许多事情可能导致这个决定,比如主窗口被缩放,窗口部件改变其状态(即,按钮被按下),或者在代码中手动调用repaint()update()等函数。不同的操作系统在决定是否触发相同条件下的更新事件时可能会有不同的行为。如果你正在制作一个需要连续和一致的图形更新的程序,可以使用定时器手动调用repaint()update()

将形状导出为 SVG 文件

可伸缩矢量图形SVG)是一种基于 XML 的语言,用于描述二维矢量图形。Qt 提供了保存矢量形状到 SVG 文件的类。这个功能可以用来创建一个简单的矢量图形编辑器,类似于 Adobe Illustrator 和 Inkscape。

在下一个示例中,我们将继续使用前一个示例中的相同项目文件。

如何做…

让我们学习如何创建一个简单的程序,在屏幕上显示 SVG 图形:

  1. 首先,通过右键单击层次结构窗口上的主窗口部件,并从弹出菜单中选择创建菜单栏选项来创建一个菜单栏。之后,在菜单栏中添加一个文件选项,然后在其下方添加一个另存为 SVG操作:如何做…

  2. 之后,在 Qt Creator 窗口底部的Action Editor窗口中会看到一个名为actionSave_as_SVG的项目。右键单击该项目,从弹出菜单中选择转到槽…。现在会出现一个窗口,其中列出了特定操作可用的槽。选择名为triggered()的默认信号,然后点击OK按钮:如何做…

  3. 点击OK按钮后,Qt Creator 将切换到脚本编辑器。你会发现一个名为on_actionSave_as_SVG_triggered()的槽已经自动添加到你的主窗口类中。在你的mainwindow.h的底部,你会看到类似这样的内容:

void MainWindow::on_actionSave_as_SVG_triggered()
{
}

当你从菜单栏点击另存为 SVG选项时,将调用上述函数。我们将在这个函数中编写代码,将所有矢量图形保存到一个 SVG 文件中。

  1. 为了做到这一点,我们首先需要在源文件顶部包含一个名为QSvgGenerator的类头文件。这个头文件非常重要,因为它用于生成 SVG 文件。然后,我们还需要包含另一个名为QFileDialog的类头文件,它将用于打开保存对话框:
#include <QtSvg/QSvgGenerator>
#include <QFileDialog>
  1. 我们还需要在项目文件中添加 SVG 模块,如下所示:
QT += core gui svg
  1. 然后,在mainwindow.h中创建一个名为paintAll()的新函数,如下所示:
public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

  virtual void paintEvent(QPaintEvent *event);
  void paintAll(QSvgGenerator *generator = 0);
  1. 之后,在mainwindow.cpp中,将所有代码从paintEvent()移动到paintAll()函数中。然后,用单一统一的QPainter替换所有单独的QPainter对象来绘制所有图形。还要在绘制任何内容之前调用begin()函数,并在完成绘制后调用end()函数。代码应该如下所示:
void MainWindow::paintAll(QSvgGenerator *generator)
{
  QPainter painter;

  if (engine)
    painter.begin(engine);
  else
    painter.begin(this);

  painter.setFont(QFont("Times", 14, QFont::Bold));
  painter.drawText(QPoint(20, 30), "Testing");

  painter.drawLine(QPoint(50, 60), QPoint(100, 100));

  painter.setBrush(Qt::BDiagPattern);
  painter.drawRect(QRect(40, 120, 80, 30));

  QPen ellipsePen;
  ellipsePen.setColor(Qt::red);
  ellipsePen.setStyle(Qt::DashDotLine);

  painter.setPen(ellipsePen);
  painter.drawEllipse(QPoint(80, 200), 50, 20);

  QPainterPath rectPath;
  rectPath.addRect(QRect(150, 20, 100, 50));

  painter.setPen(QPen(Qt::red, 1, Qt::DashDotLine, Qt::FlatCap, Qt::MiterJoin));
  painter.setBrush(Qt::yellow);
  painter.drawPath(rectPath);

  QPainterPath ellipsePath;
  ellipsePath.addEllipse(QPoint(200, 120), 50, 20);

  painter.setPen(QPen(QColor(79, 106, 25), 5, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
  painter.setBrush(QColor(122, 163, 39));
  painter.drawPath(ellipsePath);

  QImage image;
  image.load("tux.png");

  painter.drawImage(QPoint(100, 150), image);

  painter.end();
}
  1. 由于我们已将所有代码从paintEvent()移动到paintAll(),因此现在我们应该在paintEvent()中调用paintAll()函数,如下所示:
void MainWindow::paintEvent(QPaintEvent *event)
{
 paintAll();
}
  1. 然后,我们将编写将图形导出到 SVG 文件的代码。代码将写在名为on_actionSave_as_SVG_triggered()的槽函数中,该函数由 Qt 生成。我们首先调用保存文件对话框,并从用户那里获取所需文件名的目录路径:
void MainWindow::on_actionSave_as_SVG_triggered()
{
  QString filePath = QFileDialog::getSaveFileName(this, "Save SVG", "", "SVG files (*.svg)");

  if (filePath == "")
    return;
}
  1. 之后,创建一个QSvgGenerator对象,并通过将QSvgGenerator对象传递给paintAll()函数将图形保存到 SVG 文件中:
void MainWindow::on_actionSave_as_SVG_triggered()
{
  QString filePath = QFileDialog::getSaveFileName(this, "Save SVG", "", "SVG files (*.svg)");

  if (filePath == "")
    return;

 QSvgGenerator generator;
 generator.setFileName(filePath);
 generator.setSize(QSize(this->width(), this->height()));
 generator.setViewBox(QRect(0, 0, this->width(), this->height()));
 generator.setTitle("SVG Example");
 generator.setDescription("This SVG file is generated by Qt.");

 paintAll(&generator);
}
  1. 现在编译并运行程序,您应该能够通过转到文件 | 另存为 SVG来导出图形:如何做...

工作原理...

默认情况下,QPainter将使用其父对象的绘图引擎来绘制分配给它的图形。如果您没有为QPainter分配任何父对象,可以手动为其分配绘图引擎,这就是我们在这个例子中所做的。

我们将代码放入paintAll()的原因是因为我们希望将相同的代码用于两个不同的目的:用于在窗口上显示图形和将图形导出到 SVG 文件。请注意paintAll()函数中generator变量的默认值设置为0,这意味着除非指定,否则不需要QSvgGenerator对象来运行该函数。稍后,在paintAll()函数中,我们检查generator对象是否存在。如果存在,就像这样将其用作绘图引擎:

if (engine)
 painter.begin(engine);
else
  painter.begin(this);

否则,将主窗口传递给begin()函数(因为我们正在mainwindow.cpp中编写代码,所以可以直接使用this来引用主窗口的指针),这样它将使用主窗口本身的绘图引擎,这意味着图形将绘制在主窗口的表面上。

在这个例子中,需要使用单个QPainter对象将图形保存到 SVG 文件中。如果使用多个QPainter对象,生成的 SVG 文件将包含多个 XML 头定义,因此任何图形编辑软件都会将文件视为无效。

QFileDialog::getSaveFileName()将为用户打开本机保存文件对话框,以选择保存目录并设置所需的文件名。一旦用户完成,完整路径将作为字符串返回,我们将能够将该信息传递给QSvgGenerator对象以导出图形。

请注意,在上一张截图中,SVG 文件中的企鹅已被裁剪。这是因为 SVG 的画布大小设置为跟随主窗口的大小。为了帮助可怜的企鹅找回身体,导出 SVG 文件之前将窗口放大。

还有更多...

可缩放矢量图形SVG)以 XML 格式定义图形。由于它是矢量图形,所以 SVG 图形在缩放或调整大小时不会失去任何质量。

SVG 允许三种类型的图形对象:矢量图形、光栅图形和文本。包括 PNG 和 JPEG 光栅图像在内的图形对象可以分组、样式化、变换和合成到先前渲染的对象中。

您可以在www.w3.org/TR/SVG上查看 SVG 图形的完整规范。

坐标变换

在这个例子中,我们将学习如何使用坐标变换和定时器来创建实时时钟显示。

如何做...

要创建我们的第一个图形时钟显示,请按照以下步骤进行:

  1. 首先,创建一个新的Qt Widgets Application项目。然后,打开mainwindow.ui并移除菜单栏、工具栏和状态栏。

  2. 在此之后,打开mainwindow.h并包含以下头文件:

#include <QTime>
#include <QTimer>
#include <QPainter>
  1. 然后,声明paintEvent()函数,如下所示:
public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

virtual void paintEvent(QPaintEvent *event);

  1. mainwindow.cpp中,创建三个数组来存储时针、分针和秒针的形状,其中每个数组包含三组坐标:
void MainWindow::paintEvent(QPaintEvent *event)
{
 static const QPoint hourHand[3] =
 {
 QPoint(4, 4),
 QPoint(-4, 4),
 QPoint(0, -40)
 };

 static const QPoint minuteHand[3] =
 {
 QPoint(4, 4),
 QPoint(-4, 4),
 QPoint(0, -70)
 };

 static const QPoint secondHand[3] =
 {
 QPoint(2, 2),
 QPoint(-2, 2),
 QPoint(0, -90)
 };
}
  1. 在此之后,将以下代码添加到数组下面,以创建绘图器并将其移动到主窗口的中心。此外,我们调整绘图器的大小,使其在主窗口中很好地适应,即使窗口被调整大小:
int side = qMin(width(), height());

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(width() / 2, height() / 2);
painter.scale(side / 250.0, side / 250.0);
  1. 完成后,我们将通过使用for循环开始绘制刻度。每个刻度旋转增加 6 度,所以 60 个刻度将完成一个完整的圆。此外,每 5 分钟的刻度看起来会稍微长一些:
for (int i = 0; i < 60; ++i)
{
  if ((i % 5) != 0)
    painter.drawLine(92, 0, 96, 0);
  else
    painter.drawLine(86, 0, 96, 0);
  painter.rotate(6.0);
}
  1. 然后,我们继续绘制时钟的指针。每个指针的旋转根据当前时间和其相应的单位计算超过 360 度:
QTime time = QTime::currentTime();

// Draw hour hand
painter.save();
painter.rotate((time.hour() * 360) / 12);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);
painter.drawConvexPolygon(hourHand, 3);
painter.restore();

// Draw minute hand
painter.save();
painter.rotate((time.minute() * 360) / 60);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);
painter.drawConvexPolygon(minuteHand, 3);
painter.restore();

// Draw second hand
painter.save();
painter.rotate((time.second() * 360) / 60);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);
painter.drawConvexPolygon(secondHand, 3);
painter.restore();
  1. 最后,创建一个定时器,每秒刷新一次图形,使程序像一个真正的时钟一样工作!
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent), ui(new Ui::MainWindow)
{
  ui->setupUi(this);

  QTimer* timer = new QTimer(this);
  timer->start(1000);
  connect(timer, SIGNAL(timeout()), this, SLOT(update()));
}
  1. 现在编译并运行程序,你应该会看到类似这样的东西:如何做...

它是如何工作的...

每个数组都包含三个QPoint数据,形成一个细长的三角形。然后将这些数组传递给绘图器,并使用drawConvexPolygon()函数呈现为凸多边形。

在绘制每个时钟指针之前,我们使用painter.save()保存QPainter对象的状态,然后使用坐标转换继续绘制指针。完成绘制后,我们通过调用painter.restore()将绘图器恢复到先前的状态。这个函数将撤消painter.restore()之前的所有转换,以便下一个时钟指针不会继承上一个时钟指针的转换。如果不使用painter.save()painter.restore(),我们将不得不在绘制下一个指针之前手动改变位置、旋转和比例。

不使用painter.save()painter.restore()的一个很好的例子是绘制刻度。由于每个刻度的旋转是从前一个刻度增加 6 度,我们根本不需要保存绘图器的状态。我们只需在循环中调用painter.rotate(6.0),每个刻度将继承前一个刻度的旋转。我们还使用模运算符(%)来检查刻度所代表的单位是否可以被 5 整除。如果可以,我们就会稍微拉长它。

如果不使用定时器不断调用update()槽,时钟将无法正常工作。这是因为当父窗口的状态没有改变时,Qt 不会调用paintEvent()。因此,我们需要手动告诉 Qt 我们需要通过每秒调用update()来刷新图形。

我们使用painter.setRenderHint(QPainter::Antialiasing)函数在渲染时启用抗锯齿。没有抗锯齿,图形看起来会非常锯齿和像素化:

它是如何工作的...

还有更多...

QPainter类使用坐标系统来确定在屏幕上呈现图形之前的位置和大小。这些信息可以被改变,使图形出现在不同的位置、旋转和大小。这个改变图形坐标信息的过程就是我们所谓的坐标转换。有几种类型的转换,其中包括平移、旋转、缩放和剪切:

还有更多...

Qt 使用一个坐标系统,其原点位于左上角,意味着 x 值向右增加,y 值向下增加。这个坐标系统可能与物理设备使用的坐标系统不同,比如计算机屏幕。Qt 通过使用QPaintDevice类自动处理这个问题,将 Qt 的逻辑坐标映射到物理坐标。

QPainter提供了四种变换操作来执行不同类型的变换:

  • QPainter::translate(): 通过给定的单位偏移图形的位置

  • QPainter::rotate(): 以顺时针方向围绕原点旋转图形

  • QPainter::scale(): 通过给定的因子偏移图形的大小

  • QPainter::shear(): 扭曲图形的坐标系围绕原点

在屏幕上显示图像

Qt 不仅允许我们在屏幕上绘制形状和图像,还允许我们将多个图像叠加在一起,并使用不同类型的算法结合所有图层的像素信息,以创建非常有趣的结果。在这个例子中,我们将学习如何将图像叠加在一起,并对它们应用不同的组合效果。

如何做…

让我们创建一个简单的演示,通过以下步骤展示不同图像组合效果的效果:

  1. 首先,设置一个新的Qt Widgets Application项目,并移除菜单栏、工具栏和状态栏。

  2. 接下来,将 QPainter 类头文件添加到mainwindow.h中:

#include <QPainter>
  1. 之后,声明paintEvent()虚函数如下:
virtual void paintEvent(QPaintEvent* event);
  1. mainwindow.cpp中,我们将首先使用QImage类加载几个图像文件:
void MainWindow::paintEvent(QPaintEvent* event)
{
 QImage image;
 image.load("checker.png");

 QImage image2;
 image2.load("tux.png");

 QImage image3;
 image3.load("butterfly.png");
}
  1. 然后,创建一个QPainter对象,并使用它来绘制两对图像,其中一张图像叠加在另一张图像上:
QPainter painter(this);
painter.drawImage(QPoint(10, 10), image);
painter.drawImage(QPoint(10, 10), image2);
painter.drawImage(QPoint(300, 10), image);
painter.drawImage(QPoint(300, 40), image3);
  1. 现在编译并运行程序,你应该会看到类似这样的东西:如何做…

  2. 接下来,我们将在屏幕上绘制每个图像之前设置组合模式:

QPainter painter(this);

painter.setCompositionMode(QPainter::CompositionMode_Difference);
painter.drawImage(QPoint(10, 10), image);
painter.setCompositionMode(QPainter::CompositionMode_Multiply);
painter.drawImage(QPoint(10, 10), image2);

painter.setCompositionMode(QPainter::CompositionMode_Xor);
painter.drawImage(QPoint(300, 10), image);
painter.setCompositionMode(QPainter::CompositionMode_SoftLight);
painter.drawImage(QPoint(300, 40), image3);
  1. 再次编译并运行程序,你现在会看到类似这样的东西:如何做…

它是如何工作的…

在使用 Qt 绘制图像时,调用drawImage()函数的顺序将决定首先渲染哪个图像,以及后渲染哪个图像。这将影响图像的深度顺序,并产生不同的结果。

在之前的例子中,我们调用了drawImage()四次,在屏幕上绘制了四个不同的图像。第一次drawImage()渲染了checker.png,第二次drawImage()渲染了tux.png(企鹅)。后渲染的图像将始终出现在其他图像的前面,这就是为什么企鹅显示在棋盘前面。右侧的蝴蝶和棋盘也是如此。尽管蝴蝶被渲染在棋盘前面,你仍然能看到棋盘,这是因为蝴蝶图像不是完全不透明的。

现在让我们反转渲染顺序,看看会发生什么。我们将尝试首先渲染企鹅,然后是棋盘。右侧的另一对图像也是如此:蝴蝶首先被渲染,然后是棋盘:

它是如何工作的…

要对图像应用组合效果,我们必须在绘制图像之前设置绘图者的组合模式,通过调用painter.setCompositionMode()函数来选择所需的组合模式。

在之前的例子中,我们将QPainter::CompositionMode_Difference应用到左侧的棋盘上,这使其颜色反转。接下来,我们将QPainter::CompositionMode_Overlay应用到企鹅上,使其与棋盘混合,我们能够看到两个图像叠加在一起。

在右侧,我们将QPainter::CompositionMode_Xor应用于棋盘,如果源和目的地之间存在差异,则显示颜色;否则,它将呈现为黑色。由于它正在与白色背景比较差异,棋盘的不透明部分变为完全黑色。我们还将QPainter::CompositionMode_SoftLight应用于蝴蝶图像。这会将像素与背景混合,降低对比度。

如果您想在进行下一个渲染之前禁用刚刚设置的合成模式,请将其设置回默认模式,即QPainter::CompositionMode_SourceOver

还有更多…

例如,我们可以将多个图像叠加在一起,并使用 Qt 的图像合成功能将它们合并在一起,并根据我们使用的合成模式计算屏幕上的结果像素。这在图像编辑软件(如 Photoshop 和 GIMP)中经常用于合成图像图层。

Qt 中有 30 多种合成模式可用。一些最常用的模式包括:

  • 清除:目的地中的像素被设置为完全透明,与源无关。

  • :输出是源像素。这种模式是CompositionMode_Destination的反向。

  • 目的地:输出是目的地像素。这意味着混合没有效果。这种模式是CompositionMode_Source的反向。

  • 源上:通常称为 alpha 混合。源的 alpha 值用于将像素混合在目的地的顶部。这是QPainter使用的默认模式。

  • 目的地超过:目的地的 alpha 值用于在源像素的顶部混合。这种模式是CompositionMode_SourceOver的反向。

  • 源入:输出是源,其中 alpha 值减少了目标的 alpha 值。

  • 目的地内:输出是目的地,其中 alpha 值减少了源的 alpha 值。这种模式是CompositionMode_SourceIn的反向。

  • 源出:输出是源,其中 alpha 值减少了目标的倒数。

  • 目的地外:输出是目的地,其中 alpha 值减少了源的倒数。这种模式是CompositionMode_SourceOut的反向。

  • 源顶部:源像素在目标的顶部混合,源像素的 alpha 值减少了目标像素的 alpha 值。

  • 目的地顶部:目的地像素在源的顶部混合,源像素的 alpha 值减少了目的地像素的 alpha 值。这种模式是CompositionMode_SourceAtop的反向。

  • 异或:这是异或的缩写,是一种主要用于图像分析的高级混合模式。源的 alpha 值减少了目的地 alpha 值的倒数,与目的地合并,目的地的 alpha 值减少了源 alpha 值的倒数。

以下图像显示了使用不同合成模式叠加两个图像的结果:

更多内容…

应用图像效果到图形

Qt 提供了一种简单的方法,可以在使用QPainter类绘制的任何图形上添加图像效果。在这个例子中,我们将学习如何应用不同的图像效果,如阴影、模糊、着色和不透明度效果,以在屏幕上显示图形之前应用到图形上。

如何做…

让我们学习如何通过以下步骤将图像效果应用于文本和图形:

  1. 创建一个新的Qt 小部件应用程序,并删除菜单栏、工具栏和状态栏。

  2. 通过转到文件 | 新文件或项目创建一个新的资源文件,并添加项目所需的所有图像:如何做…

  3. 接下来,打开mainwindow.ui并向窗口添加四个标签。其中两个标签将是文本,另外两个将加载我们刚刚添加到资源文件中的图像:如何做…

  4. 您可能已经注意到字体大小比默认大小要大得多。例如,可以通过向标签小部件添加样式表来实现:

font: 26pt "MS Shell Dlg 2";
  1. 之后,打开mainwindow.cpp并在源代码顶部包含以下头文件:
#include <QGraphicsBlurEffect>
#include <QGraphicsDropShadowEffect>
#include <QGraphicsColorizeEffect>
#include <QGraphicsOpacityEffect>
  1. 然后,在MainWindow类的构造函数中,添加以下代码以创建一个投影效果,并将其应用于其中一个标签:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent), ui(new Ui::MainWindow)
{
  ui->setupUi(this);

 QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect();
 shadow->setXOffset(4);
 shadow->setYOffset(4);
 ui->label->setGraphicsEffect(shadow);
}
  1. 接下来,我们将创建一个着色效果,并将其应用于其中一幅图像,这里是蝴蝶。我们还将效果颜色设置为红色:
QGraphicsColorizeEffect* colorize = new QGraphicsColorizeEffect();
colorize->setColor(QColor(255, 0, 0));
ui->butterfly->setGraphicsEffect(colorize);
  1. 完成后,创建一个模糊效果,并将其半径设置为12。然后,将图形效果应用于另一个标签:
QGraphicsBlurEffect* blur = new QGraphicsBlurEffect();
blur->setBlurRadius(12);
ui->label2->setGraphicsEffect(blur);
  1. 最后,创建一个 alpha 效果,并将其应用于企鹅图像。我们将不透明度值设置为0.2,即 20%的不透明度:
QGraphicsOpacityEffect* alpha = new QGraphicsOpacityEffect();
alpha->setOpacity(0.2);
ui->penguin->setGraphicsEffect(alpha);
  1. 现在编译并运行程序,您应该能够看到类似于这样的东西:如何做…

它是如何工作的…

每种图形效果都是其自己的类,继承自QGraphicsEffect父类。您可以通过创建一个新类,该类继承自QGraphicsEffect并重新实现其中的一些函数来创建自定义效果。

每种效果都有其专门为其创建的一组变量。例如,您可以设置着色效果的颜色,但在模糊效果中没有这样的变量。这是因为每种效果与其他效果大不相同,这也是为什么它需要成为自己的类,而不是使用相同的类来处理所有不同的效果。

一次只能向小部件添加一个图形效果。如果添加多个效果,只有最后一个效果将应用于小部件,因为它替换了前一个效果。除此之外,要注意,如果创建了一个图形效果,比如投影效果,您不能将其分配给两个不同的小部件,因为它只会分配给您应用它的最后一个小部件。如果需要将相同类型的效果应用于几个不同的小部件,创建几个相同类型的图形效果,并将每个效果应用于各自的小部件。

还有更多…

目前 Qt 支持模糊、投影、着色和不透明度效果。这些效果可以通过调用以下类来使用:QGraphicsBlurEffectQGraphicsDropShadowEffectQGraphicsColorizeEffectQGraphicsOpacityEffect。所有这些类都是从QGraphicsEffect类继承的。您还可以通过创建QGrapicsEffect的子类(或任何其他现有效果)并重新实现draw()函数来创建自定义图像效果。

图形效果仅改变源的边界矩形。如果要增加边界矩形的边距,可以重新实现虚拟的boundingRectFor()函数,并在此矩形更改时调用updateBoundingRect()来通知框架。

创建一个基本的绘画程序

由于我们已经学习了关于QPainter类以及如何使用它在屏幕上显示图形,我想现在是时候做一些有趣的事情,这样我们就可以将我们的知识付诸实践了。

在这个示例中,我们将学习如何制作一个基本的绘画程序,允许我们在画布上用不同的画笔大小和颜色绘制线条。我们还将学习如何使用QImage类和鼠标事件来构建绘画程序。

如何做…

让我们通过以下步骤开始我们有趣的项目:

  1. 同样,我们首先创建一个新的Qt Widgets Application项目,并移除工具栏和状态栏。这次我们将保留菜单栏。

  2. 之后,设置菜单栏如下:如何做…

  3. 我们将暂时保留菜单栏,然后继续进行mainwindow.h。首先,包括以下头文件,因为它对项目是必需的:

#include <QPainter>
#include <QMouseEvent>
#include <QFileDialog>
  1. 接下来,声明我们将在此项目中使用的变量,如下所示:
private:
Ui::MainWindow *ui;

QImage image;
bool drawing;
QPoint lastPoint;
int brushSize;
QColor brushColor;

  1. 然后,声明事件回调函数,这些函数是从QWidget类继承的。这些函数将由 Qt 在相应事件发生时触发。我们将重写这些函数,并告诉 Qt 在这些事件被调用时该做什么:
public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

 virtual void mousePressEvent(QMouseEvent *event);
 virtual void mouseMoveEvent(QMouseEvent *event);
 virtual void mouseReleaseEvent(QMouseEvent *event);
 virtual void paintEvent(QPaintEvent *event);
 virtual void resizeEvent(QResizeEvent *event);

  1. 之后,转到mainwindow.cpp并在类构造函数中添加以下代码以设置一些变量:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent), ui(new Ui::MainWindow)
{
  ui->setupUi(this);

 image = QImage(this->size(), QImage::Format_RGB32);
 image.fill(Qt::white);

 drawing = false;
 brushColor = Qt::black;
 brushSize = 2;
}
  1. 接下来,我们将构造mousePressEvent()事件并告诉 Qt 当左鼠标按钮被按下时该做什么:
void MainWindow::mousePressEvent(QMouseEvent *event)
{
  if (event->button() == Qt::LeftButton)
  {
    drawing = true;
    lastPoint = event->pos();
  }
}
  1. 然后,我们将构造mouseMoveEvent()事件并告诉 Qt 当鼠标移动时该做什么。在这种情况下,如果左鼠标按钮被按住,我们希望在画布上绘制线条:
void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
  if ((event->buttons() & Qt::LeftButton) && drawing)
  {
    QPainter painter(&image);
    painter.setPen(QPen(brushColor, brushSize, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
    painter.drawLine(lastPoint, event->pos());

    lastPoint = event->pos();
    this->update();
  }
}
  1. 之后,我们还将构造mouseReleaseEvent()事件,当鼠标按钮释放时将被触发:
void MainWindow::mouseReleaseEvent(QMouseEvent *event)
{
  if (event->button() == Qt::LeftButton)
  {
    drawing = false;
  }
}
  1. 完成后,我们将继续进行paintEvent()事件,与我们在之前章节中看到的其他示例相比,这个事件非常简单:
void MainWindow::paintEvent(QPaintEvent *event)
{
  QPainter canvasPainter(this);
  canvasPainter.drawImage(this->rect(), image, image.rect());
}
  1. 记住我们有一个无所事事的菜单栏吗?让我们右键单击 GUI 编辑器下面的每个操作,并在弹出菜单中选择转到槽...。我们要告诉 Qt 当菜单栏上的每个选项被选择时该做什么:如何做到这一点...

  2. 然后,选择名为triggered()的默认槽,并按下确定按钮。Qt 将自动生成一个新的槽函数,分别在你的mainwindow.hmainwindow.cpp中。完成所有操作后,你应该在mainwindow.h中看到类似这样的东西:

private slots:
  void on_actionSave_triggered();
  void on_actionClear_triggered();
  void on_action2px_triggered();
  void on_action5px_triggered();
  void on_action10px_triggered();
  void on_actionBlack_triggered();
  void on_actionWhite_triggered();
  void on_actionRed_triggered();
  void on_actionGreen_triggered();
  void on_actionBlue_triggered();
  1. 接下来,我们将告诉 Qt 在每个这些槽被触发时该做什么:
void MainWindow::on_actionSave_triggered()
{
  QString filePath = QFileDialog::getSaveFileName(this, "Save Image", "", "PNG (*.png);;JPEG (*.jpg *.jpeg);;All files (*.*)");

  if (filePath == "")
    return;

  image.save(filePath);
}
void MainWindow::on_actionClear_triggered()
{
  image.fill(Qt::white);
  this->update();
}
void MainWindow::on_action2px_triggered()
{
  brushSize = 2;
}
void MainWindow::on_action5px_triggered()
{
  brushSize = 5;
}
void MainWindow::on_action10px_triggered()
{
  brushSize = 10;
}
void MainWindow::on_actionBlack_triggered()
{
  brushColor = Qt::black;
}

void MainWindow::on_actionWhite_triggered()
{
  brushColor = Qt::white;
}
void MainWindow::on_actionRed_triggered()
{
  brushColor = Qt::red;
}
void MainWindow::on_actionGreen_triggered()
{
  brushColor = Qt::green;
}
void MainWindow::on_actionBlue_triggered()
{
  brushColor = Qt::blue;
}
  1. 如果我们现在编译并运行程序,我们将得到一个简单但可用的绘图程序:如何做到这一点...

它是如何工作的...

在这个例子中,当程序启动时,我们创建了一个QImage小部件。这个小部件充当画布,当窗口被调整大小时,它将跟随窗口的大小。

为了在画布上绘制东西,我们需要使用 Qt 提供的鼠标事件。这些事件将告诉我们光标的位置,我们将能够利用这些信息来改变画布上的像素。

我们使用一个名为drawing的布尔变量来让程序知道当鼠标按钮被按下时是否应该开始绘制。在这种情况下,当左鼠标按钮被按下时,变量drawing将被设置为true。当左鼠标按钮被按下时,我们还将当前光标位置保存到lastPoint变量中,这样 Qt 就会知道从哪里开始绘制。

当鼠标移动时,Qt 将触发mouseMoveEvent()事件。这是我们需要检查drawing变量是否设置为true的地方。如果是,那么QPainter可以根据我们提供的画笔设置开始在QImage小部件上绘制线条。

画笔设置包括画笔颜色和画笔大小。这些设置被保存为变量,并可以通过从菜单栏中选择不同的设置来更改。

请记住,在用户在画布上绘制时调用update()函数。否则,尽管我们已经改变了画布的像素信息,画布仍将保持空白。当我们从菜单栏中选择文件 | 清除时,我们还必须调用update()函数来重置我们的画布。

在这个例子中,我们使用QImage::save()来保存图像文件,这非常简单和直接。我们使用文件对话框让用户决定在哪里保存图像及其所需的文件名。然后,我们将信息传递给QImage,它将自行完成剩下的工作。如果我们没有向QImage::save()函数指定文件格式,QImage将尝试通过查看所需文件名的扩展名来确定它。

QML 中的 2D 画布

在本章的所有先前示例中,我们已经讨论了使用 Qt 的 C++ API 渲染 2D 图形的方法和技术。然而,我们还没有学习如何使用强大的 QML 脚本来实现类似的结果。

如何做…

在这个项目中,我们将做一些完全不同的事情:

  1. 像往常一样,我们应该首先创建一个新项目,方法是转到文件 | 新建文件或项目,然后选择Qt Quick Application作为项目模板。

  2. 创建新项目后,从项目窗格中的Resource文件夹中右键单击打开qml.qrc。然后,从项目资源中删除MainForm.ui.qml,因为我们不需要它:如何做…

  3. 接下来,打开qml.rc项目窗格下列出的main.qml。之后,删除引用MainForm的整个部分。现在剩下的只有main.qml中的Window对象。之后,为窗口设置一个 ID,并将其宽度和高度调整为更高的值,如下所示:

import QtQuick 2.5
import QtQuick.Window 2.2

Window
{
  id: myWindow
  visible: true
  width: 540
  height: 380
}
  1. 然后,在myWindow下添加一个Canvas对象,并将其命名为myCanvas。之后,将其宽度和高度设置为与myWindow相同:
Window
{
  id: myWindow
  visible: true
  width: 540
  height: 380

 Canvas
 {
 id: myCanvas
 width: myWindow.width
 height: myWindow.height
 }
}
  1. 接下来,我们定义onPaint事件触发时会发生什么;在这种情况下,我们将在窗口上绘制一个十字架:
Canvas
{
  id: myCanvas
  width: myWindow.width
  height: myWindow.height

  onPaint:
 {
 var context = getContext('2d')
 context.fillStyle = 'white'
 context.fillRect(0, 0, width, height)
 context.lineWidth = 2
 context.strokeStyle = 'black'

 // Draw cross
 context.beginPath()
 context.moveTo(50, 50)
 context.lineTo(100, 100)
 context.closePath()
 context.stroke()

 context.beginPath()
 context.moveTo(100, 50)
 context.lineTo(50, 100)
 context.closePath()
 context.stroke()
 }
}
  1. 之后,添加以下代码以在十字架旁边绘制一个勾号:
// Draw tick
context.beginPath()
context.moveTo(150, 90)
context.lineTo(158, 100)
context.closePath()
context.stroke()

context.beginPath()
context.moveTo(180, 100)
context.lineTo(210, 50)
context.closePath()
context.stroke()
  1. 然后,通过添加以下代码来绘制一个三角形形状:
// Draw triangle
context.lineWidth = 4
context.strokeStyle = "red"
context.fillStyle = "salmon"

context.beginPath()
context.moveTo(50,150)
context.lineTo(150,150)
context.lineTo(50,250)
context.closePath()
context.fill()
context.stroke()
  1. 之后,使用以下代码绘制一个半圆和一个完整的圆:
// Draw circle
context.lineWidth = 4
context.strokeStyle = "blue"
context.fillStyle = "steelblue"

var pi = 3.141592653589793

context.beginPath()
context.arc(220, 200, 60, 0, pi, true)
context.closePath()
context.fill()
context.stroke()

context.beginPath()
context.arc(220, 280, 60, 0, 2 * pi, true)
context.closePath()
context.fill()
context.stroke()
  1. 最后,我们从文件中绘制一个 2D 图像:
// Draw image
context.drawImage("tux.png", 280, 10, 256, 297)
  1. 然而,仅仅使用上述代码将无法成功在屏幕上渲染图像,因为您还必须预先加载图像文件。在Canvas对象内添加以下代码,以便在程序启动时要求 QML 加载图像文件,然后在图像加载时调用requestPaint()信号,以便触发onPaint()事件槽:
Component.onCompleted:
{
 loadImage("tux.png")
}

onImageLoaded:requestPaint();
onPaint:
{
  // The code we added previously
}
  1. 现在构建并运行程序,您应该会得到以下结果:如何做…

第四章:OpenGL 实现

在本章中,我们将涵盖以下内容:

  • 在 Qt 中设置 OpenGL

  • 你好,世界!

  • 渲染 2D 形状

  • 渲染 3D 形状

  • OpenGL 中的纹理

  • OpenGL 中的光照和纹理滤镜

  • 使用键盘控制移动对象

  • QML 中的 3D 画布

介绍

在本章中,我们将学习如何使用开放图形库OpenGL),这是一个强大的渲染应用程序编程接口API),并将其与 Qt 结合使用。OpenGL 是一个跨语言、跨平台的 API,用于通过计算机的图形芯片内的图形处理单元GPU)在屏幕上绘制 2D 和 3D 图形。在本章中,我们将学习 OpenGL 2.x 而不是 3.x,因为对于初学者来说,固定功能管线比较新的可编程管线更容易理解。Qt 支持这两个版本,因此一旦您学会了 OpenGL 渲染的基本概念,切换到 OpenGL 3.x 及以上版本就不会有问题。

在 Qt 中设置 OpenGL

在这个示例中,我们将学习如何在 Qt 中设置 OpenGL。

操作方法…

  1. 首先,让我们通过转到文件 | 新建文件或项目来创建一个新的 Qt 小部件应用程序。

  2. 接下来,我们将删除mainwindow.ui文件,因为我们在本示例中不会使用它。右键单击mainwindow.ui文件,然后从下拉菜单中选择删除文件。然后,将出现一个消息框并要求您确认。选中永久删除文件并按确定按钮。

  3. 之后,打开您的项目文件(.pro),并通过在QT +=后面添加opengl关键字来将 OpenGL 模块添加到您的项目中,如下所示:

QT += core gui opengl
  1. 您还需要在项目文件中添加另一行,以便在启动时加载 OpenGL 和GLuOpenGL 实用程序)库。没有这两个库,您的程序将无法运行:
LIBS += -lopengl32 -lglu32
  1. 然后,打开mainwindow.h并从中删除一些内容:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>

namespace Ui {
  class MainWindow;
}
class MainWindow : public QMainWindow
{
  Q_OBJECT
  public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
  private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
  1. 接下来,将以下代码添加到您的mainwindow.h中:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QOpenGLWindow>

class MainWindow : public QOpenGLWindow
{
  Q_OBJECT
  public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

  protected:
    virtual void initializeGL();
    virtual void resizeGL(int w, int h);
    virtual void paintGL();
    void paintEvent(QPaintEvent *event);
    void resizeEvent(QResizeEvent *event);
};

#endif // MAINWINDOW_H
  1. 完成后,我们将继续进行源文件,即mainwindow.cpp。我们刚刚添加到头文件中的函数,如initializeGL()resizeGL()等,现在可以暂时留空;我们将在下一节中使用这些函数:
#include "mainwindow.h"
#include "ui_mainwindow.h"

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

MainWindow::~MainWindow()
{
  delete ui;
}
void MainWindow::initializeGL()
{
  void MainWindow::resizeGL(int w, int h)
{
}
void MainWindow::paintGL()
{
}
void MainWindow::paintEvent(QPaintEvent *event)
{
}
void MainWindow::resizeEvent(QResizeEvent *event)
{
}
  1. 最后,通过将以下代码添加到您的main.cpp文件中,为主窗口设置标题并将其调整大小为 640x480:
#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
  QApplication a(argc, argv);
  MainWindow w;
  w.setTitle("OpenGL Hello World!");
  w.resize(640, 480);
  w.show();
  return a.exec();
}
  1. 如果您现在编译并运行项目,您将看到一个带有黑色背景的空窗口。不要担心,您的程序现在正在使用 OpenGL 运行!操作方法…

它是如何工作的…

必须在项目文件(.pro)中添加 OpenGL 模块,以便访问与 OpenGL 相关的头文件,如 QtOpenGL、QOpenGLFunctions 等。我们使用了QOpenGLWindow类而不是QMainWindow用于主窗口,因为它被设计为轻松创建执行 OpenGL 渲染的窗口,并且与 QOpenGLWidget 相比具有更好的性能,因为它在其小部件模块中没有依赖项。我们必须调用setSurfaceType(QWindow::OpenGLSurface)来告诉 Qt 我们更喜欢使用 OpenGL 来将图像渲染到屏幕上,而不是使用 QPainter。QOpenGLWindow类为我们提供了几个虚拟函数(initializeGL()resizeGL()paintGL()等),方便我们设置 OpenGL 并执行图形渲染。

还有更多…

OpenGL 是一个跨语言、跨平台的 API,用于通过计算机的图形芯片内的图形处理单元GPU)在屏幕上绘制 2D 和 3D 图形。

计算机图形技术多年来发展迅速,以至于软件行业几乎无法跟上其步伐。2008 年,维护和开发 OpenGL 的 Khronos Group 公司宣布发布 OpenGL 3.0 规范,这在整个行业中引起了巨大的轰动和争议。这主要是因为 OpenGL 3.0 应该废弃 OpenGL API 中的整个固定功能流水线,对于大公司来说,从固定功能流水线一夜之间转换为可编程流水线是不可能的任务。这导致 Khronos Group 同时维护两个不同的 OpenGL 主要版本,即 OpenGL 2.x 和 3.x。

在本章中,我们将学习 OpenGL 2.x 而不是 3.x,因为对于初学者来说,固定功能流水线比可编程流水线更容易理解。对于学习计算机图形编程的基础知识来说,这是非常直接和不容易混淆的。Qt 支持这两个版本,因此一旦学会了 OpenGL 渲染的基本概念,切换到 OpenGL 3.x(及以上版本)应该没有问题。

Qt 在适当时候内部使用 OpenGL。此外,新的 Qt Quick 2 渲染器基于 OpenGL,现在是 Qt 图形提供的核心部分。这使得 OpenGL 与 Qt 的兼容性比其他任何图形 API(如 DirectX)都更好。

你好世界!

在这个示例中,我们将学习 OpenGL 的流水线以及如何将简单的形状渲染到窗口中。我们将继续使用上一个示例项目中的示例。

操作步骤…

  1. 首先,转到mainwindow.h并在源代码顶部添加以下头文件:
#include <QSurfaceFormat>
#include <QOpenGLFunctions>
#include <QtOpenGL>
#include <GL/glu.h>
  1. 接下来,在mainwindow.h中声明两个私有变量:
private:
  QOpenGLContext* context;
  QOpenGLFunctions* openGLFunctions;
  1. 之后,转到mainwindow.cpp并将表面格式设置为兼容性配置文件。我们还将 OpenGL 版本设置为 2.1,并使用我们刚刚声明的格式创建 OpenGL 上下文。然后,使用我们刚刚创建的上下文来访问仅与我们刚刚设置的 OpenGL 版本相关的 OpenGL 函数,通过调用context->functions()
MainWindow::MainWindow(QWidget *parent)
{
 setSurfaceType(QWindow::OpenGLSurface);
 QSurfaceFormat format;
 format.setProfile(QSurfaceFormat::CompatibilityProfile);
 format.setVersion(2, 1); // OpenGL 2.1
 setFormat(format);

 context = new QOpenGLContext;
 context->setFormat(format);
 context->create();
 context->makeCurrent(this);

 openGLFunctions = context->functions();
}
  1. 接下来,我们将开始向paintGL()函数中添加一些代码:
void MainWindow::paintGL()
{
 // Initialize clear color (cornflower blue)
 glClearColor(0.39f, 0.58f, 0.93f, 1.f);

 // Clear color buffer
 glClear(GL_COLOR_BUFFER_BIT);

 // Render quad
 glBegin(GL_QUADS);
 glVertex2f(-0.5f, -0.5f);
 glVertex2f(0.5f, -0.5f);
 glVertex2f(0.5f, 0.5f);
 glVertex2f(-0.5f, 0.5f);
 glEnd();

 glFlush();
}
  1. paintEvent()函数中调用paintGL()之前,屏幕上不会出现任何内容:
void MainWindow::paintEvent(QPaintEvent *event)
{
 paintGL();
}
  1. 如果现在编译并运行项目,您应该能够看到一个白色矩形在蓝色背景前被绘制出来:操作步骤…

工作原理…

我们必须将 OpenGL 版本设置为 2.1,并将表面格式设置为兼容性配置文件,以便访问在较新版本中不再存在的固定功能流水线。或者,如果要使用 OpenGL 3.x 及以上版本,可以将表面格式设置为QSurfaceFormat::CoreProfile

我们调用了glClearColor()glClear(GL_COLOR_BUFFER_BIT)来清除先前的渲染缓冲区(或者通俗地说,上一个帧),并用我们提供的颜色填充整个画布。在渲染图像后,我们将重复这一步骤,以便在进行下一帧之前清除整个屏幕。我们调用了glBegin(GL_QUAD)来告诉 OpenGL 我们将在屏幕上绘制一个四边形。之后,我们向 OpenGL 提供了所有顶点(或点)的位置,以便它知道如何将四边形放置在屏幕上,通过四次调用glVertex2f(),因为四边形只能通过连接四个不同的点来构造。然后,我们调用了glEnd()来告诉 OpenGL 我们已经完成了四边形。

在完成屏幕上的图像绘制后,始终调用glFlush(),以便 OpenGL 清除内存中的所有不需要的信息,为下一次绘制腾出空间。

最后,在paintEvent()函数中必须调用paintGL(),否则屏幕上将什么都不会被绘制。就像我们在前几章中学到的那样,所有的绘图都发生在paintEvent()函数中,只有在 Qt 认为有必要刷新屏幕时才会调用它。要强制 Qt 更新屏幕,需要手动调用update()

渲染 2D 形状

由于我们已经学会了如何在屏幕上绘制第一个矩形,我们将在本节中进一步增强它。我们将采用前面的例子,并从那里继续。

如何做...

  1. 首先,转到mainwindow.cpp中的paintGL()函数,并用新代码替换上一个示例中的四边形。这次,我们画了一个四边形和一个三角形:
void MainWindow::paintGL()
{
  // Initialize clear color (cornflower blue)
  glClearColor(0.39f, 0.58f, 0.93f, 1.f);

  // Clear color buffer
  glClear(GL_COLOR_BUFFER_BIT);

 glBegin(GL_QUADS);
 glVertex2f(-0.5f, -0.5f);
 glVertex2f(0.5f, -0.5f);
 glVertex2f(0.5f, 0.5f);
 glVertex2f(-0.5f, 0.5f);
 glEnd();

 glBegin(GL_QUADS);
 glColor3f(1.f, 0.f, 0.f); glVertex2f(-0.8f, -0.8f);
 glColor3f(1.f, 1.f, 0.f); glVertex2f(0.3f, -0.8f);
 glColor3f(0.f, 1.f, 0.f); glVertex2f(0.3f, 0.3f);
 glColor3f(0.f, 0.f, 1.f); glVertex2f(-0.8f, 0.3f);
 glEnd();

 glBegin(GL_TRIANGLES);
 glColor3f(1.f, 0.f, 0.f); glVertex2f(-0.4f, -0.4f);
 glColor3f(0.f, 1.f, 0.f); glVertex2f(0.8f, -0.1f);
 glColor3f(0.f, 0.f, 1.f); glVertex2f(-0.1f, 0.8f);
 glEnd();

  glFlush();
}
  1. 接下来,在resizeGL()函数中,添加以下代码来调整视口和正交视图,以便渲染的图像正确地遵循窗口的纵横比:
void MainWindow::resizeGL(int w, int h)
{
  // Initialize Projection Matrix
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  glViewport(0, 0, w, h);

  qreal aspectRatio = qreal(w) / qreal(h);
  glOrtho(-1 * aspectRatio, 1 * aspectRatio, -1, 1, 1, -1);
}
  1. 然后,在resizeEvent()函数中,调用resize()函数并强制主窗口刷新屏幕:
void MainWindow::resizeEvent(QResizeEvent *event)
{
 resizeGL(this->width(), this->height());
 this->update();
}
  1. 之后,在initializeGL()函数中,我们调用resizeGL()一次,以便第一个渲染的图像的纵横比是正确的(在任何窗口调整大小事件触发之前):
void MainWindow::initializeGL()
{
 resizeGL(this->width(), this->height());
}
  1. 完成后,编译并运行程序。你应该会看到类似这样的东西:如何做...

工作原理...

OpenGL 支持的几何基元类型包括点、线、线条、线环、多边形、四边形、四边形条带、三角形、三角形条带和三角形扇形。在这个例子中,我们画了一个四边形和一个三角形,每个形状都提供了一组顶点和颜色,以便 OpenGL 知道如何渲染形状。彩虹色是通过给每个顶点赋予不同的颜色来创建的。OpenGL 会自动在每个顶点之间插值颜色并在屏幕上显示。稍后渲染的形状将始终出现在其他形状的前面。在这种情况下,三角形稍后被渲染,因此它出现在矩形的前面。

我们需要在每次调整大小时计算主窗口的纵横比,以便渲染的图像不会被拉伸,导致奇怪的外观。在调用glViewport()glOrtho()之前,始终通过调用glMatrixMode()glLoadIdentity()重置投影矩阵,以便在调整主窗口大小时正确渲染形状。如果不重置投影矩阵,我们将使用上一帧的矩阵,从而产生错误的投影。

注意

记得在调整窗口大小时调用update(),否则屏幕将不会更新。

渲染 3D 形状

在上一节中,我们已经学会了如何在屏幕上绘制简单的 2D 形状。然而,为了充分利用 OpenGL API,我们还需要学习如何使用它来渲染 3D 图像。简而言之,3D 图像只是使用堆叠的 2D 形状创建的一种幻觉,使它们看起来像 3D。

这里的主要成分是深度值,它决定了哪些形状应该出现在其他形状的前面或后面。位于另一个表面后面(深度比另一个形状浅)的基本形状将不会被渲染(或部分渲染)。OpenGL 提供了一种简单的方法来实现这一点,而不需要太多的技术麻烦。

如何做...

  1. 首先,在你的mainwindow.h中添加QTimer头文件:
#include <QTimer>
  1. 然后,在你的MainWindow类中添加一个私有变量:
private:
  QOpenGLContext* context;
  QOpenGLFunctions* openGLFunctions;
 float rotation;

  1. 我们还在mainwindow.h中添加了一个公共槽,以备后用:
public slots:
  void updateAnimation();
  1. 之后,在mainwindow.cppinitializeGL()函数中添加glEnable(GL_DEPTH_TEST)以启用深度测试:
void MainWindow::initializeGL()
{
 //  Enable Z-buffer depth test
 glEnable(GL_DEPTH_TEST);
  resizeGL(this->width(), this->height());
}
  1. 接下来,我们将修改resizeGL()函数,以便使用透视视图而不是正交视图:
void MainWindow::resizeGL(int w, int h)
{
  // Set the viewport
  glViewport(0, 0, w, h);
  qreal aspectRatio = qreal(w) / qreal(h);

  // Initialize Projection Matrix
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

 glOrtho(-1 * aspectRatio, 1 * aspectRatio, -1, 1, 1, -1);
 gluPerspective(75, aspectRatio, 0.1, 400000000);

 // Initialize Modelview Matrix
 glMatrixMode(GL_MODELVIEW);
 glLoadIdentity();
}
  1. 之后,我们还需要修改paintGL()函数。首先,将GL_DEPTH_BUFFER_BIT添加到glClear()函数中,因为我们还需要清除上一帧的深度信息,然后再渲染下一帧。然后,删除我们在之前示例中使用的代码,该代码在屏幕上渲染了一个四边形和一个三角形:
void MainWindow::paintGL()
{
  // Initialize clear color (cornflower blue)
  glClearColor(0.39f, 0.58f, 0.93f, 1.f);

 // Clear color buffer
 glClear(GL_COLOR_BUFFER_BIT);
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

 glBegin(GL_QUADS);
 glColor3f(1.f, 0.f, 0.f); glVertex2f(-0.8f, -0.8f);
 glColor3f(1.f, 1.f, 0.f); glVertex2f(0.3f, -0.8f);
 glColor3f(0.f, 1.f, 0.f); glVertex2f(0.3f, 0.3f);
 glColor3f(0.f, 0.f, 1.f); glVertex2f(-0.8f, 0.3f);
 glEnd();

 glBegin(GL_TRIANGLES);
 glColor3f(1.f, 0.f, 0.f); glVertex2f(-0.4f, -0.4f);
 glColor3f(0.f, 1.f, 0.f); glVertex2f(0.8f, -0.1f);
 glColor3f(0.f, 0.f, 1.f); glVertex2f(-0.1f, 0.8f);
 glEnd();

  glFlush();
}
  1. 然后,在调用glFlush()之前,我们将添加以下代码来绘制一个 3D 立方体:
// Reset modelview matrix
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

// Transformations
glTranslatef(0.0, 0.0, -3.0);
glRotatef(rotation, 1.0, 1.0, 1.0);

// FRONT
glBegin(GL_POLYGON);
  glColor3f(0.0, 0.0, 0.0);
  glVertex3f(0.5, -0.5, -0.5); glVertex3f(0.5, 0.5, -0.5);
  glVertex3f(-0.5, 0.5, -0.5); glVertex3f(-0.5, -0.5, -0.5);
glEnd();

// BACK
glBegin(GL_POLYGON);
  glColor3f(0.0, 1.0, 0.0);
  glVertex3f(0.5, -0.5, 0.5); glVertex3f(0.5, 0.5, 0.5);
  glVertex3f(-0.5, 0.5, 0.5); glVertex3f(-0.5, -0.5, 0.5);
glEnd();

// RIGHT
glBegin(GL_POLYGON);
  glColor3f(1.0, 0.0, 1.0);
  glVertex3f(0.5, -0.5, -0.5); glVertex3f(0.5, 0.5, -0.5);
  glVertex3f(0.5, 0.5, 0.5); glVertex3f(0.5, -0.5, 0.5);
glEnd();

// LEFT
glBegin(GL_POLYGON);
  glColor3f(1.0, 1.0, 0.0);
  glVertex3f(-0.5, -0.5, 0.5); glVertex3f(-0.5, 0.5, 0.5);
  glVertex3f(-0.5, 0.5, -0.5); glVertex3f(-0.5, -0.5, -0.5);
glEnd();

// TOP
glBegin(GL_POLYGON);
  glColor3f(0.0, 0.0, 1.0);
  glVertex3f(0.5, 0.5, 0.5); glVertex3f(0.5, 0.5, -0.5);
  glVertex3f(-0.5, 0.5, -0.5); glVertex3f(-0.5, 0.5, 0.5);
glEnd();

// BOTTOM
glBegin(GL_POLYGON);
  glColor3f(1.0, 0.0, 0.0);
  glVertex3f(0.5, -0.5, -0.5); glVertex3f(0.5, -0.5, 0.5);
  glVertex3f(-0.5, -0.5, 0.5); glVertex3f(-0.5, -0.5, -0.5);
glEnd();
  1. 完成后,向MainWindow类的构造函数中添加一个定时器,如下所示:
MainWindow::MainWindow(QWidget *parent)
{
  setSurfaceType(QWindow::OpenGLSurface);
  QSurfaceFormat format;
  format.setProfile(QSurfaceFormat::CompatibilityProfile);
  format.setVersion(2, 1); // OpenGL 2.1
  setFormat(format);

  context = new QOpenGLContext;
  context->setFormat(format);
  context->create();
  context->makeCurrent(this);

  openGLFunctions = context->functions();

 QTimer *timer = new QTimer(this);
 connect(timer, SIGNAL(timeout()), this, SLOT(updateAnimation()));
 timer->start(100);

 rotation = 0;
}
  1. 最后,每当定时器调用updateAnimation()槽时,我们将旋转变量增加 10。我们还手动调用update()函数来更新屏幕:
void MainWindow::updateAnimation()
{
  rotation += 10;
  this->update();
}
  1. 如果现在编译并运行程序,您应该会在主窗口中看到一个旋转的立方体!如何做...

它是如何工作的...

在任何 3D 渲染中,深度非常重要,因此我们需要通过调用glEnable(GL_DEPTH_TEST)在 OpenGL 中启用深度测试功能。当我们清除缓冲区时,我们还必须指定GL_DEPH_BUFFER_BIT,以便深度信息也被清除,以便下一幅图像能够正确渲染。

我们使用gluPerspective()来设置透视投影矩阵,以便图形看起来具有深度和距离。透视视图的相反是正交视图,这是 OpenGL 中的默认视图,我们在之前的示例中使用过。正交投影是一种平行投影,其中物体看起来是平的,不具有深度和距离的概念:

它是如何工作的...

在这个例子中,我们使用了一个定时器,每 100 毫秒(0.1 秒)增加旋转值 10。然后在将顶点数据提供给 OpenGL 之前,通过调用glRotatef()将旋转值应用于立方体。我们还调用了glTranslatef()将立方体稍微向后移动,以便它不会太靠近相机视图。

记得手动调用update(),以便屏幕得到刷新,否则立方体将不会被动画化。

OpenGL 中的纹理

OpenGL 允许我们将图像(也称为纹理)映射到 3D 形状或多边形上。这个过程也被称为纹理映射。在这种情况下,Qt 似乎是与 OpenGL 最佳组合,因为它提供了一种简单的方式来加载属于常见格式(BMP、JPEG、PNG、TARGA、TIFF 等)的图像,而不需要自己实现。我们将使用旋转立方体的先前示例,并尝试将其与纹理映射!

如何做...

  1. 首先,打开mainwindow.h并将以下标题添加到其中:
#include <QGLWidget>
  1. 接下来,声明一个数组,用于存储由 OpenGL 创建的纹理 ID。在渲染时我们将使用它:
private:
  QOpenGLContext* context;
  QOpenGLFunctions* openGLFunctions;

  float rotation;
 GLuint texID[1];

  1. 之后,打开mainwindow.cpp并将以下代码添加到initializeGL()中以加载纹理文件:
void MainWindow::initializeGL()
{
  // Enable Z-buffer depth test
  glEnable(GL_DEPTH_TEST);

 // Enable texturing
 glEnable(GL_TEXTURE_2D);

 QImage image("bricks");
 QImage texture = QGLWidget::convertToGLFormat(image);

 glGenTextures(1, &texID[0]);
 glBindTexture(GL_TEXTURE_2D, texID[0]);

 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.width(), texture.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, texture.bits());

  // Make sure render at the correct aspect ratio
  resizeGL(this->width(), this->height());
}
  1. 然后,将以下代码添加到paintGL()函数中,将纹理应用到 3D 立方体上:
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texID[0]);

// FRONT
glBegin(GL_POLYGON);
  glColor3f(0.0, 0.0, 0.0);
  glTexCoord2f(0.0f, 0.0f); glVertex3f(0.5, -0.5, -0.5);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(0.5, 0.5, -0.5);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5, 0.5, -0.5);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5, -0.5, -0.5);
glEnd();

// BACK
glBegin(GL_POLYGON);
  glColor3f(0.0, 1.0, 0.0);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(0.5, -0.5, 0.5);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(0.5, 0.5, 0.5);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5, 0.5, 0.5);
  glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5, -0.5, 0.5);
glEnd();

// RIGHT
glBegin(GL_POLYGON);
  glColor3f(1.0, 0.0, 1.0);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(0.5, -0.5, -0.5);
  glTexCoord2f(0.0f, 0.0f); glVertex3f(0.5, 0.5, -0.5);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(0.5, 0.5, 0.5);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(0.5, -0.5, 0.5);
glEnd();

// LEFT
glBegin(GL_POLYGON);
  glColor3f(1.0, 1.0, 0.0);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5, -0.5, 0.5);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5, 0.5, 0.5);
  glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5, 0.5, -0.5);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(-0.5, -0.5, -0.5);
glEnd();

// TOP
glBegin(GL_POLYGON);
  glColor3f(0.0, 0.0, 1.0);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(0.5, 0.5, 0.5);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(0.5, 0.5, -0.5);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5, 0.5, -0.5);
  glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5, 0.5, 0.5);
glEnd();

// Red side - BOTTOM
glBegin(GL_POLYGON);
  glColor3f(1.0, 0.0, 0.0);
  glTexCoord2f(0.0f, 0.0f); glVertex3f( 0.5, -0.5, -0.5);
  glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.5, -0.5, 0.5);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5, -0.5, 0.5);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5, -0.5, -0.5);
glEnd();

glDisable(GL_TEXTURE_2D);
  1. 如果现在编译并运行程序,您应该会看到一个围绕屏幕旋转的砖块立方体!如何做...

它是如何工作的...

变量GLuint texID[1]是一个数组,用于存储由 OpenGL 在我们调用glGenTexture()时生成的纹理 ID,OpenGL 在渲染期间使用它来从内存中分配纹理。在这种情况下,我们将数组的大小设置为1,因为在这个示例中我们只使用一个纹理。我们必须告诉 OpenGL 通过调用glEnable(GL_TEXTURE_2D)来启用纹理处理,然后再进行与纹理相关的任何操作。我们使用了两个QImage类来加载纹理,第一个称为image用于加载图像文件,第二个称为texture用于将图像转换为 OpenGL 兼容格式。然后我们调用glGenTextures()使用 OpenGL 生成一个空纹理,之后我们调用glBindTexture()来选择特定的纹理。这一步是必要的,以便之后调用的函数将应用于我们刚刚选择的纹理。

接下来,我们调用了两次glTexParameteri()来将纹理缩小和纹理放大设置为点采样。这将告诉 OpenGL 纹理应该如何渲染。之后,我们调用了glTexImage2D()来提供由 Qt 加载的纹理文件中的像素信息到我们刚刚创建的空 OpenGL 纹理中。在开始渲染 3D 立方体之前,调用glEnabled(GL_TEXTURE_2D)glBindTexture()来启用 OpenGL 中的纹理处理并选择我们想要使用的纹理。然后,在调用glVertex3f()之前,我们必须调用glTexCoord2f()来告诉 OpenGL 纹理应该如何映射。我们提供纹理的坐标,OpenGL 会为我们解决其余的问题。

完成后,调用glDisable(GL_TEXTURE_2D)来禁用纹理处理。

OpenGL 中的照明和纹理滤镜

在这个示例中,我们将学习如何在 OpenGL 中对我们使用的纹理应用不同类型的滤镜效果,如点采样、双线性插值和三线性插值。

如何做...

  1. 再次,我们将使用之前的示例,并在旋转的立方体附近添加一个光源。打开mainwindow.cpp并将以下代码添加到initializeGL()函数中:
// Trilinear interpolation
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.width(), texture.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, texture.bits());

// Enable smooth shading
glShadeModel(GL_SMOOTH);

// Lighting
glEnable(GL_LIGHT1);
GLfloat lightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f };
GLfloat lightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f };
GLfloat lightPosition[]= { 3.0f, 3.0f, -5.0f, 1.0f };
glLightfv(GL_LIGHT1, GL_AMBIENT, lightAmbient);
glLightfv(GL_LIGHT1, GL_DIFFUSE, lightDiffuse);
glLightfv(GL_LIGHT1, GL_POSITION, lightPosition);

// Make sure render at the correct aspect ratio
resizeGL(this->width(), this->height());
  1. 接下来,转到paintGL()函数并添加以下代码:
glEnable(GL_LIGHTING);

// FRONT
glBegin(GL_POLYGON);
  glNormal3f(0.0f, 0.0f, 1.0f);
  glTexCoord2f(0.0f, 0.0f); glVertex3f(0.5, -0.5, -0.5);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(0.5, 0.5, -0.5);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5, 0.5, -0.5);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5, -0.5, -0.5);
glEnd();

// BACK
glBegin(GL_POLYGON);
  glNormal3f(0.0f, 0.0f,-1.0f);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(0.5, -0.5, 0.5);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(0.5, 0.5, 0.5);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5, 0.5, 0.5);
  glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5, -0.5, 0.5);
glEnd();

// RIGHT
glBegin(GL_POLYGON);
  glNormal3f(0.0f, 1.0f, 0.0f);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(0.5, -0.5, -0.5);
  glTexCoord2f(0.0f, 0.0f); glVertex3f(0.5, 0.5, -0.5);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(0.5, 0.5, 0.5);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(0.5, -0.5, 0.5);
glEnd();

// LEFT
glBegin(GL_POLYGON);
  glNormal3f(0.0f,-1.0f, 0.0f);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5, -0.5, 0.5);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5, 0.5, 0.5);
  glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5, 0.5, -0.5);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(-0.5, -0.5, -0.5);
glEnd();

// TOP
glBegin(GL_POLYGON);
  glNormal3f(1.0f, 0.0f, 0.0f);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(0.5, 0.5, 0.5);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(0.5, 0.5, -0.5);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5, 0.5, -0.5);
  glTexCoord2f(0.0f, 0.0f);glVertex3f(-0.5, 0.5, 0.5);
glEnd();

// Red side - BOTTOM
glBegin(GL_POLYGON);
  glNormal3f(-1.0f, 0.0f, 0.0f);
  glTexCoord2f(0.0f, 0.0f); glVertex3f(0.5, -0.5, -0.5);
  glTexCoord2f(1.0f, 0.0f); glVertex3f(0.5, -0.5, 0.5);
  glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5, -0.5, 0.5);
  glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5, -0.5, -0.5);
glEnd();

glDisable(GL_LIGHTING);
  1. 如果现在编译并运行程序,您应该看到照明效果的应用!如何做...

它是如何工作的...

在固定管线中,向场景中添加光源非常容易。首先,我们需要选择 OpenGL 要使用的着色模型。在我们的情况下,我们通过调用glShaderModel(GL_SMOOTH)选择了平滑着色模型。或者,您也可以通过调用glShaderModel(GL_FLAT)选择平面着色模型:

它是如何工作的...

之后,通过调用glEnable(GL_LIGHT1)来启用 OpenGL 中的第一个光源。由于固定管线中允许的光源数量有限,光源的名称都是静态的:GL_LIGHT1GL_LIGHT2GL_LIGHT3等等。接下来,我们创建了三个数组,用于存储环境光的颜色、漫射光的颜色和漫射光的位置。环境光是环境照明,影响整个场景,没有位置。另一方面,漫射光具有位置和光影响区域。然后,我们通过调用glLightfv()函数将这些信息提供给 OpenGL。然后,在paintGL()中,在开始渲染立方体之前,我们必须通过调用glEnable(GL_LIGHTING)来启用照明。如果没有它,你将看不到应用于立方体的任何照明效果。

除此之外,我们还需要为立方体的每个表面添加一个表面法线值。表面法线指示表面朝向何处,并用于光照计算。完成后,不要忘记通过调用glDisable(GL_LIGHTING)来禁用照明。

除了向场景添加光照外,我们还通过调用glTexParameteri()将纹理过滤设置为三线性插值,使纹理看起来更加平滑。您还可以尝试其他两种过滤,点过滤和双线性过滤,只需取消注释代码即可。

以下图像显示了三种不同类型的过滤之间的区别:

它是如何工作的...

双线性和三线性过滤需要 mipmap 才能工作,我们可以通过调用glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE)来要求 OpenGL 生成。Mipmaps 是预先计算的、优化的纹理序列,每个纹理都是同一图像的逐渐降低分辨率的表示。当远离摄像机移动时,OpenGL 会将物体的纹理切换到分辨率较低的 mipmap,这对于避免视觉伪影非常有效。

还有更多…

在 3D 场景中,光照是一个非常重要的方面,它有助于定义物体的 3D 形状。光不仅使面对光的表面变得更亮,而且还使其他被阻挡的表面变暗。

在 OpenGL 中,至少在固定功能管道中,您只能向场景中添加有限数量的灯光。灯光的数量受图形芯片的限制-有些支持多达四个灯光,有些支持多达八个,有些支持多达 16 个。然而,由于固定功能管道正在逐渐被淘汰,人们开始使用可编程管道,这个问题已经得到解决。在可编程管道中,您可以在场景中拥有任意数量的灯光;然而,光照模型将需要完全由您在着色器中编码,这并不是一项容易的任务。

在固定功能管道中,如果要添加的灯光多于图形芯片支持的数量,可以关闭远离摄像机视图的灯光,并只打开靠近摄像机视图的一些灯光。这种方法的缺点是,例如在迷宫中行走时,可能会看到灯光不断闪烁。

使用键盘控制移动对象

在本主题中,我们将学习如何使用键盘控制在 OpenGL 中移动对象。Qt 提供了一种简单的方法来检测键盘事件,即使用虚拟函数keyPressEvent()keyReleaseEvent()。我们将使用之前的示例并进行扩展。

如何做…

  1. 打开mainwindow.h并声明两个名为moveXmoveZ的浮点数:
private:
  QOpenGLContext* context;
  QOpenGLFunctions* openGLFunctions;

  float rotation;
  GLuint texID[1];

 float moveX;
 float moveZ;

  1. 之后,声明keyPressEvent()函数,如下所示:
public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

  void keyPressEvent(QKeyEvent *event);

  1. 然后,打开mainwindow.cpp并设置我们刚刚声明的两个变量的默认值:
MainWindow::MainWindow(QWidget *parent)
{
  setSurfaceType(QWindow::OpenGLSurface);

  QSurfaceFormat format;
  format.setProfile(QSurfaceFormat::CompatibilityProfile);
  format.setVersion(2, 1); // OpenGL 2.1
  setFormat(format);

  context = new QOpenGLContext;
  context->setFormat(format);
  context->create();
  context->makeCurrent(this);

  openGLFunctions = context->functions();

  QTimer *timer = new QTimer(this);
  connect(timer, SIGNAL(timeout()), this, SLOT(updateAnimation()));
  timer->start(100);

  rotation = 0;

 moveX = 0;
 moveZ = 0;
}
  1. 接下来,我们将实现keyPressEvent()函数:
void MainWindow::keyPressEvent(QKeyEvent *event)
{
  if (event->key() == Qt::Key_W)
  {
    moveZ -= 0.2;
  }

  if (event->key() == Qt::Key_S)
  {
    moveZ += 0.2;
  }

  if (event->key() == Qt::Key_A)
  {
    moveX -= 0.2;
  }

  if (event->key() == Qt::Key_D)
  {
    moveX += 0.2;
  }
}
  1. 之后,在绘制 3D 立方体之前调用glTranslatef(),并将moveXmoveZ都放入函数中。此外,我们禁用了旋转,以便更容易看到移动:
// Transformations
glTranslatef(0.0, 0.0, -3.0);
glRotatef(rotation, 1.0, 1.0, 1.0);
glTranslatef(moveX, 0.0, moveZ);

// Texture mapping
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texID[0]);

glEnable(GL_LIGHTING);
  1. 如果现在编译并运行程序,您应该能够通过按WASD来移动立方体:如何做…

它是如何工作的...

基本上,我们在这里所做的是在按下键时添加或减去moveXmoveZ的值。在keyPressEvent()中,我们检查键盘按下的按钮是否是WASD。然后,我们相应地从变量中添加或减去 0.2。要获取 Qt 使用的键名称的完整列表,请访问doc.qt.io/qt-5/qt.html#Key-enum

当我们按住相同的键不放时,Qt 会在一段时间后重复按键事件。键盘输入间隔在不同的操作系统之间有所不同。您可以通过调用QApplication::setKeyboardInterval()来设置间隔,但这可能在每个操作系统中都不起作用。我们在绘制立方体之前调用了glTranslatef(moveX, 0.0, moveZ),这会在按下WASD时移动立方体。

QML 中的 3D 画布

在这个示例中,我们将学习如何使用 Qt 强大的 QML 脚本语言呈现 3D 图像。

如何做…

  1. 让我们通过在 Qt Creator 中创建一个新项目来开始这个示例。这一次,我们将创建Qt Canvas 3D 应用程序,而不是我们在所有先前示例中选择的其他选项:如何做…

  2. 之后,Qt Creator 会询问您是否要创建一个基于three.js的项目。保持选项选中,然后按下一步按钮继续:如何做…

  3. 创建项目后,您会注意到一些 JavaScript(.js)文件已经添加到项目的资源中。这是正常的,因为 Qt Canvas 3D 应用程序使用 JavaScript 和 WebGL 技术在屏幕上呈现 3D 图像。在这种情况下,它正在运行一个基于 WebGL 的渲染库称为 three.js,这使我们的编程工作与编写纯 WebGL 代码相比更简单和更容易:如何做…

  4. 接下来,向我们的项目资源中添加一个图像文件,因为我们将在此示例中使用它。通过在项目窗格中右键单击qml.qrc,然后选择在编辑器中打开,以使用 Qt Creator 打开qml.qrc。一旦 Qt Creator 打开了资源文件,点击添加按钮,然后点击添加文件按钮,然后从计算机中选择要使用的图像文件。在我的情况下,我添加了一个名为bricks.png的图像,它将用作我们的 3D 对象的表面纹理:如何做…

  5. 之后,使用 Qt Creator 打开glcode.js。您会看到文件中已经有大量的代码编写。这基本上是使用three.js库在屏幕上渲染一个简单的 3D 立方体。您可以立即构建项目并运行它,看看它的样子。但是,我们将稍微更改代码以自定义其输出。

  6. initializeGL()函数中,我们将向场景添加一个定向光,加载刚刚添加到项目资源中的纹理文件,然后将纹理应用于定义 3D 立方体表面属性的材质。此外,我们将通过将其在所有维度上的比例设置为3,使立方体的比例略微变大:

function initializeGL(canvas) {
  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);
  camera.position.z = 5;

 var directionalLight = new THREE.DirectionalLight(0xffffff);
 directionalLight.position.set(1, 1, 1).normalize();
 scene.add(directionalLight);

 var texture = THREE.ImageUtils.loadTexture('bricks.jpg');

 var material = new THREE.MeshBasicMaterial({ map: texture });
 var cubeGeometry = new THREE.BoxGeometry(3, 3, 3);
  cube = new THREE.Mesh(cubeGeometry, material);
  cube.rotation.set(0.785, 0.785, 0.0);
  scene.add(cube);

  renderer = new THREE.Canvas3DRenderer(
    { canvas: canvas, antialias: true, devicePixelRatio: canvas.devicePixelRatio });
  renderer.setSize(canvas.width, canvas.height);
}
  1. 然后,在paintGL()函数中,添加一行额外的代码来在渲染场景之前旋转 3D 立方体:
function paintGL(canvas) {
  cube.rotation.y -= 0.005;
  renderer.render(scene, camera);
}
  1. 我个人觉得窗口大小有点太大,所以我还在main.qml文件中更改了窗口的宽度和高度:
import QtQuick 2.4
import QtCanvas3D 1.0
import QtQuick.Window 2.2
import "glcode.js" as GLCode

Window {
  title: qsTr("Qt_Canvas_3D")
 width: 480
 height: 320
  visible: true

  Canvas3D {
    id: canvas3d
    anchors.fill: parent
    focus: true

    onInitializeGL: {
      GLCode.initializeGL(canvas3d);
    }

    onPaintGL: {
      GLCode.paintGL(canvas3d);
    }

    onResizeGL: {
      GLCode.resizeGL(canvas3d);
    }
  }
}
  1. 完成后,让我们构建并运行项目。您应该能够在屏幕上看到一个带有砖纹理的 3D 立方体,缓慢旋转:如何做…

工作原理...

最初,three.js是一个跨浏览器的 JavaScript 库/ API,它使用 WebGL 技术在 Web 浏览器中显示动画的 3D 计算机图形。然而,Qt Canvas 3D 也使用 Web 技术,特别是 WebGL 技术,来呈现 3D 图像,就像在 Web 浏览器上一样。这意味着不仅three.js在 Qt Canvas 3D 上受到支持,而且所有基于 WebGL 技术的不同类型的库都将在 Qt Canvas 3D 上无缝运行。但是,Qt Canvas 3D 仅适用于基于 QML 的项目,不适用于 C ++。

注意

如果您有兴趣了解更多关于three.js的信息,请访问他们的网站threejs.org

第五章:使用 Qt5 构建触摸屏应用程序

在本章中,我们将涵盖以下内容:

  • 为移动应用程序设置 Qt

  • 使用 QML 设计基本用户界面

  • 触摸事件

  • QML 中的动画

  • 使用模型视图显示信息

  • 集成 QML 和 C++

介绍

Qt 不仅是 PC 平台的跨平台软件开发工具包,还支持 iOS 和 Android 等移动平台。Qt 的开发人员在 2010 年推出了 Qt Quick,它提供了一种简单的方式来构建高度动态的自定义用户界面,用户可以轻松地通过最少的编码创建流畅的过渡和效果。Qt Quick 使用一种称为QML的声明性脚本语言,类似于 Web 开发中使用的 JavaScript 语言。高级用户还可以在 C++中创建自定义函数,并将其移植到 Qt Quick 中以增强其功能。目前,Qt Quick 支持 Windows、Linux、Mac、iOS 和 Android 等多个平台。

为移动应用程序设置 Qt

在这个例子中,我们将学习如何在 Qt Quick 中设置我们的 Qt 项目,并使其能够构建和导出到移动设备。

操作步骤...

  1. 首先,让我们通过转到文件 | 新建文件或新建项目来创建一个新项目。然后,将弹出一个窗口供您选择项目模板。选择Qt Quick 应用程序并单击选择按钮:操作步骤...

  2. 之后,插入项目名称并选择项目位置。单击下一步按钮,它将要求您选择项目所需的最低 Qt 版本。请确保选择计算机上存在的版本,否则您将无法正确运行它。完成后,单击下一步按钮继续。

  3. 然后,Qt Creator 将询问您要为项目使用哪个工具。这些“工具”基本上是您可以用来为不同平台编译项目的不同编译器。由于我们正在为移动平台开发应用程序,因此我们将启用 Android 工具(或者如果您使用 Mac,则启用 iOS 工具)以构建和导出应用程序到移动设备。请注意,如果您首次使用 Android 工具,则需要配置它,以便 Qt 可以找到 Android SDK 的目录。完成后,单击下一步操作步骤...

  4. 创建项目后,Qt Creator 将自动打开项目中的一个文件,名为main.qml。您将在屏幕上看到类似于这样的东西,与您通常的 C/C++项目非常不同:

import QtQuick 2.3
import QtQuick.Window 2.2

Window {
  visible: true

  MouseArea {
    anchors.fill: parent
    onClicked: {
      Qt.quit();
    }
  }

  Text {
    text: qsTr("Hello World")
    anchors.centerIn: parent
  }
}
  1. 现在通过单击 Qt Creator 左下角的绿色箭头按钮构建和运行项目。如果将默认工具设置为桌面,将弹出一个窗口,看起来像这样:操作步骤...

  2. 我们可以通过转到项目界面并选择要使用的工具来在不同的工具之间切换。您还可以管理计算机上所有可用的工具,或者从项目界面向项目添加新的工具:操作步骤...

  3. 如果这是您第一次构建和运行项目,您需要在构建设置下为 Android 工具创建一个模板。一旦单击了创建模板按钮,Qt 将生成运行应用程序所需的所有文件。如果您不打算在项目中使用 Gradle,请禁用将 Gradle 文件复制到 Android 目录选项。否则,在尝试编译和部署应用程序到移动设备时可能会遇到问题:操作步骤...

  4. 创建模板后,单击运行按钮,现在您应该看到一个弹出窗口,询问应该导出到哪个设备:操作步骤...

  5. 选择当前连接到计算机的设备,然后按确定按钮。等待一会儿,直到项目构建完成,然后您应该在移动设备上看到类似于这样的东西:如何操作...

工作原理...

Qt Quick 应用程序项目与表单应用程序项目有很大不同。您将大部分时间编写 QML 脚本,而不是编写 C/C++代码。

构建和导出应用程序到 Android 平台需要Android 软件开发工具包SDK)、Android 本机开发工具包NDK)、Java 开发工具包JDK)和Apache Ant。或者,您也可以使用 Gradle 代替 Apache Ant 来构建您的 Android 工具包。您只需要启用使用 Gradle 代替 Ant选项,并提供 Qt Gradle 的安装路径。请注意,Android Studio 目前不受 Qt Creator 支持:

工作原理...

如果您在 Android 设备上运行应用程序,请确保已启用 USB 调试模式。要启用 USB 调试模式,您需要先在 Android 设备上启用开发者选项,方法是转到设置 | 关于手机,然后点击版本号七次。之后,转到设置 | 开发者选项,您将在菜单中看到Android 调试选项。启用该选项后,您现在可以将应用程序导出到设备进行测试。

要构建 iOS 平台,您需要在 Mac 上运行 Qt Creator,并确保最新的 XCode 也安装在您的 Mac 上。

要在 iOS 设备上测试您的应用程序,您需要在 Apple 注册开发者帐户,在开发者门户注册您的设备,并将配置文件安装到 XCode 中,这比 Android 要棘手得多。一旦您从 Apple 获得了开发者帐户,您将获得访问开发者门户的权限。

使用 QML 设计基本用户界面

在这个例子中,我们将学习如何使用 Qt Quick Designer 来设计程序的用户界面。

如何操作...

  1. 首先,创建一个新的 Qt Quick 应用程序项目,就像我们在之前的示例中所做的那样。如果愿意,您也可以使用之前的项目文件。

  2. 您将在项目资源中看到两个 QML 文件——main.qmlMainForm.ui.qml。前者是我们实现应用程序逻辑的地方,后者是我们设计用户界面的地方。我们将从 UI 设计开始,所以让我们打开MainForm.ui.qml。一旦被 Qt Creator 打开,您将看到一个与我们在之前章节中使用的完全不同的 UI 编辑器。这个编辑器称为 Qt Quick Designer,专门用于设计 Qt Quick 项目的 UI。该编辑器的组件描述如下:

  • 窗口显示了您可以添加到 UI 画布的所有预定义的 QML 类型。您还可以从导入选项卡导入自定义的 Qt Quick 组件并在此处显示它们。

  • 导航器导航器窗口以树形结构显示当前 QML 文件中的项目。

  • 连接:您可以使用连接窗口中提供的工具将对象连接到信号,为对象指定动态属性,并在两个对象的属性之间创建绑定。

  • 状态状态窗口显示了项目的不同状态。您可以通过单击状态窗口右侧的+按钮为项目添加新状态。

  • 画布:画布是您设计程序用户界面的地方。您可以从窗口将 Qt Quick 组件拖放到画布上,并立即看到它在程序中的外观。

  • 属性:这是您更改所选项目属性的地方。

  1. 导航器窗口下选择矩形对象下的所有内容(mouseAreaText)并删除它们。

  2. 我们将制作一个简单的登录界面。从窗口中,将两个文本小部件拖放到画布上。

  3. 将两个文本小部件的文本属性设置为用户名:密码:操作步骤…

  4. 窗口中拖动两个矩形到画布上,然后将两个文本输入小部件拖到画布上,并将它们各自作为父级添加到刚刚添加到画布上的矩形上。将矩形的border属性设置为1radius设置为5。然后,将一个文本字段的echo mode设置为Password

  5. 现在,我们将通过将鼠标区域小部件与矩形和文本小部件组合来手动创建一个按钮小部件。将鼠标区域小部件拖到画布上,然后将矩形和文本小部件拖到画布上,并将它们都作为父级添加到鼠标区域上。将矩形的颜色设置为#bdbdbd,然后将其border属性设置为1radius设置为5。然后,将文本设置为登录,并确保鼠标区域的大小与矩形相同。

  6. 之后,将另一个矩形拖到画布上,作为登录表单的容器,使其看起来整洁。将其border color设置为#5e5858border属性设置为2。然后,将其radius属性设置为5,使其角看起来有点圆润。

  7. 确保我们在上一步中添加的矩形在导航器窗口中的层次结构顶部,以便它出现在所有其他小部件后面。您可以通过按位于导航器窗口顶部的箭头按钮来排列层次结构中的小部件位置:操作步骤…

  8. 接下来,我们将导出三个小部件——鼠标区域和两个文本输入小部件——作为根项目的别名属性,以便以后可以从main.qml文件中访问这些小部件。通过单击小部件名称后面的小图标,并确保图标变为On状态来导出小部件:操作步骤…

  9. 到目前为止,您的 UI 应该看起来像这样:操作步骤…

  10. 现在让我们打开main.qml。Qt Creator 默认情况下不会在 Qt Quick Designer 中打开此文件,而是会在脚本编辑器中打开。这是因为所有与 UI 设计相关的任务都是在MainForm.ui.qml中完成的,而main.qml仅用于定义将应用于 UI 的逻辑和函数。但是,您可以通过单击编辑器左侧边栏上的Design按钮,使用 Qt Quick Designer 打开它以预览 UI。

  11. 在脚本顶部,添加第三行以导入对话框模块到main.qml,如下所示:

import QtQuick 2.5
import QtQuick.Window 2.2
import QtQuick.Dialogs 1.2
  1. 接下来,用以下代码替换下面的代码:
Window {
  visible: true
  width: 360
  height: 360

  MainForm {
    anchors.fill: parent
    loginButton.onClicked: {
      messageDialog.text = "Username is " + userInput.text + " and password is " + passInput.text
        messageDialog.visible = true
    }
  }

  MessageDialog {
    id: messageDialog
    title: "Fake login"
    text: ""
    onAccepted: {
      console.log("You have clicked the login button")
      Qt.quit()
    }
  }
}
  1. 在 PC 上构建并运行此程序,当您单击登录按钮时,应该会显示一个消息框的简单程序:操作步骤…

工作原理…

自 Qt 5.4 以来,引入了一个名为.ui.qml的新文件扩展名。QML 引擎处理它就像处理普通的.qml文件一样,但禁止在其中编写任何逻辑实现。它用作 UI 定义模板,可以在不同的.qml文件中重用。UI 定义和逻辑实现的分离改善了 QML 代码的可维护性,并创建了更好的工作流程。

Qt Quick - 基本下的所有小部件都是我们可以使用来混合和匹配并创建新类型小部件的最基本小部件。在前面的示例中,我们学习了如何将三个小部件组合在一起——文本、鼠标区域和矩形,以形成一个按钮小部件。

然而,如果您懒得做,可以通过转到窗口中的导入选项卡并单击<添加导入>按钮,将预制模块导入到您的 Qt Quick 项目中。然后,从下拉列表中选择要添加到项目中的模块。一旦您在 QML 脚本和 C++编程方面有所进步,还可以创建自己的 Qt Quick 模块:

工作原理…

我们在main.qml中导入了QtQuick.dialogs模块,并创建了一个消息框,当用户按下登录按钮时显示用户填写的用户名和密码,以证明 UI 功能正在工作。如果小部件没有从MainForm.ui.qml中导出,我们将无法在main.qml中访问其属性。

在这一点上,我们可以将程序导出到 iOS 和 Android,但是在一些具有更高分辨率或更高每英寸像素密度DPI)单位的设备上,UI 可能看起来不准确。我们将在本章后面解决这个问题。

触摸事件

在这一部分,我们将学习如何使用 Qt Quick 开发在移动设备上运行的触摸驱动应用程序。

如何做…

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

  2. 在 Qt Creator 中,右键单击qml.qrc,然后选择在编辑器中打开。然后,单击添加 | 添加文件,将tux.png添加到项目中:如何做…

  3. 接下来,打开MainForm.ui.qml。从窗口将图像小部件拖动到画布上。然后,将图像的源设置为tux.png,并将其fillmode设置为PreserveAspectFit。之后,将其width设置为200,将其height设置为220

  4. 确保鼠标区域小部件和图像小部件都通过单击其各自的小部件名称旁边的小图标作为根项目的别名属性导出。

  5. 在那之后,通过单击编辑器左侧边栏上的编辑按钮切换到脚本编辑器。我们需要将鼠标区域小部件更改为多点触摸区域小部件,如下所示:

MultiPointTouchArea {
  id: touchArea
  anchors.fill: parent
  touchPoints: [
    TouchPoint { id: point1 },
    TouchPoint { id: point2 }
  ]
}
  1. 我们还将Image小部件设置为默认自动放置在窗口中心:
Image {
  id: tux
  x: (window.width / 2) - (tux.width / 2)
  y: (window.height / 2) - (tux.height / 2)
  width: 200
  height: 220
  fillMode: Image.PreserveAspectFit
  source: "tux.png"
}

最终的 UI 应该看起来像这样:

如何做…

  1. 完成后,让我们打开main.qml。首先清除MainForm对象中除anchors.fill: parent之外的所有内容,如下所示:
import QtQuick 2.5
import QtQuick.Window 2.2

Window {
  visible: true

  MainForm {
    anchors.fill: parent
  }
}
  1. MainForm对象中声明几个变量,这些变量将用于重新调整图像小部件。如果您想了解以下代码中使用的property关键字的更多信息,请查看本示例末尾的还有更多…部分:
property int prevPointX: 0
property int prevPointY: 0
property int curPointX: 0
property int curPointY: 0

property int prevDistX: 0
property int prevDistY: 0
property int curDistX: 0
property int curDistY: 0

property int tuxWidth: tux.width
property int tuxHeight: tux.height
  1. 接下来,我们将定义当手指触摸多点区域小部件时会发生什么。在这种情况下,如果多个手指触摸到多点触摸区域,我们将保存第一个和第二个触摸点的位置。我们还保存图像小部件的宽度和高度,以便以后可以使用这些变量来计算手指开始移动时图像的比例:
touchArea.onPressed:
{
  if (touchArea.touchPoints[1].pressed)
  {
    if (touchArea.touchPoints[1].x < touchArea.touchPoints[0].x)
      prevDistX = touchArea.touchPoints[1].x - touchArea.touchPoints[0].x
    else
      prevDistX = touchArea.touchPoints[0].x - touchArea.touchPoints[1].x

    if (touchArea.touchPoints[1].y < touchArea.touchPoints[0].y)
      prevDistY = touchArea.touchPoints[1].y - touchArea.touchPoints[0].y
    else
      prevDistY = touchArea.touchPoints[0].y - touchArea.touchPoints[1].y

    tuxWidth = tux.width
    tuxHeight = tux.height
  }
}

以下图像显示了当两根手指触摸屏幕时,在触摸区域边界内注册的触摸点的示例。touchArea.touchPoints[0]是第一个注册的触摸点,touchArea.touchPoints[1]是第二个。然后我们计算两个触摸点之间的 X 和 Y 距离,并将它们保存为prevDistXprevDistY

如何做…

  1. 在那之后,我们将定义当我们的手指在屏幕上移动时会发生什么,同时仍然保持与触摸区域的边界接触。在这一点上,我们将使用我们在上一步中保存的变量来计算图像的比例。同时,如果我们检测到只有一个触摸点,那么我们将移动图像而不是改变其比例:
touchArea.onUpdated:{
  if (!touchArea.touchPoints[1].pressed)
  {
    tux.x += touchArea.touchPoints[0].x - touchArea.touchPoints[0].previousX
    tux.y += touchArea.touchPoints[0].y - touchArea.touchPoints[0].previousY
  }
  else
  {
    if (touchArea.touchPoints[1].x < touchArea.touchPoints[0].x)
    curDistX = touchArea.touchPoints[1].x - touchArea.touchPoints[0].x
    else
      curDistX = touchArea.touchPoints[0].x - touchArea.touchPoints[1].x

    if (touchArea.touchPoints[1].y < touchArea.touchPoints[0].y)
      curDistY = touchArea.touchPoints[1].y - touchArea.touchPoints[0].y
    else
      curDistY = touchArea.touchPoints[0].y - touchArea.touchPoints[1].y

      tux.width = tuxWidth + prevDistX - curDistX
      tux.height = tuxHeight + prevDistY - curDistY
  }
}

以下图像显示了移动触摸点的示例 - touchArea.touchPoints[0]从点 A 移动到点 B,而touchArea.touchPoints[1]从点 C 移动到点 D。然后,我们可以通过查看先前的 X、Y 变量与当前变量之间的差异来确定触摸点移动了多少单位:

如何做…

  1. 现在,您可以构建并将程序导出到您的移动设备上。您将无法在不支持多点触摸的平台上测试此程序。一旦程序在支持多点触摸的移动设备(或支持多点触摸的台式机/笔记本电脑)上运行,请尝试两件事:只在屏幕上放一个手指并移动它,以及在屏幕上放两个手指并朝相反方向移动它们。您应该看到的是,如果您只使用一个手指,企鹅将被移动到另一个位置,如果您使用两个手指,它将被放大或缩小:如何做…

它是如何工作的…

当手指触摸设备的屏幕时,多点触摸区域小部件会触发onPressed事件,并在内部数组中注册每个触摸点的位置。我们可以通过告诉 Qt 要访问的触摸点来获取这些数据。第一个触摸点将带有索引号0,第二个触摸点将是1,依此类推。然后,我们将这些数据保存到变量中,以便以后可以检索它们以计算企鹅图像的缩放。

当一个或多个手指在移动时仍然与屏幕保持接触,多点触摸区域将触发onUpdate事件。然后,我们将检查触摸点的数量 - 如果只找到一个触摸点,我们将根据手指移动的距离移动企鹅图像。如果有多个触摸点,我们将比较两个触摸点之间的距离,并将其与我们之前保存的变量进行比较,以确定我们应该如何重新调整图像的大小。

它是如何工作的…

我们还必须检查第一个触摸点是否在第二个触摸点的左侧还是右侧。这样我们就可以防止图像在手指移动的反方向上被缩放,并产生不准确的结果。

至于企鹅的移动,我们将只获取当前触摸位置和上一个触摸位置之间的差异,将其添加到企鹅的坐标中,然后完成。单点触摸事件通常比多点触摸事件简单得多,更直接。

还有更多…

在 Qt Quick 中,所有组件都具有内置属性,如宽度、高度、颜色等,默认情况下附加到组件上。但是,Qt Quick 还允许您创建自己的自定义属性,并将其附加到您在 QML 脚本中声明的组件上。对象类型的自定义属性可以通过在 QML 文档中的对象声明之前添加property关键字来定义,例如:

property int myValue;

您还可以使用冒号(:)将自定义属性绑定到值,如下所示:

property int myValue: 100;

要了解 Qt Quick 支持的属性类型的更多信息,请查看此链接:doc.qt.io/qt-5/qtqml-typesystem-basictypes.html

QML 中的动画

Qt 允许我们在不编写大量代码的情况下轻松地为 UI 组件添加动画。在这个例子中,我们将学习如何通过应用动画使我们程序的 UI 更有趣。

如何做…

  1. 我们将再次从头开始。因此,在 Qt Creator 中创建一个新的 Qt Quick 应用程序项目,并打开MainForm.ui.qml

  2. 转到窗口中的导入选项卡,并将一个名为QtQuick.Controls的 Qt Quick 模块添加到您的项目中。

  3. 之后,您将在QML 类型选项卡中看到一个名为Qt Quick - Controls的新类别,其中包含许多可以放置在画布上的新小部件。

  4. 接下来,将三个按钮小部件拖到画布上,并将它们的高度设置为45。然后,转到属性窗口上的布局选项卡,并为所有三个按钮小部件启用左右锚点。确保锚点的目标设置为父级,边距保持为0。这将使按钮根据主窗口的宽度水平调整大小。之后,将第一个按钮的y值设置为0,第二个为45,第三个为90。UI 现在应该是这样的:操作步骤…

  5. 现在,用编辑器打开qml.qrc并将fan.png添加到项目中:操作步骤…

  6. 然后,在画布上添加两个鼠标区域小部件。之后,在画布上拖动一个矩形小部件和一个图像小部件。将矩形和图像作为父级部件添加到我们刚刚添加的鼠标区域上。

  7. 将矩形的颜色设置为#0000ff,并将fan.png应用到图像小部件。您的 UI 现在应该是这样的:操作步骤…

  8. 然后,通过单击小部件名称右侧的图标,将MainForm.ui.qml中的所有小部件导出为根项目的别名属性:

  9. 接下来,我们将为 UI 应用动画和逻辑,但我们不会在MainForm.ui.qml中进行。相反,我们将在main.qml中完成所有操作。

  10. main.qml中,删除鼠标区域的默认代码,并为窗口添加宽度和高度,以便我们有更多的空间进行预览:

import QtQuick 2.5
import QtQuick.Window 2.2

Window {
  visible: true
  width: 480
  height: 550

  MainForm {
    anchors.fill: parent
  }
}
  1. 之后,在MainForm小部件中添加定义按钮行为的代码:
button1 {
  Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } }

  onClicked: {
    button1.y = button1.y + (45 * 3)
  }
}

button2 {
  Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } }

  onClicked: {
    button2.y = button2.y + (45 * 3)
  }
}

button3 {
  Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } }

  onClicked: {
    button3.y = button3.y + (45 * 3)
  }
}
  1. 然后,按照fan图像和其附加的鼠标区域小部件的行为:
fan {
  RotationAnimation on rotation {
    id: anim01
    loops: Animation.Infinite
    from: 0
    to: -360
    duration: 1000
  }
}

mouseArea1 {
  onPressed: {
    if (anim01.paused)
      anim01.resume()
    else
      anim01.pause()
  }
}
  1. 最后但并非最不重要的是,添加矩形和鼠标区域小部件的行为:操作步骤…

  2. 如果现在编译并运行程序,您应该会看到窗口顶部有三个按钮,底部左侧有一个移动的矩形,底部右侧有一个旋转的风扇。如果您点击任何按钮,它们将以流畅的动画略微向下移动。如果您点击矩形,它将从蓝色变为红色。同时,如果您在风扇图像正在动画时点击它,它将暂停动画,如果再次点击它,它将恢复动画:操作步骤…

工作原理

Qt 的 C++版本支持的大多数动画元素,如过渡、顺序动画、并行动画等,在 Qt Quick 中也是可用的。如果您熟悉 C++中的 Qt 动画框架,您应该能够很容易地掌握这个。

在这个例子中,我们为所有三个按钮添加了一个弹簧动画元素,它专门跟踪它们各自的y轴。如果 Qt 检测到y值已经改变,小部件将不会立即跳到新位置,而是进行插值运算,沿着画布移动,并在到达目的地时执行一些摇晃动画,模拟弹簧效果。我们只需写一行代码,剩下的交给 Qt 处理。

至于风扇图像,我们为其添加了旋转动画元素,并将持续时间设置为1000毫秒,这意味着它将在一秒内完成一次完整的旋转。我们还设置它循环播放动画。当我们点击它附加的鼠标区域小部件时,我们只需调用pause()resume()来启用或禁用动画。

接下来,对于矩形小部件,我们为其添加了两个状态,一个称为BLUE,一个称为RED,每个状态都带有一个在状态改变时将应用于矩形的颜色属性。同时,我们为矩形附加的鼠标区域小部件添加了一个顺序动画组,并向组中添加了两个属性动画元素。您还可以混合不同类型的组动画;Qt 可以很好地处理这一点。

使用模型视图显示信息

Qt 包括一个模型视图框架,它保持数据组织和管理方式与向用户呈现方式之间的分离。在本节中,我们将学习如何利用模型视图,特别是通过使用列表视图来显示信息,并同时应用我们自己的定制使其看起来漂亮。

操作步骤…

  1. 创建一个新的 Qt Quick 应用程序项目,并用 Qt Creator 打开qml.qrc。将六个图像home.pngmap.pngprofile.pngsearch.pngsettings.pngarrow.png添加到项目中:操作步骤…

  2. 之后,打开MainForm.ui.qml。删除画布上的所有默认小部件,并从库窗口的Qt Quick - Views类别下拖动一个列表视图小部件到画布上。然后,通过单击布局窗口中间的按钮,将其锚点设置为填充父级大小操作步骤…

  3. 接下来,切换到脚本编辑器,因为我们将定义列表视图的外观:操作步骤…操作步骤…

  4. 之后,打开main.qml并用以下代码替换原代码:

import QtQuick 2.4
import QtQuick.Window 2.2

Window {
  visible: true
  width: 480
  height: 480

  MainForm {
    anchors.fill: parent

    MouseArea {
      onPressed: row1.opacity = 0.5
      onReleased: row1.opacity = 1.0
    }
  }
}
  1. 构建并运行程序,现在您的程序应该是这个样子:操作步骤…

工作原理…

Qt Quick 允许我们轻松定制列表视图的每一行外观。delegate定义了每一行的外观,而model是您存储在列表视图上显示的数据的地方。

在这个例子中,我们在每一行上添加了一个渐变的背景,然后我们还在项目的两侧添加了一个图标、一个标题、一个描述,以及一个鼠标区域小部件,使列表视图的每一行都可以点击。委托是不静态的,因为我们允许模型更改标题、描述和图标,使每一行看起来都是独特的。

main.qml中,我们定义了鼠标区域小部件的行为,当按下时,它的不透明度值会降低一半,释放时会恢复完全不透明。由于所有其他元素,如标题、图标等,都是鼠标区域小部件的子元素,它们也会自动遵循其父小部件的行为,并变得半透明。

此外,我们终于解决了移动设备高分辨率和 DPI 显示问题。这是一个非常简单的技巧——首先,我们定义了一个名为sizeMultiplier的变量。sizeMultiplier的值是将窗口宽度除以预定义值(例如480)的结果,这是我们用于 PC 的当前窗口宽度。然后,将sizeMultiplier乘以所有与大小和位置有关的小部件变量,包括字体大小。请注意,在这种情况下,应该使用文本的pixelSize属性而不是pointSize,这样当乘以sizeMultiplier时,您将获得正确的显示。以下截图显示了在移动设备上使用和不使用sizeMultiplier时应用的样子:

工作原理…

请注意,一旦您用sizeMultiplier变量乘以所有东西,您可能会在编辑器中得到一个混乱的 UI。这是因为编辑器中的width变量可能返回为0。因此,将0乘以480,您可能会得到结果0,这会使整个 UI 看起来很滑稽。然而,在运行实际程序时,它会看起来很好。如果您想在编辑器中预览 UI,请将sizeMultiplier临时设置为1

集成 QML 和 C++

Qt 支持在 C++类和 QML 引擎之间进行桥接。这种组合允许开发人员充分利用 QML 的简单性和 C++的灵活性。您甚至可以集成来自外部库的 Qt 不支持的功能,然后将生成的数据传递给 Qt Quick 以在 UI 中显示。在这个例子中,我们将学习如何将我们的 UI 组件从 QML 导出到 C++框架,并在显示在屏幕上之前操纵它们的属性。

如何做…

  1. 我们将再次从头开始。因此,在 Qt Creator 中创建一个新的 Qt Quick 应用程序项目,并打开MainForm.ui.qml

  2. 我们可以保留鼠标区域和文本小部件,但将文本小部件放在窗口底部。将文本小部件的文本属性更改为使用 C++更改此文本,并将其字体大小设置为18。之后,转到布局选项卡,并启用垂直中心锚点水平中心锚点,以确保它始终位于窗口中间的某个位置,无论如何重新调整窗口。将垂直中心锚点边距设置为120如何做…

  3. 接下来,从窗口将Rectangle小部件拖动到画布上,并将其颜色设置为#ff0d0d。将其宽度高度设置为200,并启用垂直和水平中心锚点。之后,将水平中心锚点的边距设置为-14。您的 UI 现在应该看起来像这样:如何做…

  4. 完成后,在 Qt Creator 中右键单击项目目录,然后选择添加新内容。然后,将弹出一个窗口,让您选择文件模板。选择C++类并按选择…。之后,它将要求您填写类的信息来定义 C++类。在这种情况下,在类名字段中插入MyClass,并选择QObject作为基类。然后,确保包括 QObject选项已被选中,现在可以单击下一步按钮,然后单击完成按钮。现在将创建并添加两个文件—myclass.hmyclass.cpp—到您的项目中:如何做…

  5. 现在,打开myclass.h并在类构造函数下方添加变量和函数,如下所示:

#ifndef MYCLASS_H
#define MYCLASS_H
#include <QObject>

class MyClass : public QObject
{
  Q_OBJECT
  public:
    explicit MyClass(QObject *parent = 0);

    // Object pointer
    QObject* myObject;

    // Must call Q_INVOKABLE so that this function can be used in QML
    Q_INVOKABLE void setMyObject(QObject* obj);

  signals:

  public slots:
};

#endif // MYCLASS_H
  1. 之后,打开myclass.cpp并定义setMyObject()函数:
#include "myclass.h"

MyClass::MyClass(QObject *parent) : QObject(parent)
{
}

void MyClass::setMyObject(QObject* obj)
{
  // Set the object pointer
  myObject = obj;
}
  1. 现在可以关闭myclass.cpp并打开main.qml。在文件顶部添加第三行,导入我们刚在 C++中创建的自定义库:
import QtQuick 2.4
import QtQuick.Window 2.2
import MyClassLib 1.0
  1. 然后,在Window对象中定义MyClass并在MainForm对象中调用其函数setMyObject(),如下所示:
Window {
  visible: true
  width: 480
  height: 320

  MyClass
  {
    id: myclass
  }

  MainForm {
    anchors.fill: parent
    mouseArea.onClicked: {
      Qt.quit();
    }
    Component.onCompleted:       myclass.setMyObject(messageText);
  }
}
  1. 最后,打开main.cpp并将自定义类注册到 QML 引擎。我们还在这里使用 C++代码更改文本小部件和矩形的属性:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include <QQuickView>
#include <QQuickItem>
#include <QQuickView>
#include "myclass.h"

int main(int argc, char *argv[])
{
  // Register your class to QML
  qmlRegisterType<MyClass>("MyClassLib", 1, 0, "MyClass");

  QGuiApplication app(argc, argv);

  QQmlApplicationEngine engine;
  engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

  QObject* root = engine.rootObjects().value(0);

  QObject* messageText =     root->findChild<QObject*>("messageText");
  messageText->setProperty("text", QVariant("C++ is now in     control!"));
  messageText->setProperty("color", QVariant("green"));

  QObject* square = root->findChild<QObject*>("square");
  square->setProperty("color", QVariant("blue"));

  return app.exec();
}
  1. 现在构建和运行程序,您应该看到矩形和文本的颜色与您在 Qt Quick 中定义的完全不同。这是因为它们的属性已被 C++代码更改:如何做…

它是如何工作的…

QML 旨在通过 C++代码轻松扩展。Qt QML 模块中的类使 QML 对象可以从 C++中加载和操作。

只有从QObject基类继承的类才能与 QML 集成,因为它是 Qt 生态系统的一部分。一旦类已经在 QML 引擎中注册,我们就可以从 QML 引擎获取根项目,并使用它来查找我们想要操作的对象。之后,使用setProperty()函数来更改小部件的任何属性。

请注意,Q_INVOKABLE宏在您打算在 QML 中调用的函数前是必需的。没有它,Qt 不会将函数暴露给 Qt Quick,您将无法调用它。

第六章:XML 解析变得容易

在本章中,我们将涵盖以下内容:

  • 使用流读取器处理 XML 数据

  • 使用流写入器写入 XML 数据

  • 使用 QDomDocument 类处理 XML 数据

  • 使用 QDomDocument 类写入 XML 数据

  • 使用 Google 的地理编码 API

介绍

XML 是一种名为可扩展标记语言的文件格式的文件扩展名,用于以结构化格式存储信息。XML 格式广泛用于 Web 以及其他应用程序。例如,HTML 是用于创建网页的文件格式,基于 XML 格式。从 Microsoft Office 2007 开始,Microsoft Office 使用基于 XML 的文件格式,如.docx.xlsx.pptx等。

使用流读取器处理 XML 数据

在本节中,我们将学习如何处理从 XML 文件中提取的数据,并使用流读取器进行提取。

操作方法…

让我们按照以下步骤创建一个简单的程序,通过读取和处理 XML 文件:

  1. 像往常一样,在你想要的位置创建一个新的Qt Widgets 应用程序项目。

  2. 接下来,打开任何文本编辑器,创建一个看起来像下面这样的 XML 文件,然后将其保存为scene.xml

<?xml version="1.0" encoding="UTF-8"?> 
<scene>
  <object tag="building">
    <name>Library</name>
    <position>120.0,0.0,50.68</position>
    <rotation>0.0,0.0,0.0</rotation>
    <scale>1.0,1.0,1.0</scale>
  </object>
  <object tag="building">
    <name>Town Hall</name>
    <position>80.2,0.0,20.5</position>
    <rotation>0.0,0.0,0.0</rotation>
    <scale>1.0,1.0,1.0</scale>
  </object>
  <object tag="prop">
    <name>Tree</name>
    <position>10.46,-0.2,80.2</position>
    <rotation>0.0,0.0,0.0</rotation>
    <scale>1.0,1.0,1.0</scale>
  </object>
</scene>
  1. 接下来,返回到 Qt Creator 并打开mainwindow.h。在脚本顶部添加以下头文件,就在#include <QMainWindow>之后:
#include <QXmlStreamReader>
#include <QDebug>
#include <QFile>
#include <QFileDialog>
  1. 然后,打开mainwindow.ui,从左侧的小部件框中拖动一个Push Button到 UI 编辑器中。将按钮的对象名称更改为loadXmlButton,显示文本更改为加载 XML操作方法…

  2. 之后,右键单击按钮,选择转到槽…。一个窗口将弹出,显示可供选择的信号列表。

  3. 选择默认的clicked()选项,然后按确定按钮。Qt 现在会在你的头文件和源文件中插入一个名为on_loadXmlButton_clicked()的槽函数。

  4. 现在,将以下代码添加到on_loadXmlButton_clicked()函数中:操作方法…

  5. 现在构建并运行项目,你会看到一个弹出的窗口,看起来就像你在第 4 步中制作的窗口:操作方法…

  6. 点击加载 XML按钮,你会看到文件选择窗口弹出在屏幕上。选择在第 2 步中创建的 XML 文件,然后按选择按钮。之后,你应该在 Qt Creator 的应用程序输出窗口中看到以下调试文本,这表明程序已成功从你刚刚选择的 XML 文件中加载了数据:操作方法…

工作原理…

在这个例子中,我们要做的是使用QXmlStreamReader类从 XML 文件中提取和处理数据。想象一下,你正在制作一个电脑游戏,你正在使用 XML 文件来存储游戏场景中所有对象的属性。在这种情况下,XML 格式在以结构化方式存储数据方面发挥着重要作用,这使得数据的提取变得容易。

首先,我们需要在源文件中添加与 XML 相关的类的头文件,这种情况下是QXmlStreamReader类。QXmlStreamReader内置在 Qt 的核心库中,因此不需要使用任何附加模块,这也意味着它是在 Qt 中处理 XML 数据的推荐类。

一旦我们点击了加载 XML按钮,on_loadXmlButton_clicked()槽将被调用;这是我们编写处理 XML 数据的代码的地方。

首先,我们使用文件对话框来选择要处理的 XML 文件。然后,将所选文件的文件名和路径发送到QFile类中,以打开和读取 XML 文件的文本数据。之后,文件的数据被发送到QXmlStreamReader类进行处理。

我们使用 while 循环来读取整个 XML 文件,并检查流读取器处理的每个元素。我们确定元素是开始元素还是结束元素。如果是开始元素,我们将检查元素的名称,以确定元素是否应包含我们需要的任何数据。

然后,我们将提取数据,可以是属性或文本的形式。一个元素可能有多个属性,这就是为什么我们必须循环遍历所有属性并逐个提取它们。

还有更多…

除了 Web 浏览器之外,许多商业游戏引擎和交互应用程序也使用 XML 格式来存储游戏场景、网格和产品中使用的其他形式的资产信息。这是因为 XML 格式相对于其他文件格式提供了许多优势,如紧凑的文件大小、高灵活性和可扩展性、易于文件恢复,以及允许用于高效和性能关键应用程序的关系树结构,如搜索引擎、智能数据挖掘服务器、科学模拟等。

让我们简单了解一下 XML 文件的格式。我们将使用前面示例中使用的scene.xml,它看起来像这样:

<?xml version="1.0" encoding="UTF-8"?> 
<scene>
  <object tag="building">
    <name>Library</name>
    <position>120.0,0.0,50.68</position>
    <rotation>0.0,0.0,0.0</rotation>
    <scale>1.0,1.0,1.0</scale>
  </object>
  <object tag="building">
    <name>Town Hall</name>
    <position>80.2,0.0,20.5</position>
    <rotation>0.0,0.0,0.0</rotation>
    <scale>1.0,1.0,1.0</scale>
  </object>
  <object tag="prop">
    <name>Tree</name>
    <position>10.46,-0.2,80.2</position>
    <rotation>0.0,0.0,0.0</rotation>
    <scale>1.0,1.0,1.0</scale>
  </object>
</scene>

在 XML 中,标签是以<符号开始,以>符号结束的一行标记文本。例如,<scene>是一个名为scene的标签,<object>是一个名为object的标签,依此类推。标签有三种类型:

  • 开始标签,例如<scene>

  • 结束标签,例如</scene>

  • 空元素标签,例如<scene />

每当你写一个开始标签时,它必须以一个结束标签结束,否则你的 XML 数据将无效。然而,空元素标签是一个独立的标签,不需要在其后面加上结束标签。

scene.xml的顶部,你会看到一个名为xml的标签,其中存储了 XML 格式的版本和编码类型,本例中为 XML 版本 1.0 和 UTF-8(8 位 Unicode)编码。这一行称为 XML 声明,它必须存在于你的任何 XML 文件中以验证其格式。

之后,你会看到带有属性的标签,例如<object tag="building">。这意味着object标签包含一个名为tag的属性,其中包含一个值building。你可以在一个标签中放置尽可能多的属性,例如<object tag="building" color="red" name="LA Community Hospital" coordinate="34.0191757,-118.2567239">。这些属性中的每一个都存储着可以使用 Qt 轻松检索的独特数据。

除此之外,你还可以在开始标签和结束标签之间存储数据,例如<name>Town Hall</name>。然而,这种方法与空元素标签无关,因为它是一个独立的标签,不需要跟随一个关闭标签。因此,你只能在空元素标签中存储属性。

注意

要了解更多关于 XML 格式的信息,请访问www.w3schools.com/xml

使用流写入器写入 XML 数据

由于我们已经学会了如何处理从 XML 文件中获取的数据,在前面的示例中,我们将继续学习如何将数据保存到 XML 文件中。我们将继续使用前面的示例并对其进行扩展。

如何做…

我们将通过以下步骤学习如何将数据保存到 XML 文件中:

  1. 首先,在mainwindow.ui中添加另一个按钮,并将其对象名称设置为saveXmlButton,标签设置为保存 XML如何做…

  2. 接下来,右键单击按钮,选择转到槽…。一个窗口将弹出,显示可供选择的信号列表。选择clicked()选项,然后单击确定。一个名为on_saveXmlButton_clicked()的信号函数将被 Qt 自动添加到你的mainwindow.hmainwindow.cpp文件中:如何做…

  3. on_saveXmlButton_clicked()函数中添加以下代码:

QXmlStreamWriter xml;

QString filename = QFileDialog::getSaveFileName(this, "Save Xml", ".", "Xml files (*.xml)");
QFile file(filename);
if (!file.open(QFile::WriteOnly | QFile::Text))
  qDebug() << "Error saving XML file.";
xml.setDevice(&file);

xml.setAutoFormatting(true);
xml.writeStartDocument();

xml.writeStartElement("contact");
xml.writeAttribute("category", "Friend");
xml.writeTextElement("name", "John Doe");
xml.writeTextElement("age", "32");
xml.writeTextElement("address", "114B, 2nd Floor, Sterling Apartment, Morrison Town");
xml.writeTextElement("phone", "0221743566");
xml.writeEndElement();

xml.writeStartElement("contact");
xml.writeAttribute("category", "Family");
xml.writeTextElement("name", "Jane Smith");
xml.writeTextElement("age", "24");
xml.writeTextElement("address", "13, Ave Park, Alexandria");
xml.writeTextElement("phone", "0025728396");
xml.writeEndElement();

xml.writeEndDocument();
  1. 构建并运行程序,你应该会在程序界面上看到一个额外的按钮:操作步骤…

  2. 单击保存 XML按钮,屏幕上会出现一个保存文件对话框。输入你想要的文件名,然后单击保存按钮。

  3. 用任何文本编辑器打开你刚保存的 XML 文件。文件的内容应该是这样的:

<?xml version="1.0" encoding="UTF-8"?>
<contact category="Friend">
  <name>John Doe</name>
  <age>32</age>
  <address>114B, 2nd Floor, Sterling Apartment, Morrison Town</address>
  <phone>0221743566</phone>
</contact>
<contact category="Family">
  <name>Jane Smith</name>
  <age>24</age>
  <address>13, Ave Park, Alexandria</address>
  <phone>0025728396</phone>
</contact>

工作原理…

保存过程与前面示例中加载 XML 文件的过程基本相似。唯一的区别是,我们不再使用QXmlStreamReader类,而是改用QXmlStreamWriter类。

我们仍然使用文件对话框和QFile类来保存 XML 文件。这次,我们必须在将QFile类传递给流写入器之前,将打开模式从QFile::ReadOnly更改为QFile::WriteOnly

在我们开始向新的 XML 文件写入任何数据之前,我们必须将自动格式设置为true,否则将不会有间距;它还会向 XML 文件添加新行和缩进,使其看起来整洁且易于阅读。但是,如果这是你的意图(使用户难以阅读和编辑),那么你可以忽略setAutoFormatting()函数。

接下来,通过调用writeStartDocument()开始编写 XML 文件,然后写入要保存到文件中的所有元素,最后调用writeEndDocument()函数停止写入。

为了使读取过程正常工作,每个元素都必须有开始和结束标记。元素的属性将存储在开始标记中,而文本数据将存储在开始和结束标记之间。

如果我们要写入一个包含一组子元素的元素,那么在写入子元素之前必须调用writeStartElement()。然后,在保存所有子元素后,调用writeEndElement()来关闭该组并添加结束标记。writetextElement()函数会自动为您添加结束标记,因此您不必担心这个。

您可以调用writeAttribute()函数向元素添加属性。对于特定元素,您可以添加任意数量的属性。

使用QDomDocument类处理 XML 数据

Qt 允许多种方式解析 XML 数据,包括我们在前面示例中介绍的常见方法。这一次,我们将学习如何使用另一个名为QDomDocument的类从 XML 文件中读取数据。

操作步骤…

使用QDomDocument类处理 XML 数据非常简单:

  1. 首先,我们需要通过打开项目(.pro)文件并在coregui后面添加文本xml来将 XML 模块添加到我们的项目中,如下所示:
QT += core gui xml
  1. 然后,就像我们在本章的第一个示例中所做的那样,创建一个用户界面,上面有一个按钮,上面写着加载 XML操作步骤…

  2. 之后,右键单击按钮,选择转到槽…,然后选择clicked()选项。按下确定按钮,Qt 将在您的源代码中添加一个槽函数。

  3. 转到mainwindow.h并添加以下头文件,以便我们可以使用这些类:

#include <QDomDocument>
#include <QDebug>
#include <QFile>
#include <QFileDialog>
  1. 接下来,转到mainwindow.cpp并插入以下代码到按钮的clicked()槽函数中:操作步骤…

  2. 现在编译并运行程序。单击加载 XML按钮,然后选择第一个示例中使用的 XML 文件。你应该会看到以下输出:操作步骤…

工作原理…

QXmlStreamReader相比,QDomDocument类在加载或保存 XML 数据时不太直接。然而,QDomDocument通过确保每个元素都递归地链接到其相应的父元素,就像树结构一样,以严格的方式来完成。与QXmlStreamReader不同,QDomDocument允许我们在稍后的时间保存数据到之前创建的元素中。

由于QDomDocument不是 Qt 核心库的一部分,我们必须手动将 XML 模块添加到我们的项目中。否则,我们将无法访问QDomDocument和与之相关的其他类。

首先,我们加载 XML 文件并提取其内容到QDomDocument类。然后,我们获取其文档元素,它充当根文档,并获取其直接子元素。然后,我们将每个子节点转换为QDomElement并获取它们的标签名称。

通过检查标签名称,我们能够确定我们从每个元素中期望的数据类型。由于这是具有标签名称object的第一层元素,我们不期望从中获取任何数据;我们再次重复第 3 步,但这一次,我们将在具有标签名称object的元素上执行此操作,并获取其所有直接子元素,这意味着文档元素的孙元素。

同样,通过检查标签名称,我们能够知道我们从其子元素中期望什么数据。如果标签名称与我们期望的匹配(在本例中为namepositionrotationscale),那么我们可以通过调用QDomElement::text()获取其数据。

使用QDomDocument类编写 XML 数据

在这个例子中,我们将学习如何使用QDomDocument类将数据写入 XML 文件。我们将继续上一个例子,并添加一些内容。

如何做…

要学习如何使用QDomDocument类将数据保存到 XML 文件中,请执行以下操作:

  1. 首先,将第二个按钮添加到 UI 中,名为保存 XML如何做…

  2. 右键单击保存 XML按钮,然后选择转到槽…。然后,选择clicked()选项并单击确定。现在将在源文件中添加一个新的clicked()槽函数。

  3. 之后,在按钮的clicked()槽函数中编写以下代码:如何做…如何做…

  4. 现在编译并运行程序,然后单击保存 XML按钮。在保存文件对话框中输入所需的文件名,然后单击保存

  5. 使用任何文本编辑器打开您在第 4 步中保存的 XML 文件,您应该会看到类似于以下内容:

<!DOCTYPE contact>
<contact category="Family">
  <name>John Doe</name>
  <age>32</age>
  <address>114B, 2nd Floor, Sterling Apartment, Morrisontown</address>
  <phone>0221743566</phone>
</contact>
<contact category="Friend">
  <name>John Doe</name>
  <age>32</age>
  <address>114B, 2nd Floor, Sterling Apartment, Morrisontown</address>
  <phone>0221743566</phone>
</contact>

它是如何工作的…

与上一个例子类似,我们首先初始化文件对话框并声明一个QDomDocument对象。

然后,通过调用QDomDocument::createElement()创建根元素。从QDomDocument创建的任何元素都不会自动成为其直接子元素,除非我们将新创建的元素附加为其子元素。

要创建QDomDocument的孙元素,只需将新创建的元素附加到根元素即可。通过使用append()函数,我们可以轻松地将 XML 数据排列成树形结构,而无需费心思考。在我看来,这就是使用QDomDocument而不是QXmlStreamReader的优势。

然后,我们可以通过调用QDomElement::setAttribute()为元素添加属性。我们还可以通过调用QDomDocument::createTextNode()创建文本节点,并将其附加到 XML 结构中的任何元素。

在我们完成构造 XML 数据之后,我们可以将所有数据以文本形式输出到QTextStream类,并允许其将数据保存到文件中。

使用谷歌的地理编码 API

在这个例子中,我们将学习如何使用谷歌的地理编码 API 获取特定位置的完整地址。

如何做…

让我们创建一个程序,通过以下步骤利用地理编码 API:

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

  2. 接下来,打开mainwindow.ui并添加一些文本标签、输入字段和一个按钮,使您的 UI 看起来类似于这样:如何做…

  3. 之后,打开您的项目(.pro)文件,并将网络模块添加到您的项目中。您只需在coregui之后添加network文本即可完成此操作,如下所示:

QT += core gui network
  1. 然后,打开mainwindow.h并在#include <QMainWindow>之后的源代码中添加以下头文件:
#include <QDebug>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QXmlStreamReader>
  1. 接下来,手动声明一个槽函数并将其命名为getAddressFinished()
private slots:
  void getAddressFinished(QNetworkReply* reply);
  1. 在那之后,声明一个名为addressRequestprivate变量:
private:
  QNetworkAccessManager* addressRequest;
  1. 完成后,再次打开mainwindow.ui,右键单击获取地址按钮,然后选择转到槽…。然后选择clicked()选项并按确定。槽函数现在将添加到mainwindow.hmainwindow.cpp源文件中。

  2. 现在,打开mainwindow.cpp并将以下代码添加到类构造函数中:

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

  addressRequest = new QNetworkAccessManager();
  connect(addressRequest, SIGNAL(finished(QNetworkReply*)),   SLOT(getAddressFinished(QNetworkReply*)));
}
  1. 然后,我们将以下代码添加到我们刚刚手动声明的getAddressFinished()槽函数中:
void MainWindow::getAddressFinished(QNetworkReply* reply)
{
  QByteArray bytes = reply->readAll();

  //qDebug() << QString::fromUtf8(bytes.data(), bytes.size());

  QXmlStreamReader xml;
  xml.addData(bytes);

  while(!xml.atEnd())
  {
    if (xml.isStartElement())
    {
      QString name = xml.name().toString();
      //qDebug() << name;

      if (name == "formatted_address")
      {
        QString text = xml.readElementText();
        qDebug() << "Address:" << text;
        return;
      }
    }

    xml.readNext();
  }

  if (xml.hasError())
  {
    qDebug() << "Error loading XML:" << xml.errorString();
    return;
  }

  qDebug() << "No result.";
}
  1. 最后,将以下代码添加到 Qt 创建的clicked()槽函数中:
void MainWindow::on_getAddressButton_clicked()
{
  QString latitude = ui->latitude->text();
  QString longitude = ui->longitude->text();

  QNetworkRequest request;
  request.setUrl(QUrl("http://maps.googleapis.com/maps/api/geocode/xml?latlng=" + latitude + "," + longitude + "&sensor=false"));
  addressRequest->get(request);
}
  1. 现在构建并运行程序,您应该能够通过插入经度和纬度值并单击获取地址按钮来获取地址:如何做…

  2. 让我们尝试使用经度-73.9780838和纬度40.6712957。单击获取地址按钮,您将在应用程序输出窗口中看到以下结果:

Address: "180-190 7th Ave, Brooklyn, NY 11215, USA"

它是如何工作的…

我无法告诉您谷歌如何从其后端系统获取地址,但我可以教您如何使用QNetworkRequest从谷歌请求数据。基本上,您只需要将网络请求的 URL 设置为我在先前源代码中使用的 URL,并将纬度和经度信息附加到 URL。之后,我们只能等待来自谷歌 API 服务器的响应。

请注意,当向谷歌发送请求时,我们需要指定 XML 作为期望的格式;否则,它可能会返回 JSON 格式的结果。这可以通过在网络请求 URL 中添加xml关键字来实现,如下所示:

request.setUrl(QUrl("http://maps.googleapis.com/maps/api/geocode/xml?latlng=" + latitude + "," + longitude + "&sensor=false"));

当程序从谷歌接收到响应时,将调用getAddressFinished()槽函数,我们将能够通过QNetworkReply获取谷歌发送的数据。

谷歌通常会以 XML 格式回复一个长文本,其中包含大量我们不需要的数据。我们使用QXmlStreamReader来解析数据,因为在这种情况下,我们不必关心 XML 结构的父子关系。

我们只需要在 XML 数据中存储的formatted_address元素中的文本。由于有多个名为formatted_address的元素,我们只需要找到第一个并忽略其余的。

您还可以通过向谷歌提供地址并从其网络响应中获取位置坐标来进行反向操作。

还有更多…

谷歌的地理编码 API 是谷歌地图 API Web 服务的一部分,为您的地图应用程序提供地理数据。除了地理编码 API,您还可以使用他们的位置 API、地理位置 API、时区 API 等来实现您想要的结果。

注意

有关谷歌地图 API Web 服务的更多信息,请访问此链接:developers.google.com/maps/web-services

第七章:转换库

在本章中,我们将涵盖以下配方:

  • 数据转换

  • 图像转换

  • 视频转换

  • 货币转换

介绍

我们计算机环境中的数据以各种方式进行编码。有时它可以直接用于某种目的,其他时候需要将其转换为另一种格式以适应任务的上下文。根据源格式和目标格式,数据从一种格式转换为另一种格式的过程也各不相同。有时这个过程可能非常复杂,特别是在处理功能丰富和敏感的数据时,比如图像或视频转换。即使在转换过程中出现小错误,也可能使文件无法使用。

数据转换

Qt 提供了一组类和函数,用于轻松地在不同类型的数据之间进行转换。这使得 Qt 不仅仅是一个 GUI 库;它是一个完整的软件开发平台。QVariant类,我们将在下面的示例中使用,使 Qt 比 C++标准库提供的类似转换功能更加灵活和强大。

如何做…

让我们按照以下步骤学习如何在 Qt 中转换各种数据类型:

  1. 打开 Qt Creator,并通过文件 | 新建文件或项目创建一个新的Qt 控制台应用程序项目:如何做…

  2. 接下来,打开main.cpp并添加以下头文件:

#include <QCoreApplication>
#include <QDebug>
#include <QtMath>
#include <QDateTime>
#include <QTextCodec>
#include <iostream>
  1. 然后,在main()函数中,添加以下代码将字符串转换为数字:
int numberA = 2;
QString numberB = "5";
qDebug() << "1) " << "2 + 5 =" << numberA + numberB.toInt();
  1. 之后,我们将把一个数字转换回字符串:
float numberC = 10.25;
float numberD = 2;
QString result = QString::number(numberC * numberD);
qDebug() << "2) " <<  "10.25 * 2 =" << result;
  1. 我们还学习了如何使用qFloor()将值向下舍入:
float numberE = 10.3;
float numberF = qFloor(numberE);
qDebug() << "3) " << "Floor of 10.3 is" << numberF;
  1. 然后,通过使用qCeil(),我们能够将数字舍入到不小于其初始值的最小整数值:
float numberG = 10.3;
float numberH = qCeil(numberG);
qDebug() << "4) " << "Ceil of 10.3 is" << numberH;
  1. 之后,我们将通过从字符串转换来创建一个日期时间变量:
QString dateTimeAString = "2016-05-04 12:24:00";
QDateTime dateTimeA = QDateTime::fromString(dateTimeAString, "yyyy-MM-dd hh:mm:ss");
qDebug() << "5) " << dateTimeA;
  1. 随后,我们还可以将日期时间变量转换为具有自定义格式的字符串:
QDateTime dateTimeB = QDateTime::currentDateTime();
QString dateTimeBString = dateTimeB.toString("dd/MM/yy hh:mm");
qDebug() << "6) " << dateTimeBString;
  1. 我们可以调用QString::toUpper()函数将字符串变量转换为全大写字母:
QString hello1 = "hello world!";
qDebug() << "7) " << hello1.toUpper();
  1. 另一方面,调用QString::toLower()将把字符串转换为全小写:
QString hello2 = "HELLO WORLD!";
qDebug() << "8) " << hello2.toLower();
  1. Qt 提供的QVariant类是一种非常强大的数据类型,可以轻松转换为其他类型,程序员无需任何努力:
QVariant aNumber = QVariant(3.14159);
double aResult = 12.5 * aNumber.toDouble();
qDebug() << "9) 12.5 * 3.14159 =" << aResult;
  1. 这演示了如何将单个QVariant变量同时转换为多个数据类型,而程序员无需任何努力:
qDebug() << "10) ";
QVariant myData = QVariant(10);
qDebug() << myData;
myData = myData.toFloat() / 2.135;
qDebug() << myData;
myData = true;
qDebug() << myData;
myData = QDateTime::currentDateTime();
qDebug() << myData;
myData = "Good bye!";
qDebug() << myData;

main.cpp中的完整源代码现在看起来是这样的:

如何做…如何做…

  1. 现在编译并运行项目,你应该会看到类似这样的东西:如何做…

它是如何工作的…

Qt 提供的所有数据类型,如QStringQDateTimeQVariant等,都包含使转换到其他类型变得简单和直接的函数。

Qt 还提供了自己的对象转换函数qobject_cast(),它不依赖于标准库。它也更兼容 Qt,并且对于在 Qt 的窗口部件类型和数据类型之间进行转换非常有效。

Qt 还为您提供了QtMath类,它可以帮助您操作数字变量,例如将浮点数四舍五入或将角度从度转换为弧度。

QVariant是一个特殊的类,可以用于存储各种类型的数据。它可以通过检查变量中存储的值来自动确定数据类型。您还可以通过调用单个函数(如toFloat()toInt()toBool()toChar()toString()等)轻松地将数据转换为QVariant类支持的任何类型。

还有更多…

请注意,每次转换都需要计算资源才能实现。尽管现代计算机在处理此类操作时非常快速,但您应该小心,不要一次处理大量操作。如果您正在为复杂计算转换大量变量,可能会显著减慢计算机的速度,因此请尽量仅在必要时转换变量。

图像转换

在本节中,我们将学习如何构建一个简单的图像转换器,将图像从一种格式转换为另一种格式。Qt 支持读取和写入不同类型的图像格式,这种支持以外部 DLL 文件的形式出现,这是由于许可问题。但是,您不必担心,因为只要将这些 DLL 文件包含在项目中,它将在不同格式之间无缝工作。有些格式仅支持读取而不支持写入,还有一些支持两者。您可以在doc.qt.io/qt-5/qtimageformats-index.html上查看完整的详细信息。

如何操作...

Qt 内置的图像库使图像转换变得非常简单:

  1. 首先,打开 Qt Creator 并创建一个新的Qt Widgets Application项目。

  2. 打开mainwindow.ui,并在画布上添加一个行编辑和一个按钮,用于选择图像文件,一个下拉框,用于选择所需的文件格式,以及另一个按钮,用于启动转换过程:如何操作...

  3. 接下来,双击下拉框,将出现一个窗口,用于编辑下拉框。我们将通过点击+按钮三次并将项目重命名为PNGJPEGBMP,向下拉框列表中添加三个项目:如何操作...

  4. 之后,右键单击其中一个按钮,选择转到槽...,然后单击确定按钮。然后,槽函数将自动添加到您的源文件中。然后,对另一个按钮重复此步骤:如何操作...

  5. 完成 UI 后,让我们转到源代码。打开mainwindow.h,并添加以下头文件:

#include <QMainWindow>
#include <QFileDialog>
#include <QMessageBox>
#include <QDebug>
  1. 然后,打开mainwindow.cpp并定义单击浏览按钮时会发生什么,即打开文件对话框以选择图像文件:
void MainWindow::on_browseButton_clicked()
{
  QString fileName = QFileDialog::getOpenFileName(this, "Open Image", "", "Image Files (*.png *.jpg *.bmp)");
  ui->filePath->setText(fileName);
}
  1. 最后,我们还定义了单击转换按钮时会发生什么:如何操作...

  2. 现在构建并运行程序,我们应该得到一个非常简单的图像转换器,看起来像这样:如何操作...

工作原理...

前面的示例使用了 Qt 中的本机QImage类,其中包含可以访问像素数据并操纵它的函数。它还用于通过不同的解压缩方法加载图像文件并提取其数据,具体取决于图像的格式。一旦提取了数据,您就可以对其进行任何操作,例如在屏幕上显示图像,操纵其颜色信息,调整图像大小,或者使用另一种格式对其进行压缩并将其保存为文件。

我们使用QFileInfo将文件名与扩展名分开,以便我们可以使用用户从下拉框中选择的新格式修改扩展名。这样,我们可以将新转换的图像保存在与原始图像相同的文件夹中,并自动以相同的文件名保存,只是格式不同。

只要您尝试将图像转换为 Qt 支持的格式,您只需要调用QImage::save()。在内部,Qt 会为您解决其余问题,并将图像输出到所选格式。在QImage::save()函数中,有一个设置图像质量的参数,另一个用于设置格式。在本例中,我们将两者都设置为默认值,这将以最高质量保存图像,并让 Qt 通过检查输出文件名中的扩展名来确定格式。

还有更多...

以下是一些提示。您还可以使用 Qt 提供的QPdfWriter类将图像转换为 PDF。基本上,您要做的是将所选图像绘制到新创建的 PDF 文档的布局中,并相应地设置其分辨率。有关QPdfWriter类的更多信息,请访问doc.qt.io/qt-5/qpdfwriter.html

视频转换

在这个教程中,我们将使用 Qt 和 FFmpeg 创建一个简单的视频转换器,FFmpeg 是一个领先的多媒体框架,是免费开源的。虽然 Qt 确实支持通过其小部件播放视频文件,但目前不支持视频转换。不用担心!通过 Qt 提供的QProcess类,您实际上仍然可以通过使您的程序与另一个独立程序合作来实现相同的目标。

如何做...

让我们按照以下步骤制作一个简单的视频转换器:

  1. ffmpeg.zeranoe.com/builds下载 FFmpeg(静态包),并将内容提取到C:/FFmpeg/

  2. 然后,打开 Qt Creator,并通过文件 | 新建文件或项目…创建一个新的Qt Widgets 应用程序项目。

  3. 之后,打开mainwindow.ui,我们将在程序的用户界面上进行工作。它的用户界面与之前的示例非常相似,只是我们在画布下方添加了一个额外的文本编辑小部件,就在组合框下面:如何做…

  4. 双击组合框,将出现一个窗口以编辑组合框。我们将通过点击+按钮三次向组合框列表添加三个项目,并将项目重命名为AVIMP4MOV如何做…

  5. 之后,右键单击其中一个按钮,选择转到槽…,然后单击确定按钮。然后,槽函数将自动添加到您的源文件中。然后,对另一个按钮重复此步骤。

  6. 之后,打开mainwindow.h,并在顶部添加以下头文件:

#include <QMainWindow>
#include <QFileDialog>
#include <QProcess>
#include <QMessageBox>
#include <QScrollBar>
#include <QDebug>
  1. 然后,在public关键字下添加以下指针:
public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

 QProcess* process;
 QString outputText;
 QString fileName;
 QString outputFileName;

  1. 除此之外,我们还需要在 Qt 为我们之前创建的两个函数下添加三个额外的槽函数:
private slots:
  void on_browseButton_clicked();
  void on_convertButton_clicked();

 void processStarted();
 void readyReadStandardOutput();
 void processFinished();

  1. 接下来,打开mainwindow.cpp,并将以下代码添加到类构造函数中:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent), ui(new Ui::MainWindow)
{
  ui->setupUi(this);

 process = new QProcess(this);
 connect(process, SIGNAL(started()), this, SLOT(processStarted()));
 connect(process,SIGNAL(readyReadStandardOutput()), this,SLOT(readyReadStandardOutput()));
 connect(process, SIGNAL(finished(int)), this, SLOT(processFinished()));
}
  1. 之后,我们定义了浏览按钮点击时会发生什么,这种情况下将打开文件对话框以选择视频文件:
void MainWindow::on_browseButton_clicked()
{
  QString fileName = QFileDialog::getOpenFileName(this, "Open Video", "", "Video Files (*.avi *.mp4 *.mov)");
  ui->filePath->setText(fileName);
}
  1. 然后,我们还定义了转换按钮点击时会发生什么。我们在这里做的是将文件名和参数传递给 FFmpeg,然后转换过程将由 FFmpeg 在外部处理:如何做…

  2. 完成后,我们将告诉我们的程序在转换过程开始时要做什么:

void MainWindow::processStarted()
{
  qDebug() << "Process started.";

  ui->browseButton->setEnabled(false);
  ui->fileFormat->setEditable(false);
  ui->convertButton->setEnabled(false);
}
  1. 接下来,我们将编写在转换过程中由 FFmpeg 返回程序输出时调用的槽函数:
void MainWindow::readyReadStandardOutput()
{
  outputText += process->readAllStandardOutput();
  ui->outputDisplay->setText(outputText);

  ui->outputDisplay->verticalScrollBar()->setSliderPosition(ui->outputDisplay->verticalScrollBar()->maximum());
}
  1. 最后,我们定义了在整个转换过程完成时调用的槽函数:
void MainWindow::processFinished()
{
  qDebug() << "Process finished.";

  if (QFile::exists(outputFileName))
  {
    QMessageBox::information(this, "Success", "Video successfully converted.");
  }
  else
  {
    QMessageBox::information(this, "Failed", "Failed to convert video.");
  }

  ui->browseButton->setEnabled(true);
  ui->fileFormat->setEditable(true);
  ui->convertButton->setEnabled(true);
}
  1. 现在构建和运行项目,您应该得到一个简单但可用的视频转换器:如何做…

它是如何工作的...

Qt 提供的QProcess类用于启动外部程序并与其通信。在这种情况下,我们启动了位于C:/FFmpeg/bin/中的ffmpeg.exe作为一个进程,并开始与其通信。我们还向它发送了一组参数,告诉它启动时该做什么。我们在这个例子中使用的参数相对基本;我们只告诉 FFmpeg 源图像的路径和输出文件名。有关 FFmpeg 中可用的参数设置的更多信息,请查看www.ffmpeg.org/ffmpeg.html

FFmpeg 不仅可以转换视频文件。您还可以使用它来转换音频文件,甚至图像。有关 FFmpeg 支持的所有格式的更多信息,请查看www.ffmpeg.org/general.html#File-Formats

除此之外,您还可以通过运行位于C:/FFmpeg/bin中的ffplay.exe来播放视频或音频文件,或者通过运行ffprobe.exe以人类可读的方式打印视频或音频文件的信息。查看 FFmpeg 的完整文档:www.ffmpeg.org/about.html

还有更多…

使用这种方法可以做很多事情。这意味着您不受 Qt 提供的限制,可以通过仔细选择提供所需功能的第三方程序来摆脱这些限制。一个这样的例子是利用市场上提供的仅支持命令行的反病毒扫描程序,如 Avira ScanCL、Panda Antivirus Command Line Scanner、SAV32CLI、ClamavNet 等,制作自己的反病毒 GUI。您可以使用 Qt 构建自己的 GUI,并向反病毒进程发送命令,告诉它该做什么。

货币转换

在这个例子中,我们将学习如何使用 Qt 创建一个简单的货币转换器,借助名为Fixer.io的外部服务提供商。

如何做…

通过以下简单步骤制作一个货币转换器:

  1. 我们首先打开 Qt Creator,并从“文件”|“新建文件或项目”中创建一个新的“Qt Widgets 应用程序”项目。

  2. 接下来,打开项目文件(.pro)并将网络模块添加到我们的项目中:

QT += core gui network
  1. 之后,打开mainwindow.ui并从 UI 中删除菜单栏、工具栏和状态栏。

  2. 然后,在画布上添加三个水平布局、一条水平线和一个按钮。一旦它们都放好了,左键单击画布,然后点击画布上方的“垂直布局”按钮。然后,将按钮的标签设置为“转换”。UI 现在应该看起来像这样:如何做…

  3. 之后,将两个标签添加到顶部布局,并将左侧的文本设置为“从:”,右侧的文本设置为“到:”。紧接着,在第二个布局中添加两个行编辑小部件,并将它们的默认值都设置为1如何做…

  4. 在我们继续向最后一个布局添加最后一批小部件之前,让我们选择右侧的行编辑框,并在属性窗格中启用readOnly复选框:如何做…

  5. 除此之外,我们还必须将其cursor属性设置为Forbidden,以便用户在鼠标悬停在小部件上时知道它是不可编辑的:如何做…

  6. 完成后,让我们将两个组合框添加到底部的第三个布局中。现在我们只是把它们留空:如何做…

  7. 之后,右键单击“转换”按钮,选择“转到槽…”。一个窗口将弹出,要求您选择适当的信号。让我们保持默认的clicked()信号作为选择,然后点击“确定”。Qt Creator 现在会自动为您在mainwindow.hmainwindow.cpp中添加一个槽函数。

  8. 接下来,打开mainwindow.h,确保以下头文件被添加到源文件的顶部:

#include <QMainWindow>
#include <QDoubleValidator>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
#include <QMessageBox>
  1. 然后,我们需要添加另一个名为finished()的槽函数:
private slots:
  void on_convertButton_clicked();
 void finished(QNetworkReply* reply);

  1. 除此之外,我们还需要在private标签下添加两个变量:
private:
  Ui::MainWindow *ui;
 QNetworkAccessManager* manager;
 QString targetCurrency;

  1. 完成后,让我们这次打开mainwindow.cpp。我们将在类构造函数中的两个组合框中添加几个货币简码。我们还为左侧的行编辑小部件设置了验证器,以便它只能接受数字输入。最后,我们还初始化了网络访问管理器,并将其finished()信号连接到我们的finished()槽函数。
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent), ui(new Ui::MainWindow)
{
  ui->setupUi(this);

  QStringList currencies;
  currencies.push_back("EUR");
  currencies.push_back("USD");
  currencies.push_back("CAD");
  currencies.push_back("MYR");
  currencies.push_back("GBP");

  ui->currencyFrom->insertItems(0, currencies);
  ui->currencyTo->insertItems(0, currencies);

  QValidator *inputRange = new QDoubleValidator(this);
  ui->amountFrom->setValidator(inputRange);

  manager = new QNetworkAccessManager(this);
  connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(finished(QNetworkReply*)));
}
  1. 之后,我们定义了当用户点击转换按钮时会发生什么:
void MainWindow::on_convertButton_clicked()
{
  if (ui->amountFrom->text() != "")
  {
    ui->convertButton->setEnabled(false);
    QString from = ui->currencyFrom->currentText();
    QString to = ui->currencyTo->currentText();
    targetCurrency = to;
    QString url = "http://api.fixer.io/latest?base=" + from + "&symbols=" + to;
    QNetworkRequest request= QNetworkRequest(QUrl(url));
    manager->get(request);
  }
  else
  {
    QMessageBox::warning(this, "Error", "Please insert a value.");
  }
}
  1. 最后,定义finished()信号被触发时会发生什么:
void MainWindow::finished(QNetworkReply* reply)
{
  QByteArray response = reply->readAll();
  qDebug() << response;
  QJsonDocument jsonResponse = QJsonDocument::fromJson(response);
  QJsonObject jsonObj = jsonResponse.object();
  QJsonObject jsonObj2 = jsonObj.value("rates").toObject();
  double rate = jsonObj2.value(targetCurrency).toDouble();
  if (rate == 0)
    rate = 1;
  double amount = ui->amountFrom->text().toDouble();
  double result = amount * rate;
  ui->amountTo->setText(QString::number(result));
  ui->convertButton->setEnabled(true);
}
  1. 现在编译并运行项目,您应该能够获得一个简单的货币转换器,看起来像这样:如何做…

工作原理...

与我们之前看到的示例类似,该示例使用外部程序来实现特定任务,这次我们使用了一个外部服务提供商,他们为我们提供了一个免费且易于使用的开放应用程序编程接口API)。

这样,我们就不必考虑检索最新货币汇率的方法。相反,服务提供商已经为我们完成了这项工作,我们只需发送一个礼貌的请求并要求它。然后,我们只需等待他们服务器的响应,并根据我们的意图处理数据。

除了Fixer.iofixer.io)之外,您还可以选择其他几个不同的服务提供商。有些是免费的,但没有任何高级功能;有些提供额外的功能,尽管它们是付费的。其中一些替代方案包括 Open Exchange Rate(openexchangerates.org)、Currencylayer(currencylayer.com)、Currency API(currency-api.appspot.com)、XE Currency Data API(www.xe.com/xecurrencydata)和 Jsonrates(jsonrates.com)。

还有更多...

除了货币汇率,您还可以使用这种方法来执行其他更高级的任务,这些任务可能太复杂而无法自行完成,或者除非使用专家提供的服务,否则根本无法访问,例如可编程短信服务SMS)和语音服务、网络分析和统计生成、在线支付网关等等。大多数这些服务都不是免费的,但您可以在几分钟内轻松实现这些功能,甚至无需设置服务器基础架构、后端系统等等;这绝对是最便宜和最快的方式,让您的产品快速上线而几乎没有任何麻烦。

第八章:访问数据库

在本章中,我们将涵盖以下内容:

  • 为 Qt 设置 SQL 驱动程序

  • 连接到数据库

  • 编写基本的 SQL 查询

  • 使用 Qt 创建登录界面

  • 在模型视图上显示来自数据库的信息

  • 高级 SQL 查询

介绍

SQL 代表结构化查询语言,这是一种特殊的编程语言,用于管理关系数据库管理系统中保存的数据。SQL 服务器是一个设计用来使用多种类型的 SQL 编程语言来管理数据的数据库系统。

注意

如果您想了解更多关于 SQL 的信息,请访问此链接:www.w3schools.com/sql/sql_intro.asp

Qt 支持多种不同类型的 SQL 驱动程序,以插件/附加组件的形式提供。然而,将这些驱动程序集成到您的 Qt 项目中非常容易。我们将在以下示例中学习如何做到这一点。

如何做…

在我们深入 Qt 之前,让我们先设置我们的 SQL 服务器:

  1. 在为 SQL 设置 Qt 之前,我们需要安装和设置 MySQL 服务器。有许多安装方法。第一种方法是从官方网站dev.mysql.com/downloads/mysql/下载 MySQL 并安装。之后,您还需要从dev.mysql.com/downloads/workbench/安装 MySQL Workbench 来管理您的数据库。

  2. 另一种方法是安装一个带有 MySQL 和其他有用应用程序(如 Apache Web 服务器、phpMyAdmin 等)的第三方软件包,所有这些都在一个统一的安装程序中。此类软件包的示例包括 XAMPP,sourceforge.net/projects/xampp/,以及 AppServ,www.appservnetwork.com/en/download/

  3. 在此示例中,我们将安装 XAMPP。打开您的 Web 浏览器,从sourceforge.net/projects/xampp/下载 XAMPP 安装程序,并在计算机上安装它。

  4. 安装完 XAMPP 后,打开 XAMPP 控制面板,您应该看到类似于这样的界面:如何做…

  5. 我们需要的是 Apache Web 服务器和 MySQL 数据库服务器。在控制面板上,单击ApacheMySQL选项旁边的启动按钮。

  6. 一旦服务器启动,打开您的 Web 浏览器并访问localhost/phpmyadmin/。您将看到一个名为PhpMyAdmin的 Web 界面,看起来像这样:如何做…

  7. phpMyAdmin 是一个基于 Web 的实用程序,可以帮助您管理 MySQL 数据库,就像官方的 MySQL Workbench 一样。在我看来,phpMyAdmin 更简单,更适合初学者,这就是为什么我建议使用它而不是 MySQL Workbench。

  8. 默认情况下,phpMyAdmin 会自动使用默认用户帐户root登录到 MySQL,该用户帐户保存在其配置文件中。出于安全原因,我们不想使用它。因此,我们需要做的下一件事是为自己创建一个帐户。转到顶部的用户选项卡,一旦在该页面上,单击底部的添加用户。在登录信息窗格的字段中输入您想要的用户名和密码。暂时选择本地作为主机选项。在底部,您将看到与全局权限相关的选项;选中全部检查选项,然后单击Go如何做…

  9. 现在您已经创建了用户帐户,请转到 XAMPP 控制面板,单击 Apache 和 MySQL 的停止。然后,单击Apache列上的Config按钮,并选择phpMyAdmin(config.inc.php)选项。之后,config.inc.php文件将以您选择的文本编辑器打开。

  10. config.inc.php中搜索以下行,并将单词config更改为cookie

$cfg['Servers'][$i]['auth_type'] = 'config';
$cfg['Servers'][$i]['auth_type'] = 'cookie';
  1. 之后,通过单击启动按钮再次启动 Apache 和 MySQL。这样,我们强制 phpMyAdmin 重新加载其配置并应用更改。再次从 Web 浏览器转到 phpmyAdmin,这次应该会在屏幕上显示登录界面:操作方法…

  2. 登录到 phpMyAdmin,然后单击侧边栏上的新建链接:操作方法…

  3. 输入您想要的数据库名称,然后按创建按钮。创建完成后,数据库名称将显示在侧边栏上。单击数据库名称,将带您到另一个页面,显示消息数据库中找不到表。在消息下方,您可以通过填写所需的表名和表的列数来创建您的第一个数据表:操作方法…

  4. 单击Go按钮后,您将被带到另一个页面,您将在其中设置要创建的新表。在本例中,我们创建了一个包含五列数据的employee表:idnameagegendermarried操作方法…

  5. 完成后,单击保存,现在您将能够在侧边栏上看到employee表名。我们已成功安装了 MySQL 并设置了我们的第一个数据库和数据表。

  6. 之后,我们需要从 phpMyAdmin 向数据库插入数据,以便我们能够在下一个示例中检索它。在employee表中单击插入选项卡;然后将带您到另一个页面,用于向employee表中插入新数据:操作方法…

  7. 接下来,我们将继续为我们的 Qt 项目设置 SQL 驱动程序。基本上,您只需要转到 Qt 安装文件夹,然后查找sqldrivers文件夹。例如,我的位于C:\Qt\5.5\mingw492_32\plugins\sqldrivers

  8. 将整个sqldrivers文件夹复制到项目的构建目录中。您可以删除与您正在运行的 SQL 服务器不相关的 DLL 文件。在我们的情况下,由于我们使用的是 MySQL 服务器,我们可以删除除了qsqlmysql.dllqsqlmysqld.dll之外的所有内容。带有后面带有字母d的 DLL 文件仅用于调试构建,而另一个用于发布构建。将这些 DLL 文件放在各自的构建目录中,例如,调试构建的builds/debug/sqldrivers/qsqlmysqld.dll和发布构建的builds/release/sqldrivers/qsqlmysql.dll

  9. 在上一步提到的 DLL 文件是使 Qt 能够与不同类型的 SQL 架构进行通信的驱动程序。您可能还需要 SQL 客户端库的 DLL 文件才能使驱动程序正常工作。在我们的情况下,我们需要libmysql.dll位于与我们程序可执行文件相同的目录中。您可以从 MySQL 的安装目录获取它,或者从官方网站dev.mysql.com/downloads/connector/cpp/下载 Connector/C++包。

工作原理…

Qt 为我们提供了 SQL 驱动程序,以便我们可以轻松地连接到不同类型的 SQL 服务器,而无需自己实现它们。

目前,Qt 官方支持 SQLite、MySQL、ODBC 和 PostgreSQL。作为受支持架构之一的分支的 SQL 架构,例如 MariaDB(MySQL 的一个分支),可能仍然与 Qt 兼容,而不会出现太多问题。

如果您使用的架构不受 Qt 支持,您仍然可以通过使用 QNetworkAccessManager 向后端脚本(如 PHP、ASP、JSP 等)发送 HTTP 请求来间接地与您的 SQL 数据库进行交互,然后后端脚本可以与数据库进行通信。

如果您只需要一个简单的基于文件的数据库,并且不打算使用基于服务器的数据库,那么 SQLite 是一个很好的选择。

连接到数据库

在本教程中,我们将学习如何使用 Qt 的 SQL 模块连接到我们的 SQL 数据库。

操作方法…

在 Qt 中连接到 SQL 服务器非常简单:

  1. 首先,打开 Qt Creator 并创建一个新的Qt Widgets Application项目。

  2. 打开你的项目文件(.pro)并将 SQL 模块添加到你的项目中,就像这样:

QT += core gui sql
  1. 接下来,打开mainwindow.ui并将七个标签小部件、一个组合框和一个复选框拖到画布上。将四个标签的文本属性设置为Name:Age:Gender:Married:。然后,将其余的objectName属性设置为nameagegendermarried。对于前四个标签,不需要设置对象名称,因为它们仅用于显示目的:如何做...

  2. 之后,打开mainwindow.h并在QMainWindow头文件下添加以下头文件:

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

  1. 然后,打开mainwindow.cpp并在类构造函数中插入以下代码:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent), ui(new Ui::MainWindow)
{
  ui->setupUi(this);

 QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
 db.setHostName("127.0.0.1");
 db.setUserName("yourusername");
 db.setPassword("yourpassword");
 db.setDatabaseName("databasename");

 if (db.open())
 {
 QSqlQuery query;
 if (query.exec("SELECT name, age, gender, married FROM employee"))
 {
 while (query.next())
 {
 qDebug() << query.value(0) << query.value(1) << query.value(2) << query.value(3);

 ui->name->setText(query.value(0).toString());
 ui->age->setText(query.value(1).toString());
 ui->gender->setCurrentIndex(query.value(2).toInt());
 ui->married->setChecked(query.value(3).toBool());
 }
 }
 else
 {
 qDebug() << query.lastError().text();
 }

 db.close();
 }
 else
 {
 qDebug() << "Failed to connect to database.";
 }
}
  1. 现在编译和运行你的项目,你应该会得到类似以下的结果:如何做...

它是如何工作的...

上一个例子向你展示了如何使用从 SQL 模块派生的QSqlDatabase类连接到你的 SQL 数据库。如果没有将模块添加到 Qt 项目中,你将无法访问与 SQL 相关的任何类。

我们必须告诉 Qt 我们正在运行哪个 SQL 架构,当调用addDatabase()函数时提到它。Qt 支持的选项有 QSQLITE、QMYSQL、QMYSQL3、QODBC、QODBC3、QPSQL 和 QPSQL7

如果遇到错误消息说QSqlDatabase: QMYSQL driver not loaded,那么你应该再次检查 DLL 文件是否放在正确的目录中。

我们可以通过QSqlQuery类将我们的 SQL 语句发送到数据库,并等待它返回结果,通常是你请求的数据或由于无效语句而产生的错误消息。

如果有任何来自数据库服务器的数据,它将全部存储在QSqlQuery类中。你只需要在QSqlQuery类上进行while循环,检查所有现有记录,并通过调用value()函数检索它们。

编写基本的 SQL 查询

在上一个例子中,我们编写了我们的第一个 SQL 查询,涉及SELECT语句。这一次,我们将学习如何使用其他一些 SQL 语句,比如INSERTUPDATEDELETE

如何做...

让我们创建一个简单的程序,通过以下步骤演示基本的 SQL 查询命令:

  1. 我们可以使用之前的项目文件,但有一些需要更改的地方。首先,打开mainwindow.ui,用行编辑小部件替换名称和年龄的标签。然后,在画布上添加三个按钮,并将它们命名为更新插入删除如何做...

  2. 之后,打开mainwindow.h并在私有继承下添加以下变量:

private:
  Ui::MainWindow *ui;
 QSqlDatabase db;
 bool connected;
 int currentID;

  1. 接下来,打开mainwindow.cpp并转到类构造函数。它与上一个例子基本相同,只是我们将数据库连接状态存储在名为connected的布尔变量中,并且还获取来自数据库的数据的 ID 并将其存储到名为currentID的整数变量中:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent), ui(new Ui::MainWindow)
{
  ui->setupUi(this);

  db = QSqlDatabase::addDatabase("QMYSQL");
  db.setHostName("127.0.0.1");
  db.setUserName("yourusername");
  db.setPassword("yourpassword");
  db.setDatabaseName("databasename");

  connected = db.open();

  if (connected)
  {
    QSqlQuery query;
    if (query.exec("SELECT id, name, age, gender, married FROM employee"))
    {
      while (query.next())
      {
        currentID = query.value(0).toInt();
        ui->name->setText(query.value(1).toString());
        ui->age->setText(query.value(2).toString());
        ui->gender->setCurrentIndex(query.value(3).toInt());
        ui->married->setChecked(query.value(4).toBool());
      }
    }
    else
    {
      qDebug() << query.lastError().text();
    }
  }
  else
  {
    qDebug() << "Failed to connect to database.";
  }
}
  1. 然后,转到mainwindow.ui,右键单击我们在步骤 1 中添加到画布上的一个按钮。选择转到槽...,然后单击确定。在另一个按钮上重复这些步骤,现在你应该看到三个槽函数被添加到你的mainwindow.hmainwindow.cpp中:
private slots:
  void on_updateButton_clicked();
  void on_insertButton_clicked();
  void on_deleteButton_clicked();
  1. 之后,打开mainwindow.cpp,我们将声明当点击更新按钮时程序将做什么:
void MainWindow::on_updateButton_clicked()
{
  if (connected)
  {
    if (currentID == 0)
    {
      qDebug() << "Nothing to update.";
    }
    else
    {
      QString id = QString::number(currentID);
      QString name = ui->name->text();
      QString age = ui->age->text();
      QString gender = QString::number(ui->gender->currentIndex());
      QString married = QString::number(ui->married->isChecked());

      qDebug() << "UPDATE employee SET name = '" + name + "', age = '" + age + "', gender = " + gender + ", married = " + married + " WHERE id = " + id;

      QSqlQuery query;
      if (query.exec("UPDATE employee SET name = '" + name + "', age = '" + age + "', gender = " + gender + ", married = " + married + " WHERE id = " + id))
      {
        qDebug() << "Update success.";
      }
      else
      {
        qDebug() << query.lastError().text();
      }
    }
  }
  else
  {
    qDebug() << "Failed to connect to database.";
  }
}
  1. 完成后,我们将继续声明插入按钮被点击时会发生什么:
void MainWindow::on_insertButton_clicked()
{
  if (connected)
  {
    QString name = ui->name->text();
    QString age = ui->age->text();
    QString gender = QString::number(ui->gender->currentIndex());
    QString married = QString::number(ui->married->isChecked());

    qDebug() << "INSERT INTO employee (name, age, gender, married) VALUES ('" + name + "','" + age + "'," + gender + "," + married + ")";

    QSqlQuery query;
    if (query.exec("INSERT INTO employee (name, age, gender, married) VALUES ('" + name + "','" + age + "'," + gender + "," + married + ")"))
    {
      currentID = query.lastInsertId().toInt();
      qDebug() << "Insert success.";
    }
    else
    {
      qDebug() << query.lastError().text();
    }
  }
  else
  {
    qDebug() << "Failed to connect to database.";
  }
}
  1. 之后,我们还声明了删除按钮被点击时会发生什么:
void MainWindow::on_deleteButton_clicked()
{
  if (connected)
  {
    if (currentID == 0)
    {
      qDebug() << "Nothing to delete.";
    }
    else
    {
      QString id = QString::number(currentID);
      qDebug() << "DELETE FROM employee WHERE id = " + id;
      QSqlQuery query;
      if (query.exec("DELETE FROM employee WHERE id = " + id))
      {
        currentID = 0;
        qDebug() << "Delete success.";
      }
      else
      {
        qDebug() << query.lastError().text();
      }
    }
  }
  else
  {
    qDebug() << "Failed to connect to database.";
  }
}
  1. 最后,在类析构函数中调用QSqlDatabase::close()以正确终止程序退出前的 SQL 连接:
MainWindow::~MainWindow()
{
 db.close();
  delete ui;
}
  1. 现在编译并运行程序,您应该能够从数据库中选择默认数据;然后您可以选择更新或从数据库中删除它。您还可以通过单击插入按钮将新数据插入到数据库中。您可以使用 phpMyAdmin 来检查数据是否被正确修改:操作步骤…

工作原理…

在向数据库发送 SQL 查询之前,检查数据库是否连接是非常重要的。因此,我们将状态保存在一个变量中,并在发送任何查询之前使用它进行检查。然而,对于长时间保持打开的复杂程序,不建议使用固定变量,因为数据库在这些时间段内可能会断开连接,固定变量可能不准确。在这种情况下,最好通过调用QSqlDatabase::isOpen()来检查实际状态。

currentID变量用于保存从数据库中获取的当前数据的 ID。当您想要更新数据或从数据库中删除数据时,这个变量对于让数据库知道您要更新或删除的数据至关重要。如果您正确设置了数据库表,MySQL 将把每个数据项视为一个唯一条目,因此在保存新数据时,可以确保不会产生重复的 ID。

在将新数据插入到数据库后,我们调用QSqlQuery::lastInsertId()来获取新数据的 ID,并将其保存为currentID变量,以便它成为我们可以从数据库中更新或删除的当前数据。

在使用它们在 Qt 中之前,先在 phpMyAdmin 上测试您的 SQL 查询是一个很好的习惯。您可以立即发现您的 SQL 语句是正确还是错误,而不是等待项目构建,然后尝试,然后再次重建。作为程序员,我们必须以最有效的方式工作。努力工作,聪明工作。

使用 Qt 创建登录界面

在这个教程中,我们将学习如何运用我们的知识,使用 Qt 和 MySQL 创建一个功能性的登录界面。

操作步骤…

通过以下步骤创建您的第一个功能性登录界面:

  1. 首先,打开一个网页浏览器,转到 phpMyAdmin。我们将创建一个名为user的新数据表,如下所示:操作步骤…

  2. 接下来,将我们的第一条数据插入到新创建的表中,并将employeeID设置为现有员工数据的 ID。这样,我们创建的用户帐户将与其中一个员工的数据相关联:操作步骤…

  3. 之后,打开 Qt Creator 并创建一个新的Qt Widgets Application项目。我们将从mainwindow.ui开始。首先,在画布上放置一个堆叠窗口,并确保它包含两个页面。然后,设置堆叠窗口中的两个页面如下:操作步骤…

  4. 然后,在堆叠窗口的第一页,单击窗口顶部的编辑标签顺序按钮,以便我们可以调整程序中窗口部件的顺序:操作步骤…

  5. 单击编辑标签顺序按钮后,您将看到画布上每个部件顶部出现了一些数字。确保数字看起来像这样。否则,单击数字以更改它们的顺序。我们只对堆叠窗口的第一页进行此操作;第二页保持原样即可:操作步骤…

  6. 接下来,右键单击登录按钮,选择转到槽…。然后,确保选择clicked()选项并按确定。Qt 将在项目源文件中为您创建一个槽函数。同样的步骤也要对登出按钮进行操作。

  7. 然后,打开mainwindow.h,在#include <QMainWindow>后添加以下头文件:

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

  1. 之后,在mainwindow.h中添加以下变量:
private:
  Ui::MainWindow *ui;
 QSqlDatabase db;

  1. 完成后,让我们打开mainwindow.cpp,并将以下代码放入类构造函数中:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent),
  ui(new Ui::MainWindow)
{
  ui->setupUi(this);
 ui->stackedWidget->setCurrentIndex(0);
 db = QSqlDatabase::addDatabase("QMYSQL");
 db.setHostName("127.0.0.1");
 db.setUserName("yourusername");
 db.setPassword("yourpassword");
 db.setDatabaseName("databasename");

 if (!db.open())
 {
 qDebug() << "Failed to connect to database.";
 }
}
  1. 之后,我们将定义Login按钮被点击时会发生什么:
void MainWindow::on_loginButton_clicked()
{
  QString username = ui->username->text();
  QString password = ui->password->text();

  QSqlQuery query;
  if (query.exec("SELECT employeeID from user WHERE username = '" + username + "' AND password = '" + password + "'"))
  {
    if (query.size() > 0)
    {
      while (query.next())
      {
        QString employeeID = query.value(0).toString();
        QSqlQuery query2;
        if (query2.exec("SELECT name, age, gender, married FROM employee WHERE id = " + employeeID))
        {
          while (query2.next())
          {
            QString name = query2.value(0).toString();
            QString age = query2.value(1).toString();
            int gender = query2.value(2).toInt();
            bool married = query2.value(3).toBool();
            ui->name->setText(name);
            ui->age->setText(age);

            if (gender == 0)
              ui->gender->setText("Male");
            else
              ui->gender->setText("Female");

            if (married)
              ui->married->setText("Yes");
            else
              ui->married->setText("No");

            ui->stackedWidget->setCurrentIndex(1);
          }
        }
      }
    }
    else
    {
      QMessageBox::warning(this, "Login failed", "Invalid username or password.");
    }
  }
  else
  {
    qDebug() << query.lastError().text();
  }
}
  1. 然后,我们还定义了Log Out按钮被点击时会发生什么:
void MainWindow::on_logoutButton_clicked()
{
  ui->stackedWidget->setCurrentIndex(0);
}
  1. 最后,在主窗口关闭时关闭数据库:
MainWindow::~MainWindow()
{
  db.close();

  delete ui;
}
  1. 现在编译并运行程序,您应该能够使用虚拟帐户登录。登录后,您应该能够看到与用户帐户关联的虚拟员工信息。您也可以通过单击Log Out按钮注销:操作方法…

工作原理…

在此示例中,我们从user表中选择与我们插入到文本字段中的用户名和密码匹配的数据。如果找不到任何内容,这意味着我们提供了无效的用户名或密码。否则,从用户帐户中获取employeeID数据,并进行另一个 SQL 查询,以查找与employeeID变量匹配的employee表中的信息。然后,根据我们程序的 UI 显示数据。

我们必须在编辑标签顺序模式下设置小部件顺序,这样当程序启动时,第一个获得焦点的小部件是用户名行编辑小部件。如果用户在键盘上按下TAB按钮,焦点应切换到第二个小部件,即密码行编辑。错误的小部件顺序将完全破坏用户体验,并驱赶潜在用户。

确保密码行编辑的echoMode选项设置为Password。该设置将隐藏插入到行编辑中的实际密码,并用点符号替换以确保安全。

在模型视图上显示来自数据库的信息

在本示例中,我们将学习如何在程序中的模型视图上显示从 SQL 数据库获取的多组数据。

操作方法…

按照以下步骤在模型视图小部件上显示来自数据库的信息:

  1. 我们将使用名为employee的数据库表,这是我们在上一个示例中使用的。这次,我们需要在employee表中添加更多数据。打开您的 Web 浏览器并登录到 phpMyAdmin 控制面板。为几个员工添加数据,以便稍后在我们的程序中显示:操作方法…

  2. 之后,打开 Qt Creator,创建一个新的Qt Widgets 应用程序项目,然后将 SQL 模块添加到您的项目中。

  3. 接下来,打开mainwindow.ui并从小部件框窗格下的基于项目的小部件中添加一个表格小部件(而不是表格视图)。在画布上选择主窗口,然后单击垂直布局水平布局按钮,使表格小部件固定在主窗口的大小上,即使在调整大小时也是如此:操作方法…

  4. 之后,双击表格小部件,然后会出现一个窗口。在选项卡下,通过单击左上角的+按钮添加五个项目。将项目命名为IDNameAgeGenderMarried。完成后,单击OK操作方法…

  5. 然后,右键单击表格小部件,在弹出菜单中选择转到槽…。滚动到最底部,在弹出窗口中选择itemChanged(QTableWidgetItem*)选项,然后单击OK。将在您的源文件中创建一个槽函数。

  6. 打开mainwindow.h并将这些私有变量添加到我们的MainWindow类中:

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

  1. 我们还将以下类头文件添加到mainwindow.h中:
#include <QtSql>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QMessageBox>
#include <QDebug>
#include <QTableWidgetItem>
  1. 完成后,打开mainwindow.cpp,我们将在那里编写大量代码。首先,我们需要声明程序启动时会发生什么。将以下代码添加到MainWindow类的构造函数中:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent),
  ui(new Ui::MainWindow)
{
  hasInit = false;

  ui->setupUi(this);

  db = QSqlDatabase::addDatabase("QMYSQL");
  db.setHostName("127.0.0.1");
  db.setUserName("yourusername");
  db.setPassword("yourpassword");
  db.setDatabaseName("databasename");

  ui->tableWidget->setColumnHidden(0, true);

  if (db.open())
  {
    QSqlQuery query;
    if (query.exec("SELECT id, name, age, gender, married FROM employee"))
    {
      while (query.next())
      {
        qDebug() << query.value(0) << query.value(1) << query.value(2) << query.value(3) << query.value(4);

        QString id = query.value(0).toString();
        QString name = query.value(1).toString();
        QString age = query.value(2).toString();
        int gender = query.value(3).toInt();
        bool married = query.value(4).toBool();

        ui->tableWidget->setRowCount(ui->tableWidget->rowCount() + 1);

        QTableWidgetItem* idItem = new QTableWidgetItem(id);
        QTableWidgetItem* nameItem = new QTableWidgetItem(name);
        QTableWidgetItem* ageItem = new QTableWidgetItem(age);
        QTableWidgetItem* genderItem = new QTableWidgetItem();

        if (gender == 0)
          genderItem->setData(0, "Male");
        else
          genderItem->setData(0, "Female");

        QTableWidgetItem* marriedItem = new QTableWidgetItem();

        if (married)
          marriedItem->setData(0, "Yes");
        else
          marriedItem->setData(0, "No");

        ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 0, idItem);
        ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 1, nameItem);
        ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 2, ageItem);
        ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 3, genderItem);
        ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 4, marriedItem);
      }

      hasInit = true;
    }
    else
    {
      qDebug() << query.lastError().text();
    }
  }
  else
  {
    qDebug() << "Failed to connect to database.";
  }
}
  1. 之后,声明当表格小部件的项目被编辑时会发生什么。将以下代码添加到名为on_tableWidget_itemChanged()的槽函数中:
void MainWindow::on_tableWidget_itemChanged(QTableWidgetItem *item)
{
  if (hasInit)
  {
    QString id = ui->tableWidget->item(item->row(), 0)->data(0).toString();
    QString name = ui->tableWidget->item(item->row(), 1)->data(0).toString();
    QString age = QString::number(ui->tableWidget->item(item->row(), 2)->data(0).toInt());
    ui->tableWidget->item(item->row(), 2)->setData(0, age);

    QString gender;
    if (ui->tableWidget->item(item->row(), 3)->data(0).toString() == "Male")
    {
      gender = "0";
    }
    else
    {
      ui->tableWidget->item(item->row(), 3)->setData(0, "Female");
      gender = "1";
    }

    QString married;
    if (ui->tableWidget->item(item->row(), 4)->data(0).toString() == "No")
    {
      married = "0";
    }
    else
    {
      ui->tableWidget->item(item->row(), 4)->setData(0, "Yes");
      married = "1";
    }

    qDebug() << id << name << age << gender << married;
    QSqlQuery query;
    if (query.exec("UPDATE employee SET name = '" + name + "', age = '" + age + "', gender = '" + gender + "', married = '" + married + "' WHERE id = " + id))
    {
      QMessageBox::information(this, "Update Success", "Data updated to database.");
    }
    else
    {
      qDebug() << query.lastError().text();
    }
  }
}
  1. 最后,在类析构函数中关闭数据库:
MainWindow::~MainWindow()
{
 db.close();
  delete ui;
}
  1. 现在编译并运行示例,你应该会得到类似这样的结果:How to do it…

它是如何工作的...

表部件类似于电子表格应用程序中看到的表格,比如 Microsoft Excel 和 Open Office Calc。与其他类型的模型视图(如列表视图或树视图)相比,表视图(或表部件)是一个二维模型视图,以行和列的形式显示数据。

在 Qt 中,表视图和表部件的主要区别在于表部件是建立在表视图类之上的,这意味着表部件更容易使用,更适合初学者。然而,表部件的灵活性较差,往往比表视图不够可扩展,如果你想要自定义你的表格,这并不是最佳选择。

从 MySQL 中检索数据后,我们为每个数据项创建了一个QTableWidgetItem项目,并设置应该添加到表部件的哪一列和哪一行。在将项目添加到表部件之前,我们必须通过调用QTableWidget::setRowCount()来增加表的行数。我们也可以通过简单地调用QTableWidget::rowCount()来获取表部件的当前行数。

从左边的第一列被隐藏了,因为我们只是用它来保存数据的 ID,这样我们就可以在数据项发生变化时使用它来更新数据库。

当单元格中的数据发生变化时,槽函数on_tableWidget_itemChanged()将被调用。它不仅在你编辑单元格中的数据时被调用,而且在从数据库中检索到数据后首次添加到表中时也会被调用。为了确保这个函数只在我们编辑数据时触发,我们使用了一个名为hasInit的布尔变量来检查我们是否已经完成了初始化过程(向表中添加了第一批数据)或者没有。如果hasInitfalse,则忽略函数调用。

为了防止用户输入完全无关的数据类型,比如将字母插入到本应为数字的数据单元中,我们在数据被编辑时手动检查数据是否接近我们期望的内容。如果数据与有效数据差距较大,将其恢复为默认值。当然,这是一个简单的技巧,能够完成工作,但并不是最专业的方法。或者,你可以尝试创建一个继承了QItemDelegate类的新类,并定义你的模型视图应该如何行为。然后,调用QTableWidget::setItemDelegate()将该类应用到你的表部件上。

高级 SQL 查询

通过遵循这个步骤,我们将学习如何使用高级 SQL 语句,比如INNER JOINCOUNTLIKEDISTINCT等等。

如何做到这一点...

你可以做的不仅仅是执行简单的 SQL 数据库查询:

  1. 首先,我们需要在数据库中添加一些表,然后才能开始编程部分。打开你的网络浏览器,访问你的 phpMyAdmin。我们需要为这个示例添加几个表才能使其工作:How to do it…

  2. 我将向你展示这个项目所需的每个表的结构以及插入到表中用于测试的虚拟数据。第一个表叫做branch,用于存储虚拟公司不同分支的 ID 和名称:How to do it…

  3. 其次,我们有一个department表,用于存储虚拟公司不同部门的 ID 和名称,它也与分支数据通过分支 ID 相关联:How to do it…

  4. 接下来,我们还有一个employee表,用于存储虚拟公司所有员工的信息。这个表与我们在之前示例中使用的表类似,只是多了两列,分别是birthdaydepartmentIDHow to do it…

  5. 除此之外,我们还有一个名为log的表,其中包含每个员工的登录时间的虚拟记录。loginTime列可以是timestampdate time变量类型:操作步骤...

  6. 最后,我们还有在前面的示例中使用的user表:操作步骤...

  7. 我们已经完成了数据库;让我们继续进行 Qt。打开 Qt Creators,这一次,不再选择Qt Widgets Application,而是创建Qt Console Application操作步骤...

  8. 创建完控制台项目后,打开项目文件(.pro)并将 SQL 模块添加到项目中:

QT += core sql
QT -= gui
  1. 接下来,打开main.cpp并在源文件顶部添加以下头文件:
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QDate>
#include <QDebug>
  1. 然后,添加以下函数来显示年龄超过 30 岁的员工:
void filterAge()
{
  qDebug() << "== Employees above 30 year old =============";
  QSqlQuery query;
  if (query.exec("SELECT name, age FROM employee WHERE age > 30"))
  {
    while (query.next())
    {
      qDebug() << query.value(0).toString() << query.value(1).toString();
    }
  }
  else
  {
    qDebug() << query.lastError().text();
  }

  qDebug() << "\n";
}
  1. 之后,添加这个函数来显示每个员工的部门和分支信息:
void getDepartmentAndBranch()
{
  qDebug() << "== Get employees' department and branch =============";

  QSqlQuery query;
  if (query.exec("SELECT myEmployee.name, department.name, branch.name FROM (SELECT name, departmentID FROM employee) AS myEmployee INNER JOIN department ON department.id = myEmployee.departmentID INNER JOIN branch ON branch.id = department.branchID"))
  {
    while (query.next())
    {
      qDebug() << query.value(0).toString() << query.value(1).toString() << query.value(2).toString();
    }
  }
  else
  {
    qDebug() << query.lastError().text();
  }

  qDebug() << "\n";
}
  1. 接下来,添加这个函数,显示在纽约分支工作且年龄不到 30 岁的员工:
void filterBranchAndAge()
{
  qDebug() << "== Employees from New York and age below 30 =============";

  QSqlQuery query;
  if (query.exec("SELECT myEmployee.name, myEmployee.age, department.name, branch.name FROM (SELECT name, age, departmentID FROM employee) AS myEmployee INNER JOIN department ON department.id = myEmployee.departmentID INNER JOIN branch ON branch.id = department.branchID WHERE branch.name = 'New York' AND age < 30"))
  {
    while (query.next())
    {
      qDebug() << query.value(0).toString() << query.value(1).toString() << query.value(2).toString() << query.value(3).toString();
    }
  }
  else
  {
    qDebug() << query.lastError().text();
  }

  qDebug() << "\n";
}
  1. 然后,添加这个函数来计算虚拟公司中女性员工的总数:
void countFemale()
{
  qDebug() << "== Count female employees =============";

  QSqlQuery query;
  if (query.exec("SELECT COUNT(gender) FROM employee WHERE gender = 1"))
  {
    while (query.next())
    {
      qDebug() << query.value(0).toString();
    }
  }
  else
  {
    qDebug() << query.lastError().text();
  }

  qDebug() << "\n";
}
  1. 完成后,我们将添加另一个函数,过滤员工列表,并仅显示以Ja开头的员工:
void filterName()
{
  qDebug() << "== Employees name start with 'Ja' =============";

  QSqlQuery query;
  if (query.exec("SELECT name FROM employee WHERE name LIKE '%Ja%'"))
  {
    while (query.next())
    {
      qDebug() << query.value(0).toString();
    }
  }
  else
  {
    qDebug() << query.lastError().text();
  }

  qDebug() << "\n";
}
  1. 接下来,我们还添加另一个函数,显示在8 月份生日的员工:
void filterBirthday()
{
  qDebug() << "== Employees birthday in August =============";

  QSqlQuery query;
  if (query.exec("SELECT name, birthday FROM employee WHERE MONTH(birthday) = 8"))
  {
    while (query.next())
    {
      qDebug() << query.value(0).toString() << query.value(1).toDate().toString("d-MMMM-yyyy");
    }
  }
  else
  {
    qDebug() << query.lastError().text();
  }

  qDebug() << "\n";
}
  1. 然后,我们添加最后一个函数,检查谁在2016 年 4 月 27 日登录到虚拟系统,并在终端上显示他们的名字:
void checkLog()
{
  qDebug() << "== Employees who logged in on 27 April 2016 =============";

  QSqlQuery query;
  if (query.exec("SELECT DISTINCT myEmployee.name, FROM (SELECT id, name FROM employee) AS myEmployee INNER JOIN user ON user.employeeID = myEmployee.id INNER JOIN log ON log.userID = user.id WHERE DATE(log.loginTime) = '2016-04-27'"))
  {
    while (query.next())
    {
      qDebug() << query.value(0).toString();
    }
  }
  else
  {
    qDebug() << query.lastError().text();
  }

  qDebug() << "\n";
}
  1. 最后,在我们的main()函数中,连接我们的程序到 MySQL 数据库,并调用我们在前面步骤中定义的所有函数。之后,关闭数据库连接,我们就完成了:
int main(int argc, char *argv[])
{
  QCoreApplication a(argc, argv);

  QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
  db.setHostName("127.0.0.1");
  db.setUserName("reonyx");
  db.setPassword("reonyx");
  db.setDatabaseName("testing");

  if (db.open())
  {
    filterAge();
    getDepartmentAndBranch();
    filterBranchAndAge();
    countFemale();
    filterName();
    filterBirthday();
    checkLog();

    db.close();
  }
  else
  {
    qDebug() << "Failed to connect to database.";
  }

  return a.exec();
}
  1. 现在编译并运行项目,您应该看到一个终端窗口,显示了之前定义的数据库中的过滤结果:操作步骤...

工作原理...

控制台应用程序根本没有任何 GUI,只在终端窗口中显示文本。这通常用于后端系统,因为它使用的资源比小部件应用程序少。在本例中使用它是因为它更快地显示结果,而无需在程序中放置任何小部件,这在这种情况下是不需要的。

我们将 SQL 查询分成不同的函数,以便更容易维护代码,避免变得太混乱。请注意,在 C++中,函数必须位于main()函数之前,否则它们将无法被main()调用。

还有更多...

在前面的示例中使用的INNER JOIN语句将两个表连接在一起,并选择两个表中的所有行,只要两个表中的列之间存在匹配。在 MySQL(以及其他类型的 SQL 架构)中,还有许多其他类型的JOIN语句,例如LEFT JOINRIGHT JOINFULL OUTER JOIN等。以下图表显示了不同类型的JOIN语句及其效果:

更多内容...

  1. LIKE语句通常用于在数据库中搜索字符串变量而不是完整单词。请注意,搜索关键字之前和之后有两个%符号。

  2. 在前面的示例中使用的DISTINCT语句过滤掉具有完全相同变量的结果。例如,如果没有DISTINCT语句,您将在终端中看到两个 Larry King 的版本,因为他在同一天登录系统有两条记录。通过添加DISTINCT语句,MySQL 将消除重复的结果之一,并确保每个结果都是唯一的。

  3. 您可能想知道d-MMMM-yyyy代表什么,为什么我们在前面的例子中使用它。这实际上是提供给 Qt 中的QDateTime类的一个表达式,用于使用给定的格式显示日期时间结果。在这种情况下,它将改变我们从 MySQL 获取的日期时间数据2016-08-06,转换为我们指定的格式,结果为6-August-2016。更多信息,请查看 Qt 的文档doc.qt.io/qt-5/qdatetime.html#toString,其中包含可以用来确定日期和时间字符串格式的完整表达式列表。

第九章:使用 Qt Web 引擎开发 Web 应用程序

在本章中,我们将涵盖以下内容:

  • 介绍 Qt WebEngine

  • WebView 和 Web 设置

  • 在项目中嵌入 Google 地图

  • 从 JavaScript 调用 C++函数

  • 从 C++调用 JavaScript 函数

介绍

Qt 包括一个名为Qt WebEngine的模块,允许我们将 Web 浏览器小部件嵌入到我们的程序中,并用它来显示网页或本地 HTML 内容。在 5.6 版本之前,Qt 使用另一个类似的模块称为Qt WebKit,现在已经被弃用,并且已经被基于 Chromium 的Web 引擎模块所取代。Qt 还允许 JavaScript 和 C++代码之间的通信通过“Web 通道”,这使我们能够更有效地使用这个模块。

介绍 Qt WebEngine

在这个示例项目中,我们将探索 Qt 中 Web 引擎模块的基本功能,并尝试构建一个简单的工作 Web 浏览器。自 Qt 5.6 以来,Qt 的 WebKit 模块已被弃用,并由基于 Google 的 Chromium 引擎的 WebEngine 模块所取代。请注意,当撰写本章时,WebEngine 仍在积极开发中,可能会在不久的将来发生变化。

操作方法…

首先,让我们设置我们的 Web 引擎项目:

  1. 首先,如果您的计算机上没有安装 Microsoft Visual Studio,则需要下载并安装它。这是因为目前,Qt 的 WebEngine 模块只能与 Visual C++编译器一起使用,而不能与其他编译器(如 MinGW 或 Clang)一起使用。这可能会在将来发生变化,但这一切取决于 Google 是否愿意让他们的 Chromium 引擎支持其他编译器。与此同时,您可以从这里下载最新的 Visual Studio:www.visualstudio.com

  2. 同时,您可能还需要确保您计算机上安装的 Qt 支持 Visual C++编译器。您可以使用 Qt 的维护工具向 Qt 安装mvc2015组件。还要确保您在 Qt 中也安装了Qt WebEngine组件:操作方法…

  3. 完成后,打开 Qt Creator 并创建一个新的Qt Widgets 应用程序项目。这次,您必须选择使用 Visual C++编译器的工具包:操作方法…

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

QT += core gui webengine webenginewidgets

  1. 打开mainwindow.ui并删除menuBarmainToolBarstatusBar对象,因为在这个项目中我们不需要它们:操作方法…

  2. 在画布上放置两个水平布局,然后在顶部的布局中放置一个行编辑小部件和一个按钮:操作方法…

  3. 之后,选择画布并单击编辑器顶部的垂直布局按钮:操作方法…

  4. 单击垂直布局按钮后,布局将扩展并遵循主窗口的大小。行编辑也将根据水平布局的宽度水平扩展:操作方法…

  5. 接下来,在行编辑的左侧添加两个按钮。我们将使用这两个按钮在页面历史记录之间进行后退和前进。然后,在主窗口底部添加一个进度条小部件,以便我们可以了解页面是否已经加载完成,或者加载仍在进行中。此时我们不必担心中间的水平布局,因为我们将在稍后使用 C++代码将 Web 视图添加到其中,然后该空间将被占用:操作方法…

  6. 右键单击其中一个按钮,选择转到槽…,然后选择clicked()并单击确定。槽函数将自动在mainwindow.hmainwindow.cpp中为您创建。对所有其他按钮也重复此步骤。

  7. 之后,右键单击行编辑并选择转到槽…,然后选择returnPressed()并单击确定。现在mainwindow.hmainwindow.cpp中将自动为您创建另一个槽函数。

  8. 现在我们完成了 UI 设计,让我们转到mainwindow.h。我们需要做的第一件事是在mainwindow.h中添加以下头文件:

#include <QtWebEngineWidgets/QtWebEngineWidgets>
  1. 然后,在类析构函数下声明loadUrl()函数:
public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

  void loadUrl();

  1. 之后,在mainwindow.h中添加一个名为loading()的自定义槽函数,因为我们很快就会用到它:
private slots:
  void on_goButton_clicked();
  void on_address_returnPressed();
  void on_backButton_clicked();
  void on_forwardButton_clicked();
  void loading(int progress);

  1. 最后,声明一个QWebEngineView对象并将其命名为webview
private:
  Ui::MainWindow *ui;
  QWebEngineView* webview;

  1. 完成后,打开mainwindow.cpp并初始化 web 引擎视图。然后,将其添加到第二个水平布局中,并将其loadProgress()信号连接到我们刚刚添加到mainwindow.hloading()槽函数:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent),
  ui(new Ui::MainWindow)
{
  ui->setupUi(this);

  webview = new QWebEngineView;
  ui->horizontalLayout_2->addWidget(webview);

  connect(webview, SIGNAL(loadProgress(int)), SLOT(loading(int)));
}
  1. 之后,声明loadUrl()函数被调用时会发生什么:
void MainWindow::loadUrl()
{
  QUrl url = QUrl(ui->address->text());
  url.setScheme("http");
  webview->page()->load(url);
}
  1. 接下来,当单击Go按钮或单击Return/Enter键时,调用loadUrl()函数:
void MainWindow::on_goButton_clicked()
{
  loadUrl();
}

void MainWindow::on_address_returnPressed()
{
  loadUrl();
}
  1. 至于另外两个按钮,如果在历史堆栈中可用,我们将要求 web 视图加载上一页或下一页:
void MainWindow::on_backButton_clicked()
{
  webview->back();
}

void MainWindow::on_forwardButton_clicked()
{
  webview->forward();
}
  1. 最后,在加载网页时更改progressBar的值:
void MainWindow::loading(int progress)
{
  ui->progressBar->setValue(progress);
}
  1. 现在构建并运行程序,您将获得一个非常基本但功能齐全的网页浏览器!如何做…

工作原理…

旧的 web 视图系统基于苹果的 WebKit 引擎,仅在 Qt 5.5 及其前身中可用。自 5.6 以来,Qt 完全放弃了 WebKit,并用 Google 的 Chromium 引擎替换。API 已完全更改,因此一旦迁移到 5.6,与 Qt WebKit 相关的所有代码都将无法正常工作。如果您是 Qt 的新手,建议跳过 WebKit 并学习 WebEngine API,因为它正在成为 Qt 的新标准。如果您以前使用过 Qt 的 WebKit,本网页将教您如何将旧代码迁移到 WebEngine,wiki.qt.io/Porting_from_QtWebKit_to_QtWebEngine

在第 16 步,我们将属于 web 视图小部件的loadProgress()信号连接到loading()槽函数。当在第 17 步通过调用QWebEnginePage::load()请求加载网页时,信号将自动调用。如果需要,您还可以连接loadStarted()loadFinished()信号。

在第 17 步,我们使用QUrl类将从行编辑中获取的文本转换为 URL 格式。默认情况下,如果不指定 URL 方案(httphttpsftp等),我们插入的地址将导致本地路径。如果我们给出packtpub.com而不是http://packtpub.com,则可能无法加载页面。因此,我们通过调用QUrl::setScheme()手动为其指定 URL 方案。这将确保在将其传递给 web 视图之前,地址格式正确。

还有更多…

如果您正在运行 Qt 5.6 或更高版本,并且出于某种原因需要 Webkit 模块用于您的项目(通常用于维护旧项目),您可以从 GitHub 获取模块代码并自行构建:

github.com/qt/qtwebkit

WebView 和 web 设置

在本节中,我们将深入探讨 Qt 的 WebEngine 中可用的功能,并探索我们可以使用的设置来自定义我们的 WebView。我们将使用上一个示例的源文件,并向其添加更多代码。

如何做…

让我们探索一些 Qt WebEngine 的基本功能:

  1. 首先,打开mainwindow.ui并在进度条下添加一个垂直布局。然后,在垂直布局中添加一个纯文本编辑小部件(在输入小部件类别下),以及一个推送按钮。将推送按钮的显示更改为加载 HTML,并将纯文本编辑小部件的plaintext属性设置为以下内容:
<Img src="img/googlelogo_color_272x92dp.png"></img>
<h1>Hello World!</h1>
<h3>This is our custom HTML page.</h3>

<script>alert("Hello!");</script>

如何做…

  1. 接下来,转到文件 | 新建文件或项目。然后会弹出一个窗口,要求你选择一个文件模板。在Qt类别下选择Qt 资源文件,然后点击选择...按钮。输入你想要的文件名,然后点击下一步,接着点击完成操作步骤...

  2. 之后,通过在项目窗格中右键单击刚刚创建的资源文件并选择在编辑器中打开选项来打开资源文件。一旦文件被编辑器打开,点击添加按钮,然后点击添加前缀。然后,将前缀设置为/,点击添加,接着点击添加文件。这时,文件浏览器窗口会出现,我们会选择tux.png图像文件并点击打开。现在我们已经将图像文件添加到我们的项目中,它将被嵌入到可执行文件(.exe)中一起编译:操作步骤...

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

#include <QMainWindow>
#include <QtWebEngineWidgets/QtWebEngineWidgets>
#include <QDebug>
#include <QFile>

  1. 然后,确保以下函数和指针已在mainwindow.h中声明:
public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();
  void loadUrl();

private slots:
  void on_goButton_clicked();
  void on_address_returnPressed();
  void on_backButton_clicked();
  void on_forwardButton_clicked();

  void startLoading();
  void loading(int progress);
  void loaded(bool ok);

 void on_loadHtml_clicked();
private:
  Ui::MainWindow *ui;
  QWebEngineView* webview;

  1. 完成后,打开mainwindow.cpp并将以下代码添加到类构造函数中:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent),
  ui(new Ui::MainWindow)
{
  ui->setupUi(this);

  webview = new QWebEngineView;
  ui->horizontalLayout_2->addWidget(webview);

  //webview->page()->settings()>setAttribute(QWebEngineSettings::JavascriptEnabled, false);
  //webview->page()->settings()->setAttribute(QWebEngineSettings::AutoLoadImages, false);

  //QString fontFamily = webview->page()->settings()->fontFamily(QWebEngineSettings::SerifFont);
  QString fontFamily = webview->page()->settings()->fontFamily(QWebEngineSettings::SansSerifFont);
  int fontSize = webview->page()->settings()->fontSize(QWebEngineSettings::MinimumFontSize);
  QFont myFont = QFont(fontFamily, fontSize);
  webview->page()->settings()->setFontFamily(QWebEngineSettings::StandardFont, myFont.family());

  QFile file("://tux.png");
  if (file.open(QFile::ReadOnly))
  {
    QByteArray data = file.readAll();
    webview->page()->setContent(data, "image/png");
  }
  else
  {
    qDebug() << "File cannot be opened.";
  }

  connect(webview, SIGNAL(loadStarted()), SLOT(startLoading()));
  connect(webview, SIGNAL(loadProgress(int)), SLOT(loading(int)));
  connect(webview, SIGNAL(loadFinished(bool)), SLOT(loaded(bool)));
}
  1. MainWindow::loadUrl()函数仍然与之前的例子相同,它在加载页面之前将 URL 方案设置为http
void MainWindow::loadUrl()
{
  QUrl url = QUrl(ui->address->text());
  url.setScheme("http");
  webview->page()->load(url);
}
  1. 对于以下函数,情况也是一样的:
void MainWindow::on_goButton_clicked()
{
  loadUrl();
}

void MainWindow::on_address_returnPressed()
{
  loadUrl();
}

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

void MainWindow::on_forwardButton_clicked()
{
  webview->forward();
}
  1. 在之前的例子中,我们只有MainWindow::loading(),它在网页加载时设置进度条的值。这次,我们还添加了MainWindow::startLoading()MainWindow::loaded()槽函数,它们将被loadStarted()loadFinished()信号调用。这两个函数的作用基本上是在页面开始加载时显示进度条,在页面加载完成时隐藏进度条:
void MainWindow::startLoading()
{
  ui->progressBar->show();
}

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

void MainWindow::loaded(bool ok)
{
  ui->progressBar->hide();
}
  1. 最后,当点击加载 HTML按钮时,我们调用webview->loadHtml()将纯文本转换为 HTML 内容:
void MainWindow::on_loadHtml_clicked()
{
  webview->setHtml(ui->source->toPlainText());
}
  1. 现在构建并运行程序,你应该会看到类似这样的东西:操作步骤...

工作原理...

在这个例子中,我们使用 C++加载图像文件,并将其设置为 WebView 的默认内容(而不是空白页面)。我们可以通过在启动时加载默认 HTML 文件和图像来实现相同的结果。

类构造函数中的一些代码已被注释掉。你可以删除双斜杠//,看看它的不同之处——JavaScript 警报将不再出现(因为 JavaScript 被禁用),任何图像也将不再出现在你的 Web 视图中。

你还可以尝试将字体系列从QWebEngineSettings::SansSerifFont改为QWebEngineSettings::SerifFont。你会注意到字体在 Web 视图中的显示略有不同:

工作原理...

通过点击加载 HTML按钮,我们要求 WebView 将纯文本编辑小部件的内容视为 HTML 代码并将其加载为 HTML 页面。你可以使用这个方法来制作一个由 Qt 驱动的简单 HTML 编辑器!

在项目中嵌入谷歌地图

在这个例子中,我们将学习如何通过 Qt 的 WebEngine 模块在我们的项目中嵌入谷歌地图。这个例子并不太关注 Qt 和 C++,而是关注 HTML 代码中的谷歌地图 API。

操作步骤...

让我们按照以下步骤创建一个显示谷歌地图的程序:

  1. 首先,创建一个新的Qt Widgets 应用程序项目,并移除状态栏、菜单栏和工具栏。

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

QT += core gui webengine webenginewidgets

  1. 然后,打开mainwindow.ui并为画布添加一个垂直布局。然后,选择画布并点击画布顶部的垂直布局按钮。你会得到类似这样的东西:操作步骤...

  2. 然后,打开mainwindow.cpp并在源代码顶部添加以下头文件:

#include <QtWebEngineWidgets/QWebEngineView>
#include <QtWebEngineWidgets/QWebEnginePage>
#include <QtWebEngineWidgets/QWebEngineSettings>
  1. 之后,将以下代码添加到MainWindow构造函数中:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent),
  ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  QWebEngineView* webview = new QWebEngineView;
  QUrl url = QUrl("qrc:/map.html");
  webview->page()->load(url);
  ui->verticalLayout->addWidget(webview);
}
  1. 然后,转到文件 | 新建文件或项目并创建一个 Qt 资源文件(.qrc)。我们将在项目中添加一个名为map.html的 HTML 文件:如何操作...

  2. 完成后,用您喜欢的文本编辑器打开map.html。不建议使用 Qt Creator 打开 HTML 文件,因为它不提供 HTML 语法的颜色编码。

  3. 之后,我们将开始编写 HTML 代码,声明重要的标签,如<html><head><body>,如下所示:

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body ondragstart="return false">
  </body>
</html>
  1. 然后,在 body 中添加一个<div>标签,并将其 ID 设置为map-canvas
<body ondragstart="return false">
  <div id="map-canvas" />
</body>
  1. 之后,将以下代码添加到 HTML 文档的头部:
<meta name="viewport" content="initial-scale=1.0,user-scalable=no" />
<style type="text/css">
  html { height: 100% }
  body { height: 100%; margin: 0; padding: 0 }
  #map-canvas { height: 100% }
</style>
<script type="text/javascript" src="img/js?key=YOUR_KEY_HERE&libraries=drawing"></script>
  1. 然后,将以下代码添加到 HTML 文档的头部,就在我们在上一步中插入的代码的底部:
<script type="text/javascript">
  var map;
  function initialize()
  {
    // Add map
    var mapOptions =
    {
      center: new google.maps.LatLng(40.705311, -74.2581939),
        zoom: 6
    };

    map = new google.maps.Map(document.getElementById("map-canvas"),mapOptions);

    // Add event listener
    google.maps.event.addListener(map, 'zoom_changed', function()
    {
      //alert(map.getZoom());
    });

    // Add marker
    var marker = new google.maps.Marker(
    {
      position: new google.maps.LatLng(40.705311, -74.2581939),
        map: map,
        title: "Marker A",
    });
    google.maps.event.addListener(marker, 'click', function()
    {
      map.panTo(marker.getPosition());
    });
    marker.setMap(map);

    // Add polyline
    var points = [ new google.maps.LatLng(39.8543, -73.2183), new google.maps.LatLng(41.705311, -75.2581939), new google.maps.LatLng(40.62388, -75.5483) ];
    var polyOptions =
    {
      path: points,
      strokeColor: '#FF0000',
      strokeOpacity: 1.0,
      strokeWeight: 2
    };
    historyPolyline = new google.maps.Polyline(polyOptions);
    historyPolyline.setMap(map);

    // Add polygon
    var points = [ new google.maps.LatLng(37.314166, -75.432),new google.maps.LatLng(40.2653, -74.4325), new google.maps.LatLng(38.8288, -76.5483) ];
      var polygon = new google.maps.Polygon(
    {
      paths: points,
      fillColor:  '#000000',
      fillOpacity: 0.2,
      strokeWeight: 3,
      strokeColor: '#fff000',
    });
    polygon.setMap(map);

    // Setup drawing manager
    var drawingManager = new google.maps.drawing.DrawingManager();
    drawingManager.setMap(map);
  }

  google.maps.event.addDomListener(window, 'load', initialize);

</script>
  1. 完成后,编译并运行项目。您应该看到类似于这样的东西:如何操作...

工作原理...

谷歌允许您通过使用他们的 JavaScript 库谷歌地图 API 在网页中嵌入谷歌地图。通过 Qt 的 WebEngine 模块,我们可以通过将 HTML 文件加载到我们的 Web 视图小部件中来在我们的 C++项目中嵌入谷歌地图,该小部件使用谷歌地图 API。这种方法的唯一缺点是在没有互联网连接时无法加载地图。

谷歌允许您的网站每天多次调用任何谷歌 API。如果您计划有更多的流量,您应该从谷歌获取一个免费的 API 密钥。转到console.developers.google.com获取一个免费的密钥,并用从谷歌获得的 API 密钥替换 JavaScript 源路径中的YOUR_KEY_HERE一词。

我们必须定义一个<div>对象,它作为地图的容器。然后,当我们初始化地图时,我们指定<div>对象的 ID,以便 Google Maps API 知道在嵌入地图时要查找哪个 HTML 元素。

默认情况下,我们将地图的中心设置为纽约的坐标,并将默认缩放级别设置为6。然后,我们添加了一个事件侦听器,当地图的缩放级别发生变化时触发。删除代码中的双斜杠//以查看其运行情况。

之后,我们还通过 JavaScript 向地图添加了一个标记。标记也附加了一个事件侦听器,当单击标记时将触发panTo()函数。它的作用基本上是将地图视图移动到已单击的标记。

虽然我们已经将绘图管理器添加到地图中(地图卫星按钮旁边的图标按钮),允许用户在地图上绘制任何类型的形状,但也可以使用 JavaScript 手动添加形状,类似于我们在上一步中添加标记的方式。

最后,您可能已经注意到标题被添加到mainwindow.cpp而不是mainwindow.h。这完全没问题,除非您在mainwindow.h中声明类指针;那么您必须在其中包含这些标题。

从 JavaScript 调用 C++函数

在这个教程中,我们将学习如何运用我们的知识,使用 Qt 和 MySQL 创建一个功能性的登录界面。

操作步骤

通过以下步骤学习如何从 JavaScript 调用 C++函数:

  1. 首先,创建一个Qt Widgets 应用程序项目,完成后,打开项目文件(.pro)并将以下模块添加到项目中:
QT += core gui webengine webenginewidgets

  1. 然后,打开mainwindow.ui并删除工具栏、菜单栏和状态栏,因为在这个示例程序中我们不需要这些。

  2. 之后,向画布添加一个垂直布局,然后选择画布并单击画布顶部的垂直布局按钮。然后,在垂直布局的顶部添加一个文本标签,并将其文本设置为Hello!。还可以通过设置其stylesheet属性使其字体变大:

font: 75 26pt "MS Shell Dlg 2";

如何操作...

  1. 接下来,转到文件 | 新建文件或项目并创建一个资源文件。然后,将一个空的 HTML 文件和所有 JavaScript 文件、CSS 文件、字体文件等添加到 jQuery、Boostrap 和 Font Awesome 的项目资源中:操作步骤…

  2. 之后,打开 HTML 文件,这里称为test.html。首先,将所有必要的 JavaScript 和 CSS 文件链接到 HTML 源代码中,放在<head>标签之间:

<!DOCTYPE html>
<html>
  <head>
    <script src="img/qwebchannel.js"></script>

    <script src="img/jquery.min.js"></script>
    <script src="img/bootstrap.js"></script>

    <link rel="stylesheet" type="text/css"       href="css/bootstrap.css">
    <link rel="stylesheet" type="text/css" href="css/font-      awesome.css">
  </head>
  <body>
  </body>
</html>
  1. 然后,将以下 JavaScript 添加到<head>元素中,放在<script>标签之间:
<script>
  $(document).ready(function()
  {
    new QWebChannel(qt.webChannelTransport, function(channel)
      {
        mainWindow = channel.objects.mainWindow;
      });

      $("#login").click(function(e)
      {
        e.preventDefault();

        var user = $("#username").val();
        var pass = $("#password").val();
        mainWindow.showLoginInfo(user, pass);
      });

      $("#changeText").click(function(e)
      {
        e.preventDefault();

        mainWindow.changeQtText("Good bye!");
      });
  });
</script>
  1. 然后,将以下代码添加到<body>元素中:
<div class="container-fluid">
  <form id="example-form" action="#" class="container-fluid">
    <div class="form-group">
      <div class="col-md-12"><h3>Call C++ Function from Javascript</h3></div>

      <div class="col-md-12"><div class="alert alert-info" role="alert"><i class="fa fa-info-circle"></i> <span id="infotext">Click "Login" to send username and password variables to C++.Click "Change Cpp Text" to change the text label on Qt GUI.</span></div></div>

      <div class="col-md-12">
        <label>Username:</label> <input id="username" type="text"><p />
      </div>

      <div class="col-md-12">
        <label>Password:</label> <input id="password" type="password"><p />
      </div>

      <div class="col-md-12">
        <button id="login" class="btn btn-success" type="button"><i class="fa fa-check"></i> Login</button> <button id="changeText" class="btn btn-primary" type="button"><i class="fa fa-pencil"></i> Change Cpp Text</button>
      </div>
    </div>
  </form>
</div>
  1. 完成后,让我们打开mainwindow.h并向MainWindow类添加以下公共函数:
public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

  Q_INVOKABLE void changeQtText(QString newText);
  Q_INVOKABLE void showLoginInfo(QString user, QString pass);

  1. 之后,打开mainwindow.cpp并将以下头文件添加到源代码顶部:
#include <QtWebEngineWidgets/QWebEngineView>
#include <QtWebChannel/QWebChannel>
#include <QMessageBox>
  1. 然后,将以下代码添加到MainWindow构造函数中:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent),
  ui(new Ui::MainWindow)
{
  qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "1234");

  ui->setupUi(this);

  QWebEngineView* webview = new QWebEngineView();
  ui->verticalLayout->addWidget(webview);

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

  webview->page()->load(QUrl("qrc:///html/test.html"));
}
  1. 之后,我们将声明changeQtText()showLoginInfo()被调用时发生的事情:
void MainWindow::changeQtText(QString newText)
{
  ui->label->setText(newText);
}

void MainWindow::showLoginInfo(QString user, QString pass)
{
  QMessageBox::information(this, "Login info", "Username is " + user + " and password is " + pass);
}
  1. 现在让我们编译并运行程序;您应该会看到类似以下截图的内容。如果单击Change Cpp Text按钮,顶部的Hello!将变为Goodbye!如果单击Login按钮,将会出现一个消息框,显示您在UsernamePassword输入字段中输入的内容:操作步骤…

工作原理…

在这个例子中,我们使用了两个 JavaScript 库,jQuery 和 Boostrap。我们还使用了一个叫做Font Awesome的图标字体包。这些第三方附加组件被用来使 HTML 用户界面更有趣,并对不同的屏幕分辨率做出响应。我们还使用了 jQuery 来检测文档的就绪状态,以及获取输入字段的值。您可以从jquery.com/download下载 jQuery,从getbootstrap.com/getting-started/#download下载 Bootstrap,从fontawesome.io下载 Font Awesome。

Qt 的 WebEngine 使用一种称为Web Channel的机制,它使 C++程序和 HTML 页面之间能够进行点对点通信。WebEngine 模块提供了一个 JavaScript 库,使集成变得更加容易。JavaScript 默认嵌入在您的项目资源中,因此您不需要手动将其导入到项目中。您只需要通过调用以下内容将其包含在 HTML 页面中:

<script src="img/qwebchannel.js"></script>

一旦您包含了qwebchannel.js,您就可以初始化QWebChannel类,并将我们之前在 C++中注册的 Qt 对象分配给 JavaScript 变量。

在 C++中,如下所示:

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

然后在 JavaScript 中,如下所示:

new QWebChannel(qt.webChannelTransport, function(channel)
{
  mainWindow = channel.objects.mainWindow;
});

您可能想知道这行是什么意思:

qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "1234");

Qt 的 Web 引擎使用远程调试方法来检查 JavaScript 错误和其他问题。数字1234定义了您想要用于远程调试的端口号。一旦启用了远程调试,您可以通过打开基于 Chromium 的 Web 浏览器,如 Google Chrome(这在 Firefox 和其他浏览器中不起作用),并输入http://127.0.0.1:1234来访问调试页面。然后您将看到一个类似于这样的页面:

工作原理…

第一个页面将显示当前在您的程序中运行的所有 HTML 页面,这里是test.html。单击页面链接,它将带您到另一个用于检查的页面。您可以使用此功能来检查 CSS 错误、JavaScript 错误、丢失的文件等。请注意,一旦您的程序没有错误并且准备部署,应该禁用远程调试。这是因为远程调试需要时间来启动,并且会增加程序的启动时间。

如果您想要从 JavaScript 调用 C++函数,您必须在函数声明前放置Q_INVOKABLE宏;否则,它将无法工作:

Q_INVOKABLE void changeQtText(QString newText);

从 C++调用 JavaScript 函数

在先前的示例中,我们已经学习了如何通过 Qt 的 Web Channel 系统从 JavaScript 调用 C++函数。在这个示例中,我们将尝试做相反的事情:从 C++代码调用 JavaScript 函数。

操作步骤…

我们可以通过以下步骤从 C++中调用 JavaScript 函数:

  1. 像往常一样,创建一个新的Qt Widgets Application项目,并将webenginewebenginewidgets模块添加到你的项目中。

  2. 然后,打开mainwindow.ui并移除工具栏、菜单栏和状态栏。

  3. 在此之后,将垂直布局和水平布局添加到画布中。然后,选择画布并单击垂直布局。确保水平布局位于垂直布局的底部。

  4. 将两个按钮添加到水平布局中;一个叫做更改 HTML 文本,另一个叫做播放 UI 动画。右键单击其中一个按钮,然后单击转到槽…。现在会弹出一个窗口,要求你选择一个信号。选择clicked()选项,然后单击确定。Qt 将自动向你的源代码中添加一个槽函数。对另一个按钮也重复此步骤:操作步骤…

  5. 现在,打开mainwindow.h并向其中添加以下头文件:

#include <QtWebEngineWidgets/QWebEngineView>
#include <QtWebChannel/QWebChannel>
#include <QMessageBox>
  1. 然后,声明一个名为webviewQWebEngineView对象的类指针:
public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

  QWebEngineView* webview;

  1. 在此之后,打开mainwindow.cpp并将以下代码添加到MainWindow构造函数中:
MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent),
  ui(new Ui::MainWindow)
{
  //qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "1234");

  ui->setupUi(this);

  webview = new QWebEngineView();
  ui->verticalLayout->addWidget(webview);

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

  webview->page()->load(QUrl("qrc:///html/test.html"));
}
  1. 然后,定义当单击changeHtmlText按钮和playUIAnimation按钮时会发生什么:
void MainWindow::on_changeHtmlTextButton_clicked()
{
  webview->page()->runJavaScript("changeHtmlText('Text has been replaced by C++!');");
}

void MainWindow::on_playUIAnimationButton_clicked()
{
  webview->page()->runJavaScript("startAnim();");
}
  1. 完成后,让我们通过转到文件 | 新建文件或项目来为我们的项目创建一个资源文件。然后,在Qt类别下选择Qt 资源文件,并单击选择。然后,插入你想要的文件名,然后单击下一步,接着单击完成

  2. 然后,将一个空的 HTML 文件和所有必需的附加组件(jQuery、Bootstrap 和 Font Awesome)添加到我们的项目资源中。同时,也将tux.png图像文件添加到资源文件中,因为我们将在短时间内使用它。

  3. 在此之后,打开我们刚创建的 HTML 文件并将其添加到项目资源中,在我们的例子中,它叫做test.html。然后,将以下 HTML 代码添加到文件中:

<!DOCTYPE html>
<html>
  <head>
    <script src="img/qwebchannel.js"></script>

    <script src="img/jquery.min.js"></script>
    <script src="img/bootstrap.js"></script>

    <link rel="stylesheet" type="text/css" href="css/bootstrap.css">
    <link rel="stylesheet" type="text/css" href="css/font-awesome.css">
  </head>
  <body>
  </body>
</html>
  1. 将以下 JavaScript 代码添加到我们的 HTML 文件的<head>元素中,该代码被包裹在<script>标签中:
<script>
  $(document).ready(function()
  {
    $("#tux").css({ opacity:0, width:"0%", height:"0%" });
    $("#listgroup").hide();
    $("#listgroup2").hide();

    new QWebChannel(qt.webChannelTransport, function(channel)
    {
      mainWindow = channel.objects.mainWindow;
    });
  });

  function changeHtmlText(newText)
  {
    $("#infotext").html(newText);
  }

  function startAnim()
  {
    // Reset
    $("#tux").css({ opacity:0, width:"0%", height:"0%" });
    $("#listgroup").hide();
    $("#listgroup2").hide();

    $("#tux").animate({ opacity:1.0, width:"100%", height:"100%" }, 1000, function()
    {
      // tux animation complete
      $("#listgroup").slideDown(1000, function()
      {
        // listgroup animation complete
        $("#listgroup2").fadeIn(1500);
      });
    });
  }
</script>
  1. 最后,将以下代码添加到我们的 HTML 文件的<body>元素中:
<div class="container-fluid">
  <form id="example-form" action="#" class="container-fluid">
    <div class="form-group">
      <div class="col-md-12"><h3>Call Javascript Function from C++</h3></div>

      <div class="col-md-12"><div class="alert alert-info" role="alert"><i class="fa fa-info-circle"></i> <span id="infotext">Change this text using C++.</span></div></div>

      <div class="col-md-2">
        <img id="tux" src="img/tux.png"></img>
      </div>

      <div class="col-md-5">
        <ul id="listgroup" class="list-group">
          <li class="list-group-item">Cras justo odio</li>
           <li class="list-group-item">Dapibus ac facilisis in</li>
           <li class="list-group-item">Morbi leo risus</li>
           <li class="list-group-item">Porta ac consectetur ac</li>
           <li class="list-group-item">Vestibulum at eros</li>
        </ul>
      </div>

      <div id="listgroup2" class="col-md-5">
        <a href="#" class="list-group-item active">
          <h4 class="list-group-item-heading">Item heading</h4>
          <p class="list-group-item-text">Cras justo odio</p>
        </a>
        <a href="#" class="list-group-item">
          <h4 class="list-group-item-heading">Item heading</h4>
          <p class="list-group-item-text">Dapibus ac facilisis in</p>
        </a>
        <a href="#" class="list-group-item">
          <h4 class="list-group-item-heading">Item heading</h4>
          <p class="list-group-item-text">Morbi leo risus</p>
        </a>
      </div>

    </div>
  </form>
</div>
  1. 现在构建并运行程序;你应该会得到与以下截图中显示的类似的结果。当你单击更改 HTML 文本按钮时,信息文本位于顶部面板中。如果你单击播放 UI 动画按钮,企鹅图像以及两组小部件将依次出现,具有不同的动画效果:操作步骤…

工作原理…

这个示例与之前的示例类似。一旦我们包含了 Web Channel JavaScript 库并初始化了QWebChannel类,我们就可以通过调用webview->page()->runJavascript("jsFunctionNameHere();")从 C++中调用任何 JavaScript 函数。不要忘记将在 C++中创建的 web channel 也应用到 WebView 的页面上;否则,它将无法与 HTML 文件中的QWebChannel类进行通信。

默认情况下,我们更改企鹅图像的 CSS 属性,并将其不透明度设置为0,宽度设置为0%,高度设置为0%。我们还通过调用 jQuery 函数hide()来隐藏两个列表组。当单击播放 UI 动画按钮时,我们再次重复相同的步骤,以防动画之前已经播放过(之前单击过相同的按钮),然后再次隐藏它们,以便重新播放动画。

jQuery 的一个强大特性是你可以定义动画完成后发生的事情,这使我们能够按顺序播放动画。在这个例子中,我们从企鹅图片开始,并在 1 秒内插值其 CSS 属性到目标设置(1000毫秒)。一旦完成,我们开始另一个动画,使第一个列表组在 1 秒内从顶部滑动到底部。之后,我们运行第三个动画,使第二个列表组在 1.5 秒内从无处淡入。

为了替换顶部面板中的信息文本,我们创建了一个名为changeHtmlText()的 JavaScript 函数,在函数内部,我们通过引用其 ID 并调用html()来获取 HTML 元素以更改其内容。

posted @ 2024-05-05 00:04  绝不原创的飞龙  阅读(75)  评论(0编辑  收藏  举报