MFC SDIでGetMenu()がNULLになる原因とModifyMenuが効かない時のメニュー文字列変更方法

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)を想定します。

手順の流れ

  1. 変更したい文字列(または状態)を保持するメンバー変数を用意する
  2. 対象コマンドIDに対して ON_UPDATE_COMMAND_UI を追加する
  3. Updateハンドラで CCmdUI::SetText() を呼び、表示文字列を差し替える
  4. 状態が変わったタイミングで必要に応じて 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の差やフレーム構成に振り回されずに安定したメニュー制御ができます。

この記事を書いた人

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

コメント

コメントする

目次