written by SJTU-XHW

Reference: C++ GUI Programming with Qt 4 (2nd Edition)

注意:本文章将讲解 Qt 5 入门知识,需要一定的 C++ 基础

本人知识浅薄,文章难免有问题或缺漏,欢迎批评指正!

观前提示:本系列的所有完整代码在整理好后,都会存放在仓库中,有需要可以自取 ~


Chapter 0 前置知识

0.1 C++ 基础 和 面向对象编程

0.2 C++ 的宏(macro)

  • 宏的定义非常自由甚至可以把一个符号定义为一个很长的字符串,甚至代码;主要是因为宏的工作原理是编译前将宏直接原封不动地替换;例如下面的极端例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 直接把 HELLO 定义为一串代码
#define HELLO \
QLabel* label = new QLabel("<h2><i>Hello,</i><font color=red>Qt!</font></h2>");\
QPushButton* btn = new QPushButton("Quit");\
QObject::connect(btn, SIGNAL(clicked()), &app, SLOT(quit()));\
label->show();\
btn->show();

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

// 直接写 HELLO 就相当于替换了
HELLO
return app.exec();
}

0.3 Qt 环境配置

Unix 系统:不是安装完就能在命令行里用了吗?

Windows 系统:在下载安装的 Qt 目录中找到编译器文件夹(安装时应该提醒你设置过了),把编译器目录下 bin 文件夹目录添加到 用户/系统环境变量 Path 中;

Chapter 1 Qt 初认识

1.1 简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<QApplication>    // Qt 中一些类的定义
#include<QtWidgets/QLabel>

int main(int argc, char* argv[]) {
// QApplication 支持两个参数,说明 Qt 也有自己的命令行参数
QApplication app(argc, argv); // 创建 QApplication 对象,用于管理程序资源
// 创建了显示 “……” 的 QLabel 窗口部件(widget)
QLabel* label = new QLabel("Hello, Qt!");
// 显示该 widget,详见 tips2.
label->show();
// 将应用程序的控制权传递给 Qt,程序进入循环等待状态,监听用户动作并根据代码作出反应
return app.exec();
// 此处不考虑内存泄漏,因为整个程序结束后,空间能直接被操作系统回收
}

widget:窗口部件,指用户界面中的一个可视化元素,相当于 Windows 中的控件、容器。按钮、菜单、滚动条、框架等都是窗口部件。

tips 1. 绝大多数应用程序会使用 QMainWindowQDialog 作为窗口;而且在 Qt 中很灵活,甚至可以使用 QLabel 窗口部件来作为窗口(如上例);

tips 2. 创建 widget 时,大多都是“隐藏”属性,这可以使得我们先更改一些性质,在手动显示它们

tips 3. QLabel 类的初始化参数的字符串允许简单 HTML 文本!例如:

1
2
QLabel* label = QLabel("<h2><i>Hello,</i></h2>"
"<font color=red>Qt!</font></h2>");

1.2 要点:通过命令行(qmake)创建、编译 Qt 工程

本文单独提出为一个小节足以证明重要性,这里不会,连程序都跑不起来……

【⚠ 劝退警告】了解 Qt 前,本文默认大家已经对 C++ 从编码到编译运行的过程都基本了解;

不了解就想学 Qt,只能说你是抱着没打算学透彻的心态,或者是只会用 IDE,一碰到报错就四处提问,,建议直奔 B 站 “1天速成”,或者某 CSDN 看“教程”;

但如果想要了解这方面内容可以参见博客 “GNU Tutor” 来入门,或者配合 GCC、CMake 相关课程学习(当然要先学会 C++ 基础);

从现在开始将使用 Qt 的用户分为 2 类(和操作系统环境无关):

  1. 使用 Qt Creator 的用户(即在官网安装 Qt Creator IDE 及 全套 Qt 运行环境的用户),以后称这类用户为 “IDE 用户”

    这很像 C++ 使用 VS、VSCode、CLion 等 IDE(集成开发环境) 一键编译运行的用户;

  2. 使用单独的 Qt Designer 的用户(即 Qt 库 + 命令行编译 + Qt Designer 的用户),以后称这类用户为 “非 IDE 用户”

    这很像 C++ 使用编辑器写代码、手动使用 CMake/Make/GCC 编译、使用命令行运行的亲力亲为的用户;

非 IDE 用户如何创建、编译运行 Qt 项目

由于手动编译更困难、更接近 Qt 运行的原理,所以优先介绍非 IDE 用户的做法;

劝退警告:Windows 环境配置复杂于 Linux,不过能让你更好了解 Qt 项目编译全过程,如果感兴趣可以使用这种方法;本人在开发 Windows 桌面应用时就采用这种方法

  • 【此步仅 Windows 用户】确认编译器环境:Windows 中你可能在之前就有一个 C++ 编译器,并且已经配置在环境变量里,例如 MSVC 或者 minGW,所以为了防止手动编译用错了编译器,导致报错,这步是必须的

    在编译前,需要临时加入2个环境变量,来确保覆盖系统内其他C++编译器的环境变量

    1. 和 Qt 库配套的 C++ 编译器目录。如果你安装了 Qt Creator,那么在安装时应该顺带让你设置并安装了对应版本的 minGW 编译器,它的位置和 Qt 是放在一起的;

      通常位置是:<Qt安装根目录>\Tools\mingw<版本号>\bin;

      例如本人的位置:D:\Qt5.14.2\Tools\mingw730_64\bin

    2. Qt 库引入新的编译器,例如 moc 编译器、uic 编译器(后面会说),所在的目录;

      通常位置是:<Qt安装根目录>\<Qt版本号>\mingw<版本号>\bin

      例如本人的位置:D:\Qt5.14.2\5.14.2\mingw73_64\bin

    为了在手动编译完项目后,不影响其他C++编译器的正常使用,应该把这两个环境变量设置为临时环境变量,最方便的做法是写成 BAT 脚本,在命令行窗口中使用,只保留到本次会话结束;坏处是每次编译前都要运行这个脚本;例如本人的脚本应该这么写:

    1
    2
    3
    # File: addEnv.bat
    @echo off
    set PATH=D:\Qt5.14.2\5.14.2\mingw73_64\bin;D:\Qt5.14.2\Tools\mingw730_64\bin;%PATH%

    你可以将这个脚本保存在你的项目目录,或者其他目录,编译前运行一下就行,注意必须用命令行运行,而且编译必须使用这个命令行,不能关闭,否则临时环境变量会丢失,需要重新运行

    当然,如果你十分肯定系统中唯一的 C++ 编译器就是 Qt 安装的这个编译器,并且还在环境变量里,那么这一整个步骤就不用做了

  • 创建工程:在已安排源文件(你已经创建了一些 *.h/cpp)的目录下执行:qmake -project,生成 *.pro 文件,与平台无关的项目文件;

    或者你想让项目目录干净点可以新建一个 build 文件夹,将命令行切入 build 中,再执行 qmake -project <你的项目目录>

  • 添加 Qt 库:如果这个项目除了 QtCoreQtGui(默认包含) 以外,还想添加额外的 Qt 库,例如常用的 QtWidgetsQtNetwork,那么在 *.pro 文件的合适位置添加:QT += widgetsQT += network

    想要了解更详细的 *.pro 文件的编写规则,请查阅官方文档

    不过除了添加 Qt 库,其他应该很少会直接修改 pro 文件,例如引入项目文件就不用

    • IDE 用户可以在 Qt Creator 左侧文件栏右击添加文件,会自动更新 pro 文件;
    • 非 IDE 用户只需在相同文件夹下重新运行 qmake -project 即可更新 pro 文件;
  • 编译工程:使用 qmake *.pro 将一般项目文件编译为与平台相关的 makefile 文件;最后运行make 直接编译即可;

    qmake *.pro 的过程有点像 CMake 对照 CMakeLists.txt 生成 Makefile 的过程;

    注:Windows 下稍微麻烦一点,在项目目录下:

    1. 运行 qmake *.pro -spec win32-g++ "CONFIG+=debug" "CONFIG += qml_debug"

    2. 运行 mingw32-make.exe,这就相当于 Unix 系统下的 make

    1. 当你在生成 makefile 后,又向程序中加入一些新的包或函数,那么可能需要再次运行 qmake 来生成 新的 makefile,以防编译器无法找到新文件

    2. 有同学可能会问,能不能不用 qmake,就用 cmake?这个可以,下一节就说!

