首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

MFC 對話框機制深度解析(一):模式對話框、子類化窗口程序與模態循環實現

     編輯:關於VC++

前言

對話框是 Windows 應用程式最核心的互動單元。MFC 透過 CDialog 封裝了 Win32 對話框 API,並以獨特的子類化窗口程序架構、模態循環實現、命令路由與資料交換 (DDX/DDV) 機制,構建了功能完整、易於擴充的對話框框架。本文將從 Win32 基礎、MFC 實現層層深入。

一、Win32 對話框基礎:模態與非模態

模態對話框

  • 風格:WS_POPUP | WS_SYSMENU | WS_CAPTION | DS_MODALFRAME
  • 建立:DialogBoxIndirectParam 系列,內部啟動獨立訊息循環。
  • 特徵:停用擁有者視窗、捕獲訊息焦點、發送 WM_ENTERIDLE 給擁有者執行背景工作。
  • 結束:EndDialog(nResult) 返回結果值。

非模態對話框

  • 風格:WS_POPUP | WS_CAPTION | WS_BORDER | WS_SYSMENU
  • 建立:CreateDialogIndirectParam,不啟動獨立循環,共用主訊息循環。
  • 特徵:擁有者視窗保持啟用,需應用程式呼叫 IsDialogMessage 處理 Tab/導航鍵。
  • 結束:DestroyWindow,無返回值。

二、MFC 核心架構:子類化取代系統對話框程序

關鍵觀念

Win32 對話框使用系統提供的窗口程序(DefDlgProc),呼叫使用者提供的對話框程序(DLGPROC)。MFC 採取子類化策略:

  1. 建立對話框視窗時,透過 CBT Hook (AfxHookWindowCreate) 攔截 WM_NCCREATE/WM_CREATE。
  2. 將系統窗口程序替換為 AfxWndProc(或 AfxBaseWndProc),原程序保存於 CWnd::m_pfnSuper。
  3. 提供 MFC 專用對話框程序 AfxDlgProc,僅處理 WM_INITDIALOG。

AfxDlgProc 極簡實現

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 → 訊息映射
}

WM_INITDIALOG 處理鏈

// CDialog::HandleInitDialog
LRESULT CDialog::HandleInitDialog(WPARAM, LPARAM)
{
    Default(); // → CWnd::Default → CWnd::DefWindowProc
             // → CallWindowProc(m_pfnSuper, ...) → 系統 DefDlgProc
             // → 系統呼叫 AfxDlgProc(WM_INITDIALOG) → OnInitDialog()
    return FALSE;
}

此「繞行」設計確保:

  1. 系統對話框初始化邏輯(置中、字型設定等)完整執行。
  2. MFC 物件與 HWND 正確關聯(Attach 在 Hook 階段完成)。
  3. 使用者 OnInitDialog 在正確時機被呼叫。

三、模態對話框實現:DoModal 全流程

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;
}

關鍵差異:CreateDialogIndirect 建立的是「非模態」視窗

MFC 刻意使用非模態建立 API,再以 RunModalLoop 手動實現模態語意,原因:

  • 完全掌控訊息循環細節(Idle 處理、WM_KICKIDLE、顯示時機)。
  • 避免系統模態循環的黑盒限制(如無法自訂 Idle 行為)。
  • 統一視窗建立路徑(皆走 CreateDialogIndirect → Hook → 子類化)。

四、命令訊息路由:CDialog::OnCmdMsg

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 等)不再繼續向上路由,避免重複處理。

五、訊息預處理:PreTranslateMessage 與 IsDialogMessage

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)。
  • ESC 鍵特殊處理:多行編輯框中不關閉對話框,除非無 IDCANCEL 或已停用。
  • 模態/非模態通用此實現。

六、模態循環核心:RunModalLoop 兩階段設計

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 對話框實現的精髓在於:

  1. 子類化架構:統一視窗程序,保留系統對話框初始化邏輯。
  2. 手工模態循環:以非模態建立 + RunModalLoop 替代系統模態,獲取完全控制權。
  3. 分層訊息處理:PreTranslateMessage → 命令路由 → 訊息映射,清晰分工。
  4. 資料交換抽象:CDataExchange 方向標誌 + DDX_/DDV_ 函式族,消除樣板程式碼。

理解這些機制,不僅能熟練使用 CDialog,更能在需要自訂對話框行為(如特殊鍵盤處理、跨執行緒 UI、動態模板)時,從架構層面做出正確決策。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved