前言:本章开始逐步了解QT的控件已经使用
1.添加资源文件
流程很简单,如下:
按住Ctrl+N

选择qt和qt资源文件


出来文件名其它保持默认就好

这里点击前缀,然后你就可以添加资源文件,非常简单
2.样式表的使用
什么是样式表?
QT样式表是一种强大的QT机制,它允许自定义小部件的外观。我们知道QLabel就是标签,标签可以显示文字(可以设置字体大小、字体颜色),还可以显示图片。所以本小节就使用我们上一小节学习的资源文件,把图片添加到QLabel上。
上一小节我们已经创建好资源文件,并且往里面添加了图片。接下来只需要两步,就能把图片显示到QLabel上。
(1)添加资源文件
现在我们创建我们第三个项目,基类Qwidget,因为Qwidget是空白窗口,而我们的MainWindow有状态栏和菜单栏等

创建好后,我们添加资源文件:

然后我们点击添加文件,选择你要添加的图片(注意图片要放在资源文件的子目录下才能添加),如下:

想要返回编辑器,只需要右键资源文件,选择Open in Editor就行了,如下:


添加完成后,我们按Crel+S保存
(2)将资源文件添加到样式表
接着我们使用ui设计器来将该资源添加到Qlabel上,如下:

控件栏选择Label,然后拖动到窗口

然后点击右边对象的label,再右键点击窗口的label,选择改变样式表

可以看到这个样式表是空的,那如何添加样式表呢?我们可以去帮助文档查找,如下:

点击这个QT样式表参考

(1)搜索栏查找style(样式表)
(2)选择这个小写的
(3)看到QLabel
(4)这里写着"See Customizing QFrame for an example (a QLabel derives from QFrame)."看Customizing QFrame作为例子,所以我们点击Customizing QFrame

复制这个下来,粘贴到样式表,如下:

这些我们都用不到,border-image:跟着就是资源文件,我们将例子的也删除,然后点击添加资源,如下:

选择我们的资源文件,点击确定后如下:

可以看到我们的图片只展示一下部分,因为"background"就是填充图片的意思,我们应该修改成border(按照Label大小添加图片),如下:

然后点击运行如下:

可以看到窗口成功显示样式表(注意添加资源文件的时候,图片不要带有中文,如果有中文需要在图片文件改名,然后重新添加资源文件,添加完后,执行 qmake = 让 qmake 重新读 .pro 和 .qrc,生成新的 Makefile;旧依赖被清掉,再重新构建就正常了。)
(3)修改样式表文字的颜色和字体大小
接着是修改文字的颜色和字体大小,很简单如下:

3.qss文件的使用
什么是qss文件?
QSS 文件是使用 Qt 程序相关联的样式表文件。它由 GUI 元素的外观和感觉,包括布局,颜色,鼠标的行为,大小和字体。它的风格,一个可以合并到一个 UI(用户界面)。与 HTML 的 CSS 类似,Qt 的样式表是纯文本的格式定义,在应用程序运行时可以载入和解析这些样式定义,从而使应用程序的界面呈现不同的效果。
下面我们通过例子来了解到底为什么要使用qss文件:
下面我们不再通过ui文件设置界面,而且是通过代码设计界面,如下:
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QLabel>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
this->setStyleSheet("QWidget{background-color:black}"); //设置窗口样式表为黑色
this->resize(800,480); //设置窗口大小,不然会随机窗口大小
QLabel *qlabel=new QLabel(this); //实例化QLabel对象
qlabel->setStyleSheet("QLabel{background-color:white}"); //设置QLabel背景颜色
qlabel->setGeometry(0,0,100,100); //设置QLabel位置
qlabel->setText("hello"); //设置文字
//ui->setupUi(this); //不用ui文件设置界面
}
Widget::~Widget()
{
delete ui;
}
效果显示如下:

我们可以看到,上面在设置窗口和 QLabel 背景时,setStyleSheet 里塞了大量字符串。这种写法在样式较少时还能接受,一旦界面控件多了,代码就会变得冗长、杂乱,不利于阅读和维护。所以更推荐的做法是:把样式代码单独写到 QSS 文件中,然后在程序里加载这个文件,让样式与业务逻辑分离,界面代码会更加简洁美观。
3.1 创建 QSS 文件
在 Qt Creator 中创建 QSS 文件非常简单,步骤如下:
第一步:添加新文件
右键左侧项目名,右键点击res,选择「Add New...」,然后在弹窗中选择 QT →QT资源文件,点击 Choose。


第二步:命名文件名

把文件名写成 style.qss,注意后缀名必须是 .qss,然后点击完成。
建议把 QSS 文件也添加到 .qrc 资源文件中,这样发布程序时样式文件会被一并打包。
3.2 在 QSS 文件中编写样式
打开 style.qss,把之前写在代码中的样式字符串原样搬进来即可:
css
QWidget{
background-color:black;
}
QLabel{
background-color:white;
color:black;
}
可以看到,QSS 文件的语法和 CSS 非常相似:用「类选择器」指定控件类型,花括号里写样式属性和值。相比一行一行的 setStyleSheet 字符串,QSS 文件层次清晰、结构一目了然。
3.3 在代码中加载 QSS 文件
最后回到 .cpp 文件中,通过文件读取接口加载 QSS 并应用到程序上。重构后的代码如下:
qss文件:
cpp
QWidget{
background-color:black;
}
QLabel{
background-color:white;
color:black;
}
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QLabel>
#include <QFile>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
/*判断文件是否存在*/
QFile file(":/style.qss");
/*以只读的方式打开*/
if (file.open(QFile::ReadOnly))
{
/*以字符串的形式保存读出的结果*/
QString styleSheet = QLatin1String(file.readAll());
/*设置全局样式*/
this->setStyleSheet(styleSheet);
/*关闭文件*/
file.close();
}
this->resize(800,480); //设置窗口大小,不然会随机窗口大小
QLabel *qlabel=new QLabel(this); //实例化QLabel对象
qlabel->setGeometry(0,0,100,100); //设置QLabel位置
qlabel->setText("hello"); //设置文字
//ui->setupUi(this); //不用ui文件设置界面
}
Widget::~Widget()
{
delete ui;
}
代码说明:

