QT5入门

参考文档:QT学习之路2QT5编程入门教程QT wiki文档

一、下载及安装

1.1 下载

  1. 官网下载,但是国内一般比较慢
  2. 从上述链接进入后可以找到国内的镜像下载链接,这里推荐一个清华大学

目录结构:

目录 说明
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中五个标准重载函数:

  1. 将 signal 和 slot 作为字符串处理:

    1
    2
    3
    QMetaObject::Connection connect(const QObject *sender, const char *signal,
    const QObject *receiver, const char *slot,
    Qt::ConnectionType);
  2. 使用QMetaMethod进行类型比对

    1
    2
    3
    QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal,
    const QObject *receiver, const QMetaMethod &slot,
    Qt::ConnectionType);
    • 将每个函数看做是QMetaMethod的子类
  3. 本质上是将 this 指针作为 receiver(同第一种)

    1
    2
    3
    QMetaObject::Connection connect(const QObject *sender, const char *signal,
    const char *slot,
    Qt::ConnectionType) const;
  4. 指向成员函数的指针

    1
    2
    3
    QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal,
    const QObject *receiver, PointerToMemberFunction slot,
    Qt::ConnectionType)
  5. 接受 static 函数、全局函数以及 Lambda 表达式

    1
    2
    QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal,
    Functor slot);

QT4中有三个版本:

1
2
3
4
5
6
7
8
9
10
11
bool connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *slot,
Qt::ConnectionType);

bool connect(const QObject *sender, const QMetaMethod &signal,
const QObject *receiver, const QMetaMethod &slot,
Qt::ConnectionType);

bool connect(const QObject *sender, const char *signal,
const char *slot,
Qt::ConnectionType) const
  • 除了返回值,与 Qt 5 最大的区别在于,Qt 4 的 signal 和 slot 只有const char *这么一种形式。正因如此,一旦出现连接不成功的情况 Qt 4 是没有编译错误的,而是在运行时给出错误

两个常见宏:

  • SIGNAL、SLOT:将两个函数名转换成了字符串

2.2 自定义信号槽

newspaper.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <QObject>
class Newspaper : public QObject
{
Q_OBJECT
public:
Newspaper(const QString & name) :
m_name(name)
{
}

void send()
{
emit newPaper(m_name);
}

signals:
void newPaper(const QString &name);

private:
QString m_name;
};
  • 为了使用信号槽,必须继承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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <QObject>
#include <QDebug>

class Reader : public QObject
{
Q_OBJECT
public:
Reader() {}

void receiveNewspaper(const QString & name)
{
qDebug() << "Receives Newspaper: " << name;
}
};
  • 因为需要接受信号,所以需要继承QObject,并添加Q_OBJECT
  • Qt 5 中,任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数,且槽函数必须有实现代码。同时也会受到 public、private、protected 等访问控制符的影响

main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <QCoreApplication>

#include "newspaper.h"
#include "reader.h"

int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);

Newspaper newspaper("Newspaper A");// 创建Newspaper对象
Reader reader;// 创建Reader对象
QObject::connect(&newspaper, &Newspaper::newPaper,
&reader, &Reader::receiveNewspaper);// 连接信号与槽
newspaper.send();// 调用newspaper的信号发送函数

return app.exec();
}
  • 终端输出:Receives Newspaper: Newspaper A

与QT4中信号与槽的区别

  • 槽函数必须放在由 slots 修饰的代码块中,并且要使用访问控制符进行访问控制。其原则同其它函数一样:默认是 private 的,如果要在外部访问,就应该是 public slots;如果只需要在子类访问,就应该是 protected slots
  • QObject::connect()函数第二、第四个参数需要使用SIGNALSLOT这两个宏转换成字符串

2.3 有重载的信号

如果信号有重载,利用QT5中connet的写法会报错:由于这个函数(注意,信号实际也是一个普通的函数)有重载,因此不能用一个取址操作符获取其地址。详见[3.4 布局管理](# 3.4 布局管理)中的例子

如果想解决该问题可以通过:

  1. 采用QT4的方法,利用SIGNALSLOT宏来解决(该宏需要指明具体的函数)
  2. 使用一个函数指针来指明到底是哪一个信号

在上述例子中,可以换成如下的两种写法:

1
2
3
4
5
6
7
8
9
10
11
// 强制类型转换
QObject::connect(spinBox,
(void (QSpinBox:: *)(int))&QSpinBox::valueChanged,
slider,
&QSlider::setValue);
// 匿名指针
QObject::connect(spinBox,
(void (QSpinBox:: *)(int))&QSpinBox::valueChanged,
static_cast<void (QSpinBox:: *)(int)>(&QSpinBox::valueChanged),
slider,
&QSlider::setValue);
  • 更推荐匿名指针的写法(当参数类型发生改变时,编译器能够及时知道并报错,而强制类型转化的错误只能在运行时发现)

2.4 带有默认参数的槽函数

Qt 允许槽函数的参数数目可以比信号的参数少。但是有一种情况例外,使得槽函数的参数可以比信号的多,即槽函数的参数带有默认值

在[2.2 自定义信号槽](# 2.2 自定义信号槽)的例子中如果有如下信号和槽:

1
2
3
4
5
// Newspaper
signals:
void newPaper(const QString &name);
// Reader
void receiveNewspaper(const QString &name, const QDate &date = QDate::currentDate());

此时,如果按照上一节中推荐的匿名指针写法:

1
2
3
4
QObject::connect(&newspaper,
static_cast<void (Newspaper:: *)(const QString &)>(&Newspaper::newPaper),
&reader,
static_cast<void (Reader:: *)(const QString &, const QDate & =QDate::currentDate())>(&Reader::receiveNewspaper));

会得到一个断言错误,而本质上是C++的限制:参数默认值只能使用在直接地函数调用中,当使用函数指针取其地址的时候,默认参数是不可见的。但是此时任可以用QT4的连接写法

如果仍想使用QT5的写法,此时只能利用Lambda表达式:

1
2
3
QObject::connect(&newspaper,
static_cast<void (Newspaper:: *)(const QString &)>(&Newspaper::newPaper),
[=](const QString &name) { /* Your code here. */ });

三、MainWindow

QMainWindow是 Qt 框架带来的一个预定义好的主窗口类。即一个普通意义上的应用程序最顶层的窗口。通常是由一个标题栏,一个菜单栏,若干工具栏和一个任务栏。在这些子组件之间则是我们的工作区。

Main Window Struct

  • Window Title:即标题栏,通常用于显示标题和控制按钮,比如最大化、最小化和关闭等。通常,各个图形界面框架都会使用操作系统本地代码来生成一个窗口
  • Menu Bar:即菜单栏,用于显示菜单
  • Status Bar:即状态栏。当我们鼠标滑过某些组件时,可以在状态栏显示某些信息
  • Tool Bar Area:用于显示工具条区域(Qt 的主窗口支持多个工具条,故这里为矩形)。你可以将工具条拖放到不同的位置,因此这里说是 Area
  • Dock Widget Area:停靠窗口的显示区域。所谓停靠窗口,就像 Photoshop 的工具箱一样,可以停靠在主窗口的四周,也可以浮动显示
  • Central Widget:即工作区(重点关注)。通常我们会将程序最主要的工作区域放置在这里,类似 Word 的稿纸或者 Photoshop 的画布等等

3.1 添加动作

Qt 使用QAction类作为动作,代表了窗口的一个“动作”,这个动作可能显示在以下地方:

  1. 作为菜单项。当用户点击该菜单项,对用户的点击做出响应
  2. 作为工具栏按钮。用户点击这个按钮就可以执行相应的操作

这里直接抽象出公共的动作组成一个QAction类。当把QAction对象添加到菜单,就显示成一个菜单项,添加到工具栏,就显示成一个工具按钮。用户可以通过点击菜单项、点击工具栏按钮、点击快捷键来激活这个动作。

QAction包含了图标、菜单文字、快捷键、状态栏文字、浮动帮助等信息。当把一个QAction对象添加到程序中时,Qt 自己选择使用哪个属性来显示,无需我们关心。同时,Qt 能够保证把QAction对象添加到不同的菜单、工具栏时,显示内容是同步的。也就是说,如果在菜单中修改了QAction的图标,那么在工具栏上面这个QAction所对应的按钮的图标也会同步修改。

mainwindow.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

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

private:
void open();

QAction *openAction;
};

#endif // MAINWINDOW_H

mainwindow.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <QAction>
#include <QMenuBar>
#include <QMessageBox>
#include <QStatusBar>
#include <QToolBar>

#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
setWindowTitle(tr("Main Window"));// 设置主窗口的标题

openAction = new QAction(QIcon(":/images/doc-open"), tr("&Open..."), this);// 在堆上创建openAction对象
openAction->setShortcuts(QKeySequence::Open);// 说明QAction的快捷键
openAction->setStatusTip(tr("Open an existing file"));// 在状态栏显示的提示
connect(openAction, &QAction::triggered, this, &MainWindow::open);// 将QAction的triggered()信号与MainWindow类的open()函数连接起来

QMenu *file = menuBar()->addMenu(tr("&File"));// 创建并返回菜单栏
file->addAction(openAction);

QToolBar *toolBar = addToolBar(tr("&File"));// 创建并返回工具栏
toolBar->addAction(openAction);

statusBar() ;// 创建并返回状态栏
}

