テクニカル ノート 21: コマンドとメッセージのルーティング
Note
次のテクニカル ノートは、最初にオンライン ドキュメントの一部とされてから更新されていません。 結果として、一部のプロシージャおよびトピックが最新でないか、不正になります。 最新の情報について、オンライン ドキュメントのキーワードで関係のあるトピックを検索することをお勧めします。
このノートでは、コマンド ルーティングとディスパッチのアーキテクチャのほか、一般的なウィンドウ メッセージのルーティングにおける高度なトピックについても説明します。
ここで説明するアーキテクチャの全般的な詳細 (特に Windows メッセージ、コントロール通知、コマンドの違い) については、Visual C++ を参照してください。 このノートでは、印刷されたドキュメントに記載されている問題についてよく理解していることを前提とし、非常に高度なトピックのみを取り上げます。
コマンド ルーティングとディスパッチに関する MFC 1.0 機能が MFC 2.0 アーキテクチャに進化
Windows には、メニュー コマンド、アクセラレータ キー、ダイアログコントロールの通知を提供するためにオーバーロードされる WM_COMMAND メッセージがあります。
MFC 1.0 では、CWnd
派生クラスのコマンド ハンドラー (たとえば、"OnFileNew") を特定の WM_COMMAND に応答して呼び出せるようにすることで、それを少しだけ強化しました。 これは、メッセージ マップと呼ばれるデータ構造と合わせることで、非常に空間効率のよいコマンド メカニズムになります。
また、MFC 1.0 には、コントロール通知とコマンド メッセージを区別するための追加機能も用意されています。 コマンドは 16 ビット ID (コマンド ID と呼ばれる場合もあります) で表されます。 コマンドは、通常、CFrameWnd
(つまり、メニュー選択または変換されたアクセラレータ) から始まり、他のさまざまなウィンドウにルーティングされます。
MFC 1.0 では、マルチ ドキュメント インターフェイス (MDI) の実装に対して、限られた意味でコマンド ルーティングを使用していました (MDI フレーム ウィンドウは、コマンドをそのアクティブな MDI 子ウィンドウにデリゲートします)。
MFC 2.0 では、この機能が一般化され、コマンドを (ウィンドウ オブジェクトだけでなく) より幅広いオブジェクトで処理できるよう拡張されました。 これは、メッセージをルーティングするためのより明確で拡張性の高いアーキテクチャを提供し、コマンドの処理だけでなく UI オブジェクト (メニュー項目やツール バーのボタンなど) の更新にもコマンド ターゲット ルーティングを再利用して現在コマンドを使用できるかどうかを反映します。
コマンド ID
コマンド ルーティングとバインドのプロセスの説明については、Visual C++ を参照してください。 テクニカル ノート 20 には、ID の名前付けに関する情報が含まれています。
コマンド ID には汎用プレフィックス "ID_" を使用します。 コマンド ID は 0x8000 以上です。 コマンド ID と同じ ID の STRINGTABLE リソースがある場合、メッセージ行またはステータス バーにはコマンドの説明文字列が表示されます。
アプリケーションのリソースでは、コマンド ID がいくつかの場所に表示されます。
メッセージ行プロンプトと同じ ID が設定されている 1 つの STRINGTABLE リソース内。
同じコマンドを呼び出す複数のメニュー項目に関連付けられている MENU リソース内 (多数ある可能性があります)
(上級) GOSUB コマンドのダイアログ ボタン内。
アプリケーションのソース コードでは、コマンド ID がいくつかの場所に表示されます。
アプリケーション固有のコマンド ID を定義する RESOURCE.H (または他のメイン シンボル ヘッダー ファイル) 内。
場合によっては、ツール バーの作成に使用された ID 配列内。
ON_COMMAND マクロ内。
場合によっては、ON_UPDATE_COMMAND_UI マクロ内。
現在、MFC で 0x8000 以上のコマンド ID を必要とする実装は、GOSUB ダイアログまたはコマンドの実装のみです。
GOSUB コマンド (ダイアログでコマンド アーキテクチャを使用)
コマンドをルーティングして有効にする際のコマンド アーキテクチャは、フレーム ウィンドウ、メニュー項目、ツール バーのボタン、ダイアログ バーのボタン、その他のコントロール バーのほか、必要に応じて更新してコマンドやコントロール ID をメインのコマンド ターゲット (通常はメイン フレーム ウィンドウ) にルーティングするよう設計されたユーザーインターフェイス要素で問題なく動作します。 そのメインのコマンド ターゲットは、必要に応じて、コマンドまたはコントロールの通知を他のコマンド ターゲット オブジェクトにルーティングする場合があります。
ダイアログ (モーダルまたはモードレス) では、ダイアログ コントロールのコントロール ID を適切なコマンド ID に割り当てると、コマンド アーキテクチャの一部の機能が役立ちます。 ダイアログのサポートは自動的には行われないため、追加のコードの記述が必要になる場合があります。
これらのすべての機能が正常に機能するには、コマンド ID を 0x8000 以上にする必要があります。 多くのダイアログは同じフレームにルーティングされるため、共有コマンドは 0x8000 以上にする必要があるのに対し、特定のダイアログにある非共有の IDC は 0x7FFF 以下にする必要があります。
通常のモーダル ダイアログに通常のボタンを配置する際、そのボタンの IDC を適切なコマンド ID に設定できます。 ユーザーがこのボタンを選択すると、ダイアログの所有者 (通常はメイン フレーム ウィンドウ) は、その他のコマンドと同じようにそのコマンドを取得します。 これは GOSUB コマンドと呼ばれます。通常、別のダイアログ (最初のダイアログの GOSUB) を表示するために使用されるためです。
また、ダイアログで CWnd::UpdateDialogControls
関数を呼び出し、メイン フレーム ウィンドウのアドレスを渡すこともできます。 この関数は、フレーム内にコマンド ハンドラーがあるかどうかに基づいて、ダイアログ コントロールを有効または無効にします。 この関数は、アプリケーションのアイドル ループでコントロール バーに対して自動的に呼び出されますが、この機能を使用する通常のダイアログに対しては直接呼び出す必要があります。
ON_UPDATE_COMMAND_UI が呼び出される場合
プログラムのすべてのメニュー項目の有効またはオンの状態を維持し続けることは、負荷の大きい問題になるおそれがあります。 一般的な方法は、ユーザーがポップアップを選択した場合にのみメニュー項目を有効またはオンにすることです。 MFC 2.0 で実装した CFrameWnd
は、WM_INITMENUPOPUP メッセージを処理し、コマンド ルーティング アーキテクチャを使用して ON_UPDATE_COMMAND_UI ハンドラーでメニューの状態を確認します。
また、CFrameWnd
は、WM_ENTERIDLE メッセージを処理して、ステータス バー (メッセージ行とも呼ばれます) で選択されている現在のメニュー項目を記述します。
Visual C++ で編集されたアプリケーションのメニュー構造は、WM_INITMENUPOPUP 時に使用可能なコマンドを表すために使用されます。 ON_UPDATE_COMMAND_UI ハンドラーは、メニューの状態またはテキストを変更することも、高度な用途 (File MRU リストや OLE Verb のポップアップ メニューなど) では、メニューが描画される前にメニュー構造を変更することもできます。
同じような ON_UPDATE_COMMAND_UI 処理は、アプリケーションがアイドル ループに入ると、ツール バー (およびその他のコントロール バー) に対して実行されます。 コントロール バーの詳細については、"クラス ライブラリ リファレンス" およびテクニカル ノート 31 を参照してください。
入れ子になったポップアップ メニュー
入れ子になったメニュー構造を使用している場合は、ポップアップ メニューの最初のメニュー項目の ON_UPDATE_COMMAND_UI ハンドラーが 2 つの異なるケースで呼び出されることに気付きます。
1 つ目は、ポップアップ メニュー自体に対して呼び出されます。 ポップアップ メニューには ID がなく、ポップアップ メニューの最初のメニュー項目の ID を使用してポップアップ メニュー全体を参照するため、これは必要です。 このケースでは、CCmdUI
オブジェクトの m_pSubMenu メンバー変数は NULL 以外の値になり、ポップアップ メニューを指します。
2 つ目は、ポップアップ メニューのメニュー項目が描画される直前に呼び出されます。 このケースでは、ID は最初のメニュー項目だけを参照するため、CCmdUI
オブジェクトの m_pSubMenu メンバー変数は NULL になります。
これにより、ポップアップ メニューをそのメニュー項目と区別できるようになりますが、メニューに対応したコードを記述することが必要になります。 たとえば、次の構造を使用した入れ子になったメニューがあるとします。
File>
New>
Sheet (ID_NEW_SHEET)
Chart (ID_NEW_CHART)
ID_NEW_SHEET コマンドと ID_NEW_CHART コマンドは、個別に有効または無効にすることができます。 2 つのいずれかを有効にする場合は、New ポップアップ メニューを有効にする必要があります。
ID_NEW_SHEET のコマンド ハンドラー (ポップアップの最初のコマンド) は次のようになります。
void CMyApp::OnUpdateNewSheet(CCmdUI* pCmdUI)
{
if (pCmdUI->m_pSubMenu != NULL)
{
// enable entire pop-up for "New" sheet and chart
BOOL bEnable = m_bCanCreateSheet || m_bCanCreateChart;
// CCmdUI::Enable is a no-op for this case, so we
// must do what it would have done.
pCmdUI->m_pMenu->EnableMenuItem(pCmdUI->m_nIndex,
MF_BYPOSITION |
(bEnable MF_ENABLED : (MF_DISABLED | MF_GRAYED)));
return;
}
// otherwise just the New Sheet command
pCmdUI->Enable(m_bCanCreateSheet);
}
ID_NEW_CHART のコマンド ハンドラーは、通常の更新コマンド ハンドラーであり、次のようになります。
void CMyApp::OnUpdateNewChart(CCmdUI* pCmdUI)
{
pCmdUI->Enable(m_bCanCreateChart);
}
ON_COMMAND と ON_BN_CLICKED
ON_COMMAND と ON_BN_CLICKED のメッセージ マップ マクロは同じです。 MFC のコマンドおよびコントロールの通知ルーティング メカニズムでは、ルーティング先を決定するためにコマンド ID のみを使用します。 コントロール通知コードがゼロ (BN_CLICKED) のコントロール通知は、コマンドと解釈されます。
Note
実際、すべてのコントロール通知メッセージは、コマンド ハンドラー チェーンを経由します。 たとえば、ドキュメント クラスで EN_CHANGE のコントロール通知ハンドラーを記述することは技術的に可能です。 通常、これはお勧めしません。この機能の実用例はほとんどなく、この機能は ClassWizard でサポートされていないためです。また、この機能を使用すると、脆弱なコードになる可能性もあります。
ボタン コントロールの自動無効化を無効にする
ボタン コントロールをダイアログ バー、または独自に CWnd::UpdateDialogControls を呼び出す場所を使用しているダイアログに配置すると、ON_COMMAND または ON_UPDATE_COMMAND_UI ハンドラーがないボタンはフレームワークによって自動的に無効になることがわかります。 場合によっては、ハンドラーはなくてもかまいませんが、ボタンを有効なままにしておく必要はあります。 これを実現する最も簡単な方法は、ダミーのコマンド ハンドラーを追加し (ClassWizard で簡単に実行できます)、その中では何も実行しないことです。
Windows メッセージのルーティング
次に、MFC クラスに関するより高度なトピックと、Windows メッセージのルーティングやその他のトピックが MFC クラスに与える影響について説明します。 ここに記載されている情報は、簡単に説明したものにすぎません。 パブリック API の詳細については、"クラス ライブラリ リファレンス" を参照してください。 実装の詳細については、MFC ライブラリのソース コードを参照してください。
すべての CWnd 派生クラスにとって非常に重要なトピックであるウィンドウのクリーンアップの詳細については、テクニカル ノート 17 を参照してください。
CWnd の問題
実装メンバー関数 CWnd:: OnChildNotify は、子ウィンドウ (コントロールとも呼ばれます) がその親 (または "所有者") に送られるメッセージ、コマンド、およびコントロール通知をフックするか、それに関する通知を受けるための強力で拡張可能なアーキテクチャを提供します。 子ウィンドウ (またはコントロール) が C++ の CWnd オブジェクトそのものである場合、仮想関数 OnChildNotify は、最初に、元のメッセージのパラメーター (つまり、MSG 構造体) を使用して呼び出されます。 子ウィンドウは、メッセージをそのまま残すか、処理するか、親のメッセージを変更する (まれ) ことができます。
既定の CWnd 実装は、以下のメッセージを処理し、OnChildNotify フックを使用して子ウィンドウ (コントロール) が最初にメッセージにアクセスできるようにします。
WM_MEASUREITEM と WM_DRAWITEM (自己描画の場合)
WM_COMPAREITEM と WM_DELETEITEM (自己描画の場合)
WM_HSCROLL と WM_VSCROLL
WM_CTLCOLOR
WM_PARENTNOTIFY
OnChildNotify フックは、オーナー描画メッセージを自己描画メッセージに変更するために使用されることがわかります。
OnChildNotify フックに加え、スクロール メッセージにはさらなるルーティング動作があります。 WM_HSCROLL および WM_VSCROLL メッセージのスクロール バーとソースの詳細については、以下を参照してください。
CFrameWnd の問題
CFrameWnd クラスでは、コマンド ルーティングとユーザー インターフェイスの更新の実装の大部分を提供します。 これは主にアプリケーションのメイン フレーム ウィンドウ (CWinApp::m_pMainWnd) に使用されますが、すべてのフレーム ウィンドウに適用されます。
メイン フレーム ウィンドウは、メニュー バーのあるウィンドウで、ステータス バーまたはメッセージ行の親です。 コマンド ルーティングと WM_INITMENUPOPUP については、上記の説明を参照してください。
CFrameWnd クラスでは、アクティブなビューの管理を提供します。 次のメッセージは、アクティブなビューを通じてルーティングされます。
すべてのコマンド メッセージ (アクティブなビューは最初にこれらにアクセスします)。
兄弟スクロール バーからの WM_HSCROLL および WM_VSCROLL メッセージ (下記を参照)。
WM_ACTIVATE (および MDI の場合は WM_MDIACTIVATE) は仮想関数 CView::OnActivateView の呼び出しに変換されます。
CMDIFrameWnd または CMDIChildWnd の問題
どちらの MDI フレーム ウィンドウ クラスも CFrameWnd から派生しているため、CFrameWnd で提供される同じようなコマンド ルーティングとユーザー インターフェイスの更新に対して両方とも有効になります。 一般的な MDI アプリケーションでは、メイン フレーム ウィンドウ (つまり CMDIFrameWnd オブジェクト) のみがメニュー バーとステータス バーを保持するため、コマンド ルーティングの実装の主なソースとなります。
一般的なルーティング方式では、アクティブな MDI 子ウィンドウが最初にコマンドにアクセスします。 既定の PreTranslateMessage 関数は、MDI 子ウィンドウ (最初) と MDI フレーム (2 番目) 両方のアクセラレータ テーブルに加え、通常は TranslateMDISysAccel によって処理される標準の MDI システムコマンド アクセラレータ (最後) を処理します。
スクロール バーの問題
スクロールメッセージ (WM_HSCROLL/OnHScroll や WM_VSCROLL/OnVScroll) を処理する際は、スクロール バーのメッセージの発生元に依存しないようにハンドラーのコードを記述するようにしてください。 これは一般的な Windows の問題ばかりではありません。スクロール メッセージは、実際のスクロール バー コントロール、またはスクロール バー コントロールではない WS_HSCROLL/WS_VSCROLL スクロール バーから取得されている可能性があるためです。
MFC では、スクロール バー コントロールをスクロールするウィンドウの子または兄弟として使用できるように拡張されています (実際には、スクロール バーとスクロールするウィンドウの間の親子関係は何でもかまいません)。 これは、スプリッター ウィンドウを使用した共有スクロール バーの場合に特に重要です。 CSplitterWnd の実装の詳細 (共有スクロール バーの問題の詳細を含む) については、テクニカルノート 29 を参照してください。
ちなみに、作成時に指定されたスクロール バー スタイルがトラップされ、Windows に渡されていない CWnd 派生クラスが 2 つあります。 作成ルーチンに渡されるときに、WS_HSCROLL と WS_VSCROLL は個別に設定できますが、作成後に変更することはできません。 当然ながら、作成したウィンドウの WS_SCROLL スタイルを表すビットを直接テストまたは設定しないでください。
CMDIFrameWnd では、Create または LoadFrame に渡すスクロール バー スタイルを使用して、MDICLIENT を作成します。 スクロール可能な MDICLIENT 領域 (Windows プログラム マネージャーなど) を使用する場合は、CMDIFrameWnd の作成に使用するスタイルの両方のスクロール バー スタイル (WS_HSCROLL | WS_VSCROLL
) を設定してください。
CSplitterWnd では、スクロール バー スタイルがスプリッター領域の特殊な共有スクロール バーに適用されます。 静的なスプリッター ウィンドウでは、通常、いずれのスクロール バー スタイルも設定しません。 動的なスプリッター ウィンドウでは、通常、分割する方向に対してスクロールバー スタイルを設定します。つまり、行を分割する場合は WS_HSCROLL、列を分割する場合は WS_VSCROLL です。