Skip to content

Latest commit

 

History

History
751 lines (563 loc) · 45.2 KB

File metadata and controls

751 lines (563 loc) · 45.2 KB

四、Qt 基础

Qt 确实是构建应用的最佳跨平台框架之一。 因此,它有大量的核心类来管理数据,以及围绕平台服务(如线程、文件系统、网络 I/O,当然还有图形)的包装器。

在本章中,我们将讨论一些 Qt 的核心类,您会发现这些类在编写应用时特别方便。 在本讨论中,我们将重点介绍在为应用构建业务逻辑时特别有用的部分 Qt。 我们将从几个有用的数据类开始讨论。 之后,我们将了解 Qt 对多线程的支持,这是保持应用响应性的关键工具。 接下来,我们将讨论访问文件和 HTTP I/O,这是许多应用中的一个重要组件。 最后,我们将看一看 Qt 的 XML 解析器,您可以使用它来创建联网的应用或从文件系统加载 XML 数据。

我们将在本章介绍以下主题:

  • 使用 Qt 的核心类表示数据
  • Qt 中的多线程
  • 使用 Qt 访问文件
  • 使用 Qt 访问 HTTP 资源
  • 使用 Qt 解析 XML
  • 使用 Qt 解析 JSON

技术要求

本章的技术要求包括 Qt 5.12.3 MinGW 64 位、Qt Creator 4.9.0 和 Windows 10。

使用 Qt 的核心类表示数据

您将遇到的最常见的 Qt 核心类可能是 Qt 的字符串容器类:QString。 它具有与 C++ STLstd::wstring类类似的功能。 与wstring一样,它是多字节的。 您可以从传统的 C 样式char *字符串或另一个QString构造一个。

QString有很多辅助方法,其中一些如下所示:

  • append:这会将一个QString类附加到另一个类上。
  • arg:这用于构建格式化字符串(而不是sprintf)。
  • atoperator[]:您可以使用它们来访问字符QString中的单个字符。
  • operator==operator!=operator<operator>operator<=operator>=:它们比较两个QStrings
  • clear:这将清空QString并将其设置为空字符串。
  • contains:这将在一个字符串中搜索另一个字符串或正则表达式。
  • count:此参数统计在QString中出现的子字符串或字符。
  • startsWithendsWith:如果QString分别以特定字符串开始或结束,则返回 TRUE。
  • indexOf:这将返回子字符串在字符串中第一次出现的索引,如果字符串中不存在该子字符串,则返回-1
  • insert:这会在QString中的特定位置插入另一个QString
  • lastIndexOf:这返回子字符串在QString中的最后一个索引。
  • length:这返回字符串的长度(以字符为单位)。
  • remove:这将从QString中删除字符串的所有匹配项或多个字符。
  • setNum:这会格式化一个数字,并用给定的数字替换QString的值。
  • split:这将返回通过在特定分隔符拆分字符串而创建的QString个对象的列表(我们稍后将讨论 Qt 列表)。
  • toDoubletoFloattoInttoLong:如果可以转换,它们将返回字符串的数字表示形式。
  • toLowertoUpper:它们返回转换为 小写或大写的字符串的副本。
  • truncate:这将在给定位置截断字符串。

Qt 也有许多模板集合类。 其中最通用的是QList<T>,它针对基于索引的快速访问以及快速插入和删除进行了优化。 还有QLinkedList<T>QVector<T>,前者使用链表结构存储其值,后者将其元素存储在序列向量数组中,因此使用模板类进行索引访问最快,但调整大小的速度较慢。 Qt 还提供了QStringList,在所有目的和目的上都与QList<QString>相同。

正如您可能想象的那样,这些类型提供了operator[]元素,因此您可以访问和分配列表中的任何元素。 您将发现的其他QList<T>方法包括:

  • append:这会将一个项目追加到列表中。
  • at:这将以只读目的访问列表中的单个元素。
  • clear:这将清空列表。
  • contains:这将在列表中搜索特定元素。
  • count:此参数统计元素在列表中出现的次数。
  • empty:如果列表为空,则返回 TRUE。
  • startsWithendsWith:如果列表以指定的元素开始或结束,则返回 TRUE。
  • firstlast:它们分别返回列表的第一个和最后一个元素。
  • indexOf:如果某个项目在列表中,则返回该项目第一次出现的索引,如果没有找到该项目,则返回-1
  • lastIndexOf:如果项目在列表中,则返回项目的最后一个索引。
  • length:此参数返回列表的长度。
  • prepend:这会将项目添加到列表中。
  • push_backpush_front:它们分别将元素推送到列表的末尾或开头。
  • removeAtremoveFirstremoveLast:它们删除列表的第 i、第一个或最后一个元素。
  • replace:这将替换列表中的一个元素。
  • swap:这将在不同的索引处交换列表的两个元素。
  • toStdList:这将返回QListstd::list<T>
  • toVector:这将返回列表的QVector<T>

对于列表,您想要做的一件常见的事情就是迭代它的元素。 QList提供与 C++ STL 类似的迭代器,因此您可以迭代列表的元素,如下所示:

QList<QString> list; 
list.append("January"); 
list.append("February"); 
 ... 
list.append("December"); 

QList<QString>::const_iterator i; 
for (i = list.constBegin(); i != list.constEnd(); ++ i) 
    cout << *i << endl; 

