プロフィール

髭山髭人(ひげひと)

自分の書いた記事が、一人でも誰かの役に立てば...
活動信条の一つとして「貴方のメモは、誰かのヒント」というのがあります。

このサイトについて

本家HP packetroom.net から切り離した いわゆる技術メモ用のブログで、無料レンタルサーバーにて運用しています。広告表示はその義務なのでご容赦。
XREA さんには長年お世話になっています

C++ Win32API メニュースタイルの試行錯誤メモ

概要とか

  • 執筆意図 & 前提
    • メニュー表示型/非常駐のシンプルなランチャーアプリ作成を目標にしている
    • それゆえ、メニューのデザイン・表示自体をこねくりまわせるようになりたい

以前書いた記事 C++ 極小レベルのWinFormアプリを.NET系無しwin32で作成 の続き?です

  • 環境

    • windows10 x64 , 22H2
    • C++ / Win32Api
    • Visual Studio Community 2022
  • 基本仕様

    • ポップアップメニューのみ表示 ( メインウィンドウ自体は非表示 )
    • バックグラウンドプロセス起動
      タスクマネージャーの「アプリ」欄には出ない ( 広義で言う常駐系 )

メニューの動的生成

ポイント

ウィンドウ自体は表示させないので、本来使うはずの ShowWindow() UpdateWindow() は省略

WM_CREATE メッセージタイミング内でポップアップメニューを作成し、AppendMenu() でメニューアイテムを構築

メニュークリックは WM_COMMAND メッセージ内で拾う。
「アプリ終了」のメニューIDが 本例では 6 となるので、その条件で終了させた

IDEの「出力」部分でデバッグログを表示させたかったので、マクロ機能で MyOutputDebugString() をこさえた。
参考 : ○×つくろーどっとコム - デバッグウィンドウを知らないと大変です

ソース

#include <windows.h>
#include <stdio.h>  // _snwprintf_s 用
#define MyOutputDebugString( str, ... ) { wchar_t buffer[256]; _snwprintf_s( buffer , sizeof(buffer) , str, __VA_ARGS__ ); OutputDebugString( buffer ); }

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HMENU g_hMenu = nullptr;

