OpenSiv3Dでトレイアイコンを設定

はじめに

OpenSiv3Dはゲームだけでなく、アプリの開発も簡単に行えます。 そこで、トレイアイコンを設定することで開発の幅も広がると思って作ってみました。

作ったもの

image.png

ソースコード
#pragma once
#include <Siv3D/Windows/Windows.hpp>

// メニュー項目
struct TrayItem {
    static constexpr char32_t SeparatorLabel[] = U"---";

    String label;
    int32 id = 0;
    Array<TrayItem> submenu;

    // 通常項目用
    TrayItem(const String& _label, int32 _id) :
        label{ _label }, id{ _id } {
    }

    // サブメニュー用
    TrayItem(const String& _label, const Array<TrayItem>& _submenu)
        : label{ _label }, submenu{ _submenu }, id{ 0 } {
    }

    // 区切り用
    static TrayItem Separator() {
        return TrayItem{ SeparatorLabel, -1 };
    }

    bool isSeparator() const {
        return label == SeparatorLabel;
    }
};

// タスクトレイ管理クラス
class TaskTray {
    bool m_clicked = false;
    HWND m_hWnd = nullptr;
    HMENU m_hMenu = nullptr;
    NOTIFYICONDATA m_nid = {};
    Optional<int32> m_selectedID = none;

public:
    TaskTray(const String& tip, const Array<TrayItem>& menu = {}) {
        const wchar_t* className = L"Siv3DTaskTrayHelper";
        HINSTANCE hInst = ::GetModuleHandle(NULL);

        // クラスの作成(登録済みなら登録しない)
        WNDCLASSEX wc = { .cbSize = sizeof(WNDCLASSEX) };
        if (!::GetClassInfoEx(hInst, className, &wc)) {
            wc.lpfnWndProc = WndProc;
            wc.hInstance = hInst;
            wc.lpszClassName = className;
            ::RegisterClassEx(&wc);
        }

        // メッセージ処理用のウィンドウの作成 (メッセージ専用ウィンドウ)
        m_hWnd = ::CreateWindowEx(0, className, nullptr, 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, hInst, nullptr);
        ::SetWindowLongPtr(m_hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));

        // アイコンの作成
        m_nid.cbSize = sizeof(NOTIFYICONDATA);
        m_nid.hWnd = m_hWnd;
        m_nid.uID = 1;
        m_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
        m_nid.uCallbackMessage = WM_USER + 1;

        // リソースID 100(Resource.rcで定義されているアプリのアイコン)を読み込み。なければデフォルト
        m_nid.hIcon = ::LoadIcon(hInst, MAKEINTRESOURCE(100));
        if (!m_nid.hIcon) m_nid.hIcon = ::LoadIcon(nullptr, IDI_APPLICATION);

        // アイコンのツールチップ設定
        const std::wstring wtip = tip.toWstr();
        size_t len = std::min<size_t>(wtip.length(), 127);
        std::copy_n(wtip.begin(), len, m_nid.szTip);
        m_nid.szTip[len] = L'\0';

        // アイコンの登録
        ::Shell_NotifyIcon(NIM_ADD, &m_nid);

        // メニューの作成
        m_hMenu = ::CreatePopupMenu();
        buildMenu(m_hMenu, menu);
    }

    ~TaskTray() {
        if (m_hMenu) ::DestroyMenu(m_hMenu);
        ::Shell_NotifyIcon(NIM_DELETE, &m_nid);
        ::DestroyWindow(m_hWnd);
    }

    // 毎フレーム呼び出して選択されたメニューIDを返す
    Optional<int32> update() {
        m_selectedID = none;
        m_clicked = false;

        MSG msg;
        while (::PeekMessage(&msg, m_hWnd, 0, 0, PM_REMOVE)) {
            ::DispatchMessage(&msg);
        }

        return m_selectedID;
    }

    bool leftClicked() const {
        return m_clicked;
    }

private:
    void buildMenu(HMENU hParent, const Array<TrayItem>& items) {
        for (const auto& item : items) {
            if (item.isSeparator()) {
                ::AppendMenuW(hParent, MF_SEPARATOR, 0, 0);
            }
            else if (item.submenu.isEmpty()) {
                ::AppendMenuW(hParent, MF_STRING, item.id, item.label.toWstr().c_str());
            }
            else {
                HMENU hSub = ::CreatePopupMenu();
                buildMenu(hSub, item.submenu);
                ::AppendMenuW(hParent, MF_POPUP, reinterpret_cast<UINT_PTR>(hSub), item.label.toWstr().c_str());
            }
        }
    }

    void showPopupMenu() {
        if (!m_hMenu) return;

        POINT pt;
        ::GetCursorPos(&pt);
        ::SetForegroundWindow(m_hWnd);

        // メニューを表示し、選択されたIDを取得
        int32 id = ::TrackPopupMenu(m_hMenu, TPM_RETURNCMD | TPM_NONOTIFY | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, 0);

        ::PostMessage(m_hWnd, WM_NULL, 0, 0);

        if (id > 0) m_selectedID = id;
    }

    void handleMessage(UINT m, LPARAM l) {
        if (m == WM_USER + 1) {
            if (l == WM_LBUTTONUP) m_clicked = true;
            if (l == WM_RBUTTONUP) showPopupMenu();
        }
    }

    static LRESULT CALLBACK WndProc(HWND h, UINT m, WPARAM w, LPARAM l) {
        auto* pThis = reinterpret_cast<TaskTray*>(::GetWindowLongPtr(h, GWLP_USERDATA));
        if (pThis) pThis->handleMessage(m, l);
        return ::DefWindowProc(h, m, w, l);
    }
};

簡単なサンプル

#include <Siv3D.hpp>
#include "TaskTray.hpp"

void Main() {
    // 第一引数にツールチップ、第二引数にはメニューのオプション(メニュー名, ID or サブメニュー)
    TaskTray tray{ U"ツールチップ", {
        { U"メニュー1", 101 },
        { U"メニュー2", 102 },
        TrayItem::Separator(),
        { U"サブメニュー", {
            { U"アイテム1", 201 },
            { U"アイテム2", 202 }
        }}
    } };

    while (System::Update()) {
        if (auto id = tray.update()) {
            if (id == 101) Print << U"メニュー1";
            if (id == 102) Print << U"メニュー2";

            if (id == 201) Print << U"アイテム1";
            if (id == 202) Print << U"アイテム2";
        }

        if (tray.leftClicked()) {
            Print << U"クリックされた";
        }
    }
}

実装のポイント

メッセージ専用ウィンドウ

Siv3Dアプリのウィンドウとは別に、トレイアイコンからのメッセージを受け取るだけの非表示ウィンドウを作成しています。これにより、ウィンドウの状態(最小化など)に左右されずトレイアイコンのイベントを受け取れます。

TrackPopupMenuの挙動

右クリック時にメニューを表示する際、TPM_RETURNCMDを指定することで、選択されたメニューIDを関数の戻り値として直接受け取っています。これをm_selectedIDに格納し、update()の戻り値としてSiv3Dのループへ渡しています。

まとめ

このクラスを使えば簡単に常駐型アプリのインターフェースを実装できると思います。 Win32 APIを触ったことがあまりないので、間違いやこうしたほうがいいなどあると思います。改善点など気軽にコメントしてください🙏

ブログ一覧に戻る