ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Qt常用控件:显示类控件,标签、数字显示、进度条、日历

Qt常用控件:显示类控件,标签、数字显示、进度条、日历

目录

1. Label

代码示例:显示不同格式的文本

代码示例:显示图片

代码示例:文本对齐,自动换行,缩进,边距

代码示例:设置伙伴

2. LCD Number

代码示例:倒计时(重写updateTime)

采用 sleep 实现倒计时

采用新线程实现倒计时

3. ProgressBar

代码示例:设置进度条按时间增长

代码示例:创建一个红色的进度条

4. Calendar Widget

代码示例:获取选中的日期


1. Label

QLabel 可以用来显示文本和图片。 核心属性如下

属性说明
textQLabel 中的文本。
textFormat文本的格式。
  • Qt::PlainText:纯文本
  • Qt::RichText:富文本(支持 HTML 标签)
  • Qt::MarkdownText:Markdown 格式
  • Qt::AutoText:根据文本内容自动决定文本格式
pixmapQLabel 内部包含的图片。
scaledContents设为 true 表示内容自动拉伸填充;设为 false 则不会自动拉伸。
alignment对齐方式。可以设置水平和垂直方向如何对齐。
wordWrap设为 true 内部的文本会自动换行;设为 false 则内部文本不会自动换行。
indent设置文本缩进。水平和垂直方向都生效。
margin内部文本和边框之间的边距。不同于 indent,margin 是上下左右四个方向都同时有效;而 indent 最多只是两个方向有效(具体哪两个方向有效取决于 alignment)。
openExternalLinks是否允许打开一个外部的链接(当 QLabel 文本内容包含 URL 的时候涉及到)。
buddy给 QLabel 关联一个“伙伴”,这样点击 QLabel 时就能激活对应的伙伴。例如伙伴如果是一个 QCheckBox,那么该 QCheckBox 就会被选中。

代码示例:显示不同格式的文本

  • 在界面上创建三个 QLabel,尺寸放大一些
  • objectName 分别为 label,label_2,label_3
Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); ui->label->setTextFormat(Qt::PlainText); ui->label->setText("这是⼀段纯⽂本"); ui->label_2->setTextFormat(Qt::RichText); ui->label_2->setText("<b> 这是⼀段富⽂本 </b>"); ui->label_3->setTextFormat(Qt::MarkdownText); ui->label_3->setText("## 这是⼀段 markdown ⽂本"); }

代码示例:显示图片

虽然 QPushButton 也可以通过设置图标的方式设置图片,但是并非是一个好的选择。更多的时候还是希望通过 QLabel 来作为一个更单纯的显示图片的方式

在界面上创建一个 QLabel,objectName 为 label。

创建 resource.qrc 文件,并把图片导入到 qrc 中。

// 设置 label ⼤⼩和窗⼝⼀样⼤ ui->label->setGeometry(0, 0, 800, 600); QPixmap pixmap(":/huaji.png"); ui->label->setPixmap(pixmap);

这个图片本身的尺寸是 480 * 480,并没有把 QLabel 填充满。

修改代码,设置 scaledContents 属性。

// 设置内容伸缩 ui->label->setScaledContents(true);

此时,如果拖动窗口大小,可以看到图片并不会随着窗口大小的改变而同步变化。

为了解决这个问题,可以在 Widget 中重写 resizeEvent 函数。

resizeEvent.会在窗口大小发生改变时被自动调用.

// 在 Widget 中重写 resizeEvent. 这个函数会在窗⼝⼤⼩发⽣改变时被⾃动调⽤. void Widget::resizeEvent(QResizeEvent *event) { // 可以直接通过 this->width() 和 this->height() 设置 label 新的尺⼨, 也可以通过 event 参数拿到新的尺⼨. // ui->label->setGeometry(0, 0, this->width(), this->height()); ui->label->setGeometry(0, 0, event->size().width(), event->size().height()); qDebug() << event->size(); }

注意:

此处的 resizeEvent 函数我们没有手动调用,但是能在窗口大小变化时被自动调用。

