對話框是 Windows 應用程式最核心的互動單元。MFC 透過 CDialog 封裝了 Win32 對話框 API,並以獨特的子類化窗口程序架構、模態循環實現、命令路由與資料交換 (DDX/DDV) 機制,構建了功能完整、易於擴充的對話框框架。本文將從 Win32 基礎、MFC 實現層層深入。
WS_POPUP | WS_SYSMENU | WS_CAPTION | DS_MODALFRAMEDialogBoxIndirectParam 系列,內部啟動獨立訊息循環。WM_ENTERIDLE 給擁有者執行背景工作。EndDialog(nResult) 返回結果值。WS_POPUP | WS_CAPTION | WS_BORDER | WS_SYSMENUCreateDialogIndirectParam,不啟動獨立循環,共用主訊息循環。IsDialogMessage 處理 Tab/導航鍵。DestroyWindow,無返回值。Win32 對話框使用系統提供的窗口程序(DefDlgProc),呼叫使用者提供的對話框程序(DLGPROC)。MFC 採取子類化策略:
CBT Hook (AfxHookWindowCreate) 攔截 WM_NCCREATE/WM_CREATE。AfxWndProc(或 AfxBaseWndProc),原程序保存於 CWnd::m_pfnSuper。AfxDlgProc,僅處理 WM_INITDIALOG。BOOL CALLBACK AfxDlgProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
if (msg == WM_INITDIALOG) {
CDialog* pDlg = DYNAMIC_DOWNCAST(CDialog, CWnd::FromHandlePermanent(hWnd));
if (pDlg) return pDlg->OnInitDialog(); // 呼叫虛擬函式
return TRUE;
}
return 0; // 其他訊息交給 AfxWndProc → CWnd::WindowProc → 訊息映射
}
// CDialog::HandleInitDialog
LRESULT CDialog::HandleInitDialog(WPARAM, LPARAM)
{
Default(); // → CWnd::Default → CWnd::DefWindowProc
// → CallWindowProc(m_pfnSuper, ...) → 系統 DefDlgProc
// → 系統呼叫 AfxDlgProc(WM_INITDIALOG) → OnInitDialog()
return FALSE;
}
此「繞行」設計確保:
Attach 在 Hook 階段完成)。OnInitDialog 在正確時機被呼叫。int CDialog::DoModal()
{
// 1. 載入/鎖定模板資源
LPCDLGTEMPLATE lpTemplate = ...; // 從資源鎖定 DLGTEMPLATE
// 2. PreModal:停用父視窗、建立 Hook
HWND hWndParent = PreModal(); // 內部 AfxHookWindowCreate(this)
// 3. CreateDlgIndirect → 實際呼叫 CreateDialogIndirect
// → 觸發 Hook → 子類化 → AfxWndProc → WM_INITDIALOG → OnInitDialog
if (CreateDlgIndirect(lpTemplate, CWnd::FromHandle(hWndParent), hInst)) {
if (m_nFlags & WF_CONTINUEMODAL) {
DWORD dwFlags = MLF_SHOWONIDLE;
if (GetStyle() & DS_NOIDLEMSG) dwFlags |= MLF_NOIDLEMSG;
VERIFY(RunModalLoop(dwFlags) == m_nModalResult); // 進入模態循環
}
// 隱藏視窗、啟用父視窗、銷毀視窗
SetWindowPos(NULL, 0,0,0,0, SWP_HIDEWINDOW|SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE|SWP_NOZORDER);
}
// 4. 收尾
if (bEnableParent) ::EnableWindow(hWndParent, TRUE);
DestroyWindow(); PostModal(); // 解除 Hook、清理狀態
return m_nModalResult;
}
MFC 刻意使用非模態建立 API,再以 RunModalLoop 手動實現模態語意,原因:
WM_KICKIDLE、顯示時機)。CreateDialogIndirect → Hook → 子類化)。BOOL CDialog::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pInfo)
{
// 1. 自己/基類處理
if (CWnd::OnCmdMsg(nID, nCode, pExtra, pInfo)) return TRUE;
// 2. 控制項通知/更新命令 UI/系統命令 → 停止路由
if ((nCode != CN_COMMAND && nCode != CN_UPDATE_COMMAND_UI) || !IS_COMMAND_ID(nID) || nID >= 0xF000)
return FALSE;
// 3. 路由給父視窗
CWnd* pOwner = GetParent();
if (pOwner && pOwner->OnCmdMsg(nID, nCode, pExtra, pInfo)) return TRUE;
// 4. 路由給執行緒 (CWinApp)
CWinThread* pThread = AfxGetThread();
if (pThread && pThread->OnCmdMsg(nID, nCode, pExtra, pInfo)) return TRUE;
return FALSE; // 最終忽略
}
路由順序:對話框 → 父視窗 → 執行緒/應用程式。
特殊處理:控制項通知訊息(BN_CLICKED 等)不再繼續向上路由,避免重複處理。
BOOL CDialog::PreTranslateMessage(MSG* pMsg)
{
// 1. Tooltip 處理
if (CWnd::PreTranslateMessage(pMsg)) return TRUE;
// 2. SHIFT+F1 說明模式跳過
CFrameWnd* pFrame = GetTopLevelFrame();
if (pFrame && pFrame->m_bHelpMode) return FALSE;
// 3. ESC 鍵在多行編輯框中 → 觸發 IDCANCEL
if (pMsg->message == WM_KEYDOWN &&
(pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_CANCEL) &&
(GetWindowLong(pMsg->hwnd, GWL_STYLE) & ES_MULTILINE) &&
_AfxCompareClassName(pMsg->hwnd, _T("Edit"))) {
HWND hCancel = ::GetDlgItem(m_hWnd, IDCANCEL);
if (!hCancel || ::IsWindowEnabled(hCancel)) {
SendMessage(WM_COMMAND, IDCANCEL, 0);
return TRUE;
}
}
// 4. Dialog 訊息處理 (Tab/方向鍵/熱鍵)
return PreTranslateInput(pMsg); // 內部呼叫 ::IsDialogMessage(m_hWnd, pMsg)
}
關鍵點:
IsDialogMessage 會消耗訊息(不再進入 TranslateMessage/DispatchMessage)。IDCANCEL 或已停用。int CWnd::RunModalLoop(DWORD dwFlags)
{
m_nFlags |= (WF_MODALLOOP | WF_CONTINUEMODAL);
MSG* pMsg = &AfxGetThread()->m_msgCur;
for (;;) {
// 階段一:Idle 處理
while (bIdle && !PeekMessage(pMsg, NULL, 0, 0, PM_NOREMOVE)) {
if (bShowIdle) { ShowWindow(SW_SHOWNORMAL); UpdateWindow(); bShowIdle = FALSE; }
if (!(dwFlags & MLF_NOIDLEMSG) && hWndParent && lIdleCount == 0)
SendMessage(hWndParent, WM_ENTERIDLE, MSGF_DIALOGBOX, (LPARAM)m_hWnd);
if ((dwFlags & MLF_NOKICKIDLE) || !SendMessage(WM_KICKIDLE, MSGF_DIALOGBOX, lIdleCount++))
bIdle = FALSE;
}
// 階段二:訊息派發
do {
if (!AfxGetThread()->PumpMessage()) { AfxPostQuitMessage(0); return -1; }
if (bShowIdle && (pMsg->message == 0x118 || pMsg->message == WM_SYSKEYDOWN))
{ ShowWindow(SW_SHOWNORMAL); UpdateWindow(); bShowIdle = FALSE; }
if (!ContinueModal()) goto ExitModal;
if (IsIdleMessage(pMsg)) { bIdle = TRUE; lIdleCount = 0; }
} while (PeekMessage(pMsg, NULL, 0, 0, PM_NOREMOVE));
}
ExitModal:
m_nFlags &= ~(WF_MODALLOOP | WF_CONTINUEMODAL);
return m_nModalResult;
}
與 CWinThread::Run 同源異構:Idle 發送 WM_ENTERIDLE 給父視窗(而非 OnIdle),父視窗被停用故不接收輸入訊息。
MFC 對話框實現的精髓在於:
RunModalLoop 替代系統模態,獲取完全控制權。PreTranslateMessage → 命令路由 → 訊息映射,清晰分工。CDataExchange 方向標誌 + DDX_/DDV_ 函式族,消除樣板程式碼。理解這些機制,不僅能熟練使用 CDialog,更能在需要自訂對話框行為(如特殊鍵盤處理、跨執行緒 UI、動態模板)時,從架構層面做出正確決策。