QList<T>::const_iteratorQList<T>::iterator在列表上提供只读和可变迭代器;您可以分别通过调用constBeginbegin来获得一个迭代器,并将其与constEndend进行比较,以查看何时位于列表的末尾。

现在我们已经了解了核心类是如何工作的,让我们看看它们是如何使用键-值对的。

使用键-值对

键-值对(通常称为映射)是一种容器,它允许您轻松地排序和标识其中包含的数据。 如果你使用过其他语言,你可能知道它是字典或地图。地图中的每个元素都包含一对数据--一个键和一个值。 键是允许您快速查找元素的标识符,而值是将返回给您的实际数据。

对于刚刚开始学习编程的初学者来说,您可能会发现很难掌握键-值对的用法。 让我们用一个简单的例子来说明键-值对的用处。 假设您正在为一所只有数千名学生的学校运行一个系统,并且您试图寻找一个名为“John Smith”的特定学生。 如果您要将这个人的名字与全名列表进行比较,您的程序将需要很长时间才能找到此人。 此外,可能有不止一个学生叫约翰·史密斯。

为了方便起见,我们给每个学生分配了一个唯一的身份证号码。 当我们将所有学生放入键值对中时,ID 号将变为key,学生姓名将变为value。 因此,当我们尝试寻找特定的学生时,我们将改为搜索 ID 号,这比仅仅比较姓名要快得多,也更容易比较。 一旦我们找到了我们要找的 ID 号,我们就可以获得它的value,并确认它是否是我们要找的学生。 在搜索长列表时,键-值对非常高效。

Qt 为此提供了四个模板类:QMapQMultiMapQHashQMultiHash。 它们共享接口,但是QHash提供更快的查找,尽管它的键必须提供==运算符和全局散列函数qHash()。 这是因为其底层数据结构使用哈希表作为其数据结构。 QMap另一方面,将键-值对以对的形式存储在列表中,因此查找速度较慢,但您在选择的键结构上有更大的灵活性。 QMultiMapQMultiHash类允许您为单个键存储多个值,而QMapQHash只为每个键存储单个值。 大多数情况下,可以使用QMapQMultiMap;只有在管理大量键和值的情况下,QHash才能在访问性能方面取胜。

下面是带有字符串键和数值的QMap示例:

QMap<QString, int> map; 
map["one"] = 1; 
map["two"] = 2; 
map["three"] = 3; 

您可以使用operator[]value方法查找值。 如果要检查是否为给定键分配了值,请使用contains方法。 需要注意的一件事是,operator[]value并不完全相同;如果您使用它,并且给定键没有值,它会自动插入您提供的键的默认值,这可能不是您想要的。 其他方法包括:

  • clear:这将清除字典。
  • empty:如果字典为空,则返回 TRUE。
  • insert:这会将键-值对插入到字典中。
  • key:这将返回与您传入的值匹配的第一个键。
  • keys:这将返回一个键列表。
  • remove:这将删除您为其提供密钥的元素。

所有这些容器类(包括QString)都是轻量级的;当可以使用写入时复制实现时,它们通过引用来携带数据。 因此,让我们创建一个类的实例,并将其分配给第二个实例,如下所示:

QString oneFish = "red fish"; 
QString twoFish = oneFish; 

oneFishtwoFish都指向幕后的相同数据,只有当您开始通过其方法更改twoFish的值时,它才会获得自己的内存缓冲区。 这是这些类与 STL 类不同的一个重要方面,也是 Qt 在移动设备等低内存平台上获得更好内存性能的关键。

您应该知道的另一个轻量级数据类是QByteArray,它抽象了内存中的字节集合,用于 I/O 或其他数据操作。 您可以通过调用constData方法来获取QByteArray的常量char *,或者通过调用其data方法来获取可变的char *指针。 如果您想知道它的大小,可以调用它的length方法。 与 aQString一样,aQByteArray有很多助手函数来搜索、追加数据、预先添加数据等。 因此,如果您需要操作原始的字节集合,最好在使用 C 样式函数执行自己的实现之前查看QByteArray

For more about these and other Qt core container classes, see https://doc.qt.io/qt-5/containers.html.

我们已经了解了 Qt 提供的一些基本核心类。 通过了解如何使用这些核心类,我们将能够轻松地使用 Qt 中的所有其他类,因为它们都构建在核心类之上。

在下一节中,我们将学习如何创建利用 Qt 的多线程特性的应用。

Qt 中的多线程

线程是单个应用内的单行执行。 当今几乎所有的操作系统都是多线程的;也就是说,您的应用一次可以有多个并发执行行。 多线程是提高应用响应性的关键方法,因为当今的大多数处理器都可以并行执行多个线程,并且操作系统经过优化可以在多个线程之间共享资源。

Qt 通过三个关键类支持主机操作系统上的多线程:

  • QThread
  • QSemaphore
  • QMutex

第一个参数QThread表示单个执行线程,而后两个参数用于同步线程对数据结构的访问。

根据设计,应用完全在用户线程上运行,用户线程是在应用启动时启动的单个执行线程。 您可以通过子类化QThread并覆盖run方法来创建新的执行线程(不能操作用户界面)。 然后,当您需要执行开销很大的操作时,只需创建QThread子类的一个实例并调用其start方法即可。 (如果您熟悉 Java 中的线程,这类似于 Java。)。 反过来,该方法调用您的run方法,线程一直运行到run退出。 一旦run退出,它将使用finished信号发出完成。

