ホーム ざれごと ワシントン州 ツール NT豆知識 Win32プログラミングノート 私的用語 ジョーク いろいろ ゲーム雑記 Favorites 掲示板 Mail
ダイアログ中のエディットコントロールでは、タブキーはダイアログ内のコントロールのフォーカス移動キーとして用いられます。実は、Ctrl + TAB でタブコードの入力が出来るのですが、あまり知られていない上、 つい うっかりと誤入力してしまいがちです。
ダイアログ中でタブキーを他の用途に使うのはあまり誉められたことではないかもしれませんが、妙な要求をするお客さんというのは常にいるものですから(^_^;。
タブキーの処理は、 エディットコントロールのウィンドウプロシージャで行われていて、APIもウィンドウメッセージも定義されておらず、アプリケーションからはうまく介入することが出来ません。エディットコントロー ルは、タブキーが与えられたとき、親ウィンドウがダイアログかどうかをチェックします。もしダイアログなら、コントロールのフォーカス移動を行ってしまうのです。 おそらく、次のようなコードになっていると推測されます。
if (コントロールキーが押されている)
キー入力処理;
else if (ダイアログ中のコントロール)
フォーカス移動処理;
というわけで、OnKeyDown()でTABキーが来たのを検知し、かつCtrlキーが押されていない場合、押されていることにしてやればいいじゃないか、というのが次のリストです。
Win32には、GetKeyboardState()とSetKeyboardState()というAPIがあり、同期キーボードステートを取得・設定することが出来ます。
CEditの派生クラスCXXXEditを作り、OnKeyDown()をハンドリングします。
//////////////////////////////////////////////////////////////////// // WM_KEYDOWNメッセージハンドラ // ダイアログ上のエディットコントロールで、タブキーでのタブ入力を // 可能にするためのハック void CXXXXEdit::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { // [タブの入力] // タブは、Ctrl+IまたはCtrl+Tabで入力できる。 // しかし、Tabキーはコントロールの移動用にエディットコントロールで食われてしまう。 // Tabキーをタブの入力に使うために、以下のようにキーボードステートを // 書き換えてしまうという荒業を使ってみた。 // NT 4.0Jで動作確認 // キーボードステートを取得するためのバッファ BYTE ksOrg[256]; BYTE ksAlter[256]; BOOL keyStateModified = FALSE; // 変更したか覚えておくフラグ // 入力されたキーがタブで、タブキー入力フラグがオンで、 // ダイアログボックス内のコントロールだったら、 // コントロールキー状態のすげ替えを行う // // NOTE: ダイアログの子ウィンドウのときにのみTABキーでタブが入らないため、 // ダイアログボックス内かどうかもチェックする if (nChar == VK_TAB && m_tabInputMode == TABMODE_SIMULATE && m_inDialogBox) { // 現在処理中メッセージのキーボードステートを取得する if (GetKeyState(VK_CONTROL) >= 0) { // コントロールが押されていなければ、押されていることにする keyStateModified = TRUE; // まず、キーボードの状態を取得 GetKeyboardState(ksOrg); memcpy(ksAlter, ksOrg, sizeof ksAlter); // 最上位ビットは押されているかどうか // 最下位ビットはトグル状態かどうかのフラグ ksAlter[VK_CONTROL] = 0x80 | ksOrg[VK_CONTROL]; ksAlter[VK_LCONTROL] = 0x80 | ksOrg[VK_LCONTROL]; // 現スレッドのメッセージキューのキーボードステートを変更する SetKeyboardState(ksAlter); #ifdef _DEBUG // キーボードステートが変わったかどうか確認。0x8xxxならOK SHORT ks = GetKeyState(VK_CONTROL); TRACE1("ks=%02X\n", (UCHAR)ks); ASSERT(ks < 0); #endif } } CEdit::OnKeyDown(nChar, nRepCnt, nFlags); if (keyStateModified) { // キーボードステートを元に戻しておく SetKeyboardState(ksOrg); } }
CHistoryWndというCDialogの派生クラスから、メインウィンドウのアクセラレータを扱えるようにしてみましょう。
MFCでは、メッセージのトランスレートが行われる前に、CWnd::PreTranslateMessage()が呼び出されます。通常ダイアログではキーボードアクセラレータの処理を行いませんが、派生したダイアログクラスで以下のような処 理を行うことで、メインウィンドウのアクセラレータを有効にすることが出来ます。
BOOL CHistoryWnd::PreTranslateMessage(MSG* pMsg) { ASSERT(GetParentFrame()); if (TranslateAccelerator(GetParentFrame()->GetSafeHwnd(), GetParentFrame()->GetDefaultAccelerator(), pMsg)) return TRUE; return CDialog::PreTranslateMessage(pMsg); }
GetParentFrame()は、モードレスダイアログの所有者たるフレームへのポインタ(CFrameWnd*)を返します。
GetSafeHwnd()はCWndクラスのメンバで、ポインタがNULLの場合でもAVを起こさずNULLを返すというちょっとトリッキーな関数です。
さて、CFrameWndクラスにはGetDefaultAccelerator()という仮想メンバ関数があり、現在アクティブなドキュメントに応じたアクセラレータを返します。
GetDefaultAccelerator()から返ってきたHACCELをWin32 APIのTranslateAceelerator()に渡してやると、アクセラレータの処理が行われる場合にTRUEが返ってきます。
TRUEが返ってきたら、基本クラスを呼び出す必要はないので、そのままリターンします。このとき、PreTranslateMessage()の戻り値としてTRUEを返すと、MFCのフレームワークに対してこれ以上このメッセージに対する処理 は不要であると表明したことになります。
今作っているMFCアプリでモードレスダイアログを使っているのですが ダイアログ内のコンテクストメニューのコマンドアップデートに標準のコマンド ルーティングが使えないかとやってみたところ、OnCmdMsg()のオーバーライド で出来ました。
SDIなので、ビューにモードレスダイアログへのポインタを持たせて、
BOOL CDicntView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { CWnd* focus = GetFocus(); // 親フレームはまだ作成されていないかもしれない if (focus && focus->GetParentFrame()) { // フォーカスのあるのが、履歴ウィンドウであることを確認 if (focus == m_historyWnd || focus->GetParent() == m_historyWnd) { BOOL r = m_historyWnd->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); // 履歴ウィンドウがメッセージを処理したか確認 if (r) { return r; } } } return CFormView::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); }
てな感じです。focus->GetParent()は、実際にはループでCMainFrameにたどり着くまで 回したほうがいいのでしょう。
# ちなみにCMenu::TrackPopupMenu()の最後の引数には、メインフレームを渡します。
CDialog::OnCmdMsg()はいろいろと余計なことをしてくれるので、 それもオーバーライドして
BOOL CHistoryWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { // Note: // CDialogクラスのOnCmdMsg()は、自分のハンドラが見つからないと // オーナーウィンドウのハンドラ、CWinThreadのハンドラなどを呼び出してしまう // ハンドラの回送をここで止めるため、CWnd::OnCmdMsg()を呼ぶにとどめておく // 必要がある return CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); }
としました。これをやらないと、無限のリカージョンが発生してしまいます。
これで、CHistoryWnd::OnUpdateXXXX()でコンテクストメニューのアップデートが 出来ます。メニューの説明テキストも、メインフレームのステータスウィンドウに 表示されます。
コマンドアップデートハンドラは、通常と同じく
void CHistoryWnd::OnClearHistory() { int size = m_list.GetItemCount(); for (int i = 0; i < size; ++i) { NavigationPlus* nav = (NavigationPlus*)m_list.GetItemData(i); ASSERT(nav); delete nav; } m_list.DeleteAllItems(); } void CHistoryWnd::OnUpdateClearHistory(CCmdUI* pCmdUI) { pCmdUI->Enable(m_list.GetItemCount() > 0); }
のように記述します。この例は、ID_CLEAR_HISTORYに対するコマンドハンドラとコマンドアップデートハンドラです。
ちなみに、m_listはCListCtrlクラスのインスタンスで、ダイアログに貼り付けてあるものです。
一体全体どのようにしてCHistoryWnd::OnUpdateXXXXが呼ばれているのか、MFCではどのようにコマンドアップデートを行っているのか、必ずしも知らなくても構わないことですが、 ちょっと追っかけてみることにし ましょう。 MFCの動きがどうも良く分からない、というときの解析の参考にでもしてください。
CHistoryWnd::OnUpdateClearHistory()が呼び出されたときのスタックは、次のようになっています。
CHistoryWnd::OnUpdateClearHistory() DispatchCmdMsg() CCmdTarget::OnCmdMsg() CHistoryWnd::OnCmdMsg() CDicntView::OnCmdMsg() CFrameWnd::OnCmdMsg() CCmdUI::DoUpdate() CFrameWnd::OnInitMenuPopup() CMainFrame::OnInitMenuPopup() CWnd::OnWndMsg() CWnd::WindowProc() AfxCallWndProc() AfxWndProc() AfxWndProcBase() USER32! 77e51d6a() USER32! 77e51024() NTDLL! 77f65e6b() CMenu::TrackPopupMenu() CHistoryWnd::OnContextMenu() CWnd::OnWndMsg() CWnd::WindowProc() AfxCallWndProc() AfxWndProc() AfxWndProcBase() USER32! 77e51d6a() USER32! 77e51024() NTDLL! 77f65e6b() USER32! 77e54555() COMCTL32! 77bd2bb3() USER32! 77e52793() USER32! 77e5849c() CWnd::DefWindowProcA() CWnd::WindowProc() AfxCallWndProc() AfxWndProc() AfxWndProcBase() USER32! 77e51d6a() USER32! 77e51024() NTDLL! 77f65e6b() USER32! 77e54555() COMCTL32! 77bd2bb3() USER32! 77e52793() USER32! 77e5849c()
TrackPopupMenuの前までは、WM_CONTEXTMENUの処理が見えます。コモンコントロールからダイアログへ、WM_SETCONTEXTMENUがSendされている様子が伺えます。
そこから、CWnd::OnWndMsg()でWM_INITPOPUPMENUが処理され、CMainFrame()::OnInitPopupMenu()へ渡ります。その中で、CCmdUIオブジェクトが作成され、そのDoUpdate()が呼び出されていることが分かります。
CCmdUI::DoUpdate()から連鎖的に呼び出されるOnCmdMsg()で、最終的には、どれかのクラスのOnUpdateXXXX が呼び出されます。呼び出される優先順位は、次のようになります。
アクティブビュー → アクティブドキュメント → フレーム → アプリケーション
CCmdUI::DoUpdate()は、フレームウィンドウのOnCmdMsg()を呼び出します。OnCmdMsg()は、 OnCmdMsg()の呼び出しで、this->Enable()やSetCheck()が呼ばれますが、その結果に応じてCCmdUIはメニュー項目のEnable/Disableやチェックマークの付与を行います。
フレームウィンドウのOnCmdMsg()は、アクティブビューを探し出し、そのOnCmdMsg()を呼びます。
CFrameWnd::OnCmdMsg() { // アクティブビューのOnCmdMsg() if (GetActiveView()->OnCmdMsg()) return TRUE; // フレームのOnCmdMsg() if (CWnd::OnCmdMsg()) return TRUE; // アプリケーションクラスのOnCmdMsg() if (AfxGetApp()->OnCmdMsg()) return TRUE; return FALSE; // コマンドは処理されなかった }
通常のビューのOnCmdMsg()では、
CView::OnCmdMsg() { // ビューでハンドルされるかどうか if (CWnd::OnCmdMsg()) return TRUE; // ドキュメントでハンドルされるか? if (GetDocument()->OnCmdMsg()) return TRUE; return FALSE; }
となります(上の例ではCView::OnCmdMsg()をオーバーライドしているので、CHistoryWnd::OnCmdMsg()がビューのハンドラよりも先に呼ばれますが)。
どのクラスのOnCmsgMsg()も、 最終的には CWnd::OnCmdMsg() --- は定義されていないので、さらにその基本クラスの CCmdTarget::OnCmdMsg() へたどり着きます。CCmdTarget::OnCmdMsg()は、メッセージマップを手繰り、 コマンドハンドラまたはコマンドアップデートハンドラを実際に呼び出します。
OnCmdMsg()は、メッセージマップを手繰っていき、エントリが見つかればコマンドメッセージをディスパッチします。
メッセージマップにハンドラが見つからなければ、基本クラスのメッセージマップを次々に探します。最後までエントリが見つからなければ、FALSEを返し、コマンドメッセージが処理されなかったことを示します。
CCmdTarget::OnCmdMsg() { AFX_MSGMAP* pMap = GetCommandMap(); while (pMap != NULL) { AFX_MSGMAP_ENTRY* pEntry = AfxFindMessageEntry(); if (pEntry != NULL) { return DispatchCmdMsg(...); //メッセージのディスパッチ } pMap = pMap->pBaseMap; // 親クラスのメッセージマップ } return FALSE; }
ちなみに、AFX_MSGMAP_ENTRYは次のように定義されています。
struct AFX_MSGMAP_ENTRY { UINT nMessage; // windows message UINT nCode; // control code or WM_NOTIFY code UINT nID; // control ID (or 0 for windows messages) UINT nLastID; // used for entries specifying a range of control id's UINT nSig; // signature type (action) or pointer to message # AFX_PMSG pfn; // routine to call (or special value) };
ホーム ざれごと ワシントン州 ツール NT豆知識 Win32プログラミングノート 私的用語 ジョーク いろいろ ゲーム雑記 Favorites 掲示板 Mail