IDE 用户如何创建、编译运行 Qt 项目

  • 法1(纯 IDE 法):打开 Qt Creator -> 新建项目 -> 按指示配置环境(界面中 Qt Application 模板对新手不友好,可能需要思考一会项目结构) -> 编写项目 -> 编译运行就交给 IDE 吧~;
  • 法2(命令行法,比较自由):新建一个项目文件夹 -> 按需创建 *.cpp *.h 等项目文件 -> 命令行进入该目录运行 qmake -project -> 进入生成的 *.pro 按需添加所需 Qt 库 -> 双击 pro 文件/进入 Qt Creator 打开项目 -> 编写项目 -> 编译运行就交给 IDE 吧~;

1.2-EX 使用 CMake 代替 qmake 构建项目

使用 IDE 的小伙伴就可以跳过了哦 ~ 因为你们只需要在创建项目时,选择 “项目构建系统” 为CMake,就完成了!

下面,在原来的 CMake 语法的基础上(基础语法不作介绍,可以看本站以前的文章,或者网上学习),本人仅会介绍 和普通 C++ 项目构建的不同之处

  1. 【必要】添加 Qt 专属编译器(这些编译器在后面会一一介绍,学完可以回来看看):

    1
    2
    3
    4
    5
    6
    # 自动调用 uic 编译器处理 *.ui
    set(CMAKE_AUTOUIC ON)
    # 自动调用 moc 编译器处理 Qt 宏和关键字
    set(CMAKE_AUTOMOC ON)
    # 自动处理 *.qrc Qt 资源文件
    set(CMAKE_AUTORCC ON)
  2. 【必要】如果 你在 qmake 中需要添加诸如:

    1
    QT += widgets network    // *.pro 的写法

    的 Qt 库,在 CMake 中需要这么写:

    1
    2
    # $ENV{} 调用系统环境变量,这个 Qt_HOME 需要自己设置在系统环境变量里
    find_package(Qt5 COMPONENTS Widgets Network REQUIRED PATHS $ENV{Qt_HOME})

    很遗憾,CMake 没有 qmake 的默认设置,qmake 默认加入的 GuiCore 库需要在 CMakeLists 中手动加入

    1
    find_package(Qt5 COMPONENTS Core Gui REQUIRED)

    而且在最后还要手动链接库

    1
    target_link_libraries(exeName PRIVATE Qt5::Widgets Qt5::Core Qt5::Gui)
  3. 【注意】:在 CMake 中,由于之前添加了专属编译器,所以 *.ui*.h/cpp 一样,都需要在 add_executableadd_library 构建目标时,作为源文件加入进去

最后,以一个示例项目为例子(不同的项目没法照抄哦~)

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
CMAKE_MINIMUM_REQUIRED(VERSION 3.12)
PROJECT(HelloWorld)

# 【optional】设置工程包含当前目录
SET(CMAKE_INCLUDE_CURRENT_DIR ON)

SET(CMAKE_AUTOMOC ON)
SET(CMAKE_AUTOUIC ON)
SET(CMAKE_AUTORCC ON)

FIND_PACKAGE(Qt5 COMPONENTS Widgets Gui Core REQUIRED PATHS $ENV{Qt5_HOME})

# 查找当前文件夹中的所有相关文件
FILE(GLOB SOURCE_FILES "./*.cpp")
FILE(GLOB HEADER_FILES "./*.h")
FILE(GLOB UI_FILES "./*.ui")

# 通过Ui文件生成对应的头文件
QT5_WRAP_UI(WRAP_FILES ${UI_FILES})

# 添加资源文件
SET(RCC_FILES rcc.qrc)

# 【optional】生成UI文件夹
SOURCE_GROUP("UI" FILES ${UI_FILES} ${WRAP_FILES} )

# 生成可执行文件,需添加RCC_FILES、WRAP_FILES
ADD_EXECUTABLE(${PROJECT_NAME} ${SOURCE_FILES} ${HEADER_FILES} ${RCC_FILES} ${WRAP_FILES})

# 添加Qt5依赖项
TARGET_LINK_LIBRARIES(${PROJECT_NAME} Qt5::Widgets Qt5::Core Qt5::Gui)

1.3 建立连接

之前我们认识了简单 Qt 程序的基本运作,那么如何实现 Qt 响应用户的动作呢?