您可以将插槽连接到该信号以观察任务完成情况。 当您使用线程执行后台工作(如网络事务)时,这尤其方便;您在线程的后台执行网络事务,您将知道 I/O 何时完成,因为您的线程将完成并发出finished方法。 在本章后面的使用 Qt访问 HTTP 资源一节中,我们将看到一个这样的例子。

下面是最简单的线程示例:

class MyThread : public QThread 
{ 
    Q_OBJECT 
    void run() Q_DECL_OVERRIDE
    { 
        /* perform the expensive operation */ 
    } 
}; 

void MyObject::startWorkInAThread() 
{ 
    MyThread *myThread = new MyThread(this); 
    connect(myThread, &MyObject::threadFinished, this, 
        MyObject::notifyThreadFinished); 
    connect(myThread, &MyThread::finished, myThread, 
        &QObject::deleteLater); 
    myThread->start(); 
}

在我们深入到代码之前,让我来解释一下我们为什么要这样做。 如果您正在处理繁重的计算、批量文件操作或繁重的网络 I/O,则应该将它们放在单独的线程中,而不是在主线程中运行。 这是因为这些操作非常“昂贵”,可能会在 CPU 处理应用时使其停止。 用外行的话说,“昂贵的”操作意味着 CPU 需要很长时间才能完成计算任务的繁重进程,这就是您不希望这些繁重进程在主线程上运行的原因。

相反,我们创建第二个线程,并将这些代价高昂的操作放入第二个线程的run方法中。 只要执行run,线程就会运行;执行完成后,它将发出finished信号。 要启动其中一个执行线程,只需创建该线程的一个新实例并调用其start方法。 然后将两个信号处理程序连接到线程的finished方法;第一个信号处理程序在线程完成时简单地删除该线程,第二个信号处理程序(未显示)使用线程的执行结果更新 UI。

多线程编程可能很棘手,因为您需要注意这样的情况:一个线程正在向数据结构写入数据,而另一个线程想要从该数据结构中读取数据。 如果不小心,可能会导致读取线程接收垃圾或更新了一半的数据,从而导致难以重现的编程错误。 Qt 提供了两个线程原语QMutexQSemaphore,用于阻止线程在资源(如数据结构)上的执行,从而允许线程在线程运行时独占访问该资源。

QMutex类有两个方法:

  • lock
  • unlock

要确保一次只有一个线程可以访问一个代码块,请创建一个互斥锁并调用其lock方法。 当执行结束时,您必须调用unlock;否则,没有其他线程可以运行该代码。 还有tryLock方法,它尝试获取锁,如果在指定的超时时间内无法获取锁,则立即返回,让您执行其他操作,而不是等到线程锁定互斥锁。

QSemaphoreQMutex的通用版本,允许您管理一个由n项组成的池;线程不会阻塞单个互斥锁上的执行,而是会一直阻塞,直到它能够获得您在调用其acquire方法时指定的资源数量。 当您使用完许多资源时,您可以调用release方法,指示您要释放的项目数。 QSemaphore还有一个tryAcquire方法和一个available方法,前者在资源获取在期望的超时失败时立即返回,后者返回当前可用的资源数量。

下面是一些示例代码,说明如何使用QSemaphore类:

const int bufferSize = 10;
QSemaphore sem(bufferSize); // sem.available() == 10

sem.acquire(6); // sem.available() == 4
sem.acquire(4); // sem.available() == 0
sem.release(7); // sem.available() == 7
sem.release(3); // sem.available() == 10

sem.tryAcquire(2); // sem.available() == 8, returns true
sem.tryAcquire(540); // sem.available() == 8, returns false

尽管 Qt 为我们提供了一些让多线程变得更容易的类,但多线程本身在编程学科中是一个相当高级的话题。 但是,您必须能够掌握多线程的概念,然后才能利用诸如QThreadQMutex之类的类将计算工作负载高效地分散到您的 CPU 线程上。 请注意,多线程并不是提高应用性能的绝对解决方案。 如果你做得不对,情况可能正好相反。

Since version 5.3, Qt has also introduced some higher-level programming constructs with Qt Concurrent that are beyond the scope of this book. For more information on QThread and its supporting classes, or Qt Concurrent, consult the Qt documentation at https://doc.qt.io/qt-5/threads-technologies.html.

我们已经学习了如何通过使用诸如QThreadQSemaphoreQMutex这样的类来支持多线程,从而在 Qt 中运行繁重的操作。 接下来,我们将学习如何使用 Qt 访问本地存储中的文件。

使用 Qt 访问文件

文件基本上是以字节流的形式存储的数字信息,驻留在硬盘中的某个地方。 如果您的程序需要保存或加载数据,例如用于文字处理、图像编辑、媒体流或程序配置,则需要访问存储在本地硬盘上的文件。 Qt 为我们提供了允许我们轻松访问文件系统的类,而无需考虑操作系统的类型。

Qt 将更通用的字节流的概念封装在其QIODevice类(即QFile的父类)以及网络 I/O 类(如QTcpSocket)中。 当然,我们不会直接创建QIODevice实例,而是创建一个类似于QFile的子类,然后直接使用QFile实例对文件进行读写。

Files and network access usually take time, and thus your applications shouldn't work with them on the main thread. Consider creating a subclass of QThread to perform I/O operations such as reading from files or accessing the network.

