UI线程和Windows消息队列
在Windows應(yīng)用程序中,窗體是由一種稱為“UI線程(User Interface Thread)”的特殊類型的線程創(chuàng)建的。
首先,UI線程是一種“線程”,所以它具有一個(gè)線程應(yīng)該具有的所有特征,比如有一個(gè)線程函數(shù)和一個(gè)線程ID。
其次,“UI線程”又是“特殊”的,這是因?yàn)閁I線程的線程函數(shù)中會(huì)創(chuàng)建一種特殊的對象——窗體,同時(shí),還一并負(fù)責(zé)創(chuàng)建窗體上的各種控件。
窗體和控件大家都很熟悉了,這些對象具有接收用戶操作的功能,它們是用戶使用整個(gè)應(yīng)用程序的媒介,沒有這樣一個(gè)媒介,用戶就無法控制整個(gè)應(yīng)用程序的運(yùn)行和停止,往往也無法直接看到程序的運(yùn)行過程和最終結(jié)果。
那么,窗體和控件又是如何作到對用戶操作進(jìn)行響應(yīng)的呢?這一響應(yīng)是不是由窗體和控件自己“主動(dòng)”完成的?
換句話說:
窗體和控件具不具備獨(dú)立地響應(yīng)用戶操作(比如鍵盤和鼠標(biāo)操作)的功能?
答案是否定的。
那就奇怪了,比如我們用鼠標(biāo)點(diǎn)擊了一個(gè)按鈕,并且看到它“陷”下去了,然后又還原,之后,我們確實(shí)看到了程序執(zhí)行了此按鈕所對應(yīng)的任務(wù)。難道不是按鈕來響應(yīng)用戶操作的嗎?
這實(shí)際上是一個(gè)錯(cuò)覺。這個(gè)錯(cuò)覺產(chǎn)生的根源在于不了解Windows內(nèi)部的運(yùn)作機(jī)理。
簡單地說,窗體和控件之所以能響應(yīng)用戶操作,關(guān)鍵在于負(fù)責(zé)創(chuàng)建它們的UI線程擁有一個(gè)“消息循環(huán)(Message Loop)”。這個(gè)消息循環(huán)由線程函數(shù)負(fù)責(zé)啟動(dòng),通常具有以下的“模樣”(以C++代碼表示):
MSG msg; //代表一條消息
BOOL bRet;
//從UI線程消息隊(duì)列中取出一條消息
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
//錯(cuò)誤處理代碼,通常是直接退出程序
}
else
{
TranslateMessage(&msg); //轉(zhuǎn)換消息格式
DispatchMessage(&msg); //分發(fā)消息給相應(yīng)的窗體
}
}
可以看到,所謂消息循環(huán),其實(shí)就是一個(gè)While循環(huán)語句罷了。
其中,GetMessage()函數(shù)每次從消息隊(duì)列中取出一條消息,此消息的內(nèi)容被填充到變量msg中。
TranslateMessage()函數(shù)主要用于將WM_KEYDOWN和WM_KEYUP消息轉(zhuǎn)換WM_CHAR消息。
提示:
使用C++開發(fā)Windows程序時(shí),各種消息都有一個(gè)對應(yīng)的符號常量,比如,這里的WM_KEYDOWN和WM_KEYUP代表用戶按下一個(gè)鍵后所產(chǎn)生的消息。
消息處理的關(guān)鍵是DispatchMessage()函數(shù)。這個(gè)函數(shù)根據(jù)取出的消息中所包含的窗體句柄,將這一消息轉(zhuǎn)發(fā)給引此句柄所對應(yīng)的窗體對象。
而窗體負(fù)責(zé)響應(yīng)消息的函數(shù)稱為“窗體過程(Window Procedure)”,窗體過程是一個(gè)函數(shù),每個(gè)窗體一個(gè),它大致?lián)碛幸韵碌摹澳印?#xff08;C++代碼):
LRESULT CALLBACK MainWndProc(……)
{
//……
switch (uMsg) //依據(jù)消息標(biāo)識(shí)符進(jìn)行分類處理
{
case WM_CREATE:
// 初始化窗體.
return 0;
case WM_PAINT:
// 繪制窗體
return 0;
//
//處理其他消息
//
default:
//如果窗體沒有定義處理此種消息的代碼,則轉(zhuǎn)去調(diào)用系統(tǒng)默認(rèn)的消息處理函數(shù)
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
//……
}
可以看到,“窗體過程”不過就是一個(gè)多分支語句罷了,在這個(gè)語句中,窗體對不同類型的消息進(jìn)行處理。
在Windows中,UI控件也被視為一個(gè)“Window”,它也擁有自己的“窗體過程”,因此,它也可以同窗體一樣,具備處理消息的能力。
由此我們可以知道UI線程所完成的大致工作就是:
UI線程啟動(dòng)一個(gè)消息循環(huán),每次從本線程所對應(yīng)的消息隊(duì)列中取出一條消息,然后根據(jù)消息所包容的信息,將其轉(zhuǎn)發(fā)給特定的窗體對象,此窗體對象所對應(yīng)的“窗體過程”函數(shù)被調(diào)用以處理這些消息。
上述描述只介紹了事情的后半段,還需要了解事情的前半段,那就是:
用戶操作消息是怎樣“跑”到UI線程的消息隊(duì)列中的?
我們知道,Windows同時(shí)可以運(yùn)行多個(gè)進(jìn)程,每個(gè)進(jìn)程又擁有多個(gè)線程,其中有一些線程是UI線程,這些UI線程可能會(huì)創(chuàng)建不止一個(gè)窗體,那么問題發(fā)生了:
用戶在屏幕上某個(gè)位置按了一下鼠標(biāo),相關(guān)信息是怎樣傳給特定的UI線程,并最終由特定窗體的“窗體過程”負(fù)責(zé)處理?
答案是操作系統(tǒng)負(fù)責(zé)完成消息的投寄工作。
操作系統(tǒng)會(huì)監(jiān)控計(jì)算機(jī)上的鍵盤和鼠標(biāo)等輸入設(shè)備,為每一個(gè)輸入事件(由用戶操作所引發(fā),比如用戶按了某個(gè)鍵)生成一個(gè)消息。根據(jù)事件發(fā)生時(shí)的情況(比如當(dāng)前激活的窗體負(fù)責(zé)接收用戶按鍵,而依據(jù)用戶點(diǎn)擊鼠標(biāo)的坐標(biāo)可以知道用戶在哪個(gè)窗體區(qū)域內(nèi)點(diǎn)擊了鼠標(biāo)),操作系統(tǒng)會(huì)確定出此消息應(yīng)該發(fā)給哪個(gè)窗體對象。
這些生成的消息會(huì)統(tǒng)一地先臨時(shí)放置在一個(gè)“系統(tǒng)消息隊(duì)列(system message queue)”中,然后,操作系統(tǒng)有一個(gè)專門的線程負(fù)責(zé)從這一隊(duì)列中取出消息,根據(jù)消息的目標(biāo)對象(就是窗體的句柄),將其移動(dòng)到創(chuàng)建它的UI線程所對應(yīng)的消息隊(duì)列中。操作系統(tǒng)在創(chuàng)建進(jìn)程和線程時(shí),都同時(shí)記錄了大量的控制信息(比如通過進(jìn)程控制塊和句柄表可以查找到進(jìn)程所創(chuàng)建的所有線程和引用的核心對象),因此,根據(jù)窗體句柄來確定此消息應(yīng)屬于哪個(gè)UI線程對于操作系統(tǒng)來說是很簡單的一件事。
注意,每個(gè)UI線程都有一個(gè)消息隊(duì)列,而不是每個(gè)窗體一個(gè)消息隊(duì)列!
那么,操作系統(tǒng)是不是會(huì)為每一個(gè)線程都創(chuàng)建一個(gè)消息隊(duì)列呢?
答案是:只有當(dāng)一個(gè)線程調(diào)用Win32 API中的GDI(Graphics Device Interface)和User函數(shù)時(shí),操作系統(tǒng)才會(huì)將其看成是一個(gè)UI線程,并為它創(chuàng)建一個(gè)消息隊(duì)列。
需要注意的是,消息循環(huán)是由UI線程的線程函數(shù)啟動(dòng)的,操作系統(tǒng)不管這件事,它只管為UI線程創(chuàng)建消息隊(duì)列。因此,如果某個(gè)UI線程的線程函數(shù)中沒有定義消息循環(huán),那么,它所擁有的窗體是無法正確繪制的。
請看以下代碼:
class Program
{
static void Main(string[] args)
{
Form1 frm = new Form1();
frm.Show();
Console.ReadKey();
}
}
上述代碼屬于一個(gè)控制臺(tái)應(yīng)用程序,在Main()函數(shù)中,創(chuàng)建了一個(gè)Form1窗體對象,調(diào)用它的Show()方法顯示,然后調(diào)用Console.ReadKey()方法等待用戶按鍵結(jié)束進(jìn)程。
程序運(yùn)行的截圖如下:
如上圖所示,會(huì)發(fā)現(xiàn)窗體顯示一個(gè)空白方框,不接收任何的鼠標(biāo)和鍵盤操作。
原因何在?
產(chǎn)生這一現(xiàn)象的原因可以解釋如下:
由于控制臺(tái)程序需要運(yùn)行于一個(gè)“控制臺(tái)窗口”中,因此,操作系統(tǒng)認(rèn)為它是一個(gè)UI線程,會(huì)為其創(chuàng)建一個(gè)消息隊(duì)列。
Main()函數(shù)由于是程序入口點(diǎn),所以執(zhí)行它的線程是進(jìn)程的第一個(gè)線程(即主線程),在主線程中,創(chuàng)建了一個(gè)Form1窗體對象,對其Show()方法的調(diào)用只是設(shè)置其Visible屬性=true,這將導(dǎo)致Windows調(diào)用相應(yīng)的Win32 API函數(shù)顯示窗體,但這一調(diào)用并非阻塞調(diào)用,也沒有啟動(dòng)一個(gè)消息循環(huán),所以Show()方法很快返回,繼續(xù)執(zhí)行下一句“Console.ReadKey();”,此句的執(zhí)行導(dǎo)致主線程調(diào)用相應(yīng)的Win32 API函數(shù)等待用戶按鈕,阻塞執(zhí)行。
注意,如果這時(shí)用戶用鼠標(biāo)點(diǎn)擊窗體,嘗試與窗體交互,相應(yīng)的消息的確發(fā)到了控制臺(tái)應(yīng)用程序主線程的消息隊(duì)列中,但主線程并未啟動(dòng)一個(gè)消息循環(huán)(你看到Main()函數(shù)中有任何的循環(huán)語句嗎?)以取出消息隊(duì)列中的消息并“分發(fā)”給窗體,因此,窗體函數(shù)沒被調(diào)用,自然無法正確繪制了。
如果窗體本身是調(diào)用ShowDialog()方法顯示的,這是一個(gè)阻塞調(diào)用,它會(huì)在內(nèi)部啟動(dòng)一個(gè)消息循環(huán),此消息循環(huán)可以從主線程的消息隊(duì)列是提取消息,從而讓此窗體成為一個(gè)“正常”的窗體。
當(dāng)用戶關(guān)閉窗體后,Main()方法后繼的代碼繼續(xù)執(zhí)行,直到運(yùn)行結(jié)束。
如果在創(chuàng)建窗體對象并調(diào)用Show()方法顯示后,主線程沒有調(diào)用“Console.ReadKey();”之類方法“暫停”,而是直接退出,這將導(dǎo)致操作系統(tǒng)中止整個(gè)進(jìn)程,回收所有核心對象,因此,創(chuàng)建的窗體也會(huì)被銷毀,不可能再看見它。
現(xiàn)在再考慮復(fù)雜一些:如果我們在另一個(gè)線程中創(chuàng)建并顯示窗體,又將如何?
class Program
{
static void Main(string[] args)
{
Thread th = new Thread(ShowWindow);
th.Start();//在另一個(gè)線程中創(chuàng)建并顯示窗體
Console.WriteLine("窗體已創(chuàng)建,敲任意鍵退出...");
Console.ReadKey();
Console.WriteLine("主線程退出...");
}
static void ShowWindow()
{
Form1 frm = new Form1();
frm.ShowDialog();
}
}
程序運(yùn)行結(jié)果如下:
可以看到,由于窗體使用ShowDialog()顯示,因此,控制臺(tái)窗口和應(yīng)用程序窗體都能正常地接收用戶的鍵盤和鼠標(biāo)消息。即使主線程退出了,只要窗體沒有關(guān)閉,操作系統(tǒng)會(huì)認(rèn)為“進(jìn)程”仍在執(zhí)行,因此,控制臺(tái)窗口會(huì)保持顯示,直到窗體關(guān)閉,整個(gè)進(jìn)程才結(jié)束。
在這種情況下,本示例程序中有兩個(gè)UI線程,一個(gè)是控制臺(tái)窗口,另一個(gè)創(chuàng)建應(yīng)用程序窗體的那個(gè)線程。
如果在線程函數(shù)中創(chuàng)建窗體后,改為Show()方法顯示,由于Show()方法沒有啟動(dòng)消息循環(huán),所以窗體不能正確繪制,并且會(huì)隨著創(chuàng)建它的UI線程的終止而被操作系統(tǒng)回收資源。
有趣的是,我們可以使用Visual Studio設(shè)置“控制臺(tái)應(yīng)用程序”不創(chuàng)建“控制臺(tái)窗口”,只需將項(xiàng)目類型改為“Windows Application”即可。
這時(shí),示例程序運(yùn)行時(shí),Visual Studio會(huì)報(bào)告錯(cuò)誤:
引發(fā)這一錯(cuò)誤的原因是應(yīng)用程序主線程不再創(chuàng)建控制臺(tái)窗口,操作系統(tǒng)不再認(rèn)為它是UI線程,不為其創(chuàng)建消息隊(duì)列,主線程將無法接收到任何按鍵消息, 因此Console.ReadKey()底層調(diào)用的Win32API函數(shù)無法正常運(yùn)行,引發(fā)程序異常。
/******************************************windows消息循環(huán)標(biāo)準(zhǔn)實(shí)例*****************************
#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中有一個(gè)系統(tǒng)消息隊(duì)列,對于每一個(gè)正在執(zhí)行的Windows應(yīng)用程序,系統(tǒng)為其建立一個(gè)“消息隊(duì)列”,即應(yīng)用程序隊(duì)列,用來存放該程序可能創(chuàng)建的各種窗口的消息。應(yīng)用程序中含有一段稱作“消息循環(huán)”的代碼,用來從消息隊(duì)列中檢索這些消息并把它們分發(fā)到相應(yīng)的窗口函數(shù)中。
二 Windows為當(dāng)前執(zhí)行的每個(gè)Windows程序維護(hù)一個(gè)「消息隊(duì)列」。在發(fā)生輸入事件之后,Windows將事件轉(zhuǎn)換為一個(gè)「消息」并將消息放入程序的消息隊(duì)列中。程序通過執(zhí)行一塊稱之為「消息循環(huán)」的程序代碼從消息隊(duì)列中取出消息:
while(GetMessage (&msg, NULL, 0, 0))???????
{????????
??? TranslateMessage (&msg) ;????????
??? DispatchMessage (&msg) ;???????
}
msg變量是型態(tài)為MSG的結(jié)構(gòu),型態(tài)MSG在WINUSER.H中定義如下:
typedef struct tagMSG???????
{???????
??? HWND?? hwnd ;????????
??? UINT?? message ;????????
??? WPARAM wParam ;????????
??? LPARAM lParam ;????????
??? DWORD? time ;????????
??? POINT? pt ;???????
}???????
MSG, * PMSG ;
??????
POINT數(shù)據(jù)型態(tài)也是一個(gè)結(jié)構(gòu),它在WINDEF.H中定義如下:
typedef struct tagPOINT???????
{???????
??? LONG? x ;???????
??? LONG? y ;???????
}???????
POINT, * PPOINT;
TranslateMessage(&msg);?將msg結(jié)構(gòu)傳給Windows,進(jìn)行一些鍵盤轉(zhuǎn)換。(關(guān)于這一點(diǎn),我們將在第六章中深入討論。)
DispatchMessage(&msg);又將msg結(jié)構(gòu)回傳給Windows。然后,Windows將該消息發(fā)送給適當(dāng)?shù)拇翱谙⑻幚沓绦?#xff0c;讓它進(jìn)行處理。這也就是說,Windows將呼叫窗口消息處理程序。在HELLOWIN中,這個(gè)窗口消息處理程序就是WndProc函數(shù)。處理完消息之后,WndProc傳回到Windows。此時(shí),Windows還停留在DispatchMessage呼叫中。在結(jié)束DispatchMessage呼叫的處理之后,Windows回到HELLOWIN程序中,并且接著從下一個(gè)GetMessage呼叫開始消息循環(huán)。
????????
三 隊(duì)列化消息與非隊(duì)列化消息
????
消息能夠被分為「隊(duì)列化的」和「非隊(duì)列化的」。隊(duì)列化的消息是由Windows放入程序消息隊(duì)列中的。在程序的消息循環(huán)中,重新傳回并分配給窗口消息處理程序。非隊(duì)列化的消息在Windows呼叫窗口時(shí)直接送給窗口消息處理程序。也就是說,隊(duì)列化的消息被「發(fā)送」給消息隊(duì)列,而非隊(duì)列化的消息則「發(fā)送」給窗口消息處理程序。任何情況下,窗口消息處理程序都將獲得窗口所有的消息--包括隊(duì)列化的和非隊(duì)列化的。窗口消息處理程序是窗口的「消息中心」。
隊(duì)列化消息基本上是使用者輸入的結(jié)果,以擊鍵(如WM_KEYDOWN和WM_KEYUP消息)、擊鍵產(chǎn)生的字符(WM_CHAR)、鼠標(biāo)移動(dòng)(WM_MOUSEMOVE)和鼠標(biāo)按鈕(WM_LBUTTONDOWN)的形式給出。隊(duì)列化消息還包含時(shí)鐘消息(WM_TIMER)、更新消息(WM_PAINT)和退出消息(WM_QUIT)。
非隊(duì)列化消息則是其它消息。在許多情況下,非隊(duì)列化消息來自呼叫特定的Windows函數(shù)。例如,當(dāng)WinMain呼叫CreateWindow時(shí),Windows將建立窗口并在處理中給窗口消息處理程序發(fā)送一個(gè)WM_CREATE消息。當(dāng)WinMain呼叫ShowWindow時(shí),Windows將給窗口消息處理程序發(fā)送WM_SIZE和WM_SHOWWINDOW消息。當(dāng)WinMain呼叫UpdateWindow時(shí),Windows將給窗口消息處理程序發(fā)送WM_PAINT消息。鍵盤或鼠標(biāo)輸入時(shí)發(fā)出的隊(duì)列化消息信號,也能在非隊(duì)列化消息中出現(xiàn)。例如,用鍵盤或鼠標(biāo)選擇了一個(gè)菜單項(xiàng)時(shí),鍵盤或鼠標(biāo)消息就是隊(duì)列化的,而說明菜單項(xiàng)已選中的WM_COMMAND消息則可能就是非隊(duì)列化的。
四 SendMessage()與PostMessage()之間的區(qū)別是什么?
它們兩者是用于向應(yīng)用程序發(fā)送消息的。PostMessagex()將消息直接加入到應(yīng)用程序的消息隊(duì)列中,不等程序返回就退出;而SendMessage()則剛好相反,應(yīng)用程序處理完此消息后,它才返回。我想下圖能夠比較好的體現(xiàn)這兩個(gè)函數(shù)的關(guān)系:
?
五 函數(shù)peekmessage和getmessage的區(qū)別?
兩個(gè)函數(shù)主要有以下兩個(gè)區(qū)別:
1.GetMessage將等到有合適的消息時(shí)才返回,而PeekMessage只是撇一下消息隊(duì)列。
2.GetMessage會(huì)將消息從隊(duì)列中刪除,而PeekMessage可以設(shè)置最后一個(gè)參數(shù)wRemoveMsg來決定是否將消息保留在隊(duì)列中。
總結(jié)
以上是生活随笔為你收集整理的UI线程和Windows消息队列的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows消息机制疑问探究
- 下一篇: VUE:兄弟组件间传参