Qt——3、常用控件

常用控件

1、控件概述

Widget是Qt中的核心概念。英文原义是小部件,我们此处也把它翻译为控件。控件是构成一个图形化界面的基本要素。
实现图形化界面的程序,Qt已经给我们提供了很多的控件,所以我们需要学习和了解这些控件,学会如何使用这些控件。

Qt作为一个成熟的GUI开发框架,内置了大量的常用控件。这一点在Qt Designer中就可以看到端倪。并且Qt也提供了自定义控件的能力,可以让程序猿在现有控件不能满足需求的时候,对现有控件做出扩展,或者手搓出新的控件。

另外在上古时期,开发GUI并没有控件的概念。界面上显示的东西都是画出来,这时候开发一个图形化界面的程序就得先画出一个窗口(矩形)。后来控件的概念就被引入了,但是早期的控件比较简单,数量也比较有限。随着时代发展,新的GUI开发体系越来越丰富,提供的控件数量和质量也越来越高。比如前端开发一个比较知名的框架:element-ui。

虽然Qt提供的控件也不少,但是整体来说还是比现代化的控件体现要逊色一筹,不过Qt还提供了一些优化手段可以让控件变得更好看。Qt也提供了Qt Design Studio。


2、QWidget核心属性

Qt中各种控件都是继承自QWidget类。所以QWidget中的内容是Qt控件体系中通用的内容。

在Qt Creator右侧可以看到QWidget的各种属性,并且还能直接编辑,上面的所有属性都可以通过Qt的文档来了解。不过不需要所有都记下来,可以了解其中一部分,当需要的使用的时候再去查文档。

2.1、核心属性概览

下面表格列出了QWidget中的属性及其作用:


2.2、enabled

enabled描述了一个控件是否处于可用状态。

下面我们创建一个项目,实现如下功能:一个按钮可以点击,点击后输出调试信息。另外一个按钮用来控制前一个按钮是否可用。

前面我们也说过,默认我们用图形化界面创建控件会给控件一个objectName,但是在同一个界面中,控件的objectName不能重复。而自动生成objectName的规律就是:控件类型+下划线+数字,比如pushButton和pushButton_2,不过显然用数字的方式来命名并不好,所以后面可以自己修改。这里涉及到了Qt的元编程技术,Qt会根据ui文件生成一个ui_widget.h文件,生成过程中会根据控件的objectName的值作为控件变量名。


2.3、geometry

geometry翻译就是几何的意思,包含了以下属性:


另外Qt也对一些几何概念进行了封装,比如QPoint表示一个点,QRect表示一个矩形。

下面我们创建新项目,实现以下功能:首先创建一个target按钮,objectName设置为pushButton_target,然后再分别创建四个按钮up、down、left、right,分别设置objectName为pushButton_xxx,然后实现当点击这四个按钮,修改target按钮的位置(上、下、左、右移动)。

运行程序后,我们发现实际上效果是:调整左上角的位置,但是左上角位置改变的同时高度和宽度也发生了变化。
但是我们想实现的效果是让整个target按钮平移,左上角的位置改变但是高度和宽度不变。我们刚才的代码是修改QRect对象的x和y,这样会导致QRect宽度高度发生变化,那么如何才能实现平移的效果呢?这就需要使用上面给出的setGeometry另一个函数重载版本。

下面再写一个例子,要实现的功能是:上面QLabel显示向女神表白的信息,然后下面有两个按钮,一个欣然接受并把objectName设置为pushButton_accept,另一个残忍拒绝objectName为pushButton_reject。当点击接受修改上方按钮的信息,当点击拒绝的时候随机移动该按钮到其他位置。

但是这里是点击之后,也就是按压之后再松开才会移动,我们还可以改为当按下去就移动,编辑ui文件,然后对残忍拒绝按钮右键转到槽选择pressed(),然后修改代码即可实现。

甚至我们还可以做到,当鼠标放到残忍拒绝按钮上就让按钮移动,不过这个需要使用到Qt的事件机制,这个后续再说。
下面还需要介绍一个window frame:

如图所示,widget的范围就是黑色方框的范围,而里面的按钮相对位置就是以左上角黑色的点为标准设置的。但是实际上我们运行程序的时候会发现上面还有一小块标题的内容,另外整个黑色方块周围也会多出一小部分。整体来说这就是window frame框架。

在Qt中关于位置尺寸提供了很多的API。有的API位置信息是以Widget本体左上角为原点的,不考虑window frame。有的API位置信息是以window frame左上角为原点的。比如:geometry、setGeometry不考虑window frame。比如:frameGeometry和setFrameGeometry考虑window frame。


下面我们进行演示,创建一个新项目,在Widget的构造函数中分别调用geometry和frameGeometry获取到rect,并使用qDebug输出,查看是否有差异。

我们发现输出的两个rect是一样的,这是因为当前的代码放到了Widget的构造函数中,此时该对象正在构造还没有被加入到window frame中,因此看不到window frame的影响。
所以下面我们修改代码,创建一个按钮连接点击信号和槽,在槽函数中获取并输出查看差异。



2.4、windowTitle


因为QWidget是一个广泛的概念,控件都是继承自QWidget的,所以只有最顶层的窗口设置windowTitle才有意义。

比如在刚才的代码上添加一行给代码设置windowTitle,实际上设置了之后并没有任何效果,也没有报错。


2.5、windowIcon

下面代码示例:在Widget构造函数中创建一个QIcon,然后调用setWindowIcon。运行程序后发现窗口左上角图标发生了变化,并且在开始栏中也发生了变化。

1、在这里我们是在栈上创建的QIcon对象。之前推荐在堆上创建对象主要是因为要确保当前控件的生命周期是足够的,要通过Qt对象树来释放对象。而QIcon是一个比较小的对象,创建出来就设置了所以并不会影响最终的结果。另外QIcon也不支持对象树机制,无法给他设置父对象。
2、我们上面的路径字符串里面是用反斜杠作为路径分隔符的,但是由于反斜杠会进行转义,所以我们用了两个反斜杠来表示一个反斜杠。除了这种方式我们还可以直接使用斜杠/作为路径分隔符。另外,C++11引入了raw string解决上面的问题,也就是字符串不包含任何转移字符,所有的字符都不会被转义。写法如:R"(d:\rose.jpg)"
3、我们上面都是采用绝对路径的方式来获取图片的,但是这种方式是不科学的,因为你写的程序最终是要发布到用户的电脑上的,但是你并不能确保你开发机器上的路径和用户电脑上的路径一致。比如:我们开发机从D盘中获取图片,但是用户电脑上并没有D盘。因此使用相对路径是比较好的。但是哪怕使用相对路径,比如在当前项目目录下创建一个专门的文件夹来保存图片,但是也可能会被用户不小心删除掉了,这样还是会找不到。因此这就需要引入qrc机制。
4、qrc机制:给Qt项目引入一个额外的xml文件,后缀名用.qrc表示。在这个xml中把要使用的图片资源导入进来,并在xml中记录下来。Qt在编译项目的时候会根据qrc中描述的图片信息,找到图片内容并提取出里面的二进制数据。把这些二进制数据转换为C++代码,最终编译到exe文件里面。但是qrc也有缺点:无法导入太大的文件,比如导入几个GB视频文件,这样就会导致编译时间特别长,而且整个exe文件也会变得很大。
qrc机制就是为了解决上述两个问题的:1、确保你的图片所在路径在用户目标机器上存在。2、确保你的图片不会被用户整没了。

下面创建一个进行代码示例:
1、在项目中创建一个qrc文件,文件名不要带中文和特殊符号。

2、把图片导入到qrc文件中。

首先创建一个前缀,点击Add prefix然后设置前缀为/即可。

前缀可以理解成虚拟的目录。这个目录并没有在你的电脑上真实存在,是Qt抽象出来的。qrc机制本质就是把图片的二进制数据转换成C++代码,最终就会在代码中看到一个很大的char数组,这个数组里面存的就是图片的二进制数据。而在C++代码访问图片还是需要通过路径,所以Qt自己抽象出了虚拟目录。

