UI线程和Windows消息队列
在Windows應用程序中,窗體是由一種稱為“UI線程(User Interface Thread)”的特殊類型的線程創建的。
首先,UI線程是一種“線程”,所以它具有一個線程應該具有的所有特征,比如有一個線程函數和一個線程ID。
其次,“UI線程”又是“特殊”的,這是因為UI線程的線程函數中會創建一種特殊的對象——窗體,同時,還一并負責創建窗體上的各種控件。
窗體和控件大家都很熟悉了,這些對象具有接收用戶操作的功能,它們是用戶使用整個應用程序的媒介,沒有這樣一個媒介,用戶就無法控制整個應用程序的運行和停止,往往也無法直接看到程序的運行過程和最終結果。
那么,窗體和控件又是如何作到對用戶操作進行響應的呢?這一響應是不是由窗體和控件自己“主動”完成的?
換句話說:
窗體和控件具不具備獨立地響應用戶操作(比如鍵盤和鼠標操作)的功能?
答案是否定的。
那就奇怪了,比如我們用鼠標點擊了一個按鈕,并且看到它“陷”下去了,然后又還原,之后,我們確實看到了程序執行了此按鈕所對應的任務。難道不是按鈕來響應用戶操作的嗎?
這實際上是一個錯覺。這個錯覺產生的根源在于不了解Windows內部的運作機理。
簡單地說,窗體和控件之所以能響應用戶操作,關鍵在于負責創建它們的UI線程擁有一個“消息循環(Message Loop)”。這個消息循環由線程函數負責啟動,通常具有以下的“模樣”(以C++代碼表示):
MSG msg; //代表一條消息
BOOL bRet;
//從UI線程消息隊列中取出一條消息
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
//錯誤處理代碼,通常是直接退出程序
}
else
{
TranslateMessage(&msg); //轉換消息格式
DispatchMessage(&msg); //分發消息給相應的窗體
}
}
可以看到,所謂消息循環,其實就是一個While循環語句罷了。
其中,GetMessage()函數每次從消息隊列中取出一條消息,此消息的內容被填充到變量msg中。
TranslateMessage()函數主要用于將WM_KEYDOWN和WM_KEYUP消息轉換WM_CHAR消息。
提示:
使用C++開發Windows程序時,各種消息都有一個對應的符號常量,比如,這里的WM_KEYDOWN和WM_KEYUP代表用戶按下一個鍵后所產生的消息。
消息處理的關鍵是DispatchMessage()函數。這個函數根據取出的消息中所包含的窗體句柄,將這一消息轉發給引此句柄所對應的窗體對象。
而窗體負責響應消息的函數稱為“窗體過程(Window Procedure)”,窗體過程是一個函數,每個窗體一個,它大致擁有以下的“模樣”(C++代碼):
LRESULT CALLBACK MainWndProc(……)
{
//……
switch (uMsg) //依據消息標識符進行分類處理
{
case WM_CREATE:
// 初始化窗體.
return 0;
case WM_PAINT:
// 繪制窗體
return 0;
//
//處理其他消息
//
default:
//如果窗體沒有定義處理此種消息的代碼,則轉去調用系統默認的消息處理函數
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
//……
}
可以看到,“窗體過程”不過就是一個多分支語句罷了,在這個語句中,窗體對不同類型的消息進行處理。
在Windows中,UI控件也被視為一個“Window”,它也擁有自己的“窗體過程”,因此,它也可以同窗體一樣,具備處理消息的能力。
由此我們可以知道UI線程所完成的大致工作就是:
UI線程啟動一個消息循環,每次從本線程所對應的消息隊列中取出一條消息,然后根據消息所包容的信息,將其轉發給特定的窗體對象,此窗體對象所對應的“窗體過程”函數被調用以處理這些消息。
上述描述只介紹了事情的后半段,還需要了解事情的前半段,那就是:
用戶操作消息是怎樣“跑”到UI線程的消息隊列中的?
我們知道,Windows同時可以運行多個進程,每個進程又擁有多個線程,其中有一些線程是UI線程,這些UI線程可能會創建不止一個窗體,那么問題發生了:
用戶在屏幕上某個位置按了一下鼠標,相關信息是怎樣傳給特定的UI線程,并最終由特定窗體的“窗體過程”負責處理?
答案是操作系統負責完成消息的投寄工作。
操作系統會監控計算機上的鍵盤和鼠標等輸入設備,為每一個輸入事件(由用戶操作所引發,比如用戶按了某個鍵)生成一個消息。根據事件發生時的情況(比如當前激活的窗體負責接收用戶按鍵,而依據用戶點擊鼠標的坐標可以知道用戶在哪個窗體區域內點擊了鼠標),操作系統會確定出此消息應該發給哪個窗體對象。
這些生成的消息會統一地先臨時放置在一個“系統消息隊列(system message queue)”中,然后,操作系統有一個專門的線程負責從這一隊列中取出消息,根據消息的目標對象(就是窗體的句柄),將其移動到創建它的UI線程所對應的消息隊列中。操作系統在創建進程和線程時,都同時記錄了大量的控制信息(比如通過進程控制塊和句柄表可以查找到進程所創建的所有線程和引用的核心對象),因此,根據窗體句柄來確定此消息應屬于哪個UI線程對于操作系統來說是很簡單的一件事。
注意,每個UI線程都有一個消息隊列,而不是每個窗體一個消息隊列!
那么,操作系統是不是會為每一個線程都創建一個消息隊列呢?
答案是:只有當一個線程調用Win32 API中的GDI(Graphics Device Interface)和User函數時,操作系統才會將其看成是一個UI線程,并為它創建一個消息隊列。
需要注意的是,消息循環是由UI線程的線程函數啟動的,操作系統不管這件事,它只管為UI線程創建消息隊列。因此,如果某個UI線程的線程函數中沒有定義消息循環,那么,它所擁有的窗體是無法正確繪制的。
請看以下代碼:
class Program
{
static void Main(string[] args)
{
Form1 frm = new Form1();
frm.Show();
Console.ReadKey();
}
}
上述代碼屬于一個控制臺應用程序,在Main()函數中,創建了一個Form1窗體對象,調用它的Show()方法顯示,然后調用Console.ReadKey()方法等待用戶按鍵結束進程。
程序運行的截圖如下:
如上圖所示,會發現窗體顯示一個空白方框,不接收任何的鼠標和鍵盤操作。
原因何在?
產生這一現象的原因可以解釋如下:
由于控制臺程序需要運行于一個“控制臺窗口”中,因此,操作系統認為它是一個UI線程,會為其創建一個消息隊列。
Main()函數由于是程序入口點,所以執行它的線程是進程的第一個線程(即主線程),在主線程中,創建了一個Form1窗體對象,對其Show()方法的調用只是設置其Visible屬性=true,這將導致Windows調用相應的Win32 API函數顯示窗體,但這一調用并非阻塞調用,也沒有啟動一個消息循環,所以Show()方法很快返回,繼續執行下一句“Console.ReadKey();”,此句的執行導致主線程調用相應的Win32 API函數等待用戶按鈕,阻塞執行。
注意,如果這時用戶用鼠標點擊窗體,嘗試與窗體交互,相應的消息的確發到了控制臺應用程序主線程的消息隊列中,但主線程并未啟動一個消息循環(你看到Main()函數中有任何的循環語句嗎?)以取出消息隊列中的消息并“分發”給窗體,因此,窗體函數沒被調用,自然無法正確繪制了。
如果窗體本身是調用ShowDialog()方法顯示的,這是一個阻塞調用,它會在內部啟動一個消息循環,此消息循環可以從主線程的消息隊列是提取消息,從而讓此窗體成為一個“正常”的窗體。
當用戶關閉窗體后,Main()方法后繼的代碼繼續執行,直到運行結束。
如果在創建窗體對象并調用Show()方法顯示后,主線程沒有調用“Console.ReadKey();”之類方法“暫停”,而是直接退出,這將導致操作系統中止整個進程,回收所有核心對象,因此,創建的窗體也會被銷毀,不可能再看見它。
現在再考慮復雜一些:如果我們在另一個線程中創建并顯示窗體,又將如何?
class Program
{
static void Main(string[] args)
{
Thread th = new Thread(ShowWindow);
th.Start();//在另一個線程中創建并顯示窗體
Console.WriteLine("窗體已創建,敲任意鍵退出...");
Console.ReadKey();
Console.WriteLine("主線程退出...");
}
static void ShowWindow()
{
Form1 frm = new Form1();
frm.ShowDialog();
}
}
程序運行結果如下:
可以看到,由于窗體使用ShowDialog()顯示,因此,控制臺窗口和應用程序窗體都能正常地接收用戶的鍵盤和鼠標消息。即使主線程退出了,只要窗體沒有關閉,操作系統會認為“進程”仍在執行,因此,控制臺窗口會保持顯示,直到窗體關閉,整個進程才結束。
在這種情況下,本示例程序中有兩個UI線程,一個是控制臺窗口,另一個創建應用程序窗體的那個線程。
如果在線程函數中創建窗體后,改為Show()方法顯示,由于Show()方法沒有啟動消息循環,所以窗體不能正確繪制,并且會隨著創建它的UI線程的終止而被操作系統回收資源。
有趣的是,我們可以使用Visual Studio設置“控制臺應用程序”不創建“控制臺窗口”,只需將項目類型改為“Windows Application”即可。
這時,示例程序運行時,Visual Studio會報告錯誤:
引發這一錯誤的原因是應用程序主線程不再創建控制臺窗口,操作系統不再認為它是UI線程,不為其創建消息隊列,主線程將無法接收到任何按鍵消息, 因此Console.ReadKey()底層調用的Win32API函數無法正常運行,引發程序異常。
/******************************************windows消息循環標準實例*****************************
#include <windows.h>
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
int WINAPI WinMain(HINSTANCE hINstance,
HINSTANCE hPrevInstance,
LPSTR lpszCmdParam,
int nCmdShow)
{
static char szAppName[]="xianshi";
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
if(! hPrevInstance)
{
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra=0;
wndclass.cbWndExtra=0;
wndclass.hInstance=hINstance;
wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL,IDC_ARROW);
wndclass.hbrBackground=(HBRUSH)GetStockObject(LTGRAY_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;
RegisterClass(&wndclass);
}
hwnd=CreateWindow(
szAppName,
"The XianShi",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,CW_USEDEFAULT,
CW_USEDEFAULT,CW_USEDEFAULT,
NULL,
NULL,
hINstance,
NULL);
ShowWindow(hwnd,nCmdShow);
UpdateWindow(hwnd);
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
static LOGFONT lf;
HFONT hnewFont;//**********
HFONT holdFont;//**********
switch(message)
{
case WM_CREATE:
return 0;
case WM_PAINT:
lf.lfHeight=-64;
lf.lfWeight=500;
lf.lfPitchAndFamily=DEFAULT_PITCH & FF_DONTCARE;
lf.lfCharSet=GB2312_CHARSET;
strcpy(lf.lfFaceName,"黑體");
hnewFont=CreateFontIndirect(&lf);
hdc=BeginPaint(hwnd,&ps);
GetClientRect(hwnd,&rect);
GetClientRect(hwnd,&rect);
holdFont=(HFONT)SelectObject(hdc,hnewFont);
SetTextColor(hdc,RGB(255,0,0));
SetBkColor(hdc,RGB(255,255,0));
DrawText(hdc,
"VC中顯示字體與背景",
-1,
&rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
SelectObject(hdc,holdFont);
DeleteObject(hnewFont);
EndPaint(hwnd,&ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd,message,wParam,lParam);
}
一 Windows中有一個系統消息隊列,對于每一個正在執行的Windows應用程序,系統為其建立一個“消息隊列”,即應用程序隊列,用來存放該程序可能創建的各種窗口的消息。應用程序中含有一段稱作“消息循環”的代碼,用來從消息隊列中檢索這些消息并把它們分發到相應的窗口函數中。
二 Windows為當前執行的每個Windows程序維護一個「消息隊列」。在發生輸入事件之后,Windows將事件轉換為一個「消息」并將消息放入程序的消息隊列中。程序通過執行一塊稱之為「消息循環」的程序代碼從消息隊列中取出消息:
while(GetMessage (&msg, NULL, 0, 0))???????
{????????
??? TranslateMessage (&msg) ;????????
??? DispatchMessage (&msg) ;???????
}
msg變量是型態為MSG的結構,型態MSG在WINUSER.H中定義如下:
typedef struct tagMSG???????
{???????
??? HWND?? hwnd ;????????
??? UINT?? message ;????????
??? WPARAM wParam ;????????
??? LPARAM lParam ;????????
??? DWORD? time ;????????
??? POINT? pt ;???????
}???????
MSG, * PMSG ;
??????
POINT數據型態也是一個結構,它在WINDEF.H中定義如下:
typedef struct tagPOINT???????
{???????
??? LONG? x ;???????
??? LONG? y ;???????
}???????
POINT, * PPOINT;
TranslateMessage(&msg);?將msg結構傳給Windows,進行一些鍵盤轉換。(關于這一點,我們將在第六章中深入討論。)
DispatchMessage(&msg);又將msg結構回傳給Windows。然后,Windows將該消息發送給適當的窗口消息處理程序,讓它進行處理。這也就是說,Windows將呼叫窗口消息處理程序。在HELLOWIN中,這個窗口消息處理程序就是WndProc函數。處理完消息之后,WndProc傳回到Windows。此時,Windows還停留在DispatchMessage呼叫中。在結束DispatchMessage呼叫的處理之后,Windows回到HELLOWIN程序中,并且接著從下一個GetMessage呼叫開始消息循環。
????????
三 隊列化消息與非隊列化消息
????
消息能夠被分為「隊列化的」和「非隊列化的」。隊列化的消息是由Windows放入程序消息隊列中的。在程序的消息循環中,重新傳回并分配給窗口消息處理程序。非隊列化的消息在Windows呼叫窗口時直接送給窗口消息處理程序。也就是說,隊列化的消息被「發送」給消息隊列,而非隊列化的消息則「發送」給窗口消息處理程序。任何情況下,窗口消息處理程序都將獲得窗口所有的消息--包括隊列化的和非隊列化的。窗口消息處理程序是窗口的「消息中心」。
隊列化消息基本上是使用者輸入的結果,以擊鍵(如WM_KEYDOWN和WM_KEYUP消息)、擊鍵產生的字符(WM_CHAR)、鼠標移動(WM_MOUSEMOVE)和鼠標按鈕(WM_LBUTTONDOWN)的形式給出。隊列化消息還包含時鐘消息(WM_TIMER)、更新消息(WM_PAINT)和退出消息(WM_QUIT)。
非隊列化消息則是其它消息。在許多情況下,非隊列化消息來自呼叫特定的Windows函數。例如,當WinMain呼叫CreateWindow時,Windows將建立窗口并在處理中給窗口消息處理程序發送一個WM_CREATE消息。當WinMain呼叫ShowWindow時,Windows將給窗口消息處理程序發送WM_SIZE和WM_SHOWWINDOW消息。當WinMain呼叫UpdateWindow時,Windows將給窗口消息處理程序發送WM_PAINT消息。鍵盤或鼠標輸入時發出的隊列化消息信號,也能在非隊列化消息中出現。例如,用鍵盤或鼠標選擇了一個菜單項時,鍵盤或鼠標消息就是隊列化的,而說明菜單項已選中的WM_COMMAND消息則可能就是非隊列化的。
四 SendMessage()與PostMessage()之間的區別是什么?
它們兩者是用于向應用程序發送消息的。PostMessagex()將消息直接加入到應用程序的消息隊列中,不等程序返回就退出;而SendMessage()則剛好相反,應用程序處理完此消息后,它才返回。我想下圖能夠比較好的體現這兩個函數的關系:
?
五 函數peekmessage和getmessage的區別?
兩個函數主要有以下兩個區別:
1.GetMessage將等到有合適的消息時才返回,而PeekMessage只是撇一下消息隊列。
2.GetMessage會將消息從隊列中刪除,而PeekMessage可以設置最后一個參數wRemoveMsg來決定是否將消息保留在隊列中。
總結
以上是生活随笔為你收集整理的UI线程和Windows消息队列的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows消息机制疑问探究
- 下一篇: Windows消息循环机制详细概述