Qt 自定义加载动画控件:带文字提示的等待 spinner 实现
·
在 Qt 项目开发中,耗时操作(如数据加载、设备自检、接口请求)时,需要一个直观的加载提示控件提升用户体验。本文将分享一款自定义等待 spinner 控件的设计思路与使用场景,支持样式自定义、文字提示、灵活显示隐藏,无需 UI 提升即可直接集成。
一、控件核心功能亮点这款自定义等待控件解决了常规加载动画的痛点,核心特性如下:
- 全样式可配置:支持调整线条颜色、数量、长度、宽度,内圆半径、旋转速度等参数,适配不同界面风格。
- 文字提示增强:中间可显示自定义文字(如“自检中,请等待…”),支持单独设置文字颜色,避免文字与动画融合。
- 灵活的显示逻辑:可选择居中到父控件(如主窗口)或在指定区域内显示,支持启用/禁用“父控件锁定”(避免操作冲突)。
- 轻量无依赖:纯 Qt 原生 API 实现,无需第三方库,支持 Qt 5 及以上版本,跨平台兼容。
- 安全的生命周期管理:支持动态创建/销毁,避免内存泄漏,重复点击不会出现控件叠加问题。
二、控件设计思路
1. 核心原理
控件继承自 QWidget,通过 QTimer 触发旋转动画,在 paintEvent 中绘制旋转的线条和居中文字。线条的透明度随旋转角度渐变,形成“轨迹淡出”效果,提升视觉层次感。
2. 关键设计点
- 动画绘制:通过循环绘制多条线段,结合旋转角度计算和透明度渐变,模拟连续旋转效果。
- 文字居中:利用 QFontMetrics 计算文字尺寸,确保文字始终在控件正中心,不受动画旋转影响。
- 样式解耦:将所有可配置参数(颜色、线条数量、旋转速度等)封装为 setter 接口,外部可按需调整,无需修改控件内部逻辑。
- 位置控制:通过“是否居中到父控件”开关,支持两种显示模式,适配不同使用场景(全局加载/局部加载)。
三、适用场景
- 设备自检过程:如医疗设备、工业控制软件中的硬件初始化提示。
- 数据加载/解析:如大数据量导入、文件解析、网络接口请求时的等待提示。
- 耗时操作反馈:如报表生成、模型计算等需要用户等待的场景。
- 全局/局部加载:支持全局居中显示(阻塞式提示,可选)或局部区域显示(非阻塞,不影响其他操作)。
四、快速集成步骤
1. 项目准备
将控件相关文件(WaitingSpinnerWidget.h、WaitingSpinnerWidget.cpp)添加到 Qt 项目中,在需要使用的界面类中引入头文件。
#pragma once
#include <QWidget>
#include <QTimer>
#include <QColor>
class WaitingSpinnerWidget : public QWidget {
Q_OBJECT
public:
WaitingSpinnerWidget(QWidget *parent = 0,
bool centerOnParent = true,
bool disableParentWhenSpinning = true);
WaitingSpinnerWidget(Qt::WindowModality modality,
QWidget *parent = 0,
bool centerOnParent = true,
bool disableParentWhenSpinning = true);
public slots:
void start();
void stop();
public:
void setColor(QColor color);
void setRoundness(qreal roundness);//圆角
void setMinimumTrailOpacity(qreal minimumTrailOpacity);//最小轨迹不透明度
void setTrailFadePercentage(qreal trail);//轨迹褪色百分比
void setRevolutionsPerSecond(qreal revolutionsPerSecond);//每秒钟转数
void setNumberOfLines(int lines);//线条数量
void setLineLength(int length);//线条长度
void setLineWidth(int width);//线条宽度
void setInnerRadius(int radius);//内圆半径
QColor color();
qreal roundness();
qreal minimumTrailOpacity();
qreal trailFadePercentage();
qreal revolutionsPersSecond();
int numberOfLines();
int lineLength();
int lineWidth();
int innerRadius();
bool isSpinning() const;
private slots:
void rotate();
protected:
void paintEvent(QPaintEvent *paintEvent);
private:
static int lineCountDistanceFromPrimary(int current, int primary,
int totalNrOfLines);
static QColor currentLineColor(int distance, int totalNrOfLines,
qreal trailFadePerc, qreal minOpacity,
QColor color);
void initialize();
void updateSize();
void updateTimer();
void updatePosition();
private:
QColor _color;
qreal _roundness; // 0..100
qreal _minimumTrailOpacity;
qreal _trailFadePercentage;
qreal _revolutionsPerSecond;
int _numberOfLines;
int _lineLength;
int _lineWidth;
int _innerRadius;
private:
WaitingSpinnerWidget(const WaitingSpinnerWidget&);
WaitingSpinnerWidget& operator=(const WaitingSpinnerWidget&);
QTimer *_timer;
bool _centerOnParent;
bool _disableParentWhenSpinning;
int _currentCounter;
bool _isSpinning;
public:
void setTipText(const QString& tipText); // 设置提示文字
void setTextColor(const QColor& color); // 设置文字颜色
private:
QString _tipText; // 存储提示文字
QColor _textColor; // 存储文字颜色
};
#include "waitingspinnerwidget.h"
#include <cmath>
#include <algorithm>
#include <QPainter>
#include <QTimer>
WaitingSpinnerWidget::WaitingSpinnerWidget(QWidget *parent,
bool centerOnParent,
bool disableParentWhenSpinning)
: QWidget(parent),
_centerOnParent(centerOnParent),
_disableParentWhenSpinning(disableParentWhenSpinning) {
initialize();
}
WaitingSpinnerWidget::WaitingSpinnerWidget(Qt::WindowModality modality,
QWidget *parent,
bool centerOnParent,
bool disableParentWhenSpinning)
: QWidget(parent, Qt::Dialog | Qt::FramelessWindowHint),
_centerOnParent(centerOnParent),
_disableParentWhenSpinning(disableParentWhenSpinning){
initialize();
// We need to set the window modality AFTER we've hidden the
// widget for the first time since changing this property while
// the widget is visible has no effect.
setWindowModality(modality);
setAttribute(Qt::WA_TranslucentBackground);
}
void WaitingSpinnerWidget::initialize() {
_color = Qt::black;
_roundness = 100.0;
_minimumTrailOpacity = 3.14159265358979323846;
_trailFadePercentage = 80.0;
_revolutionsPerSecond = 1.57079632679489661923;
_numberOfLines = 20;
_lineLength = 10;
_lineWidth = 2;
_innerRadius = 10;
_currentCounter = 0;
_isSpinning = false;
// 原有初始化代码...
_tipText = QStringLiteral("自检中..."); // 默认文字
_textColor = Qt::black; // 默认文字颜色(后续可通过接口修改)
_timer = new QTimer(this);
connect(_timer, SIGNAL(timeout()), this, SLOT(rotate()));
updateSize();
updateTimer();
hide();
}
// 设置提示文字
void WaitingSpinnerWidget::setTipText(const QString& tipText) {
_tipText = tipText;
update(); // 刷新绘制
}
// 设置文字颜色
void WaitingSpinnerWidget::setTextColor(const QColor& color) {
_textColor = color;
update(); // 刷新绘制
}
void WaitingSpinnerWidget::paintEvent(QPaintEvent *) {
updatePosition();
QPainter painter(this);
painter.fillRect(this->rect(), Qt::transparent);
painter.setRenderHint(QPainter::Antialiasing, true);
// ========== 原有动画绘制逻辑(保持不变) ==========
if (_currentCounter >= _numberOfLines) {
_currentCounter = 0;
}
painter.setPen(Qt::NoPen);
for (int i = 0; i < _numberOfLines; ++i) {
painter.save();
painter.translate(_innerRadius + _lineLength,
_innerRadius + _lineLength);
qreal rotateAngle =
static_cast<qreal>(360 * i) / static_cast<qreal>(_numberOfLines);
painter.rotate(rotateAngle);
painter.translate(_innerRadius, 0);
int distance =
lineCountDistanceFromPrimary(i, _currentCounter, _numberOfLines);
QColor color =
currentLineColor(distance, _numberOfLines, _trailFadePercentage,
_minimumTrailOpacity, _color);
painter.setBrush(color);
painter.drawRoundedRect(
QRect(0, -_lineWidth / 2, _lineLength, _lineWidth), _roundness,
_roundness, Qt::RelativeSize);
painter.restore();
}
// 绘制文字部分(修改为使用配置的文字和颜色)
painter.save();
QFont textFont = painter.font();
textFont.setPointSize(10);
textFont.setBold(true);
painter.setFont(textFont);
painter.setPen(_textColor); // 使用设置的绿色文字
QString tipText = _tipText; // 使用设置的提示文字
QFontMetrics fontMetrics(textFont);
QRect textRect = fontMetrics.boundingRect(tipText);
int textX = (width() - textRect.width()) / 2;
int textY = (height() - textRect.height()) / 2 + fontMetrics.ascent();
painter.drawText(textX, textY, tipText);
painter.restore();
}
void WaitingSpinnerWidget::start() {
updatePosition();
_isSpinning = true;
show();
if(parentWidget() && _disableParentWhenSpinning) {
parentWidget()->setEnabled(false);
}
if (!_timer->isActive()) {
_timer->start();
_currentCounter = 0;
}
}
void WaitingSpinnerWidget::stop() {
_isSpinning = false;
hide();
if(parentWidget() && _disableParentWhenSpinning) {
parentWidget()->setEnabled(true);
}
if (_timer->isActive()) {
_timer->stop();
_currentCounter = 0;
}
}
void WaitingSpinnerWidget::setNumberOfLines(int lines) {
_numberOfLines = lines;
_currentCounter = 0;
updateTimer();
}
void WaitingSpinnerWidget::setLineLength(int length) {
_lineLength = length;
updateSize();
}
void WaitingSpinnerWidget::setLineWidth(int width) {
_lineWidth = width;
updateSize();
}
void WaitingSpinnerWidget::setInnerRadius(int radius) {
_innerRadius = radius;
updateSize();
}
QColor WaitingSpinnerWidget::color() {
return _color;
}
qreal WaitingSpinnerWidget::roundness() {
return _roundness;
}
qreal WaitingSpinnerWidget::minimumTrailOpacity() {
return _minimumTrailOpacity;
}
qreal WaitingSpinnerWidget::trailFadePercentage() {
return _trailFadePercentage;
}
qreal WaitingSpinnerWidget::revolutionsPersSecond() {
return _revolutionsPerSecond;
}
int WaitingSpinnerWidget::numberOfLines() {
return _numberOfLines;
}
int WaitingSpinnerWidget::lineLength() {
return _lineLength;
}
int WaitingSpinnerWidget::lineWidth() {
return _lineWidth;
}
int WaitingSpinnerWidget::innerRadius() {
return _innerRadius;
}
bool WaitingSpinnerWidget::isSpinning() const {
return _isSpinning;
}
void WaitingSpinnerWidget::setRoundness(qreal roundness) {
_roundness = std::max(0.0, std::min(100.0, roundness));
}
void WaitingSpinnerWidget::setColor(QColor color) {
_color = color;
}
void WaitingSpinnerWidget::setRevolutionsPerSecond(qreal revolutionsPerSecond) {
_revolutionsPerSecond = revolutionsPerSecond;
updateTimer();
}
void WaitingSpinnerWidget::setTrailFadePercentage(qreal trail) {
_trailFadePercentage = trail;
}
void WaitingSpinnerWidget::setMinimumTrailOpacity(qreal minimumTrailOpacity) {
_minimumTrailOpacity = minimumTrailOpacity;
}
void WaitingSpinnerWidget::rotate() {
++_currentCounter;
if (_currentCounter >= _numberOfLines) {
_currentCounter = 0;
}
update();
}
void WaitingSpinnerWidget::updateSize() {
int size = (_innerRadius + _lineLength) * 2;
setFixedSize(size, size);
}
void WaitingSpinnerWidget::updateTimer() {
_timer->setInterval(1000 / (_numberOfLines * _revolutionsPerSecond));
}
void WaitingSpinnerWidget::updatePosition() {
if (parentWidget() && _centerOnParent) {
move(parentWidget()->width() / 2 - width() / 2,
parentWidget()->height() / 2 - height() / 2);
}
}
int WaitingSpinnerWidget::lineCountDistanceFromPrimary(int current, int primary,
int totalNrOfLines) {
int distance = primary - current;
if (distance < 0) {
distance += totalNrOfLines;
}
return distance;
}
QColor WaitingSpinnerWidget::currentLineColor(int countDistance, int totalNrOfLines,
qreal trailFadePerc, qreal minOpacity,
QColor color) {
if (countDistance == 0) {
return color;
}
const qreal minAlphaF = minOpacity / 100.0;
int distanceThreshold =
static_cast<int>(ceil((totalNrOfLines - 1) * trailFadePerc / 100.0));
if (countDistance > distanceThreshold) {
color.setAlphaF(minAlphaF);
} else {
qreal alphaDiff = color.alphaF() - minAlphaF;
qreal gradient = alphaDiff / static_cast<qreal>(distanceThreshold + 1);
qreal resultAlpha = color.alphaF() - gradient * countDistance;
// If alpha is out of bounds, clip it.
resultAlpha = std::min(1.0, std::max(0.0, resultAlpha));
color.setAlphaF(resultAlpha);
}
return color;
}
2. 基础使用流程
(1)动态创建并显示控件(按钮点击触发)
- 声明控件指针(全局持有,方便销毁)。
- 点击按钮时创建控件实例,设置样式、文字及显示模式。
- 调用 start() 方法启动动画,控件自动显示在指定位置。
(2)销毁控件(停止等待)
点击“取消”或“完成”按钮时,调用 stop() 停止动画,通过 deleteLater() 销毁控件,避免内存泄漏。
void CSelfTestShow::on_pushButton_clicked()
{
if (this->ui->pushButton->text() == tr("确认")) {
emit signalStartSelfTest();
// 用定时器延迟100毫秒后执行后续逻辑,避免阻塞界面
QTimer::singleShot(100, this, [this]() {
if (this->getRunStatus()) {
ui->pushButton->setText(tr("强制停止"));
//等待动画
{
// 先销毁已存在的控件(避免重复创建)
if (m_spinner) {
m_spinner->stop();
m_spinner->deleteLater();
m_spinner = nullptr;
}
// 1. 创建控件实例(关键参数:不禁用父控件、居中到主窗口)
m_spinner = new WaitingSpinnerWidget(this,
true, // centerOnParent:居中到主窗口
false); // disableParentWhenSpinning:不禁用主窗口
// 2. 设置控件样式(按你的需求配置)
m_spinner->setRoundness(50.0);
m_spinner->setMinimumTrailOpacity(15.0);
m_spinner->setTrailFadePercentage(70.0);
m_spinner->setNumberOfLines(16);
m_spinner->setLineLength(15);
m_spinner->setLineWidth(5);
m_spinner->setInnerRadius(30);
m_spinner->setRevolutionsPerSecond(1);
m_spinner->setColor(QColor(81, 4, 71)); // 动画线条颜色
// 3. 设置提示文字(需确保之前已添加文字相关代码)
m_spinner->setTipText("自检中..."); // 自定义文字(需新增接口)
m_spinner->setTextColor(QColor("#01a97e")); // 十六进制色值01a97e,直接传入QColor
// 4. 显示并启动动画
m_spinner->start();
}
}
});
} else {
emit signalForceStopSelfTest();
ui->pushButton->setText(tr("确认"));
//关闭等待动画
{
if (m_spinner) {
m_spinner->stop(); // 停止动画
m_spinner->deleteLater(); // 销毁控件
m_spinner = nullptr; // 置空,避免野指针
}
}
}
}
3. 核心配置示例
(1)关键参数配置
- 动画样式:设置线条颜色、数量、长度、宽度,内圆半径和旋转速度。
- 文字配置:设置提示文字内容和文字颜色(支持十六进制色值,如 #01a97e)。
- 显示模式:设置是否居中到主窗口、是否禁用父控件操作。
(2)常见配置组合
- 全局加载(主窗口居中+不禁用父控件):适合非阻塞式等待,用户可同时操作其他功能。
- 局部加载(指定区域内显示+禁用父控件):适合关键操作等待,避免用户重复触发。
五、注意事项
- 版本兼容性:Qt 5.14 及以上版本支持 QTextStream::setEncoding,低版本需使用 setCodec,控件内部已做版本兼容处理。
- 文字显示:若文字未显示,需检查是否已启用文字绘制逻辑,或文字颜色与背景颜色过于接近(建议根据背景调整文字颜色)。
- 位置问题:若控件未在指定区域显示,需检查“居中到父控件”开关是否正确设置,确保父控件已正确指定。
- 内存安全:动态创建的控件必须在使用完成后销毁(如按钮点击、窗口关闭时),避免野指针和内存泄漏。
更多推荐
所有评论(0)