添加前缀后旁边的Add Files按钮就可以使用了,这时候添加图片文件进来。

在以前,如果添加文件和我们创建resource.qrc文件不在同级目录下,或者不在同级目录的子目录里面,就会如上图显示无效文件路径。处理方法就是将图片拷贝到和resource.qrc文件同级目录下再重新添加即可。但是现在不会显示这个问题了。不过我们这里还是把图片拷贝过来再添加。

看到上面这个效果就说明添加成功了,后面代码访问的时候只需要加上一个冒号:前缀,接着后面的路径就是你创建的前缀+文件名,比如:/Qt.png。

另外,既然我们前面说了会转换成C++代码,那么我们可以去看看,找到构建项目生成的临时文件build-xxx目录,再debug目录下有一个qrc_resource.cpp文件,打开看看。


这里的qt_resource_data里面存的数据就是Qt.png中每个字节的数据。当Qt项目编译的时候,这个cpp文件会被一起编译到exe中,当exe程序运行,上面的图片内容也就被加载到内存中了。


2.6、windowOpacity


代码示例:实现两个按钮,一个+按钮,objectName设置为pushButton_add,另一个-按钮,objectName设置为pushButton_sub,连接clicked信号和槽,槽函数分别设置opacity增加或者减少0.1。

两个问题:
1、窗口的不透明度变化并不是精准的。


我们在槽函数中使用qDebug()输出了opacity,可以看到不透明度的减少并不是精确0.1的减少。这里就涉及到浮点数的存储了,float和double的存储方式如下:

一个浮点数被分为三部分:符号位、有效数字、指数部分。第一个有效数字表示0.5,第二个表示0.25,第三个表示0.125,比如101就表示0.625,因此要表示0.1这样的小数很难凑出一个很接近的数。所以就如上面输出的信息,每次都不是精确的减少。这种体系优点就是运算速度快,占用空间小。缺点是有些小数无法精确表示。因此对于两个浮点数不能直接使用==判断是否相等,而是让两个浮点数作差看看绝对值是否小于允许的期望误差范围。

2、上面的代码在设置opacity之前,判断了opacity的范围,然后再决定是否设置。实际上可以不写,因为在setWindowOpacity里面也有对传入的值进行判断,但是为什么我们还是写上了?
这就是防御性编程,因为一个比较大的项目是由很多人完成的,一个项目会拆分成几个模块,不同的人复杂不同模块,模块之间往往需要进行交互,比如模块A提供API接口给模块B调用。那你在使用别人提供的API接口你怎么知道别人是否对某个参数进行了范围判断?所以你在调用这个接口之前自己也需要做一下判断。
因此需要双重判定(double check),因为函数的定义和调用可能是两伙人,万一某一方失误了没有判定,所以如果使用double check,某一方出现失误也不会产生严重后果。


2.7、cursor

cursor表示的是鼠标的形状。

下面实现一个案例:

通过图形化界面的方式创建一个QPushButton,然后可以通过右边的属性找到cursor进行修改。另外我们也可以采用代码的方式进行修改:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"

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

    QCursor cursor(Qt::WaitCursor);
    ui->pushButton->setCursor(cursor);
}

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

// 以下是枚举的鼠标形状种类
enum CursorShape {
      ArrowCursor,
      UpArrowCursor,
      CrossCursor,
      WaitCursor,
      IBeamCursor,
      SizeVerCursor,
      SizeHorCursor,
      SizeBDiagCursor,
      SizeFDiagCursor,
      SizeAllCursor,
      BlankCursor,
      SplitVCursor,
      SplitHCursor,
      PointingHandCursor,
      ForbiddenCursor,
      WhatsThisCursor,
      BusyCursor,
      OpenHandCursor,
      ClosedHandCursor,
      DragCopyCursor,
      DragMoveCursor,
      DragLinkCursor,
      LastCursor = DragLinkCursor,
      BitmapCursor = 24,
      CustomCursor = 25
  };

除了Qt提供的鼠标样式,我们也可以通过自定义图片来设置光标,准备好一张图片,然后先导入qrc中,在代码中访问这张图片,基于这张图片构造出光标对象并设置。

1、QPixmap是Qt提供的一个用于屏幕显示优化的图像类,它通常存储在显存(GPU)或与窗口系统相关的内存中,专门为快速绘制(paint)而设计。
2、通过QPixmap提供的scaled函数可以对图片进行缩放,让我们的鼠标形状不那么大。但是这里是一个新的图片,所以需要重新赋值。
3、通过QPixmap直接构造QCursor对象的话默认鼠标点击相当于是图片的左上角原点点击,如果像上面这样构造,相当于是以左上角原点为基准,在点(10, 10)处点击。


2.8、font

QFont相当于是对字体各种样式的封装,如下:

下面写个代码示例:

我们通过编辑ui文件创建了一个QLabel,可以直接通过右边的属性来修改QLabel的样式,而且还支持实时预览,不过下面我们演示通过代码修改。


2.9、toolTip

toolTip指的是当你鼠标放到某个控件上会有提示说明,比如下图中windows中的画图。一个GUI程序界面可能比较复杂,按钮比较多,当你把鼠标悬停在这个控件上可以弹出一个提示说明。

下面实现一个样例:编辑ui文件创建一个yes按钮(pushButton_yes),一个no按钮(pushButton_no),分别给两个按钮设置toolTip和提示时间。


2.10、focusPolicy

这个是设置控件获取到焦点的策略。比如某个控件能否用鼠标选中或者能否通过tab键选中。
比如界面上有一个输入框,必须要选中这个输入框接下来键盘输入的内容才会到输入框中,如果焦点没有在这个输入框中,键盘上输入的内容就不会到输入框。再比如红警游戏,需要选中一些士兵或者工人,此时就需要把焦点放到这些士兵或者工人上,给他们下达命令,让他们去打仗或者建造工厂。再比如线上的笔试,你在笔试的网页做题此时你的焦点就在当前笔试的网页,而当你切换到其他网页,人家就能立刻感知到网页失去焦点。

一般来说,一个控件获取焦点主要是两种方式:1、鼠标点击。2、键盘的tab键。

下面创建四个输入框,测试键盘点击和tab键,发现四个输入框都是正常的可以获取焦点的。

然后可以对四个输入框的右侧属性中的focusPolicy进行修改,自行验证。


2.11、styleSheet

这个属性是通过CSS来设置widget的样式的。

CSS中可以设置的样式属性非常多。基于这些属性Qt只能支持其中一部分,称为QSS (Qt Style Sheet)
。具体的支持情况可以参考Qt文档中Qt Style Sheets Reference章节。

下面我们先进行初步演示,我们先创建一个QLabel对象。

可以直接通过右侧属性栏找到styleSheet,然后点击...可以直接添加。


第二种方式就是右击空间,然后选择改变样式表也可以进行设置。


上面是我们设置的样式,可以看到,QSS和CSS类似,也是采用键值对的格式。键和值用冒号分隔,键值对之间用分号分隔。

下面我们实现一个功能:创建一个比较大的编辑框,然后底下有两个按钮,一个可以设置夜间模式,一个可以设置日间模式。设置这两种模式我们主要通过修改背景颜色和字体颜色来实现。

千万要注意这里设置格式的时候,拼写不能出错,如果拼写出错不会有任何报错,但是样式不会生效!
另外我们还发现点击日间模式后颜色和我们刚开始启动程序窗口的背景颜色不太一样。

首先我们需要知道计算机中表示颜色的方式,在计算机中使用RGB来表示颜色,red、green、blue三种颜色进行不同比例混合,就能构成不同的颜色。计算机中,通常会一个字节来分别表示R、G、B,所以R、G、B的取值范围都是0~255。比如最终表示一个颜色采用:rgb(255, 0, 255),这就表示R是255红色拉满了,G是0绿色没有,B是2555蓝色也拉满。再比如:#FF00FF,其中R是FF拉满了,G是00绿色没有,B是FF也拉满了。
所以一般我们设置颜色都不会直接用上面的单词,而是采用这两种表示颜色的方式进行设置。那么我们如何得知程序刚启动时widget的背景颜色呢?这就需要使用取色器了,比如QQ的截图就自带取色器。