1.3.1. 信号与槽的原理

  • Qt 的 widget 通过发射信号(signal,实质是一个函数,和操作系统的信号无关)来表明用户的某个动作已发生,或者状态已改变;

    举例:用户点击按钮类 QPushButton 时,按钮会发射 clicked() 信号;

  • Qt 的 槽(slot)能够接收信号,是一个实际上的函数,一旦触发该信号,slot 会自动执行;

    注意:槽就是函数!一个类如果具有一个方法,那么它就可以作为这个类的槽

  • Qt 通过宏(macro)来将 click() 等对象转化为信号str、将函数 F() 转化为槽,并使用 QObject::connect 函数进行绑定;

    宏转化的时候,如果信号 / 槽对应的函数有参数,务必填入参数类型,例如:

    SIGNAL(valueChanged(int))

ℹ 这里简单带过一下,给读者一个初印象,以后会详细深入介绍 信号-槽机制;

1.3.2. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<QApplication>
#include<QtWidgets/QPushButton>

int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QPushButton* btn = new QPushButton("quit");
// 使用 connect 静态成员函数,其参数原型为:
// QObject::connect(QObject* p1, SIGNAL, QObject* p2, SLOT)
// 其中 p1 是指向发送信号的widget指针,p2 是指向接受信号的函数槽**所在的widget**指针,这里是 含有quit()方法 的 QApplication 对象;
QObject::connect(btn, SIGNAL(clicked()), &app, SLOT(quit()));
button->show();
return app.exec();
}

1.4 Qt 窗口的布局设计

1.4.1 widget 间的父子关系

当需要在一个窗口中,合理地安排各种 widget 的摆放时,需要考虑这些 widget 间的层次关系;

Qt 中,和其他类的 GUI 设计库类似的做法是,引入 widget 间的父子关系;表示:A 是 B 的子控件 就可以理解为 A 是布局在 B 上的 控件

比如,想要制作如上图的应用界面,就需要遵循这样的步骤(仅供参考,其他方法也能实现):

  1. 在窗口最顶层设置一个 QWidget 类对象的抽象的 widget,用来盛放其他 widget,可以理解现实中的一个 “桌布”;

  2. QWidget 类对象为父控件(QWidget 自己没有父控件,它就是顶层窗口),设置 QSpinboxQSlider 对象(分别是 微调框 控件类、滑动条 控件类);

    一般情况下,QWidget 及其子类设置父控件的方法是通过布局管理器实现;理解为“用布局管理器打包在一起”;

  3. 绑定内部信号-槽的关联;

  4. 最后使用布局管理器将子控件按指定“摆放方式”显式加入父控件,显示顶层 widget 即可;

实现如下:

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
#include<QApplication>
#include<QtWidgets/QHBoxLayout> // 引入 布局管理器
#include<QtWidgets/QSlider> // 引入 滑动条控件类
#include<Qtwidgets/QSpinBox> // 引入 微调框控件类

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

QWidget* mainWindow = new QWidget;
mainWindow->setWindowTitle("Enter your age"); // QWidget 类具有成员函数 setWindowTitle

QSpinBox* spinBox = new QSpinBox;
QSlider* slider = new QSlider(Qt::Horizontal); // QSlider 的构造函数的第一个参数可以使用 Qt 枚举量 Horizontal 设置滑动方向
spinBox->setRange(0, 130);
slider->setRange(0, 130); // 两个类都具有成员函数 setRange

// 两个类都具有:valueChanged(int) 信号、setValue(int) 槽,因此将两者相互绑定
QObject::connect(spinBox, SIGNAL(valueChanged(int)), slider, SLOT(setValue(int)));
QObject::connect(slider, SIGNAL(valueChanged(int)), spinBox, SLOT(setValue(int)));

spinBox->setValue(35); // 初始化值为35

QHBoxLayout* layout = new QHBoxLayout; // 初始化布局管理器
layout->addWidget(spinBox);
layout->addWidget(slider); // 布局管理器将子控件打包在一起
// QWidget 具有成员函数 setLayout,可以传入布局管理器实现布局设计
window->setLayout(layout); // 再将包传给父控件,底层会自动将管理器中的所有子控件定向其父控件

window->show(); // 最后只需展示顶层 widget 即可
return app.exec();
}

1.4.2 布局设计的意义

有同学会问,为什么需要 layout?layout 可以让多个 widget 按想要的方式排列在一个窗口上;如果不这么做,就没法定义摆放方式了!你可以试一试,不用布局管理器,你会发现两个或多个 widget 是分布在不同的窗口下的;

1.4.3 布局管理器的类型

除了以上例子介绍的 QHBoxLayout 类,还有 QVBoxLayoutQGridLayout,其作用分别是:

  • QHBoxLayout:默认在水平方向,从左到右排列 widget;
  • QVBoxLayout:默认在竖直方向,从上到下排列 widget;
  • QGridLayout:将 widget 排列在预设的网格中;

常见的使用方法:先声明、在设置属性,最后添加打包到布局管理器中,设置给父控件;

它们都继承于 QLayout,所以它们不是 widget(QWidget),一般也不可见

1.5 章末贴士

  • 重要:一定要会使用官方文档;

  • 有些同学会想,里面的命令行参数 argcargv 究竟可以做什么?其实,举个例子就明白了,其中一个用途是设置应用界面的主题,即:./应用名 -style <style name>,常用的 style name 有:plastiqueCleanlooksCDEMotifWindowsWindows XPWindows VistaMac

  • 本章涉及到的类和一些方法的总结

  • 和 Qt 4 比较:QLabel、QPushButton、QSlider、QSpinBox 都还是 QWidget 的子类,但 Qt 5 类的头文件移动到单独的 QWidgets 模块中,即 include 时,需要:#include<QtWidgets/QXXX>

    例如 1.4.1 的例子在 Qt 4 环境下应该这么写【亲测能跑】

    1
    2
    3
    4
    5
    6
    7
    #include<QApplication>
    #include<QSpinBox>
    #include<QSlider>
    #include<QHBoxLayout>

    // …… (后面一毛一样)
    // 记得在 *.pro 中移除:QT += widgets

    最后强调一下, Qt 4 到 5、Qt 5 到 6 的很多操作都改变了,所以别轻易更换项目的 Qt 大版本!

Chapter 2 面向对象的 Qt

2.1 纯代码设计

2.1.1 示例:以简单对话框为例

想象一下,这是一个庞大应用程序的一个小部分对话框,现在想要单独设计它。但是第一章的代码都写在一个 main 函数中,如果窗口一多,不仅不利于维护,而且容易编写错误;

所以我们从现在开始采用 C++ 中的不同类来编写不同窗口,可以形成很好的封装性,增强可读性;

下面将这个窗口编写为一个类 findDialog

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
// file: findDialog.h

#ifndef FINDDIALOG_H
#define FINDDIALOG_H

#include<QtWidgets/QDialog> // 包含 Qt 对话的基类,派生于 QWidget,和 1.5 中说的一样,在 Qt 4 中要写 #include<QDialog>,以下不再赘述;