要开始使用文件,我们必须首先使用open方法打开它。 open方法接受单个参数,即打开文件的方式,它是以下各项的按位组合:

  • QIODevice::ReadOnly:用于只读访问。
  • QIODevice::WriteOnly:这用于只写访问。
  • QIODevice::ReadWrite:用于读写访问。
  • QIODevice::Append:此选项仅用于追加到文件。
  • QIODevice::Truncate:用于截断文件,在写入之前丢弃所有先前的内容。
  • QIODevice::Text:用于将文件视为文本,在读写过程中将换行符转换为平台表示形式。
  • QIODevice::Unbuffered:这用于绕过输入和输出的任何缓冲。

这些标志可以使用按位二进制运算符或*|*运算符组合在一起。 例如,读写文本文件的常见组合是QIODevice::ReadWrite | QIODevice::Text。 实际上,QIODevice:ReadWrite在内部定义为QIODevice::Read | QIODevice::Write

打开文件后,可以通过调用文件的read方法并传递要读取的字节数来读取一定数量的字节;生成的QByteArray对象包含要读取的数据。 类似地,您可以通过调用write来编写QByteArray,或者使用接受常量的重载write方法来编写QByteArray``* char。 在这两种情况下,write还需要写入字节数。 如果您只想将文件的全部内容读入单个缓冲区,则可以调用readAll,它将返回文件全部内容的QByteArray

某些QIODevice子类(如QFile)是可查找的;也就是说,您可以将读/写游标定位在文件中的任何位置,或确定其位置。 您可以使用seek方法将光标定位在文件中的特定位置,使用pos方法获取文件光标的当前位置。 请注意,其他QIODevice子类,如用于网络 I/O 的子类,不支持seekpos方法,但如果您尝试使用它们,它们会正常失败。

如果希望在不实际移动光标的情况下查看数据,可以调用peek并传递要返回的字节数;结果是QByteArray。 在peek之后调用read将返回相同的数据,因为peek不会使光标前进超过您所查看的数据。 当创建一个复杂的解析器需要了解其实现的多个位置的传入数据时,peek方法非常方便;您可以查看数据,决定如何解析数据,然后调用read来获取数据。

要确定您是否在文件的末尾并且没有更多的数据要读取,可以调用atEnd,如果没有更多的数据要读取,则返回 TRUE。 如果您想知道文件中有多少字节,可以调用bytesAvailable,它返回可供读取的字节数(如果已知,当然,网络套接字可能不会携带该信息)。