关于QWidget的属性大概就介绍这些,后续用涉及到其他属性内容可以参考Qt官方文档。

最终再说一个属性accessibleName。这个属性是给盲人/视障人士使用的。现在的手机或电脑都支持无障碍功能,比如windows中的讲述人。


3、按钮类控件

3.1、QPushButton

使用QPushButton表示一个按钮。这也是当前我们最熟悉的一个控件了。
QPushButton继承自QAbstractButton。这个类是一个抽象类。是其他按钮的父类。


前面说到QWidget是所有空间的公共部分,所以后续讲述的所有空间都有前面讲述的所有属性。
QAbstractButton是一个抽象类,这个类包含了纯虚函数,无法实例化出对象,需要子类继承该类并重写虚函数,才能够创建出子类的实例。


Qt的API设计风格是非常清晰的。此处列出的属性都是可以获取和设置的。例如:使用text()获取按钮文本;使用setText()设置文本。

下面演示给QPushButton设置一个icon,首先还是创建一个qrc文件然后导入图片。

还可以给图标设置尺寸,但是这里的参数是QSize,所以上面构造了一个匿名的QSize对象作为参数。

下面我们实现之前实现过的一个功能,就是一个target按钮,然后下方四个上下左右移动按钮,点击可以移动target按钮。只不过在原来的基础上我们加上图标icon和快捷键shortCut。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"

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

    ui->pushButton_target->setIcon(QIcon(":/images/Linux.png"));
    ui->pushButton_target->setIconSize(QSize(200, 200));

    ui->pushButton_up->setIcon(QIcon(":/images/up.png"));
    ui->pushButton_up->setIconSize(QSize(50, 50));
    ui->pushButton_down->setIcon(QIcon(":/images/down.png"));
    ui->pushButton_down->setIconSize(QSize(50, 50));
    ui->pushButton_left->setIcon(QIcon(":/images/left.png"));
    ui->pushButton_left->setIconSize(QSize(50, 50));
    ui->pushButton_right->setIcon(QIcon(":/images/right.png"));
    ui->pushButton_right->setIconSize(QSize(50, 50));
}

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

void Widget::on_pushButton_up_clicked()
{
    QRect rect = ui->pushButton_target->geometry();
    ui->pushButton_target->setGeometry(rect.x(), rect.y()-5, rect.width(), rect.height());
}

void Widget::on_pushButton_down_clicked()
{
    QRect rect = ui->pushButton_target->geometry();
    ui->pushButton_target->setGeometry(rect.x(), rect.y()+5, rect.width(), rect.height());
}

void Widget::on_pushButton_left_clicked()
{
    QRect rect = ui->pushButton_target->geometry();
    ui->pushButton_target->setGeometry(rect.x()-5, rect.y(), rect.width(), rect.height());
}

void Widget::on_pushButton_right_clicked()
{
    QRect rect = ui->pushButton_target->geometry();
    ui->pushButton_target->setGeometry(rect.x()+5, rect.y(), rect.width(), rect.height());
}

上面的代码我们是初步实现了用图标表示pushButton,同时关联四个按钮的槽函数,实现之前一样的功能。接下来我们需要给按钮设置快捷键,同时再实现一个鼠标点击能够重复触发的功能。
下面先看setShortCut函数:

cpp 复制代码
void setShortcut(const QKeySequence &shortcut)

这个函数的参数是QKeySequence,key表示按键,sequence表示序列,因为按下的按键并不一定是单个案件,也可能是组合键,比如ctrl + c或者ctrl + v。

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    ui->pushButton_target->setIcon(QIcon(":/images/Linux.png"));
    ui->pushButton_target->setIconSize(QSize(200, 200));

    ui->pushButton_up->setIcon(QIcon(":/images/up.png"));
    ui->pushButton_up->setIconSize(QSize(50, 50));
    ui->pushButton_down->setIcon(QIcon(":/images/down.png"));
    ui->pushButton_down->setIconSize(QSize(50, 50));
    ui->pushButton_left->setIcon(QIcon(":/images/left.png"));
    ui->pushButton_left->setIconSize(QSize(50, 50));
    ui->pushButton_right->setIcon(QIcon(":/images/right.png"));
    ui->pushButton_right->setIconSize(QSize(50, 50));

    ui->pushButton_up->setShortcut(QKeySequence("w"));
    ui->pushButton_down->setShortcut(QKeySequence("s"));
    ui->pushButton_left->setShortcut(QKeySequence("a"));
    ui->pushButton_right->setShortcut(QKeySequence("d"));
}

设置方式可以像上面这样直接通过按键的名字,这种方式简单,但是容易写错。
建议使用下面这种枚举的方式来设置:

cpp 复制代码
ui->pushButton_up->setShortcut(QKeySequence(Qt::Key_W));
ui->pushButton_down->setShortcut(QKeySequence(Qt::Key_S));
ui->pushButton_left->setShortcut(QKeySequence(Qt::Key_A));
ui->pushButton_right->setShortcut(QKeySequence(Qt::Key_D));

再比如使用组合键:

cpp 复制代码
ui->pushButton_up->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_W));

再比如添加鼠标连击:

cpp 复制代码
ui->pushButton_up->setAutoRepeat(true);
ui->pushButton_down->setAutoRepeat(true);
ui->pushButton_left->setAutoRepeat(true);
ui->pushButton_right->setAutoRepeat(true);

3.2、QRadioButton

QRadioButton是单选按钮。可以让我们在多个选项中选择一个。

下面实现一个选择性别的功能:

也可以在构造函数中添加默认选项,禁用其他选项,代码如下:

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    // 设置默认选项并禁用其他选项
    ui->label->setText("你选择的性别是: 男");
    ui->radioButton_male->setChecked(true);
    ui->radioButton_other->setEnabled(false);
}

前面我们对于按钮连接的信号都是clicked无参数的,接下来我们要对其他信号函数进行连接,看看有什么区别。

我们创建四个单选按钮分别连接上面四个信号函数,槽函数我们做一个简单的调试输出即可。

带bool参数的clicked信号,当我们点击之后,会把参数传递给槽函数,槽函数可以获取到这个checked是否选中的状态。pressed就是当鼠标按下那一刻就触发了,released是当鼠标按下释放之后才会触发。也就是说clicked=pressed+released。然后toggled是当状态发生变化才会触发,比如checked从false变成true、从true变成false。


下面再实现一个功能,我们都吃过麦当劳或肯德基,一般一个套餐需要选主食、小食、饮料,这三类又有很多个选项,我们就可以通过单选按钮来实现。

现在我们实现的界面有三个种类,每个种类又有不同的选项,但是我们发现选择主食之后再选择小食,此时主食的单选按钮也会被取消,这是因为QRadioButton本身是排他的。但是我们希望,组与组之间的按钮不要排他,只要组内的按钮排他。所以我们可以使用QButtonGroup类来对单选按钮进行分组。


3.3、QCheckBox

QCheckBox表示复选按钮。可以允许选中多个。和QCheckBox最相关的属性也是checkable和checked,都是继承自QAbstractButton。QCheckBox独有的属性tristate用来实现三态复选框。这个东西比较冷门,咱们课堂不做讨论。
下面就写个今天安排的多选按钮。


4、显示类控件

4.1、QLabel

QLabel可以用来显示文本和图片。

下面创建一个项目,项目中创建三个QLabel对象,分别设置上面的三种textFormat进行观察。

纯文本就是将所有的字符都直接输出。富文本可以解释html标签,比如上面使用的b标签就是加粗的作用。markdown文本可以解释markdown格式,比如上面采用的一级标题。

下面再写一个样例:就是通过QLabel来显示图片,不过这里要让图片充满整个Widget的窗口。因此我们需要修改QLabel的大小,同时通过设置上面的scaledcontents让图片铺满整个QLabel。至于这里的图片我们就可以通过qrc引入。

