上一篇文章讲解了Windows消息机制的原理,这一篇文章讲一点实际,如何编程使用消息和操控消息队列。本文属原创,知识点和部分演示代码来源于微软的MSDN,同时也发在我CSDN个人博客上,特此申明。
消息和消息队列的使用
创建消息循环
首先系统会自动的为每一个线程创建消息队列。如果线程有一个或者多个窗口,那么线程就必须创建消息循环。消息循环的功能就是要从消息队列中取出消息,然后把这些消息发给对应的窗口过程函数。
对于大部分程序来说,都是单线程,因此在WinMain函数中首先创建一个窗体,然后才开始消息循环。消息循环中一般会用到GetMessage和DispatchMessage函数。如果你在应用程序中想获得字符输入的话,那么还需要用到TranslateMessage函数。
请注意,在窗口过程函数中一定要调用DefWindowProc函数,否则创建窗口都可能会失败的。
例子1:下面的代码展示了一个简单的消息循环的创建过程。
#include <Windows.h>
HINSTANCE hinst;
HWND hwndMain;
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
switch(Msg)
{
case WM_CLOSE:
// Perform cleanup tasks.
PostQuitMessage(WM_QUIT);
return 0;
default:
return DefWindowProc(hWnd,Msg,wParam,lParam);
}
return 0;
}
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpszCmdLine, int nCmdShow)
{
MSG msg;
BOOL bRet;
WNDCLASS wc;
UNREFERENCED_PARAMETER(lpszCmdLine);
// Register the window class for the main window.
if (!hPrevInstance)
{
wc.style = 0;
wc.lpfnWndProc = (WNDPROC) WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon((HINSTANCE) NULL,
IDI_APPLICATION);
wc.hCursor = LoadCursor((HINSTANCE) NULL,
IDC_ARROW);
wc.hbrBackground =(HBRUSH) GetStockObject(WHITE_BRUSH);
wc.lpszMenuName =TEXT("MainMenu");
wc.lpszClassName =TEXT("MainWndClass");
if (!RegisterClass(&wc))
return FALSE;
}
hinst = hInstance; // save instance handle
// Create the main window.
hwndMain = CreateWindow(TEXT("MainWndClass"),TEXT( "Sample"),
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, (HWND) NULL,
(HMENU) NULL, hinst, (LPVOID) NULL);
// If the main window cannot be created, terminate
// the application.
if (!hwndMain)
return FALSE;
// Show the window and paint its contents.
ShowWindow(hwndMain, nCmdShow);
UpdateWindow(hwndMain);
// Start the message loop.
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// Return the exit code to the system.
return msg.wParam;
}
如果要使用快捷键和无模式对话框,那么还需要用到TranslateAccelerator 或者IsDialogMessage 函数。
那么对应的消息循环应该如例2所示。
例2 采用快捷键和无模式对话框的消息循环
HWND hwndMain;
HWND hwndDlgModeless = NULL;
MSG msg;
BOOL bRet;
HACCEL haccel;
//
// Perform initialization and create a main window.
//
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
if (hwndDlgModeless == (HWND) NULL ||
!IsDialogMessage(hwndDlgModeless, &msg) &&
!TranslateAccelerator(hwndMain, haccel,
&msg))
这一句的意思是,如果取回的消息是发给对话框的,那么就用IsDialogMessage函数专门进行处理,处理完毕之后,就不需要再分发消息了。
如果消息里面有自己定义的快捷键,那么就用TranslateAccelerator函数进行专门的快捷键处理,处理完毕之后,这两个函数都会默认直接调用窗口过程函数进行处理,所以这两个函数处理完毕之后,都不需要在转换消息和分发消息了。
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
因此可以写成更好懂的一种形式:
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
if(IsDialogMessage(hwndDlgModeless, &msg))continue;
if(TranslateAccelerator(hwndMain, haccel,&msg)) continue;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
查询消息队列
有时候,应用程序可能需要在线程消息循环之外查询一下线程消息队列的内容。例如,当应用程序的窗口正在进行某个所需要时间很长的绘制工作的时候,你可能想让用户能够打断这个操作。除非你的应用程序在操作过程中定期的查询消息队列消息里面是否有鼠标和键盘的消息,否则的话直到长时间的绘制工作结束之前,应用程序是不会响应任何鼠标和键盘动作的。导致这个问题的原因就是线程消息循环中的DispatchMessage函数是要等到窗口过程处理完消息才会返回的。而因为处理上个消息需要长时间的绘制,因此消息循环一直等着,导致了这种情况的发生。
遇到长时间绘制操作过程中,你可以使用PeekMessage函数来查询消息队列。PeekMessage和GetMessage功能类似,都是从消息队列中取出消息并且复制到一个MSG结构中。两个函数最主要的不同是,GetMessage一直要找到消息队列中匹配过滤条件的消息才会返回,而PeekMessage是马上返回,不管有没有匹配的消息。
例子1:
HWND hwnd;
BOOL fDone;
MSG msg;
// Begin the operation and continue until it is complete
// or until the user clicks the mouse or presses a key.
fDone = FALSE;
while (!fDone)
{
fDone = DoLengthyOperation(); // application-defined function
// Remove any messages that may be in the queue. If the
// queue contains any mouse or keyboard
// messages, end the operation.
while (PeekMessage(&msg, hwnd, 0, 0, PM_REMOVE))
{
switch(msg.message)
{
case WM_LBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_KEYDOWN:
//
// Perform any required cleanup.
//
fDone = TRUE;
}
}
}
在这个案例中,不停用PeekMessage取出消息,判断是否是鼠标或者按键消息,如果是,就把fDone=TRUE,知道消息队列中没有鼠标键盘消息的时候,循环退出,这个时候由于fDone已经是TRUE了,所以外面的大循环也开始退出,从而达到了取消长时间动作的效果。
投递消息
要投递消息,使用PostMessage函数,这个函数会把参数COPY到一个MSG结构中,还会自动给你填写时间和鼠标位置,然后把这个数据COPY到线程消息队列的末尾。这个函数投递了消息,立刻返回,不会等待消息被处理。
你还可以使用PostThreadMessage函数给指定的线程投递消息。这个函数和PostMessage功能类似,只是窗体句柄要换成线程标识符,要获得当先线程的线程标识符,可以使用GetCurrentThreadId函数。
使用PostQuitMessage函数来推出消息循环。这个函数会投递一个WM_QUIT消息给当前正在执行的线程。线程的消息循环一旦遇到WM_QUIT就会终止循环,把控制权返回给系统。例如,应用程序要响应WM_DESTROY或者WM_CLOSE消息时候,通常都会发出一个WM_QUIT消息给线程消息循环,以便于消息循环终止。
例子:
case WM_DESTROY:
// Perform cleanup tasks.
PostQuitMessage(0);
break;
这段代码演示的是对WM_DESTROY消息的处理过程。可以看到里面发出了一个WM_QUIT消息。
发送消息
发送消息一般用SendMessage函数,这个函数是直接把消息发给窗口过程函数,不会进入消息队列。这个函数发送了消息之后,会一直等到窗口过程函数处理完消息之后才会返回。消息可以发送给任何窗口,这个函数所需要的就是窗口句柄,这个东西决定消息发给哪个窗口过程函数。
在处理其他线程发来的消息之前,窗口过程函数应该先调用InSendMessage函数确定对方是不是在等待回应。如果这个函数返回TRUE,就表示对方确实在等待,那么在让出控制权之前,应该先调用ReplyMessage函数应答对方,让对方继续执行。然后在处理收到的消息也不迟。这主要是为了避免消息的死锁。
例子:
case WM_USER + 5:
if (InSendMessage())
ReplyMessage(TRUE);
DialogBox(hInst, "MyDialogBox", hwndMain, (DLGPROC) MyDlgProc);
break;
看看这段代码,是位于某个窗口过程函数中的。当收到WM_USER+5自定义消息的时候,首先调用InSendMessage判断对方是否在等待,如果是就调用ReplyMessage进行应答,因为接下来马上就会调用对话框里,而这个是可能导致线程让出控制权的,因此在让出控制权之前一定要先应答其他线程发来的消息,这样能避免死锁!
一些消息可以被发送到对话框中的控件上。这些控件消息设置外观,行为,和控件的内容或者获得有关控件的信息,例如CB_ADDSTRING消息能添加一个字符串到一个组合框中。BM_SETCHECK消息可以设置一个复选框或者单选框的选择状态。
使用SendDlgItemMessage函数发送消息给控件,指定控件标识符和对话框窗口句柄就行。
例子:这个例子来自一个对话框窗口过程函数,实现从组合框的编辑控件中复制一个字符串到列表框的功能,这个例子中使用了SendDlgItemMessage函数发送了一个CB_ADDSTRING消息到组合框。
HWND hwndCombo;
int cTxtLen;
PSTR pszMem;
switch (uMsg)
{
case WM_COMMAND: //当收到命令消息的时候,就判断消息参数wParam的低字节,里面存放的是发出命令按钮标识符。或者菜单标识符。
switch (LOWORD(wParam))
{
case IDD_ADDCBITEM:
hwndCombo = GetDlgItem(hwndDlg, IDD_COMBO); //这是为了获得组合框控件的窗口句柄
cTxtLen = GetWindowTextLength(hwndCombo); //获得组合框控件的文本长度
pszMem = (PSTR) VirtualAlloc((LPVOID) NULL, //为字符串分配内存
(DWORD) (cTxtLen + 1), MEM_COMMIT,
PAGE_READWRITE);
GetWindowText(hwndCombo, pszMem, //将字符串存在刚才分配好的内存中
cTxtLen + 1);
if (pszMem != NULL)
{
SendDlgItemMessage(hwndDlg, IDD_COMBO, //给组合框发送一个CB_ADDSTRING消息,通知对方添加文本到列表框中。
CB_ADDSTRING, 0,
(DWORD) ((LPSTR) pszMem));
SetWindowText(hwndCombo, (LPSTR) NULL); //设置组合框编辑控件的文本为NULL
}
// Free the memory and return.
VirtualFree(pszMem, 0, MEM_RELEASE); //释放刚才申请的内存
return TRUE;
//
// Process other dialog box commands.
//
}
//
// Process other dialog box messages.
//
}
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)