这个过程就是依赖 C++ 中的多态来实现的。Qt 框架内部管理着 QWidget 对象表示咱们的窗口。在窗口大小发生改变时,Qt 就会自动调用 resizeEvent 函数。

但是由于实际上这个表示窗口的并非是 QWidget,而是 QWidget 的子类,也就是咱们自己写的 Widget。此时虽然是通过父类调用函数,但是实际上执行的是子类的函数(也就是我们重写后的 resizeEvent)。

此处属于是多态机制的一种经典用法。通过上述过程,就可以把自定义的代码,插入到框架内部执行。相当于“注册回调函数”。

代码示例:文本对齐,自动换行,缩进,边距

  • 创建四个 label,objectName 分别是 label 到 label_4
  • 并且在 QFrame 中设置 frameShape 为 Box(设置边框之后看起来会更清晰一些)。

QFrame 是 QLabel 的父类,其中 frameShape 属性用来设置边框性质。

  • QFrame::Box:矩形边框
  • QFrame::Panel:带有可点击区域的面板边框
  • QFrame::WinPanel:Windows风格的边框
  • QFrame::HLine:水平线边框
  • QFrame::VLine:垂直线边框
  • QFrame::StyledPanel:带有可点击区域的面板边框,但样式取决于窗口主题

编写 widget.cpp,给这四个 label 设置属性。

Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); // 设置⽂字居中对⻬ ui->label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); ui->label->setText("垂直⽔平居中的⽂本"); // 设置⾃动换⾏ ui->label_2->setAlignment(Qt::AlignTop | Qt::AlignLeft); ui->label_2->setWordWrap(true); ui->label_2->setText("这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本"); // 设置⾸⾏缩进 ui->label_3->setAlignment(Qt::AlignTop | Qt::AlignLeft); ui->label_3->setIndent(20); ui->label_3->setText("这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本"); // 设置边距 ui->label_4->setAlignment(Qt::AlignTop | Qt::AlignLeft); ui->label_4->setMargin(20); ui->label_4->setText("这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本这是⼀个很⻓的⽂本"); }

运行程序,可以看到如下效果

  • 第一个 label 垂直水平居中
  • 第二个 label 设置了 wordWrap,能够自动换行
  • 第三个 label 设置了 Indent,左侧和上方和边框有间距。右侧则没有。
  • 第四个 label 设置了 margin,四个方向均有间距(图上仅体现出三个方向,下方看不出来)。

代码示例:设置伙伴

  • 创建两个 label 和两个 radioButton。
  • objectName 分别为 label,label_2,radioButton,radioButton_2。

此处把 label 中的文本设置为“快捷键 &A”这样的形式。

其中 & 后面跟着的字符,就是快捷键

可以通过 alt + A 的方式来触发该快捷键。

但是注意,这里的快捷键和 QPushButton 的不同。需要搭配 alt 和单个字母的方式才能触发

编写 widget.cpp,设置 buddy 属性。 当然这里也可以使用 Qt Designer 直接设置。

Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); // 设置 label 的伙伴 widget ui->label->setBuddy(ui->radioButton); ui->label_2->setBuddy(ui->radioButton_2); }

运行程序,可以看到,按下快捷键 alt + a 或者 alt + b,即可选中对应的选项。

2. LCD Number

QLCDNumber 是一个专门用来显示数字的控件。类似于“老式计算器”的效果。

QLCDNumber 的核心属性如下

属性说明
intValueQLCDNumber 显示的数字值(int)。
valueQLCDNumber 显示的数字值(double),和 intValue 是联动的。例如给 value 设为 1.5,intValue 的值就是 2。另外,设置 value 和 intValue 的方法名字为 display,而不是 setValue 或者 setIntValue。
digitCount显示几位数字。
mode数字显示形式。
  • QLCDNumber::Dec:十进制模式,显示常规的十进制数字。
  • QLCDNumber::Hex:十六进制模式,以十六进制格式显示数字。
  • QLCDNumber::Bin:二进制模式,以二进制格式显示数字。
  • QLCDNumber::Oct:八进制模式,以八进制格式显示数字。
