QT5入门
QT5入门
一、下载及安装
1.1 下载
目录结构:
目录 | 说明 |
---|---|
archive | 各种 Qt 开发工具安装包,新旧都有(可以下载 Qt 开发环境和源代码)。 |
community_releases | 社区定制的 Qt 库,Tizen 版 Qt 以及 Qt 附加源码包。 |
development_releases | 开发版,有新的和旧的不稳定版本,在 Qt 开发过程中的非正式版本。 |
learning | 有学习 Qt 的文档教程和示范视频。 |
ministro | 迷你版,目前是针对 Android 的版本。 |
official_releases | 正式发布版,是与开发版相对的稳定版 Qt 库和开发工具(可以下载Qt开发环境和源代码)。 |
online | Qt 在线安装源。 |
snapshots | 预览版,最新的开发测试中的 Qt 库和开发工具。 |
一般来说 archive 和 official_releases 两个目录都有最新的 Qt 开发环境安装包,这里以 archive 目录为例来说明, archive 目录下:
目录 | 说明 |
---|---|
vsaddin | 这是 Qt 针对 Visual Studio 集成的插件 |
qtcreator | 这是 Qt 官方的集成开发工具,但是 qtcreator 本身是个空壳,它没有编译套件和 Qt 开发库。 除了老版本的 Qt 4 需要手动下载 qtcreator、编译套件、Qt 开发库进行搭配之外,一般用不到。这里不需要下载它, Qt 5 有专门的大安装包,里面包含开发需要的东西,并且能自动配置好。 |
qt | 这是 Qt 开发环境的下载目录,Qt 5 的大安装包就在这里面。 |
online_installers | 在线安装器,国内用户不建议使用,在线安装是龟速,还经常断线 |
- 目前的长期支持版(LTS):5.6(已经超期)、5.9、5.12
1.2 安装
一直下一步即可,中间可以按照自己喜好选择是否关联所有文件。一直到选择安装组件,一般来说组件由两部分:
-
Qt 5.xx
组件 说明 MinGW 编译器模块。MinGW 是 Minimalist GNU for Windows 的缩写,MinGW 是 Windows 平台上使用的 GNU 工具集导入库的集合。必须安装。 UWP *** UWP 是 Windows 10 中 Universal Windows Platform 的简称,有不同编译器类型的 UWP,属于 MSVC 编译器生成的 Qt 库。如果不是开发 UWP 应用程序,就不需要,直接忽略。 MSVC *** 针对 Windows 平台上的 MSVC 编译器的 Qt 组件,如 msvc2015 32-bit 和 msvc2015 64-bit 等。安装该组件需要计算机上已经安装相应版本的 Visual Studio。如果你不使用 MSVC 编译器进行开发,就不用安装 Android *** 这是针对安卓应用开发的 Qt 库,如果读者有安卓开发这方面需求可以自己选择安装,一般情况下用不到。 Sources Qt 的源代码包,除非你想阅读 Qt 的源码,否则不用安装 Qt *** Qt 的附加模块,大部分建议安装,这些附加模块括号里的 TP 是指 Technology Preview ,技术预览模块的意思,还处在功能测试阶段,不是正式版模块;附加模块括号里的 Deprecated 是指抛弃的旧模块,兼容旧代码使用的,一般用不到。这些附加模块读者可以选择部分或都勾选了安装,占用空间不大。 部分组件说明:Qt Charts 是二维图表模块,用于绘制柱状图、饼图、曲线图等常用二维图表。Qt Data Visualization 是三维数据图表模块,用于数据的三维显示,如散点的三维空间分布、三维曲面等。Qt Scritp(Deprecated)是脚本模块,已被抛弃,不建议安装。 -
Tools
组件 说明 Qt Creator xxx 这是集成开发环境,强制安装的,以后所有的项目和代码都在 Qt Creator 里面新建和编辑。 Qt Creator xxx CDB Debugger surpport 用于和 CDB 调试工具对接,默认安装,一般用于调试 VC 编译的 Qt 程序 MinGW 5.3.0 这是开源的编译器套件,需要勾选安装 Strawberry Perl 用于编译 Qt 源代码的 Perl 开发环境,不需要安装。如果以后用到,也可以另外手动安装,在搜索引擎搜索 Strawberry Perl 关键词,去 Strawberry Perl 官网下载最新的安装包是一样用的
1.3 添加或删除、更新组件
在qt的安装目录下会有一个MaintenanceTool.exe
的维护软件,能够添加或删除、更新组件
如果需要账号,可以通过断开网络跳过该步骤
在 Qt Downloads 处找到任意一个国内镜像,点击右侧HTTP
按钮进入对应的镜像仓库,一般的路径在*/online/qtsdkrepository/windows_x86/
下,找到对应的平台以及需要安装的镜像即可
注意,需要对应的路径下存在
Updates.xml
文件才行
记住上述镜像的路径,并填写在MaintenanceTool.exe
的设置->
中,选择更新组件,勾上对应的组件即可
1.4 堆还是栈中创建
main函数中推荐在栈中创建
其他函数中推荐在堆中创建(通过new
关键字),但是此时就需要手动销毁,否则会带来内存泄漏。一般来说有以下几种形式进行销毁:
- 手动调用
delete
进行销毁 - 通过QT的对象模型,在new一个控件时,传入parent参数。但是此时需要注意创建对象的顺序,避免有的对象因为对象模型而进行二次析构
- 当对象为对话框dialog时,可通过
dialog->setAttribute(Qt::WA_DeleteOnClose);
设置Qt::WA_DeleteOnClose
属性在对话框关闭时自动销毁,或者通过deleteLater()
函数在当前事件循环结束后销毁
二、信号与槽函数
所谓信号槽,实际就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。
2.1 connect函数
常用形式:
1 | connect(sender, signal, receiver, slot); |
- sender:发出信号的对象
- signal:发送对象发出的信号
- receiver:接收信号的对象
- slot:接收对象在接收到信号之后所需要调用的函数
要求:
- 信号和槽的参数类型一致,槽函数的参数可以比信号的少,但是槽函数存在的那些参数的顺序必须和信号的前面几个一致
QT5中五个标准重载函数:
-
将 signal 和 slot 作为字符串处理:
1
2
3QMetaObject::Connection connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *slot,
Qt::ConnectionType); -
使用
QMetaMethod
进行类型比对1
2
3QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal,
const QObject *receiver, const QMetaMethod &slot,
Qt::ConnectionType);- 将每个函数看做是
QMetaMethod
的子类
- 将每个函数看做是
-
本质上是将 this 指针作为 receiver(同第一种)
1
2
3QMetaObject::Connection connect(const QObject *sender, const char *signal,
const char *slot,
Qt::ConnectionType) const; -
指向成员函数的指针
1
2
3QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal,
const QObject *receiver, PointerToMemberFunction slot,
Qt::ConnectionType) -
接受 static 函数、全局函数以及 Lambda 表达式
1
2QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal,
Functor slot);
QT4中有三个版本:
1 | bool connect(const QObject *sender, const char *signal, |
- 除了返回值,与 Qt 5 最大的区别在于,Qt 4 的 signal 和 slot 只有
const char *
这么一种形式。正因如此,一旦出现连接不成功的情况 Qt 4 是没有编译错误的,而是在运行时给出错误
两个常见宏:
- SIGNAL、SLOT:将两个函数名转换成了字符串
2.2 自定义信号槽
newspaper.h:
1 |
|
- 为了使用信号槽,必须继承
QObject
- 凡是
QObject
类(不管是直接子类还是间接子类),都应该在第一行代码写上Q_OBJECT
。不管是不是使用信号槽,都应该添加这个宏。这个宏的展开将为类提供信号槽机制、国际化机制以及 Qt 提供的不基于 C++ RTTI 的反射能力- 这个宏将由moc(可以将其理解为一种预处理器,比 C++ 预处理器更早执行的预处理器)做特殊处理
- moc 会读取标记了
Q_OBJECT
的头文件,生成以 moc_ 为前缀的文件。注意, moc 不会处理 cpp 文件中的类似声明 - 手动调用 moc 工具处理包含
Q_OBJECT
宏的 cpp 文件,并将 cpp 中的#include "newspaper.h"
改为#include "moc_newspaper.h"
即可处理 cpp 文件中类似声明
signals
块所列出的,就是该类的信号。信号就是一个个的函数名,返回值是void
,参数是该类需要让外界知道的数据,不需要在 cpp 函数中添加任何实现- moc 会实现信号函数所需要的函数体
emit
是 Qt 对 C++ 的扩展,是一个关键字(其实也是一个宏)。含义是发出其后的信号。这里将实际的报纸名字m_name
当做参数传给这个信号。当接收者连接这个信号时,就可以通过槽函数获得实际值。这样就完成了数据从发出者到接收者的一个转移。
reader.h:
1 |
|
- 因为需要接受信号,所以需要继承
QObject
,并添加Q_OBJECT
宏 - Qt 5 中,任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数,且槽函数必须有实现代码。同时也会受到 public、private、protected 等访问控制符的影响
main.cpp:
1 |
|
- 终端输出:Receives Newspaper: Newspaper A
与QT4中信号与槽的区别
- 槽函数必须放在由 slots 修饰的代码块中,并且要使用访问控制符进行访问控制。其原则同其它函数一样:默认是 private 的,如果要在外部访问,就应该是 public slots;如果只需要在子类访问,就应该是 protected slots
QObject::connect()
函数第二、第四个参数需要使用SIGNAL
和SLOT
这两个宏转换成字符串
2.3 有重载的信号
如果信号有重载,利用QT5中connet的写法会报错:由于这个函数(注意,信号实际也是一个普通的函数)有重载,因此不能用一个取址操作符获取其地址。详见[3.4 布局管理](# 3.4 布局管理)中的例子
如果想解决该问题可以通过:
- 采用QT4的方法,利用
SIGNAL
和SLOT
宏来解决(该宏需要指明具体的函数) - 使用一个函数指针来指明到底是哪一个信号
在上述例子中,可以换成如下的两种写法:
1 | // 强制类型转换 |
- 更推荐匿名指针的写法(当参数类型发生改变时,编译器能够及时知道并报错,而强制类型转化的错误只能在运行时发现)
2.4 带有默认参数的槽函数
Qt 允许槽函数的参数数目可以比信号的参数少。但是有一种情况例外,使得槽函数的参数可以比信号的多,即槽函数的参数带有默认值
在[2.2 自定义信号槽](# 2.2 自定义信号槽)的例子中如果有如下信号和槽:
1 | // Newspaper |
此时,如果按照上一节中推荐的匿名指针写法:
1 | QObject::connect(&newspaper, |
会得到一个断言错误,而本质上是C++的限制:参数默认值只能使用在直接地函数调用中,当使用函数指针取其地址的时候,默认参数是不可见的。但是此时任可以用QT4的连接写法
如果仍想使用QT5的写法,此时只能利用Lambda表达式:
1 | QObject::connect(&newspaper, |
三、MainWindow
QMainWindow
是 Qt 框架带来的一个预定义好的主窗口类。即一个普通意义上的应用程序最顶层的窗口。通常是由一个标题栏,一个菜单栏,若干工具栏和一个任务栏。在这些子组件之间则是我们的工作区。
- Window Title:即标题栏,通常用于显示标题和控制按钮,比如最大化、最小化和关闭等。通常,各个图形界面框架都会使用操作系统本地代码来生成一个窗口
- Menu Bar:即菜单栏,用于显示菜单
- Status Bar:即状态栏。当我们鼠标滑过某些组件时,可以在状态栏显示某些信息
- Tool Bar Area:用于显示工具条区域(Qt 的主窗口支持多个工具条,故这里为矩形)。你可以将工具条拖放到不同的位置,因此这里说是 Area
- Dock Widget Area:停靠窗口的显示区域。所谓停靠窗口,就像 Photoshop 的工具箱一样,可以停靠在主窗口的四周,也可以浮动显示
- Central Widget:即工作区(重点关注)。通常我们会将程序最主要的工作区域放置在这里,类似 Word 的稿纸或者 Photoshop 的画布等等
3.1 添加动作
Qt 使用QAction
类作为动作,代表了窗口的一个“动作”,这个动作可能显示在以下地方:
- 作为菜单项。当用户点击该菜单项,对用户的点击做出响应
- 作为工具栏按钮。用户点击这个按钮就可以执行相应的操作
这里直接抽象出公共的动作组成一个QAction
类。当把QAction
对象添加到菜单,就显示成一个菜单项,添加到工具栏,就显示成一个工具按钮。用户可以通过点击菜单项、点击工具栏按钮、点击快捷键来激活这个动作。
QAction
包含了图标、菜单文字、快捷键、状态栏文字、浮动帮助等信息。当把一个QAction
对象添加到程序中时,Qt 自己选择使用哪个属性来显示,无需我们关心。同时,Qt 能够保证把QAction
对象添加到不同的菜单、工具栏时,显示内容是同步的。也就是说,如果在菜单中修改了QAction
的图标,那么在工具栏上面这个QAction
所对应的按钮的图标也会同步修改。
mainwindow.h:
1 |
|
mainwindow.cpp:
1 |
|
tr()
:国际化函数。使用专门的国际化工具,将tr()
函数的字符串提取出来,进行国际化。由于所需进行国际化的文本应该被大多数人认识,故tr()
函中一般会是英文文本- QAction的创建:
QIcon()
:设置图标。以 : 开始,意味着从资源文件中查找资源- 文本值前面有一个
&
,意味着这将成为一个快捷键
- 使用
QKeySequence
类来添加快捷键,会根据平台的不同来定义相应的快捷键
main函数:
1 | int main(int argc, char *argv[]) |
3.2 资源文件
Qt 资源系统是一个跨平台的资源机制,用于将程序运行时所需要的资源以二进制的形式存储于可执行文件内部。如果程序需要加载特定的资源(图标、文本翻译等),那么将其放置在资源文件中,就不需要担心这些文件的丢失。也就是说,如果你将资源以资源文件形式存储,它是会编译到可执行文件内部
资源文件最后会别QT保存在 .qrc
文件中,之后编译工程后会将该资源文件编译成C++代码qrc_xxx.cpp
3.3 对象模型及moc工具
moc工具
QT为了同时具有运行时的效率以及更高级别的灵活性,“扩展”了标准 C++。即在使用标准 C++ 编译器编译 Qt 源程序之前,先使用一个叫做 moc(Meta Object Compiler,元对象编译器)的工具,对源代码进行一次预处理,生成标准 C++ 源代码,然后再使用标准 C++ 编译器进行编译。
增加的一些特性如下:
- 信号槽机制,用于解决对象之间的通讯;
- 可查询,并且可设计的对象属性;
- 强大的事件机制以及事件过滤器;
- 基于上下文的字符串翻译机制(国际化),即 tr() 函数
- 复杂的定时器实现,用于在事件驱动的 GUI 中嵌入能够精确控制的任务集成;
- 层次化的可查询的对象树,提供一种自然的方式管理对象关系。
- 智能指针(QPointer),在对象析构之后自动设为 0,防止野指针;
- 能够跨越库边界的动态转换机制
虽然利用模板可以达到类似的效果,但是 Qt 没有选择使用模板。按照 Qt 官方的说法,模板虽然是内置语言特性,但是其语法实在是复杂,并且由于 GUI 是动态的,利用静态的模板机制有时候很难处理。而自己使用 moc 生成代码更为灵活,虽然效率有些降低(一个信号槽的调用大约相当于四个模板函数调用),不过在现代计算机上,这点性能损耗实在是可以忽略。
对象模型
QObject
是以对象树的形式组织起来的。当创建一个QObject
对象时,QObject
的构造函数接收一个QObject
指针parent(父对象指针)作为参数。此时,创建的这个QObject
对象会自动添加到其父对象的children()
列表。当父对象析构的时候,这个列表中的所有对象也会被析构
注意:这里的父对象并不是继承意义上的父类!
这种机制在 GUI 程序设计中相当有用。例如,一个按钮有一个
QShortcut
(快捷键)对象作为其子对象。当我们删除按钮的时候,这个快捷键理应被删除
QWidget
是能够在屏幕上显示的一切组件的父类。QWidget
继承自QObject
,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。
例:当用户关闭一个对话框的时候,应用程序将其删除,那么,我们希望属于这个对话框的按钮、图标等应该一起被删除。事实就是如此,因为这些都是对话框的子组件。
当然也可以自己删除子对象,它们会自动从其父对象列表中删除。
可以使用QObject::dumpObjectTree()
和QObject::dumpObjectInfo()
这两个函数进行这方面的调试。
Qt 引入对象树的概念,在一定程度上解决了内存问题。
3.4 布局管理
Qt 提供了两种组件定位机制:绝对定位和布局定位。
- 绝对定位:给出这个组件的坐标和长宽值
- 布局定位:只要把组件放入某一种布局,布局由专门的布局管理器进行管理。当需要调整大小或者位置的时候,Qt 使用对应的布局管理器进行调整
例子:
1 | int main(int argc, char *argv[]) |
- 若想使用 overloaded 的 signal(更加详细的说明可以参考[2.3 有重载的信号](# 2.3 有重载的信号)):
- 使用 Qt 4 的
SIGNAL
和SLOT
宏(该宏需要指定参数信息,故不存在重载问题),但是该方法没有编译期错误检查 - 使用函数指针显式指定使用哪一个信号
- 使用 Qt 4 的
Qt 中提供的布局管理器:
QHBoxLayout
:按照水平方向从左到右布局QVBoxLayout
:按照竖直方向从上到下布局QGridLayout
:在一个网格中进行布局,类似于 HTML 的 tableQFormLayout
:按照表格布局,每一行前面是一段文本,文本后面跟随一个组件(通常是输入框),类似 HTML 的 formQStackedLayout
:层叠的布局,允许我们将几个组件按照 Z 轴方向堆叠,可以形成向导那种一页一页的效果
3.5 菜单栏、工具栏、状态栏
详见[3.1 添加动作](# 3.1 添加动作)
3.6 对话框
对话框通常会是一个顶层窗口,出现在程序最上层,用于实现短期任务或者简洁的用户交互。
Qt 中使用QDialog
类实现对话框。就像主窗口一样,我们通常会设计一个类继承QDialog
。QDialog
(及其子类,以及所有Qt::Dialog
类型的类)的对于其 parent 指针都有额外的解释:如果 parent 为 NULL,则该对话框会作为一个顶层窗口,否则则作为其父组件的子对话框(此时,其默认出现的位置是 parent 的中心)。其区别如下:
- 顶层窗口:在任务栏会有自己的位置
- 非顶层窗口:共享其父组件的位置
对话框分为模态对话框和非模态对话框:
- 模态对话框:会阻塞同一应用程序中其它窗口的输入
- 应用程序级别(默认):当该种对话框出现时,用户必须首先对对话框进行交互,直到关闭对话框,然后才能访问程序中其他的窗口。使用
QDialog::exec()
实现该模态的对话框 - 窗口级别:该模态仅仅阻塞与对话框关联的窗口,但是依然允许用户与程序中其它窗口交互,尤其适用于多窗口模式。使用
QDialog::open()
实现该模态的对话框
- 应用程序级别(默认):当该种对话框出现时,用户必须首先对对话框进行交互,直到关闭对话框,然后才能访问程序中其他的窗口。使用
- 非模态对话框:可以在显示着对话框的同时,继续对其他窗口进行输入。使用
QDialog::show()
实现该模态的对话框
注意,对话框创建在堆中时需要合理释放,可以参考[1.3 堆还是栈中创建](# 1.3 堆还是栈中创建)
消息传递
模态对话框:可以在exec()
函数之后直接从对话框的对象获取到数据值
例子:
1 | void MainWindow::open() |
非模态对话框:
在关闭时可以调用QDialog::accept()
或者QDialog::reject()
或者更通用的QDialog::done()
函数,此时可以直接在这里发出信号。或者直接重写QDialog::closeEvent()
函数,在该函数中发出信号。在需要接收数据的窗口连接到这个信号即可。
例子:
1 | // in dialog: |
- Qt 信号槽机制保证,槽函数在调用的时候始终可以使用
sender()
函数获取到 signal 的发出者。顺便说一句,sender()
函数的存在使我们可以利用这个函数,来实现一个只能打开一个的非模态对话框(方法就是在对话框打开时在一个对话框映射表中记录下标记,在对话框关闭时利用sender()
函数判断是不是该对话框,然后从映射表中将其删除)】
3.7 内置对话框
Qt 的内置对话框大致分为以下几类:
QColorDialog
:选择颜色QFileDialog
:选择文件或者目录QFontDialog
:选择字体QInputDialog
:允许用户输入一个值,并将其值返回QMessageBox
:模态对话框,用于显示信息、询问问题等QPageSetupDialog
:为打印机提供纸张相关的选项QPrintDialog
:打印机配置QPrintPreviewDialog
:打印预览QProgressDialog
:显示操作过程
标准对话框
所谓标准对话框,其实也就是一个普通的对话框。因此,我们同样可以将QDialog
所提供的其它特性应用到这种标准对话框上面
QMessageBox
用于显示消息提示,为模态对话框,是QDialog
的子类,一般会使用其提供的几个 static 函数:
void about(QWidget * parent, const QString & title, const QString & text)
:显示关于对话框。这是一个最简单的对话框,其标题是 title,内容是 text,父窗口是 parent。对话框只有一个 OK 按钮。void aboutQt(QWidget * parent, const QString & title = QString())
:显示关于 Qt 对话框。该对话框用于显示有关 Qt 的信息。StandardButton critical(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = Ok, StandardButton defaultButton = NoButton)
:显示严重错误对话框。这个对话框将显示一个红色的错误符号。可以通过 buttons 参数指明其显示的按钮。默认情况下只有一个 Ok 按钮,我们可以使用StandardButtons
类型指定多种按钮。StandardButton information(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = Ok, StandardButton defaultButton = NoButton)
:QMessageBox::information()
函数与QMessageBox::critical()
类似,不同之处在于这个对话框提供一个普通信息图标。StandardButton question(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = StandardButtons( Yes | No ), StandardButton defaultButton = NoButton)
:QMessageBox::question()
函数与QMessageBox::critical()
类似,不同之处在于这个对话框提供一个问号图标,并且其显示的按钮是“是”和“否”两个。StandardButton warning(QWidget * parent, const QString & title, const QString & text, StandardButtons buttons = Ok, StandardButton defaultButton = NoButton)
:QMessageBox::warning()
函数与QMessageBox::critical()
类似,不同之处在于这个对话框提供一个黄色叹号图标。
例子:
1 | if (QMessageBox::Yes == QMessageBox::question(this, |
自定义对话框:
1 | QMessageBox msgBox;// 创建在栈上 |
文件对话框
QFileDialog
:文件对话框。
例子:
1 | // 创建基本控件 |
槽函数:
1 | void MainWindow::openFile() |
-
QFileDialog::getOpenFileName()
:获取需要打开的文件路径1
2
3
4
5
6QString getOpenFileName(QWidget * parent = 0,
const QString & caption = QString(),
const QString & dir = QString(),
const QString & filter = QString(),
QString * selectedFilter = 0,
Options options = 0)- parent:父窗口。Qt 的标准对话框提供静态函数,用于返回一个模态对话框(在一定程度上这就是外观模式的一种体现)
- caption:对话框标题
- dir:对话框打开时的默认目录,“.” 代表程序运行目录,“/” 代表当前盘符的根目录(特指 Windows 平台;Linux 平台当然就是根目录),这个参数也可以是平台相关的,比如“C:\”等
- filter:过滤器。我们使用文件对话框可以浏览很多类型的文件,但是,很多时候我们仅希望打开特定类型的文件。比如,文本编辑器希望打开文本文件,图片浏览器希望打开图片文件。过滤器就是用于过滤特定的后缀名。如果我们使用“Image Files(*.jpg *.png)”,则只能显示后缀名是 jpg 或者 png 的文件。如果需要多个过滤器,使用“;;”分割,比如“JPEG Files(*.jpg);;PNG Files(*.png)”
- selectedFilter:默认选择的过滤器
- options:对话框的一些参数设定,比如只显示文件夹等等,它的取值是
enum QFileDialog::Option
,每个选项可以使用 | 运算组合起来
-
使用这种静态函数,在 Windows、Mac OS 上面都是直接调用本地对话框,但是 Linux 上则是
QFileDialog
自己的模拟。所以直接使用QFileDialog
进行设置,就和QMessageBox一样,对话框很可能与系统对话框的外观不一致 -
改代码仅用于演示,诸如:文件实际类型并没有检查、QTextStream::readAll()会直接读取所有内容,当文件过大时程序会直接死掉等问题均未解决
3.8 事件
事件(event)是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。事件也就是我们通常说的“事件驱动(event drive)”程序设计的基础概念。事件的出现,使得程序代码不会按照原始的线性顺序执行。
Qt 中的事件和信号槽却并不是可以相互替代的:
- 信号:由具体的对象发出,然后会马上交给由
connect()
函数连接的槽进行处理。信号一旦发出,对应的槽函数一定会被执行。 - 事件:Qt 使用一个事件队列对所有发出的事件进行维护,当新的事件产生时,会被追加到事件队列的尾部。前一个事件完成后,取出后面的事件进行处理。必要的时候 Qt 的事件也可以不进入事件队列,而是直接处理。事件可以使用“事件过滤器”进行过滤,对于有些事件进行额外的处理,另外的事件则可以不关心。
总的来说,如果我们使用组件,我们关心的是信号槽;如果我们自定义组件,我们关心的是事件
Qt 程序需要在main()
函数创建一个QCoreApplication
对象,然后调用它的exec()
函数。这个函数就是开始 Qt 的事件循环。在执行exec()
函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于QEvent
。在事件对象创建完毕后,Qt 将这个事件对象传递给QObject
的event()
函数。event()
函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler)
在所有组件的父类QWidget
中,定义了很多事件处理的回调函数,如keyPressEvent()
、keyReleaseEvent()
、mouseDoubleClickEvent()
、mouseMoveEvent()
、mousePressEvent()
、mouseReleaseEvent()
等。这些函数都是 protected virtual 的,即可以在子类中重新实现这些函数
例子:
1 | class EventLabel : public QLabel |
-
QString
的arg()
函数可以自动替换掉QString
中出现的占位符。其占位符以 % 开始,后面是占位符的位置,例如 %1,%2 这种 -
QWidget
中的mouseTracking
属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()
才会发出。- 如果
mouseTracking
是 false(默认),组件在至少一次鼠标点击之后,才能够被追踪,即发出mouseMoveEvent()
事件 - 如果
mouseTracking
为 true,则mouseMoveEvent()
直接可以被发出
- 如果
-
可以通过直接设置该属性解决上述问题:
1
2
3
4
5EventLabel *label = new EventLabel;
label->setWindowTitle("MouseEvent Demo");
label->resize(300, 200);
label->setMouseTracking(true);// 设置mouseTracking属性
label->show();
事件的接收与忽略
Qt 的事件传递是链状的,如果子组件没有处理这个事件,就会继续向其父组件传递。事件的传播是在组件层次上面的,而不是依靠类继承机制。Qt 的事件对象有两个函数:
accept()
:这个类的事件处理函数想要处理这个事件,且事件不会被继续传播给其父组件ignore()
:这个类的事件处理函数不想要处理这个事件,会从其父组件中寻找另外的接受者
在事件处理函数中,可以使用isAccepted()
来查询这个事件是不是已经被接收了
事实上,很少会使用accept()
和ignore()
函数,如果希望忽略事件(所谓忽略,是指自己不想要这个事件),只要调用父类的响应函数即可。
在一个特殊的情形下必须使用accept()
和ignore()
函数——窗口关闭的事件。对于窗口关闭QCloseEvent
事件:
accept()
: Qt 停止事件的传播,窗口关闭ignore()
:事件继续传播,即阻止窗口关闭
例子1(事件的基本使用):
1 | //!!! Qt5 |
- 如果希望忽略事件(所谓忽略,是指自己不想要这个事件),只要调用父类的响应函数即可(父类中默认调用
ignore()
),否则自定义的事件回调默认为accept()
- 作为所有组件的父类,
QWidget
的默认实现是调用的ignore()
上述代码的结果:点击按钮,出现“left”。即把父类的事件回调函数覆盖了,使之不能发送CustomButton::clicked
信号,所以对应的槽函数也不能执行。故有如下结论:
当重写事件回调函数时,时刻注意是否需要通过调用父类的同名函数来确保原有实现仍能进行
例子2(accept与ignore的使用):
1 | class CustomButton : public QPushButton |
在MainWindow
中添加了一个CustomWidget
,里面有两个按钮对象:CustomButton
和CustomButtonEx
。每一个类都重写了mousePressEvent()
函数。
-
运行程序点击 CustomButtonEx,结果输出:
1
CustomButtonEx
-
打开
代码1
出的注释,发现结果不变。即默认为accept -
在上述基础上继续打开
代码2
出的注释,结果为:1
2CustomButtonEx
CustomWidget- 此时事件继续传播,故
CustomButtonEx
的父组件CustomWidget
也收到了这个事件 - 事件的传播是在组件层次上面的,而不是依靠类继承机制
- 此时事件继续传播,故
-
在上述基础上继续打开
代码3
或代码4
出的注释,结果为:1
2
3CustomButtonEx
CustomWidget
MainWindow- 作为所有组件的父类,
QWidget
的默认实现是调用的ignore()
- 作为所有组件的父类,
例子3(窗口关闭事件):
1 | //!!! Qt5 |
setWindowTitle()
函数可以使用 [*] 这种语法来表明,在窗口内容发生改变时(通过setWindowModified(true)
函数通知),Qt 会自动在标题上面的 [*] 位置替换成 * 号- 使用 Lambda 表达式连接
QTextEdit::textChanged()
信号,将windowModified
设置为 true - 重写
closeEvent()
函数。该函数中先判断是不是有过修改,如果有,则弹出询问框,问一下是否要退出。如果用户点击了“Yes”,则接受关闭事件,这个事件所在的操作就是关闭窗口。因此,一旦接受事件,窗口就会被关闭;否则窗口继续保留。当然,如果窗口内容没有被修改,则直接接受事件,关闭窗口
event()
事件对象创建完毕后,Qt 将这个事件对象传递给QObject
的event()
函数。event()
函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。
所以event()
函数主要用于事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个event()
函数了。例如,我们希望在一个QWidget
组件中监听 tab 键的按下,那么就可以继承QWidget
,并重写它的event()
函数,来达到这个目的:
1 | bool CustomWidget::event(QEvent *e) |
QEvent *e
:需要转发的事件对象- 通过
QEvent::type()
函数可以检查事件的实际类型,返回值为QEvent::Type
类型的枚举 - 如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false
- 在
event()
中,调用事件对象的accept()
和ignore()
函数没有作用,不会影响到事件的传播 - 对于不关心的事件,应该调用父类的
event()
函数继续转发,否则就只能处理自定义的事件
查看QT源码,会发现QObject::event()
的实现与上述代码类似,通过QEvent::type()
函数检查事件的实际类型,并交给对应的事件处理器(event handler)来响应一个具体的事件,而这些事件处理器是 protected virtual 的,因此可以重写某一个事件处理器,让 Qt 调用自己的事件处理器。建议只重写事件处理器(需要考虑是否应当调用父类的同名处理器)而不改动event()
事件过滤器
Qt 创建了QEvent
事件对象之后,会调用QObject
的event()
函数处理事件的分发。如果需要在event()
函数中对事件进行操作,随着组件的增加,利用这种重写event()
函数的方式就变得更加繁琐。 Qt 提供了一种机制来达到这一目的:事件过滤器。
QObject
有一个eventFilter()
函数,用于指定组件创建事件过滤器,过滤器会检查该组件接收到的事件,如果这个事件是感兴趣的类型,就进行相关的处理;如果不是,就继续转发。签名:
1 | virtual bool QObject::eventFilter ( QObject * watched, QEvent * event ); |
- 返回值
- true:不想让感兴趣的事件继续转发,即过滤掉事件,之后的event()不再处理该事件
- 注意:如果在事件过滤器中 delete 了某个接收组件,务必将函数返回值设为 true。否则,Qt 还是会将事件分发给这个接收组件,从而导致程序崩溃。
- false:继续转发事件,即不过滤
- true:不想让感兴趣的事件继续转发,即过滤掉事件,之后的event()不再处理该事件
- 事件过滤器的调用时间是目标对象(也就是参数里面的
watched
对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched
对象以及以后所有的事件过滤器根本不会知道这么一个事件
创建完事件过滤器后,还需要进行安装过滤器:
1 | void QObject::installEventFilter ( QObject * filterObj ) |
- 任意
QObject
都可以作为事件过滤器- 如果没有重写
eventFilter()
函数,则该事件过滤器没有任何作用(默认什么都不过滤)
- 如果没有重写
- 可以调用多次,向一个对象上安装多个事件过滤器
- 最后一个安装的会第一个执行(后进先执行)
- 能够为整个应用程序添加一个事件过滤器(
QApplication
或QCoreApplication
对象都是QObject
的子类),且该全局的事件过滤器会在所有其它对象事件过滤器之前调用- 这种行为会严重降低整个应用程序的事件分发效率,一般不推荐
同样,可以通过下面的API进行移除过滤器:
1 | void QObject::removeEventFilter ( QObject * filterObj ) |
事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
例子(事件过滤器):
1 | class MainWindow : public QMainWindow |
总结
上文中,提到过两种处理事件的方法:
- 通过event()函数进行处理。在需要处理的组件较多时较为繁琐,且必须要继承该组件(protected 保护)
- 通过事件顾虑器进行处理。改进了上述问题,但是存在线程问题
拓展:
事实上,Qt 事件的调用最终都会追溯到
QCoreApplication::notify()
函数,因此,最大的控制权实际上是重写QCoreApplication::notify()
(但是不推荐)。声明:
1 virtual bool QCoreApplication::notify ( QObject * receiver, QEvent * event );将
event
发送给receiver
,即调用receiver->event(event)
,其返回值来自receiver
的事件处理器该函数为任意线程的任意对象的任意事件调用,因此不存在事件过滤器的线程的问题
但是不推荐这么做,因为
notify()
函数只有一个,而事件过滤器要灵活得多
Qt 的事件处理,实际上有五个层次:
- 重写
paintEvent()
、mousePressEvent()
等事件处理函数。这是最普通、最简单的形式,同时功能也最简单 - 重写
event()
函数。event()
函数是所有对象的事件入口,QObject
和QWidget
中的实现,默认是把事件传递给特定的事件处理函数 - 在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件
- 在
QCoreApplication::instance()
上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和notify()
函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程 - 重写
QCoreApplication::notify()
函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication
是单例的)
事件调用顺序为:
- 全局事件过滤器
- 对象上的事件过滤器
- event()
- 特定事件处理函数
例子(事件调用顺序):
1 | class Label : public QWidget |
结果:
1 | QApplication::eventFilter |
自定义事件
相比信号槽,事件的优点:
- 事件的分发既可以是同步的,又可以是异步的,而函数的调用或者说是槽的回调总是同步的
- 事件可以使用过滤器
注意:需要快速地处理事件,并且尽可能快地返回事件循环,否则可能引起事件循环的阻塞
事件类型
QT中继承类QEvent
即可自定义事件(主要时获得QEvent::Type
类型的参数作为自定义事件的类型值),为了防止和系统定义的事件冲突,这里只能取QEvent::User
(1000)和QEvent::MaxUser
(65535)之间的数(包括边界),为了防止用户自定义事件类型值冲突,QT提供以下API用于注册自定义事件类型值:
1 | static int QEvent::registerEventType ( int hint = -1 ); |
- static 函数,可以直接使用
QEvent
类调用 - 返回值:向系统注册的新的 Type 类型的值
- hint 若合法(该 hint 不会发生任何覆盖),则直接返回这个值;否则,自动分配一个合法值并返回
- 线程安全,不必另外添加同步
发送事件
方法一:
直接将event
事件发送给receiver
接受者(使用QCoreApplication::notify()
函数)
1 | static bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event); |
-
返回值是事件处理函数的返回值
-
在事件被发送的时候,
event
对象并不会被销毁,所以一般在栈上创建event
对象:1
2QMouseEvent event(QEvent::MouseButtonPress, pos, 0, 0, 0);
QApplication::sendEvent(mainWindow, &event);
方法二:
将event
事件及其接受者receiver
一同追加到事件队列中,函数立即返回
1 | static void QCoreApplication::postEvent(QObject *receiver, QEvent *event); |
- post 事件队列会持有事件对象,且在 post 时会将其 delete 掉,故必须在堆上创建
event
对象(当对象被发送之后,再试图访问event
对象会出现问题) - 保存在事件队列中的所有事件都通过
notify()
函数发送出去 - 事件会根据 post 的顺序进行处理,可以通过更改优先级来改变事件的处理顺序(默认优先级为
Qt::NormalEventPriority
) - 线程安全
方法三:
将事件队列中的 接受者为receiver
,事件类型为 event_type 的所有事件立即发送给 receiver 进行处理
1 | static void QCoreApplication::sendPostedEvents(QObject *receiver, int event_type); |
- 来自窗口系统的事件并不由该函数处理(而是
processEvent()
)
自定义事件处理函数
重写QObject::customEvent()
函数即可,可以通过转换 event 对象类型来判断不同的事件
1 | bool CustomWidget::event(QEvent *event) { |
3.9 绘制系统
四、文件
Qt 通过QIODevice
提供了对 I/O 设备的抽象,这些设备具有读写字节块的能力。下面是 I/O 设备的类图:
简要说明如下:
QIODevice
:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;QFlie
:访问本地文件或者嵌入资源;QTemporaryFile
:创建和访问本地文件系统的临时文件;QBuffer
:读写QByteArray
;QProcess
:运行外部程序,处理进程间通讯;QAbstractSocket
:所有套接字类的父类;QTcpSocket:TCP
协议网络数据传输;QUdpSocket
:传输 UDP 报文;QSslSocket
:使用 SSL/TLS 传输数据;QFileDevice:Qt5
新增加的类,提供了有关文件操作的通用实现
顺序访问设备:它们的数据只能访问一遍:从头走到尾,从第一个字节开始访问,直到最后一个字节,中途不能返回去读取上一个字节(QProcess
、QTcpSocket
、QUdpSoctet
和QSslSocket
)
随机访问设备:可以访问任意位置任意次数,还可以使用QIODevice::seek()
函数来重新定位文件访问位置指针(QFile
、QTemporaryFile
和QBuffer
)
一般会将文件路径作为参数传给QFile
的构造函数。也可以在创建好对象后使用setFileName()
来修改。QFile
需要使用 /
作为文件分隔符(会自动转换成操作系统所需要的形式)
QFile
主要提供了有关文件的各种操作,比如打开文件、关闭文件、刷新文件等。我们可以使用QDataStream
或QTextStream
类来读写文件,也可以使用QIODevice
类提供的read()
、readLine()
、readAll()
以及write()
这样的函数。值得注意的是,有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo
获取,而不是自己分析文件路径字符串
文件的打开方式:
枚举值 | 描述 |
---|---|
QIODevice::NotOpen |
未打开 |
QIODevice::ReadOnly |
以只读方式打开 |
QIODevice::WriteOnly |
以只写方式打开 |
QIODevice::ReadWrite |
以读写方式打开 |
QIODevice::Append |
以追加的方式打开,新增加的内容将被追加到文件末尾 |
QIODevice::Truncate |
以重写的方式打开,在写入新的数据时会将原有数据全部清除,游标设置在文件开头。 |
QIODevice::Text |
在读取时,将行结束符转换成 \n;在写入时,将行结束符转换成本地格式,例如 Win32 平台上是 \r\n |
QIODevice::Unbuffered |
忽略缓存 |
例子:
1 | int main(int argc, char *argv[]) |
-
可以使用
QDir::currentPath()
来获得应用程序执行时的当前路径 -
completeXXX函数输出:
1
2
3
4
5QFileInfo fi("/tmp/archive.tar.gz");
QString base = fi.baseName(); // base = "archive"
QString cbase = fi.completeBaseName(); // base = "archive.tar"
QString ext = fi.suffix(); // ext = "gz"
QString ext = fi.completeSuffix(); // ext = "tar.gz"
4.1 二进制文件
QIODevice
提供了read()
、readLine()
等基本的操作。同时,Qt 提供了更高一级的操作:用于二进制的流QDataStream
和用于文本流的QTextStream
QDataStream
提供了基于QIODevice
的二进制数据的序列化。数据流是一种二进制流,这种流完全不依赖于底层操作系统、CPU 或者字节顺序(大端或小端)
结合QIODevice
,QDataStream
可以很方便地对文件、网络套接字等进行读写操作。
写:
1 | QFile file("file.dat"); |
- 增加魔术数字及版本用于解决不同二进制文件的合法性
- 使用
QDataStream
写入时,实际上会在要写入的内容前面额外添加一个这段内容的长度值
读:
1 | QFile file("file.dat"); |
- 必须按照写入的顺序,将数据读取出来
例子(流与文件的区别):
1 | QFile file("file.dat"); |
4.2 文本文件读写
QTextStream
会自动将 Unicode 编码同操作系统的编码与换行符进行转换,这一操作对开发人员是透明的。QTextStream
使用 16 位的QChar
作为基础的数据存储单位(也支持 C++ 标准类型)
为方便起见,QTextStream
同std::cout
一样提供了很多描述符,被称为 stream manipulators:
描述符 | 等价于 |
---|---|
bin |
setIntegerBase(2) |
oct |
setIntegerBase(8) |
dec |
setIntegerBase(10) |
hex |
setIntegerBase(16) |
showbase |
`setNumberFlags(numberFlags() |
forcesign |
`setNumberFlags(numberFlags() |
forcepoint |
`setNumberFlags(numberFlags() |
noshowbase |
setNumberFlags(numberFlags() & ~ShowBase) |
noforcesign |
setNumberFlags(numberFlags() & ~ForceSign) |
noforcepoint |
setNumberFlags(numberFlags() & ~ForcePoint) |
uppercasebase |
`setNumberFlags(numberFlags() |
uppercasedigits |
`setNumberFlags(numberFlags() |
lowercasebase |
setNumberFlags(numberFlags() & ~UppercaseBase) |
lowercasedigits |
setNumberFlags(numberFlags() & ~UppercaseDigits) |
fixed |
setRealNumberNotation(FixedNotation) |
scientific |
setRealNumberNotation(ScientificNotation) |
left |
setFieldAlignment(AlignLeft) |
right |
setFieldAlignment(AlignRight) |
center |
setFieldAlignment(AlignCenter) |
endl |
operator<<('\n') 和flush() |
flush |
flush() |
reset |
reset() |
ws |
skipWhiteSpace() |
bom |
setGenerateByteOrderMark(true) |
这些描述符只是一些函数的简写:
1
2
3
4
5
6
7
8
9
10 QFile data("file.txt");
data.open(QFile::WriteOnly | QIODevice::Truncate)
QTextStream out(&data);
// 输出 12345678 的二进制形式
out << bin << 12345678;
// 相当于
out.setIntegerBase(2);
out << 12345678;
// 输出 1234567890 的带有前缀、全部字母大写的十六进制格式(0xBC614E)
out << showbase << uppercasedigits << hex << 12345678;
不仅是QIODevice
,QTextStream
也可以直接把内容输出到QString
:
1 | QString str; |
注意:在保存时,由于没有保存每段文字或每个数据的长度信息,所以一般会使用诸如QTextStream::readLine()
读取一行,或QTextStream::readAll()
读取所有文本这种函数,之后再对获得的QString
对象进行处理
QTextStream
同QDataStream
的使用基本一致:
例子(QTextStream使用):
写入:
1 | QFile data("file.txt"); |
读取:
1 | QFile data("file.txt"); |
-
默认
QTextStream
的编码格式是 Unicode,如果我们需要使用另外的编码:1
stream.setCodec("UTF-8");
五、模型
六、网络
七、进程与线程
7.1 进程
在 Qt 中,我们使用QProcess
来表示一个进程。这个类允许应用程序开启一个新的外部程序,并且与这个程序进行通讯
状态
QT中进程的状态:
QProcess::start()
函数后,QProcess
进入Starting
状态- 当程序开始执行之后,
QProcess
进入Running
状态,并且发出started()
信号 - 当进程退出时,
QProcess
进入NotRunning
状态(也是初始状态),并且发出finished()
信号finished()
信号以参数的形式提供进程的退出代码和退出状态- 如果发送错误,
QProcess
会发出error()
信号
QProcess
允许将一个进程当做一个顺序访问的 I/O 设备。故可以使用write()
函数将数据提供给进程的标准输入;使用read()
、readLine()
或者getChar()
函数获取其标准输出
由于QProcess
继承自QIODevice
,因此QProcess
可以作为QXmlReader
的输入或者直接使用QNetworkAccessManager
将其生成的数据上传到网络
通道
进程通常有两个预定义的通道:
- 标准输出通道(stdout):常规控制台的输出
- 标准错误通道(stderr):由进程输出的错误信息
这两个通道都是独立的数据流,可以通过使用setReadChannel()
函数来切换这两个通道。使用setProcessChannelMode()
函数设置MergedChannels
可以合并标准输出和标准错误通道。通道也可以发出相关的信号:
- 当进程的当前通道可用时,
QProcess
会发出readReady()
信号 - 当有了新的标准输出数据时,
QProcess
会发出readyReadStandardOutput()
信号 - 当有了新的标准错误数据时,则会发出
readyReadStandardError()
信号
另外,QProcess
还允许使用setEnvironment()
为进程设置环境变量,或者使用setWorkingDirectory()
为进程设置工作目录。
前面所说的信号槽机制与QNetworkAccessManager
都是异步的。但是QProcess
提供了同步函数:
waitForStarted()
:阻塞到进程开始;waitForReadyRead()
:阻塞到可以从进程的当前读通道读取新的数据;waitForBytesWritten()
:阻塞到数据写入进程;waitForFinished()
:阻塞到进程结束;
注意:
- 在主线程(调用了
QApplication::exec()
的线程)调用上面几个函数会让界面失去响应 - 进程的输出通道对应着
QProcess
的 读 通道,进程的输入通道对应着QProcess
的 写 通道。(使用QProcess
“读取”进程的输出,而针对QProcess
的“写入”则成为进程的输入)
例子:
1 | // 执行C:\\Windows\\System32\\cmd.exe /c dir C:\\ |
- 可以通过
setProgram()
和setArguments()
设置外部程序名字和程序启动参数
7.2 进程间通信(IPC)
进程是操作系统的基本调度单元,因此进程间交互不可避免与操作系统的实现息息相关
Qt 提供了四种进程间通信的方式:
- 使用共享内存(shared memory)交互:这是 Qt 提供的一种各个平台均有支持的进程间交互的方式。
- TCP/IP:其基本思想就是将同一机器上面的两个进程一个当做服务器,一个当做客户端,二者通过网络协议进行交互。除了两个进程是在同一台机器上,这种交互方式与普通的 C/S 程序没有本质区别。Qt 提供了 QNetworkAccessManager 对此进行支持。
- D-Bus:freedesktop 组织开发的一种低开销、低延迟的 IPC 实现。Qt 提供了 QtDBus 模块,把信号槽机制扩展到进程级别(前面强调是“普通的”信号槽机制无法实现 IPC),使得开发者可以在一个进程中发出信号,由其它进程的槽函数响应信号。
- QCOP(Qt COmmunication Protocol):QCOP 是 Qt 内部的一种通信协议,用于不同的客户端之间在同一地址空间内部或者不同的进程之间的通信。目前,这种机制只用于 Qt for Embedded Linux 版本。
通用的 IPC 实现大致只有共享内存和 TCP/IP 两种。后者前面已经大致介绍过(应用程序级别的 QNetworkAccessManager 或者更底层的 QTcpSocket 等);本章主要介绍前者
Qt 使用QSharedMemory
类操作共享内存段。可以把QSharedMemory
看做一种指针,这种指针指向分配出来的一个共享内存段。而这个共享内存段是由底层的操作系统提供,可以供多个线程或进程使用。同时,QSharedMemory
还提供了单一线程或进程互斥访问某一内存区域的能力。
当创建了QSharedMemory
实例后,可以使用其create()
函数请求操作系统分配一个共享内存段。如果创建成功(函数返回true
),Qt 会自动将系统分配的共享内存段连接(attach)到本进程。
有关共享内存段,各个平台的实现也有所不同:
- Windows:
QSharedMemory
不“拥有”共享内存段。当使用了共享内存段的所有线程或进程中的某一个销毁了QSharedMemory
实例,或者所有的都退出,Windows 内核会自动释放共享内存段。 - Unix:
QSharedMemory
“拥有”共享内存段。当最后一个线程或进程同共享内存分离,并且调用了QSharedMemory
的析构函数之后,Unix 内核会将共享内存段释放。- 注意:这里与 Windows 不同之处在于,如果使用了共享内存段的线程或进程没有调用
QSharedMemory
的析构函数,程序将会崩溃。
- 注意:这里与 Windows 不同之处在于,如果使用了共享内存段的线程或进程没有调用
- HP-UX:每个进程只允许连接到一个共享内存段。这意味着在 HP-UX 平台,
QSharedMemory
不应被多个线程使用
注意:如果某个共享内存段不是由 Qt 创建的,仍可以在 Qt 应用程序中使用。不过此时必须使用QSharedMemory::setNativeKey()
来设置共享内存段。使用原始键(native key)时,QSharedMemory::lock()
函数就会失效,必须自己保护共享内存段不会在多线程或进程访问时出现问题。
例子(程序有两个按钮,一个按钮用于加载一张图片,然后将该图片放在共享内存段;第二个按钮用于从共享内存段读取该图片并显示出来):
头文件:
1 | class QSharedMemory; |
源文件:
1 | const char *KEY_SHARED_MEMORY = "Shared"; // 共享内存段的键值 多个线程或进程使用同一个共享内存段时,该键值必须相同 |
7.3 线程
一个进程可以有一个或更多线程同时运行。线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理。
Qt 使用QThread
来管理线程
例子(用户点击按钮,开始一个非常耗时的运算,同时 LCD 开始显示逝去的毫秒数):
1 | class WorkerThread : public QThread |
run()
函数就是新的线程需要执行的代码finished()
信号是系统自动发出的
7.4 线程和事件循环
详细情况可以参考wiki文档:Threads Events QObjects
相关术语:
- 可重入的(Reentrant):如果多个线程可以在同一时刻调用一个类的所有函数,并且保证每一次函数调用都引用一个唯一的数据,就称这个类是可重入的(Reentrant means that all the functions in the referenced class can be called simultaneously by multiple threads, provided that each invocation of the functions reference unique data.)。大多数 C++ 类都是可重入的。类似的,一个函数被称为可重入的,如果该函数允许多个线程在同一时刻调用,而每一次的调用都只能使用其独有的数据。全局变量就不是函数独有的数据,而是共享的。换句话说,这意味着类或者函数的使用者必须使用某种额外的机制(比如锁)来控制对对象的实例或共享数据的序列化访问。
- 线程安全(Thread-safe):如果多个线程可以在同一时刻调用一个类的所有函数,即使每一次函数调用都引用一个共享的数据,就说这个类是线程安全的(Threadsafe means that all the functions in the referenced class can be called simultaneously by multiple threads even when each invocation references shared data.)。如果多个线程可以在同一时刻访问函数的共享数据,就称这个函数是线程安全的。
线程安全的语义要强于可重入
事件与信号的区别:
- 事件总是由某一种类型的对象表示,针对某一个特殊的对象
- 信号没有这种目标对象。所有
QObject
的子类都可以通过覆盖QObject::event()
函数来控制事件的对象。
事件可以由程序生成,也可以在程序外部生成。例如:
QKeyEvent
和QMouseEvent
对象表示键盘或鼠标的交互,通常由系统的窗口管理器产生;QTimerEvent
事件在定时器超时时发送给一个QObject
,定时器事件通常由操作系统发出;QChildEvent
在增加或删除子对象时发送给一个QObject
,这是由 Qt 应用程序自己发出的。
需要注意的是,与信号不同,事件并不是一产生就被分发。事件产生之后被加入到一个队列中(先进先出),该队列即被称为事件队列。事件分发器遍历事件队列,如果发现事件队列中有事件,那么就把这个事件发送给它的目标对象。这个循环被称作事件循环。其伪代码大致如下:
1 | while (is_active) |
在wait_for_more_events()
函数所得到的新的事件都应该是由程序外部产生的(所有内部事件都应该在事件队列中处理完毕了),并且可以被下面几种情况唤醒:
- 窗口管理器的动作(键盘、鼠标按键按下、与窗口交互等)
- 套接字动作(网络传来可读的数据,或者是套接字非阻塞写等)
- 定时器
- 由其它线程发出的事件(在后文会详细解释这种情况)
在解释为什么永远不要阻塞事件循环之前,需要了解什么是“阻塞”:
假设有一个按钮
Button
,这个按钮在点击时会发出一个信号。这个信号会与一个Worker
对象连接,这个Worker
对象会执行很耗时的操作。当点击了按钮之后,我们观察从上到下的函数调用堆栈:
1
2
3
4
5
6
7
8 main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork()
- 在
main()
函数开始事件循环QApplication::exec()
- 窗口管理器侦测到鼠标点击后,Qt 会发现并将其转换成
QMouseEvent
事件,发送给组件的event()
函数(这一过程是通过QApplication::notify()
函数实现)- 注意此时按钮并没有覆盖
event()
函数,因此其父类的实现将被执行,即QWidget::event()
函数。这个函数发现这个事件是一个鼠标点击事件,于是调用了对应的事件处理函数,即Button::mousePressEvent()
函数- 重写
mousePressEvent
函数,发出Button::clicked()
信号,而正是这个信号会调用Worker::doWork()
槽函数- 调用
Worker::doWork()
槽函数时,时间循环在一直等着事件处理函数返回(所谓阻塞事件循环),即一直等待槽函数返回,此时没有事件被派发处理- 在事件就此卡住时,组件也不会更新自身(因为
QPaintEvent
对象还在队列中),也不会有其它什么交互发生(还是同样的原因),定时器也不会超时并且网络交互会越来越慢直到停止。也就是说,各种依赖事件循环的活动都会停止- 此时窗口管理器会检测到你的应用程序不再处理任何事件,于是告诉用户程序失去响应。这就是为什么我们需要快速地处理事件,并且尽可能快地返回事件循环
怎样做才能既可以执行耗时的操作,又不会阻塞事件循环呢?
- 将任务移到另外的线程
- 手动强制运行事件循环。想要强制运行事件循环,需要在耗时的任务中一遍遍地调用
QCoreApplication::processEvents()
函数。该函数会发出事件队列中的所有事件,并且立即返回到调用者。(此时就是模拟了一个事件循环)- 为防止递归调用,调用时传入
QEventLoop::ExcludeUserInputEvents
参数即可不用再次派发用户输入事件(这些事件仍旧会保留在事件队列中)
- 为防止递归调用,调用时传入
- 使用
QEventLoop
类重新进入新的事件循环。通过调用QEventLoop::exec()
函数,重新进入新的事件循环,给QEventLoop::quit()
槽函数发送信号则退出这个事件循环。
注意:通过“其它的入口”进入事件循环要特别小心:它会导致递归调用
7.5 线程相关类
QThread
QThread
是 Qt 线程类中最核心的底层类。要使用QThread
开始一个线程,就需要先创建它的一个子类,然后覆盖其QThread::run()
函数:
1 | class Thread : public QThread |
然后这样使用新建的类来开始一个新的线程:
1 | Thread *thread = new Thread; |
- 从 Qt 4.4 开始,
QThread
不再是抽象类,QThread::run()
也有了一个默认的实现 QThread::run()
中会简单地调用QThread::exec()
函数,开始一个事件循环
QRunnable
这是一个轻量级的抽象类,用于开始一个另外线程的任务。这种任务是运行过后就丢弃的。由于这个类是抽象类,需要继承QRunnable
,然后重写其纯虚函数QRunnable::run()
1 | class Task : public QRunnable |
要执行一个QRunnable
对象,需要使用QThreadPool
类(用于管理一个线程池)。通过调用QThreadPool::start(runnable)
函数,将一个QRunnable
对象放入QThreadPool
的执行队列。一旦有线程可用,线程池将会选择一个QRunnable
对象,然后开始执行那个线程
所有 Qt 应用程序都有一个全局线程池,可以使用QThreadPool::globalInstance()
获得这个全局线程池;同样,也可以自己创建私有的线程池并进行手动管理。
注意:QRunnable
不是一个QObject
,因此也就没有内建的与其它组件交互的机制。为了与其它组件进行交互,必须自己编写低级线程原语,例如使用 mutex 守护来获取结果等。
QtConcurrent
这是一个高级 API,构建于QThreadPool
之上,用于处理大多数通用的并行计算模式:map、reduce 以及 filter。它还提供了QtConcurrent::run()
函数,用于在另外的线程运行一个函数。
注意:QtConcurrent
是一个命名空间而不是一个类,因此其中的所有函数都是命名空间内的全局函数。
不同于QThread
和QRunnable
,QtConcurrent
不要求使用低级同步原语:所有的QtConcurrent
都返回一个QFuture
对象。这个对象可以用来查询当前的运算状态(即任务的进度),可以用来暂停/回复/取消任务,当然也可以用来获得运算结果。
注意:并不是所有的QFuture
对象都支持暂停或取消的操作。
eg:由
QtConcurrent::run()
返回的QFuture
对象不能取消,但是由QtConcurrent::mappedReduced()
返回的是可以的。
QFutureWatcher
类则用来监视QFuture
的进度,可以用信号槽与QFutureWatcher
进行交互(注意:QFuture
也没有继承QObject
)。
总结
特性 | QThread |
QRunnable |
QtConcurrent |
---|---|---|---|
高级 API | ✘ | ✘ | ✔ |
面向任务 | ✘ | ✔ | ✔ |
内建对暂停/恢复/取消的支持 | ✘ | ✘ | ✔ |
具有优先级 | ✔ | ✘ | ✘ |
可运行事件循环 | ✔ | ✘ | ✘ |
7.6 线程和QObject
可以参考Github上的这篇博客多线程总结
主循环
每一个 Qt 应用程序至少有一个调用了QCoreApplication::exec()
的事件循环(主事件循环或主循环,main中,且QCoreApplication::exec()
只能在调用main()
函数的线程调用)。主循环所在的线程就是主线程,也被成为 GUI 线程(所有有关 GUI 的操作都必须在这个线程进行)
线程的事件循环
QThread
也可以开启事件循环,只不过是一个受限于线程内部的事件循环。QThread
的局部事件循环可以通过在QThread::run()
中调用QThread::exec()
开启:
1 | class Thread : public QThread |
- Qt 4.4 版本以后,
QThread::run()
不再是纯虚函数,默认实现会调用QThread::exec()
函数。QThread
中也可以通过QThread::quit()
和QThread::exit()
函数来终止事件循环
线程的事件循环用于为线程中的所有QObjects
对象分发事件,默认情况下这些对象包括线程中创建的所有对象,或者是在别处创建完成后被移动到该线程的对象(在后面详细介绍“移动”这个问题)
依附性
一个QObject
的所依附的线程(thread affinity)是指它所在的那个线程。它同样适用于在QThread
的构造函数中构建的对象:
1 | class MyThread : public QThread |
obj
、otherObj
和yetAnotherObj
这些对象不在MyThread
所表示的线程,而是在创建了MyThread
的那个线程中- 在
QCoreApplication
对象之前创建的QObject
没有所谓线程依附性,因此也就没有对象为其派发事件(可以理解为QCoreApplication
创建了主线程的QThread
对象)
线程间通信
可以使用线程安全的QCoreApplication::postEvent()
函数向一个对象发送事件。它将把事件加入到对象所在的线程的事件队列中,因此,如果这个线程没有运行事件循环,这个事件也不会被派发。
虽然QObject
是可重入的,但是 GUI 类,特别是QWidget
及其所有的子类,都是不可重入的(只能在主线程使用)。所以,不能有两个线程同时访问一个QObject
对象,除非这个对象的内部数据都已经很好地序列化(例如为每个数据访问加锁)。
在从另外的线程访问一个对象时,它可能正在处理所在线程的事件循环派发的事件!基于同样的原因,也不能在另外的线程直接
delete
一个QObject
对象,相反需要调用QObject::deleteLater()
函数,这会给对象所在线程发送一个删除的事件
QObject
的线程依附性通过调用QObject::moveToThread()
函数可以改变。该函数会改变一个对象及其所有子对象的线程依附性。由于QObject
不是线程安全的,所以只能在该对象所在线程上调用这个函数。即只能在对象所在线程将这个对象移动到另外的线程,不能在另外的线程改变对象的线程依附性。
Qt 要求QObject
的所有子对象都必须和其父对象在同一线程。这意味着:
-
不能对有父对象(parent 属性)的对象使用
QObject::moveToThread()
函数 -
不能在
QThread
中以这个QThread
本身作为父对象创建对象1
2
3
4
5class Thread : public QThread {
void run() {
QObject *obj = new QObject(this); // 错误!
}
};QThread
对象所依附的线程是创建它的那个线程,而不是它所代表的线程
在代表一个线程的QThread
对象销毁之前,所有在这个线程中的对象都必须先delete
(只需在QThread::run()
的栈上创建对象即可)。正因如此,QT中通过以下方法使线程创建的对象与其它线程的对象通信:在线程的事件队列中加入一个事件,然后在事件处理函数中调用所关心的函数。显然这需要线程有一个事件循环。
QMetaObject::invokeMethod()
静态函数会这样调用:
1 | QMetaObject::invokeMethod(object, "methodName", |
- 参数类型都必须提供一个公有构造函数,一个公有的析构函数和一个公有的复制构造函数,并且要使用
qRegisterMetaType()
函数向 Qt 类型系统注册
跨线程的信号槽也是类似的。将信号与槽连接起来时,QObject::connect()
的最后一个参数将指定连接类型:
Qt::DirectConnection
(直接连接):槽函数将在信号发出的线程直接调用Qt::QueuedConnection
(队列连接):向接受者所在线程发送一个事件,该线程的事件循环将获得这个事件,然后之后的某个时刻调用槽函数Qt::BlockingQueuedConnection
(阻塞的队列连接):像队列连接,但是发送者线程将会阻塞,直到接受者所在线程的事件循环获得这个事件,槽函数被调用之后,函数才会返回Qt::AutoConnection
(自动连接(默认)):如果接受者所在线程就是当前线程,则使用直接连接;否则将使用队列连接
注意在上面每种情况中,发送者所在线程都是无关紧要的!在自动连接情况下,Qt 需要查看信号发出的线程是不是与接受者所在线程一致,来决定连接类型。注意,Qt 检查的是信号发出的线程,而不是信号发出的对象所在的线程!我们可以看看下面的代码:
1 | class Thread : public QThread |
aSignal()
信号在一个新的线程被发出(也就是Thread
所代表的线程),并不是Object
所在的线程(Object
所在的线程和Thread
所在的是同一个线程),所以这里将会使用队列连接
另外一个常见的错误是:
1 | class Thread : public QThread |
- 这里的
obj
发出aSignal()
信号时,使用直接连接,Thread
对象所在线程发出了信号,也就是信号发出的线程与接受者是同一个 - 在
aSlot()
槽函数中,可以直接访问Thread
的某些成员变量,但是在访问这些成员变量时,Thread::run()
函数可能也在访问!这意味着二者并发进行:这是一个完美的导致崩溃的隐藏bug
另外一个例子可能更为重要:
1 | class Thread : public QThread |
- 使用队列连接
为了解决上述问题,可以这么做:Thread
构造函数中增加一个函数调用:moveToThread(this)
:
1 | class Thread : public QThread { |
- 实际上,这方案的确可行(因为
Thread
的线程依附性被改变了:它所在的线程成了自己),但是这并不是一个好主意 - 这表示误解了线程对象(
QThread
子类)的设计意图:QThread
对象不是线程本身,它是用于管理它所代表的线程的对象。因此,它应该在另外的线程被使用(通常就是它自己所在的线程),而不是在自己所代表的线程中
上述问题的最好解决方案是,将处理任务的部分与管理线程的部分分离。简单来说,是利用一个QObject
的子类,使用QObject::moveToThread()
改变其线程依附性:
1 | class Worker : public QObject |
7.7 线程总结
有关线程,可以做的是:
- 在
QThread
子类添加信号。这是绝对安全的,并且也是正确的
不应该做的是:
- 调用
moveToThread(this)
函数 - 指定连接类型:这通常意味着你正在做错误的事情,比如将
QThread
控制接口与业务逻辑混杂在了一起(而这应该放在该线程的一个独立对象中) - 在
QThread
子类添加槽函数:这意味着它们将在错误的线程被调用,也就是QThread
对象所在线程,而不是QThread
对象管理的线程。这又需要你指定连接类型或者调用moveToThread(this)
函数 - 使用
QThread::terminate()
函数
不能做的是:
- 在线程还在运行时退出程序。使用
QThread::wait()
函数等待线程结束 - 在
QThread
对象所管理的线程仍在运行时就销毁该对象。如果需要某种“自行销毁”的操作,你可以把finished()
信号同deleteLater()
槽连接起来