复制这个qss文件路径粘贴到if()判断语句就行了
- 使用
QFile读取资源文件中的style.qss,路径/style.qss前面加:表示从资源中读取; - 读取成功后通过
setStyleSheet统一应用到窗口及其子控件; - 原来分散在各处的
setStyleSheet字符串全部移动到了 QSS 文件中,业务逻辑只保留控件的创建和布局。
运行后效果与之前完全一致,但代码却清爽了很多。以后想修改界面样式,只需要打开 style.qss 单独编辑即可,不用再在 C++ 代码里翻找样式字符串。这就是 QSS 文件存在的意义------让界面样式与业务代码解耦,提升可读性和可维护性。
4.QPushButton
(1)pushButton控件的四种信号
前面我们已经简单了解过了QPushButton这个控件,我们知道该控件有个信号就是clicked()函数(就是点击一下按键),那它还有没有其它信号呢?有的,我们可以通过帮助文档查找一下,如下:

我们可以看到除了clicked(),还有其它三个信号,根据它们的名字就不难猜出,中间两个分别是按下,我们通过代码来验证一下:

我们先创建一个pushButton,然后右键点击它,选择这四个信号,然后系统会帮我们创建这四个槽,如下:

槽函数我们实现串口打印,然后点击一下按键,串口就分别打印了「按下」「松开」「点击」,符合这三个信号函数。
那 toggled() 信号又是什么呢?我们看到这个信号带一个 bool 类型参数,也就是会返回 true 或 false。它表示按钮的选中状态:按钮从"未选中"变为"选中"时,参数为 true;从"选中"变为"未选中"时,参数为 false。
但要注意,普通的 QPushButton 默认是不带选中状态的,点击后会自动弹回来,所以想要使用 toggled() 信号,必须在构造函数中先调用:
cpp
ui->pushButton_2->setCheckable(true);
setCheckable(true) 表示让按钮允许按钮拥有"选中/未选中"两种状态 。设置之后,每点击一次按钮,它都会在选中和不选中之间来回切换,也就是"按下一次选中,再按一次取消选中"(就像我们的插排开关,按下不会再弹起来,要再次按下才会弹起来)。
这时再实现 toggled 槽函数,示例代码如下:
cpp
void Widget::on_pushButton_2_toggled(bool checked)
{
qDebug() << "toggled:" << checked;
}
运行后,第一次点击按钮,串口打印 toggled: true;第二次点击,串口打印 toggled: false,这样不断交替。
所以 toggled(bool checked) 和 clicked() 的区别主要在于:
clicked():每次点击都会触发,不关心按钮是否选中;toggled():只有开启setCheckable(true)后,按钮状态真正发生改变时才触发,并且会携带当前选中状态。
实际开发中,当我们做一个"开关"一类的按钮时,toggled() 信号就非常有用,比如点击一次开灯,再点击一次关灯。串口打印如下:


补充:其实 clicked() 也有带 bool 参数的重载版本,参数表示点击时按钮是否处于选中状态。为了更直观地看清这几个信号到底谁先谁后,下面是在 Qt 5.12.9 上实测得到的信号触发顺序:
| 场景 | 实测信号顺序(Qt 5.12.9) |
|---|---|
普通按钮 click() |
pressed → released → clicked(checked=false) |
checkable 按钮 click() |
pressed → toggled(true/false) → released → clicked(true/false) |
代码调用 setChecked() |
toggled(新状态),不产生 clicked |
从表中可以总结出三点规律:
- clicked:每次点击都发,带点击后的状态;
- toggled:选中状态真正改变才发,程序调 setChecked() 也算,所以它不等于"被点击";
- checkable 时 toggled 比 clicked 先发
理解了这套顺序,在写槽函数时就不会踩坑:比如想在"状态真正切换后"再做处理,用 toggled() 更合适;而只想响应"用户点了一下"这个动作,用 clicked() 即可。
(2)设计pushButton按键的图标
在前面的学习中,我们了解了样式表,资源文件的添加,以及qss文件使用,下面我们将通过前面所学的知识设计pushButton按键的图标,怎么做呢?如下:
首先我们需要找到两个图标(以及这两个图标的不同形态),然后添加到项目的文件夹下,如下(注意,文件不能带中文):

接着我们将它们添加到资源文件下,右键res.qrc文件,选择添加现有文件,如下:

为什么要添加到res.qrc资源文件夹呢?因为该文件夹有我们的qss文件,这样在发布程序的时候样式表文件可以一并打包,添加好后,我们去到qss文件设计样式表,我们可以先设计窗口的背景板颜色,如下:


如图一,我们可以看到界面上有两个窗口和一个 pushButton。为了方便观看和设计,我们可以给它们分别取一个有意义的名字(也就是修改控件的 objectName)。图二就体现了取名字的好处:名字区分清楚后,我们就能在 QSS 文件中分别选中这两个窗口,给它们设置不同的背景板样式。
例如,两个窗口可以分别命名为 widget 和 widget_2,在 QSS 中使用 QWidget#widget 和 QWidget#widget_2 这样的「ID 选择器」来精确指定样式。这样两个窗口虽然都是 QWidget,却可以拥有不同的背景颜色或背景图片,而不会互相影响。
(注意:如果在qss设置控件的样式表,就不要在ui设计器也设置控件的样式表了,如果同一个控件在show()之前被连续设了两次样式表,第二次的样式对控件自己和整颗子树都不再生效--styleSheet()属性读出来是新字符串,但渲染还在用第一次的规则,所以如果再ui文件设计了控件的styleSheet()就要先清除掉该属性)
接着就是设计pushButton的图标,怎么设计呢?我们可以参考我们将样式表添加到label的格式,如下:

url()括号里面的路径,我们可以复制左边资源文件的这四个图标的路径,如下:

然后点击运行,看一下有没有设计成功,如下:

可以看到 pushButton 图标变成了这样。接着我们再加一段代码:不按下按钮,只把鼠标悬停在按钮上时,图标也会改变。这就是 QSS 的「状态伪类」------它用来描述控件处于某种状态时的样式 (我们已经调用 checkable,正好用上)。常用的状态伪类如下表:
| 伪类 | 触发条件 |
|---|---|
:hover |
鼠标悬停在控件上 |
:pressed |
鼠标按住控件 |
:checked |
控件是 checkable 且处于选中状态 |
:disabled |
控件被 setEnabled(false) 禁用 |
:focus |
控件获得键盘焦点 |
用法很简单:在普通选择器后面直接跟伪类即可,例如:

创建成功,我们试着把鼠标放到图标上面看看:

可以看到图标改变了,接着我们再添加按键按下图标改变的代码(注意,使用该条件,一定要加上setChecked()函数(允许按钮拥有"选中/未选中"两种状态)),如下:

可以看到,我们按下点击后,图标的样子也改变了,接着再添加,按下时,鼠标悬停在图标上,图标也会改变的代码如下:

