VC6からVS2010へ移行したMFCアプリでは、MDIで動いていた「実行時にメニュー文字列を差し替える処理」が、SDIに移植すると突然効かなくなることがあります。GetMenu()がNULL、LoadMenuで取れたメニューをModifyMenuしても表示が変わらない――本記事では、この現象が起きる理由と、SDIでも確実にメニュー表示を更新する定番の実装手順を整理します。
症状:GetMenu() が NULL/ModifyMenu() しても表示が変わらない
SDIのMFCアプリで、次のような症状に遭遇すると原因の切り分けが難しくなります。
AfxGetMainWnd()->GetMenu()がNULLを返すLoadMenu(IDR_MAINFRAME)でメニューリソースは読み込める- 読み込んだメニューに対して
ModifyMenu()(またはModifyMenuA())を呼んでも、画面上のメニュー文字列が変わらない DrawMenuBar()を呼んでも改善しない
結論から言うと、SDIでは「フレームウィンドウが通常のメニュー(HMENU)を保持していない構成」があり、その場合は GetMenu() が NULL になり得ます。さらに、仮に内部のメニュー文字列を書き換えても、MFCのUI更新機構(UPDATE_COMMAND_UI)が別のタイミングで表示を上書きし、見た目に反映されない/元に戻ることがあります。
なぜSDIで起きやすいのか
SDIでも「通常のメニュー」を持たないフレーム構成がある
VS2010以降のMFC(いわゆるFeature Pack/拡張フレーム系)では、メニューが「ウィンドウのメニュー(SetMenuで付くHMENU)」ではなく、ツールバー同様にドッキング可能なメニューバー(例:CMFCMenuBar)として管理されることがあります。
この構成では、見た目としては確かにメニューが表示されていても、フレームウィンドウ自体はHMENUを持たない(あるいは一時的に外している)ため、CWnd::GetMenu() が NULL を返す、という状況が発生します。
LoadMenu() で得られるのは「表示中のメニュー」とは限らない
LoadMenu(IDR_MAINFRAME) は、リソースから新規にメニューを作成して返します。ここで得られるメニューは、あくまで別インスタンスのCMenu/HMENUです。
つまり、SDIのフレームに実際に表示されているメニューが別の場所(メニューバーやフレームの既存HMENU)で管理されている場合、LoadMenu()したメニューをいくらModifyMenu()しても、画面表示には何も起きません。
ModifyMenu() が効かない/戻るのは UPDATE_COMMAND_UI が関係することがある
MFCはアイドル時やメニュー表示のタイミングで、メニュー項目の有効/無効、チェック状態、テキストなどを更新する仕組み(ON_UPDATE_COMMAND_UI)を持っています。プロジェクトのテンプレートや既存コードでUI更新が組み込まれていると、ModifyMenu()で一度書き換えた文字列が、後からUI更新で上書きされることがあります。
「書き換えは成功しているのに、表示だけ変わらない/すぐ戻る」タイプのトラブルは、このパターンが非常に多いです。
| 症状 | ありがちな原因 | まず見るポイント |
|---|---|---|
| GetMenu() が NULL | メニューがドッキング可能メニューバーで管理され、フレームにHMENUが付いていない | MainFrm が CFrameWndEx か/CMFCMenuBar を使っていないか |
| LoadMenu() したメニューをModifyMenuしても表示が変わらない | 表示中のメニューとは別インスタンスを編集している | 実際に画面に出ているHMENUの取得経路 |
| 一瞬変わるが戻る/まったく変わらない | UPDATE_COMMAND_UI が別タイミングでテキストを更新している | ON_UPDATE_COMMAND_UI の有無、既存のUpdateハンドラ |
原因切り分けの近道:いま画面に出ているメニューはどこが持っているか
「なぜNULLなのか」を最短で確かめるには、フレームウィンドウにHMENUが付いているかを確認します。見た目にメニューが出ていても、メニューバーが描画しているだけならフレームのHMENUは空のままです。
チェックポイント
- MainFrmの基底クラスが
CFrameWndかCFrameWndExか - MainFrmに
CMFCMenuBar m_wndMenuBar;のようなメンバーがあるか OnCreateなどでm_wndMenuBar.Create(...)/LoadMenuBar(...)を呼んでいるか
Win32の ::GetMenu(hwnd) でも確認できる
デバッグ時は、MFCの CWnd::GetMenu() だけでなく、Win32 APIの ::GetMenu(hwnd) も併用すると状況が分かりやすくなります。
CWnd* pMain = AfxGetMainWnd();
HWND hWnd = pMain ? pMain->GetSafeHwnd() : NULL;
HMENU hMenuFromWin32 = (hWnd != NULL) ? ::GetMenu(hWnd) : NULL;
TRACE(_T("Win32 GetMenu = 0x%p\n"), hMenuFromWin32);
ここで hMenuFromWin32 が NULL なのにメニューが表示されているなら、フレームのメニューではなく、メニューバー(ツールバー型)が描画している可能性が高いです。この場合、文字列変更の主戦場は「メニューバーが更新する表示」と考え、ON_UPDATE_COMMAND_UI で SetText を行う方がスムーズです。
最短で直すなら:ON_UPDATE_COMMAND_UI で CCmdUI::SetText() を使う
メニュー文字列を「実行時に変えたい」だけなら、SDI/MDIやメニューバーの種類に依存しにくい定番がUPDATE_COMMAND_UIでテキストを設定する方法です。ポイントは次の2つです。
- 表示はMFCの更新タイミングに合わせて行う(結果として“戻る”問題を回避できる)
- メニューだけでなく、同じコマンドIDを使うツールバー/リボン/ステータスなどにも一貫して反映しやすい
実装手順:表示文字列をメンバーに保持し、Updateで毎回反映する
例として、接続状態に応じて「接続(&C)」「切断(&D)」を切り替えるメニュー項目(ID_CMD_CONNECT)を想定します。
手順の流れ
- 変更したい文字列(または状態)を保持するメンバー変数を用意する
- 対象コマンドIDに対して
ON_UPDATE_COMMAND_UIを追加する - Updateハンドラで
CCmdUI::SetText()を呼び、表示文字列を差し替える - 状態が変わったタイミングで必要に応じて
DrawMenuBar()を呼び、即時反映を促す
// MainFrm.h(例)
class CMainFrame : public CFrameWndEx
{
protected: // 動的生成(DECLARE_DYNCREATE)を使う想定
CMainFrame();
// ...
bool m_bConnected; // ここに状態を保持
afx_msg void OnCmdConnect();
afx_msg void OnUpdateCmdConnect(CCmdUI* pCmdUI);
DECLARE_MESSAGE_MAP()
};
// MainFrm.cpp(例)
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWndEx)
ON_COMMAND(ID_CMD_CONNECT, &CMainFrame::OnCmdConnect)
ON_UPDATE_COMMAND_UI(ID_CMD_CONNECT, &CMainFrame::OnUpdateCmdConnect)
END_MESSAGE_MAP()
CMainFrame::CMainFrame()
: m_bConnected(false)
{
}
void CMainFrame::OnCmdConnect()
{
// 実処理はアプリの要件に合わせて
m_bConnected = !m_bConnected;
// メニューが表示中なら即時反映されるように
DrawMenuBar();
}
void CMainFrame::OnUpdateCmdConnect(CCmdUI* pCmdUI)
{
// ここが最重要:表示テキストはUpdateで決める
if (m_bConnected)
pCmdUI->SetText(_T("切断(&D)"));
else
pCmdUI->SetText(_T("接続(&C)"));
// 状態に応じてEnable/Checkも一緒に管理できる
pCmdUI->Enable(TRUE);
}
この方式にすると、メニューがフレームに直付けされているか、メニューバー(ドッキング可能)で管理されているかに関係なく、MFCが「表示すべきタイミング」で文字列を描画し直してくれます。
SetTextで気を付けたい書式(ショートカット、ニーモニック、三点リーダ)
Windowsのメニュー文字列には、実用上よく使う記法があります。Updateで生成する場合も、ここを意識すると違和感が減ります。
| 目的 | 例 | ポイント |
|---|---|---|
| Altショートカット(ニーモニック) | 接続(&C) | &で下線文字を指定。日本語UIでは (&X) 形式が一般的 |
| キーボードショートカット表示 | 検索(&F)\tCtrl+F | \t(タブ)以降は右側に寄せて表示される |
| サブダイアログ等を示す三点リーダ | 設定(&S)... | 実際に追加の入力が必要な操作は ... を付けるのが慣例 |
Updateハンドラは「どこに書くべきか」
UPDATE_COMMAND_UIのルーティングは、コマンドと同様にビュー→フレーム→ドキュメント→アプリの順で探されます。基本は「そのコマンドを処理するクラス」にUpdateも置くと保守が楽です。
- メインメニューの項目で、フレームがコマンドを持つなら CMainFrame に書く
- ビュー固有の状態(選択範囲など)で変えるなら CView 側に書く
- ドキュメントの状態(編集中、保存済みなど)で変えるなら CDocument 側に書く
ModifyMenu() を使う場合:必ず「表示中のHMENU」を操作する
UPDATE_COMMAND_UIで解決できるケースが大半ですが、次のような要件では「メニュー構造そのものを変えたい」ことがあります。
- 項目の追加・削除・並び替えを行いたい
- サブメニューごと差し替えたい
- コンテキストメニューで動的に生成したい
この場合は ModifyMenu() や InsertMenu() 等が必要になります。ただし重要なのは「LoadMenuで作った別物」ではなく、「今画面に出ているメニュー」を掴むことです。
ケースA:標準のメニューを持つフレーム(GetMenuが有効)
SDIでも、従来型のフレーム(CFrameWnd)で標準のメニューが付いている構成なら、次のように直接操作できます。
CWnd* pMain = AfxGetMainWnd();
CMenu* pMenu = pMain ? pMain->GetMenu() : nullptr;
if (pMenu != nullptr)
{
pMenu->ModifyMenu(ID_CMD_CONNECT, MF_BYCOMMAND | MF_STRING, ID_CMD_CONNECT, _T("接続(&C)"));
pMain->DrawMenuBar();
}
ただし、既にUPDATE_COMMAND_UIで同じIDのテキストを触っている場合は、後から上書きされる可能性があります。文字列だけならUpdate方式に寄せる方がトラブルは減ります。
ケースB:ドッキング可能メニューバーで管理されている(GetMenuがNULL)
CFrameWndEx テンプレートなどで CMFCMenuBar を使っている場合、メニューは「メニューバーの内部HMENU」として保持されることがあります。このときの対処の考え方は次の通りです。
- フレームからGetMenuできないなら、メニューバーからHMENUを取得する
- 取得したHMENUに対して
CMenu::FromHandle()でラップし、ModifyMenu()等を実行する - 表示更新は
DrawMenuBar()だけで足りない場合があるため、最終的にはUpdateで表示を担保する
メニューバーのメニュー取得APIは、ベースクラスやMFCの世代によって名前が異なることがあります。MainFrmに m_wndMenuBar のようなメンバーが定義されている場合は、そのクラスのメソッドをIntelliSenseで確認し、「現在セットされているHMENU」を返す関数を使います。
// 例:概念コード(実際の関数名はプロジェクトの基底クラス/メニューバー型に合わせて調整)
CMainFrame* pFrame = static_cast<CMainFrame*>(AfxGetMainWnd());
HMENU hMenu = NULL;
// 1) メニューバーがある場合は、そこからHMENUを取得(関数名は環境依存)
if (pFrame != nullptr /* && pFrame->GetMenuBar() != nullptr */)
{
// hMenu = pFrame->GetMenuBar()->GetHMenu();
// あるいは hMenu = pFrame->m_wndMenuBar.GetHMenu();
}
// 2) HMENUをCMenuにして操作
if (hMenu != NULL)
{
CMenu* pMenu = CMenu::FromHandle(hMenu);
pMenu->ModifyMenu(ID_CMD_CONNECT, MF_BYCOMMAND | MF_STRING, ID_CMD_CONNECT, _T("接続(&C)"));
// 反映(必要に応じて)
pFrame->DrawMenuBar();
}
この方法は「構造を動的に変えたい」場合に有効ですが、文字列だけの差し替えならUpdateでSetTextする方が堅牢です。特にメニューバー側が内部でキャッシュや再構築を行う場合、直接Modifyした結果が次の再構築で消えることがあります。
ケースC:OnInitMenuPopup で“今まさに開くメニュー”を直接更新する
「GetMenu()がNULLで掴めない」「メニューバーのHMENU取得がややこしい」「ただしメニューが開く直前に更新できれば十分」という場合に便利なのが OnInitMenuPopup です。ここでは引数として CMenu* が渡されるため、表示直前のポップアップメニューを確実に触れます。
このハンドラを使う場合は、メッセージマップに次の行を追加して WM_INITMENUPOPUP を受け取れるようにします。
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWndEx)
// ...
ON_WM_INITMENUPOPUP()
END_MESSAGE_MAP()
void CMainFrame::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
{
CFrameWndEx::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu);
if (pPopupMenu == nullptr || bSysMenu)
return;
// 例:このポップアップにID_CMD_CONNECTが含まれているなら、文字列を差し替える
if (m_bConnected)
pPopupMenu->ModifyMenu(ID_CMD_CONNECT, MF_BYCOMMAND | MF_STRING, ID_CMD_CONNECT, _T("切断(&D)"));
else
pPopupMenu->ModifyMenu(ID_CMD_CONNECT, MF_BYCOMMAND | MF_STRING, ID_CMD_CONNECT, _T("接続(&C)"));
}
Update方式ほど“全面的に”は統一されませんが、「メニューを開いた瞬間だけ正しい文字列になればOK」というUIなら、実装コストと効果のバランスが良い方法です。
Unicode/ANSIとA/W関数の落とし穴(VS2010移行で増える)
VC6時代のコードには ModifyMenuA() のようにANSI版を明示しているものが残りがちです。VS2010以降でUnicodeビルドに移行している場合、文字化けや想定外の変換コストが発生することがあります。
- 基本は
ModifyMenu()(TCHARマクロ)を使い、文字列は_T("...")で統一する - Unicode固定で行くなら
ModifyMenuW()とL"..."を使う CStringはプロジェクト設定(Unicode/マルチバイト)に追従するため、移行中の混在に注意する
ただし今回の「表示が変わらない」症状の本質は、文字コードよりも「表示中のメニューを操作していない」「更新機構で上書きされている」ことにあるケースが多い、という点は押さえておきましょう。
ハマりどころチェックリスト:効かない原因を最短で潰す
| ハマりどころ | 典型症状 | 対処 |
|---|---|---|
| LoadMenu()で取得した別メニューを編集している | ModifyMenuは成功するが画面が変わらない | 表示中のHMENUを取得する/Updateで表示を決める |
| MF_BYCOMMAND と MF_BYPOSITION を取り違える | 別の項目が変わる、または何も変わらない | ID指定なら MF_BYCOMMAND、位置指定なら MF_BYPOSITION |
| UPDATE_COMMAND_UI が後から上書き | 一瞬変わるが戻る/表示だけ変わらない | ON_UPDATE_COMMAND_UIでSetTextに一本化 |
| アクセラレータ表示を忘れて違和感が出る | 他の項目だけ右側にショートカットがある | \tCtrl+... 形式で整える(必要な場合) |
| MDI→SDI移植でIDが変わっている | ModifyMenu対象のIDが見つからない | リソースのコマンドIDを再確認(IDR_~、メニューエディタ) |
実務的な設計のコツ:文字列生成ロジックを散らさない
メニュー文字列を動的に変える処理は、要件が増えるほど「どこで変えているのか分からない」状態になりがちです。長期運用を前提にするなら、次のように整理すると保守が楽になります。
- 状態は
m_bConnectedのようなフラグ、または状態列挙で一元管理する - 表示文字列はUpdateハンドラに直書きせず、関数化してまとめる(例:
GetConnectMenuText()) - 同じIDをツールバーにも割り当てる場合、Updateでツールチップやテキストも統一されるようにする
// 例:文字列生成を関数に集約
CString CMainFrame::GetConnectMenuText() const
{
return m_bConnected ? _T("切断(&D)") : _T("接続(&C)");
}
void CMainFrame::OnUpdateCmdConnect(CCmdUI* pCmdUI)
{
pCmdUI->SetText(GetConnectMenuText());
}
こうしておくと、「将来は“再接続”も追加したい」「ログイン中は別表記にしたい」といった変更が入っても、UIの表記ゆれや更新漏れが起きにくくなります。
まとめ:SDIでは“GetMenuで触れるメニュー”とは限らない。表示はUpdateで担保する
SDIのMFCアプリで GetMenu() が NULL になったり、ModifyMenu() が見た目に効かなかったりする背景には、メニューがドッキング可能メニューバーで管理されている構成や、UPDATE_COMMAND_UIによる表示更新の仕組みが関係します。
文字列の差し替えが目的なら、最も確実で再発しにくいのは ON_UPDATE_COMMAND_UI で CCmdUI::SetText() を使う方法です。どうしても構造変更が必要な場合だけ「表示中のHMENU」を正しく掴んでModify系APIを使い、最後はUpdate側で表示を保証する――この方針にすると、MDI/SDIの差やフレーム構成に振り回されずに安定したメニュー制御ができます。

コメント