テクニカル ノート 21: コマンドとメッセージのルーティング

注意

次のテクニカル ノートは、最初にオンライン ドキュメントの一部とされてから更新されていません。 結果として、一部のプロシージャおよびトピックが最新でないか、不正になります。 最新の情報について、オンライン ドキュメントのキーワードで関係のあるトピックを検索することをお勧めします。

このノートではコマンドのルーティングおよびディスパッチ用アーキテクチャと、ウィンドウ メッセージ一般のルーティングに関する詳細について説明します。

このノートで説明するアーキテクチャの概要、特に Windows メッセージ、コントロール通知、コマンドの違いについては、Visual C++ のドキュメントを参照してください。 このノートではドキュメントに記述されている知識を前提として、より高度なトピックについて説明します。

コマンドのルーティングとディスパッチに関する MFC 1.0 から MFC 2.0 への変更点

Windows の WM_COMMAND メッセージはメニュー コマンドやアクセラレータ キー、ダイアログ コントロールの通知を伝えられるように、オーバーロードされています。

MFC 1.0 は、特定の WM_COMMAND に反応してコマンド ハンドラー ("OnFileNew" など) が CWnd 派生クラスで呼び出されるように設計されていました。 この機能はメッセージ マップと呼ばれるデータ構造にまとめられ、メモリ効率の面で優れたコマンド機構となっています。

また、MFC 1.0 ではコントロール通知をコマンド メッセージと分離して扱う付加機能が用意されていました。 コマンドは 16 ビットの ID (コマンド ID) で表されます。 コマンドは 16 ビットの ID (コマンド ID) で表されます。通常、コマンドは CFrameWnd から発行され (メニュー コマンドやアクセラレータ キーの選択などによって生成)、他の各種ウィンドウに送られます。

MFC 1.0 では、MDI (複数文書インターフェイス) のコマンド ルーティングは限定された形になっていました。 MDI フレーム ウィンドウはアクティブな MDI 子ウィンドウにコマンドを送ります。

MFC 2.0 ではこの機能が拡張、汎用化されて、ウィンドウ オブジェクトに限らずより広い範囲のオブジェクトがコマンドを処理できるようになっています。 MFC 2.0 ではメッセージのルーティングを行うためにより形式的で拡張可能なアーキテクチャが用意されているので、コマンド ターゲット ルーティングをコマンド処理に使うだけでなく、ユーザー インターフェイス オブジェクト (メニュー コマンドやツール バー ボタン) の状態 (そのコマンドが使用可能かどうか) の更新も行うことができます。

コマンド ID

コマンドのルーティングとバインディングの詳細については、Visual C++ のドキュメントを参照してください。 ID の名前付け規約については、「テクニカル ノート 20: ID 名および番号に関する規約」を参照してください。

コマンド ID では汎用のプリフィックス "ID_" を使用します。 コマンド ID の値は >= 0x8000 です。 コマンドと同じ ID を持つ STRINGTABLE リソースがあれば、メッセージ行やステータス バーにコマンドを説明する文字列が表示されます。

コマンド ID はアプリケーション中で使用されるリソースでも使用されます。

  • STRINGTABLE リソースでは、コマンドと同じ ID を持つものがメッセージ行のプロンプトとして使用されます。

  • 多くのメニュー リソースはメニュー コマンドに結び付けられて、同じ ID を持つコマンドを起動します。

  • (高度な利用法) ダイアログ ボタンで GOSUB コマンドを実行できます。

コマンド ID はアプリケーションのソース コードでも使用されます。

  • RESOURCE.H (またはその他のメイン シンボル ヘッダー ファイル) に、アプリケーション固有のコマンド ID を定義します。

  • 多くの場合、ツール バーを作成するために ID 配列が使用されます。

  • ON_COMMAND マクロ中で ID が使われます。

  • 多くの場合、ON_UPDATE_COMMAND_UI マクロ中で ID が使われます。

現時点で、コマンド ID が >= 0x8000 でなければならないものは、GOSUB ダイアログ/コマンドだけです。

GOSUB コマンド (ダイアログでコマンド アーキテクチャを使用する方法)

コマンドのルーティングと処理を行うコマンド アーキテクチャはフレーム ウィンドウ、メニュー コマンド、ツール バー ボタン、ダイアログ バー ボタンや、その他のコントロール バー、ユーザー インターフェイスで使用できます。コマンド アーキテクチャは動的な更新を行い、コマンドまたはコントロール ID をメイン コマンド ターゲット (通常はメイン フレーム ウィンドウ) に送ります。 メイン コマンド ターゲットはコマンドやコントロール通知をほかの適切なコマンド ターゲットに転送します。

