MFC CMFCMenuButtonで動的追加したサブメニューがダークモードにならない原因と解決策(DarkMode_Explorer)

Visual StudioのMFCでCMFCMenuButtonをダークモード(DarkMode_Explorer)にしているのに、LoadMenu→InsertMenu(MF_POPUP)で動的に追加したサブメニューだけがライト表示になることがあります。原因は描画設定ではなくCMenu/HMENUの寿命と所有権。再現しやすいパターンと安全な修正方法を整理します。

目次

現象:動的に追加した“中間層”だけダークにならない

CMFCMenuButtonは、クリックでドロップダウンメニュー(ポップアップメニュー)を表示できるMFCの便利なコントロールです。MFCのダークテーマ(DarkMode_Explorerなど)を有効にすると、通常はボタンから出るメニューも背景・文字色がダーク寄りに統一されます。

ところが、別のメニューリソースをLoadMenu()で読み込み、そのサブメニュー(HMENU)をInsertMenu(MF_POPUP)で「ボタンのメニューに動的追加」した場合に限って、次のような不整合が出ることがあります。

階層内容表示結果
レベル1CMFCMenuButtonが元々持っているメニューダーク表示(正常)
レベル2動的に追加したサブメニュー(中間層)ライト表示(ここだけ崩れる)
レベル3レベル2配下に元々ある深いサブメニューダーク表示(なぜか戻る)

見た目としては「追加したところだけ色が浮く」ため、ダークモードが効いていないように見えます。しかも深い階層(レベル3)がダークに戻るため、テーマ設定の問題に見えにくく、原因の切り分けに時間がかかりがちです。

まず押さえるポイント:これは“描画設定”より“所有権と寿命”の問題

結論から言うと、最も多い原因は追加元のCMenuオブジェクトの寿命(ライフタイム)が短いことです。具体的には、関数ローカルで作ったCMenuからHMENUを取り出して別メニューに挿し込んだのに、関数を抜けた瞬間にローカルのCMenuが破棄され、内部のHMENUがDestroyMenuされてしまう、という流れです。

この状態は「たまたま表示できる」こともありますが、実態としては破棄済み(または不完全)なメニューハンドルを参照しているので、ダークモード描画が崩れたり、アイコン・チェック表示が欠けたり、場合によってはクラッシュの温床になります。

再現しやすいコード例:ローカルCMenuからサブメニューを抜いてInsertMenuする

以下は問題が起きやすい典型例です(要点が伝わるように最小化しています)。

void CMyDlg::BuildMenu()
{
    // ボタンが持つベースメニュー(これはメンバーで保持している想定)
    // m_btnMenu.LoadMenu(IDR_MENU_BASE);

    // 追加したいサブメニューを別リソースから読み込む(関数ローカル)
    CMenu menuManageSpeakers;
    menuManageSpeakers.LoadMenu(IDR_MENU_POPUP_MANAGE);

    // 例:リソース側の先頭ポップアップを取ってくる
    HMENU hSub = menuManageSpeakers.GetSubMenu(0)->GetSafeHmenu();

    // ベースメニューに「ポップアップとして」挿し込む
    m_btnMenu.InsertMenu(0, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hSub, _T("Manage"));

    // 関数を抜けると menuManageSpeakers が破棄される
    // => デストラクタで DestroyMenu が走り、hSub が無効化される可能性がある
}

見た目の問題(ダークにならない)だけが表面化しているときでも、内部ではメニューの整合性が崩れていることが多いので、まずは「そのHMENUは生きているか」を疑うのが近道です。

CMenuとHMENUの関係:どこでDestroyMenuされるのか

MFCのCMenuは、Win32のHMENUをラップするクラスです。基本的な設計はRAII(リソース獲得は初期化、破棄はデストラクタ)に近く、CMenuオブジェクトが所有しているメニューは、オブジェクト破棄時に片付けられます。

要素役割落とし穴
CMenuHMENUを保持するMFCラッパースコープを抜けるとデストラクタが走り、所有しているHMENUが破棄され得る
HMENUWin32のメニューオブジェクト(ハンドル)参照カウントは基本なく、破棄されたら他所で使っていても無効になる
InsertMenu(MF_POPUP)サブメニューのHMENUを“別メニューの項目として”登録ハンドルの複製ではなく参照を渡すだけ。元の寿命が切れると破綻しやすい

つまり、「ローカルCMenuから取り出したHMENUを、別メニューに差し込む」という操作は、所有権が曖昧になりやすい危険なパターンです。たまたま表示できても、それは安全性が担保された挙動ではありません。

