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
2
https://mirrors.tuna.tsinghua.edu.cn/qt/online/qtsdkrepository/windows_x86/root/qt/
https://mirrors.ustc.edu.cn/qtproject/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 的安装目录中可以找到)的路径。

image-20220727094501150

在 Ubuntu 上使用

在 Ubuntu 上可以通过 apt 命令快速安装 Qt 5.15.3。

1
2
3
sudo apt install -y libxcb-xinerama0
sudo apt install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools
sudo apt install -y qtcreator

快速开始

在开始菜单中,可以找到 Qt 提供的工具集:

  • Qt Creator 集成开发环境
  • Assistant 用于查看 文档,集成在 Qt Creator 中
  • Designer 用于编辑 UI 资源文件,集成在 Qt Creator 中
  • Linguist 用于编辑语言资源文件

熟悉 Qt Creator

Qt Creator 的界面很简洁。顶部是主菜单栏。左侧是包含两组按钮的工具栏:第一组按钮用于切换到不同的界面(Edit 用于代码编辑,Design 用于 UI 设计,Projects 用于项目设置,Help 用于文档查阅);第二组按钮是构建、调试和运行按钮。右侧是工作区,工作区显示的内容取决于选择的界面。

image-20220728110221098

切换到 Edit 界面时,工作区的主要内容是一个代码编辑器。点击底部状态栏的第一个按钮可以显示或隐藏工作区的左侧边栏。左侧边栏默认包含 Projects 面板和 Open Documents 面板。Projects 面板用于显示项目的目录结构,Open Documents 面板用于列出已打开的文件。每个面板的标题都是一个下拉列表,用于切换成其他面板。

image-20220728115210397

新建项目

Qt Creator 支持创建 3 种不同的项目(File - New Project…):

image-20220728104604561

  • Qt Widgets Application 桌面应用程序(窗口程序)
  • Qt Console Application 控制台应用程序
  • Qt Quick Application 用于移动设备和嵌入式设备,使用 QML 语言进行 UI 设计的应用程序

一个桌面应用程序至少包含一个窗口类。新建桌面应用程序的步骤如下:

第一步是设置项目的名称并选择项目的保存位置。Qt Creator 会在项目的保存位置创建一个与项目同名的文件夹作为项目的根目录。比如在 D:\Code\qt_demo 创建文件夹 hello_world

image-20220728105035748

第二步是选择项目的构建工具:

image-20220728102510594

最重要的一步是选择窗口类的基类,并设置派生类的类名、派生类的头文件和源文件以及 UI 资源文件(Form file)的文件名。

image-20220802145630450

窗口类的基类决定窗口的类型,有三种基类可以选择:

  • QMainWindow 用于创建包含主菜单栏、工具栏和状态栏的窗口
  • QDialog 用于创建对话框
  • QWidget 用于创建包含各种控件的窗口

最后一步是根据需要勾选平台工具集(如 MSVC 和 MinGW)和构建配置(如 Debug、Release)。

image-20220728104825785

项目最开始通常包含 5 个文件:包含主函数的源文件 main.cpp,窗口类的头文件 mainwindow.h 和源文件 mainwindow.cpp,UI 资源文件 mainwindow.ui 以及 CMake 的配置文件 CMakeLists.txt

