一、控件概述
Widget 是 Qt 中的核心概念,英文原义是 “小部件”,此处也把它翻译为 “控件”。控件是构成一个图形化界面的基本要素。
像上述示例中的按钮、列表视图、树形视图、单行输入框、多行输入框、滚动条、下拉框都可以称为 “控件”。
Qt 作为一个成熟的 GUI 开发框架,内置了大量的常用控件。这一点在 Qt Designer 中就可以看出来,并且 Qt 也提供了 “自定义控件” 的能力,可以让我们在现有控件不能满足需求的时候,对现有控件做出扩展,或者手搓出新的控件。
所以,学习 Qt 其中一个很重要的任务就是熟悉并掌握 Qt 内置的常用控件,这些控件对于我们快速开发出符合需求的界面是至关重要的。
1、关于控件体系的发展
控件是 GUI 开发中的通用概念,不仅仅局限在 Qt 中。
(1)第一阶段
完全没有控件。此时需要通过一些绘图 API 手动的绘制出按钮或者输入框等内容,代码编写繁琐。(例如文曲星的 Lava 平台开发)
(2)第二阶段
只包含粗略的控件。只是提供了按钮、输入框、单选框、复选框等最常用的控件。(例如 html 的原生控件)
(3)第三阶段
更完整的控件体系,基本可以覆盖到 GUI 开发中的大部分场景。(例如早期的 MFC、VB、C++ Builder、Qt、Delphi、后来的 Android SDK、Java FX、前端的各种 UI 库等)
上图是前端中的 Element-ui 中的控件概览,无论是丰富程度还是颜值,都比 Qt 自带的控件更胜一筹。
二、QWidget 核心属性
在 Qt 中,使用 QWidget 类表示 “控件”,像按钮、视图、输入框、滚动条等具体的控件类,都是继承自 QWidget。QWidget 中包含了 Qt 整个控件体系中通用的部分。
在 Qt Designer 中,随便拖一个控件过来,选中该控件,即可在右下方可以看到 QWidget 中的属性。
这些属性既可以通过 QtDesigner 直接修改,也可以通过代码的方式修改。这些属性的具体含义在 Qt Assistant 中均有详细介绍。
在 Qt Assistant 中搜索 QWidget,即可找到对应的文档说明(或者在 Qt Creator 代码中选中 QWidget,按 F1(+Fn) 也可)
1、核心属性概览
下列表格列出了 QWidget 中的属性及其作用:
下面我会介绍上面列出的其中一些比较重要和常用的属性。
2、enabled
所谓 “禁用” 指的是该控件不能接收任何用户的输入事件,并且外观上往往是灰色的。如果一个 widget 被禁用,则该 widget 的子元素也被禁用。
使用代码创建一个禁用状态的按钮:
运行程序,可以看到按钮处于灰色状态,无法被点击:
【通过按钮 2 切换按钮 1 的禁用状态】
(1)使用 Qt Designer 拖两个按钮到 Widget 中
两个按钮的 objectName 分别为 pushButton 和 pushButton_2。
QObject 的 objectName 属性介绍:
QObject 是 QWidget 的父类,里面最主要的属性就是 objectName。在一个 Qt 程序中,objectName 相当于对象的身份标识,彼此之间不能重复。
在使用 Qt Designer 时,尤其是界面上存在多个 widget 的时候,可以通过 objectName 获取到指定的 widget 对象。
Qt Designer 生成的 ui 文件,本身是 xml 格式的,qmake 会把这个 xml 文件转换成 C++ 的 .h 文件(这个文件生成在 build 目录中),构成一个 ui_widget 类。
每个 widget 的 objectName 最终就会成为 ui_widget 类的属性名字。最终这个类的实例就是:Ui::Widget *ui,因此就可以通过形如 ui->pushButton 或者 ui->pushButton_2 这样的代码获取到界面上的 widget 对象了。
当前自动生成的 objectName 是有规律的:控件的类型 + 下划线 + 数字。很明显,以数字的方式命名并不是一个好的编程习惯,这里我将它修改为如下所示:
(2)生成两个按钮的 slot 函数
- 使用 isEnabled 获取当前按钮的可用状态。
- 使用 setEnabled 修改按钮的可用状态,此处是直接针对原来的可用状态进行取反后设置。
运行程序可以看到:初始情况下,上面的按钮是可用状态。
接着点击下方按钮,即可使上方按钮被禁用。
再次点击下方按钮,上方按钮就会解除禁用(禁用状态的按钮为灰色,且不可点击)
在 Qt Designer 中创建按钮的时候可以设置按钮的初始状态是 “可用” 还是 “禁用”。如果把 enabled 这一列的对钩去掉,则按钮的初始状态就是 “禁用” 状态。
3、geometry
位置和尺寸是四个属性的统称:
- x 横坐标
- y 纵坐标
- width 宽度
- height 高度
但是实际开发中并不会直接使用这几个属性,而是通过一系列封装的方法来获取 / 修改。
对于 Qt 的坐标系,不要忘记是一个 “左手坐标系”,其中坐标系的原点是当前元素的父元素的左上角。
【控制按钮的位置】
(1) 在界面中拖五个按钮
五个按钮的 objectName 分别为:pushButton_target、pushButton_up、pushButton_down、pushButton_left、pushButton_right(五个按钮的初始位置和大小都随意)
(2)在 widget.cpp 中编写四个按钮的 slot 函数
运行程序可以看到:按下下方的四个按钮就会控制 target 的左上角的位置,对应的按钮整个尺寸也会发生改变。
上述代码是直接设置的 QRect 中的 x 和 y。实际上,QRect 内部是存储了左上和右下两个点的坐标,再通过这两个点的坐标差值计算长宽。单纯修改左上坐标就会引起整个矩形的长宽发生改变。
如果想让整个按钮都移动(宽度和高度不发生改变),可以不再修改 QRect,而是通过 QRect 基于 setGeometry 第二个版本的函数重新设置位置即可:
上述代码使用 move 方法也是可以的。
【表白程序】
(1) 往界面上拖拽两个按钮和一个 Label
Label 的 objectName 为 pushButton_accept 和 pushButton_reject,label 的 objectName 为 label 控件中文本如下图所示:
(2)在 widget.cpp 中添加 slot 函数
运行程序可以看到:当点击 "Sorry.." 时,按钮就跑了。
上述代码使用的是 clicked(一下一上是点击),如果使用 pressed(鼠标按下事件)。
如果使用 mouseMoveEvent,会更狠一些, 只要鼠标移动过来,按钮就跑了。对应的代码更麻烦⼀些,需要使用到 Qt 的使用机制(需要自定义类继承自 QPushButton,重写 mouseMoveEvent 方法)这里就暂时不展开了。
A. window frame 的影响
如果 widget 作为一个窗口(带有标题栏、最小化、最大化、关闭按钮),那么在计算尺寸和坐标的时候就有两种算法,包含 window frame 和不包含 window frame。
-
其中 x()、y()、frameGeometry()、pos()、move() 都是按照包含 window frame 的方式来计算的。
-
其中 geometry()、width()、height()、rect()、size() 则是按照不包含 window frame 的方式来计算的。
如果一个不是作为窗口的 widget,上述两类方式得到的结果是一致的。
B. 相关 API
为什么要提供这么多功能重复的 API 呢?
这个就涉及到 Qt API 的设计理念(尽量符合⼈的直觉)了,举例:Qt 的 QVector,尾插元素操作有以下方法:
- push_back
- append
- +=
- <<
上述方法的效果都是等价的,即使不翻阅文档,单纯的凭借直觉就能把代码写对。
【geometry 和 frameGeometry 的区别】
(1) 在按钮的 slot 函数中编写代码 & 在构造函数中也添加同样的代码
(2)执行程序
可以看到:在构造函数中打印出的 geometry 和 frameGeometry 是相同的。但是在点击按钮时,打印的 geometry 和 frameGeometry 则存在差异。
注意:在构造方法中,Widget 刚刚创建出来,还没有加入到对象树中,此时也就不具备 Window frame。
在按钮的 slot 函数中,由于用户点击的时候,对象树已经构造好了,此时 Widget 已经具备了 Window frame,因此在位置和尺寸上均出现了差异。
如果把上述代码修改成打印 pushButton 的 geometry 和 frameGeometry,结果就是完全相同的。因为 pushButton 并非是一个窗口。
4、windowTitle
注意 :上述设置操作针对不同的 widget 可能会有不同的行为。
- 如果是顶层 widget(独立窗口),这个操作才会有效。
- 如果是子 widget,这个操作无任何效果。
【设置窗口标题】
(1)修改 widget.cpp
执行效果:
5、windowIcon
同 windowTitle,上述操作仅针对顶层 widget 有效。
(1)设置窗口图标
A. 先在 D 盘中保存一张图片,我这里命名为 Q&A.jpg
B. 修改 widget.cpp
前面推荐使用堆来创建对象,主要是因为要确保当前控件的生命周期是足够的,要通过 Qt 对象树来释放对象。
QIcon 自身是一个比较小的对象。创建出来之后,就是要设置到某个 QWidget 里面,QIcon 对象本身是否释放并不影响图标最终的显示。
QIcon 也不支持对象树,无法给它执行父对象。
注意:Windows 下路径的分隔符可以使用 / 也可以使用 \,但是如果在字符串中使用 \,需要写作转义字符的形式 \\,因此还是更推荐使用 /。
C. 运行程序
可以看到窗口图标已经成为上述图片:
于此同时,程序在任务栏中的图标也发生改变:
实际开发中,一般不会在代码中通过绝对路径引入图片。因为我们无法保证程序发布后,用户的电脑上也有同样的路径。如果使用相对路径,则需要确保代码中的相对路径写法和图片实际所在的路径匹配(比如代码中写作 "./image/Q&A.jpg",就需要在当前工作目录中创建 image 目录,并把 Q&A.jpg 放进去)。
- 绝对路径:以盘符(windows)或者以 /(Linux)开头的路径。
- 相对路径:以 .(表示当前路径)或者 以 ..(表示当前路径上级路径)开头的路径。其中,经常也会省略,相对路径的前提是需要明确 “当前工作目录”。
对于 Qt 程序来说,当前工作目录可能是变化的。比如通过 Qt Creator 运行的程序,当前工作目录是项目的构建目录,直接双击 exe 运行,工作目录则是 exe 所在目录。
所谓构建目录,是和 Qt 项目并列的,专门用来放生成的临时文件和最终 exe 的目录。
Qt 使用 qrc 机制帮我们自动完成了上述工作,更方便的来管理项目依赖的静态资源。这个机制可以从根本上解决以下两个问题:
- 确保我们的图片所在路径在目标用户机器上存在。
- 确保我们的图片不会被用户搞没了。
qrc 文件是一种 XML 格式的资源配置文件,它用 XML 记录硬盘上的文件和对应的随意指定的资源名称,应用程序通过资源名称来访问这些资源。在 Qt 开发中,可以通过将资源文件添加到项目中来方便地访问和管理这些资源,这些资源文件可以位于 qrc 文件所在目录的同级或其子目录下。
在构建程序的过程中,Qt 会把资源文件的二进制数据转成 cpp 代码,编译到 exe 中,从而使依赖的资源变得 “路径无关”。
这种资源管理机制并非 Qt 独有,很多开发框架都有类似的机制。例如:Android 的 Resources 和 AssetManager 也是类似的效果。
qrc 的缺点:无法导入太大的资源文件。
【通过 qrc 管理图片作为图标】
(1)右键项目,创建一个 Qt Resource File(qrc 文件)
文件名随意起(不要带中文和特殊符号即可),这里叫做:resource.qrc
(2)在 qrc 编辑器中添加前缀
此处我们前缀设置成 / 即可。所谓的前缀,可以理解成 “虚拟的目录”,这个目录在我们的电脑中并不是真实存在的,是 Qt 自己抽象出来的,它决定了后续我们如何在代码中访问资源。
(3)在资源编辑器中,点击 add Files 添加资源文件
此处我们需要添加的是:Q&A.jpg:
注意 :添加的文件必须是在 qrc 文件的同级目录或者同级目录的子目录中,因此我们需要把之前 D 盘中的 Q&A.jpg 复制到上述目录中。
添加完毕后,可以在资源编辑器中看到添加好的文件:
(4)在代码中使用 Q&A.jpg
A. 编辑 widget.cpp
代码中需要访问 qrc 中管理的文件时,就需要在路径上带有 : 前缀。创建的前缀叫什么名字,代码中就写什么名字:前缀 + 文件名。
注意上述路径的访问规则:
- 使用 : 作为开头,表示从 qrc 中读取资源
- / 是上面配置的前缀
- rose.jpg 是资源的名称
需要确保代码中编写的路径和添加到 qrc 中资源的路径匹配,否则资源无法被访问(同时也不会有报错提示)。
(5)运行程序
可以看到图标已经能正确设置:
我们可以进入到项目的构建目录中可以看到:目录中多了一个 qrc_resource.cpp 文件,直接打开这个文件可以看到:
qrc 中导入的图片资源会被转成这个 qrc_resource.cpp(自动生成)这个 c++ 代码:
上述代码其实就是通过 unsigned char 数组,把 rose.jpg 中的每个字节都记录下来。这些代码会被编译到 exe 中,后续无论 exe 被复制到哪个目录下都确保能够访问到该图片资源。
上述 qrc 这一套资源管理方案的优点和缺点都很明显:
- 优点:确保了图片、字体、剩余等资源能够真正做到 “目录无关”,无论如何都不会出现资源丢失的情况。
- 缺点:不适合管理体积大的资源。如果资源比较大(比如是几个 MB 的文件),或者资源特别多,生成的最终的 exe 体积就会比较大,程序运行消耗的内存也会增大,程序编译的时间也会显著增加。
6、windowOpacity
【调整窗口透明度】
(1)在界面上拖放两个按钮,分别用来增加不透明度和减少不透明度
objectName 分别为 pushButton_add 和 pushButton_sub:
(2)编写 wdiget.cpp, 编写两个按钮的 slot 函数
- 点击 pushButton_sub 会减少不透明度,也就是窗口越来越透明
- 点击 pushButton_add 会增加不透明度,窗口会逐渐恢复
(3)执行程序
点击了几下 '-' 之后,就可以透过窗⼝看到后面的内容了,点击 '+' 又会逐渐恢复:
同时控制台中也可以看到 opacity 数值的变化:
注意 :C++ 中 float 类型遵守 IEEE 754 标准,因此在进行运算的时候会有一定的精度误差,因此 1 - 0.1 的数值并非是 0.9。
7、cursor
【在 Qt Designer 中设置按钮的光标】
(1)在界面中创建一个按钮
(2)直接在右侧属性编辑区修改 cursor 属性为 “打开手势”
(3)运行程序
鼠标悬停到按钮上,即可看到光标的变化。
【通过代码设置按钮的光标】
(1) 编写 widget.cpp
其中 Qt::WaitCursor 就是自带的沙漏 / 转圈形状的光标。
系统内置的光标形状如下:
Ctrl + 左键点击 Qt::WaitCursor 跳转到源码即可看到:
【自定义鼠标光标】
Qt 自带的光标形状有限,我们也可以自己找个图片,做成鼠标的光标。比如我的 “男神懒羊羊”。
(1)创建 qrc 资源文件,添加前缀 /,并加入 huaji.png
(2)编写 widget.cpp
8、font
关于 QFont:
【在 Qt Designer 中设置字体属性】
(1)在界面上创建一个 label
(2)在右侧的属性编辑区,设置该 label 的 font 相关属性
在这里调整上述属性,可以实时的看到文字的变化。
(3)执行程序
【在代码中设置字体属性】
(1)编写 widget.cpp
(2)运行程序
在实际开发中,字体属性如何选择是一个 “审美问题”,而不是 “技术问题”,往往需要有一定的艺术细胞。幸运的是,公司中往往有专业的 “美工” / “设计” 这样的岗位,去做这方面的工作,我们程序员只要按照人家给的设计稿,把代码写出来即可,不必过多考虑怎样搭配才好看的问题。
9、toolTip
toolTip 只是给用户看的,在代码中一般不需要获取到 toolTip。
【设置按钮的 toolTip】
(1)在界面上拖放两个按钮:objectName 设置为 pushButton_yes 和 pushButton_no
(2)编写 widget.cpp
(3)运行程序
观察效果,可以看到鼠标停到按钮上之后,就能弹出提示,时间到后自行消失。
10、focusPolicy
设置控件获取到焦点的策略,比如某个控件能否用鼠标选中或者能否通过 tab 键选中。
所谓 “焦点”,指的就是能选中这个元素。接下来的操作(比如键盘操作),就都是针对该焦点元素进行的了。这个对于输入框、单选框、复选框等控件非常有用的。
这个事情就和 war3 或者 sc2 中,先选中单位,再下达命令是一样的。
- Qt::NoFocus:控件不会接收键盘焦点
- Qt::TabFocus:控件可以通过 Tab 键接收焦点
- Qt::ClickFocus:控件在鼠标点击时接收焦点
- Qt::StrongFocus:控件可以通过 Tab 键和鼠标点击接收焦点(默认值)
- Qt::WheelFocus:类似于 Qt::StrongFocus,同时控件也通过⿏标滚轮获取到焦点(新增的选项, ⼀般很少使用)
【理解不同的 focusPolicy】
(1)在界面上创建四个单行输入框(Line Edit)
(2)修改四个输入框的 focusPolicy 属性为 Qt::StrongFocus(默认取值,一般不用额外修改)
此时运行程序可以看到,使用鼠标单击 / tab,就可以移动光标所在输入框,从而接下来的输入就是针对这个获取焦点的输⼊框展开的了。
(3)修改第二个输入框的 focusPolicy 为 Qt::NoFocus,则第二个输入框不会被 tab / 鼠标左键选中
(4)修改第二个输入框 focusPolicy 为 Qt::TabFocus,则只能通过 tab 选中,无法通过鼠标选中
(5)修改第二个输入框 focusPolicy 为 Qt::ClickFocus,则只能通过 tab 选中,无法通过鼠标选中
11、styleSheet
通过 CSS 设置 widget 的样式。
CSS(Cascading Style Sheets 层叠样式表)本身属于网页前端技术,主要就是用来描述界面的
样式。所谓 “样式”,包括不限于大小、位置、颜色、间距、字体、背景、边框等。我们平时看到的丰富多彩的网页就都会用到大量的 CSS。
Qt 虽然是做 GUI 开发,但实际上和网页前端有很多异曲同工之处,因此 Qt 也引入了对于 CSS 的支持。CSS 中可以设置的样式属性非常多,基于这些属性 Qt 只能支持其中一部分,称为 QSS(Qt Style Sheet)。具体的支持情况可以参考 Qt 文档中 "Qt Style Sheets Reference" 章节。
【设置文本样式】
(1)在界面上创建 label
(2)编辑右侧的 styleSheet 属性,设置样式
或者选择下面这种方式打开:
此处的语法格式同 CSS,使用键值对的方式设置样式。其中键和值之间使用 : 分割.,键值对之间使用 ; 分割。
另外,Qt Designer 只能对样式的基本格式进行校验,不能检测出哪些样式不被 Qt ⽀持,比如 text-align: center 这样的文本居中操作,就无法支持。
编辑完成样式之后,可以看到在 Qt Designer 中能够实时预览出效果:
(3)运⾏程序
可以看到实际效果和预览效果基本一致:
【实现切换夜间模式】
- 日间模式:文字是黑色的,背景是白色的
- 夜间模式:文字是白色的,背景是黑色的
(1)在界面上创建一个多行输入框(Text Edit)和两个按钮:objectName 分别为 pushButton_light 和 pushButton_dark
(2)编写按钮的 slot 函数
- #333 是深色,但是没那么黑
- #fff 是纯白色
- #000 是纯黑色
使用在线调色版或者画图板都可以看到数字对应的颜色,可参考:
在线调色板,调色板工具—在线工具 (sojson.com)
A. 关于计算机中的颜色表示
计算机中使用 “像素” 表示屏幕上的一个基本单位(也就是一个发亮的光点)。每个光点都使用三个字节表示颜色,分别是 R(red)、G(green)、B(blue)一个字节表示(取值范围是 0-255,或者 0x00-0xFF)。
混合三种不同颜色的数值比例就能搭配出千千万万的颜色出来。
- rgb(255, 0, 0) 或者 #FF0000 或者 #F00 表示纯红色
- rgb(0, 255, 0) 或者 #00FF00 或者 #0F0 表示纯绿色
- rgb(0, 0, 255) 或者 #0000FF 或者 #00F 表示纯蓝色
- rgb(255, 255, 255) 或者 #FFFFFF 或者 #FFF 表示纯白色
- rgb(0, 0, 0) 或者 #000000 或者 #000 表示纯黑色
当然,上述规则只是针对⼀般的程序而言是这么设定的。实际的显示器可能有 8bit 色深或者 10bit 色深等,实际情况会更加复杂。
(3)运行程序
- 点击 “日间模式” 就是浅色背景、深色文字
- 点击 “夜间模式” 就是深色背景、浅色文字
三、按钮类控件
1、Push Button
使用 QPushButton 表示一个按钮,这也是当前我们最熟悉的一个控件了。QPushButton 继承自 QAbstractButton,这个类是一个抽象类,是其他按钮的父类。
在 Qt Designer 中也能够看到这里的继承关系:
QAbstractButton 中,和 QPushButton 相关性较大的属性:
- QAbstractButton 作为 QWidget 的子类,当然也继承了 QWidget 的属性。上面介绍的 QWidget 里的各种属性用法,对于 QAbstractButton 同样适勇。因此表格仅列出 QAbstractButton 独有的属性。
- Qt 的 api 设计风格是非常清晰的,此处列出的属性都是可以获取和设置的。例如,使用 text() 获取按钮文本,使用 setText() 设置文本。
事实上,QPushButton 的核心功能都是 QAbstractButton 提供的,自身提供的属性都比较简单。其中,default 和 audoDefault 影响的是按下 enter 时自动点击哪个按钮的行为,flat 把按钮设置为扁平的样式。
【带有图标的按钮】
(1)创建 resource.qrc 文件并导入图片
具体操作步骤参见 QWidget 中的 windowIcon 部分。
(2)在界面上创建一个按钮
(3)修改 widget.cpp,给按钮设置图标
(4)执行程序
观察结果:
【带有快捷键的按钮 】
(1)在界面中拖五个按钮
五个按钮的 objectName 分别为 pushButton_target、pushButton_up、pushButton_down、pushButton_left、pushButton_right。
(2)创建 resource.qrc,并导入 5 个图片
(3)修改 widget.cpp,设置图标资源和快捷键
- 使用 setShortcut 给按钮设置快捷键,参数是⼀个 QKeySequence 对象,表示一个按键序列,支持持组合键(Ctrl + C 这种)。
- QKeySequence 的构造函数参数,可以直接使用 "Ctrl + C" 这样的按键名字符串表示,也可以使用预定义好的常量(形如 Qt::CTRL + Qt::Key_C)表示。
(4)修改 widget.cpp,设置四个方向键的 slot 函数
(5)运行程序
此时点击按钮,或者使用 wasd 均可让 “柯南” 移动:
【按钮的重复触发】
在上述案例中按住快捷键,是可以进行重复触发的,但是鼠标点击则不能。
(1)修改 widget.cpp,在构造函数中开启重复触发
此时按住鼠标时,即可让 “柯南” 连续移动。
2、Radio Buttion
QRadioButton 是单选按钮,可以让我们在多个选项中选择一个。
作为 QAbstractButton 和 QWidget 的子类,上面介绍的属性和用法,对于 QRadioButton 同样适用。
QAbstractButton 中和 QRadioButton 关系较大的属性:
【选择性别】
(1)在界面上创建⼀个 label 和 3 个单选按钮
设置的文本如下图,3 个单选按钮的 objectName 分别为:radioButton_male、radioButton_female、radioButton_other
(2)修改 widget.cpp,编辑三个 QRadioButton 的 slot 函数
(3)运行程序
可以看到随着选择不同的单选按钮,label 中的提示文字就会随之变化:
(4)当前代码中,如果程序启动,则不会选择任何选项
可以修改代码,让程序启动默认选中性别男:
此时运行程序,即可看到性别男已经被选中了。
(5)当前代码中,也可以禁⽤ “其他” 被选中
修改 widget.cpp 的构造函数:
运行程序可以看到,点击 “其他” 按钮的时候,虽然不会被选中,但是可以触发点击事件,使上面的 label 显示性别为其他:
使用 setEnabled 是更彻底的禁用按钮的方式,此时该按钮无法被选中,也无法响应任何输入:
【click、press、release、toggled 的区别】
- clicked 表示⼀次 “点击”
- pressed 表示鼠标 “按下”
- released 表示鼠标 “释放”
- toggled 表示按钮状态切换
(1)在界面上创建四个单选按钮
objectName 分别为 radioButton、radioButton_2、radioButton_3、radioButton_4
(2)给 1 创建 clicked 槽函数,给 2 创建 pressed 槽函数,给 3 创建 released 槽函数,给 4 创建 toggled 槽函数
(3)运行程序
可以看到:
- clicked 是⼀次鼠标按下 + 鼠标标释放触发的
- pressed 是鼠标按下触发的
- released 是鼠标释放触发的
- toggled 是 checked 属性改变时触发的
总的来说,toggled 是最适合 QRadioButton 的。
【单选框分组】
(1)在界面上创建 6 个单选框,用来模拟麦当劳点餐界面。
objectName 分别为 radioButton 到 radioButton_6
此时直接运行程序可以看到,这六个 QRadioButton 之间都是排他的。我们希望每一组内部来控制排他,但是组和组之间不能排他。
(2)引入 QButtonGroup 进行分组
A. 修改 widget.cpp
再次执行程序可以看到可以按照正确的分组方式来完成排他了。
3、Check Box
QCheckBox 表示复选按钮,可以允许选中多个。和 QCheckBox 最相关的属性也是 checkable 和 checked,都是继承自 QAbstractButton。
至于 QCheckBox 独有的属性 tristate 用来实现 “三态复选框”,这个东西比较冷门,这里暂时不讲述。
【获取复选按钮的取值】
(1)在界面上创建三个复选按钮和一个普通按钮
objectName 分别为 checkBox_study、checkBox_game、checkBox_work 以及 pushButton
(2)给 pushButton 添加 slot 函数
(3)运行程序
可以看到点击确认按钮时,就会在控制台中输出选中的内容:
4、Tool Button
QToolButton 的大部分功能和 QPushButton 是一致的,但 QToolButton 主要应用在工具栏、菜单等场景。
四、显示类控件
1、Label
QLabel 可以用来显示文本和图片。
核心属性如下:
【显示不同格式的文本】
(1)在界面上创建三个 QLabel
尺存放大一些,objectName 分别为 label、label_2、label_3
(2)修改 widget.cpp,设置三个 label 的属性
(3)运行程序
【显示图片】
虽然 QPushButton 也可以通过设置图标的方式设置图片,但是并非是一个好的选择,更多的时候还是希望通过 QLabel 来作为一个更单纯的显示图片的方式。
(1)在界面上创建一个 QLabel、objectName 为 label
(2)创建 resource.qrc 文件,并把图片导入到 qrc 中
(3)修改 widget.cpp,给 QLabel 设置图片
这个图片本身的尺寸是 1280 * 150,超出了窗口的大小(800 * 800)。
执行程序,观察结果:
(4)修改代码,设置 scaledContents 属性
再次运行程,观察效果,可以看到图片已经被拉伸,可以把窗口填满了:
此时,如果拖动窗口大小,可以看到图片并不会随着窗口大小的改变而同步变化。
为了解决这个问题,可以在 Widget 中重写 resizeEvent 函数:
执行程序,此时改变窗口大小,图片也会随之变化:
注意:这里的 resizeEvent 函数我们没有手动调用,但是能在窗口大小变化时被自动调用,这个过程就是依赖 C++ 中的多态来实现的。
Qt 框架内部管理着 QWidget 对象表示我们的窗口,在窗口大小发生改变时,Qt 就会自动调用 resizeEvent 函数。但是由于实际上这个表示窗口的并非是 QWidget,而是 QWidget 的子类,也就是我们自己写的 Widget。此时虽然是通过父类调用函数,但是实际上执行的是子类的函数(也就是我们重写后的 resizeEvent)。
此处属于是多态机制的⼀种经典用法。通过上述过程就可以把自定义的代码插入到框架内部执行,相当于 “注册回调函数”。
【文本对齐、自动换行、缩进、边距】
(1)创建四个 label,objectName 分别是 label 到 label_4,并且在 QFrame 中设置 frameShape 为 Box(设置边框之后看起来会更清晰一些)
- QFrame::Box:矩形边框
- QFrame::Panel:带有可点击区域的面板边框
- QFrame::WinPanel:Windows 风格的边框
- QFrame::HLine:水平线边框
- QFrame::VLine:垂直线边框
- QFrame::StyledPanel:带有可点击区域的面板边框,但样式取决于窗口主题。
(2)编写 widget.cpp,给这四个 label 设置属性
(3)运行程序
【设置伙伴】
(1)创建两个 label 和 两个 radioButton
objectName 分别为 label、label_2、radioButton、radioButton_2
此处把 label 中的文本设置为 “快捷键 &A” 这样的形式。其中 & 后面跟着的字符就是快捷键,可以通过 alt + A 的方式来触发该快捷键。但是注意,这里的快捷键和 QPushButton 的不同需要搭配 alt 和 单个字母的方式才能触发。
绑定了伙伴关系之后,通过快捷键就可以选中对应的单选按钮 / 复选按钮。
(2)编写 widget.cpp,设置 buddy 属性
这里也可以使用 Qt Designer 直接设置:
(3)运行程序
可以看到,按下快捷键 alt + a 或者 alt + b,即可选中对应的选项:
2、LCD Number
QLCDNumer 是一个专门用来显示数字的控件,类似于 “老式计算器” 的效果。
核心属性:
【倒计时】
(1)在界面上创建⼀个 QLCDNumber,初始值设为 10
objectName 为 lcdNumber
(2)修改 widget.h 代码,创建一个 QTimer 成员和一个 handle 函数
(3)修改 widget.cpp,在构造函数中初始化 QTimer
- QTimer 表示定时器,通过 start 方法启动定时器之后,就会每隔一定周期触发一次 QTimer::timeout 信号
- 使用 connect 把 QTimer::timeout 信号和 Widget::updateTime 连接起来,意味着每次触发 QTimer::timeout 都会执行 Widget::updateTime
(4)修改 widget.cpp,实现 handle
- 通过 intValue 获取到 QLCDNumber 内部的数值。
- 如果 value 的值归 0 了,就停止 QTimer,接下来 QTimer 也就不会触发 timeout 信号了。
(5)执行程序
可以看到每隔一秒钟,显示的数字就减少 1:
(6)针对上述代码,存在两个问题
A. 上述代码如果直接在 Widget 构造函数中,通过一个循环 + sleep 的方式是否可以呢?
显然,上面这段代码是不行的,循环会使 Widget 的构造函数无法执行完毕,此时界面是不能正确构造和显示的。
B. 上述代码如果是在 Widget 构造函数中,另起一个线程,在新线程中完成循环 + sleep 是否可以呢?
这个代码同样是不行的。Qt 中规定:任何对于 GUI 上内容的操作必须在主线程中完成。像 Widget 构造函数,以及 connect 连接的 slot 函数,都是在主线程中调用的。而我们自己创建的线程则不是,当我们自己的线程中尝试对界面元素进行修改时,Qt 程序往往会直接崩溃。
这样的约定主要是因为 GUI 中的状态往往是牵一发动全身的,修改一个地方,就需要同步的对其他内容进行调整。比如调整了某个元素的尺存,就可能影响到内部的文字位置,或者其他元素的位置。这里一连串的修改都是需要按照一定的顺序来完成的。
由于多线程执行的顺序无法保障,因此 Qt 从根本上禁止了其他线程修改 GUI 状态,避免后续的一系列问题。
对于 Qt 的槽函数来说,默认情况下,槽函数都是由主线程调用到,在槽函数中修改界面是没有任何问题的。
综上所述,使用定时器是实现上述功能的最合理方案。后续如果也有类似的需要 “周期性修改界面状态” 的需求也需要优先考虑使用定时器。
3、ProgressBar
使用 QProgressBar 表示一个进度条。
核心属性:
【设置进度条按时间增长】
(1)在界面上创建进度条,objectName 为 progressBar
其中最小值设为 0,最大值设为 100,当前值设为 0:
(2)修改 widget.h,创建 QTimer 和 handle 函数
虽然在 widget.h 中用到了 QTimer,但是却没在 widget.h 文件中包含 <QTimer> 头文件,为什么这个代码编译没有出错呢?
上述问题其实是通过 Qt 内部提供的一个特殊技巧来实现的。在 Qt 中有一个专门的头文件(#include <QWidget>),这个头文件中包含了 Qt 中所有类的 “前置声明”(class QWidget,class QPushButton,class QTimer)。这个头文件我们一般不会直接接触到,但是包含其它的 Qt 的头文件,都会间接的包含到这个头文件。
如果 Widget 类的前面以及提供了 QTimer 类的声明的话,此时就可以在 Widget 中声明 QTimer 的指针 / 引用类型的成员。后续如果要真正使用 QTimer 的头文件(包括创建实例,使用里面的成员),仍然要包含 QTimer 的头文件(包含了 QTimer 的详细的类的定义)。
Qt 为什么要使用上述技巧呢?上述技巧能解决什么问题?有什么提升呢?
主要解决的是编译速度的问题。C/C++ 代码,编译速度在其他语言横向对比中是非常慢的。C++ 编译速度慢和 #include 头文件有直接关系。由于 include 关系错综复杂,所以尽可能减少 include 头文件的个数就可以有效地减少编译时间。
Qt 中就使用 class 前置声明的方式来尽量减少头文件的包含。通过前置声明的方式,Qt 中每个头文件包含的其它头文件数量都能得到一定的降低。
(3)修改 widget.cpp,初始化 QTimer
此处设置 100ms 触发一次 timeout 信号,也就是⼀秒钟触发 10 次
(4)修改 widget.cpp,实现 handle
(5) 运行程序
可以看到进度条中的进度在快速增长:
在实际开发中,进度条的取值往往是根据当前任务的实际进度来进行设置的。比如需要读取一个很大的文件,就可以获取文件的总的大小和当前读取完毕的大小,来设置进度条的比例。
由于前面我们介绍了 Qt 禁止在其他线程修改界面,因此进度条的更新往往也是需要搭配定时器来完成的。
通过定时器周期触发信号,主线程调用对应的 slot 函数,再在 slot 函数中对当前的任务进度进行计算,并更新进度条的界面效果。
【创建一个红色的进度条】
上述的进度条是用绿⾊表示的,但是考虑到有些人可能不喜欢绿⾊,因此我们改成一个红色的进度条。
QProgressBar 同样也是 QWidget 的子类,因此我们可以使用 styleSheet 通过样式来修改进度条的颜色。
(1)在界面上创建一个进度条
(2)在 Qt Designer 右侧的属性编辑器中找到 QWidget 的 styleSheet 属性
编辑内容:其中的 chunk 是选中进度条中的每个 “块”,使用 QProgressBar::text 则可以选中文本。
同时把 QProcessBar 的 alignment 属性设置为垂直水平居中:
此处如果不设置 alignment,进度条中的数字会跑到左上角。
(3)执行程序
可以看到如下效果,就得到了一个红色的进度条:
4、Calendar Widget
QCalendarWidget 表示⼀个 “日历”,形如:
核心属性:
重要信号:
【获取选中的日期】
(1)在界面上创建一个 QCalendarWidget 和一个 label
objectName 为 calendarWidget,label
(2)给 QCalendarWidget 添加 slot 函数
(3)执行程序
可以看到当选择不同的日期时,label 中的内容就会随之改变:
五、输入类控件
1、Line Edit
QLineEdit 用来表示单行输入框,可以输入一段文本,但是不能换行。
核心属性:
核心信号:
【录入个人信息】
(1)在界面上创建三个输入框和两个单选按钮,一个普通按钮
三个输入框的 objectName 为 lineEdit_name、lineEdit_password、lineEdit_phone
两个单选按钮的 objectName 为 radioButton_male、radioButton_female
按钮的 objectName 为 pushButton
(2)编写 widget.cpp,在构造函数中编写初始化代码
(3)继续修改 widget.cpp,给按钮添加 slot 函数
(4)执行程序
可以看到,随着用户输⼊内容之后,点击 “提交” 按钮就能打印到输入的信息:
inputMask 只能进行简单的输入格式校验。实际开发中,基于正则表达式的方式是更核心的方法。
【使用正则表达式验证输入框的数据】
此处要求在输入框中输入一个合法的电话号码(1 开头,11 位,全都是数字)。如果验证不通过,则确定按钮无法点击。
(1)关于正则表达式
正则表达式是一种在计算机中常用的,使用特殊字符描述一个字符串的特征的机制,在进行字符串匹配时非常有用。
正则表达式的语法还比较复杂,一般都是随用随查,不需要背下来。
可参考:
正则表达式文档:正则表达式语法 | Microsoft Learn
正则表达式在线工具:正则表达式语法测试工具 - 在线工具 (buyaocha.com)
(2)在界面上创建输入框和一个按钮
此处的规则:输入框要检查输入的内容是否为合法的手机号码。
- 如果是,则按钮设为可用状态。
- 如果不是,则按钮设为禁用状态。
(3)编写 widget.cpp,把按钮初始 enabled 设为 false,给输入框添加验证器
- 使用 QRegExp 创建一个正则表达式对象,"^1\\d{10}$" 表示" 表示以 xxx 开头,后面跟着的 1 表示以 1 开头,\d 表示数字,为了在 C++ 字符串中石油,需要写作 \\d,{10} 表示前面的内容重复出现 10 次,\d 数字要重复出现 10 次,$ 表示结尾。
- 使用 QRegExpValidator 创建一个验证器对象,Qt 中内置了四个主要的验证器对象。
QRegularExpressionValidator 在匹配性能上做出了一定优化。但是从使用角度讲,和 QRegExpValidator 差别不大,我们使用 QRegExpValidator 即可。
(4)编写 widget.cpp,给 lineEdit 添加 textEdited 信号的 slot 函数
- on_lineEdit_textEdited 的参数是当前输入框的内容
- 通过 lineEdit->validator() 获取到内置的验证器
- 通过 validate 方法验证文本是否符合要求
- 第一个参数填写的是要验证的字符串。由于参数要求是 QString& 而不是 const QString&,需要把这个变量复制一下。
- 第二个参数是⼀个 int&,是输出型参数。当验证的字符串不匹配时,返回这个字符串的长度(没有什么实质作用)。
- 返回值是⼀个枚举。 QValidator::Acceptable 表示 验证通过,QValidator::Invalid 表示验证不通过。
(5)执行程序
观察效果,可以看到此时尝试输入字母是无法输入的,并且只有当输入的内容符合要求,确定按钮才能被使用
【验证两次输入的密码一致】
(1)在界面上创建两个输入框和一个 label
(2)编写代码,设置两个输入框的 echoMode 为 Password
(3)给两个输入框设置 textEdited slot 函数
由于两个槽函数内容一致,我们可以选择封装一个函数,然后在槽函数内直接调用即可。
(4)执行程序
可以看到当两个输入框内的密码相同时,就会提示密码相同:
【切换显示密码】
(1)创建一个输入框和一个复选按钮
(2)修改 widget.cpp,设置输入框的 echoMode 为 Password
(3)修改 widget.cpp,给 checkBox 添加 slot 函数
(4)执行程序
可以看到切换复选框的状态,就可以控制输入框显示密码:
2、Text Edit
QTextEdit 表示多行输入框,也是一个富文本 & markdown 编辑器,并且能在内容超出编辑框范围时自动提供滚动条。
核心属性:
核心信号:
【获取多行输入框的内容】
(1)创建一个多行输入框和一个 label
(2)给多行输入框添加 slot 函数,处理 textChanged 信号
- 通过 toPlainText 方法获取到内部的文本
- 类似的,QTextEdit 还提供了 toMarkdown 和 toHtml,根据需要我们调整不同的获取方式
(3)执行程序
可以看到当输入框中的内容发生变化时,label 中的内容同步发生改变:
【验证输入框的各种信号】
(1)创建多行输入框
(2)给输入框添加以下几个 slot 函数
QTextEdit 中包含了一个 QTextCursor 对象,通过这个对象可以获取到当前光标位置和选中的内容。
(3)执行程序
A. 编写内容时,textChanged 和 cursorPositionChanged 会触发
B. 选中一段文本时,cursorPositionChanged、selectChanged、copyAvailable 会触发
C. 按下 Ctrl + Z 时,textChanged、undoAvailable、redoAvailable、cursorPositionChanged 会触发
D. 按下 Ctrl + Y,textChanged、undoAvailable、redoAvailable、cursorPositionChanged 会触发
3、Combo Box
QComboBox 表示下拉框。
核心属性:
核心方法:
核心信号:
【使用下拉框模拟麦当劳点餐】
(1)在界面上创建三个下拉框和一个按钮
(2)编写 widget.cpp,初始化三个下拉框的内容
也可以选择直接在 ui 界面添加选项内容:
(3)编写 widget.cpp,给按钮添加 slot 函数
(4)执行程序
在点击确定按钮时,就能获取到当前下拉框中选中的内容:
【从文件中加载下拉框的选项】
很多时候下拉框的选项并非是固定的,而是通过读取文件 / 读取网络获取到的。
(1)在界面上创建一个下拉框
(2)创建文件 d:/config.txt,编写选项,每个选项占一行
(3)修改 widget.cpp,从文件中读取选项
- 使用 ifstream 打开文件
- 使用 getline 读取每一行
- 使用 QString::fromStdString 把 std::string 转成 QString
(4)执行程序
可以看到文件内容已经被加载到下拉框中:
Qt 中也提供了 QFile 实现读写文件的功能,当然使用 C++ 标准库的 std::fstream 也是完全可以的。之所以存在两套,是因为 Qt 诞生较早(1991 年左右),此时 C++ 还没有完成 “标准化” 的工作,C++ 标准库这样的概念自然也没有诞生。
因此 Qt 就自己打造了一套库,实现了字符串、容器、文件操作、多线程、网络操作、定时器、正则表达式等内容。(由于 C++ 标准委员会的不作为,至今仍然有些 Qt 提供的功能,是标准库不具备的)
4、Spin Box
使用 QSpinBox 或者 QDoubleSpinBox 表示 “微调框”,它是带有按钮的输入框,可以用来输入整数 / 浮点数,通过点击按钮来修改数值大小。
由于 SpinBox 和 QDoubleSpinBox 用法基本相同,就只介绍 SpinBox 的使用了。
Spin 英文原意为 “旋转”,此处引申成 “微调”。
事实上很多术语在翻译的时候,不一定非要按照原始的翻译来表示,更追求的是 “信达雅”。举例:地铁上的 "Priority Seat" 会翻译成 “爱心专座”,而不是 “优先座位”。
(1)QSpinBox 关键属性
核心信号:
【调整麦当劳购物车中的份数】
(1)在界面上创建下列内容
- 三个下拉框:objectName 为 comboBox 到 comboBox_3
- 三个微调框:objectName 为 spinBox 到 spinBox_3
- 一个按钮:objectName 为 pushButton
(2)编写代码,修改 widget.cpp,给下拉框设置初始值
(3)编写代码,给按钮添加 slot 函数
(4)执行程序
可以看到当用户选择不同的内容时,点击按钮就能获取到对应的结果,同时我们也无法输入一些超出范围的非法值。
5、Date Edit & Time Edit
使用 QDateEdit 作为日期的微调框:
使用 QTimeEdit 作为时间的微调框:
使用 QDateTimeEdit 作为时间日期的微调框:
这几个控件用法非常相似,这里以 QDateTimeEdit 为例进行介绍。
QDateTimeEdit 核心属性:
(1)关于本地时间(LocalTime)和协调世界时(UTC)
UTC 时间是一个基于原子钟的标准时间,不受地球的自转周期影响,和格林威治时间(GMT)是非常接近的,科学家会通过精密的设备来测量并维护。
我们的计算机内部使用的时间就是基于 UTC 时间。
本地时间则是基于不同的时区,对 UTC 时间做出了一些调整。比如咱们使用的北京时间,位于 “东八区”,就需要在 UTC 时间基础上 +8 个小时的时差。
核心信号:
【实现日期计算器】
(1)在界面上创建两个 QDateTimeEdit 和一个按钮,一个 label
QDateTimeEdit 的 objectName 为 dateTimeEdit_old 和 dateTimeEdit_new
(2)编写计算按钮的 slot 函数
- 使用 daysTo 函数可以计算两个日期的天数
- 使用 secsTo 函数可以计算两个时间的秒数
- 通过(秒数 / 3600)换算成小时数,再余上 24 得到零几个小时
- 使用 QString::number 把整数转成 QString 进行拼接
(3)执行程序
6、Dial
使用 QDial 表示一个旋钮。
有些程序通过鼠标拖动旋钮旋转,即可完成一些相关的设置。
核心属性:
核心信号:
【调整窗口透明度】
(1)在界面上创建一个旋钮,并对其数值进行初始化
(2)编写 widget.cpp,设置旋钮的 valueChanged slot 函数
(3)运行程序
观察效果,可以看到随着拖动旋钮旋转,不透明度发生明显变化:
7、Slider
使用 QSlider 表示一个滑动条:
QSlider 和 QDial 都是继承自 QAbstractSlider,因此用法上基本相同。
核心属性:
核心信号:
【调整窗口大小】
(1)在界面上创建两个滑动条,分别是水平和垂直滑动条
objectName 分别为 horizontalSlider 和 verticalSlider
(2)编写代码初始化滑动条
(3)编写滑动条的 valueChanged slot 函数
(4)执行程序
可以看到调整滑动条,窗口大小就会随之改变:
【通过自定义快捷键调整滑动条位置】
设置 - 减小 value,设置 = 增加 value
默认情况下滑动条可以通过方向键或者 pageUp / pageDown 调整大小。
(1)在界面上创建滑动条和 label
(2)创建 valueChanged 的 slot 函数
(3)修改 widget.cpp 构造函数,增加快捷键
- 使用 QShortCut 类设置快捷键
- 快捷键触发时,会发出 QShortcut::activated 信号,我们连接到⾃⼰写的 slot 函数
(4)编写自定义 slot 函数
(5)执行程序
观察效果,可以看到此时按下 - 和 = 就可以调整 value 的值了:
六、多元素控件
Qt 中提供的多元素控件有:
- QListWidget
- QListView
- QTableWidget
- QTableView
- QTreeWidget
- QTreeView
1、xxWidget 和 xxView 之间的区别
以 QTableWidget 和 QTableView 为例。
- QTableView 是基于 MVC 设计的控件。QTableView 自身不持有数据,使用 QTableView 的时候需要用户创建一个 Model 对象(比如 QStandardModel),并且把 Model 和 QTableView 关联起来。后续修改 Model 中的数据就会影响 QTableView 的显示,修改 QTableView 的显示也会影响到 Model 中的数据(双向绑定)。
- QTableWidget 则是 QTableView 的子类,对 Model 进行了封装,不需要用户手动创建 Model 对象,直接就可以往 QTableWidget 中添加数据了。
2、List Widget
使用 QListWidget 能够显示一个纵向的列表。形如:
每个选项都可以被选中。
核心属性:
核心方法:
核心信号:
在上述介绍中涉及到⼀个关键的类:QListWidgetItem,这个类表示 QListWidget 中的一个元素。
核心方法如下,本质上就是⼀个 “文本+图标” 构成的。
【使用 ListWidget】
(1)在界面上创建一个 ListView,右键 => 变形为 => ListWidget,再创建一个 lineEdit 和两个按钮
注意:ListWidget 是 ListView 的子类,功能比 ListView 更丰富。我们使用 ListWidget 即可。
(2)编写 widget.cpp,在构造函数中添加初始元素
或者直接在图形化界面选择 “编辑项目”:
(3)编写 listWidget 的 slot 函数
此处需要判定 current 和 previous 非空,初始情况下是没有元素选中的,就导致这两个指针可能是 NULL。
这⾥需要给 widget.h 前⾯加上 #include <QListWidgetItem>。
(4)编写按钮的 slot 函数
(5)执行程序
可以新增元素,选中元素,删除元素
3、Table Widget
使用 QTableWidget 表示一个表格控件。一个表格中包含若干行,每一行又包含若干列。表格中的每个单元格,是一个 QTableWidgetItem 对象。
QTableWidget 核心方法:
QTableWidgetItem 核心信号:
QTableWidgetItem 核心方法:
【使用 QTableWidget】
(1)在界面上创建 QTableWidget 和 三个按钮,一个输入框
注意:QTableWidget 是 QTableView 的子类,功能比 QTableView 更丰富。我们使用 QTableWidget 即可。
(2)编写 widget.cpp 构造函数,构造表格中的初始数据
(3)编写按钮的 slot 函数
(4)执行程序,即可完成表格的基本操作
默认情况下,单元格中的内容直接就是可编辑的。如果不想让用户编辑,可以设置 ui->tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
4、Tree Widget
使用 QTreeWidget 表示一个树形控件,里面的每个元素都是一个 QTreeWidgetItem,每个 QTreeWidgetItem 可以包含多个文本和图标,每个文本 / 图标为一个列。
可以给 QTreeWidget 设置顶层节点(顶层节点可以有多个),然后再给顶层节点添加子节点,从而构成树形结构。
QTreeWidget 核心方法:
QTreeWidget 核心信号:
QTreeWidgetItem 核心属性:
.
QTreeWidgetItem 核心方法:
【使用 QTreeWidget】
(1)在界面上创建一个 TreeView,右键 => 变形为 => TreeWidget,再创建一个 lineEdit 和两个按钮
注意:TreeWidget 是 TreeView 的子类,功能比 TreeView 更丰富。我们使用 TreeWidget 即可。
(2)编写代码,构造初始数据
(3)编写代码,实现按钮的 slot 函数
(4)执行程序,可以针对树形框进行编辑
七、容器类控件
1、Group Box
使用 QGroupBox 实现一个带有标题的分组框,可以把其他的控件放到里面作为一组,这样看起来能更好看一点。
注意:不要把 QGroupBox 和 QButtonGroup 混淆(之前在介绍 QRadionButton 的时候提到了 QButtonGroup)。
核心属性:
分组框只是一个用来 “美化界面” 这样的组件,并不涉及到用户交互和业务逻辑,属于 “锦上添花"。
【给麦当劳案例加上分组框】
(1)在界面上创建三个分组框,并且在分组框内部创建下拉框和微调框
注意:在复制粘贴控件的时候,一定要先选中对应的父控件,再粘贴。
2、Tab Widget
使用 QTabWidget 实现一个带有标签页的控件,可以往里面添加一些 widget,进一步的就可以通过标签页来切换。
核心属性:
核心信号:
【使用标签页管理多组控件】
(1)在界面上创建一个 QTabWidget 和两个按钮
注意 :
- QTabWidget 中的每个标签页都是⼀个 QWidget
- 点击标签页就可以直接切换
- 右键 QTabWidget,可以添加标签页或者删除标签页
(2)编写 widget.cpp,进行初始化,给标签页中放个简单的 label
注意新创建的 label 的父元素,是 ui->tab 和 ui->tab_2。Qt 中使用父子关系决定该控件 “在哪里”。
(3)编写按钮的 slot 函数
- 使用 count() 获取到标签页的个数
- 使用 addTab 新增标签页
- 使用 removeTab 删除标签页
- 使用 currentIndex 获取到当前标签页的下标
- 使用 setCurrentIndex 切换当前标签页
(4)编写 QTabWidget 的 currentChanged 函数
(5)运行程序
- 点击新建标签页,可以创建出新的标签
- 点击删除当前标签页,可以删除标签
- 切换标签页时,可以看到 qDebug 打印出的标签页编号
八、布局管理器
之前使用 Qt 在界面上创建的控件都是通过 “绝对定位” 的方式来设定的,也就是每个控件所在的位置都需要计算坐标,最终通过 setGeometry 或者 move 方式摆放过去。
这种设定方式其实并不方便,尤其是界面如果内容比较多,不好计算,而且一个窗口大小往往是可以调整的,按照绝对定位的方式,也无法自适应窗口大小。因此 Qt 引入 “布局管理器”(Layout)机制来解决上述问题。
当然,布局管理器并非 Qt 独有。其他的 GUI 开发框架,像 Android、前端等也有类似的机制。
1、垂直布局
使用 QVBoxLayout 表示垂直的布局管理器,V 是 vertical 的缩写。
核心属性:
Layout 只是用于界面布局,并没有提供信号。
【使用 QVBoxLayout 管理多个控件】
(1)编写代码,创建布局管理器和三个按钮,并且把按钮添加到布局管理器中
- 使用 addWidget 把控件添加到布局管理器中
- 使用 setLayout 设置该布局管理器到 widget 中
(2)运行程序
可以看到此时界面上的按钮就存在于布局管理器中,随着窗口尺存变化而发生改变。此时的三个按钮的尺存和位置都是自动计算出来的。
通过上述代码的方式,只能给这个 widget 设定一个布局管理器。实际上也可以通过 Qt Design 在一个窗口中创建多个布局管理器。
【创建两个 QVBoxLayout】
(1)在界面上创建两个 QVBoxLayout,每个 QVBoxLayout 各放三个按钮
(2)运行程序
可以看到这些按钮已经自动排列好,只不过当前这些按钮的位置不能随着窗口大小自动变化。
通过 Qt Designer 创建的布局管理,其实是先创建了一个 widget,设置过 geometry 属性的,再把这个 layout 设置到这个 widget 中。
实际上,一个 widget 只能包含一个 layout。打开 ui 文件的原始 xml,可以看到其中的端倪。这种情况下 layout 并非是窗口 widget 的布局管理器,因此不会随着窗口大小改变。
2、水平布局
使用 QHBoxLayout 表示垂直的布局管理器,H 是 horizontal 的缩写。
核心属性(和 QVBoxLayout 属性是一致的)
【使用 QHBoxLayout 管理控件】
(1)编写代码,创建布局管理器和三个按钮,并且把按钮添加到布局管理器中
(2)运行程序
可以看到此时界面上的按钮就存在于布局管理器中,随着窗口尺寸变化而发生改变。此时的三个按钮的尺存和位置都是自动计算出来的。
Layout 里面可以再嵌套上其他的 layout,从而达到更复杂的布局效果。
【嵌套的 layout】
(1)在代码中创建以下内容
使用 addLayout 给 layout 中添加子 layout:
(2)执行程序
3、网格布局
Qt 中还提供了 QGridLayout 用来实现网格布局的效果,可以达到 M * N 的这种网格的效果。
核心属性:
整体和 QVBoxLayout 以及 QHBoxLayout 相似,但是设置 spacing 的时候是按照垂直水平两个方向来设置的。
【使用 QGridLayout 管理元素】
(1)代码中创建 QGridLayout 和 4 个按钮
使用 addWidget 添加控件到布局管理器中,但是添加的同时会指定两个坐标,表示放在第几行,第几列。
(2)执行代码
可以看到当前的这几个按钮是按照 2 行 2 列的方式排列的:
(3)如果调整行列坐标为下列代码
执行代码可以看到这几个按钮都在同一行了,相当于 QHBoxLayout:
(4)如果调整行列坐标为下列代码
执行代码可以看到这几个按钮都在同一列了,相当于 QVBoxLayout:
(5)任意调整行列,即可看到不同的效果
(6)编写代码形如
注意 :设置行和列的时候,如果设置的是一个很大的值,但是这个值和上一个值之间并没有其他的元素,那么并不会在中间腾出额外的空间。虽然把 button4 设置在第 100 行,但是由于 3-99 行没有元素,因此 button4 仍然会紧挨在 button3 下方看起来和上面的 0 1 2 3 的情况是相同的。
【设置 QGridLayout 中元素的大小比例】
(1)创建 6 个按钮,按照 2 行 3 列的方式排列
使用 setColumnStretch 设置每一列的拉伸系数:
(2)执行程序
可以看到每一列的宽度是不同的,并且随着窗口调整动态变化:
另外,QGridLayout 也提供了 setRowStretch 设置行之间的拉伸系数。
上述案例中,直接设置 setRowStretch 效果不明显,因为每个按钮的高度是固定的。需要把按钮的垂直方向的 sizePolicy 属性设置为 QSizePolicy::Expanding 尽可能填充满布局管理器,才能看到效果。
如果拉伸系数设为 0,意思是不参与拉伸,此时按钮的宽度是固定值。
【设置垂直方向的拉伸系数】
(1)编写代码, 创建 6 个按钮, 按照 3 行 2 列方式排列.
- QSizePolicy::Ignored:忽略控件的尺寸,不对布局产生影响。
- QSizePolicy::Minimum:控件的最小尺寸为固定值,布局时不会超过该值。
- QSizePolicy::Maximum:控件的最大尺寸为固定值,布局时不会小于该值。
- QSizePolicy::Preferred:控件的理想尺寸为固定值,布局时会尽量接近该值。
- QSizePolicy::Expanding:控件的尺寸可以根据空间调整,尽可能占据更多空间。
- QSizePolicy::Shrinking:控件的尺寸可以根据空间调整,尽可能缩小以适应空间。
(2)执行代码
此时的按钮垂直方向都舒展开了,并且调整窗口尺寸,也会按照设定的比例同步变化。
总的来说,使用 QGridLayout 能够代替很多 QHBoxLayout 和 QVBoxLayout 嵌套的场景。毕竟嵌套的代码写起来是比较麻烦的。另外不要忘了,QGridLayout 里面也能嵌套 QHBoxLayout 和 QVBoxLayout,QHBoxLayout 和 QVBoxLayout 里面也能嵌套 QGridLayout。灵活使用上述布局管理器就可以实现出任意的复杂界面。
4、表单布局
除了上述的布局管理器之外,Qt 还提供了 QFormLayout,属于是 QGridLayout 的特殊情况,专门用于实现两列表单的布局。
这种表单布局多用于让用户填写信息的场景,左侧列为提示,右侧列为输入框。
【使用 QFormLayout 创建表单】
(1)编写代码,创建 QFormLayout,以及三个 label 和三个 lineEdit
- 使用 addRow 方法来添加一行,每行包含两个控件。第一个控件固定是 QLabel / 文本,第二个控件则可以是任意控件。
- 如果把第⼀个参数填写为 NULL,则什么都不显示。
(2)执行程序
5、Spacer
使用布局管理器的时候,可能需要在控件之间添加一段空白,就可以使用 QSpacerItem 来表示。
核心属性:
【创建一组左右排列的按钮】
(1)在界面上创建一个 QVBoxLayout ,并添加两个按钮
(2)直接运行程序,可以看到两个按钮是紧挨着的
(3)在两个按钮中间添加一个 spacer
(4)运行程序
可以看到两个按钮之间已经存在了间隔了,调整 QSpacerItem 不同的尺寸,即可看到不同的间距:
在 Qt Designer 中,也可以直接给界面上添加 spacer: