QtのQWidgetActionを用いた高機能カスタムメニューの実装

Qtで標準的なメニューを実装する場合、QMenuQActionの組み合わせに加え、QSS(Qt Style Sheets)を適用することである程度の見た目のカスタマイズが可能です。しかし、セキュリティソフトやシステム管理ツール(360セキュリティやPCマネージャーなど)で見られるような、リッチなビジュアル効果を持つメニューを実現するには、QWidgetActionを利用してウィジェットレベルでの自由なレイアウトを行う必要があります。本記事では、Qt 4.2以降で利用可能なこの機能を活用し、アイコンや背景色が動的に変化する独自のメニュー項目を作成する手順を解説します。

実装目標とするUIは、各メニュー項目が「アイコンエリア(左側)」と「テキストエリア(右側)」で構成されています。マウスカーソルが項目上に移動した際(ホバー状態)、背景色が赤に変化し、アイコンが切り替わり、テキストの色も変化するというインタラクティブなデザインです。

1. システムトレイクラスの実装

まず、システムトレイアイコンを管理するクラスを定義します。QSystemTrayIconを継承し、メニューの生成やアイコンのアニメーション制御(点滅機能など)を行います。以下のSystemTrayManagerクラスは、トレイアイコンのクリックイベント処理やカスタムメニューの保持を担当します。


class SystemTrayManager : public QSystemTrayIcon
{
    Q_OBJECT

signals:
    void showMainWindow();
    void quitApplication();

public:
    SystemTrayManager(const QIcon &icon, QObject *parent = nullptr);
    ~SystemTrayManager();

    // アイコンの点滅設定
    void setIconBlinking(bool enabled);
    // バルーンヒントの表示
    void displayNotification(const QString &title, const QString &msg, 
                             QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::Information, 
                             int timeout = 5000);

    QAction* addCustomAction(const QString &text, const QString &iconPath);

protected:
    bool event(QEvent *e) override;
    void timerEvent(QTimerEvent *event) override;
    bool eventFilter(QObject *watched, QEvent *event) override;

private slots:
    void onTrayActivated(QSystemTrayIcon::ActivationReason reason);

private:
    void initializeMenu();

    bool isMouseOutside = true;
    int blinkTimerId = 0;
    QIcon baseIcon;
    CustomPopupMenu *contextMenu = nullptr;
    
    // カスタムアクションのリスト
    CustomMenuAction *actionShow = nullptr;
    CustomMenuAction *actionExit = nullptr;
};

2. QWidgetActionの継承

メニュー項目として機能するカスタムウィジェットを作成するために、QWidgetActionを継承したクラスCustomMenuActionを定義します。このクラスは、QActionの特性を保ちつつ、独自のUIウィジェットを内部に持つことができます。


class CustomMenuAction : public QWidgetAction
{
    Q_OBJECT
    // QSSでの状態管理用プロパティ
    Q_PROPERTY(bool isHovered READ isHovered WRITE setHovered)

public:
    explicit CustomMenuAction(const QString &text, QWidget *parent = nullptr);
    ~CustomMenuAction();

    void setLabel(const QString &text);
    void setIcons(const QString &normal, const QString &hovered);
    void setIcons(const QString &normal, const QString &hovered, const QString &pressed);

    MenuEntryWidget* entryWidget() const;

    bool isHovered() const { return hoverState; }
    void setHovered(bool state) { hoverState = state; }

protected:
    QWidget* createWidget(QWidget *parent) override;
    void deleteWidget(QWidget *widget) override;

private:
    bool hoverState = false;
    MenuEntryWidget *widgetContainer = nullptr;
};

コンストラクタでは、MenuEntryWidget(後述)のインスタンスを生成し、setDefaultWidgetメソッドで設定します。これにより、アクションがメニューに追加されると、このウィジェットが表示されます。


