Qt 5 入门指南和参考手册
获取 Qt
Qt 是一个跨平台的 C++ 桌面应用程序开发框架,其源码和安装程序都可以从 Qt 官网 下载。其中,源码和离线安装程序位于 qt 文件夹中;在线安装程序位于 online_installers 文件夹中。在每个文件的 详情页 中还可以找到分布在世界各地的镜像站提供的下载链接。
Qt 可以在商业许可证或开源许可证的授权下使用。从 Qt 5.15 开始,Qt 公司不再为开源许可的 Qt 提供离线安装程序。最后一个离线安装程序是 Qt 5.14.2。该版本还不支持 Visual Studio 2019。
Qt Creator 是 Qt 提供的跨平台 IDE,也是 Qt 的组件之一,可以用 Qt 的安装程序安装,也可以下载 独立的安装包 进行安装。
Qt 的在线安装程序需要从网上下载数据。为了提高下载速度,需要让安装程序从镜像站下载数据。设置方式如下:打开安装程序,点击左下角的设置按钮,再点击底部的「添加」按钮,粘贴如下链接之一:
1 | https://mirrors.tuna.tsinghua.edu.cn/qt/online/qtsdkrepository/windows_x86/root/qt/ |
在 Visual Studio 中使用
在 Visual Studio 2019 中使用 Qt 需要安装插件 Qt Visual Studio Tools。Visual Studio 的插件是 .vsix
文件,可以用 Visual Studio 打开进行安装。安装完毕后,要在插件的设置(拓展 - Qt VS Tools - Qt Versions)中添加 qmake.exe
(在 Qt 的安装目录中可以找到)的路径。
在 Ubuntu 上使用
在 Ubuntu 上可以通过 apt
命令快速安装 Qt 5.15.3。
1 | sudo apt install -y libxcb-xinerama0 |
快速开始
在开始菜单中,可以找到 Qt 提供的工具集:
- Qt Creator 集成开发环境
- Assistant 用于查看 文档,集成在 Qt Creator 中
- Designer 用于编辑 UI 资源文件,集成在 Qt Creator 中
- Linguist 用于编辑语言资源文件
熟悉 Qt Creator
Qt Creator 的界面很简洁。顶部是主菜单栏。左侧是包含两组按钮的工具栏:第一组按钮用于切换到不同的界面(Edit 用于代码编辑,Design 用于 UI 设计,Projects 用于项目设置,Help 用于文档查阅);第二组按钮是构建、调试和运行按钮。右侧是工作区,工作区显示的内容取决于选择的界面。
切换到 Edit 界面时,工作区的主要内容是一个代码编辑器。点击底部状态栏的第一个按钮可以显示或隐藏工作区的左侧边栏。左侧边栏默认包含 Projects 面板和 Open Documents 面板。Projects 面板用于显示项目的目录结构,Open Documents 面板用于列出已打开的文件。每个面板的标题都是一个下拉列表,用于切换成其他面板。
新建项目
Qt Creator 支持创建 3 种不同的项目(File - New Project…):
- Qt Widgets Application 桌面应用程序(窗口程序)
- Qt Console Application 控制台应用程序
- Qt Quick Application 用于移动设备和嵌入式设备,使用 QML 语言进行 UI 设计的应用程序
一个桌面应用程序至少包含一个窗口类。新建桌面应用程序的步骤如下:
第一步是设置项目的名称并选择项目的保存位置。Qt Creator 会在项目的保存位置创建一个与项目同名的文件夹作为项目的根目录。比如在 D:\Code\qt_demo
创建文件夹 hello_world
:
第二步是选择项目的构建工具:
最重要的一步是选择窗口类的基类,并设置派生类的类名、派生类的头文件和源文件以及 UI 资源文件(Form file)的文件名。
窗口类的基类决定窗口的类型,有三种基类可以选择:
QMainWindow
用于创建包含主菜单栏、工具栏和状态栏的窗口QDialog
用于创建对话框QWidget
用于创建包含各种控件的窗口
最后一步是根据需要勾选平台工具集(如 MSVC 和 MinGW)和构建配置(如 Debug、Release)。
项目最开始通常包含 5 个文件:包含主函数的源文件 main.cpp
,窗口类的头文件 mainwindow.h
和源文件 mainwindow.cpp
,UI 资源文件 mainwindow.ui
以及 CMake 的配置文件 CMakeLists.txt
。
1 | hello_world $ tree . |
可视化 UI 设计
打开 UI 资源文件时会自动切换到 UI 设计界面。
UI 设计界面的左侧是控件的列表。要将控件添加到窗口中,直接将其拖动到中间的窗口中即可。例如,添加一个 Push Button
和一个 Label
,并设置它们的 text
属性。
添加到窗口中的控件会在右上角列出。在右上角选中一个控件后,左下角会列出该控件的属性。添加到窗口中的每个控件都有不同的 objectName
属性,它们是控件的变量名。为了便于使用控件,最好自定义控件的变量名,即自定义控件的 objectName
属性,比如将 Close
按钮的 objectName
属性设置为 pBtnClose
。
设置窗口大小和标题
窗口大小由基类 QWidget
的 geometry
属性决定;窗口的标题由基类 QWidget
的 windowTitle
属性决定。
关联信号和槽
窗口底部是信号和槽的列表。信号和槽都是类的成员方法,用于实现控件间通信。信号表示一个事件,比如 clicked()
表示控件被单击的事件;槽表示可以响应信号的动作,比如 close()
是关闭窗口的动作。
要在点击 Close
按钮后关闭窗口,可以在信号和槽的列表添加如下条目,将按钮控件的 clicked()
信号和窗口的 close()
槽关联起来:
构建并运行
在完成 UI 设计后,点击左下角绿色的运行按钮即可启用程序。点击 Close
按钮可以关闭窗口。
可视化 UI 设计的工作原理
在构建目录的 hello_world_autogen/include
子文件夹中可以找到头文件 ui_mainwindow.h
。它是由 UIC 根据 UI 资源文件生成的。头文件的文件名比 UI 资源文件多了前缀 ui_
,例如 mainwindow.ui
会被编译成 ui_mainwindow.h
。头文件中的代码首先被包含在两个宏 QT_BEGIN_NAMESPACE
和 QT_END_NAMESPACE
之间。这两个宏将 UIC 生成的代码置于 Qt 专用的命名空间。
ui_mainwindow.h
1 | QT_BEGIN_NAMESPACE |
头文件中的代码主要由两个类定义组成:一个名为 Ui_MainWindow
,它有一个名为 setupUi()
的方法,包含 UI 初始化的代码;另一个名为 Ui::MainWindow
,它只是简单地继承前者,不包含实质性的代码。要使用可视化 UI 设计,就需要在窗口类的构造函数中调用 setupUi()
方法。
窗口类的头文件只需声明类 Ui::MainWindow
,不必包含 UIC 生成的头文件 ui_mainwindow.h
。这是因为窗口类头文件只需声明一个 Ui::MainWindow
类的指针。
mainwindow.h
1 |
|
mainwindow.cpp
1 |
|
非可视化 UI 设计
进行一次非可视化 UI 设计可以帮助理解可视化 UI 设计的工作原理。不过,在实际开发中都会采用可视化 UI 设计,因为它不仅能够让 UI 设计工作变得直观,而且能够大大减少编码的工作量。
创建一个新的项目,在选择窗口的基类时,不要勾选 Generate form。这样 Qt Creator 就不会生成 .ui
文件。
不采用可视化 UI 设计时,控件和窗口的初始化都要在窗口类中完成。在窗口类的头文件中需要声明各个控件的指针和负责 UI 初始化的 setupUi()
方法。要使用信号和槽的窗口类还需要包含 Q_OBJECT
宏。
mydialog.h
1 |
|
窗口类的构造函数首先要调用父类的构造函数完成窗口的初始化,然后调用 setupUi()
完成 UI 的初始化,最后调用静态方法 QObject::connect()
完成信号和槽的关联。
mydialog.cpp
1 |
|
在 setupUi()
方法中,首先创建各个控件的对象,根据需要设置控件的属性,然后创建一个布局容器,将各个控件加入其中,最后,用 setLayout()
方法将布局容器设置为窗口的布局容器。
关联信号和槽
自动关联
在 UI 设计界面右击控件,点击 Go to slot…,可以快速添加一个新的方法作为控件的信号的槽。Designer 会在窗口类的头文件中新增方法的声明,在源文件中新增方法的定义。
mywidget.h
1 | private slots: |
mywidget.cpp
1 | void MyWidget::on_checkBox_clicked(bool checked) |
UIC 生成的 setupUi()
方法调用了静态方法 QMetaObject::connectSlotsByName()
。这个方法可以根据方法名自动关联对象中的信号和槽。比如名为 on_checkBox_clicked
方法就是控件 checkBox
的 clicked()
信号的槽。如果没有调用 connectSlotsByName()
方法,就需要调用静态方法 QObject::connect()
一一关联每一对信号和槽。
手动关联
信号和槽通常在构造函数中用 QObject::connect()
方法关联。该方法的第一、三个参数分别是信号的发送方和接收方,第二、四个参数分别是发送方的信号和接收方的方法。信号和作为槽的方法分别要用 SIGNAL()
和 SLOT()
宏处理。
1 | MyDialog::MyDialog(QWidget *parent) |
一个信号不仅可以关联到一个槽,还可以关联到另一个信号。
1 | connect(myButton, SIGNAL(clicked()), this, SIGNAL(buttonClicked())); |
一个信号可以关联多个槽和信号。多个信号关联可以同一个槽。重复关联信号和槽会导致槽被调用多次。
基础专题
常用控件
多行文本框
多行文本框用 QPlainTextEdit
对象表示。要向文本框追加文本,可以调用它的 appendPlainText()
方法:
1 | ui->plainTextEdit->appendPlainText("Hello, World!"); |
控件的字体
调用控件的 font()
方法可以获得一个字体对象,它描述控件文本的字体、字号和字体风格。
1 | QFont font = ui->plainTextEdit->font(); |
控件的颜色
调用控件的 palette()
方法可以获得一个调色板对象,它描述控件要使用的各种前颜色和背景色。
1 | QPalette palette = ui->plainTextEdit->palette(); |
控件的状态
控件的状态由 enabled
属性决定,可用 setEnabled()
方法设置。
单行文本框
单行文本框由 QLineEdit
对象表示。当 echoMode
属性为 Password
时,输入框的内容会显示为 *
星号。
1 | QString text(); // 获取文本 |
旋钮
QSpinBox
用于整数的输入,基数由 displayIntegerBase
属性决定,默认使用十进制;QDoubleSpinBox
用于小数的输入,显示多少位小数由 decimals
属性决定,默认显示两位。两者都可以给数字设置前缀、后缀、最小值、最大值和增量。
输入框的内容可以用 text()
方法获取,用 setText()
方法设置。实际表示的数字要用 value()
方法获取,用 setValue()
方法设置。
1 | ui->doubleSpinBox->setValue(1.5); |
滑动条
QSlider
用于整数的输入。可以给数字设置最小值、最大值和增量。关闭 tracking
属性时,控件仅在松开滑块时才发送 valueChanged()
信号。
时间日期
Qt 为时间日期的表示和显示提供了 3 种数据类型和 4 个控件。
Type | Widget |
---|---|
QTime |
QTimeEdit |
QDate |
QDateEdit |
QDateTime |
QDateTimeEdit |
- | QCalendarWidget |
QTimeEdit
和 QDateEdit
都是 QDateTimeEdit
的派生类。QDateTimeEdit
的常用属性如下:
dateTime
,QDateTime
类型,指示时间日期。date
,QDate
类型,指示日期。time
,QTime
类型,指示时间。currentSection
,枚举类型QDateTimeEdit::Section
,指示用户正在编辑的字段,比如YearSection
表示用户正在编辑年份。calendarPopup
,布尔型,指示是否提供日历选择框,区别见下图。displayFormat
,字符串型,指示时间日期的格式,如yyyy-MM-dd HH:mm::ss
表示2022-09-19 10:30:00
。
不提供日历选择框:
提供日历选择框:
时间日期类
要获取当前时间日期,可以用静态方法 QDateTime::currentDateTime()
,它返回一个 QDateTime
对象。调用 QDateTime
对象的 toString()
方法还可以获得时间日期的字符串表示。
1 | QDateTime dateTime = QDateTime::currentDateTime(); |
当前时间或日期也可以单独获取,或者通过 QDateTime
对象获取:
1 | QDate date = QTime::currentTime(); |
QDate
和 QTime
对象也都提供 toString()
方法。toString()
方法的参数可以包含下列转义序列:
- 年
yy
用两位数表示年份, 如 22yyyy
用四位数表示年份,如 2022
- 月
M
1-12MM
01-12
- 日
d
1-31dd
01-31
- 时
H
0-23 或 1-12HH
00-23 或 01-12
- 分
m
0-59mm
00-59
- 秒
s
0-59ss
00-59
- 毫秒
z
0-999zzz
000-999
AP
或A
是否采用 12 小时制并在结尾追加AM
或PM
的本地译名ap
或a
是否采用 12 小时制并在结尾追加am
或pm
的本地译名
要从字符串解析时间日期,可以用静态方法 QDateTime::fromString()
。字符串中包含的时间日期的格式要用第二个参数指示。
1 | QDateTime dateTime = QDateTime::fromString("2022919", "yyyyMdd"); |
计时器和定时器
计时器是 QElapsedTimer
对象,它的 start()
方法用于开始计时,elapsed()
方法返回经过的毫秒数。
1 | QElapsedTimer counter; |
定时器是 QTimer
对象,它的 interval
属性指示定时周期,单位毫秒,每经过一个周期就会发送一次 timeout()
信号。调用定时器的 start()
和 stop()
方法可以启动和关闭定时器。
1 | QTimer *timer; |
下拉列表
下拉列表是 QComboBox
对象。列表的每一项都可以关联一个 QVariant
对象,用于存储自定义数据。常用的方法如下:
void addItem(const QString &text, const QVariant &userData = QVariant())
新增项int count()
项数QString itemText(int index)
根据索引返回文本QVariant itemData(int index)
根据索引返回自定义数据int currentIndex()
当前项的索引QString currentText()
当前项的文本QVariant currentData()
当前项关联的自定义数据
选中项改变时发送的信号:
currentIndexChanged(int index)
currentTextChanged(const QString &text)
1 | ui->comboBox->addItem("Beijing", 10); |
列表
列表是 QListWidget
对象。列表的每一项是一个 QListWidgetItem
对象,移除时,要释放它的内存空间。
1 | QListWidgetItem *newItem = new QListWidgetItem("Beijing"); |
常用方法如下:
int count()
项数QListWidgetItem *item(int row)
根据行号返回项int row(const QListWidgetItem *item)
返回项的行号QListWidgetItem *currentItem()
当前项int currentRow()
当前项的行号QList<QListWidgetItem *> selectedItems()
选中项的列表clear()
清空QListWidgetItem *takeItem(int row)
根据行号从列表移除项并返回被移除的项
日志处理
可以用 qDebug()
宏输出字符串。
1 |
|
同类的宏还有 qInfo()
、qWarning()
、qCritical()
和 qFatal()
。只有 qFatal()
会终止程序。
所有日志最终都由一个日志处理程序处理。默认的日志处理程序不会添加文件名、行号和函数名等信息作为前缀。要自定义日志的格式,可以用全局函数 qInstallMessageHandler()
设置自定义的日志处理程序。
1 | void myMessageHandler(QtMsgType type, const QMessageLogContext &ctx, const QString &msg) |
参数 type
是枚举类型,指示日志的类型:
1 | enum QtMsgType { QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg, QtInfoMsg }; |
参数 ctx
指示日志的上下文,包含日志的文件名、行号和函数名,仅在调试模式有效。
1 | const char *file = ctx.file ? ctx.file : "unknown"; |
容器类
QList
是最常用的容器。在列表两端添加新元素可以用 append()
和 prepend()
方法。
1 |
|
在末端追加新元素还可以用 <<
运算符。
1 | list << "five" << "six"; |
QList
可以像数组那样使用。
1 | qDebug() << list[0]; |
Java 风格的迭代器
用 Java 风格的迭代器遍历列表时,主要依靠 hasNext()
和 next()
两个方法。hasNext()
方法用于检查下一个位置是否存在元素;next()
方法用于将迭代器移到下一个位置并返回该位置上的元素。
1 | QList<QString> list = { "one", "two", "three" }; |
toFront()
将迭代器移到第一个元素之前toBack()
将迭代器移到最后一个元素之后hasPrevious()
检查上一个位置是否存在元素previous()
将迭代器移到上一个位置并返回该位置上的元素
QListIterator
是只读迭代器。要改写元素,需要用 QMutableListIterator
。
1 | QList<int> list = { 1, 2, 3, 4 }; |
可写迭代器比只读迭代器多出以下三个方法:
remove()
移除当前位置的元素。setValue()
修改当前位置的元素insert()
在当前位置插入新元素
STL 风格的迭代器
用 STL 风格的迭代器遍历列表时,主要依靠 ++
和 *
两个运算符。++
运算符将迭代器移到下一个位置;*
运算符返回当前位置上的元素。
1 | QList<QString> list = { "one", "two", "three" }; |
顺序容器
顺序容器,也就是列表,有以下 5 种:
QList
基于堆内存的列表QLinkedList
基于链表的列表QVector
基于数组的列表QStack
栈QQueue
队列
前 3 种列表的内部实现不同,但它们提供的 API 基本相同。
映射
映射是特殊的列表,它们的元素是一个个键值对。
QMap
映射QHash
基于散列表的映射
QMap
会根据键名顺序存储每个键值,而 QHash
则不会。
1 |
|
在遍历映射时,迭代器的 key()
方法返回键名,value()
方法返回键值。
1 | QMap<char, int>::iterator p; |
集合
QSet
是基于 QHash
的集合。集合是特殊的列表,它的元素不能重复。
1 |
|
工具类
QString
QString
类用于处理字符串。
1 | QString s = "Hello, World!"; |
count()
、length()
和 size()
都返回字符个数。
append()
和 prepend()
用于在首尾追加字符串。
1 | QString s = "bbb"; |
检查首尾:
1 | QString s = "ui_mainwindow.h"; |
trimmed()
去掉首尾的空格。simplified()
不仅去掉首尾的空格,还会将中间的连续空格替换成一个。
1 | QString s = " Hello, World! "; |
在字符串中检索:
1 | QString s = "Made in China"; |
截取:
1 | QString s = "root:x:0:0:root:/root:/bin/bash"; |
在字符串和数字之间转换:
1 | qDebug() << QString("12").toInt(); // 12 |
格式化字符串
格式化字符串可以用 arg()
方法,它将字符串中的 %n
替换为给定参数,其中 n
是从 1
开始的整数。
1 | const int i = 1; // current file's number |
字符编码转换
QString
内部使用 UTF-16 编码。在创建 QString
对象时,如果提供的字符串使用本机编码,就需要用静态方法 QString::fromLocal8Bit()
来创建 QString
对象。
1 | const char src[] = "好"; // 0xba 0xc3 0x00 (GBK) |
需要 QString
对象提供本机编码的字符串时,可以借助 toLocal8Bit()
方法,它用一个 QByteArray
对象保存转换后的字符串。
1 | QByteArray bytes = str.toLocal8Bit(); // 0xba 0xc3 (GBK) |
还可以用 toUtf8()
方法获取 UTF-8 编码的字符串:
1 | bytes = str.toUtf8(); // 0xe5 0xa5 0xbd (UTF-8) |
在 Windows 上,本机编码默认为 GBK 编码。如果不是,应该用静态方法 QTextCodec::setCodecForLocale()
设置正确的本机编码:
1 |
|
QStringList
QStringList
是字符串列表。
1 | QStringList stringList; |
在 QStringList
和 QString
之间转换很容易:QString
的 split()
方法用分隔符将一个字符串分割成若干子串;QStringList
的 join()
方法用分隔符将若干子串拼接成一个字符串。
1 | QString s1 = "John,David,Mike"; |
文件系统
文件类
文件用 QFile
对象表示。创建 QFile
对象时需要指定一个文件作为当前文件(可以是不存在的文件)。
1 | QFile f("a.txt"); |
在打开文件之前可以用 setFileName()
方法重新设置当前文件。
文件操作
QFile
类提供下列操作文件的方法:
exists()
检查当前文件是否存在rename(const QString &newName)
移动当前文件copy(const QString &newName)
复制当前文件remove()
删除当前文件moveToTrash()
移动当前文件到回收站
读写文件
读写文件前要先用 open()
方法打开文件。打开文件时需要选择打开模式。文件打开后要用 close()
方法关闭。读写模式由枚举类型 IODevice
表示。
1 | if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { |
有以下打开模式:
NotOpen
,文件未打开ReadOnly
,只读WriteOnly
,只写,隐含Truncate
ReadWrite
,读写Append
,追加Truncate
,先清空再打开Text
,读文件时,统一将行结束符替换成\n
;写文件时,统一将行结束符替换成平台相关的行结束符。Unbuffered
,不要缓存NewOnly
,新建文件(若文件已存在,则打开失败)ExistingOnly
,打开已存在的文件(若文件不存在,则打开失败)
调用 readAll()
方法可以一次性将文件的内容全部读取出来。
1 | QByteArray bytes = f.readAll(); |
也可以用 readLine()
方法按行读取文件的内容。
1 | qint64 QIODevice::readLine(char *data, qint64 maxSize); |
readLine()
方法每次最多从文件读取 maxSize-1
个字节并返回实际读取的字节数。如果遇到行结束符,或者到达文件末尾,则提前返回。注意,readLine()
方法最后还会向 data
写入一个字符串结束符 \0
。
1 | // a.txt |
readLine()
方法有一个返回 QByteArray
对象的重载,它返回的 QByteArray
对象剔除了 \0
。
1 | QByteArray bytes = f.readLine(10); // 123\n |
在读文件的过程中可以用 atEnd()
方法检查是否到达文件末尾。
1 | while (!f.atEnd()) { |
向文件写入数据可以用 write()
方法。
1 | qint64 QIODevice::write(const char *data, qint64 maxSize) |
write()
方法的返回值都是实际写入的字节数。
1 | QByteArray bytes = "12345"; |
文件信息类
要提取文件或目录的信息,可以借助 QFileInfo
类。
exists()
是否存在isDir()
是否为目录isFile()
是否为文件isSymLink()
是否为符号链接或快捷方式path()
文件所在目录的路径absolutePath()
文件所在目录的绝对路径absoluteFilePath()
文件的绝对路径fileName()
文件名(含扩展名)baseName()
文件名(不含扩展名)suffix()
扩展名
目录类
下列是常用的 QDir
类的静态方法:
QDir::homePath()
获取用户文件夹的路径QDir::tempPath()
获取系统临时目录的路径QDir::currentPath()
获取工作目录的路径QDir::isAbsolutePath(const QString &path)
是否为绝对路径QDir::isRelativePath(const QString &path)
是否为相对路径QDir::cleanPath(const QString &path)
统一目录分隔符、移除多余的目录分隔符、消去.
和..
目录操作
创建 QDir
对象时,需要指定一个目录作为当前目录(可以是不存在的目录)。
1 | QDir dir("D:/a/b/c"); |
当前目录可以用 path()
方法获取,用 setPath()
方法重新设置。
exists()
检查当前目录是否存在removeRecursively()
删除当前目录(递归处理子目录)mkdir(const QString &dirName)
创建目录rmdir(const QString &dirName)
删除目录mkpath(const QString &dirPath)
创建目录(递归处理父级目录)rmpath(const QString &dirPath)
删除目录(递归处理父级目录)rename(const QString &oldName, const QString &newName)
移动目录(递归处理子目录)
目录的路径
可以用下列方法对路径进行转换或分解:
makeAbsolute()
将当前目录的路径转换为绝对路径absolutePath()
获取当前目录的绝对路径(可能是符号链接)canonicalPath()
获取当前目录的绝对路径(自动解析符号链接)dirName()
获取当前目录的目录名
杂项
JSON 文档
JSON 文档是文本文档,用于存储结构化数据,支持下列 6 种数据类型:
- 用双引号
""
括起来的字符串 - 整数或浮点数
- 布尔值
true
或false
- 空值
null
- 数组
- 对象
数组是元素的集合,元素要放在中括号 []
中并用逗号 ,
隔开,元素支持 6 种数据类型,例如:
1 | "Phone numbers": [ |
对象是属性的集合,属性要放在大括号 {}
中并用逗号 ,
隔开,属性是一个键值对,键名和键值之间用冒号 :
隔开,键名都是字符串,键值支持 6 种数据类型,例如:
1 | { |
JSON 文档的内容就是一个字符串,它要么是一个对象,要么是一个数组。
在 Qt 中,JSON 文档用 QJsonDocument
对象表示;JSON 数组用 QJsonArray
对象表示;JSON 对象用 QJsonObject
对象表示。JSON 对象的属性和 JSON 数组的元素都用 QJsonValue
对象表示。使用这些类需要包含下列头文件:
1 |
JSON 文档对象可以用 QJsonDocument::fromJson()
函数根据 JSON 文档的内容创建。
1 | QJsonDocument doc = QJsonDocument::fromJson(R"({ "name": "John", "age": 18 })"); |
JSON 文档对象的 toJson()
方法的作用与之相反:
1 | QString str = doc.toJson(QJsonDocument::Compact); // "{\"age\":18,\"name\":\"John\"}" |
可以用 JSON 文档对象的 isNull()
方法检查它是否为空,即不包含任何数组或对象;用 isArray()
或 isObject()
方法检查它是否包含一个数组或对象;用 array()
或 object()
方法获取它包含的数组或对象。
1 | if (!doc.isNull()) { |
QJsonObject
对象的 keys()
方法返回一个字符串列表,其中包含各个属性的键名。
1 | QStringList keyNames = obj.keys(); // ("age", "name") |
访问某个属性或元素的值,首先要用 value()
方法或中括号运算符,获取代表该属性或元素的 QJsonValue
对象。
1 | QJsonValue val = obj["name"]; |
有两种方式可以检查 QJsonValue
对象是否代表一个不存在属性或元素:
- 将对象与
QJsonValue::Undefined
比较 - 调用对象的
isUndefined()
方法
然后用 is*()
系列方法检查 QJsonValue
对象包含的数据的类型:
isString()
是否为字符串isDouble()
是否为整数或浮点数isBool()
是否为布尔值true
或false
isNull()
是否为空值null
isArray()
是否为数组isObject()
是否为对象
最后用 to*()
系列方法获取 QJsonValue
对象包含的数据:
1 | if (val.isString()) { |
动作
动作是菜单项、工具栏按钮或快捷键对应的命令,被调用时会发出 triggered()
信号。动作是独立对象,可以为之设置图标、文本、提示和快捷键等。一个动作可以同时被添加到菜单和工具栏中。
1 | QAction *openAction = new QAction(); |
多语言国际化
在开发多语言应用时,显示在 UI 中的字符串要用静态方法 QObject::tr()
或 QObject::trUtf8()
处理,它负责把字符串替换成目标语言的版本。
可执行文件的路径
用 QCoreApplication
类的静态方法可以获取可执行文件的路径。
1 | QCoreApplication::applicationFilePath(); // 可执行文件的路径 |
信号源
在作为槽的方法中,可以用静态方法 QObject::sender()
获取发送信号的对象。它的返回值要用 qobject_cast<>()
做强制类型转换。
1 | QSpinBox *pSpinBox = qobject_cast<QSpinBox *>( QObject::sender() ); |
进阶专题
结合 CMake
用 find_package()
命令就可以将 Qt 整合到项目中。
1 | set(CMAKE_AUTOMOC ON) |
前三个变量指示 CMake 自动寻找 Qt 的 MOC、RCC 和 UIC 并分别用它们处理 .cpp
文件、.qrc
文件和 .ui
文件。CMAKE_INCLUDE_CURRENT_DIR
变量指示 CMake 将源目录和构建目录都添加到附加包含目录,这样才能使用 UIC 生成的头文件。目标的 WIN32_EXECUTABLE
属性指示目标的产物是一个桌面应用程序而不是控制台程序。
在生成构建系统时需要设置 CMAKE_PREFIX_PATH
变量:
1 | cmake -S . -B build -DCMAKE_PREFIX_PATH="D:/Qt/5.15.2/msvc2019_64" |
元对象系统
元对象系统(Meta-Object System)的主要作用是提供了一种对象间通信机制。要使用元对象系统的类必须满足下列三个条件:
- 继承
QObject
类; - 在类的声明的
private
部分包含Q_OBJECT
宏; - 用元对象编译器(MOC,Meta-Object Compiler)处理。
元对象编译器只是一个预处理器,并非真正的编译器。它会为每个声明中包含 Q_OBJECT
宏的类生成一个源文件。这些源文件包含实现元对象系统所需的代码,在构建时与类的源文件一起编译和链接。
QObject
类还实现了一个简易的定时器。
信号-槽机制
信号-槽机制是由元对象系统提供的一种对象间通信机制。信号是用来代表一个事件的方法;槽是用作事件处理器的方法。当事件发生时,只需发射信号,与之关联的槽就会被调用。
声明和发射信号
信号是特殊的方法,只需声明,无需定义。在类的声明中,信号的声明要放在 signals
部分。发射信号的方法与调用函数的方法相同,只是发射信号的语句前要包含 emit
关键字。例如,在 Sender
类的声明中添加信号 messageGenerated()
的声明,并 generateMessage()
方法中发射 messageGenerated()
信号:
1 | class Sender : public QObject |
声明和定义槽
槽跟普通的方法一样,既要声明,也要定义,可以当作普通的方法直接调用。在类的声明中,槽的声明要放在 * slots
部分。
1 | class Receiver : public QObject |
关联信号和槽
信号发射后,只有与之关联的槽会被调用。一个信号可以关联多个槽;一个槽也可以被多个信号关联。关联信号和槽的方法是:调用 QObject::connect()
方法,用信号和槽所在对象的地址作为第一和第三个参数,用 SIGNAL()
和 SLOT()
宏作为第二和第四个参数,SIGNAL()
和 SLOT()
宏的参数分别是信号和槽的签名。例如,将 Sender
对象的 messageGenerated()
信号与 Receiver
对象的 printMessage()
槽关联:
1 | int main() |
属性系统
属性是逻辑上的公开成员变量。每个属性对应一个私有成员变量和两个专门用于读写该私有成员变量的公开成员函数。这两个公开成员函数分别称为属性的获取器(Getter)和设置器(Setter)。只有获取器的属性称为只读属性。每个属性还可以对应一个表示它的值发生变化的信号。属性的获取器和设置器以及它对应的信号的声明要仿照以下形式:
1 | // Getter |
属性的作用之一是方便组件样式的定义。
定义属性
例如,要定义属性 age
,需要声明一个私有成员变量 age_
和一个表示 age_
的值发生变化的信号 ageChanged()
,再声明两个专门用于读写 age_
的公开成员函数 age()
和 setAge()
,让 setAge()
在适当的时机发射信号 ageChanged()
:
1 | class User : public QObject |
有了上述声明和定义,User
对象就好像拥有了一个名为 age
的公开成员变量,可以在其他函数中读写:
1 | User user; |
声明属性
属性系统提供了统一的读写属性的方法 setProperty()
和 property()
。使用两个方法的前提是在类的声明中用 Q_PROPERTY()
宏声明属性。Q_PROPERTY()
宏支持两种声明属性的方式:一种是指定属性的获取器(和设置器),而无需指定属性对应的私有成员变量;另一种是指定属性对应的私有成员变量,而无需定义属性的获取器和设置器。例如,通过指定获取器和设置器的方式声明属性 age
:
1 | class User : public QObject |
int age
表示该属性是 int
型变量,属性名为 age
。属性名无需和它对应的私有成员变量名保持一致。READ age
表示该属性的获取器为 age()
。WRITE setAge
表示该属性可写,设置器为 setAge()
;NOTIFY ageChanged
表示该属性发生变化时会发射的信号为 ageChanged()
。
声明过的属性可以用 setProperty()
和 property()
两个方法读写:
1 | user.setProperty("age", 19); |
property()
方法总是返回一个 QVariant
对象。它有一系列 to*()
方法用于将属性的值转换为需要的类型。
用第二种方式声明属性时,无需定义属性的获取器和设置器。例如,要定义一个名为 email
的属性并用第二种方式声明,只需修改类的头文件:
1 | class User : public QObject |
MEMBER email_
表示该属性对应的私有成员变量是 email_
。
没有获取器和设置器的属性同样可以用 setProperty()
和 property()
两个方法读写:
1 | user.setProperty("email", "your@example.com"); |
动态属性
动态属性是在运行时才定义的属性。在用 setProperty()
方法设置属性时,如果要设置的属性不存在,它就会立即定义一个动态属性。例如,定义一个名为 name
的动态属性:
1 | user.setProperty("name", "John"); |
元对象
QObject
类及其派生类都对应一个 QMetaObject
对象。QObject
类及其派生类的元对象可以通过它们的 metaObject()
方法获取。元对象包含派生类的元数据,比如派生类包含的成员函数、构造函数和枚举成员的信息以及派生类的类名等。例如,派生类的类名可以通过调用元对象的 className()
方法获得:
1 | const QMetaObject* userMetaObject = user.metaObject(); |
每个 QObject
派生类对应一个元对象,不同的 QObject
派生类对应不同的元对象。同一个 QObject
派生类的不同对象的 metaObject()
方法返回的元对象都是同一个。
为了便于叙述,下文将 QObject
类及其派生类的对象都称为 QObject
对象。
属性的信息
在类的声明中用 Q_PROPERTY()
宏声明的各个属性的信息,可以通过元对象获取。元对象的 propertyCount()
方法返回属性的个数;property()
方法返回包含指定属性元数据的 QMetaProperty
对象。QMetaProperty
对象的 name()
和 typeName()
方法分别返回属性的属性名和类型名。
1 |
|
有了属性名,就可以访问属性的值。此时有两种方式可以访问属性的值:一种是像上文那样调用 QObject
对象的 property()
方法,用属性名作为参数;另一种是调用 QMetaProperty
对象的 read()
方法,用 QObject
对象的地址作为参数。
1 | QMetaProperty metaProperty = userMetaObject->property(1); |
类的附加信息
类的附加信息是一组键名和键值都是字符串的键值对,要用 Q_CLASSINFO()
宏声明,例如:
1 | class User : public QObject |
派生类及其基类的附加信息都可以通过派生类的元对象访问。元对象的 classInfoOffset()
方法返回派生类第一项附加信息的索引;classInfoCount()
方法返回派生类的附加信息数;classInfo()
方法根据附加信息的索引返回一个用于表示一项附加信息的 QMetaClassInfo
对象,其 name()
和 value()
方法分别返回附加信息的键名和键值。
1 |
|
对象树
QObject
类及其派生类的对象可以组成一棵树。每个 QObject
对象可以有一个父对象和多个子对象。父对象可以用 setParent()
方法设置,用 parent()
方法获取。例如,创建三个 QObject
对象,将其中一个对象设置为另外两个对象的父对象:
1 | QObject* parent = new QObject; |
为 QObject
对象设置父对象后,该对象就自动成为父对象的子对象,因此没有专门用于添加子对象的方法。子对象的列表可以用 children()
方法获取。例如:
1 | QList<QObject*> children = parent->children(); |
QObject
对象的析构函数会将它从父对象的子对象列表中移除,从而解除它和父对象之间的父子关系。
为了区分不同的 QObject
对象,可以利用它们的 objectName
属性。QObject
对象的 objectName
属性是它的名称,可以用 setObjectName()
方法设置,用 objectName()
方法获取,默认值为空字符串。例如,为上述三个 QObject
对象设置不同的名称:
1 | parent->setObjectName("Parent"); |
QObject
对象的 dumpObjectTree()
方法可以将它和它的子对象形成的树状结构用文字直观地表示出来并输出到调试输出窗口中,方便调试。例如:
1 | parent->dumpObjectTree(); |
析构顺序
父对象的析构函数会调用子对象的析构函数。如果树中的对象是局部变量,为了避免子对象的析构函数被调用两次,要确保子对象的析构函数先于父对象的析构函数被调用,从而解除它们之间的父子关系。换言之,子对象要在父对象之后定义,因为 C++ 标准规定,局部对象的析构函数的调用顺序与它们的构造函数的相反。例如:
1 | // correct |
事件系统
事件系统是元对象系统提供的另一种对象间通信机制。事件用抽象类 QEvent
的派生类表示。事件发生时,特定 QObject
对象可以接收到一个事件对象。QObject
对象中负责接收事件对象的方法是虚函数 event()
。它负责检查事件的类型,并将事件对象传递给事件处理器。事件类型可以通过事件对象的 type()
方法查询。事件类型用 QEvent::Type
的枚举成员表示。常见的事件类型有:
- 键盘事件
QEvent::KeyPress
按键按下QEvent::KeyRelease
按键松开
- 鼠标事件
QEvent::MouseMove
鼠标移动QEvent::MouseButtonPress
鼠标按下QEvent::MouseButtonRelease
鼠标松开QEvent::MouseButtonDblClick
鼠标双击QEvent::Enter
鼠标进入QEvent::Leave
鼠标离开
接收和处理事件
QObject
类的派生类可以通过重写虚函数 event()
来自定义事件对象的处理过程,以便使用自定义的事件类型和事件处理器。例如,在 event()
方法中将代表按键按下事件的事件对象传递给自定义的事件处理器 keyPressEvent()
:
1 | class MyObject : public QObject |
若当前对象中有合适的事件处理器可以接收和处理事件对象,则 event()
方法返回 true
,否则返回 false
。派生类的 event()
方法应该调用基类的 event()
方法来处理它无法处理的事件。在 QWidget
类及其派生类的对象的 event()
方法中返回 false
会导致事件对象被发送给当前对象的父对象。
创建和发送事件
事件对象可以用 QApplication::sendEvent()
方法立即发送给指定 QObject
对象。例如,将一个代表回车键按下事件的事件对象 event
发送给对象 myObject
:
1 | MyObject myObject; |
事件过滤器
可以在一个 QObject
对象上安装一个或多个事件过滤器,用来过滤要发送给这个 QObject
对象的事件对象。事件过滤器也是 QObject
类的派生类,它的 eventFilter()
方法可以在它所在 QObject
对象之前接收到事件对象并决定它所在 QObject
对象能否接收到事件对象。如果 eventFilter()
方法返回 true
,事件对象就会被丢弃。例如,定义一个将所有按键按下事件丢弃的事件过滤器:
1 | class KeyPressFilter : public QObject |
要给一个 QObject
对象安装事件过滤器,只需调用它的 installEventFilter()
方法,用事件过滤器作为参数:
1 | KeyPressFilter keyPressFilter; |
用户界面
QWidget
类是 QObject
的子类。QWidget
类及其派生类的对象表示一个组件,它是组成图形用户界面的基本元素。界面上的元素,小到按钮,大到窗口,都是由组件形成的。例如,要创建并显示一个窗口,只需创建一个 QWidget
对象并调用它的 show()
方法:
1 |
|
在调用它的 show()
方法将窗口显示出来之前,还可以调用它的 setWindowTitle()
方法设置窗口的标题,调用它的 resize()
方法设置窗口的大小。
为了便于叙述,下文将 QWidget
类及其派生类的对象统称为 QWidget
对象。
组件和窗口
窗口是特殊的组件,通常带有标题栏和边框。窗口可以分为主窗口(Primary window)和副窗口(Secondary window)两种。它们的区别在于有无对应的任务栏按钮(Task bar entry)。对话框就是最典型的副窗口。主窗口是由没有父组件的组件形成的;副窗口是由有父组件且带有 Qt::Window
标志的组件形成的。这两种组件称为窗口组件或顶级组件(Top-level widgets),其他组件都称为非窗口组件(Non-window widgets)。非窗口组件是镶嵌在父组件中的组件。
Type | Parent | Flag |
---|---|---|
Primary window | N | |
Secondary window | Y | Qt::Window |
Non-window widgets | Y |
QWidget
类的派生类 QMainWindow
和 QDialog
是上述两种窗口的封装。QMainWindow
对象表示一个带有标题栏、工具栏、中心组件和状态栏的主窗口。中心组件可以是任意非窗口组件。QDialog
对象表示一个带有一组按钮的对话框,属于副窗口。
组件位置和尺寸
组件由客户区(Client Area)和四周的非客户区两部分组成。非客户区也称窗口框架(Window frame),包括窗口的标题栏和边框两部分。具有非客户区的窗口组件也称有装饰(Decorated)的顶级组件。非窗口组件默认不具非客户区。每个组件都有两个指示其位置和尺寸的属性:
geometry
属性是一个QRect
对象,指示客户区的尺寸(宽和高)及其相对父组件(或桌面)的位置(左上角的坐标),可以用resize()
方法设置,用下列方法读取:geometry()
返回一个QRect
对象,指示客户区的尺寸和位置。size()
返回一个QSize
对象,指示客户区的尺寸,相当于geometry().size()
。width()
返回客户区的宽度,相当于geometry().width()
。height()
返回客户区的高度,相当于geometry().height()
。rect()
返回一个QSize
对象,指示客户区的尺寸,相当于QRect(0, 0, width(), height())
。
frameGeometry
属性是一个QRect
对象,指示非客户区的尺寸及其相对父窗口(或桌面)的位置,可以用move()
方法设置,用下列方法读取:frameGeometry()
返回一个QRect
对象,指示非客户区的尺寸和位置。frameSize()
返回一个QSize
对象,指示非客户区的尺寸,相当于frameGeometry().size()
。pos()
返回非客户区左上角的坐标,相当于frameGeometry().topLeft()
。x()
返回非客户区的横坐标,相当于frameGeometry().x()
。y()
返回非客户区的纵坐标,相当于frameGeometry().y()
。
对于非窗口组件或无装饰的窗口组件,这两个属性的值相同。对于有装饰的窗口组件,窗口的标题和图标可以用 setWindowTitle()
和 setWindowIcon()
方法分别设置。
让一个组件成为非窗口组件的方法是调用它的 setParent()
方法,为它设置一个父对象。例如,创建一个按钮组件,并将它设置为窗口的子组件:
1 | QPushButton *button = new QPushButton("Press me"); |
坐标原点为客户区左上角,x 和 y 轴正方向分别为向右和向下方向。
组件布局
布局容器能够为其中的每个组件设置合适的位置和尺寸,使这些组件按一定规则排列。虽然一个组件只能安装一个布局容器,但是布局容器可以嵌套,从而组成各种复杂布局。
下面的程序首先创建两个组件,然后创建一个水平布局容器,接着将两个组件添加到水平布局容器中,最后将水平布局容器设置为主窗口的布局容器。
1 | QLabel* label = new QLabel("Password"); |
在上面的例子中, 非窗口组件 label
和 lineEdit
会成为窗口组件 window
的子组件,而不是布局容器 layout
的子组件。换言之,添加到布局容器中的组件会成为布局容器所在组件的子组件,而不是布局容器本身的子组件。
常用的布局容器有水平布局容器、垂直布局容器、网格布局容器和表单布局容器四种:
QHBoxLayout
使子组件排成一行QVBoxLayout
使子组件排成一列QGridLayout
将子组件放置在网格中QFormLayout
使子组件两两一行排列
将组件添加到网格布局容器时,需要以行和列为单位设置组件的位置和尺寸。QGridLayout
对象的 addWidget()
方法有五个参数,原型如下:
1 | QGridLayout::addWidget(widget, fromRow, fromColumn, rowSpan, columnSpan); |
它表示将组件 widget
放置到第 fromRow
行、第 fromColumn
列,跨越 rowSpan
行、rowSpan
列。
1 | QGridLayout* layout = new QGridLayout(); |
要向表单布局容器添加组件,应该使用它的 addRow()
方法,一次添加两个组件。
1 | QLabel* label1 = new QLabel("Email"); |
尺寸信息
组件的实际尺寸受下列四个属性的影响:
sizeHint
默认尺寸minimumSizeHint
最小尺寸minimumSize
最小尺寸maximumSize
最大尺寸
只读属性 sizeHint
和 minimumSizeHint
都只是建议性尺寸。组件的实际尺寸既可以大于 sizeHint
,也可以小于 minimumSizeHint
,但不能小于 minimumSize
,也不能大于 maximumSize
。
组件的默认尺寸可以用 sizeHint()
方法查询,例如:
1 | QPushButton* button = new QPushButton("Press me"); |
要规定自定义组件的默认尺寸,只需重写 sizeHint()
方法。
尺寸调整策略
组件的 sizePolicy
属性是一个 QSizePolicy
对象,它描述了组件的尺寸调整策略,可以用 sizePolicy()
方法获取,用 setSizePolicy()
方法设置。
1 | QPushButton* button = new QPushButton("Press me"); |
一个 QSizePolicy
对象包含两个 QSizePolicy::Policy
类型的枚举成员和两个伸缩因子。两个枚举成员分别规定组件在水平和垂直两个方向是否可以被拉伸或压缩。QSizePolicy::Policy
类型有下列成员:
枚举成员 | 含义 |
---|---|
QSizePolicy::Fixed |
组件的尺寸固定为默认尺寸,不可伸缩。 |
QSizePolicy::Minimum |
默认尺寸是最小尺寸。组件只能被拉伸。 |
QSizePolicy::Maximum |
默认尺寸是最大尺寸。组件只能被压缩。 |
QSizePolicy::Preferred |
默认尺寸是最佳尺寸。组件既能被拉伸,也能被压缩。 |
QSizePolicy::Expanding |
默认尺寸是合理尺寸。组件既能被拉伸,也能被压缩,并且将优先占据更多空间。 |
QSizePolicy::MinimumExpanding |
默认尺寸是最小尺寸。组件只能被拉伸,并且将优先占据更多空间。 |
QSizePolicy::Ignored |
默认尺寸不重要。组件需要占据尽可能多的空间。 |
例如,准备一个可以垂直拉伸,但不可以水平伸缩的按钮:
1 | QPushButton* button1 = new QPushButton("Button 1"); |
伸缩因子的取值范围是 0 - 255。各组件的伸缩因子之比就是它们的伸缩量之比。例如,将两个按钮的水平伸缩因子分别设置为 1 和 2:
1 | QPushButton* button1 = new QPushButton("Button 1"); |
绘图系统
QWidget
、QPixmap
和 QImage
都是 QPaintDevice
的派生类。QPaintDevice
对象表示一个绘图设备。用绘图工具可以在绘图设备的客户区中绘制图形和文字。绘图工具用 QPainter
对象表示。
当一个组件的部分或全部客户区需要被绘制或重新绘制时,组件会收到 QPaintEvent
事件对象,此时组件的事件处理器 paintEvent()
需要完成必要的绘图工作。比如在 (100, 50) 处绘制一个 200×100 的矩形:
1 | void paintEvent(QPaintEvent* e) |
坐标原点为客户区左上角,x 和 y 轴正方向分别为向右和向下方向。
图形的轮廓和填充图案分别由绘图工具使用的画笔和画刷决定。在开始绘图前,应该设置画笔和画刷。
1 | QPen pen; |
绘制图形
线段可以用 drawLine()
方法绘制。
1 | painter.drawLine(QLine(10, 80, 90, 20)); |
默认情况下,QPainter
绘制的图形的边缘存在锯齿。要消除图形边缘的锯齿,只需调用 setRenderHints()
方法,添加抗锯齿标志。
1 | painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); |
四边形和多段线分别可以用 drawPolygon()
和 drawPolyline()
方法绘制。
1 | const QPointF points[4] = { |
弧弧、弦和扇形分别可以用 drawArc()
、drawChord()
和 drawPie()
方法绘制。例如,绘制以矩形中心为圆点,从 0° 开始,转过 270° 的弧、弦和扇形:
1 | QRect rect = QRect(50, 50, 100, 100); |
矩形、圆角矩形和椭圆分别可以用 drawRect()
、drawRoundedRect()
和 drawEllipse()
方法绘制。
1 | QRect rect = QRect(50, 50, 100, 100); |
填充矩形可以用 fillRect()
方法绘制。图片可以用 drawImage()
或 drawPixmap()
方法绘制。
1 | painter.fillRect(rect, Qt::blue); |
设置字体
在绘制文本之前,需要设置一种字体。首先要通过 QFontDatabase
对象确定可用字体的字型、风格和字号,然后创建一个 QFont
对象传递给 QPainter
对象。
QFontDatabase
对象的 families()
方法可以列出系统中所有适用于某种书写系统的字型:
1 | QFontDatabase fontDatabase; |
某个字型支持的所有风格可以用 styles()
方法列出。某个字型的某种风格支持的所有字号可以用 pointSizes()
方法列出。
1 | qDebug() << fontDatabase.styles("Microsoft YaHei UI"); |
可以用静态方法 addApplicationFont()
从字体文件加载更多的字体。它返回一个整数作为字体 ID。若字体加载失败,则方法返回 -1
。已加载的字体需要用 removeApplicationFont()
方法移除。
1 | int fontId = QFontDatabase::addApplicationFont("./simsun.ttc"); |
静态方法 applicationFontFamilies
方法可以列出从字体文件加载的字体中包含的所有字型:
1 | qDebug() << QFontDatabase::applicationFontFamilies(fontId); |
QFont
对象可以直接用 font()
方法创建:
1 | QFont font = fontDatabase.font("Microsoft YaHei UI", "Light", 14); |
绘制文本
字体的尺寸信息可以通过 QFontMetrics
对象获取。例如:
1 | QFontMetrics fontMetrics(font); |
其中,ascent
是字符升部的高度;descent
是字符降部的高度;height
是字符的高度,等于 ascent
与 descent
之和;lineSpacing
是行高,等于 leading
与 height
之和。
用 drawText()
方法绘制文本时,需要指定基线左端的坐标。文本的宽度可以用 boundingRect()
方法获取。
1 | QString text = QString::fromLocal8Bit("你好,世界!"); |
绘制路径
路径用一个 QPainterPath
对象表示,它可以保存一组图形、记录一组操作,常用于创建需要重复使用的复杂图形。例如,要反复绘制一个包含对角线的矩形,就可以借助路径来简化操作:
1 | QPainterPath path; |
坐标变换
在用绘图工具绘制图形和文字时,图形和文字先后被映射到三个不同的坐标平面:首先,绘图工具将图形和文字绘制到「逻辑坐标平面」上;然后,绘图工具用一个坐标变换矩阵将图形和文字的逻辑坐标转换成窗口坐标,从而将图形和文字映射到「窗口坐标平面」上;最后,绘图工具再通过窗口-视口机制将图形和文字映射到「物理坐标平面」上。物理坐标平面就是绘图设备的客户区所在平面,其原点固定在客户区左上角。默认情况下,这三个坐标平面重合。
视口是物理坐标平面中的一块矩形区域;窗口是窗口坐标平面中的一块要和视口对应起来的矩形区域。简单来说,绘制在窗口中的图形将出现在视口中。例如,将物理坐标平面中左上角为原点的 200×200 矩形区域和窗口坐标平面中左上角为原点的 100×100 矩形区域对应起来:
1 | painter.setViewport(0, 0, 200, 200); |
通过修改坐标变换矩阵,可以实现逻辑坐标平面的平移、旋转、缩放和剪切。例如,在上例的基础上,将逻辑坐标系的原点由窗口的左上角平移到中心,再将逻辑坐标平面顺时针旋转 90°:
1 | painter.translate(50, 50); |
此时在原点处绘制一个 100×100 矩形和一段文本就可以得到上图的效果:
1 | painter.fillRect(QRect(-50, -50, 100, 100), Qt::gray); |
坐标变换矩阵也可以直接用 setWorldTransform()
方法设置,用 worldTransform()
方法读取,用 resetTransform()
方法重置。因此调用 resetTransform()
方法就可以让逻辑坐标平面和窗口坐标平面恢复到重合状态。
图形视图框架
图形视图框架提供绘制可交互图形和文字的能力。
QGraphicsItem
类的派生类的对象表示一个图形或一段文本,比如:
QGraphicsLineItem
对象表示一段线;QGraphicsRectItem
对象表示一个矩形;QGraphicsEllipseItem
对象表示一个椭圆;QGraphicsPolygonItem
对象表示一个多边形;QGraphicsPixmapItem
对象表示一幅位图;QGraphicsTextItem
对象表示一段文本。
为了便于叙述,下文将这些派生类的对象统称为图元。
QGraphicsScene
对象表示一个场景。场景是图元的容器,相当于一块画布。每个场景对应一个坐标平面,称为「场景坐标平面」。场景的位置和尺寸用一个矩形表示,称为「场景矩形」。例如,创建一个场景矩形为 (0, 0, 160×100)
的场景:
1 | QGraphicsScene* scene = new QGraphicsScene(QRect(0, 0, 160, 100)); |
场景中的图元可以落在场景矩形之外。添加图元时,要用一个矩形指示图元的位置和最大尺寸,称为「边界矩形」。例如,添加一个边界矩形为 (0, 0, 100×50)
的矩形到场景中:
1 | rect = new QGraphicsRectItem(QRect(0, 0, 100, 50)); |
图元的轮廓和填充图案由它们使用的画笔和画刷决定。
添加到场景中的图元都是从各自的「图元坐标平面」映射到场景坐标平面的。一个图元坐标平面相当于 Photoshop 中的一个图层。图元坐标系默认与场景坐标系重合,可以用 setPos()
方法让图元坐标系的原点与场景坐标平面上的另一点重合。例如,绘制一个边界矩形为 (0, 0, 50×50)
的圆,再将圆坐标系的原点平移到 (-25, 25)
点,这样圆心就会与场景坐标系的原点重合:
1 | ellipse = new QGraphicsEllipseItem(QRect(0, 0, 50, 50)); |
通过平移、旋转和缩放图元坐标平面,就可以独立地控制每个图元的位置、朝向和尺寸。例如,让上例中的矩形图元坐标平面以 (0, 50)
为基点顺时针旋转 90°:
1 | rect->setTransformOriginPoint(QPoint(0, 50)); |
QGraphicsView
对象表示一个视图。视图是一种能够按需提供滚动条的组件,用于可视化场景。例如,创建一个 300×220
的视图,用它显示上述场景:
1 | view = new QGraphicsView(); |
默认情况下,当场景的尺寸小于视图时,即滚动条未出现时,视图会根据场景矩形自动居中显示场景。
场景在视图中的对齐方式是由视图的 alignment
属性决定的,可以用 setAlignment()
方法改变。例如,让场景停靠在视图左上角:
1 | view->setAlignment(Qt::AlignLeft | Qt::AlignTop); |
视图的客户区对应的坐标平面称为「视图坐标平面」,其原点固定在客户区左上角。视图用一个坐标变换矩阵将场景坐标平面映射到视图坐标平面,因此它可以显示经过平移、旋转和缩放的场景。例如,将上述场景坐标平面顺时针旋转 90°,基点是场景坐标平面上与视图中心重合的那一点:
1 | view->rotate(90); |
场景缩放和旋转的基点由 transformationAnchor
属性决定的,可以用 setTransformationAnchor()
方法改变,有三种基点可以选择:
QGraphicsView::NoAnchor
场景坐标系的原点;QGraphicsView::AnchorViewCenter
与视图中心重合的点(场景绕视图中心旋转);QGraphicsView::AnchorUnderMouse
当鼠标在视图中时,以鼠标的位置为基点(场景绕鼠标旋转);否则以视图中心为基点。
视图控制
默认情况下,图元既不能获得焦点,也不能被选中,更不能被拖动。要调整图元的行为,只需调用它的 setFlags()
方法给它设置不同的标志。例如,给图元设置 QGraphicsItem::ItemIsMovable
标志,让它可以被拖动:
1 | rect->setFlags(QGraphicsItem::ItemIsMovable); |
其他标志的含义如下:
QGraphicsItem::ItemIsSelectable
让图元可以被选中;QGraphicsItem::ItemIsFocusable
让图元可以获得焦点;QGraphicsItem::ItemClipsToShape
让图元以它的外轮廓为边界矩形。图元不能接收到发生在边界矩形之外的键盘事件和鼠标事件。
视图的拖拽模式决定用户在场景的空白处按下并拖拽鼠标时会发生什么。视图的拖拽模式可以用 setDragMode()
方法设置:当拖拽模式为 QGraphicsView::ScrollHandDrag
时,用户可以用鼠标拖动场景;当拖拽模式为 QGraphicsView::RubberBandDrag
时,用户可以用鼠标框选场景中的图元。
1 | view->setDragMode(QGraphicsView::ScrollHandDrag); |
缩放和旋转场景需要调用视图的 scale()
和 rotate()
方法。例如,在自定义的视图类中,可以定义下列负责缩放和旋转场景的槽:
1 | class MyGraphicsView : public QGraphicsView |
坐标转换
在自定义视图的鼠标事件处理器中,鼠标单击位置是用视图坐标表示的。要将视图坐标转换成场景坐标,可以用 mapToScene()
;反之用 mapFromScene()
方法。例如:
1 | class MyGraphicsView : public QGraphicsView |
在得到场景坐标后,就可以调用场景的 itemAt()
方法获取位于该点且处于最顶层的图元。图元的 mapFromScene()
方法可以把场景坐标进一步转换成图元坐标。例如:
1 | QGraphicsItem* item = this->scene()->itemAt(scenePos, this->transform()); |
在上面的例子中,单击场景坐标平面中的点 (50, 50)
,相当于单击矩形所在图元坐标平面的原点。
图元的边界矩形可以用它的 boundingRect()
或 sceneBoundingRect()
方法获取,前者返回的矩形是用图元坐标表示的;后者返回的矩形是用场景坐标表示的。
1 | QRectF boundingRect = item->boundingRect(); |
组件事件
滚轮事件
滚轮事件在用户滚动鼠标滚轮时发生。滚轮事件对象的 angleDelta()
方法可以返回滚轮转过的角度,以八分之一度为单位。角度的符号指示滚轮滚动的方向:正数表示滚轮朝着远离用户的方向向前滚动,通常代表向上翻(或向左翻)或放大操作;负数表示滚轮朝着靠近用户的方向向后滚动,通常代表向下翻(或向右翻)或缩小操作。根据用户在滚动鼠标滚轮时是否有按下 Shift 键,可以把用户滚动鼠标滚轮的目的分为横向滚动和纵向滚动。这两种情况下滚轮转过的角度分别由 angleDelta().y()
和 angleDelta().x()
指示。用户滚动鼠标滚轮时,鼠标指针的位置可以用 position()
方法获取。
1 | void wheelEvent(QWheelEvent* event) |
拖放机制
拖放即拖拽(Drag)和放置(Drop),是一种可视化的组件间和应用间数据传输机制。拖放操作的过程是用户按下鼠标左键并移动鼠标一段距离后再松开鼠标左键的过程。可见,拖放操作包含拖拽和放置两个动作,其结果是源组件向目标组件传输了一份数据。源组件和目标组件可以是同一个。如果拖放操作的放置动作只是移动数据而不是复制数据,源组件还要负责删除原始数据。
开始拖拽
拖放操作通常要在用户按下鼠标左键并移动鼠标一段距离后才能触发。为此,在 mousePressEvent()
方法中记录鼠标的位置,在 mouseMoveEvent()
方法中检查鼠标移动过的距离并决定是否要开始一次拖放操作。
1 | QPoint dragStartPos; |
开始一次拖放操作的方法是在 mouseMoveEvent()
方法中创建一个 QDrag
对象并调用它的 exec()
方法。需要传输的数据要用一个 QMimeData
对象描述和保存。
1 | void mouseMoveEvent(QMouseEvent* event) |
QDrag
和 QMimeData
对象均无需手动销毁。
QDrag
对象的 setPixmap()
方法用于设置在拖放过程中跟随鼠标移动的位图;setHotSpot()
方法用于设置鼠标相对位图左上角的位置;setMimeData()
方法用于设置需要传输的数据。
1 | QDrag* drag = new QDrag(this); |
QMimeData
对象使用媒体类型(MIME type)来描述数据的性质和格式,既可以携带常规的文本和图像数据,也可以携带二进制数据,因此,QString
对象、QPoint
对象和 QPixmap
对象等数据结构在用 QDataStream
对象序列化成二进制数据后都可以通过拖放机制进行传输。
1 | QPixmap pixmap = label->pixmap(Qt::ReturnByValue); |
不同的放置动作(Drop actions)用 Qt::DropAction
类型的枚举成员表示:
Qt::CopyAction
复制数据(按住 Ctrl)Qt::MoveAction
移动数据(按住 Shift)Qt::LinkAction
创建快捷方式Qt::IgnoreAction
取消操作(按下 ESC)
QDrag
对象 exec()
方法会阻塞当前事件处理器但不影响事件循环,它的第一个参数用于列出可供选择的放置动作,第二个参数用于指示默认放置动作,返回值指示实际选择的放置动作。
1 | Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction, |
鼠标形状
鼠标划过组件时的形状由组件的 cursor
属性决定,可以用 setCursor()
方法设置,用 cursor()
方法读取。鼠标形状用枚举成员表示。常用的鼠标形状有:
Value | Shape | Description |
---|---|---|
Qt::ArrowCursor |
标准箭头 | |
Qt::CrossCursor |
十字准星 | |
Qt::WaitCursor |
需等待 | |
Qt::IBeamCursor |
可输入横排文本 | |
Qt::SizeVerCursor |
可拉动上、下边框 | |
Qt::SizeHorCursor |
可拉动左、右边框 | |
Qt::SizeBDiagCursor |
可拉动左下角、右上角 | |
Qt::SizeFDiagCursor |
可拉动左上角、右下角 | |
Qt::SizeAllCursor |
可移动 | |
Qt::BlankCursor |
透明 | |
Qt::PointingHandCursor |
手指 | |
Qt::ForbiddenCursor |
禁止 | |
Qt::OpenHandCursor |
可抓取 | |
Qt::ClosedHandCursor |
已抓取 | |
Qt::BusyCursor |
忙 |
对话框
标准对话框
文件对话框
QString QFileDialog::getOpenFileName()
打开一个文件QStringList QFileDialog::getOpenFileNames()
打开多个文件QString QFileDialog::getSaveFileName()
保存一个文件QString QFileDialog::getExistingDirectory()
打开现有目录
QColor QColorDialog::getColor()
选择颜色QFont QFontDialog::getFont()
选择字体输入对话框
QString QInputDialog::getText()
输入单行文本QString QInputDialog::getMultiLineText()
输入多行文本int QInputDialog::getInt()
输入整数double QInputDialog::getDouble()
输入浮点数QString QInputDialog::getItem()
用下拉列表做出选择
消息框
StandardButton QMessageBox::information()
提示StandardButton QMessageBox::critical()
错误StandardButton QMessageBox::question()
询问
打开文件时,可以设置过滤器。只有匹配过滤器的文件才会显示出来。不同的过滤器用两个分号 ;;
隔开。
1 | QString title = "Open File"; |
输入文本时,可以设置默认值,还可以设置单行文本输入框的响应模式。输入密码时,可以将响应模式设置为 QLineEdit::Password
。
1 | QString title = "Input"; |
输入数字时,可以设置默认值、最小值、最大值和步进量。
1 | QString title = "Input"; |
下拉列表的选项用 QStringList
对象指示。
1 | QString title = "Input"; |
提示、警告消息框通常只有一个确定按钮。
1 | QString title = "Information"; |
消息框的按钮用枚举类型 StandardButton
表示。
1 | QString title = "Question"; |
自定义对话框
创建对话框的头文件、源文件和 UI 资源文件(File - New File… - Qt - Qt Designer Form Class)。
模态框
用 exec()
方法创建。
1 | MyDialog *myDialog = new MyDialog(this); |
非模态框
用 show()
方法创建。
1 | MyDialog *myDialog = new MyDialog(this); |
事件
要自定义非模态框关闭时的动作,可以重写对话框的 close
事件处理程序。
1 | void MyDialog::closeEvent(QCloseEvent *event) |
信号和槽
对话框之间的通信也可以借助信号和槽进行:在一个对话框中定义信号,在另一个对话框定义信号的槽。例如,父窗口中有一个按钮,用于创建一个子对话框,创建子对话框时要禁用该按钮,关闭子对话框时再启用该按钮。
dialog.h
1 | private slots: |
dialog.cpp
1 | void Dialog::on_pushButton_clicked() |
mydialog.h
1 | protected: |
mydialog.cpp
1 | void MyDialog::closeEvent(QCloseEvent *event) |
视图和模型
视图是一类组件,模型是数据的抽象,同一个模型可以用不同的视图呈现。例如,QFileSystemModel
是文件系统的模型,可以用树视图 QTreeView
和列表视图 QListView
两种组件呈现:
在左边的 QTreeView
中点击一个目录时,右边的 QListView
将列出该目录下的所有文件和目录。
文件系统模型创建后,要用 setRootPath()
方法设置模型关联的目录。视图关联的模型都可以用 setModel()
方法设置。
1 | MyWidget::MyWidget(QWidget *parent) |
访问模型中的条目(Item)要通过 QModelIndex
对象,它是条目的索引。在 QTreeView
中点击一个目录时,它的 clicked(QModelIndex)
槽将得到被点击目录的索引,此时调用 QListView
的 setRootIndex()
方法,将其设置为列表的根目录,就可以让 QListView
列出该目录下的所有文件和目录。
1 | void MyWidget::on_treeView_clicked(const QModelIndex &index) |
QStringListModel
QStringListModel
是字符串列表的模型,它的 setStringList()
和 stringList()
方法分别用于设置和获取模型关联的字符串列表:
1 | MyWidget::MyWidget(QWidget *parent) |
条目的索引由行号、列号和父级索引组成,它的 row()
和 column()
方法分别用于获取行号、列号。模型的条目总数可以用 rowCount()
方法获取。模型可以通过 model()
方法获取。
1 | void MyWidget::on_listView_clicked(const QModelIndex &index) |
模型可以分成列表模型、列表模型和表格模型。对于列表模型来说,所有条目的列号都是 0;对于列表模型和表格模型来说,所有条目都有相同的父级索引。
条目包含的数据都用 QVariant
对象表示,可以用 setData()
和 Data()
方法设置和获取。条目的索引都可以用模型的 index()
方法获取。例如,获取最后一个条目的索引,再将它的数据设置为 Xiamen
:
1 | QStringListModel *model = index->model(); |
一个条目可以包含多项数据,不同的数据有不同的用途。设置多项数据时,要给不同的数据分配不同的角色。数据的默认角色是 Qt::EditRole
或 Qt::DisplayRole
。
1 | model->setData(lastIndex, |
QStandardItemModel
QStandardItemModel
可以作为表格视图 QTableView
的模型。模型的每一个条目对应一个单元格。
在创建模型时可以指定行数、列数。表头可以用 setHorizontalHeaderLabels()
方法设置。在指定位置上添加条目可以用 setItem()
方法。条目要用 QStandardItem
对象表示,条目的文本可以用 setText()
和 text()
方法设置和获取。条目对应的单元格可以包含一个复选框,复选框的状态可以用 setCheckState()
方法设置。
1 | QStandardItemModel *theModel; |
条目的文本实际上就是角色为 Qt::DisplayRole
或 Qt::EditRole
的数据,因此下面两条语句等价:
1 | username->setText(user[0]); |
QItemSelectionModel
QItemSelectionModel
是选择模型,用于跟踪在视图中被选中的条目。创建选择模型时要用数据模型作为参数。
1 | QStandardItemModel *theModel; |
用户在视图中点击不同的条目时,选择模型会发送 on_currentChanged()
信号,两个参数分别当前被点击的条目和上一次被点击的条目的索引。
1 | void MyWidget::on_currentChanged(const QModelIndex ¤t, |
所有选中条目的索引可以用选择模型的 selectedIndexes()
方法获取。
1 | if (!theSelectionModel->hasSelection()) { |
自定义代理类
代理类让用户可以在视图中编辑条目。
用户在视图中双击某个条目时,需要创建合适的控件用于编辑条目,此时会调用代理类的下列三个方法:
createEditor()
创建合适的控件作为编辑器updateEditorGeometry()
将编辑器的尺寸调整为合适的大小setEditorData()
用条目的数据设置编辑器的内容
用户结束编辑时,需要将编辑器的内容同步到模型中,此时会调用代理类的 setModelData()
方法。
默认的代理类使用单行文本框作为编辑器。如果要使用其他控件,就需要自定义代理类。自定义代理类通常继承 QStyledItemDelegate
类:
.h
1 |
|
.cpp
1 |
|
要让视图使用自定义的代理类,可以用 setItemDelegateForColumn()
方法设置:
1 | ui->tableView->setItemDelegateForColumn(2, new MyDelegate); |
多线程
创建、启动和结束线程
线程要用线程类表示。线程类要继承 QThread
类并重写 void run()
方法。线程要完成的任务就在 run()
方法里实现。run()
方法返回前需要调用 exit()
方法。调用 exit()
方法时,可以提供一个退出码。
1 | class MyTimer: public QThread |
sleep()
、msleep()
和 usleep()
都是 QThread
类的静态方法,它们可以让当前线程休眠一段时间。
启动线程的方法是调用 start()
方法。start()
方法会调用 run()
方法。run()
方法返回时,线程结束。父线程可以调用 isRunning()
或 isFinished()
方法检查子线程的状态,调用 terminate()
方法强制结束子线程。terminate()
方法会立即打断 run()
方法。无论子线程是因为 run()
方法返回而结束,还是因为父线程调用了 terminate()
方法而结束,父线程都要在希望子线程结束的时候调用 wait()
方法等待子线程结束。
1 | MyTimer *myTimer; |
线程启动时会发送 started()
信号;结束时会发送 finished()
信号。
线程同步
互斥锁 QMutex
当两个线程在读写同一个变量时,相互打断有可能导致错误的结果。
1 | class MyElapsedTimer: public QThread |
当父线程调用 elapsed()
方法读取变量时,子线程的 run()
方法有可能正在更新这些变量,此时父线程就有可能得到 hour
为 24
、minute
为 60
或 second
为 60
的错误结果。
1 | myElapsedTimer->start(); |
如果两段代码不能相互打断,就需要用一个 QMutex
对象来管理它们:在它们的开头调用 lock()
或 tryLock()
方法;在它们的结尾调用 unlock()
方法。
1 | bool MyElapsedTimer::elapsed(int &hour, int &minute, int &second) |
QMutexLocker
QMutex
对象的 lock()
和 unlock()
方法必须配对使用。QMutexLocker
类用于简化 QMutex
类的用法,它的构造函数会调用给定 QMutex
对象的 lock()
方法,析构函数会调用 unlock()
方法。
1 | bool MyElapsedTimer::elapsed(int &hour, int &minute, int &second) |
读写锁 QReadWriteLock
如果两个线程都只是单纯地读取变量而不更改变量,就无需顾虑它们相互打断的情况。要区分这种情况,可以用 QReadWriteLock
类代替 QMutex
类。
1 | bool MyElapsedTimer::elapsed(int &hour, int &minute, int &second) |
QReadLocker & QWriteLocker
就像 QMutex
类的用法可以用 QMutexLocker
类简化一样,QReadWriteLock
类的用法可以用 QReadLocker
和 QWriteLocker
类简化。
1 | bool MyElapsedTimer::elapsed(int &hour, int &minute, int &second) |
QWaitCondition
QWaitCondition
类用于保证不同线程负责的任务的执行顺序:调用 wait()
方法的线程会被阻塞,直到另一个线程调用 wakeAll()
或 wakeOne()
方法。为了确保 wait()
方法在 wakeAll()
方法之前被调用,QWaitCondition
类必须结合 QMutex
类使用。在调用 wait()
方法时,需要提供一个已经上锁的 QMutex
对象。wait()
方法被调用时会给它解锁,在返回时再给它重新上锁。
1 | QMutex mutex; |
信号量 QSemaphore
QSemaphore
类的作用与 QMutex
类相同。但是 QMuter
类只能管理一个资源,而 QSemaphore
类可以管理一定数量的资源。资源总数在创建 QSemaphore
对象时就需要确定。资源可以用 acquire()
和 release()
方法批量占用和释放。
1 | char buffer[5]; |
如果没有足够数量的资源可以占用,调用 acquire()
方法的线程就会进入阻塞状态,直到有足够数量的资源被释放出来。