#include<QtWidgets/QLineEdit>
#include<QtWidgets/QPushButton>
#include<QtWidgets/QCheckBox>
#include<QtWidgets/QLabel>

class findDialog : public QDialog {
Q_OBJECT // 很重要的一个宏,里面写了 QObject 类的几乎所有信号和槽,还有其他属性和方法

public:
// 这是一个典型的 Qt widget 类的定义方式,第一个参数指定父控件
findDialog(QWidget* parent = 0);

// 这个 marco 说明以下定义的函数都是 信号函数
signals:
// Qt::CaseSensitivity 是 enum 类型,和 C++ 中的 true、false 如出一辙,本身的值也是 0 或 1,用于区分大小写是否敏感的具体情况,含有值 Qt:CaseSensitive 和 Qt::CaseInSensitive
void findNext(const QString& str, Qt::CaseSensitivity cs);
void findPrevious(const QString& str, Qt::CaseSensitivity cs);

// 声明了私有的槽
private slots:
void findClicked();
// 思考一下,已知 QPushButton 自己有槽函数 setEnabled(bool),为什么还要包装一层 私有的槽呢?(答案在实现这个槽的时候揭晓)
void enabledFindButton(const QString& text);

private:
QLabel* label;
QLineEdit* lineEdit;
QCheckBox* caseCheckBox;
QCheckBox* backwardCheckBox;
QPushButton* findButton;
QPushButton* closeButton;
};

#endif

这是窗口的实现 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
// file: findDialog.cpp

#include<QtGui> // 详见注解 tips 1.

#include "findDialog.h"

// findDialog 继承于 QDialog,所以也使用它的一些数据成员,自然需要委托构造
findDialog::findDialog(QWidget* parent) : QDialog(parent) {
// 这里有 3 点需要解释,详见 tips 2. 、 tips 3. 和 tips 4.
label = new QLabel(tr("Find &what")); // tips2. tips3.
// 新的类:QLineEdit 类,单行输入框类
lineEdit = new QLineEdit;
label->setBuddy(lineEdit); // tips4.

// 新的类:QCheckBox 类,勾选框类,初始化参数和 QLabel 一样,也是str内容
caseCheckBox = new QCheckBox(tr("Match &case"));
backwardCheckBox = new QCheckBox(tr("Search &backward"));

findButton = new QPushButton(tr("&Find"));
findButton->setDefault(true); // tips 5.
findButton->setEnabled(false); // tips 6.

closeButton = new QPushButton(tr("Close"));

// 这里不用写作用域 “QObject::”,因为 QDialog 就是 QObject 的子类
// 详见 tips 7.
// 还记得之前声明的私有槽吗?(再等会实现定义)这里是将 lineEdit 的文字改变信号连接到 findDiag 窗体的 enabledFindButton(const QString&) 私有槽上;
connect(lineEdit, SIGNAL(textChanged(const QString&)),
this, SLOT(enableFindButton(const QString&)));
connect(findButton, SIGNAL(clicked()),
this, SLOT(findClicked()));
// tips 8.
connect(closeButton, SIGNAL(clicked()),
this, SLOT(close()));

// 代码块未完待续----------------------------
  • tips 1. 头文件 QtGui.h 包含了 Qt GUI 类的 QtCoreQtGui 模块的所有类的定义;

    回顾一下 Qt 的主要模块:QtCoreQtGuiQtNetworkQtOpenGLQtSqlQtSvgQtXml

    • 同学会问,为什么不在 findDialog.h 中直接 #include<QtGui>因为这个包比较大,引入他可能造成引用的不清晰,不是一个好习惯;理论上用到什么引入什么
  • tip 2. 在 Qt 中,所有字符串都认为是 QString ——有 tr(QString) 方法可以将它们翻译;这就意味着,在所有用户可见的字符串周围加上 tr() 函数是个好习惯;这样方便软件后期的翻译工作,对 tr() 的翻译会在后面介绍;

  • tips 3. 如果想在用户可见的字符串中加入快捷键来控制焦点(选中的区域,意味着用户可以直接输入,或者按 ENTER=点击),那么在字符串前中写 “&” 符号,表示Alt + 字符串第一个字符 作为快捷键

  • tips 4. 几乎所有 QWidget 都有一个方法 setBuddy(QWidget* ptr) 用来绑定两个 widget 为兄弟控件,具体表现在共用同一个快捷键(这个快捷键会同时聚焦这两个控件);

  • tips 5. 大多数 QWidget 都有一个方法 setDefault(bool flag) 用来指定刚打开窗口时聚焦的控件

  • tips 6. QPushButton 有一个特有属性 enabled,如果是 true,则这个按钮是可以点击的,否则按钮呈现灰色不可点击的状态;

  • tips 7. 由上面的 connect 函数可以看出,QLineEdit 类有一个 textChanged(const QString&) 信号;

  • tips 8. 这里 QDialogclose() 方法继承于 QWidget,默认行为是将 widget 隐藏起来(而非删除),这和 QApplication 类的 quit() 方法不一样,quit() 方法是关闭并删除窗口及其上的所有布局、widget;

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
    // 上接上一个代码块 --------------------------	

// 记住在 1.4.1 中说的 4 个基本步骤,这里是倒数第二个:打包布局
// 这里的案例告诉我们,对于多个 widget 的布局,可以采用多个不同位置、不同类型的布局管理器来进行,这里的具体划分见 tips 9.
QHBoxLayout* topLeftLayout = new QHBoxLayout;
topLeftLayout->addWidget(label);
topLeftLayout->addWidget(lineEdit);

QVBoxLayout* leftLayout = new QVBoxLayout;
// tip 10.
leftLayout->addLayout(topLeftLayout);
leftLayout->addWidget(caseCheckBox);
leftLayout->addWidget(backwardCheckBox);

QVBoxLayout* rightLayout = new QVBoxLayout;
rightLayout->addWidget(findButton);
rightLayout->addWidget(closeButton);
// tip 11.
rightLayout->addStretch();

QHBoxLayout* mainLayout = new QHBoxLayout;
mainLayout->addLayout(leftLayout);
mainLayout->addLayout(rightLayout);
// 回想之前在 1.4.1 中对 QWidget 顶层窗口使用 setLayout
setLayout(mainLayout);

// 和 1.4.1 中一样,这里是 QWidget 派生来的方法
setWindowTitle(tr("Find"));
// tip 12.
setFixedHeight(sizeHint().height());
}
  • tips 9. 此处的布局划分的方式如下图所示:

    这样的划分思路很类似 HTML 的设计框架布局,先划分大的区域,再根据功能或对齐位置逐个 “切开”

  • tips 10. QLayout 类的对象都有 addLayout(QLayout* ptr),与 addWidget(QWidget* ptr) 类似;前者可以将布局嵌套布局,形成更复杂的结构;

  • tips 11. QLayout 类中的 addStretch() 方法,如 tip9 中的图片中的 “分隔符” 的作用,用来撑开当前的 Layout,与同级的 Layout 高度或宽度对齐适应,同时使之前加入布局管理器的 widget 更加紧凑;

  • tips 12. setFixedHeight(int h)QWidget 类的方法,可以设定一个固定的 widget 高度,QWidget::sizeHint() 可以计算当前 widget 中各个布局管理器中各子 widget 默认 size,从而得出比较适宜的高度;