上面的代码效果如右图,但是这里有个问题就是,当我用鼠标拖拽窗口右下角进行放大时,我们会发现此时图片是固定不变的,并不会随着窗口的增大自适应增大。这是因为我们在构造函数中设置的QLabel大小之后,QLabel的大小就固定了,此时我们修改窗口大小的时候并不会修改QLabel的大小,因此QLabel不变,图片的大小也就不会变化。
那么如果我们想要实现上述功能,就需要引入Qt中的事件机制。用户的操作不仅对应信号,还对应事件。实际上当用户拖拽窗口大小的时候,就会触发resize事件(resizeEvent),像resize这样的事件是连续变化的,比如把窗口从A拖拽到B的过程中,会触发一系列的resizeEvent。
因此可以借助resizeEvent来实现上述功能,可以让Widget窗口类重写父类QWidget的resizeEvent虚函数。重写了虚函数后就是多态,在鼠标拖拽窗口的过程中这个函数会被反复执行,每次触发一个resizeEvent都会调用一次虚函数。


此处的形参event是非常有用的,这里就包含了触发这个resize事件这一时刻窗口尺寸的数值。event->size()返回一个QSize对象。

下面实现一个例子:创建四个QLabel对象,分别设置不同的文本对齐方式。
对其方式有如下的枚举类型:

cpp 复制代码
enum AlignmentFlag {
    AlignLeft = 0x0001,
    AlignLeading = AlignLeft,
    AlignRight = 0x0002,
    AlignTrailing = AlignRight,
    AlignHCenter = 0x0004,
    AlignJustify = 0x0008,
    AlignAbsolute = 0x0010,
    AlignHorizontal_Mask = AlignLeft | AlignRight | AlignHCenter | AlignJustify | AlignAbsolute,

    AlignTop = 0x0020,
    AlignBottom = 0x0040,
    AlignVCenter = 0x0080,
    AlignBaseline = 0x0100,
    // Note that 0x100 will clash with Qt::TextSingleLine = 0x100 due to what the comment above
    // this enum declaration states. However, since Qt::AlignBaseline is only used by layouts,
    // it doesn't make sense to pass Qt::AlignBaseline to QPainter::drawText(), so there
    // shouldn't really be any ambiguity between the two overlapping enum values.
    AlignVertical_Mask = AlignTop | AlignBottom | AlignVCenter | AlignBaseline,

    AlignCenter = AlignVCenter | AlignHCenter
};

AlignHCenter就是水平居中对齐,H就是horizontal。AlignVCenter就是垂直居中对齐,V就是vetical。
同时我们为了能够方便观察效果需要在每个QLabel中设置右侧属性中QFrame中的frameShape为Box,让他有一个边框的效果。

下面继续演示自动换行、缩进、边距功能。

默认不设置自动换行的话,如果文本超出了QLabel的宽度不会显示。另外如果设置了缩进并且设置了自动换行,那么每一行都会缩进。

最后演示一下QLabel设置伙伴关系buddy。

首先创建两个单选按钮,然后创建两个QLabel,里面分别写上快捷键 &A/B。接着在构造函数中给QLabel设置伙伴。

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    ui->label->setBuddy(ui->radioButton);
    ui->label_2->setBuddy(ui->radioButton_2);
}

显示效果如图:

此时我们按键盘上的alt + a / alt + b,就可以选择对应的按钮。
Qt中QLabel写的是文本,但是可以指定快捷键,在文本中使用&跟上一个字符来表示快捷键。比如&A通过键盘上的alt + a触发,&B就通过键盘上的alt + b触发。绑定了伙伴关系后就可以选中对应的单选框/复选框。


4.2、QLCDNumber

QLCDNumer是一个专门用来显示数字的控件。类似于老式计算器的效果。
核心属性如下:

下面实现一个样例:使用QLCDNumber显示一个初始数值10,然后每隔一秒钟就让该值减1,一直到0为止,实现一个类似倒计时的功能。
那么具体如何实现让QLCDNumber每秒减1呢?这就需要周期性执行某个逻辑。需要用到定时器,C++标准库空中并没有提供这类功能,boost库里面则有,Qt中也封装了对应的定时器QTimer,此处我们就可以使用QTimer来实现。通过QTimer创建出来的对象,会产生一个timeout信号,可以通过start启动定时器,并设定触发timeout信号的周期,可以通过stop停止定时器。
QTimer结合了Qt中信号和槽的机制,因此可以用connect把timeout信号绑定到槽函数中,当timeout信号触发在槽函数中修改QLCDNumber的值,即可实现倒计时功能。

那么上面采用的方式是定时器QTimer,能不能采用sleep休眠的方式来实现呢?每次sleep 1秒,然后让QLCDNumber的值减去1。还有个问题是如何休眠,以前我们用的是C语言的接口Sleep,这个是在Windows.h头文件里面的,在C++11中引入了sleep_for,因此我们可以使用这个函数。

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    int value = ui->lcdNumber->intValue();
    while (value > 0) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        ui->lcdNumber->display(--value);
    }
}

但是上面的代码并不会实现倒计时的功能,原因是在main函数中是先构造Widget对象,先执行构造函数,然后再显示界面。因此构造函数已经走完了才会显示,此时界面上的值就是0。

那换一种思路:在构造函数中创建一个线程,在新线程中执行上述循环+更新操作呢?

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    int value = this->ui->lcdNumber->intValue();
    std::thread t([this]() {
        int value = this->ui->lcdNumber->intValue();
        while (value > 0) {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            this->ui->lcdNumber->display(--value);
        }
    });
}


我们发现出现了一个异常,这是因为在Qt中有一个专门的线程去负责维护更新界面,这个线程就是主线程,main函数所在的线程。因为对于GUI来说,内部包含了很多隐藏状态,Qt为了保证在修改界面的过程中线程安全不会收到影响,Qt禁止了其他线程直接修改界面。
所以我们对于界面的所有修改操作都必须在主线程中完成。另外对于Qt的槽函数,默认情况下槽函数都是由主线程调用的,所以在槽函数中修改界面是没有问题的。在main函数中有一句return a.exec();这会让主线程进入事件循环,每执行一次循环都会有一些固定的事情要做。


4.3、QProgressBar

QProgressBar表示一个进度条。核心属性如下:

下面实现一个样例:创建一个进度条,设置初始值为0,然后让进度条随着时间的增长而增长,假设每隔100ms让进度条数值加1。

这个例子和上面倒计时类似,使用QTimer的一个计时器功能。但是这里有一个问题,在.cpp文件中我们包含了QTimer类的定义,所以我们可以使用QTimer,可以调用他的成员函数。但是在.h文件中我们并没有包含QTimer的头文件,但是却可以声明QTimer变量,而不会报错,这是为什么呢?
这是通过Qt内部提供的一个特殊技巧来实现的。在Qt中有一个专门的头文件用来包含Qt所有类的前置声明,这个头文件一般不会直接接触,但是包含其他头文件的时候比如上面的QWidget,都会间接包含这个头文件。因此在.h头文件中,实际上前面还有类似class QTimer;等这样的前置声明,所以不会报错。

但是,如果要真正的使用QTimer,比如创建实例、使用里面的成员,就需要包含QTimer头文件,因为里面包含了QTimer的详细定义。
那么为什么要有这个技巧?能解决什么问题?
主要是用来解决编译速度的问题,C/C++项目对比其他的语言,编译速度是很慢的。而编译速度又和#include头文件有关系,一个项目的#include头文件里面的关系错综复杂,所以需要尽可能的减少#include头文件个数,这样可以有效的减少编译时间。所以Qt就使用class前置声明的方式来减少头文件的包含,这样Qt中的头文件包含其他头文件的数量就能减少,提高编译速度。
同时C++20引入了module模块,通过import使用模块,减少编译时间。

下面,我们创建一个红色的进度条。


这里我们通过设置QSS样式,让进度条的背景颜色变为红色。但是当设置完之后,进度条数值会跑到内部的左上角去,这个可能是个BUG,可以自行把他设置居中或者其他位置。


