模態對話框引發的窗口過程重入問題

首先,先普及下什麼是模態對話框,模態對話框的話就是不能在對話框和該程序的其他窗口之間進行切換,必須結束該對話框纔可以進行其他操作。

可以使用DialogBox創建一個模態對話框,至於爲啥這個對話框會造成上面的原因呢,其實msdn上有那麼一段解釋:

The DialogBox macro uses the CreateWindowEx function to create the dialog box. DialogBox then sends a WM_INITDIALOG message (and a WM_SETFONT message if the template specifies the DS_SETFONT or DS_SHELLFONT style) to the dialog box procedure. The function displays the dialog box (regardless of whether the template specifies the WS_VISIBLE style), disables the owner window, and starts its own message loop to retrieve and dispatch messages for the dialog box. 

When the dialog box procedure calls the EndDialog function, DialogBox destroys the dialog box, ends the message loop, enables the owner window (if previously enabled), and returns the nResult parameter specified by the dialog box procedure when it called EndDialog. 

在第一段的最後末尾,提到了會禁用父窗口,並啓動自己的消息循環來檢索和分發對話框的消息。

第二段闡述了使用EndDialog關閉對話框時,會結束自己的消息循環並啓用父窗口。

這裏的啓用禁用父窗口可以使用EnableWindow函數,該函數可以啓用或禁用指定窗口或控件的鼠標和鍵盤輸入。我們可以其實可以在WM_INITDIALOG中調用該函數啓用父窗口得到驗證。

好了,因爲上面並不是想要記錄的重要,所以簡單概括下,重點在於自建的消息循環,此時可能會造成一些窗口過程重入帶來的問題,所謂的窗口過程就是回調(消息處理函數WinProc),看下面的案例

#include <windows.h>

LRESULT CALLBACK WinProc(HWND,UINT,WPARAM,LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASS wndClass = {0};
    wndClass.lpfnWndProc = WinProc;
    wndClass.lpszClassName = TEXT("keyTest");
    
    RegisterClass(&wndClass);
    
    HWND hwnd = CreateWindow(TEXT("keyTest"), TEXT("keyBoard"),
        WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        NULL,NULL,hInstance,NULL);
    
    ShowWindow(hwnd,nCmdShow);
    UpdateWindow(hwnd);
    
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    
    return 0;
}

LRESULT CALLBACK WinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_KEYDOWN: //輸入法得切換英文
        {
            TCHAR szBuff[MAXBYTE] = { 0 };
            if (wParam >= 'A' && wParam <= 'Z')
            {
                wsprintf(szBuff, TEXT("OnKeyDown: %c\r\n"), wParam);
                MessageBox(hwnd, szBuff, NULL, MB_OK); //第一處
            }
            OutputDebugString(szBuff);
            return 0;
        }
        case WM_CHAR:
        {
            TCHAR szBuff[MAXBYTE] = { 0 };
            if (wParam >= 'a' && wParam <= 'z')
            {
                wsprintf(szBuff, TEXT("OnChar: %c\r\n"), wParam);
                MessageBox(hwnd, szBuff, NULL, MB_OK);//第二處
            }
            OutputDebugString(szBuff);
            return 0;
        }
        case WM_DESTROY:
        {
            PostQuitMessage(0);
            return 0;
        }
    }
    return DefWindowProc(hwnd,uMsg,wParam,lParam);
}

在上面的代碼中,有兩處MessageBox,第一處在擊鍵消息中,第二處在於字符消息中。那麼我們應該先來談談鍵盤的消息處理,

在消息循環中有這麼一行代碼

TranslateMessage(&msg);

查看MSDN我們可知,該函數用於將擊鍵消息翻譯成一個新的字符消息,並將該字符消息放到當前線程的消息隊列中。注意這個字符消息是新生成的,該並不會改變傳入參數的msg。

也就是說,當我鍵盤是英文小寫字母狀態時,會產生一個A的擊鍵消息,然後翻譯該消息後又生成了一個a的字符消息。

好了,然後我們看代碼,通過上面的分析,是不是按理來說,應該會是先彈出WM_KEYDOWN中的彈窗,然後再彈出WM_CHAR中的彈窗。

運行結果是相反的,先彈出了第二處,然後再彈出了第一處。你可能會想到是不是翻譯完後先發送了WM_CHAR再發送了WM_KEYDOWN消息呢?

不不,其實調試下可知,其流程是沒問題的,先進入了WM_KEYDOWN消息,然後在MessageBox的時候,神奇的事情發生了,流程又轉到了WM_CHAR消息,然後彈窗,最後返回到WM_KEYDOWN消息的彈窗。

爲什麼會這樣?這個問題開始也困擾了我好幾天。下面分析一下

MessageBox是個模態對話框,那麼對於模態對話框,其內部就會有自己的消息循環,那麼對於一些輸入型消息都會被該MessageBox先處理,而一些其餘消息會根據其消息體(msg.hwnd)發送給對於的窗體處理。

OK,模擬下流程,當我們按下a鍵,先產生了擊鍵消息,後經過翻譯後產生了字符消息(該消息屬於主窗體),那麼先來到WM_KEYDOWN,裏面調用了MessageBox,此時,主窗口的消息循環進入MessageBox的消息循環,在該MessageBox的消息循環中接收到了字符消息,該消息屬於主窗體,分發消息後那麼又回到該主窗體的回調函數中的WM_CHAR(此時窗口過程被重入),裏面又調用了MessageBox,所以此時消息循環又轉移了,那麼只有當WM_CHAR中的MessageBox結束後才返回到WM_KEYDOWN中的MessageBox,KEYDOWN結束後纔回到了主程序的消息循環。

與上面問題類似的簡單說法就是,當一個窗口過程(回調)調用爲其發送其他消息的函數時,這種情況下,在該函數調用返回前,窗口過程必須將第二個消息處理完,窗口過程才處理前一條消息,也就是相當於同步,順序流程處理消息。

在大多數情況下,窗口過程的重入並不會帶來什麼問題,但是我們需要做到心中有數,知道可能會有該情況的發生。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章