在 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)动态创建并显示控件(按钮点击触发)
  1. 声明控件指针(全局持有,方便销毁)。
  2. 点击按钮时创建控件实例,设置样式、文字及显示模式。
  3. 调用 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)常见配置组合
  • 全局加载(主窗口居中+不禁用父控件):适合非阻塞式等待,用户可同时操作其他功能。
  • 局部加载(指定区域内显示+禁用父控件):适合关键操作等待,避免用户重复触发。

五、注意事项

  1. 版本兼容性:Qt 5.14 及以上版本支持 QTextStream::setEncoding,低版本需使用 setCodec,控件内部已做版本兼容处理。
  2. 文字显示:若文字未显示,需检查是否已启用文字绘制逻辑,或文字颜色与背景颜色过于接近(建议根据背景调整文字颜色)。
  3. 位置问题:若控件未在指定区域显示,需检查“居中到父控件”开关是否正确设置,确保父控件已正确指定。
  4. 内存安全:动态创建的控件必须在使用完成后销毁(如按钮点击、窗口关闭时),避免野指针和内存泄漏。
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