Qtカスタムウィジェット:QPainterによる回転待機アニメーションの実装

GUIアプリケーション開発において、処理中の待機時間をユーザーに視覚的に伝えるためのインジケータは重要な要素です。GIFアニメーションを利用する手法もありますが、QtのQPainterを活用して純粋なコードで描画することで、解像度に依存しない鮮明な表示が可能になり、動的な色やサイズの変更も容易になります。

ここでは、QWidgetを継承したカスタムクラスを作成し、QTimerで駆動する回転アニメーションを実装する手法について解説します。

実装のポイント

このコントロールの実装は主に以下の要素で構成されます。

  • カスタム描画: paintEventを再実装し、計算に基づいて円形に並んだ線(または点)を描画します。
  • アニメーション駆動: QTimerを使用して定期的に再描画をトリガーし、回転の錯覚を生み出します。
  • トレイル効果: 先頭の線から末尾の線にかけて不透明度を徐々に下げることで、動きのある軌跡を表現します。

ヘッダファイルの定義

スピナーウィジェットのクラス定義です。設定可能なプロパティとして、線の数、長さ、幅、内側の半径、回転速度などを用意します。

#pragma once

#include <QWidget>
#include <QTimer>
#include <QColor>

class LoadingSpinner : public QWidget {
    Q_OBJECT

public:
    explicit LoadingSpinner(QWidget *parent = nullptr);

    // プロパティ設定メソッド
    void setLineColor(const QColor &color);
    void setLineCount(int count);
    void setLineLength(int length);
    void setLineWidth(int width);
    void setInnerRadius(int radius);
    void setRotationSpeed(double speed); // 回転あたりの秒数

    QSize sizeHint() const override;

public slots:
    void startAnimation();
    void stopAnimation();

protected:
    void paintEvent(QPaintEvent *event) override;

private:
    void initializeDefaults();
    void refreshSize();

private:
    QColor m_lineColor;
    int m_lineCount;
    int m_lineLength;
    int m_lineWidth;
    int m_innerRadius;
    double m_rotationSpeed;

    QTimer *m_timer;
    int m_currentAngle; // 現在の回転角度を管理
    bool m_isActive;
};

実装コード

コンストラクタで初期設定を行い、タイマーの設定を行います。paintEvent内では、現在の角度に基づいて各線の位置と透明度を計算し、描画します。

#include "LoadingSpinner.h"
#include <QPainter>
#include <cmath>

LoadingSpinner::LoadingSpinner(QWidget *parent)
    : QWidget(parent), m_timer(new QTimer(this)), m_currentAngle(0), m_isActive(false) {
    initializeDefaults();
    connect(m_timer, &QTimer::timeout, this, [this]() {
        m_currentAngle = (m_currentAngle + 360 / m_lineCount) % 360;
        update();
    });
    refreshSize();
}

void LoadingSpinner::initializeDefaults() {
    m_lineColor = Qt::black;
    m_lineCount = 12;
    m_lineLength = 10;
    m_lineWidth = 3;
    m_innerRadius = 10;
    m_rotationSpeed = 1.0; // 1回転1秒
}

void LoadingSpinner::startAnimation() {
    if (!m_isActive) {
        m_isActive = true;
        int interval = static_cast<int>(1000.0 * m_rotationSpeed / m_lineCount);
        m_timer->start(interval);
        show();
    }
}

void LoadingSpinner::stopAnimation() {
    if (m_isActive) {
        m_isActive = false;
        m_timer->stop();
        hide();
    }
}

void LoadingSpinner::setRotationSpeed(double speed) {
    m_rotationSpeed = speed;
    if (m_timer->isActive()) {
        int interval = static_cast<int>(1000.0 * m_rotationSpeed / m_lineCount);
        m_timer->setInterval(interval);
    }
}

void LoadingSpinner::refreshSize() {
    int size = (m_innerRadius + m_lineLength) * 2;
    setFixedSize(size, size);
}

QSize LoadingSpinner::sizeHint() const {
    return QSize((m_innerRadius + m_lineLength) * 2, (m_innerRadius + m_lineLength) * 2);
}

void LoadingSpinner::paintEvent(QPaintEvent *event) {
    Q_UNUSED(event);
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    QPointF center(width() / 2.0, height() / 2.0);

    for (int i = 0; i < m_lineCount; ++i) {
        // 描画する線の角度を計算(現在の角度 + オフセット)
        qreal angle = m_currentAngle + (i * 360.0 / m_lineCount);
        qreal radian = qDegreesToRadians(angle);

        // 線の開始点と終了点を計算
        qreal x1 = center.x() + m_innerRadius * std::cos(radian);
        qreal y1 = center.y() + m_innerRadius * std::sin(radian);
        qreal x2 = center.x() + (m_innerRadius + m_lineLength) * std::cos(radian);
        qreal y2 = center.y() + (m_innerRadius + m_lineLength) * std::sin(radian);

        // 透明度の計算(トレイル効果)
        // 現在の位置に近いほど濃く、遠いほど薄くする
        qreal opacity = 1.0 - (static_cast<qreal>(i) / m_lineCount);
        QColor color = m_lineColor;
        color.setAlphaF(opacity);

        QPen pen(color, m_lineWidth, Qt::SolidLine, Qt::RoundCap);
        painter.setPen(pen);
        painter.drawLine(QPointF(x1, y1), QPointF(x2, y2));
    }
}

// Setterメソッドの実装
void LoadingSpinner::setLineColor(const QColor &color) { m_lineColor = color; }
void LoadingSpinner::setLineCount(int count) { m_lineCount = count; refreshSize(); }
void LoadingSpinner::setLineLength(int length) { m_lineLength = length; refreshSize(); }
void LoadingSpinner::setLineWidth(int width) { m_lineWidth = width; }
void LoadingSpinner::setInnerRadius(int radius) { m_innerRadius = radius; refreshSize(); }

使用方法

実装したLoadingSpinnerクラスを使用するには、インスタンスを生成し、パラメータを設定してstartAnimation()を呼び出します。

#include <QApplication>
#include <QVBoxLayout>
#include <QPushButton>
#include "LoadingSpinner.h"

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QWidget window;
    QVBoxLayout *layout = new QVBoxLayout(&window);

    // スピナーの作成と設定
    LoadingSpinner *spinner = new LoadingSpinner(&window);
    spinner->setLineColor(QColor(25, 120, 200)); // 青系の色
    spinner->setLineCount(16);
    spinner->setLineLength(20);
    spinner->setLineWidth(5);
    spinner->setInnerRadius(25);
    spinner->setRotationSpeed(0.8); // 少し速めに回転

    // 開始ボタン
    QPushButton *btnStart = new QPushButton("Start Loading", &window);
    QObject::connect(btnStart, &QPushButton::clicked, spinner, &LoadingSpinner::startAnimation);

    // 停止ボタン
    QPushButton *btnStop = new QPushButton("Stop", &window);
    QObject::connect(btnStop, &QPushButton::clicked, spinner, &LoadingSpinner::stopAnimation);

    layout->addWidget(spinner, 0, Qt::AlignCenter);
    layout->addWidget(btnStart);
    layout->addWidget(btnStop);

    window.resize(300, 300);
    window.show();

    return app.exec();
}

この実装では、アニメーションの各フレームで角度を計算し直すことで、滑らかな回転を実現しています。透明度のグラデーション(トレイル効果)は、線が回転方向に進んでいるように見せる重要な要素です。

タグ: Qt C++ QPainter QWidget UI Animation

6月22日 22:56 投稿