写完构造函数,无需写析构函数。因为:Qt 在删除父对象时,会自动删除所有子 widget 和 子布局;

下面定义之前声明的私有槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void findDialog::findClicked() {
// 读取当前 lineEdit 中的字符串到 text 中
QString text = lineEdit->text();
// tips 13.
Qt::CaseSensitivity cs =
caseCheckBox->isChecked() ? Qt:CaseSensitive : Qt:CaseInSensitive;
if (backwardCheckBox->isChecked())
// tips 14.
emit findPrevious(text, cs);
else emit findNext(text, cs);
}

void findDialog::enableFindButton(const QString& text) {
// 代码块 1 中的疑问,答案揭晓:tips 15.
findButton->setEnabled(!text.isEmpty());
}
  • tips 13. QCheckBox 具有方法 isChecked(),指示这个选择框有没有被选中;

  • tips 14. emit <function> 是 Qt 的关键字之一,表示向函数 function 发射信号

  • tips 15. 之所以要单独设计一个私有槽,是因为考虑到不仅仅是在输入变换的时候,使这个按钮处于 enable 状态,还要考虑在文本框为空的时候,使按钮再次 disable,而这需要额外的逻辑设计;这里就是 !text.isEmpty() 这个方法;

    整体逻辑:如果改变了文本,就调用 enableFindButton 槽;如果文本为空,就 disable “Find” 按钮;

这下将所有的部件放在一起:

1
2
3
4
5
6
7
8
9
#include<QApplication>
#include "findDialog.h"

int main(int argc, char* argv[]) {
QApplication app(argc, argv);
findDialog* dialog = new findDialog;
dialog->show();
return app.exec();
}

⚠注意:因为这里没有实际应用场景,所以以上代码的信号 findPrevious(str, cs)findNext(str, cs) 暂时没有应用,以后继续补充

2.1.2 进一步了解信号-槽机制

  • 槽(slot):和普通 C++ 成员函数几乎一模一样:可以是虚函数、可以被重载、可以是 public/protected/private、可以被其他 C++ 成员函数之间调用、参数可以是任意类型;唯一不同的就是槽可以和信号连接在一起,只要 emit 了对应的信号,就会自动调用这个槽;

    当普通 C++ 函数变成槽调用时,一般会忽略原本的返回值

  • 信号 - 槽的连接的函数:QObject::connect(QObject* sender, SIGNAL(signal), QObject* receiver, SLOT(slot))

    其中宏 SIGNAL()SLOT() 会将它们的参数转换成相应的字符串(暂时不必了解这些字符串的结构);

  • 信号-槽连接的要求:要想信号和槽成功连接,它们的参数必须有相同的顺序和相同的类型

    有一种情况例外:信号的参数多于槽的参数,但对应的参数类型相同(这样多余的参数会被简单地忽略掉)

    1
    2
    connect(ftp, SIGNAL(rawCommnadReply(int, const QString&)),
    this, SLOT(checkErrorCode(int)));
  • 信号-槽连接的特性

    1. 一个信号可以连接多个槽

      1
      2
      3
      4
      5
      // 在这个例子中,如果信号 slider 被 emit,那么会以不确定的顺序一个接着一个调用这些槽(setValue(int) 和 updateStatusBarIndicator(int),可以不止两个)
      connect(slider, SIGNAL(valueChanged(int)),
      spinBox, SLOT(setValue(int)));
      connect(slider, SIGNAL(valueChanged(int)),
      this, SLOT(updateStatusBarIndicator(int)));
    2. 多个信号可以连接一个槽

      1
      2
      3
      // 无论发射其中的哪一个信号,都会调用这个槽
      connect(lcd, SIGNAL(overflow()), this, SLOT(handleMathError()));
      connect(calc, SIGNAL(divisionByZero()), this, SLOT(handleMathError()));
    3. 一个信号可以连接另一个信号

      1
      2
      3
      // emit 第一个信号,就会触发 emit 第二个信号
      connect(lineEdit, SIGNAL(textChanged(const QString&)),
      this, SIGNAL(updateRecord(const QString&)));
    4. 连接可以被移除:这种情况应用较少,因为 Qt 在移除对象时,会自动移除和对象相关的所有连接

      1
      2
      disconnect(lcd, SINGAL(overflow()),
      this, handlerMathError());
    5. 信号-槽不仅可以应用在图形化界面的编写中,在哪怕不是为了设计 GUI,在类中声明宏 Q_OBJECT 也可以实现信号-槽机制

2.1.3 Qt 的元对象编译器 moc 和 元对象系统

在我一开始尝试写一些基本的程序的时候,一直很疑惑,Qt 的宏 slotssignalQ_OBJECT 究竟是什么?因为我不理解这其中的原理,所以犯过一个低级的错误——将声明成信号(signal)的函数加以定义,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
// File: XXX.h
class XXX {
// ...
signal:
void signalA(Type1 p1, Type2 p2);
// ...
};

// File: XXX.cpp
void XXX::signalA(Type1 p1, Type2 p2) {
// 某些代码逻辑
// ...
}

这样写实际上会在 make 编译是报 multiple definition 的错误,我正纳闷,为啥会 “重复定义” 呢?再一看报错的信息:重复定义的位置位于 moc_XXX.cpp ,我再想,我也没有写过 moc_XXX.cpp 呀?于是就引出了 Qt 中相当重要的概念:moc 元对象编译器

Qt 的主要成就之一就是使用一种机制对 C++ 进行了扩展,并且使用这种机制创建了软件组件;

这种机制叫做 “元对象系统(meta-object system)”,它提供了关键的 2 项技术:信号-槽机制内省(introspection)

