Visual StudioのMFCでCMFCMenuButtonをダークモード(DarkMode_Explorer)にしているのに、LoadMenu→InsertMenu(MF_POPUP)で動的に追加したサブメニューだけがライト表示になることがあります。原因は描画設定ではなくCMenu/HMENUの寿命と所有権。再現しやすいパターンと安全な修正方法を整理します。
現象:動的に追加した“中間層”だけダークにならない
CMFCMenuButtonは、クリックでドロップダウンメニュー(ポップアップメニュー)を表示できるMFCの便利なコントロールです。MFCのダークテーマ(DarkMode_Explorerなど)を有効にすると、通常はボタンから出るメニューも背景・文字色がダーク寄りに統一されます。
ところが、別のメニューリソースをLoadMenu()で読み込み、そのサブメニュー(HMENU)をInsertMenu(MF_POPUP)で「ボタンのメニューに動的追加」した場合に限って、次のような不整合が出ることがあります。
| 階層 | 内容 | 表示結果 |
|---|---|---|
| レベル1 | CMFCMenuButtonが元々持っているメニュー | ダーク表示(正常) |
| レベル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オブジェクトが所有しているメニューは、オブジェクト破棄時に片付けられます。
| 要素 | 役割 | 落とし穴 |
|---|---|---|
| CMenu | HMENUを保持するMFCラッパー | スコープを抜けるとデストラクタが走り、所有しているHMENUが破棄され得る |
| HMENU | Win32のメニューオブジェクト(ハンドル) | 参照カウントは基本なく、破棄されたら他所で使っていても無効になる |
| 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するのがおすすめです。概念としては次の流れです。
- LoadMenuでメニューをロード
- 必要なサブメニューを取得
- 元の親メニューからそのサブメニュー項目をRemoveMenuして“切り離す”
- サブメニューHMENUをDetachして、ローカルCMenuのデストラクタが壊さないようにする
- ボタン側のメニューへInsertMenu(MF_POPUP)で挿入
- 最終的に破棄すべき場所で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のいずれかで寿命を保証してください。これだけでダークモードの不整合が解消し、クラッシュや描画の不安定さまでまとめて改善するケースが少なくありません。

コメント