int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
{
    const WCHAR* szClassName = L"myWindowClass";
    WNDCLASSEX wcex;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(wcex.hInstance, IDI_APPLICATION);
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = szClassName;
    wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION);

    if (!RegisterClassEx(&wcex))
    {
        return 1;
    }

    HWND hWnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, szClassName, L"MenuApp", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 250, 100,
        NULL, NULL, hInstance, NULL);

    if (hWnd == NULL)
    {
        return 1;
    }

    // ▼ ウィンドウを表示させない(する必要がない)ので、この2処理は省かれる
    //ShowWindow(hWnd, nCmdShow);
    //UpdateWindow(hWnd);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    MyOutputDebugString( L"MSG 0x%x\t\t%d \n" , message , message);

    int idCount = 0;  // とりあえずメニューID相当
    WORD cmdId, cmdEvent;
    switch (message)
    {
        case WM_CREATE:
            // メニュー作成準備
            g_hMenu = CreatePopupMenu();
            AppendMenu(g_hMenu, MF_STRING, idCount++, L"アイテム1");
            AppendMenu(g_hMenu, MF_STRING, idCount++, L"アイテム2");
            AppendMenu(g_hMenu, MF_MENUBARBREAK , idCount++, L"改行付きアイテム");
            AppendMenu(g_hMenu, MF_STRING, idCount++, L"アイテム3");
            AppendMenu(g_hMenu, MF_SEPARATOR, idCount++, L"セパレーター");
            AppendMenu(g_hMenu, MF_STRING, idCount++, L"アイテム4");
            AppendMenu(g_hMenu, MF_STRING, idCount++, L"アプリ終了");    // メニューID:6 相当
            // 表示
            TrackPopupMenu(g_hMenu, TPM_LEFTALIGN | TPM_TOPALIGN, 100, 200, 0, hWnd, NULL);
            break;
        case WM_COMMAND:
            cmdId = LOWORD(wParam);
            cmdEvent = HIWORD(wParam);
            MyOutputDebugString(L"WM_COMMAND ID=%d Event=%d\n", cmdId, cmdEvent);
            // 「アプリ終了」(ID:6)のアイテムが押されたら終了。
            switch (cmdId) {
                case 6:
                    PostQuitMessage(0);
                    break;
            }
            break;
        case WM_DESTROY:
            DestroyMenu(g_hMenu); // メニュー破棄
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

メニュースタイルについて

上記で用いた AppendMenu() は古い関数で、新しい InsertMenuItem() を使うと昨今のOS用スタイルになるっぽい ( 画像左 )
ただし、この InsertMenuItem() を使ったとしても、
改行(横列の追加切替)である MENUBARBREAK 系を割り当てた場合は旧スタイルとなる? ( 画像右 )

下記はメニュー作成処理を AppendMenu() から InsertMenuItem() に切り替えた箇所の抜粋。
MFT_MENUBARBREAK の改行付きアイテム有無に応じて、↑ 画像のように見た目(スタイル)が変わるはず…

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    int idCount = 0;
    WORD cmdId, cmdEvent;
    MENUITEMINFO mii;
    switch (message)
    {
        case WM_CREATE:
            g_hMenu = CreatePopupMenu();
            {
                memset(&mii, 0, sizeof(MENUITEMINFO));
                mii.cbSize = sizeof(MENUITEMINFO);
                mii.fMask = MIIM_TYPE;

                mii.fType = MFT_STRING;
                mii.wID = idCount++;
                mii.dwTypeData = (LPWSTR)L"アイテム";
                InsertMenuItem(g_hMenu, -1 , false , &mii);

                // ↓ のあるなしでスタイルが変わる
+                mii.fType = MFT_MENUBARBREAK;
+                mii.wID = idCount++;
+                mii.dwTypeData = (LPWSTR)L"改行付きアイテム";
+                InsertMenuItem(g_hMenu, -1, false, &mii);

                mii.fType = MF_SEPARATOR;
                mii.wID = idCount++;
                mii.dwTypeData = (LPWSTR)L"セパレーター";
                InsertMenuItem(g_hMenu, -1, false, &mii);
            }
            TrackPopupMenu(g_hMenu, TPM_LEFTALIGN | TPM_TOPALIGN, 300, 300, 0, hWnd, NULL);
            break;
        // …以下略…

オーナードローを試す

各メニューの描画内容を細かく弄れるらしいが、やることが幾分増える模様。

触ってみて感じたポイントは4つ

  • MENUITEMINFO.fType = MFT_OWNERDRAW としてオーナードロー明示
  • WM_MEASUREITEM メッセージ内で文字表示系の描画領域情報を確保する
    • 位置計算の折、CreateFont()SelectObject() を駆使して、適切なフォントをセット
  • WM_DRAWITEM メッセージ内で色や文字・背景色などの描画

上記スクショは試しに描画させてみたもの。

メニューアイテムのID:0と1とで処理を変えてみたところ、ID:1は選択状態でも背景色が変わらない。
もっと言うと、case 1: 処理を省けば文字すら描画されなくなる ( それはそう )

「オーナードローの選択肢 = 何もかも自分で描画する」ゆえ、ソース上で記載された必要最低限(文字だけ)の領域しか得られず、
露骨に狭くなってるのがわかるかも。 ( チェックボックス用の空きすら無い )

  • 個人的ハマりどころ
    CreateFont()SelectObject() でフォントを一時設定する処理を省くと、正しい描画領域が得られないので注意。

ソース

#include <windows.h>
#include <stdio.h>  // _snwprintf_s 用
#define MyOutputDebugString( str, ... ) { wchar_t buffer[256]; _snwprintf_s( buffer , sizeof(buffer) , str, __VA_ARGS__ ); OutputDebugString( buffer ); }

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

HMENU g_hMenu = nullptr;

int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
{
    const WCHAR* szClassName = L"myWindowClass";

    WNDCLASSEX wcex;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(wcex.hInstance, IDI_APPLICATION);
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = szClassName;
    wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION);

    if (!RegisterClassEx(&wcex))
    {
        return 1;
    }

    HWND hWnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, szClassName, L"MenuApp", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 250, 100,
        NULL, NULL, hInstance, NULL);

    if (hWnd == NULL)
    {
        return 1;
    }

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    int idCount = 0;
    MENUITEMINFO mii;

    LPMEASUREITEMSTRUCT lpMI;
    LPDRAWITEMSTRUCT lpDI;
    HDC hdc;
    RECT rect;
    SIZE size;
    LPCTSTR menuText = L"メニューアイテム";
    HBRUSH hBrush;
    HFONT hFontOld;
    HFONT hFont;
    switch (message)
    {
    case WM_CREATE:
        // メニュー作成準備
        g_hMenu = CreatePopupMenu();
        {
            memset(&mii, 0, sizeof(MENUITEMINFO));
            mii.cbSize = sizeof(MENUITEMINFO);

            mii.fMask = MIIM_FTYPE | MIIM_ID | MIIM_STATE | MIIM_STRING;
            mii.fType = MFT_OWNERDRAW;
            mii.wID = idCount++;        // ID:0
            mii.cch = lstrlen(menuText);
            InsertMenuItem(g_hMenu, -1, false, &mii);

            mii.fMask = MIIM_FTYPE | MIIM_ID | MIIM_STATE | MIIM_STRING;
            mii.fType = MFT_OWNERDRAW;
            mii.wID = idCount++;        // ID:1
            mii.cch = lstrlen(menuText);
            InsertMenuItem(g_hMenu, -1, false, &mii);
        }
        TrackPopupMenu(g_hMenu, TPM_LEFTALIGN | TPM_TOPALIGN, 400, 300, 0, hWnd, NULL);
        break;
    case WM_MEASUREITEM:
        // テキスト表示の下準備 ( 描画領域情報の取得・確保 )
        lpMI = (LPMEASUREITEMSTRUCT)lParam;
        hdc = GetDC(hWnd);
        // 正しい領域を得る為、フォントを設定
        hFont = CreateFont(16, 0, 0, 0, FW_REGULAR, FALSE, FALSE, FALSE,
            SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
            FIXED_PITCH | FF_ROMAN, L"Yu Gothic UI");
        hFontOld = (HFONT)SelectObject(hdc, hFont);
        GetTextExtentPoint32(hdc, menuText, lstrlen(menuText) - 1, &size);
        // ※ そのままだと文字分の領域しか得られないので、アイコン等を追加表示させたい場合は、
        //    このタイミングで itemWidth , itemHeight 等任意にサイズを加算させることを推奨
        lpMI->itemWidth = ( size.cx + 0 ); // 任意で加算
        lpMI->itemHeight = ( size.cy + 0 ); // 任意で加算
        SelectObject(hdc, hFontOld);
        DeleteObject(hFont);
        ReleaseDC(hWnd, hdc);
        break;
    case WM_DRAWITEM:
        lpDI = (LPDRAWITEMSTRUCT)lParam;
        rect = lpDI->rcItem;
        hdc = lpDI->hDC;
        switch (lpDI->itemID)
        {
        case 0: // ID:0
            // 選択有無に応じて色を変える
            if (lpDI->itemState & ODS_SELECTED) {
                SetBkColor(hdc, GetSysColor(COLOR_HIGHLIGHT));
                SetTextColor(hdc, GetSysColor(COLOR_HIGHLIGHTTEXT));
                hBrush = CreateSolidBrush(GetSysColor(COLOR_HIGHLIGHT));
            }
            else {
                hBrush = CreateSolidBrush(GetBkColor(hdc));
            }
            FillRect(hdc, &rect, hBrush);
            TextOut(hdc, rect.left, rect.top, menuText, lstrlen(menuText));
            DeleteObject(hBrush);
            break;
        case 1:
            // メニュー文字列だけ表示するので、色は変わらない
            TextOut(hdc, rect.left, rect.top, menuText, lstrlen(menuText));
        default:
            break;
        }
        break;
    case WM_DESTROY:
        DestroyMenu(g_hMenu);
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

アイコン取得 & 描画処理を追加

大まかな流れ

  1. ExtractIconEx() でアイコンハンドルを取得
    ExtractIconExA 関数 (shellapi.h)
    この時、大・小の2サイズを得られるが、本例では小サイズのみ利用する
  2. GetSystemMetrics() でシステムアイコン(小)の大きさを取得
    GetSystemMetrics > パラメータ SM_CXSMICON
  3. WM_DRAWITEM メッセージ内で描画
    描画には DrawIconEx() を使う。
    DrawIconEx 関数 (winuser.h)
  4. 終了時 DestroyIcon() でアイコンハンドルを片付ける
  • メモ : アイコンハンドル破棄の有無
    本ケースでは終了時に DestroyIcon() でアイコンハンドル破棄をしているが、
    以下リンク先で解説される特定関数で得た「共有アイコン」と呼ばれる物は、逆に破棄しないのが望ましい模様
    DestroyIcon 関数 (winuser.h) > 解説

以下スクショは WM_DRAWITEM メッセージ内の物
本例では、メモ帳 ( notepad.exe ) のアイコンを表示させている。
文字用領域 + アイコン表示用 を意識して、サイズ確保時やレンダリング時の座標に手を加えているのがポイント

ソース

diff フォーマットで表記。

#include <windows.h>
#include <stdio.h>  // _snwprintf_s 用
#define MyOutputDebugString( str, ... ) { wchar_t buffer[256]; _snwprintf_s( buffer , sizeof(buffer) , str, __VA_ARGS__ ); OutputDebugString( buffer ); }

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HMENU g_hMenu = nullptr;
+ HICON g_hIconSmall = NULL;
+ SIZE g_iconSize;

int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
{
    const WCHAR* szClassName = L"myWindowClass";

    WNDCLASSEX wcex;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(wcex.hInstance, IDI_APPLICATION);
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = szClassName;
    wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION);

    if (!RegisterClassEx(&wcex))
    {
        return 1;
    }

    HWND hWnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, szClassName, L"MenuApp", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 250, 100,
        NULL, NULL, hInstance, NULL);

    if (hWnd == NULL)
    {
        return 1;
    }

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    int idCount = 0;
    WORD cmdId, cmdEvent;
    MENUITEMINFO mii;

    LPMEASUREITEMSTRUCT lpMI;
    LPDRAWITEMSTRUCT lpDI;
    HDC hdc;
    RECT rect;
    SIZE size;
    LPCTSTR menuText = L"メニューアイテム";
    HBRUSH hBrush;
    switch (message)
    {
        case WM_CREATE:
            g_hMenu = CreatePopupMenu();
            {
                memset(&mii, 0, sizeof(MENUITEMINFO));
                mii.cbSize = sizeof(MENUITEMINFO);

                mii.fMask = MIIM_FTYPE | MIIM_ID | MIIM_STATE | MIIM_STRING;
                mii.fType = MFT_OWNERDRAW;
                mii.wID = idCount++;        // ID:0
                mii.cch = lstrlen(menuText);
                InsertMenuItem(g_hMenu, -1, false, &mii);

                mii.fMask = MIIM_FTYPE | MIIM_ID | MIIM_STATE | MIIM_STRING;
                mii.fType = MFT_OWNERDRAW;
                mii.wID = idCount++;        // ID:1
                mii.cch = lstrlen(menuText);
                InsertMenuItem(g_hMenu, -1, false, &mii);
            }
            {
+                // メモ帳 exe に含まれているアイコン (0番) を、小さいほうで取得
+                WCHAR path[MAX_PATH] = L"c:\\windows\\system32\\notepad.exe";
+                ExtractIconEx(path, 0, nullptr, &g_hIconSmall, 1);
+                // システム情報からアイコンサイズ(小)相当を確保
+                g_iconSize.cx = (int)GetSystemMetrics(SM_CXSMICON);
+                g_iconSize.cy = (int)GetSystemMetrics(SM_CYSMICON);
            }
            TrackPopupMenu(g_hMenu, TPM_LEFTALIGN | TPM_TOPALIGN, 400, 300, 0, hWnd, NULL);
            break;
        case WM_MEASUREITEM:
        lpMI = (LPMEASUREITEMSTRUCT)lParam;
        hdc = GetDC(hWnd);
        hFont = CreateFont(16, 0, 0, 0, FW_REGULAR, FALSE, FALSE, FALSE,
            SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
            FIXED_PITCH | FF_ROMAN, L"Yu Gothic UI");
        hFontOld = (HFONT)SelectObject(hdc, hFont);
        GetTextExtentPoint32(hdc, menuText, lstrlen(menuText) - 1, &size);
+       // 左側にアイコンを表示させるぶん、領域を増やす
+       lpMI->itemWidth = size.cx + (g_iconSize.cx + 5);
+       // アイコンサイズ(高さ) or 文字レンダリング領域(高さ) を比べて大きいほうを採用 
+       lpMI->itemHeight = max( size.cy , g_iconSize.cx );
-       lpMI->itemWidth = size.cx;
-       lpMI->itemHeight = size.cy;
        SelectObject(hdc, hFontOld);
        DeleteObject(hFont);
        ReleaseDC(hWnd, hdc);
        case WM_DRAWITEM:
            lpDI = (LPDRAWITEMSTRUCT)lParam;
            rect = lpDI->rcItem;
            hdc = lpDI->hDC;
            switch (lpDI->itemID)
            {
                case 0: // ID:0
                    // 選択有無に応じて色を変える
                    if (lpDI->itemState & ODS_SELECTED) {
                        SetBkColor(hdc, GetSysColor(COLOR_HIGHLIGHT));
                        SetTextColor(hdc, GetSysColor(COLOR_HIGHLIGHTTEXT));
                        hBrush = CreateSolidBrush(GetSysColor(COLOR_HIGHLIGHT));
                    }
                    else {
                        hBrush = CreateSolidBrush(GetBkColor(hdc));
                    }
                    FillRect(hdc, &rect, hBrush);
+                   // 左側にアイコンを表示させるので、テキスト開始位置を右側へずらす
+                   TextOut(hdc, (rect.left + g_iconSize.cx + 5), rect.top, menuText, lstrlen(menuText));
+                   // アイコン表示
+                   DrawIconEx(hdc, (rect.left+0), (rect.top+0), g_hIconSmall, g_iconSize.cx, g_iconSize.cy , 0, NULL, DI_NORMAL);
-                   TextOut(hdc, rect.left, rect.top, menuText, lstrlen(menuText));
                    DeleteObject(hBrush);
                    break;
                case 1:
                    TextOut(hdc, rect.left, rect.top, menuText, lstrlen(menuText));
                default:
                    break;
            }
            break;
        case WM_DESTROY:
+            DestroyIcon(g_hIconSmall);
            DestroyMenu(g_hMenu);
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

その他

自分がメニュー上で欲しい描画処理はこれでざっくり得られた…と思う

ここから自分の望む簡易メニューランチャーに近づけるには、
exeパス / メニューテキスト / 場合によってはアイコンパス …あたりの3つを保持した独自定義をこさえて、
それを for とかで回してメニュー構築 & クリック判定拾う処理 を作る感じになりそう。

更に作りこもうとすると、展開型の入れ子メニューの事も考えないといけないからカオスの予感

本記事のタイトルとは離れてきたのでこのあたりで〆