内省功能对于实现信号和槽是必需的,还允许开发人员获得有关 QObject 子类的 “元信息(meta-information,包含一些类名和它所支持的信号-槽列表)”,还支持 Qt 设计师属性(下一节将提到)和文本翻译(之前所说的 tr()),为 QScript 模块奠定基础(不过目前接触不到);

但以上提到的这些,标准 C++ 没有,这意味着用普通的 C++ 编译器一定没法实现;所以 Qt 引入了新的编译器:moc 元对象编译器;

因此,Qt 整个编码到运行的工作流程是:

  1. qmake 效仿 cmake ,以平台无关的方式指定了程序编译所需的库,这里包含了标准 C++ 所没有的 Qt 的库;最后生成了普通的 Makefile
  2. moc 元对象编译器一边识别 Qt 特定的宏或关键字(例如 QObjectslotssignal),添加特定内容(例如自动实现信号函数),一边和普通 C++ 编译器一样,编译链接源文件;
  3. moc 元对象编译器在编译时还会补充 QObject 的 内省函数,完成特殊的触发工作;

以上内容一般很少需要开发者去考虑,都封装在 qmakemocQObject内部;如果感兴趣,可以阅读有关 QMetaObject 文档,或者是前面提到的 moc 自动生成的 C++ 源码 moc_XXX.cpp/h

2.2 Qt Designer:UI 快速设计

在上面一些纯代码设计的例子中,我们会发现 GUI 的设计遵循一些基本的规律定式:

  1. 创建、初始化(例如设置文本内容)子窗口部件;
  2. 将 widget 放置到布局中;
  3. 设置 Tab 键顺序;
  4. 建立信号 — 槽连接;
  5. 实现自定义槽;

现在,可以使用 Qt Designer 将图形化设计的一部分(指前三步)交给图形界面;

2.2.1 Qt Designer 的基本使用

本部分将介绍 Qt Designer 如何设计基本 UI 界面,完成上面所提到的 前3步,同时回顾之前所学到的方法

以上面的窗体为目标设计一个窗口类;

  • 打开 Qt Designer:在进行此步前,建议按之前的方法先创建一个 Qt 项目;

    • IDE 用户可以在 Qt Creator 中右击创建 Qt 设计师文件文件名很重要,将要作为这个窗体的变量名,需要记住,下面提到),在左边栏的列表中直接双击打开 *.ui
    • 非 IDE 用户可以直接进入 Qt Designer 按所需模板新建一个 ui 文件(文件名很重要,需要记住,下面提到);
  • 创建、初始化子窗口部件 和 部分常用属性

    1. text 属性:大部分组件的显示内容(还记得之前 QPushButtonQLabel 的初始化参数吗?),拖动出来双击就可以编辑;
    2. objectName 属性:这个名字建议自己设置,需要记住,因为这是控件的变量名,之后设计信号-槽时需要用到
    3. default 属性:记得之前的方法 QWidget::setDefault 吗?这就是它的图形化;
    4. enabled 属性(QPushButton):相当于在创建 widget 的同时指定 btn->setEnabled(bool)
    5. windowTitle 属性(QMainWindow,这里点击窗体在右边栏就能搜到):相当于 win->setWindowTitle(str)
  • Qt Designer 设计模式

    • Edit Widgets 模式:默认模式,可以直接编辑上述部件及其属性,在程序顶部“Edit”菜单可以点击进入;
    • Edit Buddies 模式:点击顶部菜单栏相应按钮进入。此模式下,点击控件并拖到另一个部件上可以完成部件伙伴的设置,就是之前设置的 widget1->setBuddy(widget2)
    • Edit Tab Order 模式:点击顶部菜单栏相应按钮进入。此模式下可以设置 Tab 键顺序
  • Qt Designer 中的布局设置

    • 方法1:使用左边栏的 Layout 控件;
    • 方法2:按住 CTRL 选中一些 widget,点击顶部菜单栏中的 Lay out Vertically/Horizontally

    注:在布局中加入左边栏中的 Spacer 就等价于之前设置的 layout->addStretch()

  • Qt Designer 中的窗口大小设计

    可以点击顶部菜单栏中的 Adjust size(调整大小),可以自动将窗体大小定义为最佳形式(等价于之前的 setFixedHeight(sizeHint().height())

2.2.2 Qt Designer 的运行原理【重要】

说了这么多 Qt Designer 的基本使用,那么它是怎么将 图形界面中设计的 UI 转换为之前的纯代码,并交给 moc 编译器 和 C++ 编译器的呢?

细心的同学可能以文本形式打开过 *.ui ,会发现里面的格式是 XML 文件格式,那么它又是如何转化为 *.h/cpp 的呢?下面先从非 IDE 用户的视角讲述,IDE 用户也建议看一下,因为 Qt Creator IDE 的自动操作比较奇怪,可能不好理解

以下的案例以名为 myDialog 的主窗口 MainWindow 的设计为例;

非 IDE 用户的视角

首先,我们向项目中导入这个 myDialog.ui 文件(创建文件并 qmake -project,即前面的要点🔗);

你会发现,qmake 自动更新了 pro 文件:FORM += myDialog.ui(不用自己写);

紧接着运行 qmake myDialog.pro 生成 Makefile 的同时,qmake 智能识别 myDialog.ui,会在 Makefile 中加入配置规则 调用 Qt 的新的一种编译器,这不是 GCC,也不是 moc,而是 Qt 用户界面编译器(user interface compiler,uic);它会将 myDialog.ui 转换为 C++ 代码存储于 ui_myDialog.h 中;

ui_myDialog.h 会生成一个类,类名是 myDialog位于 Ui 命名空间(命名空间 Ui 是 Qt 中用于存放各种 UI 类 的命名空间,通常存放在里面是一种规范)

注意:这里 ui 文件名 XXX.ui、生成的 ui_XXX.h 中的 XXX、生成的类名 XXX 应该是一个名字!!!

不建议轻易修改,不然有可能在下次编译时,编译器找不到相应组件;

这也是为什么之前提醒 “创建 *.ui 文件的文件名很重要”;

ui_myDialog.h 中自动生成的类看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once
// #include something

QT_BEGIN_NAMESPACE // Qt 独有的宏,将其中的类加入特定的namespace中

class ui_myDialog { // 生成类,由于它谁都不继承,功能少,通常作为中间类使用
public:
// Widgets
// ...
void setupUi(QMainWindow* myDialog) {
// realize ...
}
};

QT_END_NAMESPACE

namespace Ui {
class myDialog: public ui_myDialog {}; // Qt 自动在 Ui 命名空间中
} // 定义了继承于 ui_myDialog 的类 myDialog

而真正想要应用这个窗口类,需要进行多继承,使用它和 QMainWindow 的子类——毕竟这个类不是 QObject,没有办法完成信号-槽的创建

所以一般情况下将 ui_myDialog 类作为中间类,再手动为这个窗口创建 myDialog.hmyDialog.cpp,分别书写:

1
2
3
4
5
6
7
8
9
10
11
12
13
// File: myDialog.h
#pragma once
#include <QMainWindow>
#include "ui_myDialog.h"

// 请注意!!!这里的 myDialog 和之前定义在 ui_myDialog.h 文件中 Ui 命名空间中的类 myDialog 不一样!
// 这里的 myDialog 和 Ui::myDialog 类进行了继承,使 myDialog 具有了 Ui::myDialog 类一样的控件作为属性
// 同时 myDialog 还和 QMainWindow 进行多继承,使 myDialog 还具有 QMainWindow的属性
class myDialog : public QMainWindow, public Ui::myDialog {
Q_OBJECT // 为何需要继承 Ui::myDialog,组合不行吗?不行。
public: // 因为需要修改信号-槽连接,涉及其中的控件
explicit myDialog(QWidget* parent = nullptr);
};
1
2
3
4
5
6
7
8
9
10
11
// File: myDialog.cpp
#include "myDialog.h"

// 由于 myDialog 是 QMainWindow 的子类,因此想要重用它的属性,
// 需要委托调用父类的构造函数:QMainWindow(parent);
// 此外,setupUi(this) 是以当前窗口为顶级控件,按 UI 设计部署控件
myDialog::myDialog(QWidget* parent)
: QMainWindow(parent)
{
setupUi(this);
}

至此,一个只有图形界面、没有添加 槽-函数 连接的主窗口类 myDialog 就设计完成了;

提示:除了上面的继承方法,还可以把 Ui::myDialog 作为 myDialog 的一个数据成员使用。

IDE 用户视角

事实上,使用 Qt Creator 的用户在一开始,向项目中添加 UI 设计师文件,IDE 会提示用户起名的时候,就会同时创建 myDialog.uimyDialog.hmyDialog.cpp 三个文件,并更新 <项目名称>.pro 文件,直接省去非 IDE 方法中所有步骤;

值得一提的是,Qt Creator 在编译时生成的 ui_myDialog.h 不在项目目录中(也许是考虑到相关性),而藏在上层 build_XXX 目录里,不过使用的时候也无需注意,因为引入工作已经在自动生成的 myDialog.h 中写好了;

这下关于 Qt Designer 的运行机制、IDE 封装的机制是不是更清楚了呢?

2.2.3 案例演示

本节将一步步地完成 2.2.1 中的窗体设计目标;将以非 IDE 的方式完成(IDE 的操作简单就不演示了)(注意,它的角色是子窗口

  1. 创建一个项目目录:新建项目文件夹 testUI,创建文件 main.cppGoToCellDialog.cppGoToCellDialog.h

  2. 打开 Qt Designer,选择 Dialog without button 模板,按照图中要求设计出 UI,窗体命名为 GoToCellDialogobjectName),保存文件为 GoToCellDialog.ui,记得保存在项目目录中;

  3. 命令行切换至项目目录,新建目录 build(为了让项目目录更干净,build 就设置在项目目录里面,你也可以设置在其他地方,比如上层目录——Qt Creator IDE 就是这么干的),命令行切入,运行 qmake -project ../,向 生成的 testUI.pro 中添加 QT += widgets

  4. 编写 Go to Cell 窗体的主要逻辑代码(包括信号-槽的定义):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // File: GoToCellDialog.h
    #pragma once
    #include <QtWidgets/QDialog>
    #include "ui_GoToCellDialog.h"

    class GoToCellDialog : public QDialog, public Ui::GoToCellDialog {
    Q_OBJECT
    public:
    GoToCellDialog(QWidget* parent = nullptr);

    // 自定义槽
    private slots:
    // 注意:这么命名是有讲究的!!!
    // 在 uic 和 moc 编译时,会识别所有 on_<objectName>_<signalName>() 命名的函数,自动连接:
    // connect(lineEdit, SIGNAL(textChanged(const QString&)),
    // this, SLOT(on_lineEdit_textChanged()));
    void on_lineEdit_textChanged();

    };
    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
    // File: GoToCellDialog.cpp
    #include <QtGui>
    #include "GoToCellDialog.h"

    // 这个函数中的 widget 变量名就是之前你在 GoToCellDialog.ui 设计中的 objectName
    GoToCellDialog::GoToCellDialog(QWidget* parent)
    : QDialog(parent)
    {
    setupUi(this); // 以当前对象为父 widget 初始化窗体部件

    QRegExp reg("[a-zA-Z][1-9][0-9]{0,2}"); // 正则表达式类
    // 新方法:为 QLineEdit 类设置正则可接受检验器
    // QRegExpValidator 的构造函数 第一个参数是 QRegExp(正则Pattern)
    // 第二个参数是 parent,使 QRegExpValidator 对象成为 parent 的
    // 子控件,这样就不要手动 delete,在父控件析构时,子控件一起析构了(之前提过)
    lineEdit->setValidator(new QRegExpValidator(reg, this));

    // 设置信号-槽
    // 这里的 accept() 和 reject() 槽是 QDialog 的固有槽,
    // 触发这两槽之一都会关闭窗口,但是分别会修改:
    // QDialog::Accepted 和 QDialog::Rejected 数据成员的值,
    // 以便主窗口判断用户执行了什么操作
    connect(okBtn, SIGNAL(clicked()), this, SLOT(accept()));
    connect(cancelBtn, SIGNAL(clicked()), this, SLOT(reject()));
    }

    // 这里是指,当文本框改变,就进入这个函数
    void GoToCellDialog::on_lineEdit_textChanged()
    {
    // 如果经过 QRegExpValidator 检查符合,那么激活 okBtn,否则禁用
    okBtn->setEnabled(lineEdit->hasAcceptableInput());
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // File: main.cpp
    #include <QApplication>
    #include "GoToCellDialog.h"

    int main(int argc, char* argv[]) {
    QApplication app(argc, argv);
    GoToCellDialog* dialog = new GoToCellDialog;

    dialog->show();

    return app.exec();
    }
  5. 执行 qmake testUI.pro

  6. Windows 用户请按照之前所说的,在当前命令行运行 配置临时环境变量的脚本🔗

  7. Unix 用户 执行 make,Windows 用户执行 mingw32-make,构建完成;

如此一来,一个子窗口类的演示就做好了;

Chapter 3 Qt:样式更丰富的子窗口

前面几章,只是零碎地介绍基本编写方法;

在 1.4 中,初步学习了 面向过程的简单主窗口纯代码设计;

在 2.1 中,初步学习了 面向对象的简单子窗口纯代码设计;

在 2.2 中,初步学习了 面向对象的简单子窗口快速 UI 设计;

本章将介绍更多其他样式的子窗口的设计;

3.1 扩展对话框

本节技术栈并没有拓展,还是之前的 Qt Creator、Qt Designer 使用技术;

此处仅会提及新出现的控件属性或方法等信息;

  • QPushButton 的属性 checkable :如果修改为 true,则在用户点击一下后持续有效(相当于 checkBox),再次点击才会还原;
  • QPushButton 的槽 toggled(bool):当按钮 enabled 属性被改变时,toggle 会发射信号,参数就不用说了吧,,这个槽在按钮为 checkable 时有用;
  • QPushButton 的槽 setText(QString):可以在中途改变按钮的文本;
  • QGridLayout 布局管理器:在 1.4.3 中介绍过,如果发现按钮较多,而且摆不整齐的时候可以尝试这个布局,它可以使控件按照行、列的规则摆放;
  • 有些人会疑惑水平/竖直分隔符(spacer)有什么用,其实它就像 Qt Designer 上画的一样,用来在窗口伸缩时,调节控件之间的位置关系的;
  • 在 2.2.1 中,其实还有一种 Qt Designer 设计模式没有介绍到:Edit Signals/Slots,在此模式下可以直接编辑信号-槽连接,无需手动写 connect 函数;使用方法 和 Edit Buddy 模式类似,感兴趣可以尝试一下;
  • 新的类 QGroupBox 组群盒:如上图,就是那一个个小方框;
  • 大多数 Widget 都有一个槽:QWidget::setVisible(bool),可以理解为含参数、可重用的 QWidget::close()
  • 快捷复制:按住 CTRL,单击要复制的控件,再拖动就能复制了 ~
  • 新的类 QComboBox 下拉栏选择器
    • 具有方法 clear(),常用在初始化时,清空选项;
    • 具有方法 addItem(QString),添加下拉栏内容,一般在 Qt Designer 里添加,也可自己在代码里写;
    • 具有方法 setMinimumSize(int),设置下拉栏的最小大小值;
  • 新的类 QChar 字符类

    • 具有方法 unicode():转化为 unicode 码,可以运算;
    • 可以作为 QString 的初始化参数;
  • 设置窗口固定尺寸的常用方法:layoutName->setSizeConstraint(QLayout::SetFixedSize)

1
2
3
4
5
6
7
8
9
10
11
12
13
// File: SortDialog.h
#pragma once

#include "ui_SortDialog.h"
#include <QtWidgets/QDialog>

class SortDialog : public QDialog, public Ui::SortDialog {
Q_OBJECT
public:
SortDialog(QWidget* parent = nullptr);
// Initialize the content of each column comboBox
void setColumnRange(QChar first, QChar last);
};
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
// File: SortDialog.cpp
#include "SortDialog.h"
#include <QtGui>

SortDialog::SortDialog(QWidget* parent)
: QDialog(parent) {
setupUi(this);
// Hide the group box first,
// Because the setVisible(bool) function hasn't been called.
secondaryGroupBox->hide();
tertiaryGroupBox->hide();
// Fix the size of the window
layout()->setSizeConstraint(QLayout::SetFixedSize);

setColumnRange('A', 'Z');
}

void SortDialog::setColumnRange(QChar first, QChar last) {
// Clear the content of each comboBox
primaryColCombo->clear();
secondaryColCombo->clear();
tertiaryColCombo->clear();

// Optional
secondaryColCombo->addItem(tr("None"));
tertiaryColCombo->addItem(tr("None"));

// 由于 primaryColCombo 中显示单字符时,展示的宽度小于
// secondary 和 tertiary ColCombo 显示的 “None”,所以设置
// primaryColCombo 的最小宽度,防止宽度不相同的现象
primaryColCombo->setMinimumSize(secondaryColCombo->sizeHint());

QChar ch = first;
while (ch <= last) {
primaryColCombo->addItem(QString(ch));
secondaryColCombo->addItem(QString(ch));
tertiaryColCombo->addItem(QString(ch));
ch = ch.unicode() + 1;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// File: main.cpp
#include <QApplication>
#include "SortDialog.h"

int main(int argc, char* argv[]) {
QApplication app(argc, argv);
SortDialog* dialog = new SortDialog;
// 测试函数是否能用
dialog->setColumnRange('C', 'F');
dialog->show();

return app.exec();
}

编译运行项目即可;

3.2 Qt 内置的更多部件和对话框

这里仅作初步介绍,在完整项目的应用中会进一步介绍使用方法,因为一次性看完很可能记不住……

  • Qt 中的按钮类

    • QPushButton:之前演示的普通按钮;
    • QToolButton:具有图标的功能按钮;
    • QCheckBox:复选框类;
    • QRadioButton:单选框类,只能在一组中选一个激活;

  • Qt 中的单页容器部件

    • QGroupBox:之前演示的群组框;
    • QFrame:QLabel 的父类,可以用来展示图片、文字等信息(所以 QLabel 也行);
  • Qt 中的多页容器部件

    • QTabWidget:切换多个 Tab 的窗口控件;
    • QToolBox:切换不同工具分类的窗口控件;

  • Qt 中的显示窗口部件:QLabelQLCDNumberQProgressBarQTextBrowser

    注:QTextBrowser 是只读的 QTextEdit 子类,也可以显示带格式的文本,建议处理大型格式化文本,因为它和 QLabel 不同,可以在必要时自动提供滚动条;

  • Qt 中的输入窗口部件:QSpinBoxQDoubleSpinBoxQComboBoxQDateEditQTimeEditQDateTimeEditQScrollBarQSliderQTextEditQLineEditQDial

    注:QTextEdit 支持输入掩码、检验器(2.2.3 已演示)等功能;

  • Qt 的反馈对话框:QInputDialogQProgressDialogQMessageBoxQErrorMessageQColorDialogQFontDialogQFileDialogQPrintDialog 等;

3.3 Qt 类的第二次总结 & 下文预告

学完了以上的知识,目前使用到的 Qt 类的框架如下图所示:

同系列下一篇文章预告:将会是 第一个完整的 Qt 入门项目(会非常地长,比本篇还长),目的是通过实战来学习 Qt 的更多类的用法,源代码和程序 届时会放在仓库,以供读者参考。