MainWindow::~MainWindow()
{
}

void MainWindow::open()
{
QMessageBox::information(this, tr("Information"), tr("Open"));
}
  • tr():国际化函数。使用专门的国际化工具,将tr()函数的字符串提取出来,进行国际化。由于所需进行国际化的文本应该被大多数人认识,故tr()函中一般会是英文文本
  • QAction的创建:
    • QIcon():设置图标。以 : 开始,意味着从资源文件中查找资源
    • 文本值前面有一个 &,意味着这将成为一个快捷键
  • 使用QKeySequence类来添加快捷键,会根据平台的不同来定义相应的快捷键

main函数:

1
2
3
4
5
6
7
8
9
int main(int argc, char *argv[])
{
QApplication app(argc, argv);

MainWindow win;
win.show();

return app.exec();
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main(int argc, char *argv[])
{
QApplication app(argc, argv);

QWidget window;
window.setWindowTitle("Enter your age");

QSpinBox *spinBox = new QSpinBox(&window);// 只能输入数字的输入框,并且带有上下箭头的步进按钮
QSlider *slider = new QSlider(Qt::Horizontal, &window);// 带有滑块的滑竿
spinBox->setRange(0, 130);
slider->setRange(0, 130);

QObject::connect(slider, &QSlider::valueChanged, spinBox, &QSpinBox::setValue);
void (QSpinBox:: *spinBoxSignal)(int) = &QSpinBox::valueChanged;//valueChanged函数有两个重装,这里通过函数指针指名是用的那个,否则下面的connect会报错:无法解析的重载函数类型
QObject::connect(spinBox, spinBoxSignal, slider, &QSlider::setValue);
spinBox->setValue(35);

QHBoxLayout *layout = new QHBoxLayout;// 布局管理器
layout->addWidget(spinBox);
layout->addWidget(slider);
window.setLayout(layout);// 将该布局管理器设为整个窗口的布局管理器

window.show();

return app.exec();
}
  • 若想使用 overloaded 的 signal(更加详细的说明可以参考[2.3 有重载的信号](# 2.3 有重载的信号)):
    1. 使用 Qt 4 的SIGNALSLOT宏(该宏需要指定参数信息,故不存在重载问题),但是该方法没有编译期错误检查
    2. 使用函数指针显式指定使用哪一个信号

Qt 中提供的布局管理器:

  • QHBoxLayout:按照水平方向从左到右布局
  • QVBoxLayout:按照竖直方向从上到下布局
  • QGridLayout:在一个网格中进行布局,类似于 HTML 的 table
  • QFormLayout:按照表格布局,每一行前面是一段文本,文本后面跟随一个组件(通常是输入框),类似 HTML 的 form
  • QStackedLayout:层叠的布局,允许我们将几个组件按照 Z 轴方向堆叠,可以形成向导那种一页一页的效果

3.5 菜单栏、工具栏、状态栏

详见[3.1 添加动作](# 3.1 添加动作)

3.6 对话框

对话框通常会是一个顶层窗口,出现在程序最上层,用于实现短期任务或者简洁的用户交互。

Qt 中使用QDialog类实现对话框。就像主窗口一样,我们通常会设计一个类继承QDialogQDialog(及其子类,以及所有Qt::Dialog类型的类)的对于其 parent 指针都有额外的解释:如果 parent 为 NULL,则该对话框会作为一个顶层窗口,否则则作为其父组件的子对话框(此时,其默认出现的位置是 parent 的中心)。其区别如下:

  • 顶层窗口:在任务栏会有自己的位置
  • 非顶层窗口:共享其父组件的位置

对话框分为模态对话框和非模态对话框:

  • 模态对话框:会阻塞同一应用程序中其它窗口的输入
    • 应用程序级别(默认):当该种对话框出现时,用户必须首先对对话框进行交互,直到关闭对话框,然后才能访问程序中其他的窗口。使用QDialog::exec()实现该模态的对话框
    • 窗口级别:该模态仅仅阻塞与对话框关联的窗口,但是依然允许用户与程序中其它窗口交互,尤其适用于多窗口模式。使用QDialog::open()实现该模态的对话框
  • 非模态对话框:可以在显示着对话框的同时,继续对其他窗口进行输入。使用QDialog::show()实现该模态的对话框

注意,对话框创建在堆中时需要合理释放,可以参考[1.3 堆还是栈中创建](# 1.3 堆还是栈中创建)

消息传递

模态对话框:可以在exec()函数之后直接从对话框的对象获取到数据值

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MainWindow::open()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Hello, dialog!"));
dialog.exec();
qDebug() << dialog.result();
}

// 更为直接的一种方式:
QDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {// 点击 确定
// do something
} else {// 点击 取消
// do something else
}

非模态对话框:

在关闭时可以调用QDialog::accept()或者QDialog::reject()或者更通用的QDialog::done()函数,此时可以直接在这里发出信号。或者直接重写QDialog::closeEvent()函数,在该函数中发出信号。在需要接收数据的窗口连接到这个信号即可。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// in dialog:
void UserAgeDialog::accept()
{
emit userAgeChanged(newAge); // newAge is an int
QDialog::accept();
}

// in main window:
void MainWindow::showUserAgeDialog()
{
UserAgeDialog *dialog = new UserAgeDialog(this);
connect(dialog, &UserAgeDialog::userAgeChanged, this, &MainWindow::setUserAge);
dialog->show();
}

// ...

void MainWindow::setUserAge(int age)
{
userAge = age;
}
  • 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
2
3
4
5
6
7
8
9
if (QMessageBox::Yes == QMessageBox::question(this,
tr("Question"),
tr("Are you OK?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes)) {
QMessageBox::information(this, tr("Hmmm..."), tr("I'm glad to hear that!"));
} else {
QMessageBox::information(this, tr("Hmmm..."), tr("I'm sorry!"));
}

自定义对话框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
QMessageBox msgBox;// 创建在栈上
msgBox.setText(tr("The document has been modified."));// 设置其主要文本信息
msgBox.setInformativeText(tr("Do you want to save your changes?"));// 设置简单说明文字
msgBox.setDetailedText(tr("Differences here..."));// 当点击"详细信息"按钮时显示的信息
msgBox.setStandardButtons(QMessageBox::Save
| QMessageBox::Discard
| QMessageBox::Cancel);// 自定义按键
msgBox.setDefaultButton(QMessageBox::Save);// 设置默认按键
int ret = msgBox.exec();//使其成为模态对话框
switch (ret) {
case QMessageBox::Save:
qDebug() << "Save document!";
break;
case QMessageBox::Discard:
qDebug() << "Discard changes!";
break;
case QMessageBox::Cancel:
qDebug() << "Close document!";
break;
}

文件对话框

QFileDialog:文件对话框。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 创建基本控件
openAction = new QAction(QIcon(":/images/file-open"), tr("&Open..."), this);
openAction->setShortcuts(QKeySequence::Open);
openAction->setStatusTip(tr("Open an existing file"));

saveAction = new QAction(QIcon(":/images/file-save"), tr("&Save..."), this);
saveAction->setShortcuts(QKeySequence::Save);
saveAction->setStatusTip(tr("Save a new file"));

QMenu *file = menuBar()->addMenu(tr("&File"));
file->addAction(openAction);
file->addAction(saveAction);

QToolBar *toolBar = addToolBar(tr("&File"));
toolBar->addAction(openAction);
toolBar->addAction(saveAction);

textEdit = new QTextEdit(this);
setCentralWidget(textEdit);
/// !!!Qt5
connect(openAction, &QAction::triggered, this, &MainWindow::openFile);
connect(saveAction, &QAction::triggered, this, &MainWindow::saveFile);
/// !!!Qt4
connect(openAction, SIGNAL(triggered()), this, SLOT(openFile()));
connect(saveAction, SIGNAL(triggered()), this, SLOT(saveFile()));

槽函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void MainWindow::openFile()
{
QString path = QFileDialog::getOpenFileName(this,
tr("Open File"),
".",
tr("Text Files(*.txt)"));
if(!path.isEmpty()) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {// 按照只读和文本方式打开文件
QMessageBox::warning(this, tr("Read File"),
tr("Cannot open file:\n%1").arg(path));
return;
}
QTextStream in(&file);
textEdit->setText(in.readAll());// 读取文件内容并赋值给textEdit显示出来
file.close();
} else {
QMessageBox::warning(this, tr("Path"),
tr("You did not select any file."));
}
}

void MainWindow::saveFile()
{
QString path = QFileDialog::getSaveFileName(this,
tr("Open File"),
".",
tr("Text Files(*.txt)"));
if(!path.isEmpty()) {
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {// 按照只写和文本方式打开文件
QMessageBox::warning(this, tr("Write File"),
tr("Cannot open file:\n%1").arg(path));
return;
}
QTextStream out(&file);
out << textEdit->toPlainText();// 将textEdit的内容输出到一个文件中
file.close();
} else {
QMessageBox::warning(this, tr("Path"),
tr("You did not select any file."));
}
}
  • QFileDialog::getOpenFileName():获取需要打开的文件路径

    1
    2
    3
    4
    5
    6
    QString 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 将这个事件对象传递给QObjectevent()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler)

在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如keyPressEvent()keyReleaseEvent()mouseDoubleClickEvent()mouseMoveEvent()mousePressEvent()mouseReleaseEvent()等。这些函数都是 protected virtual 的,即可以在子类中重新实现这些函数


例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class EventLabel : public QLabel
{
protected:
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
};

void EventLabel::mouseMoveEvent(QMouseEvent *event)
{
this->setText(QString("<center><h1>Move: (%1, %2)</h1></center>")
.arg(QString::number(event->x()), QString::number(event->y())));// QLabel支持 HTML
}

void EventLabel::mousePressEvent(QMouseEvent *event)
{
this->setText(QString("<center><h1>Press: (%1, %2)</h1></center>")
.arg(QString::number(event->x()), QString::number(event->y())));
}

void EventLabel::mouseReleaseEvent(QMouseEvent *event)
{
QString msg;
msg.sprintf("<center><h1>Release: (%d, %d)</h1></center>",
event->x(), event->y());
this->setText(msg);
}

int main(int argc, char *argv[])
{
QApplication a(argc, argv);

EventLabel *label = new EventLabel;
label->setWindowTitle("MouseEvent Demo");
label->resize(300, 200);
label->show();

return a.exec();
}
  • QStringarg()函数可以自动替换掉QString中出现的占位符。其占位符以 % 开始,后面是占位符的位置,例如 %1,%2 这种

  • QWidget中的mouseTracking属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()才会发出。

    • 如果mouseTracking是 false(默认),组件在至少一次鼠标点击之后,才能够被追踪,即发出mouseMoveEvent()事件
    • 如果mouseTracking为 true,则mouseMoveEvent()直接可以被发出
  • 可以通过直接设置该属性解决上述问题:

    1
    2
    3
    4
    5
    EventLabel *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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//!!! Qt5
// ---------- custombutton.h ---------- //
class CustomButton : public QPushButton
{
Q_OBJECT
public:
CustomButton(QWidget *parent = 0);
protected:
void mousePressEvent(QMouseEvent *event); // 事件回调函数
private:
void onButtonCliecked();
};

// ---------- custombutton.cpp ---------- //
CustomButton::CustomButton(QWidget *parent) :
QPushButton(parent)
{
connect(this, &CustomButton::clicked,
this, &CustomButton::onButtonCliecked);
}

void CustomButton::mousePressEvent(QMouseEvent *event) // 事件回调函数
{
if (event->button() == Qt::LeftButton) {
qDebug() << "left";
} else {
QPushButton::mousePressEvent(event);
}
}

void CustomButton::onButtonCliecked()
{
qDebug() << "You clicked this!";
}

// ---------- main.cpp ---------- //
int main(int argc, char *argv[])
{
QApplication a(argc, argv);

CustomButton btn;
btn.setText("This is a Button!");
btn.show();

return a.exec();
}
  • 如果希望忽略事件(所谓忽略,是指自己不想要这个事件),只要调用父类的响应函数即可(父类中默认调用ignore()),否则自定义的事件回调默认为accept()
  • 作为所有组件的父类,QWidget的默认实现是调用的ignore()

上述代码的结果:点击按钮,出现“left”。即把父类的事件回调函数覆盖了,使之不能发送CustomButton::clicked信号,所以对应的槽函数也不能执行。故有如下结论:

当重写事件回调函数时,时刻注意是否需要通过调用父类的同名函数来确保原有实现仍能进行


例子2(accept与ignore的使用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class CustomButton : public QPushButton
{
Q_OBJECT
public:
CustomButton(QWidget *parent) : QPushButton(parent)
{
}
protected:
void mousePressEvent(QMouseEvent *event)
{
qDebug() << "CustomButton";
}
};

class CustomButtonEx : public CustomButton//继承自定义按键CustomButton
{
Q_OBJECT
public:
CustomButtonEx(QWidget *parent) : CustomButton(parent)
{
}
protected:
void mousePressEvent(QMouseEvent *event)
{
// event->accept(); //代码1
// event->ignore(); //代码2
qDebug() << "CustomButtonEx";
}
};

class CustomWidget : public QWidget
{
Q_OBJECT
public:
CustomWidget(QWidget *parent) : QWidget(parent)
{
}
protected:
void mousePressEvent(QMouseEvent *event)
{
// QWidget::mousePressEvent(event);// 代码3
// event->ignore();// 代码4
qDebug() << "CustomWidget";
}
};

class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0) : QMainWindow(parent)
{
CustomWidget *widget = new CustomWidget(this);// 自定义Widget
CustomButton *cbex = new CustomButton(widget);// 自定义按钮
cbex->setText(tr("CustomButton"));
CustomButtonEx *cb = new CustomButtonEx(widget);// 自定义按钮Ex
cb->setText(tr("CustomButtonEx"));
QVBoxLayout *widgetLayout = new QVBoxLayout(widget);// 垂直布局
widgetLayout->addWidget(cbex);
widgetLayout->addWidget(cb);
this->setCentralWidget(widget);
}
protected:
void mousePressEvent(QMouseEvent *event)
{
qDebug() << "MainWindow";
}
};

MainWindow中添加了一个CustomWidget,里面有两个按钮对象:CustomButtonCustomButtonEx。每一个类都重写了mousePressEvent()函数。

  1. 运行程序点击 CustomButtonEx,结果输出:

    1
    CustomButtonEx
  2. 打开代码1出的注释,发现结果不变。即默认为accept

  3. 在上述基础上继续打开代码2出的注释,结果为:

    1
    2
    CustomButtonEx
    CustomWidget
    • 此时事件继续传播,故CustomButtonEx父组件CustomWidget也收到了这个事件
    • 事件的传播是在组件层次上面的,而不是依靠类继承机制
  4. 在上述基础上继续打开代码3代码4出的注释,结果为:

    1
    2
    3
    CustomButtonEx
    CustomWidget
    MainWindow
    • 作为所有组件的父类,QWidget的默认实现是调用的ignore()

例子3(窗口关闭事件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//!!! Qt5
...
textEdit = new QTextEdit(this);
setCentralWidget(textEdit);
connect(textEdit, &QTextEdit::textChanged, [=]() {
this->setWindowModified(true);
});

setWindowTitle("TextPad [*]");
...

void MainWindow::closeEvent(QCloseEvent *event)
{
if (isWindowModified()) {
bool exit = QMessageBox::question(this,
tr("Quit"),
tr("Are you sure to quit this application?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) == QMessageBox::Yes;
if (exit) {// 点击 Yes
event->accept();
} else {// 点击 No
event->ignore();
}
} else {
event->accept();
}
}
  • setWindowTitle()函数可以使用 [*] 这种语法来表明,在窗口内容发生改变时(通过setWindowModified(true)函数通知),Qt 会自动在标题上面的 [*] 位置替换成 * 号
  • 使用 Lambda 表达式连接QTextEdit::textChanged()信号,将windowModified设置为 true
  • 重写closeEvent()函数。该函数中先判断是不是有过修改,如果有,则弹出询问框,问一下是否要退出。如果用户点击了“Yes”,则接受关闭事件,这个事件所在的操作就是关闭窗口。因此,一旦接受事件,窗口就会被关闭;否则窗口继续保留。当然,如果窗口内容没有被修改,则直接接受事件,关闭窗口

event()

事件对象创建完毕后,Qt 将这个事件对象传递给QObjectevent()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。

所以event()函数主要用于事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个event()函数了。例如,我们希望在一个QWidget组件中监听 tab 键的按下,那么就可以继承QWidget,并重写它的event()函数,来达到这个目的:

1
2
3
4
5
6
7
8
9
10
11
bool CustomWidget::event(QEvent *e)
{
if (e->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
if (keyEvent->key() == Qt::Key_Tab) {
qDebug() << "You press tab.";
return true;
}
}
return QWidget::event(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事件对象之后,会调用QObjectevent()函数处理事件的分发。如果需要在event()函数中对事件进行操作,随着组件的增加,利用这种重写event()函数的方式就变得更加繁琐。 Qt 提供了一种机制来达到这一目的:事件过滤器。

QObject有一个eventFilter()函数,用于指定组件创建事件过滤器,过滤器会检查该组件接收到的事件,如果这个事件是感兴趣的类型,就进行相关的处理;如果不是,就继续转发。签名:

1
virtual bool QObject::eventFilter ( QObject * watched, QEvent * event );
  • 返回值
    • true:不想让感兴趣的事件继续转发,即过滤掉事件,之后的event()不再处理该事件
      • 注意:如果在事件过滤器中 delete 了某个接收组件,务必将函数返回值设为 true。否则,Qt 还是会将事件分发给这个接收组件,从而导致程序崩溃。
    • false:继续转发事件,即不过滤
  • 事件过滤器的调用时间是目标对象(也就是参数里面的watched对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched对象以及以后所有的事件过滤器根本不会知道这么一个事件

创建完事件过滤器后,还需要进行安装过滤器:

1
void QObject::installEventFilter ( QObject * filterObj )
  • 任意QObject都可以作为事件过滤器
    • 如果没有重写eventFilter()函数,则该事件过滤器没有任何作用(默认什么都不过滤)
  • 可以调用多次,向一个对象上安装多个事件过滤器
    • 最后一个安装的会第一个执行(后进先执行)
  • 能够为整个应用程序添加一个事件过滤器(QApplicationQCoreApplication对象都是QObject的子类),且该全局的事件过滤器会在所有其它对象事件过滤器之前调用
    • 这种行为会严重降低整个应用程序的事件分发效率,一般不推荐

同样,可以通过下面的API进行移除过滤器:

1
void QObject::removeEventFilter ( QObject * filterObj )

事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。


例子(事件过滤器):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MainWindow : public QMainWindow
{
public:
MainWindow();
protected:
bool eventFilter(QObject *obj, QEvent *event);
private:
QTextEdit *textEdit;
};

MainWindow::MainWindow()
{
textEdit = new QTextEdit;
setCentralWidget(textEdit);

textEdit->installEventFilter(this); // 安装过滤器
}

bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (obj == textEdit) { // 判断是否为感兴趣的组件
if (event->type() == QEvent::KeyPress) { // 判断是否为感兴趣的类型
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
qDebug() << "Ate key press" << keyEvent->key();
return true; // 过滤掉按键事件
} else {
return false; // 其他事件 继续处理
}
} else {
// 以防还有过滤器 对于其它的组件,保险起见还是调用父类函数
return QMainWindow::eventFilter(obj, event);
}
}

总结

上文中,提到过两种处理事件的方法:

  1. 通过event()函数进行处理。在需要处理的组件较多时较为繁琐,且必须要继承该组件(protected 保护)
  2. 通过事件顾虑器进行处理。改进了上述问题,但是存在线程问题

拓展:

事实上,Qt 事件的调用最终都会追溯到QCoreApplication::notify()函数,因此,最大的控制权实际上是重写QCoreApplication::notify()(但是不推荐)。声明:

1
virtual bool QCoreApplication::notify ( QObject * receiver, QEvent * event );

event发送给receiver,即调用receiver->event(event),其返回值来自receiver的事件处理器

该函数为任意线程的任意对象的任意事件调用,因此不存在事件过滤器的线程的问题

但是不推荐这么做,因为notify()函数只有一个,而事件过滤器要灵活得多

Qt 的事件处理,实际上有五个层次:

  1. 重写paintEvent()mousePressEvent()等事件处理函数。这是最普通、最简单的形式,同时功能也最简单
  2. 重写event()函数。event()函数是所有对象的事件入口,QObjectQWidget中的实现,默认是把事件传递给特定的事件处理函数
  3. 在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件
  4. QCoreApplication::instance()上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和notify()函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程
  5. 重写QCoreApplication::notify()函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)

事件调用顺序为:

  1. 全局事件过滤器
  2. 对象上的事件过滤器
  3. event()
  4. 特定事件处理函数

例子(事件调用顺序):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Label : public QWidget
{
public:
Label()
{
installEventFilter(this);
}

bool eventFilter(QObject *watched, QEvent *event)
{
if (watched == this) {
if (event->type() == QEvent::MouseButtonPress) {
qDebug() << "eventFilter";
}
}
return false;
}

protected:
void mousePressEvent(QMouseEvent *)
{
qDebug() << "mousePressEvent";
}

bool event(QEvent *e)
{
if (e->type() == QEvent::MouseButtonPress) {
qDebug() << "event";
}
return QWidget::event(e);
}
};

class EventFilter : public QObject
{
public:
EventFilter(QObject *watched, QObject *parent = 0) :
QObject(parent),
m_watched(watched)
{
}

bool eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_watched) {
if (event->type() == QEvent::MouseButtonPress) {
qDebug() << "QApplication::eventFilter";
}
}
return false;
}

private:
QObject *m_watched;
};

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
Label label;
app.installEventFilter(new EventFilter(&label, &label));
label.show();
return app.exec();
}

结果:

1
2
3
4
QApplication::eventFilter
eventFilter
event
mousePressEvent

自定义事件

相比信号槽,事件的优点:

  1. 事件的分发既可以是同步的,又可以是异步的,而函数的调用或者说是槽的回调总是同步的
  2. 事件可以使用过滤器

注意:需要快速地处理事件,并且尽可能快地返回事件循环,否则可能引起事件循环的阻塞

事件类型

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
    2
    QMouseEvent 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
2
3
4
5
6
7
8
bool CustomWidget::event(QEvent *event) {
if (event->type() == MyCustomEventType) {
CustomEvent *myEvent = static_cast<CustomEvent *>(event);
// processing...
return true;
}
return QWidget::event(event);
}

3.9 绘制系统

四、文件

Qt 通过QIODevice提供了对 I/O 设备的抽象,这些设备具有读写字节块的能力。下面是 I/O 设备的类图:

Qt5 IO 设备类图

简要说明如下:

  • QIODevice:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;
  • QFlie:访问本地文件或者嵌入资源;
  • QTemporaryFile:创建和访问本地文件系统的临时文件;
  • QBuffer:读写QByteArray
  • QProcess:运行外部程序,处理进程间通讯;
  • QAbstractSocket:所有套接字类的父类;
  • QTcpSocket:TCP协议网络数据传输;
  • QUdpSocket:传输 UDP 报文;
  • QSslSocket:使用 SSL/TLS 传输数据;
  • QFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现

顺序访问设备:它们的数据只能访问一遍:从头走到尾,从第一个字节开始访问,直到最后一个字节,中途不能返回去读取上一个字节(QProcessQTcpSocketQUdpSoctetQSslSocket

随机访问设备:可以访问任意位置任意次数,还可以使用QIODevice::seek()函数来重新定位文件访问位置指针(QFileQTemporaryFileQBuffer

一般会将文件路径作为参数传给QFile的构造函数。也可以在创建好对象后使用setFileName()来修改。QFile需要使用 / 作为文件分隔符(会自动转换成操作系统所需要的形式)

QFile主要提供了有关文件的各种操作,比如打开文件、关闭文件、刷新文件等。我们可以使用QDataStreamQTextStream类来读写文件,也可以使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main(int argc, char *argv[])
{
QApplication app(argc, argv);

QFile file("in.txt"); // 创建文件对象
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { // 只读方式,文本格式 打开文件
qDebug() << "Open file failed.";
return -1;
} else {
while (!file.atEnd()) {
qDebug() << file.readLine(); // 读取一整行
}
}

QFileInfo info(file); // 获取有关该文件的信息
qDebug() << info.isDir(); // 检查该文件是否是目录
qDebug() << info.isExecutable(); // 检查该文件是否是可执行文件
qDebug() << info.baseName(); // 直接获得文件名
qDebug() << info.completeBaseName(); // 完整文件名
qDebug() << info.suffix(); // 直接获取文件后缀名
qDebug() << info.completeSuffix(); // 完整后缀名

return app.exec();
}
  • 可以使用QDir::currentPath()来获得应用程序执行时的当前路径

  • completeXXX函数输出:

    1
    2
    3
    4
    5
    QFileInfo 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 或者字节顺序(大端或小端)

结合QIODeviceQDataStream可以很方便地对文件、网络套接字等进行读写操作。

写:

1
2
3
4
5
6
7
8
9
10
11
12
QFile file("file.dat");
file.open(QIODevice::WriteOnly);
QDataStream out(&file); // 类似于std::cout标准输出流
// 写入魔术数字和版本
out << (quint32)0xA0B0C0D0; // 魔术数字:防止程序不同版本之间按照不同的方式读取
out << (qint32)123; // 标识文件的版本
out.setVersion(QDataStream::Qt_4_0); // QT的版本:指定 Qt 按照哪个版本去读

// 写入用户数据
out << QString("the answer is"); // QDataStream重载了输出重定向<<运算符
out << (qint32)42; // 最好使用 Qt 整型来进行读写,保证在任意平台和任意编译器都能够有相同的行为
file.close(); // 如果不想关闭文件,可以使用 file.flush();
  • 增加魔术数字及版本用于解决不同二进制文件的合法性
  • 使用QDataStream写入时,实际上会在要写入的内容前面额外添加一个这段内容的长度值

读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
QFile file("file.dat");
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
// 检查魔术数字
quint32 magic;
in >> magic;
if (magic != 0xA0B0C0D0) {
return BAD_FILE_FORMAT;
}
// 检查版本
qint32 version;
in >> version;
if (version < 100) {
return BAD_FILE_TOO_OLD;
}
if (version > 123) {
return BAD_FILE_TOO_NEW;
}

if (version <= 110) {
in.setVersion(QDataStream::Qt_3_2); // 按照Qt_3_2的格式读取
} else {
in.setVersion(QDataStream::Qt_4_0); // 按照Qt_4_0的格式读取
}
// 读取数据
QString str;
qint32 a;
in >> str >> a;
  • 必须按照写入的顺序,将数据读取出来

例子(流与文件的区别):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
QFile file("file.dat");
file.open(QIODevice::ReadWrite);

QDataStream stream(&file);
QString str = "the answer is 42";
QString strout;

// 这种方式strout不会有任何输出
stream << str;
file.flush();
stream >> strout;

// strout有输出
stream << str;
stream.device()->seek(0); // 重新把游标设置为0
stream >> strout;

4.2 文本文件读写

QTextStream会自动将 Unicode 编码同操作系统的编码与换行符进行转换,这一操作对开发人员是透明的。QTextStream使用 16 位的QChar作为基础的数据存储单位(也支持 C++ 标准类型)

为方便起见,QTextStreamstd::cout一样提供了很多描述符,被称为 stream manipulators:

描述符 等价于
bin setIntegerBase(2)
oct setIntegerBase(8)
dec setIntegerBase(10)
hex setIntegerBase(16)
showbase setNumberFlags(numberFlags() | ShowBase)
forcesign setNumberFlags(numberFlags() | ForceSign)
forcepoint setNumberFlags(numberFlags() | ForcePoint)
noshowbase setNumberFlags(numberFlags() & ~ShowBase)
noforcesign setNumberFlags(numberFlags() & ~ForceSign)
noforcepoint setNumberFlags(numberFlags() & ~ForcePoint)
uppercasebase setNumberFlags(numberFlags() | UppercaseBase)
uppercasedigits setNumberFlags(numberFlags() | UppercaseDigits)
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;

不仅是QIODeviceQTextStream也可以直接把内容输出到QString

1
2
QString str;
QTextStream(&str) << oct << 31 << " " << dec << 25 << endl;

注意:在保存时,由于没有保存每段文字或每个数据的长度信息,所以一般会使用诸如QTextStream::readLine()读取一行,或QTextStream::readAll()读取所有文本这种函数,之后再对获得的QString对象进行处理

QTextStreamQDataStream的使用基本一致:


例子(QTextStream使用):

写入:

1
2
3
4
5
QFile data("file.txt");
if (data.open(QFile::WriteOnly | QIODevice::Truncate)) { // 只写并且覆盖已有内容的形式
QTextStream out(&data);
out << "The answer is " << 42;
}

读取:

1
2
3
4
5
6
7
QFile data("file.txt");
if (data.open(QFile::ReadOnly)) {
QTextStream in(&data);
QString str;
int ans = 0;
in >> str >> ans; // 这种方式会有问题
}
  • 默认QTextStream的编码格式是 Unicode,如果我们需要使用另外的编码:

    1
    stream.setCodec("UTF-8");

五、模型

六、网络

七、进程与线程

7.1 进程

在 Qt 中,我们使用QProcess来表示一个进程。这个类允许应用程序开启一个新的外部程序,并且与这个程序进行通讯

状态

QT中进程的状态:

  1. QProcess::start()函数后,QProcess进入Starting 状态
  2. 当程序开始执行之后,QProcess进入Running 状态,并且发出started()信号
  3. 当进程退出时,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():阻塞到进程结束;

注意:

  1. 在主线程(调用了QApplication::exec()的线程)调用上面几个函数会让界面失去响应
  2. 进程的输出通道对应着QProcess 通道,进程的输入通道对应着QProcess 通道。(使用QProcess“读取”进程的输出,而针对QProcess的“写入”则成为进程的输入)

例子:

1
2
3
4
5
6
7
8
9
10
11
// 执行C:\\Windows\\System32\\cmd.exe /c dir C:\\
QString program = "C:/Windows/System32/cmd.exe"; // 外部程序名字
QStringList arguments;
arguments << "/c" << "dir" << "C:\\"; // 程序启动参数
QProcess *cmdProcess = new QProcess;
QObject::connect(cmdProcess, &QProcess::readyRead, [=] () { // readReady()信号
QTextCodec *codec = QTextCodec::codecForName("GBK"); // Windows 控制台的默认编码是 GBK,为了避免出现乱码,必须设置文本的编码方式
QString dir = codec->toUnicode(cmdProcess->readAll()); // 获取进程输出
qDebug() << dir;
});
cmdProcess->start(program, arguments); // 开启新的进程
  • 可以通过setProgram()setArguments()设置外部程序名字和程序启动参数

7.2 进程间通信(IPC)

进程是操作系统的基本调度单元,因此进程间交互不可避免与操作系统的实现息息相关

Qt 提供了四种进程间通信的方式:

  1. 使用共享内存(shared memory)交互:这是 Qt 提供的一种各个平台均有支持的进程间交互的方式。
  2. TCP/IP:其基本思想就是将同一机器上面的两个进程一个当做服务器,一个当做客户端,二者通过网络协议进行交互。除了两个进程是在同一台机器上,这种交互方式与普通的 C/S 程序没有本质区别。Qt 提供了 QNetworkAccessManager 对此进行支持。
  3. D-Bus:freedesktop 组织开发的一种低开销、低延迟的 IPC 实现。Qt 提供了 QtDBus 模块,把信号槽机制扩展到进程级别(前面强调是“普通的”信号槽机制无法实现 IPC),使得开发者可以在一个进程中发出信号,由其它进程的槽函数响应信号。
  4. 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的析构函数,程序将会崩溃。
  • HP-UX:每个进程只允许连接到一个共享内存段。这意味着在 HP-UX 平台,QSharedMemory不应被多个线程使用

注意:如果某个共享内存段不是由 Qt 创建的,仍可以在 Qt 应用程序中使用。不过此时必须使用QSharedMemory::setNativeKey()来设置共享内存段。使用原始键(native key)时,QSharedMemory::lock()函数就会失效,必须自己保护共享内存段不会在多线程或进程访问时出现问题。


例子(程序有两个按钮,一个按钮用于加载一张图片,然后将该图片放在共享内存段;第二个按钮用于从共享内存段读取该图片并显示出来):

头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
class QSharedMemory;

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
MainWindow(QWidget *parent = 0);
~MainWindow();

private:
QSharedMemory *sharedMemory; // 共享内存段
};

源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const char *KEY_SHARED_MEMORY = "Shared"; // 共享内存段的键值 多个线程或进程使用同一个共享内存段时,该键值必须相同

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
sharedMemory(new QSharedMemory(KEY_SHARED_MEMORY, this)) // 初始化共享内存段
{
QWidget *mainWidget = new QWidget(this);
QVBoxLayout *mainLayout = new QVBoxLayout(mainWidget);
setCentralWidget(mainWidget);
// 两个按钮和一个标签
QPushButton *saveButton = new QPushButton(tr("Save"), this);
mainLayout->addWidget(saveButton);
QLabel *picLabel = new QLabel(this);
mainLayout->addWidget(picLabel);
QPushButton *loadButton = new QPushButton(tr("Load"), this);
mainLayout->addWidget(loadButton);
// 加载图片按钮
connect(saveButton, &QPushButton::clicked, [=]() {
if (sharedMemory->isAttached()) { // 如果sharedMemory已经与某个线程或进程连接
sharedMemory->detach(); // 则将其断开(因为就要向共享内存段写入内容了)
}
QString filename = QFileDialog::getOpenFileName(this); // 使用QFileDialog选择一张图片
QPixmap pixmap(filename);
picLabel->setPixmap(pixmap);

QBuffer buffer; // 创建图片缓存
QDataStream out(&buffer);
buffer.open(QBuffer::ReadWrite);
out << pixmap;

int size = buffer.size();
if (!sharedMemory->create(size)) { // 请求系统创建一个共享内存段
qDebug() << tr("Create Error: ") << sharedMemory->errorString();
} else {
sharedMemory->lock(); // 在读取或写入共享内存时,需要对共享内存段加锁
char *to = static_cast<char *>(sharedMemory->data());
const char *from = buffer.data().constData();
memcpy(to, from, qMin(size, sharedMemory->size())); // 复制内存段
sharedMemory->unlock(); // 解锁
}
});
// 加载按钮
connect(loadButton, &QPushButton::clicked, [=]() {
if (!sharedMemory->attach()) {
qDebug() << tr("Attach Error: ") << sharedMemory->errorString();
} else {
QBuffer buffer;
QDataStream in(&buffer);
QPixmap pixmap;
sharedMemory->lock();
buffer.setData(static_cast<const char *>(sharedMemory->constData()), sharedMemory->size());
buffer.open(QBuffer::ReadWrite);
in >> pixmap;
sharedMemory->unlock();
sharedMemory->detach(); // 在读取完毕后,将共享内存段断开连接
picLabel->setPixmap(pixmap);
}
});

7.3 线程

一个进程可以有一个或更多线程同时运行。线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理。

Qt 使用QThread管理线程


例子(用户点击按钮,开始一个非常耗时的运算,同时 LCD 开始显示逝去的毫秒数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class WorkerThread : public QThread
{
Q_OBJECT
public:
WorkerThread(QObject *parent = 0)
: QThread(parent)
{
}
protected:
void run() // 重写run()函数
{
for (int i = 0; i < 1000000000; i++); // 非常耗时的运算
emit done(); // 发出计算完成的信号
}
signals:
void done();
};



MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QWidget *widget = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout;
widget->setLayout(layout);
QLCDNumber *lcdNumber = new QLCDNumber(this);
layout->addWidget(lcdNumber);
QPushButton *button = new QPushButton(tr("Start"), this);
layout->addWidget(button);
setCentralWidget(widget);

QTimer *timer = new QTimer(this); // 毫秒数计数器
connect(timer, &QTimer::timeout, [=]() {
static int sec = 0;
lcdNumber->display(QString::number(sec++));
});

WorkerThread *thread = new WorkerThread(this);
connect(thread, &WorkerThread::done, timer, &QTimer::stop);
connect(thread, &WorkerThread::finished, thread, &WorkerThread::deleteLater); // 将WorkerThread::deleteLater()函数与WorkerThread::finished()信号连接起来,当线程完成时,系统可以自动清除线程实例
connect(button, &QPushButton::clicked, [=]() {
timer->start(1); // 开始计时
thread->start(); // 开启线程
});
}
  • 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()函数来控制事件的对象。

事件可以由程序生成,也可以在程序外部生成。例如:

  • QKeyEventQMouseEvent对象表示键盘或鼠标的交互,通常由系统的窗口管理器产生;
  • QTimerEvent事件在定时器超时时发送给一个QObject,定时器事件通常由操作系统发出;
  • QChildEvent在增加或删除子对象时发送给一个QObject,这是由 Qt 应用程序自己发出的。

需要注意的是,与信号不同,事件并不是一产生就被分发。事件产生之后被加入到一个队列中(先进先出),该队列即被称为事件队列。事件分发器遍历事件队列,如果发现事件队列中有事件,那么就把这个事件发送给它的目标对象。这个循环被称作事件循环。其伪代码大致如下:

1
2
3
4
5
6
7
while (is_active)
{
while (!event_queue_is_empty) { // 遍历整个事件队列
dispatch_next_event(); // 发送从队列中找到的事件
}
wait_for_more_events(); // 阻塞事件循环,直到又有新的事件产生
}

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()
  1. main()函数开始事件循环QApplication::exec()
  2. 窗口管理器侦测到鼠标点击后,Qt 会发现并将其转换成QMouseEvent事件,发送给组件的event()函数(这一过程是通过QApplication::notify()函数实现)
  3. 注意此时按钮并没有覆盖event()函数,因此其父类的实现将被执行,即QWidget::event()函数。这个函数发现这个事件是一个鼠标点击事件,于是调用了对应的事件处理函数,即Button::mousePressEvent()函数
  4. 重写mousePressEvent函数,发出Button::clicked()信号,而正是这个信号会调用Worker::doWork()槽函数
  5. 调用Worker::doWork()槽函数时,时间循环在一直等着事件处理函数返回(所谓阻塞事件循环),即一直等待槽函数返回,此时没有事件被派发处理
  6. 在事件就此卡住时,组件也不会更新自身(因为QPaintEvent对象还在队列中),也不会有其它什么交互发生(还是同样的原因),定时器也不会超时并且网络交互会越来越慢直到停止。也就是说,各种依赖事件循环的活动都会停止
  7. 此时窗口管理器会检测到你的应用程序不再处理任何事件,于是告诉用户程序失去响应。这就是为什么我们需要快速地处理事件,并且尽可能快地返回事件循环

怎样做才能既可以执行耗时的操作,又不会阻塞事件循环呢?

  1. 将任务移到另外的线程
  2. 手动强制运行事件循环。想要强制运行事件循环,需要在耗时的任务中一遍遍地调用QCoreApplication::processEvents()函数。该函数会发出事件队列中的所有事件,并且立即返回到调用者。(此时就是模拟了一个事件循环)
    • 为防止递归调用,调用时传入QEventLoop::ExcludeUserInputEvents参数即可不用再次派发用户输入事件(这些事件仍旧会保留在事件队列中)
  3. 使用QEventLoop类重新进入新的事件循环。通过调用QEventLoop::exec()函数,重新进入新的事件循环,给QEventLoop::quit()槽函数发送信号则退出这个事件循环。

注意:通过“其它的入口”进入事件循环要特别小心:它会导致递归调用

7.5 线程相关类

QThread

QThread是 Qt 线程类中最核心的底层类。要使用QThread开始一个线程,就需要先创建它的一个子类,然后覆盖其QThread::run()函数:

1
2
3
4
5
6
7
8
class Thread : public QThread
{
protected:
void run()
{
/* 线程的相关代码 */
}
};

然后这样使用新建的类来开始一个新的线程:

1
2
Thread *thread = new Thread;
thread->start(); // 使用 start() 开始新的线程
  • 从 Qt 4.4 开始,QThread不再是抽象类,QThread::run()也有了一个默认的实现
  • QThread::run()中会简单地调用QThread::exec()函数,开始一个事件循环

QRunnable

这是一个轻量级的抽象类,用于开始一个另外线程的任务。这种任务是运行过后就丢弃的。由于这个类是抽象类,需要继承QRunnable,然后重写其纯虚函数QRunnable::run()

1
2
3
4
5
6
7
8
class Task : public QRunnable
{
public:
void run()
{
/* 线程的相关代码 */
}
};

要执行一个QRunnable对象,需要使用QThreadPool类(用于管理一个线程池)。通过调用QThreadPool::start(runnable)函数,将一个QRunnable对象放入QThreadPool的执行队列。一旦有线程可用,线程池将会选择一个QRunnable对象,然后开始执行那个线程

所有 Qt 应用程序都有一个全局线程池,可以使用QThreadPool::globalInstance()获得这个全局线程池;同样,也可以自己创建私有的线程池并进行手动管理。

注意:QRunnable不是一个QObject,因此也就没有内建的与其它组件交互的机制。为了与其它组件进行交互,必须自己编写低级线程原语,例如使用 mutex 守护来获取结果等。

QtConcurrent

这是一个高级 API,构建于QThreadPool之上,用于处理大多数通用的并行计算模式:map、reduce 以及 filter。它还提供了QtConcurrent::run()函数,用于在另外的线程运行一个函数。

注意:QtConcurrent是一个命名空间而不是一个类,因此其中的所有函数都是命名空间内的全局函数。

不同于QThreadQRunnableQtConcurrent不要求使用低级同步原语:所有的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
2
3
4
5
6
7
8
class Thread : public QThread
{
protected:
void run() {
/* ... 初始化 ... */
exec();
}
};
  • Qt 4.4 版本以后,QThread::run()不再是纯虚函数,默认实现会调用QThread::exec()函数。QThread中也可以通过QThread::quit()QThread::exit()函数来终止事件循环

线程的事件循环用于为线程中的所有QObjects对象分发事件,默认情况下这些对象包括线程中创建的所有对象,或者是在别处创建完成后被移动到该线程的对象(在后面详细介绍“移动”这个问题)

依附性

一个QObject的所依附的线程(thread affinity)是指它所在的那个线程。它同样适用于在QThread的构造函数中构建的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyThread : public QThread
{
public:
MyThread()
{
otherObj = new QObject;
}

private:
QObject obj;
QObject *otherObj;
QScopedPointer yetAnotherObj;
};
  • objotherObjyetAnotherObj这些对象不在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
    5
    class Thread : public QThread {
    void run() {
    QObject *obj = new QObject(this); // 错误!
    }
    };
    • QThread对象所依附的线程是创建它的那个线程,而不是它所代表的线程

在代表一个线程的QThread对象销毁之前,所有在这个线程中的对象都必须先delete(只需在QThread::run()的栈上创建对象即可)。正因如此,QT中通过以下方法使线程创建的对象与其它线程的对象通信:在线程的事件队列中加入一个事件,然后在事件处理函数中调用所关心的函数。显然这需要线程有一个事件循环。

QMetaObject::invokeMethod()静态函数会这样调用:

1
2
3
4
QMetaObject::invokeMethod(object, "methodName",
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));
  • 参数类型都必须提供一个公有构造函数,一个公有的析构函数和一个公有的复制构造函数,并且要使用qRegisterMetaType()函数向 Qt 类型系统注册

跨线程的信号槽也是类似的。将信号与槽连接起来时,QObject::connect()的最后一个参数将指定连接类型:

  • Qt::DirectConnection(直接连接):槽函数将在信号发出的线程直接调用
  • Qt::QueuedConnection(队列连接):向接受者所在线程发送一个事件,该线程的事件循环将获得这个事件,然后之后的某个时刻调用槽函数
  • Qt::BlockingQueuedConnection(阻塞的队列连接):像队列连接,但是发送者线程将会阻塞,直到接受者所在线程的事件循环获得这个事件,槽函数被调用之后,函数才会返回
  • Qt::AutoConnection(自动连接(默认)):如果接受者所在线程就是当前线程,则使用直接连接;否则将使用队列连接

注意在上面每种情况中,发送者所在线程都是无关紧要的!在自动连接情况下,Qt 需要查看信号发出的线程是不是与接受者所在线程一致,来决定连接类型。注意,Qt 检查的是信号发出的线程,而不是信号发出的对象所在的线程!我们可以看看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Thread : public QThread
{
Q_OBJECT
signals:
void aSignal();
protected:
void run() {
emit aSignal();
}
};

/* ... */
Thread thread;
Object obj;
QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot())); // 发出信号的线程为Thread,不是同一个线程 --> 队列连接
thread.start();
  • aSignal()信号在一个新的线程被发出(也就是Thread所代表的线程),并不是Object所在的线程(Object所在的线程和Thread所在的是同一个线程),所以这里将会使用队列连接

另外一个常见的错误是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Thread : public QThread
{
Q_OBJECT
slots:
void aSlot() {
/* ... */
}
protected:
void run() {
/* ... */
}
};

/* ... */
Thread thread;
Object obj;
QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot())); // 发出信号的线程为当前线程,是同一个线程 --> 直接连接
thread.start();
obj.emitSignal();
  • 这里的obj发出aSignal()信号时,使用直接连接,Thread对象所在线程发出了信号,也就是信号发出的线程与接受者是同一个
  • aSlot()槽函数中,可以直接访问Thread的某些成员变量,但是在访问这些成员变量时,Thread::run()函数可能也在访问!这意味着二者并发进行:这是一个完美的导致崩溃的隐藏bug

另外一个例子可能更为重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Thread : public QThread
{
Q_OBJECT
slots:
void aSlot() {
/* ... */
}
protected:
void run() {
QObject *obj = new Object;
connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot()));// 发出信号的线程不是Thread --> 队列连接
/* ... */
}
};
  • 使用队列连接