なぜ“中間層だけ”ライト表示になるのか

この症状が厄介なのは、「全部おかしい」ではなくレベル2だけが崩れることです。これは、メニュー表示のタイミングと内部処理が階層ごとに分かれているため、次のような状況が起こり得ます。

  • レベル1は最初からCMFCMenuButtonに正しく紐付いており、MFCのダーク描画(ビジュアルマネージャ)対象になっている
  • レベル2は“後から挿した”ハンドルで、しかも元のCMenuが破棄されていると、MFC側の管理情報が欠けたり、OS標準描画にフォールバックしたりしてライト表示になりやすい
  • レベル3は、カーソルホバーで開くタイミングで改めてサブメニューとして生成・初期化され、結果的にダーク描画パスに戻ることがある

要するに、見た目の差は「ダークモードのON/OFF」ではなく、その階層のメニューがMFCの想定する形で生きているかに左右されます。寿命が切れたハンドルや、所有権が二重化したハンドルは、ここで破綻しやすいです。

対処法:3つの安全な修正パターン

ここからが本題です。修正の方針はシンプルで、「追加元CMenuの寿命を、少なくともメニュー表示が終わるまで保証する」か、「HMENUの所有権を明確に移す」のどちらかです。

CMenuをクラスメンバーにして寿命を延ばす(推奨)

最も分かりやすく、運用で事故が起きにくい方法です。ローカルではなく、ダイアログやビューのメンバーとしてCMenuを保持し、必要なタイミングでLoadMenu()します。

// ヘッダ(例:CMyDlg.h)
class CMyDlg : public CDialogEx
{
    // ...
    CMenu m_menuButtonBase;        // ボタンが使うベース
    CMenu m_menuManageSpeakersRes; // 追加元(リソースロード用)
};
BOOL CMyDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    m_menuButtonBase.LoadMenu(IDR_MENU_BASE);
    m_menuManageSpeakersRes.LoadMenu(IDR_MENU_POPUP_MANAGE);

    // 例:追加元の先頭ポップアップを挿入
    HMENU hSub = m_menuManageSpeakersRes.GetSubMenu(0)->GetSafeHmenu();
    m_menuButtonBase.InsertMenu(0, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hSub, _T("Manage"));

    // CMFCMenuButtonに関連付け(実際の呼び出しはプロジェクトの実装に合わせる)
    // m_btn.SetMenu(m_menuButtonBase.GetSubMenu(0)->GetSafeHmenu());

    return TRUE;
}

この方法のメリットは、「関数を抜けた瞬間にDestroyMenuされる」事故を防ぎやすい点です。ダークモードの崩れが消えるケースが非常に多いです。

ただし注意点もあります。同じサブメニュー(HMENU)を複数の親メニューで共有すると、破棄タイミングによって二重破棄・ぶら下がりが起きる可能性があります。長期運用するなら、次のいずれかも検討すると安全です。

  • 追加元側からサブメニューを「取り外して」から挿入する(共有状態を作らない)
  • そもそも“複製したポップアップメニュー”を作って挿入する(後述)

ヒープに確保して保持する(後で必ず解放)

画面生成のタイミングや差し替え頻度の都合で「メンバーにしづらい」場合は、ヒープ確保して保持する方法もあります。ポイントは、所有権が明確になる形で保持し、破棄場所を決めることです。

// 例:メンバーとして保持(推奨はunique_ptr)
std::unique_ptr<CMenu> m_spManageMenu;

void CMyDlg::PrepareDynamicMenu()
{
    m_spManageMenu = std::make_unique<CMenu>();
    m_spManageMenu->LoadMenu(IDR_MENU_POPUP_MANAGE);

    HMENU hSub = m_spManageMenu->GetSubMenu(0)->GetSafeHmenu();
    m_menuButtonBase.InsertMenu(0, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hSub, _T("Manage"));
}

newで確保した場合は、不要になった時点でdeleteが必要です。手動管理が不安なら、上記のようにstd::unique_ptrで保持すると「いつ解放するか」がコード上で明確になり、メモリリークの心配を大きく減らせます。

Detachで“所有権を手放す”

ローカルのまま使いたい場合に使えるのがDetach()です。Detach()はMFCラッパー側が持つハンドルを切り離し、デストラクタで破棄されないようにします。

ただし、ここが重要です。Detachした瞬間から、そのHMENUは自分で破棄(DestroyMenu)する責任が発生します。便利ですが、破棄場所が曖昧なまま使うと今度はリソースリークになります。

さらに一歩安全にするなら、「親メニューからサブメニューを取り外して共有状態を解消してから」InsertMenuするのがおすすめです。概念としては次の流れです。

  1. LoadMenuでメニューをロード
  2. 必要なサブメニューを取得
  3. 元の親メニューからそのサブメニュー項目をRemoveMenuして“切り離す”
  4. サブメニューHMENUをDetachして、ローカルCMenuのデストラクタが壊さないようにする
  5. ボタン側のメニューへInsertMenu(MF_POPUP)で挿入
  6. 最終的に破棄すべき場所でDestroyMenuする(またはCMenuにAttachして管理する)
HMENU CMyDlg::CreatePopupFromResourceAndDetach()
{
    CMenu menuRes;
    menuRes.LoadMenu(IDR_MENU_POPUP_MANAGE);

    // 先頭のポップアップを取得
    CMenu* pPopup = menuRes.GetSubMenu(0);
    if (pPopup == nullptr)
        return nullptr;

    // 共有状態を作らないため、親メニューから該当項目を取り外す
    menuRes.RemoveMenu(0, MF_BYPOSITION);

    // ここでポップアップの所有権を切り離す(以後は呼び出し側が破棄責任を持つ)
    HMENU hPopup = pPopup->Detach();

    // menuRes はローカルで破棄されるが、RemoveMenu済みなので hPopup は壊されにくい
    return hPopup;
}
void CMyDlg::BuildMenuSafely()
{
    HMENU hPopup = CreatePopupFromResourceAndDetach();
    if (hPopup == nullptr)
        return;

    m_menuButtonBase.InsertMenu(0, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hPopup, _T("Manage"));

    // hPopup はどこかのタイミングで DestroyMenu が必要
    // 例:ダイアログ破棄時にまとめて破棄する、CMenuにAttachしてメンバーで管理する等
}

「Detachは最終手段」というより、所有権と破棄責任を設計できるなら強力な選択肢です。運用ルールが曖昧なプロジェクトでは、まずは“メンバーで保持”を選ぶ方が事故は少なくなります。

どの方法を選ぶべきか:比較表

方法難易度メリット注意点おすすめ場面
メンバーCMenuで保持低寿命問題を直感的に解消。コードが読みやすいサブメニュー共有を作ると破棄順で事故が起き得るダイアログ/ビューに常設するメニュー
ヒープ確保して保持中生成タイミングを柔軟にできる。unique_ptrでリーク回避保持期間と破棄地点を設計する必要があるメニュー差し替えや生成が条件分岐する場合
Detachで所有権移譲高ローカルで完結しやすい。意図的に所有権を移せるDestroyMenu責任が発生。RemoveMenuなど手順を誤ると二重破棄/リーク一時的に生成して別所有者に渡したい場合

“ダークモードが崩れた”ときのチェックリスト

寿命が原因かどうかを短時間で判断するために、以下を順に確認すると効率的です。

  • InsertMenuで渡しているHMENUは、表示時点で有効か(::IsMenu(h)で確認)
  • サブメニュー元のCMenuはローカル変数になっていないか
  • 同じHMENUを複数の親メニューに共有していないか(破棄順の事故)
  • メニューを動的に組み替えた後、不要になったHMENUを破棄し忘れていないか(リーク)
  • メニュー挿入でIDや位置指定を誤っていないか(MF_BYPOSITION/MF_BYCOMMANDの混同)

特に最初の::IsMenuは即効性があります。表示直前に有効でなければ、描画以前にハンドルの寿命問題が濃厚です。

実用的なデバッグ例:寿命が切れているかをログで見抜く

「本当にDestroyMenuされているのか?」を確かめたい場合は、メニュー構築直後と表示直前でハンドルが生きているかを確認します。

static void TraceMenuAlive(LPCTSTR label, HMENU hMenu)
{
    BOOL alive = ::IsMenu(hMenu);
    TRACE(_T("%s : hMenu=0x%p IsMenu=%d\\n"), label, hMenu, alive);
}

void CMyDlg::BuildMenu()
{
    CMenu menuManageSpeakers;
    menuManageSpeakers.LoadMenu(IDR_MENU_POPUP_MANAGE);

    HMENU hSub = menuManageSpeakers.GetSubMenu(0)->GetSafeHmenu();
    TraceMenuAlive(_T("after GetSubMenu"), hSub);

    m_menuButtonBase.InsertMenu(0, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hSub, _T("Manage"));

    // この時点では生きていても…
    TraceMenuAlive(_T("before leaving scope"), hSub);
}