在处理文件时,我们通常也需要处理目录。 QDir类允许我们检查目录的内容,确定文件或目录是否存在,以及删除文件或目录。 需要注意的一件事是,无论主机平台使用什么目录分隔符路径,Qt 始终使用正斜杠--/*来表示目录,因此即使我们正在为 Windows 编写 Qt 程序,我们也会使用正斜杠,而不是反斜杠。 这使得编写在 Windows 和 Linux 上运行的跨平台兼容代码变得很容易,而不需要对目录处理代码使用特殊情况。

首先,通过传递文件路径来创建QDir的实例;之后,您可以使用它执行以下操作:

  • 通过调用absolutePath获取目录的绝对路径。
  • 通过调用cd切换到其他有效目录。
  • 通过调用entryInfoList获取目录中的文件列表。
  • 通过调用exists确定特定文件或目录是否存在。
  • 通过调用isRoot确定该目录是否为文件系统的根目录。
  • 通过调用remove并将文件名传递给remove来删除文件。
  • 通过调用rename重命名文件。
  • 通过调用rmdir并将目录名称传递给remove来删除空目录。
  • 使用==!=运算符比较两个目录。

当然,文件的位置(如应用首选项和/或临时文件)因平台而异;QStandardPaths类有一个静态的standardLocations方法,该方法返回我们要查找的存储类型的路径。 要使用它,我们从QStandardPaths::StandardLocation枚举中传递一个值,该值如下所示:

  • DesktopLocation:这将返回桌面目录。
  • DocumentsLocation:这将返回文档目录。
  • MusicLocation:此参数返回音乐在文件系统上的位置。
  • PicturesLocation:此参数返回照片在文件系统上的位置。
  • TempLocation:返回存储临时文件的路径。
  • HomeLocation:这将返回当前用户主目录的路径。
  • DataLocation:这将返回持久数据特定于应用的目录的路径。
  • CacheLocation:这将返回应用可以缓存数据的路径。
  • ConfigLocation:这将返回应用可以存储配置设置的路径。

好了,理论部分说得够多了。 让我们开始着手编写一些代码吧! 首先,让我们看看如何加载文本文件并读取其内容:

QFile file("myFile.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
    return;
}
while (!file.atEnd())
{
    QByteArray line = file.readLine();
    qDebug() << line;
}

我们首先将一个myFile.txt文本文件的名称提供给一个QFile对象进行初始化。 然后,我们调用open()让 Qt 打开该文件,并告诉它作为文本文件只读(不写)。 如果文件已成功打开,open()函数将返回true,如果打开文件失败,则返回false

打开文件后,我们使用while循环检查读取过程是否已到达文本文件的末尾。 如果没有,那么我们重复调用readLine()来读取文本文件的每一行,直到它到达末尾。 然后,我们使用qDebug()显示刚刚阅读的文本。

接下来,让我们尝试一些不同的东西:

QString fileName = QFileDialog::getOpenFileName(this, "Open Image", "", "Image Files (*.png *.jpg *.bmp)", QStandardPaths::DesktopLocation);
QImage image = QImage(fileName);

前面的代码只是打开一个文件选择对话框,让用户选择一个图像文件。 我们调用getOpenFileName()来启动文件选择对话框。 我们还将对话框的标题设置为Open Image,并将文件类型限制为 PNG、JPG 和 BMP。 然后,我们使用DesktopLocation枚举将默认位置设置为用户的桌面。 一旦用户选择了图像文件,图像文件的完整路径将存储在fileName变量中。 然后,我们可以使用fileName变量将图像数据转换为QImage对象。

For more information about files and network I/O, see the Qt documentation at https://doc.qt.io/qt-5/io-functions.html.

就是这样,我们已经明确了如何使用 Qt 类从本地存储加载文件。 让我们继续下一节,学习如何使用 Qt 访问 HTTP 资源。

使用 Qt 访问 HTTP 资源

在当今网络世界中,一件常见的事情是使用超文本传输协议(HTTP)来访问 Web 上的远程资源或服务。 要做到这一点,您的应用应该首先使用 Qt 的支持来选择一个承载网络来发出 HTTP 请求。 然后,它应该使用其对 HTTP 的支持,通过承载网络服务打开的网络连接发出一个或多个 HTTP 请求。

首先,您需要编辑项目的(.pro)文件以包括以下内容,以确保在 Qt 声明中包含网络模块:

QT += network

今天的计算设备支持多种方式访问网络。 例如,Android 平板电脑可以内置 4G 无线广域网(WAN)适配器和 Wi-Fi 无线电,为不同的接入点提供多种网络配置。 Android 平台包含复杂的代码,可根据以最佳成本提供最佳带宽的无线网络调出合适的网络接口,或提示用户选择所需的 Wi-Fi 网络。 各种 Linux 发行版都有类似的功能,Microsoft Windows 和 MacOS X 也是如此。为了封装此功能,Qt 提供了承载网络模块,该模块封装了平台的底层支持,以提示用户使用哪个网络连接、如何启动网络连接、停止网络连接等。

如果您正在编写网络应用,以确定如何连接到网络,则在首次尝试使用网络之前,您需要使用此模块来提示用户。 要做到这一点,最简单的方法是使用如下代码片段:

bool OpenNetworkConnection()
{
    QNetworkConfigurationManager manager;
    const bool canStartIAP = (manager.capabilities() & QNetworkConfigurationManager::CanStartAndStopInterfaces); // Is there 
//  default access point, use it
    QNetworkConfiguration cfg = manager.defaultConfiguration();
    if (!cfg.isValid() || (!canStartIAP && cfg.state() != 
        QNetworkConfiguration::Active))
    {
        QMessageBox::information(this, tr("Network"), tr("No Access 
            Point found.")); 
        return false; 
    } 
    QNetworkSession* session = new QNetworkSession(cfg, this); 
    session->open(); 
    return session->waitForOpened(-1); 
} 

此代码执行以下操作:

  • 它创建网络配置管理器的一个实例。
  • 它确定管理器是否可以启动和停止系统上的网络接口。
  • 它会获取网络连接的默认配置。
  • 如果默认配置无效,或者配置管理器无法启动网络配置且没有网络连接,则会弹出一个对话框,指示应用无法启动新的网络连接,并返回 False 以通知调用方没有建立网络连接。
  • 如果默认配置有效,代码将获取由默认配置配置的网络会话,将其打开,然后等待,直到连接打开。 在幕后,平台可能会提示用户进行所需的网络连接、管理一个或多个无线电等。 一旦配置了会话,您的应用就可以使用网络了。

一旦建立了到网络的连接,您就可以发出网络请求,或者创建用于 TCP 或 UDP 访问的低级通信。 接下来,我们将了解当需要在线通信时 HTTP 请求是如何工作的。

Further information about low-level networking will not be discussed in this book; if you're curious, you can see the Qt documentation at https://doc.qt.io/qt-5/qtnetwork-index.html.

执行 HTTP 请求

Qt 提供了三个关键类来执行 HTTP 请求:QNetworkAccessManagerQNetworkRequestQNetworkReply。 HTTP 请求对于需要与服务器进行在线通信的应用非常重要,例如:用户登录、数据输入、新闻提要、通知等。

我们使用QNetworkAccessManager请求来配置 HTTP 请求的语义,配置代理服务器以及实际发出请求。 最简单的做法是创建一个,将它的finished信号连接到您希望在请求完成时调用的槽,然后调用它的get方法,如下所示:

mNetManager = new QNetworkAccessManager(this); 
connect(mNetManager, &QNetworkAccessManager::finished, this, &MainWindow::handleNetFinished); 

// later, when you want to make a request 
QNetworkReply *reply = mNetManager->get(QNetworkRequest(QUrl(url))); 

QNetworkAccessManager类有用于 HTTP 的每个方法GETPOSTDELETEHEADPUT的方法,分别恰当地命名为getpostdeleteheadput。 对于大多数事务,要简单地获取数据,您将使用get,这是获取远程网络资源的标准 HTTP 方法。 如果要通过put方法触发远程过程调用,请调用QNetworkAccessManager类的参数put方法,传递一个QNetworkRequest对象和一个指向要放入服务器的数据的QByteArrayQIODevice指针。

If you need to configure a proxy server as part of your request, you can do so using the setProxy method of QNetworkAccessManager. Note that Qt will configure itself with whatever system the HTTP proxy is by default, so you should only need to override the proxy server settings if you're working with an application-specific proxy server for your application.

QNetworkAccessManager类使用QNetworkRequest类来封装请求的语义,允许您通过调用其setHeadersetRawHeader方法来设置伴随请求的 HTTP 标头。 setHeader方法允许您设置特定的 HTTP 标头,如User-Agent标头,而setRawHeader方法允许您提供自定义的 HTTP 标头名称和值作为QByteArray值。

发出请求后,QNetworkAccessManager类将接管,执行必要的网络 I/O 以查找远程主机、联系远程主机并发出请求。 当应答准备好时,它用finished信号通知您的代码,传递与请求相关联的QNetworkReply类。 使用QNetworkReply类,您可以通过调用headerrawHeader分别获取标准或自定义 HTTP 标头来访问与回复相关联的标头。 QNetworkReply继承自QIODevice,因此您只需使用readreadAll方法根据需要读取响应,如下所示:

void MyClass::handleNetFinished(QNetworkReply* reply) 
{ 
    if (reply->error() == QNetworkReply::NoError)
    { 
        QByteArray data = reply->readAll(); 
    }
    else
    { 
        qDebug() << QString("net error %1").arg(reply->error()); 
    } 
} 

For more information about bearer network configuration, see the Qt documentation at https://doc.qt.io/qt-5/bearer-management.html. For more information about all of Qt's support for networking, see the Qt documentation at https://doc.qt.io/qt-5/qtnetwork-index.html. There are also some good network samples at https://doc.qt.io/qt-5/examples-network.html.

我们已经学习了如何允许我们的应用通过 HTTP 请求与在线服务器通信。 接下来,我们将研究如何使用 Qt 解析 XML 数据。

使用 Qt 解析 XML

Qt 的早期版本有许多 XML 解析器,每个解析器都适合不同的任务和不同的解析风格。每个 XML 解析器在旧版本的 Qt 中都用于不同的格式。 幸运的是,在 Qt5 中,这一点得到了简化;目前,您只需要一个 XML 解析器来解析 XML 数据。 在最新的 Qt 版本中使用的关键 xml 解析器是QXmlStreamReaderhttps://doc.qt.io/qt-5/qxmlstreamreader.html 类(有关详细信息,请参阅XML类)。 这个类从QIODevice子类读取,一次读取一个 XML 标记,允许您打开解析器遇到的标记类型。 因此,我们的解析器如下所示:

QXmlStreamReader xml; 
xml.setDevice(input); 
while (!xml.atEnd())
{ 
    QXmlStreamReader::TokenType type = xml.readNext(); 
    switch(type) 
    { 
        ... // do processing 
    } 
} 
if (xml.hasError())
{ 
    ... // do error handling 
} 

每次调用readNext方法时,QXMLStreamReader类依次读取 XML 的每个标记。 对于每次读取的标记,readNext返回读取的标记的类型,可以是以下类型之一:

  • StartDocument:表示文档的开头。
  • EndDocument:表示文档结束。
  • StartElement:这表示元素的开始。
  • EndElement:这表示元素的结束。
  • Characters:这表示读取了某些字符。
  • Comment:这表示已读取评论。
  • DTD:这表示文档类型声明已读取。
  • EntityReference:这表示读取了无法解析的实体引用。
  • ProcessingInstruction:这表示读取了 XML 处理指令。

了解了使用 Qt 解析 XML 的这些基础知识之后,让我们看看如何使用它们。

将 XML 解析与 HTTP 结合使用

让我们用一些示例代码将多线程、HTTP I/O 和 XML 解析结合在一起,该示例代码使用从远程服务器获取具有唯一标记的平面 XML 文档,并从 XML 解析选定的标记,将结果作为名称-值对存储在QMap<QString, QString>中。

平面 XML 文件是没有嵌套元素的文件,即以下形式的 XML 文档:

<?xml version="1.0"?> 
<document> 
    <tag>Value</tag> 
    <tag2>Value 2</tag2> 
</document> 

我们将从WorkerThread类头开始:

#include <QMap> 
#include <QThread> 
#include <QXmlStreamReader>
#include <QNetworkAccessManager>
#include <QNetworkReply>

class WorkerThread : public QThread 
{ 
    Q_OBJECT 

public: 
    WorkerThread(QObject* owner); 
    void run(); 

    void fetch(const QString& url); 
    void cancel(); 

signals: 
    void error(const QString& error); 
    void finished(const QMap<QString, QString>&); 

private slots: 
    void handleNetFinished(QNetworkReply* reply); 

private: 
    bool mCancelled; 
    QNetworkAccessManager* mNetManager; 
    QNetworkReply* mReply; 
} 

这个类扩展了QThread,所以它是一个QObject。 它的槽是私有的,因为它只在这个类的作用域内使用,不能作为其公共接口的一部分使用。 要使用它,您需要创建它并调用它的fetch方法,将 URL 传递给 FETCH。 它的作用是发出成功结果的信号,通过finished信号从 XML 传递名称-值对的字典,或者如果请求失败,则通过error信号传递带有错误消息的字符串。 如果我们启动一个请求,而用户想要取消它,我们只需调用cancel方法。

该类携带的数据非常少:一个新的mCancelled取消标志,它用来执行 I/O 的QNetworkAccessManager实例mNetManager,以及来自请求的QNetworkReply请求mReply。 接下来,我们将了解如何实现WorkerThread来解析 XML。

实现 WorkerThread

现在我们已经了解了 XML 中的解析工作原理,我们可以看到WorkerThread核心的实现是什么样子,如下所示:

  1. 以下代码显示了WorkerThread的实现:
WorkerThread::WorkerThread(QObject* owner)
{
    this->setParent(owner);
    mNetManager = new QNetworkAccessManager(this);
    connect(mNetManager, &QNetworkAccessManager::finished, this, &WorkerThread::handleNetFinished);
}

void WorkerThread::run() 
{ 
    QXmlStreamReader xml; 
    QXmlStreamReader::TokenType type; 
    QString fieldName; 
    QString value; 
    QString tag; 
    bool successful = false; 
    bool gotValue = false; 
    QMap<QString, QString> result; 

    xml.setDevice(mReply); 
  1. 然后,我们继续编写代码,并通过循环遍历文件并读取每个 XML 元素来开始解析 XML 数据:
    while(!xml.atEnd()) 
    { 
        // If we've been cancelled, stop processing. 
        if (mCancelled) break; 

        type = xml.readNext(); 
        bool gotEntry = false; 
        switch( type ) 
        { 
            case QXmlStreamReader::StartElement: 
            {  
                QString tag = xml.name().toString().toLower(); 
                fieldName = tag; 
                gotValue = false; 
                qDebug() << "tag" << tag;
            } 
            break; 
  1. 我们继续检查 XML 元素的类型,并相应地保存其值:
            case QXmlStreamReader::Characters: 
            // Save aside any text 
            if (!gotValue) 
            { 
                value = xml.text().toString().simplified(); 
                if (value != "")
                {
                    gotValue = true;
                    qDebug() << "value" << value;
                }
            } 
            break; 
            case QXmlStreamReader::EndElement: 
            // Save aside this value 
            if (gotEntry && gotValue)
            { 
                result[fieldName] = value; 
            }  
            gotEntry = false; 
            gotValue = false; 
            break; 
            default: 
            break; 
        } 
    } 
  1. 然后,我们检查解析是否成功。 如果成功,我们触发finished信号,如果不成功,则调用error信号:
    successful = xml.hasError() ? false : true; 

    if (!mCancelled && successful) { 
        emit finished(result); 
    } else if (!mCancelled) { 
        emit error(tr("Could not interpret the server's response.")); 
    } 
} 
  1. 之后,我们编写fetchhandleNetFinished函数从服务器获取数据。 我们还编写了用于取消请求的cancel函数:
void WorkerThread::fetch(const QString& url) 
{ 
    // Don't try to re-start if we're running 
    if (isRunning()) { this->cancel(); } 

    QNetworkReply *reply = mNetManager->get(QNetworkRequest
        (QUrl(url))); 

    if (!reply) { emit error(tr("Could not contact the server.")); } 
} 

void WorkerThread::cancel()
{ 
    mCancelled = true; 
    wait(); 
}; 

void WorkerThread::handleNetFinished(QNetworkReply* reply) 
{ 
    // Start parse by starting the thread. 
    if (reply->error() == QNetworkReply::NoError)
    { 
        if (!this->isRunning())
        { 
              mReply = reply; 
              start(); 
        } 
    }
    else
    { 
        emit error(tr("A network error occurred.")); 
        qDebug() << QString("net error %1").arg(reply->error()); 
    } 
} 

这里有很多代码(完整的类在本书附带的下载中显示),所以让我们一个方法一个方法地学习:

  • 构造函数初始化每个成员字段,并将QNetworkAccessManagerfinished信号连接到我们的handleNetFinished 槽。 (这里省略了构造器,但本书附带的示例代码 中提供了构造器。)
  • run方法是该类的核心,负责读取和解析 XML 响应。 我们将read和 parse 放在run方法中,因为它可能会占用最多的时间,这样它就可以在后台线程上运行,这样它就不会阻塞用户界面。

run方法执行以下操作:

  • 使用我们的网络响应mReply初始化QXMLStreamReader类。

  • 循环遍历它正在读取的 XML 文档中找到的标记。 对于每个标记:

    • 如果标记是开始元素,则它获取标记的名称并注意到它已接收到新的开始元素。
    • 如果标签是一个字符串,它会将该字符串保存下来,并注意到它有一个标签的值。
    • 如果它是一个 XML 元素的末尾,并且它同时有一个标记名 和一个值,它会将该标记值分配给结果散列中指定的槽。
  • 一旦读取了所有标记或出现错误,代码将首先测试错误。

  • 如果解析没有取消并且成功,代码将发出finished信号,传递结果为QMap的 XML 文档中的名称和值。

  • 如果解析遇到错误,代码将发出错误信号。

  • 在使用QNetworkAccessManager发出 HTTPGET请求之前,如果有一个请求挂起,则fetch方法简单地取消该请求。

  • cancel方法设置由run方法检查的取消标志,并等待线程完成,确保在cancel返回之前取消。

  • 当 HTTPGET请求返回时,QNetworkAccessManager调用handleNetFinished方法,保存生成的网络请求,启动从远程服务器读取的线程,并解析结果。 如果发生错误,它会用错误信号发出错误信号,并将 HTTP 错误消息记录到调试器控制台。

现在我们已经了解了如何通过 HTTP web 请求获取 XML 数据,然后使用 Qt 解析 XML 数据。 接下来,我们将学习如何解析另一种称为 JSON 的流行格式。

使用 Qt 解析 JSON

JSON 是继 XML 之后流行的数据传输格式。 它于 2013 年首次标准化,自那以来已成为网络上最受欢迎的格式。 JSON 数据如下所示:

{
 "firstName": "John",
 "lastName": "Smith",
 "age": 42,
 "address": {
 "streetAddress": "14 2nd Street",
 "city": "New York",
 "state": "NY",
 "postalCode": "10021-3100"
 },
 "phoneNumbers": [{
 "type": "home",
 "number": "212 686-7890"
 },
 {
 "type": "mobile",
 "number": "321 456-7788"
 }]
}

如您所见,它与 XML 格式完全不同。 它如此受欢迎的主要原因是因为它非常便于人类阅读,而且与 XML 相比要短得多,没有所有的开始和结束标记。

要使用 Qt 解析 JSON 数据,让我们开始编写一些代码:

  1. 首先,我们必须包括与 JSON 类相关的标头:
#include <QJsonDocument>
#include <QJsonObject>
#include <QVariantMap>
  1. 之后,我们将尝试解析前面的 JSON 数据,在转换为QString格式时如下所示:
QString jsonString = "{\"firstName\": \"John\",\"lastName\": \"Smith\",\"age\": 42,\"address\": {\"streetAddress\": \"14 2nd Street\",\"city\": \"New York\",\"state\": \"NY\",\"postalCode\": \"10021-3100\"},\"phoneNumbers\": [{\"type\": \"home\",\"number\": \"212 686-7890\"},{\"type\": \"mobile\",\"number\": \"321 456-7788\"}]}";
  1. 接下来,我们将通过首先将数据转换为QJsonDocument对象来解析数据,然后通过调用QJsonDocument::fromJson()函数获得QJsonObject对象。
  2. 然后,我们将QJsonObject转换为QVariantMap,然后才能从它获得所需的数据:
QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());
QJsonObject obj = doc.object();           // Get the json object
QVariantMap map = obj.toVariantMap();     // Convert json object to variant map

qDebug() << map["firstName"].toString();  // Obtain firstName data
  1. 前面的代码会产生以下结果:
John

如您所见,代码简单明了。 我们还利用从本章开头学到的有关键-值对的知识,从我们的 JSON 变体映射中获得了firstName数据。

  1. 接下来,我们将了解如何从 JSON 数组获取数据,例如:
{
"name":"John",
"age":30,
"cars":[ "Ford", "BMW", "Fiat" ]
}
  1. cars数据由数组格式包装的三个单独的数据项组成。 让我们开始编写一些代码来解析 JSON 数据。 同样,我们将把前面的 JSON 文本转换为QString格式:
QString jsonString = "{\"name\":\"John\",\"age\":30,\"cars\":[ \"Ford\", \"BMW\", \"Fiat\" ]}";
  1. 然后,我们必须在代码的顶部添加两个新的头,即QJsonArrayQJsonValue
#include <QJsonArray>
#include <QJsonValue>
  1. 之后,我们开始解析 JSON 数据,如下所示:
QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());
QJsonObject obj = doc.object();        // Get the json object
QJsonValue value = obj.value("cars");  // Get cars data in QJsonValue format
QJsonArray array = value.toArray();    // Convert it to QJsonArray
  1. 一旦将cars数据转换为QJsonArray格式,我们就可以像任何普通的 C++ 数组一样遍历它:
for (int i = 0; i < array.size(); i++)
{
    qDebug() << array.at(i).toString(); // Get data
}

前面的代码会产生以下结果:

Ford
BMW
Fiat

同样,代码非常简短和直观。 Qt 使通过以下QJsonDocumentQJsonObjectQJsonValueQJsonArray类解析 JSON 数据变得非常容易。

For more information about JSON support in Qt, please refer to the link here: https://doc.qt.io/qt-5/json.html.

在本章中,我们学习了如何使用 Qt 的核心功能。 我们还学习了如何使用 Qt 类解析 XML 数据和 JSON 数据。 在下一章中,我们将学习如何使用 Qt 小工具创建表单应用。

简略的 / 概括的 / 简易判罪的 / 简易的

我们在本章涵盖了很多内容,从数据结构到文件,再到网络。 您已经了解了如何使用基本的 Qt 核心和网络类来构建后端逻辑,这可以帮助您构建应用的业务逻辑。

不仅如此,我们还学习了如何利用多线程将工作负载分散到不同的 CPU 线程,以加快进程。 如果您正在创建一个执行大量计算的应用,并且您不想让它在计算仍在进行时变得没有响应,这一点尤其有用。 这将严重影响用户体验,并最终影响您作为品牌或公司的声誉。

除此之外,我们还了解了如何利用 HTTP 请求与远程服务器通信并从中获取数据。 Qt 提供了自己的类,这些类在后台得到了很好的实现,从而使这个过程变得更容易。 这对那些希望构建支持云架构并向其用户提供动态内容的现代应用的开发人员非常有利。

我们还学习了如何解析不同类型的数据格式,如 XML 和 JSON,这两种格式在 Web 和桌面应用中都非常流行和常用。 通过将这些功能整合到您的应用中,您将能够使其与市场上的任何第三方系统兼容,从而提高其价值。

在下一章中,我们将开始研究 Qt 支持来构建您的演示逻辑。 我们将暂时离开这些基础知识,回顾构建桌面应用的关键 Qt Widget 类。 您将了解可用于您的应用的大量基本 Qt Widget 类、Qt 对模型-视图-控制器范例的支持如何工作,以及如何使用 QWebEngineView-Qt 集成的基于 Web 引擎的浏览器在应用开发中呈现 Web 内容。