为了解决上述问题,可以这么做:Thread构造函数中增加一个函数调用:moveToThread(this)

1
2
3
4
5
6
7
8
9
class Thread : public QThread {
Q_OBJECT
public:
Thread() {
moveToThread(this); // 错误!
}

/* ... */
};
  • 实际上,这方案的确可行(因为Thread的线程依附性被改变了:它所在的线程成了自己),但是这并不是一个好主意
  • 这表示误解了线程对象(QThread子类)的设计意图:QThread对象不是线程本身,它是用于管理它所代表的线程的对象。因此,它应该在另外的线程被使用(通常就是它自己所在的线程),而不是在自己所代表的线程中

上述问题的最好解决方案是,将处理任务的部分与管理线程的部分分离。简单来说,是利用一个QObject的子类,使用QObject::moveToThread()改变其线程依附性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork() {
/* ... */
}
};

/* ... */
QThread *thread = new QThread;
Worker *worker = new Worker;
connect(obj, SIGNAL(workReady()), worker, SLOT(doWork()));
worker->moveToThread(thread);
thread->start();

7.7 线程总结

有关线程,可以做的是:

  • QThread子类添加信号。这是绝对安全的,并且也是正确的

不应该做的是:

  • 调用moveToThread(this)函数
  • 指定连接类型:这通常意味着你正在做错误的事情,比如将QThread控制接口与业务逻辑混杂在了一起(而这应该放在该线程的一个独立对象中)
  • QThread子类添加槽函数:这意味着它们将在错误的线程被调用,也就是QThread对象所在线程,而不是QThread对象管理的线程。这又需要你指定连接类型或者调用moveToThread(this)函数
  • 使用QThread::terminate()函数

不能做的是:

  • 在线程还在运行时退出程序。使用QThread::wait()函数等待线程结束
  • QThread对象所管理的线程仍在运行时就销毁该对象。如果需要某种“自行销毁”的操作,你可以把finished()信号同deleteLater()槽连接起来