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