概要
Qtのアニメーションシステムとステートマシンを活用した視覚効果に優れたフローティングメニューの実装方法について解説します。QtCreatorのサンプルプログラムである「2D Painting」と「Animated Tiles」を参考に、メニューの展開アニメーションを実現します。
実装例
以下の2種類のフローティングメニューを実装しました:
- 基本円形メニュー - シンプルな円形配置のメニュー項目
- 高度なフローティングメニュー - サブメニュー機能をサポート
実装コード
1. メニュー項目クラス
class FloatingMenuItem : public QLabel
{
Q_OBJECT
public:
explicit FloatingMenuItem(QWidget *parent = nullptr);
~FloatingMenuItem();
void setDiameter(int diameter);
int getDiameter() const;
void linkAction(QAction *action);
signals:
void hoverStateChanged(bool);
protected:
void enterEvent(QEvent *event) override;
void leaveEvent(QEvent *event) override;
void paintEvent(QPaintEvent *event) override;
private:
int m_diameter = 50;
QAction *m_linkedAction = nullptr;
};
2. フローティングメニュークラス
class QVariantAnimation;
class QPropertyAnimation;
class FloatingMenu : public FloatingMenuItem
{
Q_OBJECT
public:
explicit FloatingMenu(QWidget *parent = nullptr);
~FloatingMenu();
signals:
void doubleClickDetected();
public:
void setActionList(const QVector<QAction *> &actions);
void setIconList(const QVector<QString> &icons);
void enableAnimation(bool enable);
bool isAnimationEnabled() const;
void setAutoFade(bool enable);
bool isAutoFadeEnabled() const;
void setSpreadDistance(int distance);
int getSpreadDistance() const;
void setInitialAngle(int angle);
int getInitialAngle() const;
void setAngleIncrement(int angle);
int getAngleIncrement() const;
void setMainButtonSize(int size);
int getMainButtonSize() const;
void setItemSize(int size);
int getItemSize() const;
protected:
void enterEvent(QEvent *event) override;
void leaveEvent(QEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void timerEvent(QTimerEvent *event) override;
bool event(QEvent *event) override;
private slots:
void onHoverStateChange(bool);
private:
void updateItemPositions(int progressValue);
void expandMenu();
void collapseMenu();
void startFadeOut();
void cancelFadeOut();
bool isMouseOverAnyItem() const;
void scheduleCollapse();
void cancelCollapseTimer();
private:
int m_spreadDistance = 70;
int m_initialAngle = 0;
int m_angleStep = 60;
int m_mainButtonSize = 70;
int m_itemSize = 60;
int m_collapseTimerId = -1;
QPropertyAnimation *m_fadeAnimation = nullptr;
QVariantAnimation *m_spreadAnimation = nullptr;
QVector<FloatingMenuItem *> m_menuItems;
};
3. 実装の要点
アニメーションの初期化:
m_spreadAnimation = new QVariantAnimation(this);
m_spreadAnimation->setEasingCurve(QEasingCurve::InCubic);
m_spreadAnimation->setStartValue(AnimationStart);
m_spreadAnimation->setEndValue(AnimationEnd);
m_spreadAnimation->setDuration(AnimationDuration);
connect(m_spreadAnimation, &QVariantAnimation::valueChanged,
this, [this](const QVariant &value){
updateItemPositions(value.toInt());
});
マウスホバー時の展開処理:
void FloatingMenu::expandMenu()
{
if (m_spreadAnimation)
{
if (m_spreadAnimation->state() != QAbstractAnimation::Running
&& m_spreadAnimation->currentValue().toInt() != AnimationEnd)
{
m_spreadAnimation->setDirection(QVariantAnimation::Forward);
m_spreadAnimation->start();
}
}
else
{
updateItemPositions(AnimationEnd);
}
cancelCollapseTimer();
cancelFadeOut();
}
メニュー収集時の処理:
void FloatingMenu::collapseMenu()
{
if (!isMouseOverAnyItem())
{
if (m_spreadAnimation)
{
m_spreadAnimation->setDirection(QVariantAnimation::Backward);
m_spreadAnimation->start();
}
else
{
updateItemPositions(AnimationStart);
}
cancelCollapseTimer();
startFadeOut();
}
}
アニメーション中のアイテム配置計算:
void FloatingMenu::updateItemPositions(int progress)
{
int currentDistance = progress * m_spreadDistance / AnimationEnd;
for (int i = 0; i < m_menuItems.size(); ++i)
{
FloatingMenuItem *item = m_menuItems.at(i);
double radians = qDegreesToRadians(m_angleStep * i * 1.0 + m_initialAngle);
int offsetX = currentDistance * qCos(radians);
int offsetY = currentDistance * qSin(radians);
item->move(pos() + QPoint(offsetX, offsetY));
int currentSize = progress * m_itemSize / AnimationEnd;
item->setDiameter(currentSize);
item->setVisible(AnimationStart != progress);
};
::SetWindowPos(HWND(winId()), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}
自動フェードアウト機能:
void FloatingMenu::setAutoFade(bool enable)
{
if (enable)
{
if (!m_fadeAnimation)
{
m_fadeAnimation = new QPropertyAnimation(this, "windowOpacity");
m_fadeAnimation->setEasingCurve(QEasingCurve::OutCubic);
m_fadeAnimation->setStartValue(FadeStart);
m_fadeAnimation->setEndValue(FadeEnd);
m_fadeAnimation->setDuration(FadeDuration);
}
}
else
{
if (m_fadeAnimation)
{
delete m_fadeAnimation;
m_fadeAnimation = nullptr;
}
}
}