void CMyDlg::OnButtonClicked()
{
    // 表示直前に確認
    HMENU h = /* InsertMenuで追加したhSubを保持しているならそれ */;
    TraceMenuAlive(_T("just before TrackPopupMenu"), h);

    // ここでIsMenu=0なら寿命が切れている
}

UIの色崩れは“結果”でしかないので、ハンドル整合性を先に検証すると、ダークテーマ固有の挙動に惑わされにくくなります。

さらに堅牢にする:ポップアップメニューを“複製”して共有を避ける

プロジェクトの規模が大きい、または「同じメニューリソースを複数箇所で使い回す」設計の場合、共有ハンドルが事故要因になりがちです。その場合は、元リソースのポップアップをそのまま渡すのではなく、新しくCreatePopupMenuしたものへ項目をコピーする方が堅牢です。

コピー処理は少し手間ですが、得られるメリットは大きいです。

  • 親メニュー間でハンドル共有しないため、破棄順に左右されにくい
  • 動的に表示/非表示を切り替える項目を作りやすい
  • 将来的にメニュー構造が変わっても、所有権の境界が保たれる

概念例(実装の詳細はプロジェクト方針に合わせてください)。

HMENU ClonePopupMenu(HMENU hSrc)
{
    if (!::IsMenu(hSrc))
        return nullptr;


HMENU hDst = ::CreatePopupMenu();
int count = ::GetMenuItemCount(hSrc);

for (int i = 0; i < count; ++i)
{
    MENUITEMINFO mii{};
    mii.cbSize = sizeof(mii);
    mii.fMask = MIIM_FTYPE | MIIM_STATE | MIIM_ID | MIIM_SUBMENU | MIIM_STRING;

    // 文字列長を取得
    mii.dwTypeData = nullptr;
    mii.cch = 0;
    ::GetMenuItemInfo(hSrc, i, TRUE, &mii);

    CString text;
    if (mii.cch > 0)
    {
        ::GetMenuString(hSrc, i, text.GetBufferSetLength(mii.cch), mii.cch + 1, MF_BYPOSITION);
        text.ReleaseBuffer();
        mii.dwTypeData = text.GetBuffer();
    }

    // サブメニューがある場合は再帰的に複製
    HMENU hSub = nullptr;
    if (mii.hSubMenu != nullptr)
    {
        hSub = ClonePopupMenu(mii.hSubMenu);
        mii.hSubMenu = hSub;
    }

    ::InsertMenuItem(hDst, i, TRUE, &mii);

    // text のバッファはCStringが管理
}

return hDst;


}

この方法なら、複製したHMENUをボタン側が単独で所有できるため、「どこでDestroyMenuするか」も明確になります。例えばダイアログ終了時にまとめて::DestroyMenuすれば良い、という設計にしやすいです。

よくある質問

newした場合、どこかでdeleteが必要?

必要です。newで確保したCMenuは自動では解放されません。ダイアログやビューのメンバーとして保持し、終了時にdeleteするか、std::unique_ptrで所有権を明確にしてスコープ終了で自動解放させるのが安全です。

Detachしたら、DestroyMenuはどこで呼ぶ?

Detachで切り離したHMENUは、以後“誰も面倒を見ない”状態になり得ます。おすすめは次のどちらかです。

  • CMenuメンバーにAttachして管理する(RAIIに戻す)
  • HMENUを保持するコンテナを用意し、ダイアログ破棄時にDestroyMenuする(一括破棄)

ダークモード用のAPI呼び出し(再描画)を入れないと直らない?

今回の症状に限っては、描画更新よりも先にメニューの生存と所有権を疑う方が当たりやすいです。レベル3がダークになるのにレベル2だけライト、という挙動は「テーマが無効」では説明しづらく、ハンドル破綻のサインであることが多いです。

まとめ:DarkMode_Explorerでのメニュー色崩れは“ハンドル設計”で止められる

CMFCMenuButtonのメニューがダークモードに揃わないとき、テーマやビジュアルマネージャの設定を疑いたくなります。しかし、動的に追加したサブメニューだけ崩れる場合は、CMenuの寿命やHMENUの所有権が原因になっていることが非常に多いです。

まずは「ローカルCMenuから取り出したHMENUをInsertMenuしていないか」「表示直前にIsMenuで生存確認できるか」を確認し、必要ならメンバー保持・ヒープ保持・Detachのいずれかで寿命を保証してください。これだけでダークモードの不整合が解消し、クラッシュや描画の不安定さまでまとめて改善するケースが少なくありません。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次