ダイアログ コントロールのコントロール ID を該当するコマンド ID に割り当てると、ダイアログ (モーダル、モードレスに関係なく) でコマンド アーキテクチャの機能を利用できます。 ダイアログは自動的にはサポートされないので、処理のためのコードを独自に記述する必要があります。

これらの機能を正しく動作させるには、コマンド ID 値を >= 0x8000 にする必要があります。 1 つのフレームが多数のダイアログからのコマンドを受け取ることが多いので、共有できるコマンドは >= 0x8000 に割り当て、特定のダイアログ (共有されない) コマンド IDC は <= 0x7FFF にします。

通常のボタンを通常のモーダル ダイアログに配置して、そのボタンの IDC を該当するコマンド ID に設定できます。 ユーザーがこのボタンを選択すると、ダイアログのオーナー (通常はメイン フレーム ウィンドウ) はそのボタンに対応するコマンドをほかのコマンドと同じように、受け取ります。 このコマンドは通常、別のダイアログを呼び出すので、GOSUB コマンドと呼ばれます。

ダイアログに対して関数 CWnd::UpdateDialogControls を呼び出して、メイン フレーム ウィンドウのアドレスを渡すこともできます。 この関数は、ダイアログ コントロールの状態 (コマンド ハンドラーがフレーム中にあるかどうか) に応じて、コントロールの表示を選択可能または選択不可能に設定します。 この関数はコントロール バーに対してはアプリケーションのアイドル時に自動的に呼び出されますが、通常のダイアログに対して更新を行うときは直接呼び出す必要があります。

ON_UPDATE_COMMAND_UI の呼び出し

プログラムのすべてのメニュー項目の状態を常時管理する処理は、常に効率上の問題となります。 これを解決するために主に使われるのは、ユーザーがポップアップ メニューを選択したときだけメニュー項目の状態をオンにする方法です。 MFC 2.0 では CFrameWndWM_INITMENUPOPUP メッセージを処理しますが、この中でコマンド ルーティング アークテクチャを使用して ON_UPDATE_COMMAND_UI ハンドラーを呼び出し、メニューの状態を決定します。

CFrameWndWM_ENTERIDLE メッセージも処理します。このメッセージにより、現在選択されているメニュー コマンドの説明がステータス バー (メッセージ行) に表示されます。

アプリケーションのメニュー構造は Visual C++ によって編集されます。この構造は WM_INITMENUPOPUP が発生したときに、選択される可能性のあるコマンドを表示するために使用されます。 ON_UPDATE_COMMAND_UI はメニューの状態や表示される文字を変更したり、その他の高度な処理 ([ファイル] メニューの MRU リストや OLE ポップアップ メニューの作成) を行うために使用できます。実際には、メニューが描画される前にメニュー構造が変更されます。

ツール バー (やその他のコントロール バー) に対しても同様の ON_UPDATE_COMMAND_UI 処理がアプリケーションのアイドル ループ時に行われます。 コントロール バーについては、『MFC ライブラリ リファレンス』と「テクニカル ノート 31: コントロール バー」を参照してください。

入れ子になっているポップアップ メニュー

入れ子になっているメニューを使うと、ポップアップ メニューの先頭のメニュー コマンドに対する ON_UPDATE_COMMAND_UI ハンドラーが 2 つの異なる状況の下で呼び出されます。

まず、ポップアップ メニュー自体に対して呼び出されます。 ポップアップ メニュー自身は ID を持たないので、ポップアップ メニューの先頭のメニュー コマンドの ID を使ってポップアップ メニュー全体を表すからです。 この場合、CCmdUI オブジェクトの m_pSubMenu メンバー変数が NULL 以外の値 (ポップアップ メニューを指す値) になります。

次に、ポップアップ メニュー内の項目が描画される直前に呼び出されます。 この場合、この ID は先頭のメニュー コマンドを示しており、CCmdUI オブジェクトの m_pSubMenu メンバー変数の値は NULL です。

上の違いを利用してポップアップ メニューとその中のメニュー コマンドを区別できますが、入れ子になっているメニューを正しく扱うコードを記述するには多少の注意が必要です。 たとえば、次のようなメニューを考えてみます。

File>
    New>
        Sheet (ID_NEW_SHEET)
        Chart (ID_NEW_CHART)

