前言:
在本篇文章中,我们将深入探索Qt框架中的一些核心组件,这些组件对于构建具有丰富用户界面的应用程序至关重要。文章将详细讨论QLabel
、QLCDNumber
、QProgressBar
以及QCalendarWidget
等控件的使用和功能。通过一系列代码示例,读者将学习到如何利用这些控件来增强应用程序的交互性和用户体验。无论是显示文本和图片、实现倒计时功能、创建进度条还是处理日期选择,本文都将提供实用的指导和深入的解析。
1.Label
1.1. QLabel 可以用来显示文本和图片
核心属性如下:
Qt::PainText
: 纯文本 最普通的文本
Qt::RichText
: 富文本(支持html标签)内容更丰富的文本,支持
html,word工具,编辑的文本,就可以认为是一种富文本。
Qt::MarkdownText
: markdown
格式:markdown 是一个非常常用的书写文档的格式。
Qt::AutoText
:根据文本内容自动决定文本的格式,提供了各种符号,表示不同的样式/格式。
url: http协议,通俗意义上说的“网址”。
1.2. QLabel的文本格式:
1.2.1. 代码示例: 显示不同格式的文本。
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 把第一个 label 设置成显示纯文本
ui->label->setTextFormat(Qt::PlainText);
ui->label->setText("<b>这是一段纯文本</b>"); // <b>标签在纯文本中,只会解析为字符
// 把第二个 label 设置成显示富文本
ui->label_2->setTextFormat(Qt::RichText);
ui->label_2->setText("<b>这是一段富文本</b>"); // <b>标签:加粗
// 把第三个 label 设置成显示 markdown
ui->label_3->setTextFormat(Qt::MarkdownText);
ui->label_3->setText("# 这是 markdown 文本"); //# :一级标题
}
Widget::~Widget()
{
delete ui;
}
1.2.2. 代码示例: 显示图片
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 先把 QLabel 设置成和窗口一样大,并且把这个 QLabel 左上角设置到窗口的左上角
// 让整个 QLabel 铺满整个窗口
QRect windowRect = this->geometry();
ui->label->setGeometry(0, 0, windowRect.width(), windowRect.height()); // 软编码
//在代码中直接写死某个数值,这样的做法叫做“硬编码”
QPixmap pixmap(":/tupian.png");
ui->label->setPixmap(pixmap);
// 启动自动拉伸图片,此时图面就能填充满整个窗口了
ui->label->setScaledContents(true);
}
Widget::~Widget()
{
delete ui;
}
// 启动自动拉伸图片,此时图面就能填充满整个窗口了
ui->label->setGeometry(0, 0, windowRect.width(), windowRect.height());
在上面代码中是在构造函数里面,进行这样的尺寸设置 这个设置相当于是“一次性的”,一旦程序运行起来后,QLabel的尺寸就固定下来了。
如果窗口发生改变,此时,QLabel 是不会变化的。
解决方法:
事件:用户的操作,会对应一些信号,Qt中,表示用户操作,有两类概念,一个是信号,另一个是事件
当用户拖拽修改窗口大小的时候,就会触发resize事件(resizeEvent
)。
像resize这样的事件,是连续变化的。把窗口尺寸从A拖到B这个过程中,会触发一系列的resizeEvent
,此时就可以借助,resizeEvent
来完成上述的功能。
可以让Widget
窗口类,重写父类(QWidget
) 的 resizeEvent
虚函数,在鼠标拖动窗口尺寸的过程中,这个函数就会被反复调用执行。
每次触发一个 resizeEvent
事件就会调用一次对应的虚函数。
- 由于此处进行了函数重写,调用父类的虚函数就会实际调用子类对应的函数(多态)
在实际编程中,指定回调函数其实有很多种写法 :
1)设置函数指针
2)设置仿函数(函数对象)
3)设置 lambda
4) 通过重写父类虚函数(框架中拿着父类的指针调用这个函数,如果你创建了子类重写这个函数,此时在多态机制下,实际执行的就是子类的函数)
5)Qt的信号槽
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void resizeEvent(QResizeEvent* event); // 重写QWidget父类提供的虚函数
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QResizeEvent>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 先把 QLabel 设置成和窗口一样大,并且把这个 QLabel 左上角设置到窗口的左上角
// 让整个 QLabel 铺满整个窗口
QRect windowRect = this->geometry();
ui->label->setGeometry(0, 0, windowRect.width(), windowRect.height()); // 软编码
QPixmap pixmap(":/tupian.png");
ui->label->setPixmap(pixmap);
// 启动自动拉伸图片,此时图面就能填充满整个窗口了
ui->label->setScaledContents(true);
}
Widget::~Widget()
{
delete ui;
}
// 此处的形参 event是非常有用的,这里就包含了触发这个resize事件这一时刻,窗口尺寸的数值。
void Widget::resizeEvent(QResizeEvent* event) // Qt自己调用
{
qDebug() << event->size();
ui->label->setGeometry(0, 0, event->size().width(), event->size().height());
}
1.2.3. 代码示例: 文本对齐,自动换行,缩进,边距
在Qt Creator 右侧属性编辑区,能够看到类之间的继承关系。
QLabel 继承自 QFrame,QFrame 继承自 QWidget
这里在frameShape 中选择 Box,课文本框加上边框,是其查看起来更清晰。
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 在构造函数中,给这几个 label 设置不同属性
// 设置对齐方式
ui->label->setText("这是一段文本");
ui->label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); // 垂直+水平居中
// 设置自动换行
ui->label_2->setText("这是一段很长的文本,这是一段很长的文本,这是一段很长的文本,这是一段很长的文本"
"这是一段很长的文本,这是一段很长的文本,这是一段很长的文本,这是一段很长的文本");
ui->label_2->setWordWrap(true);
// 设置缩进
ui->label_3->setText("这是一段很长的文本,这是一段很长的文本,这是一段很长的文本,这是一段很长的文本"
"这是一段很长的文本,这是一段很长的文本,这是一段很长的文本,这是一段很长的文本");
ui->label_3->setWordWrap(true);
ui->label_3->setIndent(50); // 此处设置的缩进及即使文本换行了,后续也会产生缩进,不仅仅是首行缩进
// 设置边距
ui->label_4->setText("这是一段很长的文本,这是一段很长的文本,这是一段很长的文本,这是一段很长的文本"
"这是一段很长的文本,这是一段很长的文本,这是一段很长的文本,这是一段很长的文本");
ui->label_4->setWordWrap(true);
ui->label_4->setMargin(20);
}
Widget::~Widget()
{
delete ui;
}
1.2.4. QLabel 伙伴:
Qt 中,QLabel
中写的文本,是可以指定 “快捷键”
此处的快捷键的规则功能上要比 QPushButton
若很多,是在文本中使用 & 跟上一个字符来表示快捷键
比如 :&A
=> 通过键盘上的 alt + a 来触发这个按键
&B
=> 通过键盘上的 alt + b 来触发
绑定了伙伴关系之后,通过快捷键就可以选中对应的 单选按钮 / 复选按钮
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 设置 label 和 radioButton 伙伴关系
ui->label->setBuddy(ui->radioButton);
ui->label_2->setBuddy(ui->radioButton_2);
}
Widget::~Widget()
{
delete ui;
}
2.LCD Number
2.1. 代码示例:倒计时
使用 QLCDNumber
显示一个初始值,比如 10,每隔一面钟,数字就-1,一直到0,就停止了。
此处的关键要点是要实现“每秒钟 -1” 这个效果。
- 周期性的执行某个逻辑:
“定时器” C++ 标准库中,没有提供定时器的实现,Boost 里面提供了对应的功能。
Qt 中也封装了对应得定时器(结合了信号槽机制)
QTimer
: 通过这个类创建出来的对象,会产生一个timeout
这样的信号 可以通过
start
方法来开启定时器,并且参数中设定触发 timeout
信号的周期
结合 connect
, 把这个timeout
绑定到需要槽函数中,就可以执行逻辑,修改LCDNumber
中的数字了。
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void handle();
private:
Ui::Widget *ui;
QTimer* timer;
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QTimer>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 设置初始值
ui->lcdNumber->display("10");
// 创建一个 QTimer 实例, 定义放到类里面,使其变为成员变量
timer = new QTimer(this);
// 把 QTimer 的 timeout 信号和 咱们自己的槽函数进行连接
connect(timer, &QTimer::timeout, this, &Widget::handle);
// 启动定时器,参数是触发timeout的周期,单位是ms
timer->start(1000);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handle()
{
// 先拿到 LCDNumber 中的数字
int value = ui->lcdNumber->intValue();
if (value <= 0) {
// 数字减到 0 了,停止定时器
timer->stop();
return;
}
ui->lcdNumber->display(value - 1);
}
Sleep
=> Windows 的 api,需要包含“Windows.h
” 头文件才能使用的
C++11 标准中引入了sleep操作:std::this_thread::sleep_for(std::chrono::seconds(1));
#include "widget.h"
#include "ui_widget.h"
#include<thread>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
int value = ui->lcdNumber->intValue();
while (true) {
// 休眠 1s
std::this_thread::sleep_for(std::chrono::seconds(1));
if(value <= 0) {
break;
}
value -= 1;
ui->lcdNumber->display(--value);
}
}
Widget::~Widget()
{
delete ui;
}
在构造函数中,另外创建一个新线程,执行上述循环操作+更新操作
线程操作本身是操作系统提供的api,比方说:Linux中:pthread_create
#include "widget.h"
#include "ui_widget.h"
#include<thread>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
std::thread t([this](){
int value = this->ui->lcdNumber->intValue();
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (value <= 0) {
break;
}
value -= 1;
ui->lcdNumber->display(value); // 修改界面
}
});
}
Widget::~Widget()
{
delete ui;
}
Qt里,界面有一个专门的线程区负责维护更新(主线程)(main
函数 所在的线程)
对于 GUI 来说,内部包含了很多的隐藏状态, Qt为了保证修改界面的过程中,线程安全是不会受到影响的,Qt禁止了其他线程直接修改界面。
ui->lcdNumber->display(value);
形如这种操作,就是在修改界面
因此 Qt 为了确保线程安全,直接要求所有对界面的修改操作,必须在主线程中完成
对于Qt的槽函数来说,默认情况下,槽函数都是由主线程调用的,在槽函数中修改界面是没有任何问题的!
return a.exec();
a.exec()
就会调用主线程进入“事件循环”
exec
就会一直循环下去,每执行一次循环,都会有一些固定的事情要操作
3. ProgressBar
使用QProgressBar
表示一个进度条
注意,不要把ProgessBar拼写成ProcessBar!
3.1. 代码示例:创建一个进度条,让这个进度条的进度跟随时间增长(可以假设,每隔100ms,让进度条数值+1)
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &Widget::handle);
// 别忘了启动定时器,启动操作要在槽函数 connect 之后
timer->start(100);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handle()
{
// 获取到进度条的当前数值
int value = ui->progressBar->value();
if (value >= 100) {
// 进度条满了,就可以停止定时器了
timer->stop();
return;
}
ui->progressBar->setValue(value+1);
}
- 如果在
widget.h
中用到了QTimer
,但是却没在.h
中包含<QTimer>
头文件,而是。cpp
中包含,为啥这个代码编译不会出错? - 为啥此处的 QTimer 就不会提示“找不到定义”之类的?
上述问题其实可以通过 Qt 内部提供的一个特殊技巧实现的!
在Qt中,有一个专门的头文件,这个头文件中包含了 Qt 中所有类的“前置声明”(在使用某个类的指针或引用之前,先声明该类的存在,而不需要将该类的头文件包含进来)
class QWidget;
class QPushButton;
class QTimer;
专门的头文件:一般不会直接接触到,但是包其它的Qt的头文件,都会间接的包含到这个头文件。
QTimer* timer;
Widget
类的前面已经提供了 QTimer
类的声明的话,此时就可以在Widget
中声明QTimer
的指针/引用类型的成员。
后续如果需要真正使用QTimer
(包括创建实例,使用里面的成员…)
仍然需要包含QTimer
的头文件(包含了QTimer
的详细类的定义)
- Qt 为啥要使用上述技巧,上述技巧解决什么问题?有啥提升呢?
主要解决的是编译速度的问题,C/C++ 的代码,编译速度在其他语言横向对比中是非常慢的。
C++ 编译速度慢,和
#include
头文件,有直接关系,由于 include由于 include
关系错中复杂,因此,尽可能的减少#include 头文件的个数,就可以有效的减少编译时间。Qt 中就使用
class
前置声明的方式,来尽量减少头文件的包含。 通过前置声明的方式,Qt头文件,每个头文件数量都能得到一定的降低。在C++20标准开始,就引入了“模块”(
module
)来代替#include
。但是在实际开发中,还是该包含就包含的,与其通过特殊技巧来缩短编译时间,不如引入更好的硬件资源,来更高效的编译。一些互联网大厂,都有专门的“编译集群”(分布式编译)
3.2. 代码示例:创建一个红色的进度条
QProgressBar::chunk{ background-color:red; }
QProgressBar::chunk
: 选择器,咱们的样式,到底要针对哪个控件生效!
调整数字百分比的显示位置:
进度条如何设置,一般是更具实际任务类型来灵活设置的,例如,要读一个很大的文件,就可以先获取到文件的总大小,每读取一部分数据(可以计算出读了多少的数据的),更新一次进度条的数值。设置进度条的过程中,往往要搭配定时器的。
4. Calendar Widget
QCalendarWidget
表示一个“日历”
4.1. 代码示例:获取选中的日期
#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_calendarWidget_selectionChanged()
{
QDate date = ui->calendarWidget->selectedDate();
qDebug() << date;
ui->label->setText(date.toString());
}
总结:
本文详细介绍了Qt中的几个关键GUI组件,包括QLabel
、QLCDNumber
、QProgressBar
和QCalendarWidget
。通过具体的代码示例,我们学习了如何使用这些组件来创建功能丰富的用户界面。
QLabel
是一个多功能的标签组件,能够显示文本、图片,并且支持多种文本格式,如纯文本、富文本、Markdown格式等。此外,QLabel还具备文本对齐、自动换行和设置缩进及边距的功能。QLCDNumber
是一个用于显示数字的LCD样式控件,特别适合用来实现倒计时等数字递减的效果。通过结合QTimer,我们可以实现周期性更新显示数字的功能。QProgressBar
是一个直观展示任务进度的控件,可以通过定时器来动态更新其进度值,也可以自定义其样式,如设置为红色进度条,以适应不同的应用场景。QCalendarWidget
提供了一个日历界面,允许用户选择日期。通过事件处理,我们可以捕获用户的选择并进行相应的处理。
文章还讨论了Qt中的一些高级特性,如线程安全、信号与槽机制、以及如何通过重写事件处理函数来响应用户操作。通过这些知识,读者将能够更加灵活和高效地使用Qt框架来开发桌面应用程序。