利用現有圖形庫可以做到快速開發一個桌面程式,但當你不滿足于庫産生的控件想自己定義控件或者想了解圖形系統運作時,卻不能随心所欲。圖形庫把底層接口封裝,固然大大簡化了開發的工作量,但其也把具體實作隐與表層之下,WIN32API是作業系統留給我們的接口,直接用API進行GUI的簡單設計無疑能夠極大的貼近圖形庫運轉的底層面貌,去了解消息循環的真正面貌。
以下為MSDN的建立一個主視窗的例子:
#include <windows.h>
// Global variable
HINSTANCE hinst;
// Function prototypes.
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int);
InitApplication(HINSTANCE);
InitInstance(HINSTANCE, int);
LRESULT CALLBACK MainWndProc(HWND, UINT, WPARAM, LPARAM);
// Application entry point.
int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
MSG msg;
if (!InitApplication(hinstance))
return FALSE;
if (!InitInstance(hinstance, nCmdShow))
return FALSE;
BOOL fGotMessage;
while ((fGotMessage = GetMessage(&msg, (HWND) NULL, 0, 0)) != 0 && fGotMessage != -1)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
UNREFERENCED_PARAMETER(lpCmdLine);
}
BOOL InitApplication(HINSTANCE hinstance)
{
WNDCLASSEX wcx;
// Fill in the window class structure with parameters
// that describe the main window.
wcx.cbSize = sizeof(wcx); // size of structure
wcx.style = CS_HREDRAW |
CS_VREDRAW; // redraw if size changes
wcx.lpfnWndProc = MainWndProc; // points to window procedure
wcx.cbClsExtra = 0; // no extra class memory
wcx.cbWndExtra = 0; // no extra window memory
wcx.hInstance = hinstance; // handle to instance
wcx.hIcon = LoadIcon(NULL,
IDI_APPLICATION); // predefined app. icon
wcx.hCursor = LoadCursor(NULL,
IDC_ARROW); // predefined arrow
wcx.hbrBackground = GetStockObject(
WHITE_BRUSH); // white background brush
wcx.lpszMenuName = "MainMenu"; // name of menu resource
wcx.lpszClassName = "MainWClass"; // name of window class
wcx.hIconSm = LoadImage(hinstance, // small class icon
MAKEINTRESOURCE(5),
IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
LR_DEFAULTCOLOR);
// Register the window class.
return RegisterClassEx(&wcx);
}
BOOL InitInstance(HINSTANCE hinstance, int nCmdShow)
{
HWND hwnd;
// Save the application-instance handle.
hinst = hinstance;
// Create the main window.
hwnd = CreateWindow(
"MainWClass", // name of window class
"Sample", // title-bar string
WS_OVERLAPPEDWINDOW, // top-level window
CW_USEDEFAULT, // default horizontal position
CW_USEDEFAULT, // default vertical position
CW_USEDEFAULT, // default width
CW_USEDEFAULT, // default height
(HWND) NULL, // no owner window
(HMENU) NULL, // use class menu
hinstance, // handle to application instance
(LPVOID) NULL); // no window-creation data
if (!hwnd)
return FALSE;
// Show the window and send a WM_PAINT message to the window
// procedure.
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
return TRUE;
}
總的來說建立一個主視窗大緻分為三個部分:1.注冊視窗類2.建立主視窗3.建立消息循環。
一。注冊視窗類
這裡有一個結構體WNDCLASSEX:
typedef struct tagWNDCLASSEX {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX, *PWNDCLASSEX;
這個結構體包含了視窗類的資訊,被用于 RegisterClassEx (即注冊視窗類的函數)和GetClassInfoEx這兩個函數。這個結構體比較重要的一個參數是lpfnWndProc,lpfn即long pointer to function,是一個指向函數的長指針,指向視窗的處理函數。
在Window Procedures的使用中,MSDN給出了其例子:
LRESULT CALLBACK MainWndProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam) // second message parameter
{
switch (uMsg)
{
case WM_CREATE:
// Initialize the window.
return 0;
case WM_PAINT:
// Paint the window's client area.
return 0;
case WM_SIZE:
// Set the size and position of the window.
return 0;
case WM_DESTROY:
// Clean up window-specific data objects.
return 0;
//
// Process other messages.
//
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
注冊視窗類的函數為RegisterClassEx :
ATOM WINAPI RegisterClassEx(
_In_ const WNDCLASSEX *lpwcx
);
它是以指向 結構體WNDCLASSEX的指針為參數的。
二。建立主視窗
建立主視窗用的是CreatWindows函數,傳回的是指向視窗的指針。調用ShowWindow就可以顯示主視窗了
三。建立消息循環
一般消息循環建立如下:
/* Run the message loop. It will run until GetMessage() returns 0 */
while (GetMessage (&messages, NULL, 0, 0))
{
/* Translate virtual-key messages into character messages */
TranslateMessage(&messages);
/* Send message to WindowProcedure */
DispatchMessage(&messages);
}
GetMessage不斷抓取消息,轉換後分派給相應的視窗處理函數(視窗過程Window Procedure)。
每當使用者移動滑鼠,單擊滑鼠按鈕或敲擊鍵盤時,滑鼠或鍵盤的裝置驅動程式将輸入轉換為消息并将其放置在系統消息隊列中。系統從系統消息隊列中删除一個消息,檢查它們以确定目标視窗,然後将其釋出到建立目标視窗的線程的消息隊列中。線程的消息隊列接收線程建立的視窗的所有滑鼠和鍵盤消息(GetMessage)。該線程從其隊列中删除消息,并引導系統将其發送到适當的視窗過程(TranslateMessage DispatchMessage)進行處理。
-------------------------------------------------------我是帥氣的分割線--------------------------------------------------------------------------
上面簡單說了建立一個主視窗,但我想要的是消息是如何循環的。現在知道的是程式會将消息隊列中的消息一條條抓取并分派到相應視窗的處理函數讓其對這條消息進行處理,我現在想知道的是消息的産生及分派:消息如何産生,并且還能标記出要發往的視窗,是作業系統完成了這一切嗎?還有就是消息如何分派,是抓取之後直接傳給控件所在主視窗(假設按下了主視窗的一個按鈕),還是說傳給了按鈕,再由按鈕把消息給主視窗?下面進行實驗:
建立按鈕:
hwndButton=CreateWindow(
"BUTTON", // Predefined class; Unicode assumed
"OK", // Button text
WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON, // Styles
10, // x position
10, // y position
100, // Button width
100, // Button height
hwnd, // Parent window
(HMENU)520, // No menu.
(HINSTANCE)GetWindowLong(hwnd, GWL_HINSTANCE),
NULL); // Pointer not needed.
主視窗的Window Procedures:
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) /* handle the messages */
{
case WM_DESTROY:
PostQuitMessage (0); /* send a WM_QUIT to the message queue */
break;
case WM_COMMAND:
if (LOWORD(wParam) == 520 && HIWORD(wParam) == BN_CLICKED)
{
MessageBox(hwnd, TEXT("22222"), TEXT("11111"), MB_OK);
}
break;
default: /* for messages that we don't deal with */
return DefWindowProc (hwnd, message, wParam, lParam);
}
return 0;
}
其中WM_COMMAND即為處理按鍵按下消息的處理分支。按鍵能正常工作,如下:
下面擷取按鈕的處理函數并設定新的處理函數:
SetWindowLong函數傳回一個指向原始視窗過程的指針; 使用此指針将消息傳遞到原始過程。子類視窗過程必須使用CallWindowProc函數調用原始視窗過程。
LRESULT CALLBACK ButtonWinProc(HWND, UINT, WPARAM, LPARAM);//新的按鍵回調函數
WNDPROC OldButtonwinProc;//儲存舊回調函數的指針
OldButtonwinProc=(WNDPROC)SetWindowLong(hwndButton,GWL_WNDPROC,(LONG)ButtonWinProc);//設定新的回調函數儲存舊的
LRESULT CALLBACK ButtonWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)//新的回調函數
{
if(message==WM_LBUTTONDOWN) cout<<"down ";
if(message==WM_LBUTTONUP) cout<<"up ";
if(message==BM_CLICK) cout<<"click ";
switch (message) /* handle the messages */
{
default:
return CallWindowProc(OldButtonwinProc,hwnd,message,wParam,lParam);//預設調用原有處理函數
}
}
在WinMain裡發送模拟按鍵點選的消息給按鍵:
SendMessage(hwndButton,BM_CLICK,520,0);
主視窗回調函數如下:
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) /* handle the messages */
{
case WM_DESTROY:
PostQuitMessage (0); /* send a WM_QUIT to the message queue */
break;
case WM_COMMAND:
if (LOWORD(wParam) == 520 && HIWORD(wParam) == BN_CLICKED)
{
cout<<"inmainframe ";
}
break;
default: /* for messages that we don't deal with */
return DefWindowProc (hwnd, message, wParam, lParam);
}
return 0;
}
結果如下:
而如果按鍵的回調函數改為如下:
LRESULT CALLBACK ButtonWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if(message==WM_LBUTTONDOWN) cout<<"down ";
if(message==WM_LBUTTONUP) cout<<"up ";
if(message==BM_CLICK) cout<<"click ";
switch (message) /* handle the messages */
{
default:;
//return CallWindowProc(OldButtonwinProc,hwnd,message,wParam,lParam);
}
}
則隻會列印click。
這就能夠有一定可信度的說明,當按鍵處理函數接到BM_CLICK後,會給按鍵發送WM_LBUTTONDOWN和WM_LBUTTONUP,然後按鍵給主視窗發送了按鍵點選的消息,也就完成了一次按鍵點選的模拟。另外類似BM_CLICK中的M表示向控件發送的消息,也就是指令控件做事情,而類似BN_CLICKED中的N則表明是控件向外部(父視窗)發出的的通知,表示自己的狀态。
再将WinMian中消息循環改為如下:
while (GetMessage (&messages, NULL, 0, 0))
{
/* Translate virtual-key messages into character messages */
TranslateMessage(&messages);
/* Send message to WindowProcedure */
if(messages.hwnd==hwndButton&&messages.message==WM_LBUTTONDOWN) cout<<"111";
if(messages.hwnd==hwnd&&messages.message==WM_COMMAND) cout<<"222";
DispatchMessage(&messages);
}
結果為:
這說明按鍵按下确實是WinMain循環裡抓取并直接送往按鍵的,而按鍵則是負責按鍵點選消息的發送,且不會再次進入WinMain消息循換了。
由此可總結,作業系統将各種消息放入應用程式隊列,而應用程式抓取消息驅動整個程式一步步響應。一次點選按鍵,WinMain裡面抓取并派送給按鍵滑鼠左鍵按下與擡起這兩個消息,而後按鍵把自身“點選”這個狀态發送給父視窗,進而使父視窗能夠處理滑鼠點選事件。至于消息的産生,如何标記出要發往的視窗?誰标記的?這些還有待探索。。。。