コマンド ID_NEW_SHEET および ID_NEW_CHART はそれぞれ独立して選択可能または不可能の状態を取れます。 どちらかが選択可能状態なときに、ポップアップ メニュー [新規作成] を選択可能にします。

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_COMMANDON_BN_CLICKED に対しては同じメッセージ マップ マクロが使用されます。 MFC のコマンドとコントロール通知用のルーティング機構では、転送先をコマンド ID だけで判定します。 通知コード 0 (BN_CLICKED) のコントロール通知はコマンドとして解釈されます。

注意

実際には、すべてのコントロール通知メッセージがコマンド ハンドラー機構に送られます。 したがって、EN_CHANGE などに対するコントロール通知ハンドラーをドキュメント クラス中に記述することが構造上可能です。 しかし、実際のアプリケーションではこのような方法はめったに使用されません。ClassWizard もこのような方法は使用しません。この方法はコードの保守性の面でも不適切です。

ボタン コントロール選択状態の自動処理の停止

独自に CWnd::UpdateDialogControls を呼び出すダイアログやダイアログ バー上にボタン コントロールを配置すると、ON_COMMANDON_UPDATE_COMMAND_UI に対するハンドラーを持たないボタンはフレームワークによって自動的に選択不可能に設定されます。 しかし場合によっては、ハンドラーを持たないボタンでも選択可能な状態に設定する場合があります。 このような設定を行う最も簡単な方法は、何の処理も行わないダミーのコマンド ハンドラーを追加することです (ClassWizard を使えば簡単に追加できます)。

Window メッセージのルーティング

ここでは MFC クラスの高度な利用法と Windows メッセージ ルーティングとの関係について説明します。 ここでの説明は、ごく簡単なものです。 パブリック API の詳細については、『MFC ライブラリ リファレンス』を参照してください。 具体的な実装の詳細については、MFC ライブラリのソース コードを参照してください。

Windows システム リソースの解放方法については、「テクニカル ノート 17: ウィンドウ オブジェクトの破棄」を参照してください。システム リソースの解放は CWnd 派生クラスでは重要な問題です。

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 フレーム ウィンドウ、標準の MDI システム コマンドのアクセラレータ キー テーブルをこの順で処理できます。MDI システム コマンド アクセラレータは通常は TranslateMDISysAccel によって処理されます。

スクロール バー

スクロール メッセージ (WM_HSCROLL/OnHScroll および WM_VSCROLL/OnVScroll) を処理するときは、スクロール バー メッセージの発生源に依存しないハンドラーを作成してください。 この方針は Windows 一般に言えることですが、スクロール メッセージはスクロール バー コントロールからだけではなく WS_HSCROLL/WS_VSCROLL スクロール バー (これはスクロール バー コントロールではありません) からも送られることがあるためです。

MFC では、スクロール バー コントロールはスクロールされるウィンドウの子コントロールか兄弟コントロールにすることができます (実際に、スクロール バーとスクロールされるウィンドウの親子関係を自由に設定できます)。 これは分割ウィンドウでスクロール バーを共有するときに特に重要です。 CSplitterWnd やスクロール バーの共有の詳細については、「テクニカル ノート 29: 分割ウィンドウ」を参照してください。

補足すると、生成時に指定されるスクロール バーのスタイルがトラップされて Windows に送られないような CWnd 派生クラスは 2 つあります。 WS_HSCROLLWS_VSCROLL は作成ルーチンに渡すときにそれぞれ独立して指定できますが、作成後は変更できません。 もちろん、作成後のウィンドウの WS_?SCROLL スタイル ビットを直接調べたり設定したりすることはできません。

CMDIFrameWnd では CreateLoadFrame に渡すスクロール バー スタイルは MDICLIENT の作成時に使用されます。 Windows プログラム マネージャーのような、スクロール可能な MDICLIENT 領域を作成するには、WS_HSCROLL |WS_VSCROLL の両方のスクロール バー スタイルを設定して CMDIFrameWnd を作成します。

CSplitterWnd では、スクロール バー スタイルが分割領域用の専用共有スクロール バーに適用されます。 静的分割ウィンドウでは、通常どちらのスクロール バー スタイルも設定しません。 動的分割ウィンドウでは、通常、分割方向のスクロール バー スタイルを設定します。つまり、横方向に分割するときは WS_HSCROLL を、縦方向に分割するときは WS_VSCROLL を設定します。

参照

その他の技術情報

番号順テクニカル ノート

カテゴリ別テクニカル ノート