CustomMenuAction::CustomMenuAction(const QString &text, QWidget *parent)
    : QWidgetAction(parent)
{
    setEnabled(true);
    widgetContainer = new MenuEntryWidget();
    
    // ウィジェット内のクリックシグナルをアクションのトリガーに接続
    connect(widgetContainer, &MenuEntryWidget::itemClicked, this, [this] {
        this->trigger();
    });
    
    widgetContainer->setLabel(text);
    setDefaultWidget(widgetContainer);
}

createWidgetメソッドは、メニューが表示される際にQtフレームワークによって呼び出されます。ここでは、親ウィジェットの設定を適切に行います。


QWidget* CustomMenuAction::createWidget(QWidget *parent)
{
    widgetContainer->setParent(parent);
    return widgetContainer;
}

3. カスタムウィジェットの実装

実際の見た目を描画するMenuEntryWidgetクラスです。QWidgetを継承し、アイコン用のボタンとテキスト用のラベルを配置します。マウスの进出イベントをオーバーライドすることで、ホバー効果の制御を行います。


class MenuEntryWidget : public QWidget
{
    Q_OBJECT

signals:
    void itemClicked();

public:
    explicit MenuEntryWidget(QWidget *parent = nullptr);
    ~MenuEntryWidget();

    void setLabel(const QString &text);
    void setIcons(const QString &normal, const QString &hovered);

    void updateStyle(bool hovered);

protected:
    void enterEvent(QEvent *event) override;
    void leaveEvent(QEvent *event) override;
    bool eventFilter(QObject *watched, QEvent *event) override;

private:
    void setupLayout();

    QPushButton *iconButton = nullptr;
    QLabel *textLabel = nullptr;
    QString iconNormalPath;
    QString iconHoverPath;
};

4. カスタムメニュークラス

メニュー自体の挙動を制御するために、QMenuを継承したCustomPopupMenuクラスを作成します。主な目的は、メニューが表示されたタイミング(QEvent::Show)を検知し、必要に応じて位置調整などの処理を行うシグナルを発火させることです。


class CustomPopupMenu : public QMenu
{
    Q_OBJECT

signals:
    void menuShown(); // 位置調整などのトリガー用シグナル

public:
    explicit CustomPopupMenu(QWidget *parent = nullptr) : QMenu(parent) {}

protected:
    bool event(QEvent *e) override
    {
        if (e->type() == QEvent::Show) {
            emit menuShown();
        }
        return QMenu::event(e);
    }
};

5. QSSセレクタの問題とイベントフィルタによる回避策

カスタムメニューの実装中、QSSのプロパティセレクタ(例:QLabel[isHovered=true]{ color: red; })が期待通りに動作しないケースがあります。この問題に対処するため、親クラス(SystemTrayManager)のイベントフィルタを利用して、描画イベント(QEvent::Paint)発生時にマウスカーソルの位置を監視し、手動でスタイルを更新するアプローチを取ります。


bool SystemTrayManager::eventFilter(QObject *watched, QEvent *event)
{
    // マウスがトレイアイコン外にあるかの簡易判定
    if (watched == this) {
        isMouseOutside = false;
    }

    // カスタムウィジェットのPaintイベント時にカーソル位置をチェック
    if (watched->inherits("QWidget") && event->type() == QEvent::Paint) {
        if (MenuEntryWidget *entry = qobject_cast<MenuEntryWidget *>(watched)) {
            // グローバル座標でのカーソル位置がウィジェット内にあるか判定
            if (entry->rect().contains(entry->mapFromGlobal(QCursor::pos()))) {
                entry->updateStyle(true);
            } else {
                entry->updateStyle(false);
            }
        }
    }

    return QSystemTrayIcon::eventFilter(watched, event);
}

この手法により、QSSだけでは制御しにくい動的な背景色やアイコンの切り替えを、確実に実行できるようになります。以上の手順で、Qt標準のメニュー機能を拡張し、柔軟で高度なUIを持つシステムトレイメニューを構築することが可能です。

タグ: Qt QWidgetAction QSystemTrayIcon C++ GUI

6月26日 18:08 投稿