1
2
3
4
5
6
7
8
9
hello_world $ tree .
.
|-- CMakeLists.txt
|-- main.cpp
|-- mainwindow.cpp
|-- mainwindow.h
`-- mainwindow.ui

0 directories, 5 files

可视化 UI 设计

打开 UI 资源文件时会自动切换到 UI 设计界面。

image-20220916111609503

UI 设计界面的左侧是控件的列表。要将控件添加到窗口中,直接将其拖动到中间的窗口中即可。例如,添加一个 Push Button 和一个 Label,并设置它们的 text 属性。

image-20220916113841110

添加到窗口中的控件会在右上角列出。在右上角选中一个控件后,左下角会列出该控件的属性。添加到窗口中的每个控件都有不同的 objectName 属性,它们是控件的变量名。为了便于使用控件,最好自定义控件的变量名,即自定义控件的 objectName 属性,比如将 Close 按钮的 objectName 属性设置为 pBtnClose

设置窗口大小和标题

窗口大小由基类 QWidgetgeometry 属性决定;窗口的标题由基类 QWidgetwindowTitle 属性决定。

image-20220916113415503

关联信号和槽

窗口底部是信号和槽的列表。信号和槽都是类的成员方法,用于实现控件间通信。信号表示一个事件,比如 clicked() 表示控件被单击的事件;槽表示可以响应信号的动作,比如 close() 是关闭窗口的动作。

要在点击 Close 按钮后关闭窗口,可以在信号和槽的列表添加如下条目,将按钮控件的 clicked() 信号和窗口的 close() 槽关联起来:

image-20220916114213728

构建并运行

在完成 UI 设计后,点击左下角绿色的运行按钮即可启用程序。点击 Close 按钮可以关闭窗口。

image-20220916114046483

可视化 UI 设计的工作原理

在构建目录的 hello_world_autogen/include 子文件夹中可以找到头文件 ui_mainwindow.h。它是由 UIC 根据 UI 资源文件生成的。头文件的文件名比 UI 资源文件多了前缀 ui_,例如 mainwindow.ui 会被编译成 ui_mainwindow.h。头文件中的代码首先被包含在两个宏 QT_BEGIN_NAMESPACEQT_END_NAMESPACE 之间。这两个宏将 UIC 生成的代码置于 Qt 专用的命名空间。

ui_mainwindow.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
QT_BEGIN_NAMESPACE

class Ui_MainWindow
{
public:
void setupUi(QMainWindow *MainWindow)
{
// ...
}
};

namespace Ui {
class MainWindow: public Ui_MainWindow {};
} // namespace Ui

QT_END_NAMESPACE

头文件中的代码主要由两个类定义组成:一个名为 Ui_MainWindow,它有一个名为 setupUi() 的方法,包含 UI 初始化的代码;另一个名为 Ui::MainWindow,它只是简单地继承前者,不包含实质性的代码。要使用可视化 UI 设计,就需要在窗口类的构造函数中调用 setupUi() 方法。

窗口类的头文件只需声明类 Ui::MainWindow,不必包含 UIC 生成的头文件 ui_mainwindow.h。这是因为窗口类头文件只需声明一个 Ui::MainWindow 类的指针。

mainwindow.h

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

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
Q_OBJECT

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

private:
Ui::MainWindow *ui;
};

mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "mainwindow.h"
#include "ui_mainwindow.h"

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

MainWindow::~MainWindow()
{
delete ui;
}

非可视化 UI 设计

进行一次非可视化 UI 设计可以帮助理解可视化 UI 设计的工作原理。不过,在实际开发中都会采用可视化 UI 设计,因为它不仅能够让 UI 设计工作变得直观,而且能够大大减少编码的工作量。

创建一个新的项目,在选择窗口的基类时,不要勾选 Generate form。这样 Qt Creator 就不会生成 .ui 文件。

image-20220916155630888

不采用可视化 UI 设计时,控件和窗口的初始化都要在窗口类中完成。在窗口类的头文件中需要声明各个控件的指针和负责 UI 初始化的 setupUi() 方法。要使用信号和槽的窗口类还需要包含 Q_OBJECT 宏。

mydialog.h

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

class MyDialog: public QDialog
{
Q_OBJECT

public:
MyDialog(QWidget *parent = nullptr);

private:
QLabel *labelMsg;
QPushButton *btnClose;

void setupUi();
};

窗口类的构造函数首先要调用父类的构造函数完成窗口的初始化,然后调用 setupUi() 完成 UI 的初始化,最后调用静态方法 QObject::connect() 完成信号和槽的关联。

mydialog.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
#include "mydialog.h"
#include <QVBoxLayout>

MyDialog::MyDialog(QWidget *parent)
: QDialog(parent)
{
this->setupUi();
QObject::connect(btnClose, SIGNAL(clicked()), this, SLOT(close()));
}

void MyDialog::setupUi()
{
btnClose = new QPushButton(QObject::tr("Close"));
labelMsg = new QLabel(QObject::tr("Hello, World!"));

QFont font = labelMsg->font();
font.setPointSize(24);
labelMsg->setFont(font);

QVBoxLayout *VBox = new QVBoxLayout();
VBox->addWidget(labelMsg);
VBox->addWidget(btnClose);
this->setLayout(VBox);
}

setupUi() 方法中,首先创建各个控件的对象,根据需要设置控件的属性,然后创建一个布局容器,将各个控件加入其中,最后,用 setLayout() 方法将布局容器设置为窗口的布局容器。

关联信号和槽

自动关联

在 UI 设计界面右击控件,点击 Go to slot…,可以快速添加一个新的方法作为控件的信号的槽。Designer 会在窗口类的头文件中新增方法的声明,在源文件中新增方法的定义。

mywidget.h

1
2
private slots:
void on_checkBox_clicked(bool checked);

mywidget.cpp

1
2
3
4
void MyWidget::on_checkBox_clicked(bool checked)
{
// ...
}

UIC 生成的 setupUi() 方法调用了静态方法 QMetaObject::connectSlotsByName()。这个方法可以根据方法名自动关联对象中的信号和槽。比如名为 on_checkBox_clicked 方法就是控件 checkBoxclicked() 信号的槽。如果没有调用 connectSlotsByName() 方法,就需要调用静态方法 QObject::connect() 一一关联每一对信号和槽。

手动关联

信号和槽通常在构造函数中用 QObject::connect() 方法关联。该方法的第一、三个参数分别是信号的发送方和接收方,第二、四个参数分别是发送方的信号和接收方的方法。信号和作为槽的方法分别要用 SIGNAL()SLOT() 宏处理。

1
2
3
4
5
6
MyDialog::MyDialog(QWidget *parent)
: QDialog(parent)
{
setupUi();
QObject::connect(myButton, SIGNAL(clicked()), this, SLOT(close()));
}

一个信号不仅可以关联到一个槽,还可以关联到另一个信号。

1
connect(myButton, SIGNAL(clicked()), this, SIGNAL(buttonClicked()));

一个信号可以关联多个槽和信号。多个信号关联可以同一个槽。重复关联信号和槽会导致槽被调用多次。

基础专题

常用控件

多行文本框

多行文本框用 QPlainTextEdit 对象表示。要向文本框追加文本,可以调用它的 appendPlainText() 方法:

1
ui->plainTextEdit->appendPlainText("Hello, World!");

控件的字体

调用控件的 font() 方法可以获得一个字体对象,它描述控件文本的字体、字号和字体风格。

1
2
3
4
5
6
7
QFont font = ui->plainTextEdit->font();

font.setFamily("Noto Sans"); // 思源黑体
font.setBold(true); // 加粗
font.setPointSize(14); // 字号

ui->plainTextEdit->setFont(font);

控件的颜色

调用控件的 palette() 方法可以获得一个调色板对象,它描述控件要使用的各种前颜色和背景色。

1
2
3
4
5
6
7
8
QPalette palette = ui->plainTextEdit->palette();

QColor color;
color.setRgb(0, 0, 255);
palette.setColor(QPalette::Base, color); // 控件背景色
palette.setColor(QPalette::Text, Qt::red); // 字体颜色,即前景色

ui->plainTextEdit->setPalette(palette);

控件的状态

控件的状态由 enabled 属性决定,可用 setEnabled() 方法设置。

image-20220919134331554

单行文本框

单行文本框由 QLineEdit 对象表示。当 echoMode 属性为 Password 时,输入框的内容会显示为 * 星号。

image-20220916152352895

1
2
QString text(); // 获取文本
void setText(const QString &text); // 设置文本

旋钮

QSpinBox 用于整数的输入,基数由 displayIntegerBase 属性决定,默认使用十进制;QDoubleSpinBox 用于小数的输入,显示多少位小数由 decimals 属性决定,默认显示两位。两者都可以给数字设置前缀、后缀、最小值、最大值和增量。

image-20220916151237996

输入框的内容可以用 text() 方法获取,用 setText() 方法设置。实际表示的数字要用 value() 方法获取,用 setValue() 方法设置。

1
2
3
ui->doubleSpinBox->setValue(1.5);
qDebug() << ui->doubleSpinBox->text(); // "$1.50"
qDebug() << ui->doubleSpinBox->value(); // 1.5

滑动条

QSlider 用于整数的输入。可以给数字设置最小值、最大值和增量。关闭 tracking 属性时,控件仅在松开滑块时才发送 valueChanged() 信号。

image-20220916170703668

时间日期

Qt 为时间日期的表示和显示提供了 3 种数据类型和 4 个控件。

Type Widget
QTime QTimeEdit
QDate QDateEdit
QDateTime QDateTimeEdit
- QCalendarWidget

QTimeEditQDateEdit 都是 QDateTimeEdit 的派生类。QDateTimeEdit 的常用属性如下:

  • dateTimeQDateTime 类型,指示时间日期。
  • dateQDate 类型,指示日期。
  • timeQTime 类型,指示时间。
  • currentSection,枚举类型 QDateTimeEdit::Section,指示用户正在编辑的字段,比如 YearSection 表示用户正在编辑年份。
  • calendarPopup,布尔型,指示是否提供日历选择框,区别见下图。
  • displayFormat,字符串型,指示时间日期的格式,如 yyyy-MM-dd HH:mm::ss 表示 2022-09-19 10:30:00

不提供日历选择框:

image-20220919095233699

提供日历选择框:

image-20220919095406145

时间日期类

要获取当前时间日期,可以用静态方法 QDateTime::currentDateTime(),它返回一个 QDateTime 对象。调用 QDateTime 对象的 toString() 方法还可以获得时间日期的字符串表示。

1
2
3
QDateTime dateTime = QDateTime::currentDateTime();
QString string = dateTime.toString("yyyy-MM-dd HH:mm:ss");
qDebug() << string;

当前时间或日期也可以单独获取,或者通过 QDateTime 对象获取:

1
2
3
4
5
QDate date = QTime::currentTime();
QTime time = QData::currentDate();

QDate date = dateTime.date();
QTime time = dateTime.time();

QDateQTime 对象也都提供 toString() 方法。toString() 方法的参数可以包含下列转义序列:

    • yy 用两位数表示年份, 如 22
    • yyyy 用四位数表示年份,如 2022
    • M 1-12
    • MM 01-12
    • d 1-31
    • dd 01-31
    • H 0-23 或 1-12
    • HH 00-23 或 01-12
    • m 0-59
    • mm 00-59
    • s 0-59
    • ss 00-59
  • 毫秒
    • z 0-999
    • zzz 000-999
  • APA 是否采用 12 小时制并在结尾追加 AMPM 的本地译名
  • apa 是否采用 12 小时制并在结尾追加 ampm 的本地译名

要从字符串解析时间日期,可以用静态方法 QDateTime::fromString()。字符串中包含的时间日期的格式要用第二个参数指示。

1
2
QDateTime dateTime = QDateTime::fromString("2022919", "yyyyMdd");
qDebug() << dateTime.toString("yy-MM-dd"); // "22-09-19"

计时器和定时器

计时器是 QElapsedTimer 对象,它的 start() 方法用于开始计时,elapsed() 方法返回经过的毫秒数。

1
2
3
4
QElapsedTimer counter;
counter.start();
// ...
int ms = counter.elapsed();

定时器是 QTimer 对象,它的 interval 属性指示定时周期,单位毫秒,每经过一个周期就会发送一次 timeout() 信号。调用定时器的 start()stop() 方法可以启动和关闭定时器。

1
2
3
4
5
QTimer *timer;
timer = new QTimer;
timer->setInterval(3000);
timer->start();
QObject::connect(timer, SIGNAL(timeout()), this, SLOT(on_timeout()));

下拉列表

下拉列表是 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
2
3
4
5
6
ui->comboBox->addItem("Beijing", 10);
ui->comboBox->addItem("Shanghai", 21);
ui->comboBox->addItem("Guangzhou", 20);
ui->comboBox->addItem("Shenzhen", 755);

qDebug() << ui->comboBox->itemData(3).toInt(); // 755

列表

列表是 QListWidget 对象。列表的每一项是一个 QListWidgetItem 对象,移除时,要释放它的内存空间。

1
2
3
4
5
6
QListWidgetItem *newItem = new QListWidgetItem("Beijing");
ui->listWidget->addItem(newItem);

int row = ui->listWidget->currentRow();
QListWidgetItem *removedItem = ui->listWidget->takeItem(row);
delete removedItem;

常用方法如下:

  • 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
2
3
4
#include <QDebug>

qDebug("Hello");
qDebug() << "Hello";

同类的宏还有 qInfo()qWarning()qCritical()qFatal()。只有 qFatal() 会终止程序。

所有日志最终都由一个日志处理程序处理。默认的日志处理程序不会添加文件名、行号和函数名等信息作为前缀。要自定义日志的格式,可以用全局函数 qInstallMessageHandler() 设置自定义的日志处理程序。

1
2
3
4
5
6
7
8
9
10
void myMessageHandler(QtMsgType type, const QMessageLogContext &ctx, const QString &msg)
{
// ...
}

int main(int argc, char *argv[])
{
qInstallMessageHandler(myMessageHandler);
// ...
}

参数 type 是枚举类型,指示日志的类型:

1
enum QtMsgType { QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg, QtInfoMsg };

参数 ctx 指示日志的上下文,包含日志的文件名、行号和函数名,仅在调试模式有效。

1
2
3
const char *file = ctx.file ? ctx.file : "unknown";
const int line = ctx.line;
const char *function = ctx.function ? ctx.function : "unknown";

容器类

QList 是最常用的容器。在列表两端添加新元素可以用 append()prepend() 方法。

1
2
3
4
5
6
7
#include <QList>

QList<QString> list = { "one", "two", "three" };
list.prepend("zero");
list.append("four");
qDebug() << list;
// ("zero", "one", "two", "three", "four")

在末端追加新元素还可以用 << 运算符。

1
2
3
list << "five" << "six";
qDebug() << list;
// ("zero", "one", "two", "three", "four", "five", "six")

QList 可以像数组那样使用。

1
2
3
4
5
6
qDebug() << list[0];
qDebug() << list[1];
qDebug() << list.at(2);
"zero"
"one"
"two"

Java 风格的迭代器

用 Java 风格的迭代器遍历列表时,主要依靠 hasNext()next() 两个方法。hasNext() 方法用于检查下一个位置是否存在元素;next() 方法用于将迭代器移到下一个位置并返回该位置上的元素。

1
2
3
4
5
6
QList<QString> list = { "one", "two", "three" };

QListIterator<QString> listIterator(list);
while (listIterator.hasNext()) {
qDebug() << listIterator.next();
}
  • toFront() 将迭代器移到第一个元素之前
  • toBack() 将迭代器移到最后一个元素之后
  • hasPrevious() 检查上一个位置是否存在元素
  • previous() 将迭代器移到上一个位置并返回该位置上的元素

QListIterator 是只读迭代器。要改写元素,需要用 QMutableListIterator

1
2
3
4
5
6
7
8
9
10
QList<int> list = { 1, 2, 3, 4 };

QMutableListIterator<int> mutableListIterator(list);
while (mutableListIterator.hasNext()) {
int n = mutableListIterator.next();
if (n % 2 == 0) {
mutableListIterator.remove();
}
}
qDebug() << list; // (1, 3)

可写迭代器比只读迭代器多出以下三个方法:

  • remove() 移除当前位置的元素。
  • setValue() 修改当前位置的元素
  • insert() 在当前位置插入新元素

STL 风格的迭代器

用 STL 风格的迭代器遍历列表时,主要依靠 ++* 两个运算符。++ 运算符将迭代器移到下一个位置;* 运算符返回当前位置上的元素。

1
2
3
4
5
6
7
8
9
QList<QString> list = { "one", "two", "three" };

QList<QString>::iterator p;
for (p = list.begin(); p != list.end(); p++) {
qDebug() << *p;
}
// "one"
// "two"
// "three"

顺序容器

顺序容器,也就是列表,有以下 5 种:

  • QList 基于堆内存的列表
  • QLinkedList 基于链表的列表
  • QVector 基于数组的列表
  • QStack
  • QQueue 队列

前 3 种列表的内部实现不同,但它们提供的 API 基本相同。

映射

映射是特殊的列表,它们的元素是一个个键值对。

  • QMap 映射
  • QHash 基于散列表的映射

QMap 会根据键名顺序存储每个键值,而 QHash 则不会。

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

QMap<char, int> map;
map['a'] = 1;
map['b'] = 2;
map['c'] = 3;
qDebug() << map;
// QMap((a, 1)(b, 2)(c, 3))

QHash<char, int> hash;
hash['a'] = 1;
hash['b'] = 2;
hash['c'] = 3;
qDebug() << hash;
// QHash((c, 3)(b, 2)(a, 1))

在遍历映射时,迭代器的 key() 方法返回键名,value() 方法返回键值。

1
2
3
4
5
6
7
QMap<char, int>::iterator p;
for (p = map.begin(); p != map.end(); p++) {
qDebug() << p.key() << p.value();
}
// a 1
// b 2
// c 3

集合

QSet 是基于 QHash 的集合。集合是特殊的列表,它的元素不能重复。

1
2
3
4
5
#include <QSet>

QSet<char> set = { 'a', 'b', 'b', 'c', 'd' };
qDebug() << set;
// QSet(c, b, a, d)

工具类

QString

QString 类用于处理字符串。

1
2
3
4
5
QString s = "Hello, World!";
qDebug() << s.isEmpty(); // false
qDebug() << s.toUpper(); // "HELLO, WORLD!"
qDebug() << s.toLower(); // "hello, world!"
qDebug() << s.size(); // 13

count()length()size() 都返回字符个数。

append()prepend() 用于在首尾追加字符串。

1
2
3
4
QString s = "bbb";
s.prepend("aa");
s.append("cc");
qDebug() << s; // "aabbbcc"

检查首尾:

1
2
3
QString s = "ui_mainwindow.h";
qDebug() << s.startsWith("ui_", Qt::CaseInsensitive); // true
qDebug() << s.endsWith(".h", Qt::CaseInsensitive); // true

trimmed() 去掉首尾的空格。simplified() 不仅去掉首尾的空格,还会将中间的连续空格替换成一个。

1
2
3
QString s = "  Hello,  World!   ";
qDebug() << s.trimmed(); // "Hello, World!"
qDebug() << s.simplified(); // "Hello, World!"

在字符串中检索:

1
2
3
4
QString s = "Made in China";
qDebug() << s.indexOf("in"); // 5
qDebug() << s.lastIndexOf("in"); // 10
qDebug() << s.contains("china", Qt::CaseInsensitive); // true

截取:

1
2
3
QString s = "root:x:0:0:root:/root:/bin/bash";
qDebug() << s.left(s.indexOf(":")); // "root"
qDebug() << s.right(s.size() - s.lastIndexOf(":") - 1); // "/bin/bash"

在字符串和数字之间转换:

1
2
3
4
5
qDebug() << QString("12").toInt();     // 12
qDebug() << QString("10.5").toFloat(); // 10.5
qDebug() << QString::number(3); // "3"
qDebug() << QString::number(3, 2); // "11"
qDebug() << QString::number(2.5); // "2.5"
格式化字符串

格式化字符串可以用 arg() 方法,它将字符串中的 %n 替换为给定参数,其中 n 是从 1 开始的整数。

1
2
3
4
5
6
7
8
const int i = 1;                 // current file's number
const int total = 2; // number of files to process
const char fileName[] = "a.txt"; // current file's name

QString status = QString("Processing file %1 of %2: %3")
.arg(i).arg(total).arg(fileName);

qDebug() << status; // "Processing file 1 of 2: a.txt"
字符编码转换

QString 内部使用 UTF-16 编码。在创建 QString 对象时,如果提供的字符串使用本机编码,就需要用静态方法 QString::fromLocal8Bit() 来创建 QString 对象。

1
2
const char src[] = "好"; // 0xba 0xc3 0x00 (GBK)
QString str = QString::fromLocal8Bit(src); // 0x597d (UTF-16)

需要 QString 对象提供本机编码的字符串时,可以借助 toLocal8Bit() 方法,它用一个 QByteArray 对象保存转换后的字符串。

1
2
QByteArray bytes = str.toLocal8Bit(); // 0xba 0xc3 (GBK)
const char *dst = bytes.constData();

还可以用 toUtf8() 方法获取 UTF-8 编码的字符串:

1
bytes = str.toUtf8(); // 0xe5 0xa5 0xbd (UTF-8)

在 Windows 上,本机编码默认为 GBK 编码。如果不是,应该用静态方法 QTextCodec::setCodecForLocale() 设置正确的本机编码:

1
2
3
4
5
6
7
#include <QTextCodec>

QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));

const char src[] = u8"好"; // 0xe5 0xa5 0xbd (UTF-8)
QString str = QString::fromLocal8Bit(src); // 0x597d (UTF-16)
QByteArray bytes = str.toLocal8Bit(); // 0xe5 0xa5 0xbd (UTF-8)

QStringList

QStringList 是字符串列表。

1
2
3
QStringList stringList;
stringList << "Beijing" << "Shanghai" << "Guangzhou" << "Shenzhen";
qDebug() << stringList; // ("Beijing", "Shanghai", "Guangzhou", "Shenzhen")

QStringListQString 之间转换很容易:QStringsplit() 方法用分隔符将一个字符串分割成若干子串;QStringListjoin() 方法用分隔符将若干子串拼接成一个字符串。

1
2
3
4
5
QString s1 = "John,David,Mike";
qDebug() << s1.split(','); // ("John", "David", "Mike")

QStringList s2 = { "John", "David", "Mike" };
qDebug() << s2.join(';'); // "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
2
3
4
5
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return -1;
}
// ...
f.close();

有以下打开模式:

  • NotOpen,文件未打开
  • ReadOnly,只读
  • WriteOnly,只写,隐含 Truncate
  • ReadWrite,读写
  • Append,追加
  • Truncate,先清空再打开
  • Text,读文件时,统一将行结束符替换成 \n;写文件时,统一将行结束符替换成平台相关的行结束符。
  • Unbuffered,不要缓存
  • NewOnly,新建文件(若文件已存在,则打开失败)
  • ExistingOnly,打开已存在的文件(若文件不存在,则打开失败)

调用 readAll() 方法可以一次性将文件的内容全部读取出来。

1
2
3
4
QByteArray bytes = f.readAll();
if (bytes.isEmpty()) {
return -1;
}

也可以用 readLine() 方法按行读取文件的内容。

1
qint64 QIODevice::readLine(char *data, qint64 maxSize);

readLine() 方法每次最多从文件读取 maxSize-1 个字节并返回实际读取的字节数。如果遇到行结束符,或者到达文件末尾,则提前返回。注意,readLine() 方法最后还会向 data 写入一个字符串结束符 \0

1
2
3
4
5
6
7
8
// a.txt
// 123\r\n
// 456
char buf[10]; // 123\n\0
int lineLength = f.readLine(buf, sizeof(buf)); // 4
if (lineLength < 0) {
return -1;
}

readLine() 方法有一个返回 QByteArray 对象的重载,它返回的 QByteArray 对象剔除了 \0

1
QByteArray bytes = f.readLine(10); // 123\n

在读文件的过程中可以用 atEnd() 方法检查是否到达文件末尾。

1
2
3
4
while (!f.atEnd()) {
QByteArray line = f.readLine();
// ...
}

向文件写入数据可以用 write() 方法。

1
2
qint64 QIODevice::write(const char *data, qint64 maxSize)
qint64 QIODevice::write(const QByteArray &byteArray);

write() 方法的返回值都是实际写入的字节数。

1
2
3
4
5
QByteArray bytes = "12345";
int ret = f.write(bytes);
if (ret < 0) {
return -1;
}

文件信息类

要提取文件或目录的信息,可以借助 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 种数据类型:

  • 用双引号 "" 括起来的字符串
  • 整数或浮点数
  • 布尔值 truefalse
  • 空值 null
  • 数组
  • 对象

数组是元素的集合,元素要放在中括号 [] 中并用逗号 , 隔开,元素支持 6 种数据类型,例如:

1
2
3
4
"Phone numbers": [
"+44 1234567",
"+44 2345678"
]

对象是属性的集合,属性要放在大括号 {} 中并用逗号 , 隔开,属性是一个键值对,键名和键值之间用冒号 : 隔开,键名都是字符串,键值支持 6 种数据类型,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"FirstName": "John",
"LastName": "Doe",
"Age": 43,
"Address": {
"Street": "Downing Street 10",
"City": "London",
"Country": "Great Britain"
},
"Phone numbers": [
"+44 1234567",
"+44 2345678"
]
}

JSON 文档的内容就是一个字符串,它要么是一个对象,要么是一个数组。

在 Qt 中,JSON 文档用 QJsonDocument 对象表示;JSON 数组用 QJsonArray 对象表示;JSON 对象用 QJsonObject 对象表示。JSON 对象的属性和 JSON 数组的元素都用 QJsonValue 对象表示。使用这些类需要包含下列头文件:

1
2
3
4
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>

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
2
3
4
5
6
if (!doc.isNull()) {
if (doc.isObject()) {
QJsonObject obj = doc.object();
// ...
}
}

QJsonObject 对象的 keys() 方法返回一个字符串列表,其中包含各个属性的键名。

1
QStringList keyNames = obj.keys(); // ("age", "name")

访问某个属性或元素的值,首先要用 value() 方法或中括号运算符,获取代表该属性或元素的 QJsonValue 对象。

1
2
3
4
QJsonValue val = obj["name"];
if (val != QJsonValue::Undefined) {
// ...
}

有两种方式可以检查 QJsonValue 对象是否代表一个不存在属性或元素:

  • 将对象与 QJsonValue::Undefined 比较
  • 调用对象的 isUndefined() 方法

然后用 is*() 系列方法检查 QJsonValue 对象包含的数据的类型:

  • isString() 是否为字符串
  • isDouble() 是否为整数或浮点数
  • isBool() 是否为布尔值 truefalse
  • isNull() 是否为空值 null
  • isArray() 是否为数组
  • isObject() 是否为对象

最后用 to*() 系列方法获取 QJsonValue 对象包含的数据:

1
2
3
4
if (val.isString()) {
QString str = val.toString(); // "John"
// ...
}

动作

动作是菜单项、工具栏按钮或快捷键对应的命令,被调用时会发出 triggered() 信号。动作是独立对象,可以为之设置图标、文本、提示和快捷键等。一个动作可以同时被添加到菜单和工具栏中。

1
2
3
4
5
QAction *openAction = new QAction();
openAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_O));
this->addAction(openAction);

connect(openAction, SIGNAL(triggered(bool)), this, SLOT(on_actionOpen_triggered(bool)));

多语言国际化

在开发多语言应用时,显示在 UI 中的字符串要用静态方法 QObject::tr()QObject::trUtf8() 处理,它负责把字符串替换成目标语言的版本。

可执行文件的路径

QCoreApplication 类的静态方法可以获取可执行文件的路径。

1
2
3
QCoreApplication::applicationFilePath(); // 可执行文件的路径
QCoreApplication::applicationDirPath(); // 可执行文件所在目录的路径
QCoreApplication::applicationName(); // 可执行文件的文件名(不含扩展名)

信号源

在作为槽的方法中,可以用静态方法 QObject::sender() 获取发送信号的对象。它的返回值要用 qobject_cast<>() 做强制类型转换。

1
QSpinBox *pSpinBox = qobject_cast<QSpinBox *>( QObject::sender() );

进阶专题

结合 CMake

find_package() 命令就可以将 Qt 整合到项目中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)

find_package(Qt5 REQUIRED Widgets)

add_executable(helloworld
main.cpp
mydialog.h
mydialog.cpp
mydialog.ui
)
set_target_properties(helloworld PROPERTIES WIN32_EXECUTABLE ON)

target_link_libraries(helloworld Qt::Widgets)

前三个变量指示 CMake 自动寻找 Qt 的 MOC、RCC 和 UIC 并分别用它们处理 .cpp 文件、.qrc 文件和 .ui 文件。CMAKE_INCLUDE_CURRENT_DIR 变量指示 CMake 将源目录和构建目录都添加到附加包含目录,这样才能使用 UIC 生成的头文件。目标的 WIN32_EXECUTABLE 属性指示目标的产物是一个桌面应用程序而不是控制台程序。

在生成构建系统时需要设置 CMAKE_PREFIX_PATH 变量:

1
2
cmake -S . -B build -DCMAKE_PREFIX_PATH="D:/Qt/5.15.2/msvc2019_64"
cmake --build build --config Release -j

元对象系统

元对象系统(Meta-Object System)的主要作用是提供了一种对象间通信机制。要使用元对象系统的类必须满足下列三个条件:

  • 继承 QObject 类;
  • 在类的声明的 private 部分包含 Q_OBJECT 宏;
  • 用元对象编译器(MOC,Meta-Object Compiler)处理。

元对象编译器只是一个预处理器,并非真正的编译器。它会为每个声明中包含 Q_OBJECT 宏的类生成一个源文件。这些源文件包含实现元对象系统所需的代码,在构建时与类的源文件一起编译和链接。

QObject 类还实现了一个简易的定时器。

信号-槽机制

信号-槽机制是由元对象系统提供的一种对象间通信机制。信号是用来代表一个事件的方法;槽是用作事件处理器的方法。当事件发生时,只需发射信号,与之关联的槽就会被调用。

声明和发射信号

信号是特殊的方法,只需声明,无需定义。在类的声明中,信号的声明要放在 signals 部分。发射信号的方法与调用函数的方法相同,只是发射信号的语句前要包含 emit 关键字。例如,在 Sender 类的声明中添加信号 messageGenerated() 的声明,并 generateMessage() 方法中发射 messageGenerated() 信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Sender : public QObject
{
Q_OBJECT

public:
void generateMessage()
{
emit messageGenerated("Hello, World!");
}

signals:
void messageGenerated(QString message);
};
声明和定义槽

槽跟普通的方法一样,既要声明,也要定义,可以当作普通的方法直接调用。在类的声明中,槽的声明要放在 * slots 部分。

1
2
3
4
5
6
7
8
9
10
class Receiver : public QObject
{
Q_OBJECT

private slots:
void printMessage(QString message)
{
qDebug() << message;
}
};
关联信号和槽

信号发射后,只有与之关联的槽会被调用。一个信号可以关联多个槽;一个槽也可以被多个信号关联。关联信号和槽的方法是:调用 QObject::connect() 方法,用信号和槽所在对象的地址作为第一和第三个参数,用 SIGNAL()SLOT() 宏作为第二和第四个参数,SIGNAL()SLOT() 宏的参数分别是信号和槽的签名。例如,将 Sender 对象的 messageGenerated() 信号与 Receiver 对象的 printMessage() 槽关联:

1
2
3
4
5
6
7
8
9
10
int main()
{
Sender sender;
Receiver receiver;

QObject::connect(&sender, SIGNAL(messageGenerated(QString)),
&receiver, SLOT(printMessage(QString)));

sender.generateMessage(); // "Hello, World!"
}

属性系统

属性是逻辑上的公开成员变量。每个属性对应一个私有成员变量和两个专门用于读写该私有成员变量的公开成员函数。这两个公开成员函数分别称为属性的获取器(Getter)和设置器(Setter)。只有获取器的属性称为只读属性。每个属性还可以对应一个表示它的值发生变化的信号。属性的获取器和设置器以及它对应的信号的声明要仿照以下形式:

1
2
3
4
5
6
7
8
9
// Getter
void setProp(T value);

// Setter
T prop() const;

// Signal
void propChanged();
void propChanged(const T newValue);

属性的作用之一是方便组件样式的定义。

定义属性

例如,要定义属性 age,需要声明一个私有成员变量 age_ 和一个表示 age_ 的值发生变化的信号 ageChanged(),再声明两个专门用于读写 age_ 的公开成员函数 age()setAge(),让 setAge() 在适当的时机发射信号 ageChanged()

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
class User : public QObject
{
Q_OBJECT

public:
User() : age_(0)
{}

void setAge(int age)
{
age_ = age;
emit ageChanged(age_);
}

int age() const
{
return age_;
}

signals:
void ageChanged(const int newAge);

private:
int age_;
};

有了上述声明和定义,User 对象就好像拥有了一个名为 age 的公开成员变量,可以在其他函数中读写:

1
2
3
User user;
user.setAge(18);
qDebug() << user.age(); // 18
声明属性

属性系统提供了统一的读写属性的方法 setProperty()property()。使用两个方法的前提是在类的声明中用 Q_PROPERTY() 宏声明属性。Q_PROPERTY() 宏支持两种声明属性的方式:一种是指定属性的获取器(和设置器),而无需指定属性对应的私有成员变量;另一种是指定属性对应的私有成员变量,而无需定义属性的获取器和设置器。例如,通过指定获取器和设置器的方式声明属性 age

1
2
3
4
class User : public QObject
{
Q_OBJECT
Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)

int age 表示该属性是 int 型变量,属性名为 age。属性名无需和它对应的私有成员变量名保持一致。READ age 表示该属性的获取器为 age()WRITE setAge 表示该属性可写,设置器为 setAge()NOTIFY ageChanged 表示该属性发生变化时会发射的信号为 ageChanged()

声明过的属性可以用 setProperty()property() 两个方法读写:

1
2
user.setProperty("age", 19);
qDebug() << user.property("age").toInt(); // 19

property() 方法总是返回一个 QVariant 对象。它有一系列 to*() 方法用于将属性的值转换为需要的类型。

用第二种方式声明属性时,无需定义属性的获取器和设置器。例如,要定义一个名为 email 的属性并用第二种方式声明,只需修改类的头文件:

1
2
3
4
5
6
7
8
9
10
11
class User : public QObject
{
Q_OBJECT
Q_PROPERTY(QString email MEMBER email_ NOTIFY emailChanged)

signals:
void emailChanged(const QString &newEmail);

private:
QString email_;
};

MEMBER email_ 表示该属性对应的私有成员变量是 email_

没有获取器和设置器的属性同样可以用 setProperty()property() 两个方法读写:

1
2
user.setProperty("email", "your@example.com");
qDebug() << user.property("email").toString(); // "your@example.com"
动态属性

动态属性是在运行时才定义的属性。在用 setProperty() 方法设置属性时,如果要设置的属性不存在,它就会立即定义一个动态属性。例如,定义一个名为 name 的动态属性:

1
2
user.setProperty("name", "John");
qDebug() << user.property("name").toString(); // "John"

元对象

QObject 类及其派生类都对应一个 QMetaObject 对象。QObject 类及其派生类的元对象可以通过它们的 metaObject() 方法获取。元对象包含派生类的元数据,比如派生类包含的成员函数、构造函数和枚举成员的信息以及派生类的类名等。例如,派生类的类名可以通过调用元对象的 className() 方法获得:

1
2
const QMetaObject* userMetaObject = user.metaObject();
qDebug() << userMetaObject->className(); // User

每个 QObject 派生类对应一个元对象,不同的 QObject 派生类对应不同的元对象。同一个 QObject 派生类的不同对象的 metaObject() 方法返回的元对象都是同一个。

为了便于叙述,下文将 QObject 类及其派生类的对象都称为 QObject 对象。

属性的信息

在类的声明中用 Q_PROPERTY() 宏声明的各个属性的信息,可以通过元对象获取。元对象的 propertyCount() 方法返回属性的个数;property() 方法返回包含指定属性元数据的 QMetaProperty 对象。QMetaProperty 对象的 name()typeName() 方法分别返回属性的属性名和类型名。

1
2
3
4
5
6
7
8
9
#include <QMetaProperty>
const int count = userMetaObject->propertyCount();
for (int i = 0; i < count; i++) {
QMetaProperty metaProperty = userMetaObject->property(i);
qDebug() << metaProperty.name() << ":" << metaProperty.typeName();
}
// objectName: QString
// age : int
// email : QString

有了属性名,就可以访问属性的值。此时有两种方式可以访问属性的值:一种是像上文那样调用 QObject 对象的 property() 方法,用属性名作为参数;另一种是调用 QMetaProperty 对象的 read() 方法,用 QObject 对象的地址作为参数。

1
2
3
4
QMetaProperty metaProperty = userMetaObject->property(1);
user.setProperty("age", 20);
qDebug() << user.property(metaProperty.name()); // QVariant(int, 18)
qDebug() << metaProperty.read(&user); // QVariant(int, 18)
类的附加信息

类的附加信息是一组键名和键值都是字符串的键值对,要用 Q_CLASSINFO() 宏声明,例如:

1
2
3
4
5
class User : public QObject
{
Q_OBJECT
Q_CLASSINFO("Author", "John")
Q_CLASSINFO("Version", "1.0.0")

派生类及其基类的附加信息都可以通过派生类的元对象访问。元对象的 classInfoOffset() 方法返回派生类第一项附加信息的索引;classInfoCount() 方法返回派生类的附加信息数;classInfo() 方法根据附加信息的索引返回一个用于表示一项附加信息的 QMetaClassInfo 对象,其 name()value() 方法分别返回附加信息的键名和键值。

1
2
3
4
5
6
7
8
9
#include <QMetaClassInfo>
int offset = userMetaObject->classInfoOffset();
int count = userMetaObject->classInfoCount();
for (int i = 0; i < count; i++) {
QMetaClassInfo classInfo = userMetaObject->classInfo(i + offset);
qDebug() << classInfo.name() << ":" << classInfo.value();
}
// Author : John
// Version : 1.0.0

对象树

QObject 类及其派生类的对象可以组成一棵树。每个 QObject 对象可以有一个父对象和多个子对象。父对象可以用 setParent() 方法设置,用 parent() 方法获取。例如,创建三个 QObject 对象,将其中一个对象设置为另外两个对象的父对象:

1
2
3
4
5
6
QObject* parent = new QObject;
QObject* child1 = new QObject;
QObject* child2 = new QObject;

child1->setParent(parent);
child2->setParent(parent);

QObject 对象设置父对象后,该对象就自动成为父对象的子对象,因此没有专门用于添加子对象的方法。子对象的列表可以用 children() 方法获取。例如:

1
QList<QObject*> children = parent->children();

QObject 对象的析构函数会将它从父对象的子对象列表中移除,从而解除它和父对象之间的父子关系。

为了区分不同的 QObject 对象,可以利用它们的 objectName 属性。QObject 对象的 objectName 属性是它的名称,可以用 setObjectName() 方法设置,用 objectName() 方法获取,默认值为空字符串。例如,为上述三个 QObject 对象设置不同的名称:

1
2
3
parent->setObjectName("Parent");
child1->setObjectName("Child 1");
child2->setObjectName("Child 2");

QObject 对象的 dumpObjectTree() 方法可以将它和它的子对象形成的树状结构用文字直观地表示出来并输出到调试输出窗口中,方便调试。例如:

1
2
3
4
parent->dumpObjectTree();
// QObject::Parent
// QObject::Child 1
// QObject::Child 2
析构顺序

父对象的析构函数会调用子对象的析构函数。如果树中的对象是局部变量,为了避免子对象的析构函数被调用两次,要确保子对象的析构函数先于父对象的析构函数被调用,从而解除它们之间的父子关系。换言之,子对象要在父对象之后定义,因为 C++ 标准规定,局部对象的析构函数的调用顺序与它们的构造函数的相反。例如:

1
2
3
4
5
6
7
8
9
// correct
QObject parent;
QObject child;
child.setParent(&parent);

// incorrect
QObject child;
QObject parent;
child.setParent(&parent);

事件系统

事件系统是元对象系统提供的另一种对象间通信机制。事件用抽象类 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyObject : public QObject
{
Q_OBJECT

public:
bool event(QEvent* e) override
{
if (e->type() == QEvent::KeyPress) {
keyPressEvent(static_cast<QKeyEvent*>(e));
return true;
}
return QObject::event(e);
}

protected:
virtual void keyPressEvent(QKeyEvent* e)
{
qDebug() << "key pressed";
}
};

若当前对象中有合适的事件处理器可以接收和处理事件对象,则 event() 方法返回 true,否则返回 false。派生类的 event() 方法应该调用基类的 event() 方法来处理它无法处理的事件。在 QWidget 类及其派生类的对象的 event() 方法中返回 false 会导致事件对象被发送给当前对象的父对象。

创建和发送事件

事件对象可以用 QApplication::sendEvent() 方法立即发送给指定 QObject 对象。例如,将一个代表回车键按下事件的事件对象 event 发送给对象 myObject

1
2
3
4
MyObject myObject;

QKeyEvent event(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier);
QApplication::sendEvent(&myObject, &event); // key pressed
事件过滤器

可以在一个 QObject 对象上安装一个或多个事件过滤器,用来过滤要发送给这个 QObject 对象的事件对象。事件过滤器也是 QObject 类的派生类,它的 eventFilter() 方法可以在它所在 QObject 对象之前接收到事件对象并决定它所在 QObject 对象能否接收到事件对象。如果 eventFilter() 方法返回 true,事件对象就会被丢弃。例如,定义一个将所有按键按下事件丢弃的事件过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class KeyPressFilter : public QObject
{
Q_OBJECT

protected:
bool eventFilter(QObject* obj, QEvent* event) override
{
if (event->type() == QEvent::KeyPress) {
qDebug() << "key event filtered";
return true;
}
return QObject::eventFilter(obj, event);
}
};

要给一个 QObject 对象安装事件过滤器,只需调用它的 installEventFilter() 方法,用事件过滤器作为参数:

1
2
KeyPressFilter keyPressFilter;
myObject.installEventFilter(&keyPressFilter);

用户界面

QWidget 类是 QObject 的子类。QWidget 类及其派生类的对象表示一个组件,它是组成图形用户界面的基本元素。界面上的元素,小到按钮,大到窗口,都是由组件形成的。例如,要创建并显示一个窗口,只需创建一个 QWidget 对象并调用它的 show() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <QApplication>
#include <QWidget>

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

QWidget window;
window.setWindowTitle("My app");
window.resize(320, 240);
window.show();

return application.exec();
}

在调用它的 show() 方法将窗口显示出来之前,还可以调用它的 setWindowTitle() 方法设置窗口的标题,调用它的 resize() 方法设置窗口的大小。

image-20230221104534422

为了便于叙述,下文将 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 类的派生类 QMainWindowQDialog 是上述两种窗口的封装。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
2
3
4
QPushButton *button = new QPushButton("Press me");
button->setParent(&window);
button->move(100, 100);
button->show();

坐标原点为客户区左上角,x 和 y 轴正方向分别为向右和向下方向。

image-20230221105603029

组件布局

布局容器能够为其中的每个组件设置合适的位置和尺寸,使这些组件按一定规则排列。虽然一个组件只能安装一个布局容器,但是布局容器可以嵌套,从而组成各种复杂布局。

下面的程序首先创建两个组件,然后创建一个水平布局容器,接着将两个组件添加到水平布局容器中,最后将水平布局容器设置为主窗口的布局容器。

1
2
3
4
5
6
7
8
9
10
11
12
QLabel* label = new QLabel("Password");
QLineEdit* lineEdit = new QLineEdit();

QHBoxLayout* layout = new QHBoxLayout();
layout->addWidget(label);
layout->addWidget(lineEdit);

QWidget window;
window.setWindowTitle("My app");
window.setMinimumWidth(240);
window.setLayout(layout);
window.show();

image-20230214173845116

在上面的例子中, 非窗口组件 labellineEdit 会成为窗口组件 window 的子组件,而不是布局容器 layout 的子组件。换言之,添加到布局容器中的组件会成为布局容器所在组件的子组件,而不是布局容器本身的子组件。

常用的布局容器有水平布局容器、垂直布局容器、网格布局容器和表单布局容器四种:

  • QHBoxLayout 使子组件排成一行
  • QVBoxLayout 使子组件排成一列
  • QGridLayout 将子组件放置在网格中
  • QFormLayout 使子组件两两一行排列

将组件添加到网格布局容器时,需要以行和列为单位设置组件的位置和尺寸。QGridLayout 对象的 addWidget() 方法有五个参数,原型如下:

1
QGridLayout::addWidget(widget, fromRow, fromColumn, rowSpan, columnSpan);

它表示将组件 widget 放置到第 fromRow 行、第 fromColumn 列,跨越 rowSpan 行、rowSpan 列。

1
2
3
4
QGridLayout* layout = new QGridLayout();
layout->addWidget(button1, 0, 0, 1, 1);
layout->addWidget(button2, 1, 0, 1, 2);
layout->addWidget(button3, 2, 1, 1, 1);

image-20230223141429993

要向表单布局容器添加组件,应该使用它的 addRow() 方法,一次添加两个组件。

1
2
3
4
5
6
7
8
9
QLabel* label1 = new QLabel("Email");
QLabel* label2 = new QLabel("Password");

QLineEdit* lineEdit1 = new QLineEdit();
QLineEdit* lineEdit2 = new QLineEdit();

QFormLayout* layout = new QFormLayout();
layout->addRow(label1, lineEdit1);
layout->addRow(label2, lineEdit2);

image-20230223141514897

尺寸信息

组件的实际尺寸受下列四个属性的影响:

  • sizeHint 默认尺寸
  • minimumSizeHint 最小尺寸
  • minimumSize 最小尺寸
  • maximumSize 最大尺寸

只读属性 sizeHintminimumSizeHint 都只是建议性尺寸。组件的实际尺寸既可以大于 sizeHint,也可以小于 minimumSizeHint,但不能小于 minimumSize,也不能大于 maximumSize

组件的默认尺寸可以用 sizeHint() 方法查询,例如:

1
2
QPushButton* button = new QPushButton("Press me");
qDebug() << button->sizeHint(); // QSize(75, 23)

要规定自定义组件的默认尺寸,只需重写 sizeHint() 方法。

尺寸调整策略

组件的 sizePolicy 属性是一个 QSizePolicy 对象,它描述了组件的尺寸调整策略,可以用 sizePolicy() 方法获取,用 setSizePolicy() 方法设置。

1
2
3
4
QPushButton* button = new QPushButton("Press me");
QSizePolicy sizePolicy = button->sizePolicy();
// ...
button->setSizePolicy(sizePolicy);

一个 QSizePolicy 对象包含两个 QSizePolicy::Policy 类型的枚举成员和两个伸缩因子。两个枚举成员分别规定组件在水平和垂直两个方向是否可以被拉伸或压缩。QSizePolicy::Policy 类型有下列成员:

枚举成员 含义
QSizePolicy::Fixed 组件的尺寸固定为默认尺寸,不可伸缩。
QSizePolicy::Minimum 默认尺寸是最小尺寸。组件只能被拉伸。
QSizePolicy::Maximum 默认尺寸是最大尺寸。组件只能被压缩。
QSizePolicy::Preferred 默认尺寸是最佳尺寸。组件既能被拉伸,也能被压缩。
QSizePolicy::Expanding 默认尺寸是合理尺寸。组件既能被拉伸,也能被压缩,并且将优先占据更多空间。
QSizePolicy::MinimumExpanding 默认尺寸是最小尺寸。组件只能被拉伸,并且将优先占据更多空间。
QSizePolicy::Ignored 默认尺寸不重要。组件需要占据尽可能多的空间。

例如,准备一个可以垂直拉伸,但不可以水平伸缩的按钮:

1
2
3
4
5
6
7
QPushButton* button1 = new QPushButton("Button 1");
QSizePolicy sizePolicy1 = button1->sizePolicy();

sizePolicy1.setHorizontalPolicy(QSizePolicy::Fixed);
sizePolicy1.setVerticalPolicy(QSizePolicy::Minimum);

button1->setSizePolicy(sizePolicy1);

image-20230223141600883

伸缩因子的取值范围是 0 - 255。各组件的伸缩因子之比就是它们的伸缩量之比。例如,将两个按钮的水平伸缩因子分别设置为 1 和 2:

1
2
3
4
5
6
7
8
9
10
11
QPushButton* button1 = new QPushButton("Button 1");
QPushButton* button2 = new QPushButton("Button 2");

QSizePolicy sizePolicy1 = button1->sizePolicy();
QSizePolicy sizePolicy2 = button2->sizePolicy();

sizePolicy1.setHorizontalStretch(1);
sizePolicy2.setHorizontalStretch(2);

button1->setSizePolicy(sizePolicy1);
button2->setSizePolicy(sizePolicy2);

image-20230223141632103

绘图系统

QWidgetQPixmapQImage 都是 QPaintDevice 的派生类。QPaintDevice 对象表示一个绘图设备。用绘图工具可以在绘图设备的客户区中绘制图形和文字。绘图工具用 QPainter 对象表示。

当一个组件的部分或全部客户区需要被绘制或重新绘制时,组件会收到 QPaintEvent 事件对象,此时组件的事件处理器 paintEvent() 需要完成必要的绘图工作。比如在 (100, 50) 处绘制一个 200×100 的矩形:

1
2
3
4
5
void paintEvent(QPaintEvent* e)
{
QPainter painter(this);
painter.drawRect(QRect(100, 50, 200, 100));
}

坐标原点为客户区左上角,x 和 y 轴正方向分别为向右和向下方向。

image-20230223100346357

图形的轮廓和填充图案分别由绘图工具使用的画笔和画刷决定。在开始绘图前,应该设置画笔和画刷。

1
2
3
4
5
6
7
8
9
10
11
12
QPen pen;
pen.setColor(Qt::red); // 颜色
pen.setWidth(3); // 线型
pen.setStyle(Qt::DashLine); // 线宽
pen.setCapStyle(Qt::FlatCap); // 端点样式
pen.setJoinStyle(Qt::MiterJoin); // 连接点样式
painter.setPen(pen);

QBrush brush;
brush.setColor(Qt::blue); // 颜色
brush.setStyle(Qt::Dense1Pattern); // 样式
painter.setBrush(brush);
绘制图形

线段可以用 drawLine() 方法绘制。

1
painter.drawLine(QLine(10, 80, 90, 20));

image-20230223131435974

默认情况下,QPainter 绘制的图形的边缘存在锯齿。要消除图形边缘的锯齿,只需调用 setRenderHints() 方法,添加抗锯齿标志。

1
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

四边形和多段线分别可以用 drawPolygon()drawPolyline() 方法绘制。

1
2
3
4
5
6
7
8
const QPointF points[4] = {
QPointF(60.0, 130.0),
QPointF(70.0, 60.0),
QPointF(130.0, 90.0),
QPointF(140.0, 120.0)
};
painter.drawPolygon(points, 4);
painter.drawPolyline(points, 4);

image-20230224011813149

弧弧、弦和扇形分别可以用 drawArc()drawChord()drawPie() 方法绘制。例如,绘制以矩形中心为圆点,从 0° 开始,转过 270° 的弧、弦和扇形:

1
2
3
4
5
QRect rect = QRect(50, 50, 100, 100);
int startAngle = 0, spanAngle = 270;
painter.drawArc(rect, startAngle * 16, spanAngle * 16);
painter.drawChord(rect, startAngle * 16, spanAngle * 16);
painter.drawPie(rect, startAngle * 16, spanAngle * 16);

image-20230223131746388

矩形、圆角矩形和椭圆分别可以用 drawRect()drawRoundedRect()drawEllipse() 方法绘制。

1
2
3
4
QRect rect = QRect(50, 50, 100, 100);
painter.drawRect(rect);
painter.drawRoundedRect(rect, 15, 15);
painter.drawEllipse(rect);

image-20230223140827681

填充矩形可以用 fillRect() 方法绘制。图片可以用 drawImage()drawPixmap() 方法绘制。

1
2
3
4
painter.fillRect(rect, Qt::blue);

painter.drawImage(QPoint(50, 50), QImage(":/images/icon_Qt_78x78px.png"));
painter.drawPixmap(QPoint(50, 50), QPixmap(":/images/icon_Qt_78x78px.png"));

image-20230224012309532

设置字体

在绘制文本之前,需要设置一种字体。首先要通过 QFontDatabase 对象确定可用字体的字型、风格和字号,然后创建一个 QFont 对象传递给 QPainter 对象。

QFontDatabase 对象的 families() 方法可以列出系统中所有适用于某种书写系统的字型:

1
2
3
QFontDatabase fontDatabase;
qDebug() << fontDatabase.families(QFontDatabase::SimplifiedChinese);
// ("仿宋", "宋体", "新宋体", ..., "黑体")

某个字型支持的所有风格可以用 styles() 方法列出。某个字型的某种风格支持的所有字号可以用 pointSizes() 方法列出。

1
2
3
4
5
qDebug() << fontDatabase.styles("Microsoft YaHei UI");
// ("Regular", "Bold", "Light")

qDebug() << fontDatabase.pointSizes("Microsoft YaHei UI", "Light");
// (6, 7, 8, ..., 72)

可以用静态方法 addApplicationFont() 从字体文件加载更多的字体。它返回一个整数作为字体 ID。若字体加载失败,则方法返回 -1。已加载的字体需要用 removeApplicationFont() 方法移除。

1
2
3
4
5
6
7
int fontId = QFontDatabase::addApplicationFont("./simsun.ttc");
if (fontId == -1) {
qDebug() << "font could not be loaded";
return;
}
// ...
QFontDatabase::removeApplicationFont(fontId);

静态方法 applicationFontFamilies 方法可以列出从字体文件加载的字体中包含的所有字型:

1
2
qDebug() << QFontDatabase::applicationFontFamilies(fontId);
// ("SimSun", "NSimSun")

QFont 对象可以直接用 font() 方法创建:

1
2
QFont font = fontDatabase.font("Microsoft YaHei UI", "Light", 14);
painter.setFont(font);
绘制文本

字体的尺寸信息可以通过 QFontMetrics 对象获取。例如:

1
2
3
4
5
6
QFontMetrics fontMetrics(font);
int leading = fontMetrics.leading();
int ascent = fontMetrics.ascent();
int descent = fontMetrics.descent();
int height = fontMetrics.height();
int lineSpacing = fontMetrics.lineSpacing();

其中,ascent 是字符升部的高度;descent 是字符降部的高度;height 是字符的高度,等于 ascentdescent 之和;lineSpacing 是行高,等于 leadingheight 之和。

drawText() 方法绘制文本时,需要指定基线左端的坐标。文本的宽度可以用 boundingRect() 方法获取。

1
2
3
4
5
QString text = QString::fromLocal8Bit("你好,世界!");
painter.drawText(QPoint(50, 50 + leading + ascent), text);

int width = fontMetrics.boundingRect(text).width();
painter.drawRect(QRect(50, 50, width, height));

image-20230224012636783

绘制路径

路径用一个 QPainterPath 对象表示,它可以保存一组图形、记录一组操作,常用于创建需要重复使用的复杂图形。例如,要反复绘制一个包含对角线的矩形,就可以借助路径来简化操作:

1
2
3
4
5
6
7
QPainterPath path;
path.addRect(50, 50, 100, 100);
path.moveTo(50, 50);
path.lineTo(150, 150);
path.moveTo(150, 50);
path.lineTo(50, 150);
painter.drawPath(path);

image-20230224091138327

坐标变换

在用绘图工具绘制图形和文字时,图形和文字先后被映射到三个不同的坐标平面:首先,绘图工具将图形和文字绘制到「逻辑坐标平面」上;然后,绘图工具用一个坐标变换矩阵将图形和文字的逻辑坐标转换成窗口坐标,从而将图形和文字映射到「窗口坐标平面」上;最后,绘图工具再通过窗口-视口机制将图形和文字映射到「物理坐标平面」上。物理坐标平面就是绘图设备的客户区所在平面,其原点固定在客户区左上角。默认情况下,这三个坐标平面重合。

image-20230228013417275

视口是物理坐标平面中的一块矩形区域;窗口是窗口坐标平面中的一块要和视口对应起来的矩形区域。简单来说,绘制在窗口中的图形将出现在视口中。例如,将物理坐标平面中左上角为原点的 200×200 矩形区域和窗口坐标平面中左上角为原点的 100×100 矩形区域对应起来:

1
2
painter.setViewport(0, 0, 200, 200);
painter.setWindow(0, 0, 100, 100);

通过修改坐标变换矩阵,可以实现逻辑坐标平面的平移、旋转、缩放和剪切。例如,在上例的基础上,将逻辑坐标系的原点由窗口的左上角平移到中心,再将逻辑坐标平面顺时针旋转 90°:

1
2
painter.translate(50, 50);
painter.rotate(90);

此时在原点处绘制一个 100×100 矩形和一段文本就可以得到上图的效果:

1
2
painter.fillRect(QRect(-50, -50, 100, 100), Qt::gray);
painter.drawText(QPoint(0, 0), QString::fromLocal8Bit("ABC"));

坐标变换矩阵也可以直接用 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));

image-20230301093025322

场景中的图元可以落在场景矩形之外。添加图元时,要用一个矩形指示图元的位置和最大尺寸,称为「边界矩形」。例如,添加一个边界矩形为 (0, 0, 100×50) 的矩形到场景中:

1
2
3
4
rect = new QGraphicsRectItem(QRect(0, 0, 100, 50));
rect->setPen(pen);
rect->setBrush(brush);
scene->addItem(rect);

图元的轮廓和填充图案由它们使用的画笔和画刷决定。

image-20230301104728241

添加到场景中的图元都是从各自的「图元坐标平面」映射到场景坐标平面的。一个图元坐标平面相当于 Photoshop 中的一个图层。图元坐标系默认与场景坐标系重合,可以用 setPos() 方法让图元坐标系的原点与场景坐标平面上的另一点重合。例如,绘制一个边界矩形为 (0, 0, 50×50) 的圆,再将圆坐标系的原点平移到 (-25, 25) 点,这样圆心就会与场景坐标系的原点重合:

1
2
3
ellipse = new QGraphicsEllipseItem(QRect(0, 0, 50, 50));
ellipse->setPos(-25, -25);
scene->addItem(ellipse);

image-20230301161029547

通过平移、旋转和缩放图元坐标平面,就可以独立地控制每个图元的位置、朝向和尺寸。例如,让上例中的矩形图元坐标平面以 (0, 50) 为基点顺时针旋转 90°:

1
2
rect->setTransformOriginPoint(QPoint(0, 50));
rect->setRotation(90);

image-20230301160954557

QGraphicsView 对象表示一个视图。视图是一种能够按需提供滚动条的组件,用于可视化场景。例如,创建一个 300×220 的视图,用它显示上述场景:

1
2
3
4
5
view = new QGraphicsView();
view->setParent(this);
view->resize(300, 220);
view->setScene(scene);
view->show();

默认情况下,当场景的尺寸小于视图时,即滚动条未出现时,视图会根据场景矩形自动居中显示场景。

image-20230301160911064

场景在视图中的对齐方式是由视图的 alignment 属性决定的,可以用 setAlignment() 方法改变。例如,让场景停靠在视图左上角:

1
view->setAlignment(Qt::AlignLeft | Qt::AlignTop);

视图的客户区对应的坐标平面称为「视图坐标平面」,其原点固定在客户区左上角。视图用一个坐标变换矩阵将场景坐标平面映射到视图坐标平面,因此它可以显示经过平移、旋转和缩放的场景。例如,将上述场景坐标平面顺时针旋转 90°,基点是场景坐标平面上与视图中心重合的那一点:

1
view->rotate(90);

image-20230301162249727

场景缩放和旋转的基点由 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
2
3
4
5
6
7
8
9
10
class MyGraphicsView : public QGraphicsView
{
Q_OBJECT

public slots:
void zoomIn() { scale(1.2, 1.2); }
void zoomOut() { scale(1 / 1.2, 1 / 1.2); }
void rotateLeft() { rotate(-90); }
void rotateRight() { rotate(90); }
};
坐标转换

在自定义视图的鼠标事件处理器中,鼠标单击位置是用视图坐标表示的。要将视图坐标转换成场景坐标,可以用 mapToScene();反之用 mapFromScene() 方法。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyGraphicsView : public QGraphicsView
{
Q_OBJECT

// ...

protected:
void mousePressEvent(QMouseEvent* event)
{
QPoint viewPos = event->pos();
QPointF scenePos = this->mapToScene(viewPos);

// ...

QGraphicsView::mousePressEvent(event);
}
};

在得到场景坐标后,就可以调用场景的 itemAt() 方法获取位于该点且处于最顶层的图元。图元的 mapFromScene() 方法可以把场景坐标进一步转换成图元坐标。例如:

1
2
3
4
5
6
7
8
QGraphicsItem* item = this->scene()->itemAt(scenePos, this->transform());
if (item == nullptr) {
qDebug() << viewPos << "=>" << scenePos;
}
else {
QPointF itemPos = item->mapFromScene(scenePos);
qDebug() << viewPos << "=>" << scenePos << "=>" << itemPos;
}

在上面的例子中,单击场景坐标平面中的点 (50, 50),相当于单击矩形所在图元坐标平面的原点。

图元的边界矩形可以用它的 boundingRect()sceneBoundingRect() 方法获取,前者返回的矩形是用图元坐标表示的;后者返回的矩形是用场景坐标表示的。

1
2
QRectF boundingRect = item->boundingRect();
QRectF sceneBoundingRect = item->sceneBoundingRect();

组件事件

滚轮事件

滚轮事件在用户滚动鼠标滚轮时发生。滚轮事件对象的 angleDelta() 方法可以返回滚轮转过的角度,以八分之一度为单位。角度的符号指示滚轮滚动的方向:正数表示滚轮朝着远离用户的方向向前滚动,通常代表向上翻(或向左翻)或放大操作;负数表示滚轮朝着靠近用户的方向向后滚动,通常代表向下翻(或向右翻)或缩小操作。根据用户在滚动鼠标滚轮时是否有按下 Shift 键,可以把用户滚动鼠标滚轮的目的分为横向滚动和纵向滚动。这两种情况下滚轮转过的角度分别由 angleDelta().y()angleDelta().x() 指示。用户滚动鼠标滚轮时,鼠标指针的位置可以用 position() 方法获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void wheelEvent(QWheelEvent* event)
{
QPointF pos = event->position();
QPointF degree = event->angleDelta() / 8;

if (!degree.isNull()) {
int horDegree = degree.y();
if (horDegree > 0) {
qDebug() << "pos" << pos << ", horDegree" << horDegree;
}
}

event->accept();
}

拖放机制

拖放即拖拽(Drag)和放置(Drop),是一种可视化的组件间和应用间数据传输机制。拖放操作的过程是用户按下鼠标左键并移动鼠标一段距离后再松开鼠标左键的过程。可见,拖放操作包含拖拽和放置两个动作,其结果是源组件向目标组件传输了一份数据。源组件和目标组件可以是同一个。如果拖放操作的放置动作只是移动数据而不是复制数据,源组件还要负责删除原始数据。

开始拖拽

拖放操作通常要在用户按下鼠标左键并移动鼠标一段距离后才能触发。为此,在 mousePressEvent() 方法中记录鼠标的位置,在 mouseMoveEvent() 方法中检查鼠标移动过的距离并决定是否要开始一次拖放操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
QPoint dragStartPos;

void mousePressEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton)
dragStartPos = event->pos();
}

void mouseMoveEvent(QMouseEvent* event)
{
if (!(event->buttons() & Qt::LeftButton))
return;

int dis = (event->pos() - dragStartPos).manhattanLength();
if (dis < QApplication::startDragDistance())
return;

QLabel* label = static_cast<QLabel*>(this->childAt(event->pos()));
if (label == nullptr)
return;

// ...
}

开始一次拖放操作的方法是在 mouseMoveEvent() 方法中创建一个 QDrag 对象并调用它的 exec() 方法。需要传输的数据要用一个 QMimeData 对象描述和保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void mouseMoveEvent(QMouseEvent* event)
{
// ...
QPixmap pixmap = label->pixmap(Qt::ReturnByValue);
QPoint pixmapPos = event->pos() - label->pos();

QByteArray itemData;
QDataStream dataStream(&itemData, QIODevice::WriteOnly);
dataStream << pixmap << pixmapPos;

QMimeData* mimeData = new QMimeData;
mimeData->setData("application/octet-stream", itemData);

QDrag* drag = new QDrag(this);
drag->setPixmap(pixmap);
drag->setHotSpot(pixmapPos);
drag->setMimeData(mimeData);

Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction,
Qt::MoveAction);
if (dropAction == Qt::MoveAction) {
delete label;
}
}

QDragQMimeData 对象均无需手动销毁。

QDrag 对象的 setPixmap() 方法用于设置在拖放过程中跟随鼠标移动的位图;setHotSpot() 方法用于设置鼠标相对位图左上角的位置;setMimeData() 方法用于设置需要传输的数据。

1
2
3
4
QDrag* drag = new QDrag(this);
drag->setPixmap(pixmap);
drag->setHotSpot(pixmapPos);
drag->setMimeData(mimeData);

QMimeData 对象使用媒体类型(MIME type)来描述数据的性质和格式,既可以携带常规的文本和图像数据,也可以携带二进制数据,因此,QString 对象、QPoint 对象和 QPixmap 对象等数据结构在用 QDataStream 对象序列化成二进制数据后都可以通过拖放机制进行传输。

1
2
3
4
5
6
7
8
9
QPixmap pixmap = label->pixmap(Qt::ReturnByValue);
QPoint pixmapPos = event->pos() - label->pos();

QByteArray itemData;
QDataStream dataStream(&itemData, QIODevice::WriteOnly);
dataStream << pixmap << pixmapPos;

QMimeData* mimeData = new QMimeData;
mimeData->setData("application/octet-stream", itemData);

不同的放置动作(Drop actions)用 Qt::DropAction 类型的枚举成员表示:

  • Qt::CopyAction 复制数据(按住 Ctrl)
  • Qt::MoveAction 移动数据(按住 Shift)
  • Qt::LinkAction 创建快捷方式
  • Qt::IgnoreAction 取消操作(按下 ESC)

QDrag 对象 exec() 方法会阻塞当前事件处理器但不影响事件循环,它的第一个参数用于列出可供选择的放置动作,第二个参数用于指示默认放置动作,返回值指示实际选择的放置动作。

1
2
3
4
5
Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction, 
Qt::MoveAction);
if (dropAction == Qt::MoveAction) {
delete label;
}
鼠标形状

鼠标划过组件时的形状由组件的 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
2
3
4
5
QString title  = "Open File";
QString dir = QDir::homePath();
QString filter = "Images (*.png *.xpm *.jpg);;Text files (*.txt);;XML files (*.xml)";
QString path = QFileDialog::getOpenFileName(this, title, dir, filter);
qDebug() << path;

输入文本时,可以设置默认值,还可以设置单行文本输入框的响应模式。输入密码时,可以将响应模式设置为 QLineEdit::Password

1
2
3
4
5
6
7
8
QString title = "Input";
QString label = "User name:";
QLineEdit::EchoMode echoMode = QLineEdit::Normal;
QString defaultUsername = "John";
bool ok = false;
QString username = QInputDialog::getText(this, title, label,
echoMode, defaultUsername, &ok);
qDebug() << username; // "John"

image-20220923165757806

输入数字时,可以设置默认值、最小值、最大值和步进量。

1
2
3
4
5
6
7
8
QString title = "Input";
QString label = "User credit:";
int defaultValue = 5, minValue = 0, maxValue = 10, stepValue = 1;
bool ok = false;
int inputValue = QInputDialog::getInt(this, title, label,
defaultValue, minValue, maxValue, stepValue,
&ok);
qDebug() << inputValue;

image-20220930154946185

下拉列表的选项用 QStringList 对象指示。

1
2
3
4
5
6
7
8
9
QString title = "Input";
QString label = "User role:";
QStringList items = { "Administrator", "User" };
int currentIndex = 0;
bool editable = false;
bool ok = false;
QString item = QInputDialog::getItem(this, title, label,
items, currentIndex, editable, &ok);
qDebug() << item; // "Administrator"

image-20220930112850845

提示、警告消息框通常只有一个确定按钮。

1
2
3
QString title = "Information";
QString text = "Saved successfully!";
btn = QMessageBox::information(this, title, text);

image-20220930165942487

消息框的按钮用枚举类型 StandardButton 表示。

1
2
3
4
5
6
7
QString title = "Question";
QString text = "Are you sure?";
QMessageBox::StandardButtons btns = QMessageBox::Yes | QMessageBox::No;
QMessageBox::StandardButton defaultBtn = QMessageBox::NoButton;
QMessageBox::StandardButton btn;
btn = QMessageBox::question(this, title, text, btns, defaultBtn);
qDebug() << (btn == QMessageBox::Yes ? "Yes" : "No");

image-20220930170406934

自定义对话框

创建对话框的头文件、源文件和 UI 资源文件(File - New File… - Qt - Qt Designer Form Class)。

模态框

exec() 方法创建。

1
2
3
4
5
6
7
8
9
10
MyDialog *myDialog = new MyDialog(this);

int ret = myDialog->exec();
if (ret == QDialog::Accepted) {
qDebug() << "OK";
} else {
qDebug() << "Cancel";
}

delete myDialog;
非模态框

show() 方法创建。

1
2
3
4
5
6
7
8
MyDialog *myDialog = new MyDialog(this);

myDialog->setAttribute(Qt::WA_DeleteOnClose); // 关闭后自动析构

Qt::WindowFlags flags = myDialog->windowFlags();
myDialog->setWindowFlags(flags | Qt::WindowStaysOnTopHint); // 置顶窗口

myDialog->show();

事件

要自定义非模态框关闭时的动作,可以重写对话框的 close 事件处理程序。

1
2
3
4
void MyDialog::closeEvent(QCloseEvent *event)
{
qDebug() << "closed";
}
信号和槽

对话框之间的通信也可以借助信号和槽进行:在一个对话框中定义信号,在另一个对话框定义信号的槽。例如,父窗口中有一个按钮,用于创建一个子对话框,创建子对话框时要禁用该按钮,关闭子对话框时再启用该按钮。

dialog.h

1
2
3
private slots:
void on_pushButton_clicked();
void setButtonEnabled();

dialog.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Dialog::on_pushButton_clicked()
{
// 禁用按钮
ui->pushButton->setEnabled(false);

// 创建子对话框
MyDialog *myDialog = new MyDialog(this);
myDialog->setAttribute(Qt::WA_DeleteOnClose);
Qt::WindowFlags flags = myDialog->windowFlags();
myDialog->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
myDialog->show();

// 关联信号和槽
QObject::connect(myDialog, SIGNAL(myDialogClosed()),
this, SLOT(setButtonEnabled()));
}

void Dialog::setButtonEnabled()
{
// 启用按钮
ui->pushButton->setEnabled(true);
}

mydialog.h

1
2
3
4
5
protected:
void closeEvent(QCloseEvent *event);

signals:
void myDialogClosed(); // 表示对话框关闭的信号

mydialog.cpp

1
2
3
4
5
void MyDialog::closeEvent(QCloseEvent *event)
{
// 发送信号
emit myDialogClosed();
}

视图和模型

视图是一类组件,模型是数据的抽象,同一个模型可以用不同的视图呈现。例如,QFileSystemModel 是文件系统的模型,可以用树视图 QTreeView 和列表视图 QListView 两种组件呈现:

image-20220921111040438

在左边的 QTreeView 中点击一个目录时,右边的 QListView 将列出该目录下的所有文件和目录。

文件系统模型创建后,要用 setRootPath() 方法设置模型关联的目录。视图关联的模型都可以用 setModel() 方法设置。

1
2
3
4
5
6
7
8
9
10
11
12
MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MyWidget)
{
ui->setupUi(this);

QFileSystemModel *fileSystemModel = new QFileSystemModel;
fileSystemModel->setRootPath(QDir::currentPath());

ui->treeView->setModel(fileSystemModel);
ui->listView->setModel(fileSystemModel);
}

访问模型中的条目(Item)要通过 QModelIndex 对象,它是条目的索引。在 QTreeView 中点击一个目录时,它的 clicked(QModelIndex) 槽将得到被点击目录的索引,此时调用 QListViewsetRootIndex() 方法,将其设置为列表的根目录,就可以让 QListView 列出该目录下的所有文件和目录。

1
2
3
4
void MyWidget::on_treeView_clicked(const QModelIndex &index)
{
ui->listView->setRootIndex(index);
}

QStringListModel

QStringListModel 是字符串列表的模型,它的 setStringList()stringList() 方法分别用于设置和获取模型关联的字符串列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MyWidget)
{
ui->setupUi(this);

QStringList cities = { "Beijing", "Shanghai", "Guangzhou", "Shenzhen" };

QStringListModel *model = new QStringListModel;
model->setStringList(cities);

ui->listView->setModel(model);
}

条目的索引由行号、列号和父级索引组成,它的 row()column() 方法分别用于获取行号、列号。模型的条目总数可以用 rowCount() 方法获取。模型可以通过 model() 方法获取。

1
2
3
4
5
6
7
8
9
void MyWidget::on_listView_clicked(const QModelIndex &index)
{
const int row = index.row();
const int column = index.column();
const int total = index->model()->rowCount();
QString status = QString("Row %1, Column %2, Total %3")
.arg(row).arg(column).arg(total);
ui->label->setText(status);
}

模型可以分成列表模型、列表模型和表格模型。对于列表模型来说,所有条目的列号都是 0;对于列表模型和表格模型来说,所有条目都有相同的父级索引。

条目包含的数据都用 QVariant 对象表示,可以用 setData()Data() 方法设置和获取。条目的索引都可以用模型的 index() 方法获取。例如,获取最后一个条目的索引,再将它的数据设置为 Xiamen

1
2
3
4
5
QStringListModel *model = index->model();
const int total = model->rowCount();
QModelIndex lastIndex = model->index(total - 1, 0);
qDebug() << model->data(lastIndex).toString(); // "Shenzhen"
model->setData(lastIndex, "Xiamen");

一个条目可以包含多项数据,不同的数据有不同的用途。设置多项数据时,要给不同的数据分配不同的角色。数据的默认角色是 Qt::EditRoleQt::DisplayRole

1
2
3
model->setData(lastIndex, 
"Located on the southern coast of the Fujian Province",
Qt::ToolTipRole);

QStandardItemModel

QStandardItemModel 可以作为表格视图 QTableView 的模型。模型的每一个条目对应一个单元格。

image-20220921173702898

在创建模型时可以指定行数、列数。表头可以用 setHorizontalHeaderLabels() 方法设置。在指定位置上添加条目可以用 setItem() 方法。条目要用 QStandardItem 对象表示,条目的文本可以用 setText()text() 方法设置和获取。条目对应的单元格可以包含一个复选框,复选框的状态可以用 setCheckState() 方法设置。

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
QStandardItemModel *theModel;

MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MyWidget)
{
ui->setupUi(this);
theModel = new QStandardItemModel(2, 3);
theModel->setHorizontalHeaderLabels({ "username", "password", "status" });

QList<QStringList> users = {
{ "root", "secret", "enabled" },
{ "John", "123456", "disabled" }
};

for (int i = 0; i < users.size(); i++) {
QStringList user = users[i];
QStandardItem *username = new QStandardItem;
QStandardItem *password = new QStandardItem;
QStandardItem *status = new QStandardItem;
username->setText(user[0]);
password->setText(user[1]);
status->setCheckable(true);
status->setCheckState(user[2] == "enabled" ? Qt::Checked : Qt::Unchecked);
theModel->setItem(i, 0, username);
theModel->setItem(i, 1, password);
theModel->setItem(i, 2, status);
}
ui->tableView->setModel(theModel);
}

条目的文本实际上就是角色为 Qt::DisplayRoleQt::EditRole 的数据,因此下面两条语句等价:

1
2
3
username->setText(user[0]);
// 等价于
username->setData(user[0], Qt::EditRole);

QItemSelectionModel

QItemSelectionModel 是选择模型,用于跟踪在视图中被选中的条目。创建选择模型时要用数据模型作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
QStandardItemModel *theModel;
QItemSelectionModel *theSelectionModel;

MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MyWidget)
{
ui->setupUi(this);

theModel = new QStandardItemModel(2, 3);
theSelectionModel = new QItemSelectionModel(theModel);

ui->tableView->setModel(theModel);
ui->tableView->setSelectionModel(theSelectionModel);

QObject::connect(theSelectionModel, SIGNAL(currentChanged(QModelIndex,QModelIndex)),
this, SLOT(on_currentChanged(QModelIndex,QModelIndex)));
}

用户在视图中点击不同的条目时,选择模型会发送 on_currentChanged() 信号,两个参数分别当前被点击的条目和上一次被点击的条目的索引。

1
2
3
4
5
6
7
8
9
void MyWidget::on_currentChanged(const QModelIndex &current, 
const QModelIndex &previous)
{
if (current.isValid()) {
QString status = QString("Row %1, Column %2")
.arg(current.row()).arg(current.column());
ui->label->setText(status);
}
}

所有选中条目的索引可以用选择模型的 selectedIndexes() 方法获取。

1
2
3
4
5
6
7
8
if (!theSelectionModel->hasSelection()) {
return;
}
QModelIndexList indexes = theSelectionModel->selectedIndexes();
for (int i = 0; i < indexes.count(); i++) {
QStandardItem *item = theModel->itemFromIndex(indexes[i]);
qDebug() << item->text();
}

自定义代理类

代理类让用户可以在视图中编辑条目。

image-20220923151853391

用户在视图中双击某个条目时,需要创建合适的控件用于编辑条目,此时会调用代理类的下列三个方法:

  • createEditor() 创建合适的控件作为编辑器
  • updateEditorGeometry() 将编辑器的尺寸调整为合适的大小
  • setEditorData() 用条目的数据设置编辑器的内容

用户结束编辑时,需要将编辑器的内容同步到模型中,此时会调用代理类的 setModelData() 方法。

默认的代理类使用单行文本框作为编辑器。如果要使用其他控件,就需要自定义代理类。自定义代理类通常继承 QStyledItemDelegate 类:

.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <QStyledItemDelegate>

class MyDelegate : public QStyledItemDelegate
{
Q_OBJECT

public:
MyDelegate(QObject *parent = nullptr);

virtual QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;

virtual void setEditorData(QWidget *editor,
const QModelIndex &index) const override;

virtual void setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index) const override;

virtual void updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};

.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
36
37
38
39
40
41
#include "mydelegate.h"
#include <QComboBox>

MyDelegate::MyDelegate(QObject *parent)
{
//
}

QWidget *MyDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QComboBox *comboBox = new QComboBox;
comboBox->setParent(parent);
comboBox->setFrame(false);
comboBox->addItem("enabled");
comboBox->addItem("disabled");
return comboBox;
}

void MyDelegate::setEditorData(QWidget *editor,
const QModelIndex &index) const
{
QComboBox *comboBox = static_cast<QComboBox *>(editor);
comboBox->setCurrentText(index.data().toString());
}

void MyDelegate::setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index) const
{
QComboBox *comboBox = static_cast<QComboBox *>(editor);
model->setData(index, comboBox->currentText());
}

void MyDelegate::updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
editor->setGeometry(option.rect);
}

要让视图使用自定义的代理类,可以用 setItemDelegateForColumn() 方法设置:

1
ui->tableView->setItemDelegateForColumn(2, new MyDelegate);

多线程

创建、启动和结束线程

线程要用线程类表示。线程类要继承 QThread 类并重写 void run() 方法。线程要完成的任务就在 run() 方法里实现。run() 方法返回前需要调用 exit() 方法。调用 exit() 方法时,可以提供一个退出码。

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 MyTimer: public QThread
{
Q_OBJECT

public:
MyTimer(int msec);
void stop();

protected:
void run() Q_DECL_OVERRIDE;

private:
bool isRunning_;
int interval_;

signals:
void timeout();
};

MyTimer::MyTimer(int msec)
{
interval_ = msec;
}

void MyTimer::stop()
{
isRunning_ = false;
}

void MyTimer::run()
{
qDebug() << "pid :" << QThread::currentThreadId();
isRunning_ = true;
while (isRunning_) {
msleep(interval_);
emit timeout();
}
exit(0);
}

sleep()msleep()usleep() 都是 QThread 类的静态方法,它们可以让当前线程休眠一段时间。

启动线程的方法是调用 start() 方法。start() 方法会调用 run() 方法。run() 方法返回时,线程结束。父线程可以调用 isRunning()isFinished() 方法检查子线程的状态,调用 terminate() 方法强制结束子线程。terminate() 方法会立即打断 run() 方法。无论子线程是因为 run() 方法返回而结束,还是因为父线程调用了 terminate() 方法而结束,父线程都要在希望子线程结束的时候调用 wait() 方法等待子线程结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
MyTimer *myTimer;

myTimer = new MyTimer(3000);

qDebug() << "ppid :" << QThread::currentThreadId();
if (myTimer->isRunning()) {
myTimer->stop(); // myTimer->terminate();
myTimer->wait();
qDebug() << "finished";
} else {
myTimer->start();
qDebug() << "started";
}

线程启动时会发送 started() 信号;结束时会发送 finished() 信号。

线程同步

互斥锁 QMutex

当两个线程在读写同一个变量时,相互打断有可能导致错误的结果。

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
class MyElapsedTimer: public QThread
{
Q_OBJECT

public:
MyElapsedTimer();
bool elapsed(int &hour, int &minute, int &second);

protected:
void run() Q_DECL_OVERRIDE;

private:
int hour_;
int minute_;
int second_;
};

MyElapsedTimer::MyElapsedTimer()
{
hour_ = minute_ = second_ = 0;
}

bool MyElapsedTimer::elapsed(int &hour, int &minute, int &second)
{
hour = hour_;
minute = minute_;
second = second_;
return true;
}

void MyElapsedTimer::run()
{
while (true) {
msleep(1000);
second_++;

if (second_ >= 60) {
second_ = 0;
minute_++;

if (minute_ >= 60) {
minute_ = 0;
hour_++;

if (hour_ >= 24) {
hour_ = 0;
}
}
}
}
}

当父线程调用 elapsed() 方法读取变量时,子线程的 run() 方法有可能正在更新这些变量,此时父线程就有可能得到 hour24minute60second60 的错误结果。

1
2
3
4
5
myElapsedTimer->start();

int hour, minute, second;
myElapsedTimer->elapsed(hour, minute, second);
qDebug("%02d:%02d:%02d", hour, minute, second);

如果两段代码不能相互打断,就需要用一个 QMutex 对象来管理它们:在它们的开头调用 lock()tryLock() 方法;在它们的结尾调用 unlock() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool MyElapsedTimer::elapsed(int &hour, int &minute, int &second)
{
if (mutex.tryLock()) {
hour = hour_;
minute = minute_;
second = second_;
mutex.unlock();
return true;
}
return false;
}

void MyElapsedTimer::run()
{
while (true) {
msleep(1000);

mutex.lock();
// ...
mutex.unlock();
}
}
QMutexLocker

QMutex 对象的 lock()unlock() 方法必须配对使用。QMutexLocker 类用于简化 QMutex 类的用法,它的构造函数会调用给定 QMutex 对象的 lock() 方法,析构函数会调用 unlock() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool MyElapsedTimer::elapsed(int &hour, int &minute, int &second)
{
QMutexLocker mutexLocker(&mutex);
hour = hour_;
minute = minute_;
second = second_;
return true;
}

void MyElapsedTimer::run()
{
while (true) {
msleep(1000);

QMutexLocker mutexLocker(&mutex);
// ...
}
}
读写锁 QReadWriteLock

如果两个线程都只是单纯地读取变量而不更改变量,就无需顾虑它们相互打断的情况。要区分这种情况,可以用 QReadWriteLock 类代替 QMutex 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool MyElapsedTimer::elapsed(int &hour, int &minute, int &second)
{
if (readWriteLock.tryLockForRead()) {
hour = hour_;
minute = minute_;
second = second_;
readWriteLock.unlock();
return true;
}
return false;
}

void MyElapsedTimer::run()
{
while (true) {
msleep(1000);

readWriteLock.lockForWrite();
// ...
readWriteLock.unlock();
}
}
QReadLocker & QWriteLocker

就像 QMutex 类的用法可以用 QMutexLocker 类简化一样,QReadWriteLock 类的用法可以用 QReadLockerQWriteLocker 类简化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool MyElapsedTimer::elapsed(int &hour, int &minute, int &second)
{
QReadLocker readLocker(&readWriteLock);
hour = hour_;
minute = minute_;
second = second_;
return true;
}

void MyElapsedTimer::run()
{
while (true) {
msleep(1000);

QWriteLocker writeLocker(&readWriteLock);
// ...
}
}
QWaitCondition

QWaitCondition 类用于保证不同线程负责的任务的执行顺序:调用 wait() 方法的线程会被阻塞,直到另一个线程调用 wakeAll()wakeOne() 方法。为了确保 wait() 方法在 wakeAll() 方法之前被调用,QWaitCondition 类必须结合 QMutex 类使用。在调用 wait() 方法时,需要提供一个已经上锁的 QMutex 对象。wait() 方法被调用时会给它解锁,在返回时再给它重新上锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
QMutex mutex;
QWaitCondition condition;

void MyThread::run()
{
mutex.lock();
qDebug() << "step 2";
condition.wakeAll();
mutex.unlock();
exit(0);
}

void main()
{
mutex.lock();
myThread.start();
QThread::sleep(1);
qDebug() << "step 1";
condition.wait(&mutex);
qDebug() << "step 3";
mutex.unlock();
}
信号量 QSemaphore

QSemaphore 类的作用与 QMutex 类相同。但是 QMuter 类只能管理一个资源,而 QSemaphore 类可以管理一定数量的资源。资源总数在创建 QSemaphore 对象时就需要确定。资源可以用 acquire()release() 方法批量占用和释放。

1
2
3
4
5
char buffer[5];

QSemaphore freeBytes(5);
freeBytes.acquire(2);
qDebug() << freeBytes.available(); // 3

如果没有足够数量的资源可以占用,调用 acquire() 方法的线程就会进入阻塞状态,直到有足够数量的资源被释放出来。