4.4、QCalendarWidget

QCalendarWidget表示一个日历,形如:

核心属性如下:

重要信号如下:

下面写一个样例:创建一个QCalendarWidget和QLabel,当选择日期发生变化就修改QLabel的值。


5、输入类控件

5.1、QLineEdit

QLineEdit用来表示单行输入框。可以输入一段文本,但是不能换行。核心属性如下:

核心信号如下:

下面实现一个样例:让用户输入个人信息姓名、密码、性别、电话,然后通过提交按钮统一获取。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

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

    ui->lineEdit_name->setPlaceholderText("请输入姓名");
    ui->lineEdit_name->setClearButtonEnabled(true);

    ui->lineEdit_password->setPlaceholderText("请输入密码");
    ui->lineEdit_password->setEchoMode(QLineEdit::Password);
    ui->lineEdit_password->setClearButtonEnabled(true);

    ui->lineEdit_phone->setPlaceholderText("请输入手机号码");
    ui->lineEdit_phone->setClearButtonEnabled(true);
    ui->lineEdit_phone->setInputMask("000-0000-0000");
}

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

void Widget::on_pushButton_clicked()
{
    QString s = ui->radioButton_male->isChecked() ? "男" : "女";
    qDebug() << "姓名: " << ui->lineEdit_name->text() << "\n"
             << "密码: " << ui->lineEdit_password->text() << "\n"
             << "性别: " << s << "\n"
             << "电话: " << ui->lineEdit_phone->text();
}

这里我们设置了输入的格式,但是inputMask功能比较有限,只能设置简单的验证。如果我们要设置更加严格的验证方式就需要使用到正则表达式,关于正则表达式的概念请自行了解。
下面针对正则表达式实现一个样例:用户输入手机号码,接着我们进行验证,如果验证通过下方的按钮可以点击提交,否则下方的提交按钮始终处于禁用状态。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QRegExpValidator>

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

    ui->lineEdit->setPlaceholderText("请输入手机号码");
    ui->lineEdit->setClearButtonEnabled(true);
    ui->pushButton->setEnabled(false);
    // 给单行输入框注册一个验证器
    QRegExp regexp("^1\\d{10}$");
    ui->lineEdit->setValidator(new QRegExpValidator(regexp));
}

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

void Widget::on_lineEdit_textEdited(const QString &text)
{
    QString content = text;
    int pos = 0;
    if (ui->lineEdit->validator()->validate(content, pos) == QValidator::Acceptable) {
        ui->pushButton->setEnabled(true);
    } else {
        ui->pushButton->setEnabled(false);
    }
}

效果如图:

1、上面正则表达式的含义:^1表示以1开头,^表示以xxx开头。\d表示数字0-9,但是C++中字符\会进行转移,所以需要两个反斜杠来表示一个反斜杠。{10}表示前面的数字重复出现10次。$表示结尾。
2、validate函数的参数是QString引用和int引用。通过该函数来验证输入框的值是否合法,第一个参数表示要验证的字符串,第二个参数表示如果验证失败是从哪个位置开始不符合的。返回值State是枚举类型,有如下三种。我们可以自定义个一个Validator来重写validate函数,上面使用的QRexExpValidator是Qt中内置的类型。

cpp 复制代码
virtual State validate(QString &, int &) const = 0;
enum State {
    Invalid,
    Intermediate,
    Acceptable
};

下面实现一个样例:两个输入框分别输入密码,接着验证两次输入的密码是否一致,通过输入框上方的label进行提示。

最后实现一个样例:默认一个输入框输入密码,但是显示方式是QLineEdit::Password,然后通过上方的单选按钮可以实现修改密码的显示方式。


5.2、QTextEdit

QTextEdit表示多行输入框。也是一个富文本 & markdown 编辑器。并且能在内容超出编辑框范围时自动提供滚动条。核心属性如下:

核心信号如下:

下面实现一个样例:创建一个QLabel和一个QTextEdit,当多行输入框中的内容发生变化时同步到QLabel中。

这里需要注意的是,获取多行编辑框里面的文本使用的函数时toPlaintText,与之对应的还有toHtml和toMarkdown。

下面验证一下其他的槽函数:textChanged、selectionChanged、cursorPositionChanged、undoAvailable、redoAvailable。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

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

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

void Widget::on_textEdit_textChanged()
{
    qDebug() << "textChanged: " << ui->textEdit->toPlainText();
}

void Widget::on_textEdit_selectionChanged()
{
    QTextCursor cursor = ui->textEdit->textCursor();
    qDebug() << "selectionChanged: " << cursor.selectedText();
}

void Widget::on_textEdit_cursorPositionChanged()
{
    QTextCursor cursor = ui->textEdit->textCursor();
    qDebug() << "cursorPositionChanged: " << cursor.position();
}

void Widget::on_textEdit_undoAvailable(bool b)
{
    qDebug() << "undoAvailable: " << b;
}

void Widget::on_textEdit_redoAvailable(bool b)
{
    qDebug() << "redoAvailable: " << b;
}

void Widget::on_textEdit_copyAvailable(bool b)
{
    qDebug() << "copyAvailable: " << b;
}

selectionChanged:当选择的字符串改变了就会触发。
cursorPositionChanged:当光标的位置改变就会触发。
undoAvailable:当输入内容可以进行ctrl + z撤销的时候就会触发,此时槽函数参数为true。
redoAvailable:当ctrl+z撤销之后就会触发,此时可以进行ctrl+y取消撤销,此时槽函数参数为true。而undoAvailable的参数变成false。
copyAvailable:当选中字符串时触发,此时可以进行复制操作,此时槽函数参数为true。


5.3、QComboBox

QComboBox表示下拉框。核心属性如下:



这里的activated信号就是当弹出下拉框之后鼠标滑过某个选项,此时会带有高亮的效果,这时候就表示激活。

下面实现一个样例:还是之前的麦当劳点餐功能,只不过此时选择的方式不再是单选框,而是通过下拉框的方式来选择,然后提供一个pushButton,当选择好了之后连接pushButton的clicked信号,在槽函数中获取选择的内容并输出。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

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

    ui->comboBox->addItem("麦辣鸡腿堡");
    ui->comboBox->addItem("巨无霸");
    ui->comboBox->addItem("培根蔬萃双层牛堡");

    ui->comboBox_2->addItem("麦乐鸡块");
    ui->comboBox_2->addItem("麦辣鸡块");
    ui->comboBox_2->addItem("中薯条");

    ui->comboBox_3->addItem("可乐");
    ui->comboBox_3->addItem("雪碧");
    ui->comboBox_3->addItem("咖啡");
}

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

void Widget::on_pushButton_clicked()
{
    qDebug() << "汉堡: " << ui->comboBox->currentText()
             << "小食: " << ui->comboBox_2->currentText()
             << "饮料: " << ui->comboBox_3->currentText();
}

上面是通过代码的方式来实现,也可以通过图形化界面的方式来添加item,右击下拉框,选择编辑项目即可进行添加。

但是下拉框里面的内容,很多时候都不是写死的,而是通过文件或者网络加载数据得到的。下面我们在实现一个样例:从本地中读取文件,把读取到的内容加载到下拉框中。


首先通过图形化界面的方式创建一个QLabel和一个QComboBox,然后接着在桌面上创建一个txt文件,内容如下所示:

接着在代码中实现打开文件,根据文件流循环读取文件中的每行数据,然后将读取到的数据添加到下拉框中。

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    std::ifstream ifs(R"(C:\Users\25617\Desktop\config.txt)");
    if (!ifs.is_open()) {
        qDebug() << "打开文件失败";
        return;
    }
    std::string line;
    while (std::getline(ifs, line))
    {
        ui->comboBox->addItem(QString::fromStdString(line));
    }
    ifs.close();
}


这里需要注意的是comboBox的addItem函数的参数是QString,然后我们从文件中读取的字符串是std::string,所以需要利用QString::fromStdString函数将std::string转换成QString。同样的,如果一个QString的对象s,也可以通过s.toStdString()从QString转换成std::string。