整体的代码如下:
cpp
QWidget#widget{background-color:#DCDCDC}
QWidget#widget_2{background-color:#FFFFFF}
QPushButton#pushButton_2{border-image:url(:/icons2.png)}
QPushButton#pushButton_2:hover{border-image:url(:/icons1.png)}
QPushButton#pushButton_2:checked{border-image:url(:/icons3.png)}
QPushButton#pushButton_2:checked:hover{border-image:url(:/icons4.png)}
这样按钮在「普通 / 悬停 / 按住 / 选中」几种状态下就会显示不同的图标,交互效果更直观。注意 :checked 只有在按钮调用了 setCheckable(true) 之后才会生效,正好对应前面讲过的 toggled() 信号。
再补充一下,如果想换窗口的背景颜色,我们可以去网上收缩RGB颜色对照表,上面有对应的颜色的RGB和HEX格式,使用RGB要这样写background-color:RGB(xxx),所以上面我使用的是hex,直接复制就行了(网址:https://www.fuwa.org/)\*\*

5.QRadioButton
QRadioButton控件可以实现互斥的作用,所以使用QRadioButton常常要两个及以上配合使用,不然就达不到互斥的作用了,效果如下:


本节想实现的目的如下:

(1)修改QRadioButton按键前的小图标
可以看到再QRadioButton前面的小图标也是可以改变的,现在我们要实现的第一个效果就是:改变QRadioButton两种状态下前面的小图标,怎么做呢?可以参考我们上节学习的QPushButton,所以我们首先要添加图标到qqs文件,如下:


接着再qqc文件编辑这两个控件的样式表,如下:

indicator就是QRadioButton控件前面的小图标,unchecked和checked对应该按键的两种状态(注意子控件要用::,伪类只有一个:)
我们发现这个图标有点小,所以我们更改一下这个图标大小,如下:

文字也有点小,并且我们再添加3个QRadioButton看看,再改一下:

那这样就完美了吗?我们看看:


如何实现分组互斥:
可以看到这五个按键是互斥的,但我们只想要左边两个互斥,右边三个互斥,所以怎么改呢?如下:

来到ui文件,右键将左边两个按键放一起

然后选择分配给按键组->新键扭组,右边三个划分为另一组:

最终效果如下:

可以看到左边两个按键为互斥,右边三个按键为互斥,你还可以选择这三个按键不互斥,怎么做呢?如下:

左边选择扭组2,然后红框属性取消掉,效果如下:

(2)实现图标隐藏,修改文字状态栏。
怎么实现呢?很简单,前面我们知道怎么修改小图标的大小,那我们直接设置为0不就好了吗?如下:

接着是设置QRadioButton的背景板颜色,不然如上图一样与Widget背景板颜色分不开:

但是我们发现这个文字是不居中的,而且这个按键也没有设置文字居中的方法,所以我们可以用QPushButton来代替,如下:

这样就实现文字居中,但是怎么实现按键互斥啊?其实我们只要将这三个按键设为一组就行了,然后也选择互斥就行了,如下:

然后每个按键属性要选择checked(也就是该按键存在"按下"与"没按下"的状态)

这样就能实现QPushButton按键互斥,效果如下:


最后只要改一下QPushButton按下时和没按下时两种状态的背景板就行了,最终效果如下:


(3)了解QRadioButton信号

我们可以看到该按键与QPushButton拥有的信号是一样的,我们选择toggled(bool)看看,接着系统就会为我们创建好槽,如下:

可以看到,按下按键1后,按键1状态就变成 true;按下按键2后,按键2状态变成 true,按键1状态自动变成 false,说明它们之间是互斥的。
不过 QRadioButton 不用像 QPushButton 那样手动调用 setCheckable(true),因为它本身就是可选中的,天生就带有"选中 / 未选中"两种状态。
下面是总代码:
:/style.qss:
cpp
QRadioButton::indicator:unchecked{border-image:url(:/uncheck.svg)}
QRadioButton::indicator:checked{border-image:url(:/check.svg)}
QRadioButton::indicator{width:32px;height:32px}
QRadioButton{font-size:20px;background-color:#00FFFF}
QPushButton:checked{background-color:#00FF7F}
QPushButton{background-color:#00FFFF}
widget.cpp:
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QFile>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
/*判断文件是否存在*/
QFile file(":/style.qss");
/*以只读的方式打开*/
if (file.open(QFile::ReadOnly))
{
/*以字符串的形式保存读出的结果*/
QString styleSheet = QLatin1String(file.readAll());
/*设置全局样式*/
this->setStyleSheet(styleSheet);
/*关闭文件*/
file.close();
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_radioButton_toggled(bool checked)
{
qDebug()<<"按键1状态:"<<checked<<endl;
}
void Widget::on_radioButton_2_toggled(bool checked)
{
qDebug()<<"按键2状态:"<<checked<<endl;
}
补充:图标文件可以到该网站下载(https://icon-icons.com/zh/icons/button?price=free\&sort=popular)
6.CheckBox
CheckBox主要作用其实在我们学校QRadioButton的时候已经学习了,那就是可以选择多个按钮,但除此之外CheckBox还多了一种状态,那就是半按下状态,不过这种模式我们很少使用,如下:

我们大多数选择使用右边的模式,下面我们来演示一下:


可以看到效果跟我们使用QRadioButton是一样的(注意:我说的效果是指将这几个QRadioButton分为一组,然后取消互斥(属性:exclusive)时是一样的),简单来说QRadioButton自带互斥属性,而且我们的CheckBox自带复选属性。
设计图标样式表和添加qss文件的流程是一样的,这里不做赘述,下面我们来学习一下它的三态选择按钮模式
我们创建好QCheckBox控件后,点击该控件,属性拉到底部,就会看到下面这个属性:

然后我们点击红框这个属性(tristate)选择:真,如下

然后右键点击这个控件,选择转到槽

然后选择下面这个信号

系统就会自动帮我们创建好槽,如下:

arg1是三种状态(跟我们pdTRue和pdFalse是一样的),也就是枚举,如下图:

所以我们使用swich{}来识别控件不同的状态下执行什么代码,如下:

创建好后,我们点击CheckBox看看会有什么反应,如下:



其实这种模式很少用到,该模式就介绍到这里
7.margin&padding
margin: 一个控件的边框到另一个控件的边框的距离,属于容器外部距离
padding: 自身边框到自身内部另一个容器边框之间的距离,属于容器内距离
怎么理解呢?如下图:

下面我们通过按照上图的样式表设计QPushButton控件的样式表进一步了解,如下:

我们先设计QPushButton的背景板颜色,如上,效果如下:

同时我还设置margin为50px,可以看到该控件变小了并且四周多出来一段距离(这段距离就是控件到margin的距离,如上图为50px),那我是怎么知道的呢?通过帮助文档,如下:

(1)帮助文档搜索style
(2)按Ctrl+F(页内查找),搜索margin
(3)这是设计margin样式表的代码演示
(4)这是能使用margin的控件
因为border没有填充颜色所以我们观察不太清楚,现在我们先给它填上颜色看看,怎么填呢?还是通过帮助手册查找:

我们按照这个示例写上去(solid就是实心填充),效果如下:

再把margin改成margin-right:

也就是说margin是设置控件边框到原控件边框的距离,接着我们试试看把border改成border-top会怎么样,如下:

从这可以看出border是控件边框到控件内部padding边框的距离,接着我们再添加padding看看:

color:字体颜色,font-size:字体大小
可以看到我们设置padding-right后,字体往左边移动了,说明padding就是自身边框到字体边框的距离,没错字体也是有边框的(从我们设置font-size就可以知道),我们再改一下看看:


可以看到,当把 padding 设置为 40px 时,文字有一部分被裁剪掉了。这正好说明:padding 是控件边框到内容(文字)边框之间的距离。padding 越大,内容离边框越远;如果控件本身不够大,内容就可能显示不下。

总结一下:
margin:控件边框到其他控件边框的距离,属于控件外部的距离;border:控件边框,可以设置宽度、颜色、样式(如solid);padding:控件边框到内部内容(如文字、图标)之间的距离,属于控件内部的距离。
所以布局时可以这样理解:margin 管自己和别人之间的间距,padding 管自己和内容之间的留白。
8.水平布局 QHBoxLayout(Horizontal Layout):
QBoxLayout 继承 QLayout。QBoxLayout 类提供水平或垂直地排列子部件。QBoxLayout 获取从它的父布局或从 parentWidget()中所获得的空间,将其分成一列框,并使每个托管小部件填充一个框。
涉及的属性有:
spacing:间隔距离
stretch:拉伸因子
sizePolicy:大小策略
什么意思呢?我们通过例子来了解:
(1)spacing
我们添加三个控件,然后鼠标左键画出一个矩形包围它们,然后点击左边控件中的Horizontal Layout,这三个控件就会变成水平布局,如下图:

然后我们可以看到右下角是关于该布局的属性,我们修改spacing属性看看会发生什么:

(2)Stretch
如上图,我们可以看到控件之间的距离变大了,所以spacing就是设置在这个布局中,各个控件之间的间隔,接着我们再修改Stretch属性看看:

可以看到,我们修改Stretch为1,3,5后,这三个控件的水平长度比例变成了1、3、5,我们再点击运行,生成窗口看看,如下:

我们发现我们改变窗口后,这三个控件并不会随着我们窗口的大小改变而跟着改变,这不是我们想要的效果,那如何修改呢?如下:

我们来带widget的源文件,然后调用setLayout()函数,参数传入ui->horizontalLayout就行了,效果如下;

可以看到,就算把窗口修改到最小,这三个控件都不会消失,而且这三个控件只能缩小到默认尺寸,不能再小下去了,回到布局属性,我们还可以看见我们上小节学习的margin,我们试着修改一下,看看效果,如下:

可以看到空间与布局边框之间的间隔变大了,所以这四个margin就是设置控件边框与布局边框之间的间隔。
(3)sizePolicy
那什么是大小策略呢?我们点击第一个控件看看:

可以看到右边属性栏有个sizePolicy,我们点击展开就可以看到这些策略如下:

我们试着修改水平策略为ignored(忽略),效果如下:


可以看到本来控件只能缩小到默认尺寸,现在就能之间缩小到为0,所以ignored就是忽略这个只能缩小到默认尺寸的限制,我们再试着修改垂直策略看看,修改为Expanding(扩展):

可以看到该按键将垂直空间扩展完,这就是Expanding的作用,其它等我们用到再继续研究
9.垂直布局 QVBoxLayout(Vertical Layout)
其实垂直布局与水平布局的用法是一样的,只不过一个是水平来布局这些控件,一个是垂直来布局这些控件,我们来看看实际效果:
(1)设置垂直布局
我们新添加三个控件,然后选择垂直布局,如下:



可以看到这三个控件就变成了垂直布局,然后试着修改它们的间隔距离和拉伸因子,如下:

我们发现修改了Stretch(拉伸因子)好像没有如何变化啊,这是为啥?我们点击这三个控件属性看看:

可以发现,是因为这些控件属性的垂直策略为Fixed(固定),所以我们修改了它们的拉伸因子也不会发生变化,所以我们将这三个控件的垂直策略改成ignored看看,如下:
(2)设置布局嵌套

可以看到左边三个选择垂直布局的控件的垂直拉伸因子比例为1,1,2,那现在我们蓝框是要干嘛?这是将这两个布局又设置成一个布局,也就是嵌合布局,我们选择水平布局,如下:


可以看到,现在我们已经在垂直布局和水平布局的外面又包了一层水平布局,这就是 布局嵌套。所谓布局嵌套,就是把一个布局当成"大控件",继续放到另一个布局里面,而不是最外层只能直接放控件。
这样做的好处是:界面可以同时兼顾水平和垂直两个方向。比如左边是一个垂直布局,里面控件从上往下排列;右边是一个水平布局,里面控件从左往右排列;最外层再用一个水平布局把左右两部分并排放在一起。这样即使界面控件很多,结构也依然清晰,各个区域互不影响。
如果想要垂直布局占比多一点,可以设置外面这层水平布局的拉伸因子,如下:

接着我们点击运行,生成窗口看看:

可以看到这两个布局不随着窗口变化而变化,所以我们按照我们上小节学的方法去Widget源文件设置一下窗口,如下:

注意这次传入的参数是ui->horizontalLayout_2,也就是最外层的水平布局,效果如下:

可以看到内部布局随窗口变化了,我们还可以修改一下垂直布局内的控制的水平策略,这样修改窗口,也可以把这些控件大小变为0,如下:

10.网格布局(QGridLayout):
这个也很简单,我们添加6个控件,如何选择网格布局,如下:


可以看到这六个控件各占一个网格,我们看到属性也多个几个,我们修改一下这些属性看看:

row(就是行)、column(就是列),所以
(1)就是一行之间的最小距离大小,也就是每个网格的最小宽度
(2)就是设置一列之间的距离大小,也就是每个网格的最小长度
我们还可以设置控件的大小和在网格的位置,怎么做呢?点击想要设置的控件,如下:

可以看到控件属性就又最小Size和最大Size,我们设置一下看看

可以看到该控件就变成这样长和宽都为40个像素点的控件,但是我们想要控件居中怎么做?先右键点击控件:

按上图选择,效果如下:

11.不使用ui编辑器,实行上诉所学:
(1)按钮实验
本次实验目的:将使用QPushButton控件并选择该控件具有两种状态(即setCheckbel)实行对串口的打印(按下控件串口打印"打开串口",控件弹起打印"串口关闭"),接着使用QRadioButton控件实行对串口波特率的选择(涉及知识:qss文件编辑QRadioButton控件样式表、connect()函数(信号与槽连接函数))
widget.h:
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QPushButton>
#include <QRadioButton>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void pushButton_slot(bool checked);
void radioButton1_slot(bool checked);
void radioButton2_slot(bool checked);
private:
Ui::Widget *ui;
QPushButton *pb1;
QRadioButton *rb1;
QRadioButton *rb2;
};
#endif // WIDGET_H
widget.cpp:
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QFile>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
// ui->setupUi(this); //不用ui文件编辑
/*判断文件是否存在*/
QFile file(":/style.qss");
/*以只读的方式打开*/
if (file.open(QFile::ReadOnly))
{
/*以字符串的形式保存读出的结果*/
QString styleSheet = QLatin1String(file.readAll());
/*设置全局样式*/
this->setStyleSheet(styleSheet);
/*关闭文件*/
file.close();
}
pb1=new QPushButton(this); //示例化QPushButton类对象,并把widget作为它的父对象
pb1->setGeometry(400,200,100,50);
pb1->setText("打开串口");
pb1->setCheckable(true);
connect(pb1,&QPushButton::toggled,this,&Widget::pushButton_slot);
rb1=new QRadioButton(this);
rb2=new QRadioButton(this);
rb1->setGeometry(600,200,100,50);
rb1->setText("9600");
rb2->setGeometry(600,400,100,50);
rb2->setText("115200");
connect(rb1,&QRadioButton::toggled,this,&Widget::radioButton1_slot);
connect(rb2,&QRadioButton::toggled,this,&Widget::radioButton2_slot);
}
void Widget::pushButton_slot(bool checked) //告诉编译器这个Widget类的成员函数
{
if(checked)
{
qDebug()<<"打开串口"<<endl;
}
else
{
qDebug()<<"串口关闭"<<endl;
}
}
void Widget::radioButton1_slot(bool checked)
{
if(checked)
{
qDebug()<<"波特率为9600"<<endl;
}
}
void Widget::radioButton2_slot(bool checked)
{
if(checked)
{
qDebug()<<"波特率为1155200"<<endl;
}
}
Widget::~Widget()
{
delete ui;
}
.qss文件如下:
cpp
QRadioButton::indicator:unchecked{border-image:url(:/uncheck.svg)}
QRadioButton::indicator:checked{border-image:url(:/check.svg)}
实验现象如下:




补充:
两个 radio 按钮属于同一个父控件,而 autoExclusive (互斥)默认值就是 true,所以不需要额外写任何互斥代码;
代码流程总结:
三查法:写代码前只查三处
- 查类的属性表(Properties)------想知道"能设置什么";
- 查信号表(Signals)------想知道"什么时候能响应";
- 查文档末尾的 Example------抠一个能跑的最小片段。
举个例子,需求是"点按钮,文字在'打开/关闭'之间切换":
- QPushButton 文档 → Properties 里有 checkable、text、checked → 对应 setCheckable()、setText()、isChecked()
- Signals 里有 clicked(bool)、toggled(bool) → 选 toggled
- 拼起来:
如下:

(1)帮助手册查找QPushButton
(2)找到Properties(属性)
(3)这是抽象类的按键,顾名思义就是实现按键功能的,如下:

你点击这些属性,它还会给示例,如下:

(4)关于窗口的属性,那自然是关于控件样式表的属性,如下:

QPushButton控件表:
| 项 | QPushButton |
|---|---|
| 创建 | new QPushButton("文本", parent) |
| 常用设置 | setText / setGeometry / setEnabled / setCheckable / setStyleSheet |
| 常用读取 | text() / isChecked() / isEnabled() |
| 信号 | clicked(bool) / toggled(bool) / pressed() / released() |
这张表其实就是上面「三查法」的落地结果:创建看构造函数,设置看 Properties 对应的 set 函数,读取看 get 函数,信号看 Signals 表。以后遇到别的控件(QLabel、QRadioButton、QCheckBox 等),都可以照这个格式自己整理一张,用起来比翻文档快得多。
QRadioButton控件表:
| 项 | QRadioButton |
|---|---|
| 创建 | new QRadioButton("文本", parent) |
| 常用设置 | setText / setGeometry / setChecked(true) / setAutoExclusive / setEnabled / setStyleSheet |
| 常用读取 | text() / isChecked() / isEnabled() / autoExclusive() |
| 信号 | clicked(bool) / toggled(bool) / pressed() / released() |
和 QPushButton 的差异(记这几条就够):
- 默认就是 checkable:QRadioButton 构造时就
checkable=true,不用(也不该)再调setCheckable(true)。 - 互斥规则:同一父控件下的兄弟 radio +
autoExclusive(默认 true)自动互斥;换了父控件就不互斥------这种情况要用QButtonGroup。 - 点击已选中的 radio 不会取消选中(互斥组里总有一个是选中的),这和 checkable 的 QPushButton 不一样(它点第二次会取消)。
- QSS 只认
::indicator这个子控件:QRadioButton::indicator:checked/:unchecked,记得设width/height(SVG 不会自动定尺寸)。 - 选一个 → radio;选多个 → QCheckBox。
配套:QButtonGroup(跨父控件互斥 / 要拿编号时用)
cpp
QButtonGroup *group = new QButtonGroup(this);
group->addButton(radio1, 0); // 0/1 是自定义 id
group->addButton(radio2, 1);
connect(group, QOverload<int>::of(&QButtonGroup::buttonClicked),
this, [](int id){ qDebug() << "选中了" << id; });
int cur = group->checkedId(); // 当前选中项的 id
(2)布局实验
widget.h:
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QPushButton>
#include <QRadioButton>
#include <QHBoxLayout>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void pushButton_slot(bool checked);
void radioButton1_slot(bool checked);
void radioButton2_slot(bool checked);
private:
Ui::Widget *ui;
QPushButton *pb1;
QPushButton *pb2;
QPushButton *pb3;
QRadioButton *rb1;
QRadioButton *rb2;
QHBoxLayout *hlayout;
};
#endif // WIDGET_H
widget.cpp:
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QFile>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
// ui->setupUi(this); //不用ui文件编辑
/*判断文件是否存在*/
QFile file(":/style.qss");
/*以只读的方式打开*/
if (file.open(QFile::ReadOnly))
{
/*以字符串的形式保存读出的结果*/
QString styleSheet = QLatin1String(file.readAll());
/*设置全局样式*/
this->setStyleSheet(styleSheet);
/*关闭文件*/
file.close();
}
this->resize(800,480); //设置Widget窗口大小,不然会被布局的大小覆盖
pb1=new QPushButton(this); //示例化QPushButton类对象,并把widget作为它的父对象
pb1->setGeometry(400,200,100,50);
pb2=new QPushButton("开关2",this); //示例化QPushButton类对象,并把widget作为它的父对象
pb2->setGeometry(400,200,100,50);
pb3=new QPushButton("开关3",this); //示例化QPushButton类对象,并把widget作为它的父对象
pb3->setGeometry(400,200,100,50);
pb1->setText("打开串口");
pb1->setCheckable(true);
connect(pb1,&QPushButton::toggled,this,&Widget::pushButton_slot);
// rb1=new QRadioButton(this);
// rb2=new QRadioButton(this);
// rb1->setGeometry(600,200,100,50);
// rb1->setText("9600");
// rb2->setGeometry(600,400,100,50);
// rb2->setText("115200");
// connect(rb1,&QRadioButton::toggled,this,&Widget::radioButton1_slot);
// connect(rb2,&QRadioButton::toggled,this,&Widget::radioButton2_slot);
hlayout=new QHBoxLayout(this); //实例化水平布局
hlayout->addWidget(Widget::pb1,1); //将Widget的控件加入到hlayout布局, 参数二是设置控件的拉伸因子
hlayout->addWidget(Widget::pb2,2);
hlayout->addWidget(Widget::pb3,3);
hlayout->setSpacing(20); //设置控件之间的间隔
hlayout->setContentsMargins(20,0,20,0); //设置窗口边框与控件边框之间的距离
pb1->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding); //设置控件的大小策略,选择允许拉伸
pb1->setMaximumWidth(100); //设置控件的宽
pb1->setMaximumHeight(50); //设置控件的高
}
void Widget::pushButton_slot(bool checked) //告诉编译器这个Widget类的成员函数
{
if(checked)
{
qDebug()<<"打开串口"<<endl;
}
else
{
qDebug()<<"串口关闭"<<endl;
}
}
void Widget::radioButton1_slot(bool checked)
{
if(checked)
{
qDebug()<<"波特率为9600"<<endl;
}
}
void Widget::radioButton2_slot(bool checked)
{
if(checked)
{
qDebug()<<"波特率为1155200"<<endl;
}
}
Widget::~Widget()
{
delete ui;
}
实验效果:

再次总结一下我是怎么知道这些布局是用什么函数,有属性的,如下:
这里改成四查法:写代码前查四处
- 查类的属性表(Properties)------想知道"能设置什么";
- 查类的函数表(Public Functions)------知道设置这些属性应该用什么函数;
- 查信号表(Signals)------想知道"什么时候能响应";
- 查文档末尾的 Example------抠一个能跑的最小片段。
例如:
我是怎么知道addWidget()函数就是将Widget的控件加入到hlayout布局, 参数二是设置控件的拉伸因子?如下:

(1)还是老样子帮助手册查找该类名
(2)可以搜索property(属性)
(3)这就是QHBoxLayout类的属性继承哪个基类,点进去看看

1.这就是我用到的将Widget的控件加入到hlayout布局函数,你可以点击该函数,里面就有使用介绍,如下:

其实,上图只是 addWidget 某个重载成员函数的简要注解,并且只解释了第一个参数。点击(4)该函数说明中的「继承成员函数」,可以看到更完整的介绍,如下所示:

这个重载成员函数的说明就清晰多了,从中可以确认第二个参数用来设置该控件在布局中的拉伸因子,也就是我前面提到的 stretch 参数。
2.这就是设置我们的控件间隔大小函数
3.这是设置设置窗口边框与控件边框之间的距离函数
(4)这就是介绍该类的成员函数继承哪些基类的成员函数,这里的函数更好,介绍的也更详细,如上面说的 addWidget,我们可以点进去看到:

1.这个就是将两个布局加入到外层布局的函数,使用方法注解如下;

将两个布局加入到外层布局以及实现两组QRadiboButton的代码示例:
用 QGroupBox 作为父容器,同时实现「视觉分组边框」和「逻辑互斥隔离」
cpp
w.setWindowTitle("单选分组示例");
// ----- 第1组:2个单选按钮,父容器为 group1 -----
QGroupBox *group1 = new QGroupBox("分组1", &w);
QRadioButton *optA = new QRadioButton("选项A", group1);
QRadioButton *optB = new QRadioButton("选项B", group1);
optA->setChecked(true); // 默认选中
QVBoxLayout *lay1 = new QVBoxLayout(group1);
lay1->addWidget(optA);
lay1->addWidget(optB);
// ----- 第2组:3个单选按钮,父容器为 group2 -----
QGroupBox *group2 = new QGroupBox("分组2", &w);
QRadioButton *opt1 = new QRadioButton("选项1", group2);
QRadioButton *opt2 = new QRadioButton("选项2", group2);
QRadioButton *opt3 = new QRadioButton("选项3", group2);
opt1->setChecked(true); // 默认选中
QVBoxLayout *lay2 = new QVBoxLayout(group2);
lay2->addWidget(opt1);
lay2->addWidget(opt2);
lay2->addWidget(opt3);
// 主窗口布局
QVBoxLayout *mainLay = new QVBoxLayout(&w);
mainLay->addWidget(group1);
mainLay->addWidget(group2);
2.上诉提及的addWidget
后面查找关于怎么在布局内设置控件的大小是通过查找QPushButton的函数知道的(包括设置大小策略、控件的最小尺寸和最大尺寸)
后面的垂直布局不再赘述,流程大致相同
(3)布局控件表
| 项 | 布局(QVBoxLayout / QHBoxLayout / QGridLayout / QFormLayout) |
|---|---|
| 创建 | new QVBoxLayout(parent) / new QHBoxLayout(parent) / new QGridLayout(parent) / new QFormLayout(parent)(或先 new、再对控件调 setLayout()) |
| 添加 | addWidget(widget, stretch=0, alignment=0) / addLayout(子布局, stretch=0) / addStretch() / addSpacing(px) |
| 常用设置 | setSpacing(px) / setContentsMargins(l,t,r,b) / setStretch(i, n) / setAlignment(w, Qt::AlignLeft) |
| 常用读取 | spacing() / contentsMargins() / count() / itemAt(i) |
| 信号 | 无(布局只负责算位置,不是信号源) |
各布局的"专属"添加方式:
| 布局 | 专属函数 | 排列方式 |
|---|---|---|
| QVBoxLayout | addWidget(w, stretch, align)(继承自 QBoxLayout) |
纵向,从上到下 |
| QHBoxLayout | 同上 | 横向,从左到右 |
| QGridLayout | addWidget(w, row, col, rowSpan=1, colSpan=1)、addLayout(l, row, col) |
网格,行列从 0 开始 |
| QFormLayout | addRow("标签", 控件)、addRow(控件)、addRow(QLabel*, 控件) |
两列:左标签右控件 |
补充说明:
QVBoxLayout和QHBoxLayout都继承自QBoxLayout,所以添加控件的函数完全一样,区别只在于排列方向;QGridLayout的行列下标都从 0 开始,rowSpan/colSpan用来让一个控件跨多行或多列;QFormLayout是专门为「表单」设计的,一行就是「左边标签 + 右边输入控件」,所以它不需要手动指定行列,直接addRow即可。
各自多出来的设置:
| 布局 | 常用设置 | 常用读取 |
|---|---|---|
| QGridLayout | setRowStretch(row, n) / setColumnStretch(col, n) / setRowMinimumHeight / setColumnMinimumWidth |
rowCount() / columnCount() / itemAtPosition(row, col) |
| QFormLayout | setLabelAlignment(Qt::AlignRight) / setRowWrapPolicy(...) / setFieldGrowthPolicy(...) / setHorizontalSpacing / setVerticalSpacing |
rowCount() / itemAt(row, role) |
三条必须记住的规则
- 布局不是控件(QLayout 继承 QObject),自己不显示,只是"位置计算器"------所以布局没有信号。
- addWidget 会把控件 reparent 到布局所属的控件;进了布局就别再 setGeometry,会被布局覆盖。
- 嵌套用 addLayout;一个控件只能有一个顶层布局------.ui 里已经有布局时,代码里要 ui->xxxLayout->addWidget(...),不能 new QVBoxLayout(this)(会警告 already has a layout)。
12.QSpacer(隔离弹簧):
QSpacerItem 继承 QLayoutItem。QSpacerItem 类在布局中提供空白(空间间隔)。所以QSpacerItem 是在布局中使用的。它包含 Horizontal Spacer(水平间隔)与 Vertical Spacer(垂直间隔)。
帮助文档资料如下:

使用方法很简单,代码如下:
widget.h
cpp
#include <QSpacerItem>
QSpacerItem *hsapcer;
widget.cpp
cpp
hsapcer=new QSpacerItem(100,10,QSizePolicy::Expanding,QSizePolicy::Expanding); //实例化水平间隔对象
hlayout=new QHBoxLayout(this); //实例化水平布局
hlayout->addWidget(pb1,1); //将Widget的控件加入到hlayout布局, 参数二是设置控件的拉伸因子
hlayout->addSpacerItem(hsapcer); //加入水平间隔
hlayout->addWidget(pb2,2);
hlayout->addSpacerItem(hsapcer); //加入水平间隔
hlayout->addWidget(pb3,3);
上面的代码是在之前的设置布局代码的基础上添加
效果如下:


可以看到弹簧是"一个可以无限伸缩的空位",作用是吃掉剩余空间,从而间接决定别的控件被推到哪儿。