Qtフローティングメニューの実装手法

概要

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;
        }
    }
}

タグ: Qt アニメーション フローティングメニュー QPropertyAnimation QVariantAnimation

6月5日 16:57 投稿