5.4、QSpinBox

使用QSpinBox或者QDoubleSpinBox表示微调框,它是带有按钮的输入框。可以用来输入整数/浮点数。通过点击按钮来修改数值大小。

这里说明一下前缀和后缀,有的输入的内容可能是这样的:第5个,那么5就是数值,第就是前缀,个就是后缀。

下面实现一个样例,还是类似麦当劳的点餐,只不过这时候在点餐的基础上我们增加了QSpinBox让用户选择数量。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

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

    ui->comboBox->addItem("麦辣鸡腿堡");
    ui->comboBox->addItem("巨无霸");
    ui->comboBox->addItem("培根蔬萃双层牛堡");
    ui->comboBox_2->addItem("麦乐鸡块");
    ui->comboBox_2->addItem("麦辣鸡翅");
    ui->comboBox_2->addItem("中薯条");
    ui->comboBox_3->addItem("可乐");
    ui->comboBox_3->addItem("雪碧");
    ui->comboBox_3->addItem("咖啡");

    // 针对QSpinBox设置范围
    ui->spinBox->setRange(1, 5);
    ui->spinBox_2->setRange(1, 5);
    ui->spinBox_3->setRange(1, 5);
    // 设置QSpinBox默认值为1
    ui->spinBox->setValue(1);
    ui->spinBox_2->setValue(1);
    ui->spinBox_3->setValue(1);
}

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

void Widget::on_pushButton_clicked()
{
    qDebug() << "当前下单的内容: "
             << ui->comboBox->currentText() << ":" << ui->spinBox->value()
             << ui->comboBox_2->currentText() << ":" << ui->spinBox_2->value()
             << ui->comboBox_3->currentText() << ":" << ui->spinBox_3->value();
}

5.5、QDateEdit && QTimeEdit


这里需要介绍的就是timeSpec,全称为timeSpecification,也就是时间规范。可以设置的选项如上所示,其中UTC表示的是协调时间时,这是标准时间,是科学家通过时间钟得到的。我们所在的时间为北京时间东八区,这个就是localtime,在UTC的基础上加上8h。而在英国的格林威治天文台就是标准时间。

下面实现一个样例,计算两个日期时间之间间隔多少天、多少个小时。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

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

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

void Widget::on_pushButton_clicked()
{
    // 1.获取两个日期时间
    QDateTime oldTime = ui->dateTimeEdit->dateTime();
    QDateTime newTime = ui->dateTimeEdit_2->dateTime();
    qDebug() << oldTime << newTime;
    // 2.计算小时和天数
    int seconds = oldTime.secsTo(newTime);
    int hours = (seconds / 3600) % 24;
    int days = seconds / 3600 / 24;
    ui->label->setText("间隔了 " + QString::number(days) + QString(" 天零 ")
                       + QString::number(hours) + QString(" 个小时"));

}


说明:
1、可以利用QDateTime中提供的daysTo函数计算两个时间间隔的天数,不过使用这个函数要注意如果两天间隔一个是1月1日23::55分,另一个是1月2日00:05分,这样计算出来的也是间隔一天。所以这个并不是非常准确的。
2、可以利用QDateTime中提供的secsTo函数计算两个时间间隔的秒数,这个计算的时间是精确的,因此上面我们通过间隔秒数重新计算出间隔的小时和天数。
3、如果是一个整形要转换成QString类型,可以用QString::number函数来实现。


5.6、QDial

使用QDial表示一个旋钮。

下面通过图形化界面的方式创建一个QDial,然后可以观察右侧属性,QDial是继承自QAbstractSlider,然后QAbstractSlider又继承了QWidget。我们可以手动修改右侧的属性,然后测试一下效果。

下面实现一个样例:创建一个QDial,然后通过编写代码来实现,当调节旋钮的时候,窗口的不透明度发生变化。

首先通过图形化界面创建QDial,然后右侧属性设置最小值和最大值,并设置可以重复循环,刻度线可见。
接着我们右击连接信号和槽

cpp 复制代码
void Widget::on_dial_valueChanged(int value)
{
    double opacity = (double)value / 100;
    this->setWindowOpacity(opacity);
}

那么能否借助这个旋钮来实现调整系统音量呢? 可以是可以,但是调整系统音量涉及到Windows的api,Qt中并没有对此封装,所以此时需要我们自己通过VS封装一下API,然后让Qt来调用动态库。


5.7、QSlider

使用QSlider表示一个滑动条。


下面实现一个样例:创建两个滑动条,一个水平一个垂直,当滑动这两个滑动条时可以调整窗口的大小,水平可以调整窗口的宽度,垂直可以调整窗口的高度。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"

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

    // 设置滑动条的初始值、最小值、最大值、步长
    ui->horizontalSlider->setMinimum(200);
    ui->horizontalSlider->setMaximum(2000);
    ui->horizontalSlider->setValue(800);
    ui->horizontalSlider->setSingleStep(50);

    ui->verticalSlider->setMinimum(200);
    ui->verticalSlider->setMaximum(1500);
    ui->verticalSlider->setValue(600);
    ui->verticalSlider->setSingleStep(50);
}

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


void Widget::on_horizontalSlider_valueChanged(int value)
{
    QRect rect = this->geometry();
    this->setGeometry(rect.x(), rect.y(), value, rect.height());
}

void Widget::on_verticalSlider_valueChanged(int value)
{
    QRect rect = this->geometry();
    this->setGeometry(rect.x(), rect.y(), rect.width(), value);
}

效果如图:

下面实现一个样例:创建一个滑动条,但是此时我们要通过快捷键的方式来操作滑动条,滑动条本身可以通过方向键和pageup、pagedown来操作。但是我们也可以自定义。我们通过QShortcut来自定义两个快捷键,-和=。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QShortcut>

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

    QShortcut* shortCut1 = new QShortcut(this);
    QShortcut* shortCut2 = new QShortcut(this);
    // 设置快捷键
    shortCut1->setKey(QKeySequence("-"));
    shortCut2->setKey(QKeySequence("="));
    // 使用信号槽,感知快捷键被按下,然后槽函数中对value进行加减操作。
    connect(shortCut1, &QShortcut::activated, this, &Widget::subValue);
    connect(shortCut2, &QShortcut::activated, this, &Widget::addValue);
}

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

void Widget::addValue()
{
    int value = ui->horizontalSlider->value();
    if (value >= ui->horizontalSlider->maximum()) {
        return;
    }
    ui->horizontalSlider->setValue(value+5);
}

void Widget::subValue()
{
    int value = ui->horizontalSlider->value();
    if (value <= ui->horizontalSlider->minimum()) {
        return;
    }
    ui->horizontalSlider->setValue(value-5);
}

void Widget::on_horizontalSlider_valueChanged(int value)
{
    ui->label->setText(QString("当前的值为: ") + QString::number(value));
}

6、多元素控件

Qt中提供的多元素控件有:

简单来说,xxView是更底层的实现,xxWidget是基于xxView封装而来的。此处的xxView是MVC结构的一种典型实现。MVC是软件开发中非常经典的软件结构组织形式。其中M是model数据,V是view视图(界面),C是controller控制器实现数据和视图之间的业务流程。
此处的xxView只是负责实现了视图,不负责数据如何存储表示,也不负责数据和视图之间的交互。而xxWidget基于xxView同时把model和controller实现好了,所以我们可以很方便的直接使用。

再说上面的三组结构:
QListWidget和QListView:表示列表,就是一列下来的数据,可以是字符串或者图片等等。
QTableWidget和QTableView:表示表格,是多行多列的数据组织形式。
QTreeWidget和QTreeView:树形结构的组织形式。


6.1、QListWidget



列表中的每个元素/每一项就称为是一个item,更具体地说是通过QListWidgetItem类来表示的。
还有就是这里的insertItem需要传入一个行号,比如传入2,就是在第三行插入,此时插入的元素就变成的第三行。这个就好比赛跑比赛,你超过了第二名你就是第二名的意思。

下面看一下QListWidgetItem的核心属性:

下面实现一个样例:创建一个QListWidget,然后再提供两个按钮和一个输入框,其中一个按钮配合输入框可以添加元素,然后另一个按钮可以根据当前选中的元素进行删除操作。
同时右击有个编辑项目的选项,可以直接通过图形化界面的方式来添加元素,也可以通过下面讲的例子用代码来添加。如果初始内容是固定采用这两种办法中的任意一种都可以,如果需要通过文件/网络来构造数据那就只能通过代码的方式了。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

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

    // 设置一些默认值
//    ui->listWidget->addItem("C++");
//    ui->listWidget->addItem("Java");
//    ui->listWidget->addItem("Python");

    ui->listWidget->addItem(new QListWidgetItem("C++"));
    ui->listWidget->addItem(new QListWidgetItem("Java"));
    ui->listWidget->addItem(new QListWidgetItem("Python"));
}

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

void Widget::on_pushButton_insert_clicked()
{
    QString text = ui->lineEdit->text();
    ui->listWidget->addItem(text);
}

void Widget::on_pushButton_2_clicked()
{
    int row = ui->listWidget->currentRow();
    // 如果什么都没有被选中就会返回-1,此时什么也不做
    if (row < 0) {
        return;
    }
    ui->listWidget->takeItem(row);
}

// 连接信号槽来感知当 QListWidget 选择元素发生变化
void Widget::on_listWidget_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
{
    if (current) {
        qDebug() << "当前选中的元素: " << current->text();
    }
    if (previous) {
        qDebug() << "上次选中的元素: " << previous->text();
    }
}

6.2、QTableWidget

使用QTableWidget表示一个表格控件。一个表格中包含若干行,每一行又包含若干列。表格中的每个单元格,是一个QTableWidgetItem对象。


这里主要要说明的是setHorizontalHeaderItem和setVerticalHeaderItem这两个函数,这两个函数是用来设置表头的,比如下图

这是一张课程表,所以我们可以设置第一行的表头比如周几,第一列的表头比如第几节课等等。

下面实现一个样例:创建一个QTableWidget,然后设置一些初始值,提供四个按钮,分别可以进行新增一行、删除选中行、输入列名新增一列、删除选中列。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"

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

    // 创建3行3列
    ui->tableWidget->insertRow(0);
    ui->tableWidget->insertRow(1);
    ui->tableWidget->insertRow(2);
    ui->tableWidget->insertColumn(0);
    ui->tableWidget->insertColumn(1);
    ui->tableWidget->insertColumn(2);
    // 设置3列表头
    ui->tableWidget->setHorizontalHeaderItem(0, new QTableWidgetItem("学号"));
    ui->tableWidget->setHorizontalHeaderItem(1, new QTableWidgetItem("姓名"));
    ui->tableWidget->setHorizontalHeaderItem(2, new QTableWidgetItem("年龄"));
    // 添加元素
    ui->tableWidget->setItem(0, 0, new QTableWidgetItem("1001"));
    ui->tableWidget->setItem(0, 1, new QTableWidgetItem("张三"));
    ui->tableWidget->setItem(0, 2, new QTableWidgetItem("20"));

    ui->tableWidget->setItem(1, 0, new QTableWidgetItem("1002"));
    ui->tableWidget->setItem(1, 1, new QTableWidgetItem("李四"));
    ui->tableWidget->setItem(1, 2, new QTableWidgetItem("19"));

    ui->tableWidget->setItem(2, 0, new QTableWidgetItem("1003"));
    ui->tableWidget->setItem(2, 1, new QTableWidgetItem("王五"));
    ui->tableWidget->setItem(2, 2, new QTableWidgetItem("20"));
}

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

void Widget::on_pushButton_insertRow_clicked()
{
    int rowCount = ui->tableWidget->rowCount();
    ui->tableWidget->insertRow(rowCount);
}

void Widget::on_pushButton_insertColumn_clicked()
{
    const QString& text = ui->lineEdit->text();
    int columnCount = ui->tableWidget->columnCount();
    ui->tableWidget->insertColumn(columnCount);
    ui->tableWidget->setHorizontalHeaderItem(columnCount, new QTableWidgetItem(text));
}

void Widget::on_pushButton_deleteRow_clicked()
{
    int row = ui->tableWidget->currentRow();
    ui->tableWidget->removeRow(row);
}

void Widget::on_pushButton_deleteColumn_clicked()
{
    int column = ui->tableWidget->currentColumn();
    ui->tableWidget->removeColumn(column);
}

6.3、QTreeWidget

使用QTreeWidget表示一个树形控件。里面的每个元素,都是一个QTreeWidgetItem,每个QTreeWidgetItem可以包含多个文本和图标,每个文本/图标为一个列。 可以给QTreeWidget设置顶层节点(顶层节点可以有多个),然后再给顶层节点添加子节点,从而构成树形结构。



这里需要注意的是QTreeWidget虽然是树形结构,但是这个树形结构没有体现出根节点,是从根节点的下一层子节点开始计算的。

下面实现一个样例:首先创建一个QTreeWidget,然后提供三个按钮和一个输入框,输入框用来获取插入节点的名称,然后三个按钮分别实现添加到顶层节点、添加到选中元素、删除选中元素。

首先可以通过图形化界面的方式去编辑,如上图所示,在项目中点击第一个加号可以添加项目,然后对于添加好了的项目可以继续点击下方的第二个按钮新建子项目。
下面通过代码来实现:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QTreeWidgetItem>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    // 设置根节点的名字
    ui->treeWidget->setHeaderLabel("动物");
    // 新增顶层节点
    QTreeWidgetItem* item1 = new QTreeWidgetItem();
    item1->setText(0, "猫"); // 每个节点可以设置多列,这里为了简单就设置一列
    ui->treeWidget->addTopLevelItem(item1);

    QTreeWidgetItem* item2 = new QTreeWidgetItem();
    item2->setText(0, "狗"); // 每个节点可以设置多列,这里为了简单就设置一列
    ui->treeWidget->addTopLevelItem(item2);

    QTreeWidgetItem* item3 = new QTreeWidgetItem();
    item3->setText(0, "鸟"); // 每个节点可以设置多列,这里为了简单就设置一列
    ui->treeWidget->addTopLevelItem(item3);

    // 给顶层节点添加子节点
    QTreeWidgetItem* item4 = new QTreeWidgetItem();
    item4->setText(0, "中华田园猫");
    item1->addChild(item4);
    QTreeWidgetItem* item5 = new QTreeWidgetItem();
    item5->setText(0, "布偶猫");
    item1->addChild(item5);
}

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

void Widget::on_pushButton_insertTopLevelItem_clicked()
{
    const QString& text = ui->lineEdit->text();
    QTreeWidgetItem* item = new QTreeWidgetItem();
    item->setText(0, text);
    ui->treeWidget->addTopLevelItem(item);
}

void Widget::on_pushButton_insertItem_clicked()
{
    const QString& text = ui->lineEdit->text();
    QTreeWidgetItem* item = new QTreeWidgetItem();
    item->setText(0, text);
    QTreeWidgetItem* currentItem = ui->treeWidget->currentItem();
    currentItem->addChild(item);
}

void Widget::on_pushButton_deleteItem_clicked()
{
     QTreeWidgetItem* currentItem = ui->treeWidget->currentItem();
     if (currentItem == nullptr) {
         return;
     }
     // 删除选中的元素需要通过父元素进行删除
     QTreeWidgetItem* parent = currentItem->parent();
     if (parent == nullptr) {
         // 顶层元素
         int index = ui->treeWidget->indexOfTopLevelItem(currentItem);
         ui->treeWidget->takeTopLevelItem(index);
     } else {
         // 孩子节点
         parent->removeChild(currentItem);
     }
}

7、容器类控件

7.1、QGroupBox

多元素控件包含的内容是一个一个自定义好的Item对象。
容器类控件包含的内容是前面学习过的各种控件,比如:QPushButton、QLabel、QLineEdit等。

QGroupBox表示一个带有标题的分组框。可以把其他的控件放到里面作为一组。这样看起来能更好看一点。