只有十进制的时候才能显示小数点后的内容。
segmentStyle设置显示风格。
  • QLCDNumber::Flat:平面的显示风格,数字呈现在一个平坦的表面上。
  • QLCDNumber::Outline:轮廓显示风格,数字具有清晰的轮廓和阴影效果。
  • QLCDNumber::Filled:填充显示风格,数字被填充颜色并与背景区分开。
smallDecimalPoint设置比较小的小数点。

代码示例:倒计时(重写updateTime)

  • 在界面上创建一个 QLCDNumber,初始值设为 10。
  • objectName 为 lcdNumber。

修改 widget.h 代码,创建一个 QTimer 成员,和一个 updateTime 函数。

QTimer* timer; void updateTime();

修改 widget.cpp,在构造函数中初始化 QTimer

  • QTimer 表示定时器。通过 start 方法启动定时器之后,就会每隔一定周期,触发一次 QTimer::timeout 信号。
  • 使用 connect 把 QTimer::timeout 信号和 Widget::updateTime 连接起来,意味着每次触发 QTimer::timeout 都会执行 Widget::updateTime。
Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); // 创建 QTimer 实例 timer = new QTimer(this); // 连接信号槽. QTimer 会每隔⼀定的时间触发⼀个 timeout 信号. 现在把 timeout 信号和 updateTime 连接起来. // 此时意味着每次触发 timeout 信号都会伴随 updateTime 函数的执⾏. connect(timer, &QTimer::timeout, this, &Widget::updateTime); // 启动 QTimer, 并且规定每隔 1000ms 触发⼀次 timeout 信号. timer->start(1000); }

修改 widget.cpp,实现 updateTime

  • 通过 intValue 获取到 QLCDNumber 内部的数值。
  • 如果 value 的值归 0 了,就停止 QTimer。接下来 QTimer 也就不会触发 timeout 信号了。
void Widget::updateTime() { qDebug() << "updateTime"; int value = ui->lcdNumber->intValue(); if (value <= 0) { // 如果时间到, 停⽌定时器. timer->stop(); return; } ui->lcdNumber->display(value - 1); }

采用 sleep 实现倒计时

针对上述代码,存在两个问题:

上述代码如果直接在 Widget 构造函数中,通过一个循环 + sleep 的方式是否可以呢? 代码形如

Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); int value = ui->lcdNumber->intValue(); while (true) { std::this_thread::sleep_for(std::chrono::seconds(1)); if (value <= 0) { break; } ui->lcdNumber->display(value - 1); } }

显然,这个代码是不行的。循环会使 Widget 的构造函数无法执行完毕,此时界面是不能正确构造和显示的

采用新线程实现倒计时

上述代码如果是在 Widget 构造函数中,另起一个线程,在新线程中完成循环 + sleep 是否可以呢? 代码形如

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; } this->ui->lcdNumber->display(value - 1); } }); }

这个代码同样是不行的。Qt 中规定,任何对于 GUI 上内容的操作,必须在主线程中完成。像 Widget 构造函数,以及 connect 连接的 slot 函数,都是在主线程中调用的。而我们自己创建的线程则不是。

当我们自己的线程中尝试对界面元素进行修改时,Qt 程序往往会直接崩溃。

这样的约定主要是因为 GUI 中的状态往往是牵一发而动全身的,修改一个地方,就需要同步的对其他内容进行调整。

比如调整了某个元素的尺寸,就可能影响到内部的文字位置,或者其他元素的位置。这里一连串的修改,都是需要按照一定的顺序来完成的。

由于多线程执行的顺序无法保障,因此 Qt 从根本上禁止了其他线程修改 GUI 状态,避免后续的一系列问题。

综上所述,使用定时器,是实现上述功能的最合理方案。 后续如果我们也有类似的需要“周期性修改界面状态”的需求,也需要优先考虑使用定时器。

3. ProgressBar

使用 QProgressBar 表示一个进度条。

注意,不要把 ProgressBar 拼写成 ProcessBar!

QProgressBar 的核心属性如下

属性说明
minimum进度条最小值。
maximum进度条最大值。
value进度条当前值。
alignment文本在进度条中的对齐方式。
  • Qt::AlignLeft:左对齐
  • Qt::AlignRight:右对齐
  • Qt::AlignCenter:居中对齐
  • Qt::AlignJustify:两端对齐
textVisible进度条的数字是否可见。
orientation进度条的方向是水平还是垂直。
invertAppearance是否是朝反方向增长进度。
textDirection文本的朝向。
format展示的数字格式。
  • %p:表示进度的百分比(0-100)
  • %v:表示进度的数值(0-100)
  • %m:表示剩余时间(以毫秒为单位)
  • %t:表示总时间(以毫秒为单位)

代码示例:设置进度条按时间增长

  • 在界面上创建进度条,objectName 为 progressBar。
  • 其中最小值设为 0,最大值设为 100。当前值设为 0。

修改 widget.h,创建 QTimer 和 updateProgressBar 函数。

QTimer* timer; void updateProgressBar ();
  • 修改 widget.cpp,初始化 QTimer
  • 此处设置 100ms 触发一次 timeout 信号。也就是一秒钟触发 10 次。
Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &Widget::updateProgressBar); timer->start(100); }

修改 widget.cpp,实现 updateProgressBar。

void Widget::updateProgressBar() { int value = ui->progressBar->value(); if (value >= 100) { timer->stop(); return; } ui->progressBar->setValue(value + 1); }

在实际开发中,进度条的取值,往往是根据当前任务的实际进度来进行设置的。

比如需要读取一个很大的文件,就可以获取文件的总的大小,和当前读取完毕的大小,来设置进度条的比例。

由于上面我们介绍了 Qt 禁止在其他线程修改界面,因此进度条的更新往往也是需要搭配定时器来完成的。

通过定时器周期触发信号,主线程调用对应的 slot 函数。再在 slot 函数中对当前的任务进度进行计算,并更新进度条的界面效果。

代码示例:创建一个红色的进度条

  • 上述的进度条使用绿色表示的,但是考虑到有些人可能不喜欢绿色,因此我们改成一个红色的进度条。
  • 不要忘了,QProgressBar 同样也是 QWidget 的子类,因此我们可以使用 styleSheet 通过样式来修改进度条的颜色。
  • 在 Qt Designer 右侧的属性编辑器中,找到 QWidget 的 styleSheet 属性。 编辑如下内容:
  • 其中的 chunk 是选中进度条中的每个“块”。使用 QProgressBar::text 则可以选中文本。

QProgressBar::chunk {background-color: #FF0000;}
  • 同时把 QProgressBar 的 alignment 属性设置为垂直水平居中。
  • 如果不设置 alignment,进度条中的数字会跑到左上角。这个怀疑是 Qt 本身的 bug,暂时只能先使用 alignment 来手动调整下。
  • 通过上述方式,也可以修改文字的颜色,字体大小等样式。

4. Calendar Widget

QCalendarWidget 表示一个“日历”,形如

QCalendarWidget 的核心属性如下

属性说明
selectDate当前选中的日期。
minimumDate最小日期。
maximumDate最大日期。
firstDayOfWeek每周的第一天(也就是日历的第一列)是周几。
gridVisible是否显示表格的边框。
selectionMode是否允许选择日期。
navigationBarVisible日历上方标题是否显示。
horizontalHeaderFormat日历上方标题显示的日期格式。
verticalHeaderFormat日历第一列显示的内容格式。
dateEditEnabled是否允许日期被编辑。

QCalendarWidget 的核心信号如下

信号说明
selectionChanged(const QDate&)当选中的日期发生改变时发出。
activated(const QDate&)当双击一个有效的日期或者按下回车键时发出,形参是一个 QDate 类型,保存了选中的日期。
currentPageChanged(int, int)当年份月份改变时发出,形参表示改变后的新年份和月份。

代码示例:获取选中的日期

  • 在界面上创建一个 QCalendarWidget 和一个 label
  • objectName 为 calendarWidget,label。
  • 给 QCalendarWidget 添加 slot 函数。

void Widget::on_calendarWidget_selectionChanged() { QDate date = ui->calendarWidget->selectedDate(); qDebug() << date; ui->label->setText(date.toString()); }

执行程序,可以看到当选择不同的日期时,label 中的内容就会随之改变。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进