1.2 消息和消息循環
在Windows系列操作系統中,廣泛使用了消息驅動的概念。在MiniGUI中,我們也使用了消息驅動作為應用程序的創建構架。
在消息驅動的應用程序中,計算機外設發生的事件,例如鍵盤鍵的敲擊、鼠標鍵的按擊等,都由支持系統收集,將其以事先的約定格式翻譯為特定的消息。應用程序一般包含有自己的消息隊列,系統將消息發送到應用程序的消息隊列中。應用程序可以建立一個循環,在這個循環中讀取消息並處理消息,一直處理到特定的消息傳來為止。這樣的循環稱為消息循環。一般地,消息由代表消息的一個整型數和消息的附加參數組成。例如,鼠標左鍵的按下消息,可能由133這個數來表示,其附加參數可能包含按下時的鼠標所在位置信息。例如,MiniGUI中如下定義消息:
typedef struct
{
HWND hwnd;
int message;
WPARAM wParam;
LPARAM lParam;
...
}MSG;
message指定了特定的消息類型,wParam是以unsigned int類型定義的消息的短參數,lParam 是以long 類型定義的消息長參數。
應用程序一般要提供一個處理消息的標准函數。在消息循環中,系統可以調用此函數,應用程序在此函數中處理相應消息。
圖 1.2是一個消息驅動的應用程序的簡單構架示意。
圖 1.2 消息驅動的應用程序的簡單構架
在 MiniGUI 中,消息分為如下幾種類型:
ü 系統消息,為系統內部管理使用。
ü 鼠標消息,鼠標的點擊、移動等產生的消息。
ü 鍵盤消息,鍵盤的按鍵消息。
ü 窗口消息,窗口管理消息。
ü 菜單消息,菜單管理消息。
ü 命令消息等。
1.3 窗口過程和窗口類
窗口過程是用來處理窗口消息的函數過程。對於同一類型的控件,其窗口過程一般是一樣的。因此,系統一般利用窗口的窗口類名來區分不同的窗口類並調用不同的窗口過程。由於幾乎每一個主窗口均和其他窗口有着不同的窗口過程,因此,在 MiniGUI 中,窗口類的概念只存在於控件和窗片中。對於主窗口來說,其窗口過程在建立主窗口時指定,而對控件和窗片來說,則在注冊窗口類時指定,而在建立窗片或控件時指定所屬窗口類。
1.4 句柄
句柄是 MiniGUI 用來標識對象的標識符。句柄和指針概念類似,但它不一定是指針值。利用句柄,MiniGUI 將系統變量從應用項目中分離了出來,因為程序員只能通過句柄訪問對象,因而就沒有利用指針是可能發生的因非法訪問而導致的數據不一致問題。
在 MiniGUI 中,窗口、控件、設備環境、菜單、圖標等均使用句柄訪問。
二、窗口
2.1 應用程序和主窗口
我們將基於 MiniGUI 的一個會話(session)稱為一個應用項目,而其中每個單獨的線程或線程組稱為應用。每個應用項目可建立多個應用。主窗口是建立在 MiniGUI 基礎上的應用的主界面。MiniGUI 為每個主窗口建立單獨的消息隊列,在該主窗口基礎上派生出的窗片、對話框及其控件均使用同一消息隊列。在 MiniGUI 中,每個應用對應於一個線程。理論上講,每個應用可以具備多個主窗口,但在 MiniGUI 中,主窗口均以單獨的線程實現。但多個主窗口對應單一線程的情況也是可以在 MiniGUI 中實現的。
每個應用項目有一個MiniGUIMain函數,在這個函數中,可建立初始的應用線程。在調用MiniGUIMain之前,MiniGUI 啟動自己的桌面窗口(Desktop)。桌面窗口作為 MiniGUI 的窗口管理器而存在。下面的代碼段在MiniGUIMain中啟動了三個主窗口線程:
int MiniGUIMain(int args, char* arg[])
{
pthread_t thread, thread2, thread3;
CreateThreadForMainWindow(&thread, NULL, TestWindowMain, 0);
CreateThreadForMainWindow(&thread2, NULL, TestWindowMain2, 0);
CreateThreadForMainWindow(&thread3, NULL, TestWindowMain3, 0);
return 0;
}
CreateThreadForMainWindow函數為主窗口建立線程,並返回線程標識符。
其中的第三個參數是線程的入口函數地址。如下的代碼段定義了上述代碼中第一個主窗口線程的入口函數:
void InitCreateInfo(PMAINWINCREATE pCreateInfo)
{
pCreateInfo->dwStyle = WS_THICKFRAME;
pCreateInfo->spCaption = "The first main window" ;
pCreateInfo->hMenu = 0;
pCreateInfo->hCursor = GetSystemCursor(2);
pCreateInfo->hIcon = LoadIconFromFile("res/table.ico");
pCreateInfo->MainWindowProc = TestMainWinProc;
pCreateInfo->lx = 50;
pCreateInfo->ty = 50;
pCreateInfo->rx = 300;
pCreateInfo->by = 480;
pCreateInfo->iBkColor = COLOR_lightwhite;
pCreateInfo->dwAddData1 = 0;
pCreateInfo->dwAddData2 = 0;
}
void* TestWindowMain(void* data)
{
MSG Msg;
MAINWINCREATE CreateInfo;
HWND hMainWnd;
InitCreateInfo(&CreateInfo);
if( !(hMainWnd = CreateMainWindow(&CreateInfo)) )
return NULL;
ShowWindow(hMainWnd, SW_SHOWNORMAL);
while( GetMessage(&Msg, hMainWnd) ) {
DispatchMessage(&Msg);
}
MainWindowThreadCleanup(hMainWnd);
return NULL;
}
在上面的代碼段中,該線程首先調用CreateMainWindow建立了主窗口,然后調用ShowWindow顯示了主窗口,最后啟動了消息循環。當消息循環因為接收到MSG_QUIT消息而終止時,該函數調用了MainWindowThreadCleanup清除了相關的線程數據。
從上述代碼中可看出主函數不支持窗口類,在調用 CreateMainWindow 函數時直接指定主窗口的窗口過程地址。我們也可以從中看到主窗口所支持的其他屬性:
1.窗口風格。表 2.1 給出了所支持的窗口風格
表 2.1 MiniGUI 支持的主窗口風格
風格 |
描述 |
WS_BORDER |
創建一個具有單線邊框的窗口 |
WS_THICKFRAME |
創建一個具有寬邊框的窗口 |
WS_THINFRAME |
創建一個具有細邊框的窗口 |
WS_CAPTION |
創建一個具有標題欄的窗口 |
WS_HSCROLL |
創建一個具有水平滾動條的窗口 |
WS_MAXMIZEBOX |
創建一個具有最大化框的窗口 |
WS_MINIMIZEBOX |
創建一個具有最小化框的窗口 |
WS_SYSMENU |
創建一個具有系統菜單的窗口 |
WS_VSCROLL |
創建一個具有垂直滾動條的窗口 |
WS_DISABLED |
創建一個初始為禁止的窗口 |
WS_MAXIMIZE |
創建一個初始最大化的窗口 |
WS_MINIMIZE |
創建一個初始最小化的窗口 |
WS_VISIBLE |
創建一個初始可見的窗口 |
WS_EX_TOPMOST |
創建一個頂層窗口,這是一個 Win32 的擴展風格 |
2.窗口標題。
3.窗口菜單。
4.窗口圖標。
5.窗口背景色。
在利用 MiniGUI 開發應用程序之前,首先要理解的兩個概念就是消息循環和窗口過程。消息循環是事件驅動的 GUI 編程之基礎。而窗口則是圖形用戶界面的最基本交互元素。本文描述了 MiniGUI 中與消息相關的幾個重要函數,也描述了 MiniGUI-Threads 和 MiniGUI-Lite 在消息循環實現上的幾個不同。本文還講述了在 MiniGUI 中的窗口建立和銷毀過程,並解釋了窗口過程的概念以及對一些重要消息的處理。
引言
我們知道,流行的 GUI 編程都有一個重要的概念與之相關,即"事件驅動編程"。事件驅動的含義就是,程序的流程不再是只有一個入口和若干個出口的串行執行線路;相反,程序會一直處於一個循環狀態,在這個循環當中,程序從外部輸入設備獲取某些事件,比如用戶的按鍵或者鼠標的移動,然后根據這些事件作出某種的響應,並完成一定的功能,這個循環直到程序接受到某個消息為止。"事件驅動"的底層設施,就是常說的"消息隊列"和"消息循環"。本文將具體描述 MiniGUI 中用來處理消息的幾個重要函數,並描述 MiniGUI-Threads 和 MiniGUI-Lite 在消息循環實現上的一些不同。
窗口是 MiniGUI 當中最基本的 GUI 元素,一旦窗口建立之后,窗口就會從消息隊列當中獲取屬於自己的消息,然后交由它的窗口過程進行處理。這些消息當中,有一些是基本的輸入設備事件,而有一些則是與窗口管理相關的邏輯消息。本文將講述 MiniGUI 中的窗口建立和銷毀過程,並解釋了窗口過程的概念以及對一些重要消息的處理。
2 消息和消息循環
在 MiniGUI 中,消息被如下定義(include/window.h):
352 typedef struct _MSG
353 {
354 HWND hwnd;
355 int message;
356 WPARAM wParam;
357 LPARAM lParam;
358 #ifdef _LITE_VERSION
359 unsigned int time;
360 #else
361 struct timeval time;
362 #endif
363 POINT pt;
364 #ifndef _LITE_VERSION
365 void* pAdd;
366 #endif
367 }MSG;
368 typedef MSG* PMSG;
一個消息由該消息所屬的窗口(hwnd)、消息編號(message)、消息的 WPARAM 型參數(wParam)以及消息的 LPARAM 型參數(lParam)組成。消息的兩個參數中包含了重要的內容。比如,對鼠標消息而言,lParam 中一般包含鼠標的位置信息,而 wParam 參數中則包含發生該消息時,對應的 SHIFT 鍵的狀態信息等。對其他不同的消息類型來講,wParam 和 lParam 也具有明確的定義。當然,用戶也可以自定義消息,並定義消息的 wParam 和 lParam 意義。為了用戶能夠自定義消息,MiniGUI 定義了 MSG_USER 宏,可如下定義自己的消息:
#define MSG_MYMESSAGE1 (MSG_USER + 1)
#define MSG_MYMESSAGE2 (MSG_USER + 2)
用戶可以在自己的程序中使用自定義消息,並利用自定義消息傳遞數據。
在理解消息之后,我們看消息循環。簡而言之,消息循環就是一個循環體,在這個循環體中,程序利用 GetMessage 函數不停地從消息隊列中獲得消息,然后利用 DispatchMessage 函數將消息發送到指定的窗口,也就是調用指定窗口的窗口過程,並傳遞消息及其參數。典型的消息循環如下所示:
while (GetMessage (&Msg, hMainWnd)) {
TranslateMessage (&Msg);
DispatchMessage (&Msg);
}
如上所示,GetMessage 函數從 hMainWnd 窗口所屬的消息隊列當中獲得消息,然后調用 TranslateMessage 函數將 MSG_KEYDOWN 和 MSG_KEYUP 消息翻譯成 MSG_CHAR 消息,最后調用 DispatchMessage 函數將消息發送到指定的窗口。
在 MiniGUI-Threads 版本中,每個建立有窗口的 GUI 線程有自己的消息隊列,而且,所有屬於同一線程的窗口共享同一個消息隊列。因此,GetMessage 函數將獲得所有與 hMainWnd 窗口在同一線程中的窗口的消息。
而在 MiniGUI-Lite 版本中,只有一個消息隊列,GetMessage 將從該消息隊列當中獲得所有的消息,而忽略 hMainWnd 參數。
3 幾個重要的消息處理函數
除了上面提到的 GetMessage 和 TranslateMessage、DispatchMessage 函數以外,MiniGUI 支持如下幾個消息處理函數。
PostMessage:該函數將消息放到指定窗口的消息隊列后立即返回。這種發送方式稱為"郵寄"消息。如果消息隊列中的郵寄消息緩沖區已滿,則該函數返回錯誤值。在下一個消息循環中,由 GetMessage 函數獲得這個消息之后,窗口才會處理該消息。PostMessage 一般用於發送一些非關鍵性的消息。比如在 MiniGUI 中,鼠標和鍵盤消息就是通過 PostMessage 函數發送的。
SendMessage:該函數和 PostMessage 函數不同,它在發送一條消息給指定窗口時,將等待該消息被處理之后才會返回。當需要知道某個消息的處理結果時,使用該函數發送消息,然后根據其返回值進行處理。在 MiniGUI-Threads 當中,如果發送消息的線程和接收消息的線程不是同一個線程,發送消息的線程將阻塞並等待另一個線程的處理結果,然后繼續運行;否則,SendMessage 函數將直接調用接收消息窗口的窗口過程函數。MiniGUI-Lite 則和上面的第二種情況一樣,直接調用接收消息窗口的窗口過程函數。
SendNotifyMessage:該函數和 PostMessage 消息類似,也是不等待消息被處理即返回。但和 PostMessage 消息不同,通過該函數發送的消息不會因為緩沖區滿而丟失,因為系統采用鏈表的形式處理這種消息。通過該函數發送的消息一般稱為"通知消息",一般用來從控件向其父窗口發送通知消息。
PostQuitMessage:該消息在消息隊列中設置一個 QS_QUIT 標志。GetMessage 在從指定消息隊列中獲取消息時,會檢查該標志,如果有 QS_QUIT 標志,GetMessage 消息將返回 FALSE,從而可以利用該返回值終止消息循環。
4 MiniGUI-Threads 和 MiniGUI-Lite 在消息處理上的不同
表 1 總結了 MiniGUI-Threads 和 MiniGUI-Lite 在消息處理上的不同
表 1 MiniGUI-Threads 和 MiniGUI-Lite 在消息處理上的不同
MiniGUI-Threads MiniGUI-Lite
多消息隊列 每個創建窗口的線程擁有獨立的消息隊列 只有一個消息隊列。所有窗口共享一個消息隊列。除非嵌套消息循環,否則一個程序中只有一個消息循環。
內建多線程處理 是。可以自動處理跨線程的消息傳遞 不能。從一個線程向另外一個線程發送或者郵寄消息時,必須通過互斥處理保護消息隊列。
其他 可以利用 PostSyncMessage 函數跨線程發送消息,並等待消息的處理結果 不能使用 PostSyncMessage、SendAsynMessage 等消息。
5 窗口的建立和銷毀
5.1 窗口的建立
我們知道,MiniGUI 的 API 類似 Win32 的 API。因此,窗口的建立過程和 Windows 程序基本類似。不過也有一些差別。首先我們回顧一下 Windows 應用程序的框架:
在 WinMain () 中創建窗口,使用以下步驟:創建窗口類、登記窗口類、創建並顯示窗口、啟動消息循環。
在 WndProc () 中,負責對發到窗口中的各種消息進行響應。
在 MiniGUI 中也同樣要有這兩個函數。不過稍微有點不同。程序的入口函數名字叫MiniGUIMain (),它負責創建程序的主窗口。在建立主窗口之后,程序進入消息循環。
在 Win32 程序中,在建立一個主窗口之前,程序首先要注冊一個窗口類,然后創建一個屬於該窗口類的主窗口。MiniGUI 卻沒有在主窗口中使用窗口類的概念。在 MiniGUI 程序中,首先初始化一個 MAINWINCREATE 結構,該結構中元素的含義是:
CreateInfo.dwStyle: 窗口風格
CreateInfo.spCaption: 窗口的標題
CreateInfo.dwExStyle : 窗口的附加風格
CreateInfo.hMenu: 附加在窗口上的菜單句柄
CreateInfo.hCursor: 在窗口中所使用的鼠標光標句柄
CreateInfo.hIcon: 程序的圖標
CreateInfo.MainWindowProc: 該窗口的消息處理函數指針
CreateInfo.lx: 窗口左上角相對屏幕的絕對橫坐標,以象素點表示
CreateInfo.ty: 窗口左上角相對屏幕的絕對縱坐標,以象素點表示
CreateInfo.rx: 窗口的長,以象素點表示
CreateInfo.by: 窗口的高,以象素點表示
CreateInfo.iBkColor: 窗口背景顏色
CreateInfo.dwAddData: 附帶給窗口的一個 32 位值
CreateInfo.hHosting: 窗口消息隊列所屬
其中有如下幾點要特別說明:
CreateInfo.dwAddData:在程序編制過程中,應該盡量減少靜態變量,但是如何不使用靜態變量而給窗口傳遞參數呢?這時可以使用這個域。該域是一個 32 位的值,因此可以把所有需要傳遞給窗口的參數編制成一個結構,而將結構的指針賦予該域。在窗口過程中,可以使用 GetWindowAdditionalData 函數獲取該指針,從而獲得所需要傳遞的參數。
CreateInfo.hHosting:該域表示的是將要建立的主窗口使用哪個主窗口的消息隊列。使用其他主窗口消息隊列的主窗口,我們稱為"被托管"的主窗口。當然,這只在 MiniGUI-Threads 版本中有效。
MainWinProc 函數負責處理窗口消息。這個函數就是主窗口的"窗口過程"。窗口過程一般有四個入口參數,第一個是窗口句柄,第二個是消息類型,第三個和第四個是消息的兩個參數。
在准備好MAINWINCREATE 結構之