从C++入门Qt(一)
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 | // 直接把 HELLO 定义为一串代码 |
0.3 Qt 环境配置
Unix 系统:不是安装完就能在命令行里用了吗?
Windows 系统:在下载安装的 Qt 目录中找到编译器文件夹(安装时应该提醒你设置过了),把编译器目录下 bin
文件夹目录添加到 用户/系统环境变量 Path
中;
Chapter 1 Qt 初认识
1.1 简单示例
1 |
|
widget:窗口部件,指用户界面中的一个可视化元素,相当于 Windows 中的控件、容器。按钮、菜单、滚动条、框架等都是窗口部件。
tips 1. 绝大多数应用程序会使用
QMainWindow
或QDialog
作为窗口;而且在 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 类(和操作系统环境无关):
使用 Qt Creator 的用户(即在官网安装 Qt Creator IDE 及 全套 Qt 运行环境的用户),以后称这类用户为 “IDE 用户”;
这很像 C++ 使用 VS、VSCode、CLion 等 IDE(集成开发环境) 一键编译运行的用户;
使用单独的 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++编译器的环境变量:
和 Qt 库配套的 C++ 编译器目录。如果你安装了 Qt Creator,那么在安装时应该顺带让你设置并安装了对应版本的 minGW 编译器,它的位置和 Qt 是放在一起的;
通常位置是:
<Qt安装根目录>\Tools\mingw<版本号>\bin
;例如本人的位置:
D:\Qt5.14.2\Tools\mingw730_64\bin
;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 库:如果这个项目除了
QtCore
、QtGui
(默认包含) 以外,还想添加额外的 Qt 库,例如常用的QtWidgets
,QtNetwork
,那么在*.pro
文件的合适位置添加:QT += widgets
、QT += network
;想要了解更详细的
*.pro
文件的编写规则,请查阅官方文档;不过除了添加 Qt 库,其他应该很少会直接修改
pro
文件,例如引入项目文件就不用:- IDE 用户可以在 Qt Creator 左侧文件栏右击添加文件,会自动更新 pro 文件;
- 非 IDE 用户只需在相同文件夹下重新运行
qmake -project
即可更新 pro 文件;
编译工程:使用
qmake *.pro
将一般项目文件编译为与平台相关的makefile
文件;最后运行make
直接编译即可;qmake *.pro
的过程有点像 CMake 对照 CMakeLists.txt 生成 Makefile 的过程;注:Windows 下稍微麻烦一点,在项目目录下:
运行
qmake *.pro -spec win32-g++ "CONFIG+=debug" "CONFIG += qml_debug"
运行
mingw32-make.exe
,这就相当于 Unix 系统下的make
;
⚠ 当你在生成
makefile
后,又向程序中加入一些新的包或函数,那么可能需要再次运行qmake
来生成 新的makefile
,以防编译器无法找到新文件;有同学可能会问,能不能不用
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++ 项目构建的不同之处:
【必要】添加 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)【必要】如果 你在 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 默认加入的
Gui
、Core
库需要在 CMakeLists 中手动加入:1
find_package(Qt5 COMPONENTS Core Gui REQUIRED)
而且在最后还要手动链接库:
1
target_link_libraries(exeName PRIVATE Qt5::Widgets Qt5::Core Qt5::Gui)
【注意】:在 CMake 中,由于之前添加了专属编译器,所以
*.ui
和*.h/cpp
一样,都需要在add_executable
或add_library
构建目标时,作为源文件加入进去;
最后,以一个示例项目为例子(不同的项目没法照抄哦~):
1 | CMAKE_MINIMUM_REQUIRED(VERSION 3.12) |
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 |
|
1.4 Qt 窗口的布局设计
1.4.1 widget 间的父子关系
当需要在一个窗口中,合理地安排各种 widget 的摆放时,需要考虑这些 widget 间的层次关系;
Qt 中,和其他类的 GUI 设计库类似的做法是,引入 widget 间的父子关系;表示:A 是 B 的子控件 就可以理解为 A 是布局在 B 上的 控件;
比如,想要制作如上图的应用界面,就需要遵循这样的步骤(仅供参考,其他方法也能实现):
在窗口最顶层设置一个
QWidget
类对象的抽象的 widget,用来盛放其他 widget,可以理解现实中的一个 “桌布”;以
QWidget
类对象为父控件(QWidget 自己没有父控件,它就是顶层窗口),设置QSpinbox
和QSlider
对象(分别是 微调框 控件类、滑动条 控件类);一般情况下,QWidget 及其子类设置父控件的方法是通过布局管理器实现;理解为“用布局管理器打包在一起”;
绑定内部信号-槽的关联;
最后使用布局管理器将子控件按指定“摆放方式”显式加入父控件,显示顶层 widget 即可;
实现如下:
1 |
|
1.4.2 布局设计的意义
有同学会问,为什么需要 layout?layout 可以让多个 widget 按想要的方式排列在一个窗口上;如果不这么做,就没法定义摆放方式了!你可以试一试,不用布局管理器,你会发现两个或多个 widget 是分布在不同的窗口下的;
1.4.3 布局管理器的类型
除了以上例子介绍的 QHBoxLayout
类,还有 QVBoxLayout
、QGridLayout
,其作用分别是:
QHBoxLayout
:默认在水平方向,从左到右排列 widget;QVBoxLayout
:默认在竖直方向,从上到下排列 widget;QGridLayout
:将 widget 排列在预设的网格中;
常见的使用方法:先声明、在设置属性,最后添加打包到布局管理器中,设置给父控件;
它们都继承于 QLayout
,所以它们不是 widget(QWidget
),一般也不可见;
1.5 章末贴士
重要:一定要会使用官方文档;
有些同学会想,里面的命令行参数
argc
和argv
究竟可以做什么?其实,举个例子就明白了,其中一个用途是设置应用界面的主题,即:./应用名 -style <style name>
,常用的style name
有:plastique
、Cleanlooks
、CDE
、Motif
、Windows
、Windows XP
、Windows Vista
、Mac
;本章涉及到的类和一些方法的总结
和 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
// …… (后面一毛一样)
// 记得在 *.pro 中移除:QT += widgets最后强调一下, Qt 4 到 5、Qt 5 到 6 的很多操作都改变了,所以别轻易更换项目的 Qt 大版本!
Chapter 2 面向对象的 Qt
2.1 纯代码设计
2.1.1 示例:以简单对话框为例
想象一下,这是一个庞大应用程序的一个小部分对话框,现在想要单独设计它。但是第一章的代码都写在一个 main 函数中,如果窗口一多,不仅不利于维护,而且容易编写错误;
所以我们从现在开始采用 C++ 中的不同类来编写不同窗口,可以形成很好的封装性,增强可读性;
下面将这个窗口编写为一个类 findDialog
;
1 | // file: findDialog.h |
这是窗口的实现 cpp:
1 | // file: findDialog.cpp |
tips 1. 头文件
QtGui.h
包含了 Qt GUI 类的QtCore
和QtGui
模块的所有类的定义;回顾一下 Qt 的主要模块:
QtCore
、QtGui
、QtNetwork
、QtOpenGL
、QtSql
、QtSvg
、QtXml
;- 同学会问,为什么不在
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. 这里
QDialog
的close()
方法继承于QWidget
类,默认行为是将 widget 隐藏起来(而非删除),这和QApplication
类的quit()
方法不一样,quit()
方法是关闭并删除窗口及其上的所有布局、widget;
1 | // 上接上一个代码块 -------------------------- |
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 | void findDialog::findClicked() { |
tips 13.
QCheckBox
具有方法isChecked()
,指示这个选择框有没有被选中;tips 14.
emit <function>
是 Qt 的关键字之一,表示向函数function
发射信号;tips 15. 之所以要单独设计一个私有槽,是因为考虑到不仅仅是在输入变换的时候,使这个按钮处于 enable 状态,还要考虑在文本框为空的时候,使按钮再次 disable,而这需要额外的逻辑设计;这里就是
!text.isEmpty()
这个方法;整体逻辑:如果改变了文本,就调用
enableFindButton
槽;如果文本为空,就 disable “Find” 按钮;
这下将所有的部件放在一起:
1 |
|
⚠注意:因为这里没有实际应用场景,所以以上代码的信号 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
2connect(ftp, SIGNAL(rawCommnadReply(int, const QString&)),
this, SLOT(checkErrorCode(int)));信号-槽连接的特性
一个信号可以连接多个槽:
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)));多个信号可以连接一个槽:
1
2
3// 无论发射其中的哪一个信号,都会调用这个槽
connect(lcd, SIGNAL(overflow()), this, SLOT(handleMathError()));
connect(calc, SIGNAL(divisionByZero()), this, SLOT(handleMathError()));一个信号可以连接另一个信号:
1
2
3// emit 第一个信号,就会触发 emit 第二个信号
connect(lineEdit, SIGNAL(textChanged(const QString&)),
this, SIGNAL(updateRecord(const QString&)));连接可以被移除:这种情况应用较少,因为
Qt
在移除对象时,会自动移除和对象相关的所有连接:1
2disconnect(lcd, SINGAL(overflow()),
this, handlerMathError());信号-槽不仅可以应用在图形化界面的编写中,在哪怕不是为了设计 GUI,在类中声明宏
Q_OBJECT
也可以实现信号-槽机制;
2.1.3 Qt 的元对象编译器 moc 和 元对象系统
在我一开始尝试写一些基本的程序的时候,一直很疑惑,Qt 的宏 slots
、signal
、Q_OBJECT
究竟是什么?因为我不理解这其中的原理,所以犯过一个低级的错误——将声明成信号(signal
)的函数加以定义,像这样:
1 | // File: XXX.h |
这样写实际上会在 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 整个编码到运行的工作流程是:
qmake
效仿cmake
,以平台无关的方式指定了程序编译所需的库,这里包含了标准 C++ 所没有的 Qt 的库;最后生成了普通的Makefile
;moc
元对象编译器一边识别 Qt 特定的宏或关键字(例如QObject
、slots
、signal
),添加特定内容(例如自动实现信号函数),一边和普通 C++ 编译器一样,编译链接源文件;moc
元对象编译器在编译时还会补充QObject
的 内省函数,完成特殊的触发工作;
以上内容一般很少需要开发者去考虑,都封装在 qmake
、moc
、QObject
内部;如果感兴趣,可以阅读有关 QMetaObject
文档,或者是前面提到的 moc
自动生成的 C++ 源码 moc_XXX.cpp/h
;
2.2 Qt Designer:UI 快速设计
在上面一些纯代码设计的例子中,我们会发现 GUI 的设计遵循一些基本的规律定式:
- 创建、初始化(例如设置文本内容)子窗口部件;
- 将 widget 放置到布局中;
- 设置 Tab 键顺序;
- 建立信号 — 槽连接;
- 实现自定义槽;
现在,可以使用 Qt Designer 将图形化设计的一部分(指前三步)交给图形界面;
2.2.1 Qt Designer 的基本使用
本部分将介绍 Qt Designer 如何设计基本 UI 界面,完成上面所提到的 前3步,同时回顾之前所学到的方法;
以上面的窗体为目标设计一个窗口类;
打开 Qt Designer:在进行此步前,建议按之前的方法先创建一个 Qt 项目;
- IDE 用户可以在 Qt Creator 中右击创建
Qt 设计师文件
(文件名很重要,将要作为这个窗体的变量名,需要记住,下面提到),在左边栏的列表中直接双击打开*.ui
; - 非 IDE 用户可以直接进入 Qt Designer 按所需模板新建一个
ui
文件(文件名很重要,需要记住,下面提到);
- IDE 用户可以在 Qt Creator 中右击创建
创建、初始化子窗口部件 和 部分常用属性
text
属性:大部分组件的显示内容(还记得之前QPushButton
、QLabel
的初始化参数吗?),拖动出来双击就可以编辑;objectName
属性:这个名字建议自己设置,需要记住,因为这是控件的变量名,之后设计信号-槽时需要用到;default
属性:记得之前的方法QWidget::setDefault
吗?这就是它的图形化;enabled
属性(QPushButton
):相当于在创建 widget 的同时指定btn->setEnabled(bool)
;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 |
|
而真正想要应用这个窗口类,需要进行多继承,使用它和 QMainWindow
的子类——毕竟这个类不是 QObject
,没有办法完成信号-槽的创建;
所以一般情况下将 ui_myDialog
类作为中间类,再手动为这个窗口创建 myDialog.h
和 myDialog.cpp
,分别书写:
1 | // File: myDialog.h |
1 | // File: myDialog.cpp |
至此,一个只有图形界面、没有添加 槽-函数 连接的主窗口类 myDialog
就设计完成了;
提示:除了上面的继承方法,还可以把
Ui::myDialog
作为myDialog
的一个数据成员使用。
IDE 用户视角
事实上,使用 Qt Creator 的用户在一开始,向项目中添加 UI 设计师文件
,IDE 会提示用户起名的时候,就会同时创建 myDialog.ui
、myDialog.h
、myDialog.cpp
三个文件,并更新 <项目名称>.pro
文件,直接省去非 IDE 方法中所有步骤;
值得一提的是,Qt Creator 在编译时生成的 ui_myDialog.h
不在项目目录中(也许是考虑到相关性),而藏在上层 build_XXX 目录里,不过使用的时候也无需注意,因为引入工作已经在自动生成的 myDialog.h
中写好了;
这下关于 Qt Designer 的运行机制、IDE 封装的机制是不是更清楚了呢?
2.2.3 案例演示
本节将一步步地完成 2.2.1 中的窗体设计目标;将以非 IDE 的方式完成(IDE 的操作简单就不演示了)(注意,它的角色是子窗口)
创建一个项目目录:新建项目文件夹
testUI
,创建文件main.cpp
、GoToCellDialog.cpp
、GoToCellDialog.h
;打开 Qt Designer,选择
Dialog without button
模板,按照图中要求设计出 UI,窗体命名为GoToCellDialog
(objectName
),保存文件为GoToCellDialog.ui
,记得保存在项目目录中;命令行切换至项目目录,新建目录 build(为了让项目目录更干净,build 就设置在项目目录里面,你也可以设置在其他地方,比如上层目录——Qt Creator IDE 就是这么干的),命令行切入,运行
qmake -project ../
,向 生成的testUI.pro
中添加QT += widgets
;编写 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
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
// 这个函数中的 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
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
GoToCellDialog* dialog = new GoToCellDialog;
dialog->show();
return app.exec();
}执行
qmake testUI.pro
;Windows 用户请按照之前所说的,在当前命令行运行 配置临时环境变量的脚本🔗 ;
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 | // File: SortDialog.h |
1 | // File: SortDialog.cpp |
1 | // File: main.cpp |
编译运行项目即可;
3.2 Qt 内置的更多部件和对话框
这里仅作初步介绍,在完整项目的应用中会进一步介绍使用方法,因为一次性看完很可能记不住……
Qt 中的按钮类
- QPushButton:之前演示的普通按钮;
- QToolButton:具有图标的功能按钮;
- QCheckBox:复选框类;
- QRadioButton:单选框类,只能在一组中选一个激活;
Qt 中的单页容器部件
- QGroupBox:之前演示的群组框;
- QFrame:QLabel 的父类,可以用来展示图片、文字等信息(所以 QLabel 也行);
Qt 中的多页容器部件
- QTabWidget:切换多个 Tab 的窗口控件;
- QToolBox:切换不同工具分类的窗口控件;
Qt 中的显示窗口部件:
QLabel
、QLCDNumber
、QProgressBar
、QTextBrowser
注:QTextBrowser 是只读的 QTextEdit 子类,也可以显示带格式的文本,建议处理大型格式化文本,因为它和 QLabel 不同,可以在必要时自动提供滚动条;
Qt 中的输入窗口部件:
QSpinBox
、QDoubleSpinBox
、QComboBox
、QDateEdit
、QTimeEdit
、QDateTimeEdit
、QScrollBar
、QSlider
、QTextEdit
、QLineEdit
、QDial
注:QTextEdit 支持输入掩码、检验器(2.2.3 已演示)等功能;
Qt 的反馈对话框:
QInputDialog
、QProgressDialog
、QMessageBox
、QErrorMessage
、QColorDialog
、QFontDialog
、QFileDialog
、QPrintDialog
等;
3.3 Qt 类的第二次总结 & 下文预告
学完了以上的知识,目前使用到的 Qt 类的框架如下图所示:
同系列下一篇文章预告:将会是 第一个完整的 Qt 入门项目(会非常地长,比本篇还长),目的是通过实战来学习 Qt 的更多类的用法,源代码和程序 届时会放在仓库,以供读者参考。