如图所示,可以把其他各种控件放到QGroupBox中,这些内容的控件父元素就变成了QGroupBox,而不再是this指针了。当一个界面比较复杂的时候,包含了很多控件的时候,就可以使用分组框把具有关联关系的控件组织到一起。

下面实现一个样例:还是之前的麦当劳点餐,只不过这时候我们不再用QLabel来表示选择的种类了,这时候我们直接使用一个QGroupBox,设置QGroupBox的标题为汉堡/小食/饮料,然后QGroupBox里面嵌套了QComboBox和QSpinBox。


7.2、QTabWidget

QTabWidget表示一个带有标签页的控件,可以往里面添加一些widget。进一步的就可以通过标签页来切换。

标签页的效果如下图所示:然后右击标签页可以选择插入标签页和删除标签页。可以在标签页中创建任何你想要的控件。

下面实现一个样例:创建一个QTabWidget,然后提供两个按钮,一个可以创建标签页,一个可以关闭当前选中的标签页。另外当我们切换标签的时候也要能够感知到变化。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QLabel>
#include <QDebug>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    // 先在每个标签页中创建一个QLabel对象
    QLabel* label1 = new QLabel(ui->tab);
    label1->setText("标签页1");
    label1->resize(100, 50);
    QLabel* label2 = new QLabel(ui->tab_2);
    label2->setText("标签页2");
    label2->resize(100, 50);
}

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

void Widget::on_pushButton_clicked()
{
    // 通过addTab来创建新的标签页
    // 参数1:要指定一个QWidget, 标签页实际上就是一个QWidget
    // 参数2:指定这个标签页的标题
    QWidget* w = new QWidget();
    int count = ui->tabWidget->count();
    ui->tabWidget->addTab(w, QString("Tab ") + QString::number(count + 1));
    // 给新创建出来的标签页添加一个label
    QLabel* label = new QLabel(w);
    label->setText(QString("标签页") + QString::number(count+1));
    label->resize(100, 50);
    // 创建标签页后自动切换到最新的标签页
    ui->tabWidget->setCurrentIndex(count);
}

void Widget::on_pushButton_2_clicked()
{
    int index = ui->tabWidget->currentIndex();
    ui->tabWidget->removeTab(index);
}

void Widget::on_tabWidget_currentChanged(int index)
{
    qDebug() << "当前选中的标签页下标为: " << index;
}

这里有个点需要说明一下,就是我们在给每个标签页设置QLabel的时候,用的是ui->tab。如下图currentTabName,第一个标签页对应的值为tab,所以可以通过ui->tab来访问,同理第二个标签页就是ui->tab_2。


8、布局管理器

之前把控件放到界面上都是通过手动的方式来布局的,这种手动调整的方式极其不科学。一个是手动布局方式很复杂而且不精确,一个是无法对窗口大小进行自适应。所以就需要使用布局管理器。Qt中的布局管理器有:垂直布局、水平布局、网格布局、表单布局。

8.1、QVBoxLayout

QVBoxLayout表示垂直的布局管理器。V是vertical的缩写。

下面实现一个样例:创建一个垂直布局管理器,然后创建三个按钮,用这个垂直布局管理器将三个按钮管理起来。

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    // 创建三个按钮
    QPushButton* button1 = new QPushButton("按钮1");
    QPushButton* button2 = new QPushButton("按钮2");
    QPushButton* button3 = new QPushButton("按钮3");
    // 创建垂直布局管理器
    QVBoxLayout* layout = new QVBoxLayout();
    // 把按钮添加到垂直布局管理器中
    layout->addWidget(button1);
    layout->addWidget(button2);
    layout->addWidget(button3);
    // 把垂直布局管理器添加到窗口中
    this->setLayout(layout);
}


当我们改变窗口大小的时候,会发现这三个按钮也会跟着变化,也就是自适应。
但是呢,每个Widget中只能设置一个布局管理器。下面我们再创建项目实现一个样例,通过图形化界面的方式创建两个垂直布局管理器:


这时候我们运行程序发现确实可以看到两个布局管理器的六个按钮,但是我们拖动窗口改变窗口的大小,我们发现这两个布局管理器好像是固定住了一样。不会像上面代码的方式那样进行自适应。

实际上,我们通过代码的方式创建一个layout是真实的直接创建了一个layout。如果通过QtDesigner创建的layout,实际上是先创建一个Widget然后在这个新的Widget中添加了一个layout。我们可以查看ui文件的代码。

可以看到这里代码是现有一个widget,然后在widget内部有一个layout,layout里面还可以包含若干个item。

另外我们正常是先拖动一个layout到界面中,然后再拖动控件到layout里面。但是我们也可以反过来,先创建三个按钮,然后同时选中这三个按钮,在上方点击使用垂直布局管理器即可。


8.2、QHBoxLayout

QHBoxLayout表示垂直的布局管理器。H 是horizontal的缩写。

下面实现一个样例:创建三个按钮和一个水平布局管理器,然后把三个按钮放到水平布局管理器中看看效果。

布局管理器也可以进行嵌套,比如我们要实现下图所示的一个效果:

实现思路是:创建一个垂直布局管理器,一个水平布局管理器,在水平布局管理器中添加两个按钮。然后给垂直布局管理器先添加两个按钮,再添加一个水平布局管理器即可。


8.3、QGridLayout

Qt中还提供了QGridLayout用来实现网格布局的效果。可以达到M * N的这种网格的效果。

下面实现一个样例:创建四个按钮和一个网格布局,然后实现按钮2x2排列的效果。

还可以设置1x4的效果:

还可以设置4x1的效果:

甚至还可以有下面这样的效果:

前面创建的布局管理器控件的尺寸都是均等的,当需要创建出尺寸不同的控件时,就可以通过拉伸系数来设置。拉伸系数就相当于设置控件尺寸的比例。
下面实现一个样例:创建六个按钮,实现2x3的布局排列,并设置拉伸系数为1:1:2。

下面设置第一个按钮拉伸系数为0:

如果拉伸系数为0,意思就是不参与拉伸,此时按钮的宽度是固定值。
上面的例子是针对水平方向的,下面我们要实现垂直方向的拉伸。

上面代码我们确实设置了拉伸系数,但是发现并没有起效果。这是因为SizePolicy的影响,这个是QWidget的属性。

由于按钮垂直方向默认没有拉伸开,水平方向默认是拉伸开的。因此垂直方向不会收到拉伸系数的影响。如果想要让垂直方向的拉伸系数生效,需要让按钮能够拉伸展开。


8.4、QFormLayout

除了上述的布局管理器之外,Qt还提供了QFormLayout,属于是QGridLayout的特殊情况,专门用于实现两列表单的布局。
表单布局是一个N行两列的一个布局效果,一般用于让用户输入数据。
下面实现一个样例:创建一个表单布局,实现一个3x2的效果,让用户输入姓名、年龄、电话。

还可以创建一个按钮添加到第四行,如下所示:由于第一列没有控件所以设置为nullptr。


8.5、QSpacerItem

使用布局管理器的时候,可能需要在控件之间,添加一段空白。就可以使用QSpacerItem来表示。

下面实现一个样例:我们创建一个水平布局管理器,里面包含两个按钮。但是默认的效果跟前面一样,两个按钮之间的间隔是很小的,所以我们通过添加一个QSpacerItem来增加两个按钮之间的间隔。

如上所示,我们是在两个按钮中间添加间隔所以就在他们之间添加。如果是要在按钮1前面添加空格的话,那就需要把addSpacerItem移动到最上面,让它最先添加。


另外在QtDesigner中也提供了如图所示的水平和垂直的spacer供我们使用。

最后,上面所讲的所有控件都是可扩展的。每个控件都是Qt内置的一个类,可以在代码中继承这个类实现一个子类,在自定义的子类中可以添加很多的属性和方法,实现自己的需求场景。还可以在子类中把多个控件组合到一起。

相关推荐
Scott9999HH3 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社4 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖5 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大8 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜9 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa9 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio