本文说明两个问题:
1.windows的消息处理机制;
2.怎么往SetTimer的回调函数传递参数。
首先看第一个问题,我们都知道windows是消 息驱动的,windows呈现给用户的任何可以看到听到的东西几乎都是消息驱动的,在底层windows为每个线程准备了一个消息队列,如果用户线程注册 了某个消息,那么在适当的时候windows就会将消息投递到该线程的消息队列中,然后由该线程取出队列中的消息,然后处理之,这个过程有两个参与者,一 个是windows系统,它主要负责投递消息,收不收是用户线程的事,另一个就是用户线程,它主要负责取出消息并处理消息,即使用户线程因为睡眠或者根本就没有设定消息循环,系统还是会投递的,系统和用户线程的消息接口就是消息队列,这就在用户和系统之间关于消息解除了耦合,在用户线程处理消息的时候,其实还有一个消息队列,因为一个线程不一定只接收一种消息而且不一定马上就能处理完并返回,这个消息队列我们把它叫做消息分发队列或者简称分发队列用来与系统的消息队列区分,注意分发队列里面的消息都是已经格式化后的消息,分发给谁呢?当然是分发给消息的回调函数了,对于有窗口的就是先分发给窗口过程,然后 由窗口过程分发给具体的处理函数。
下面我们来通过一个例子说明一下,用vs2005或VC建立一个Win32工程,然后看自动生成的代码:
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
// 主消息循环:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);//msg中按照消息号识别
}
}
return (int) msg.wParam;
}
以上就是消息循环,该线程循环接收消息,然后DispatchMessage消息,Dispatch到窗口过程:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
switch (message)//message就是消息号
{
case WM_COMMAND:
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
以上实际上就是windows消息机制的全景,对于windows的timer当然也要套用上面的模式了,在SetTimer调用后,实际上就注册了WM_TIMER消息,以下是函数定义:
UINT_PTR SetTimer(
HWND hWnd,
UINT_PTR nIDEvent,
UINT uElapse,
TIMERPROC lpTimerFunc
);
lpTimerFunc就是回调函数,其形式为:
VOID CALLBACK TimerProc(
HWND hwnd,
UINT uMsg,
UINT_PTR idEvent,
DWORD dwTime
);
SetTimer的参数uElapse就是时间间隔,比如设置为1000即1秒,现在有了一个问题,请看下列代码:
VOID CALLBACK TimerFunc(HWND hwnd,UINT uMsg,UINT_PTR idEvent,DWORD dwTime)
{
Sleep(5000);
}
DWORD CALLBACK AutoBakup( PVOID lpParam )
{
MSG msg;
UINT id=SetTimer(NULL,1,1000,TimerFunc);
BOOL bRet;
while( ((bRet = GetMessage(&msg,NULL,0,0))!= 0) )
{
if(bRet==-1)
{
break;
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
KillTimer(NULL,id);
return 0;
}
竟然在timer的处理函数中睡眠了,那么SetTimer时的1000毫秒触发一次timer回调还会进行吗?其实不会进行了,本质上那个1000毫秒就 不是说触发回调函数的间隔,而是产生WM_TIMER消息的间隔,因为回调函数中睡眠了,所以也就阻塞了本线程,这个线程就不再往前走了,但是底层的 WM_TIMER消息也会因此而不再投递吗?不会,消息的投递其实不是本线程进行的,而是系统进行的,本线程已经睡眠了,但是系统却没有睡眠,它会继续往该线程的消息队列投递WM_TIMER消息,只不过这些消息不再由该线程的GetMessage取出了,因为它睡眠了(如果对回调函数是否和 GetMessage是否为统一线程有疑义,那么用GetCurrentThreadId()检测一下就好),消息全部堆积在队列里面,然后等待睡眠结束后再一个一个处理。好吧,这个问题解决了,下一个问题又来了,试着将回调函数改为下面的:
VOID CALLBACK TimerFunc(HWND hwnd,UINT uMsg,UINT_PTR idEvent,DWORD dwTime)
{
MessageBoxA(NULL,"zhaoya","msg",MB_OK);
Sleep(5000);
}
发现完全按照SetTimer时设置的那样,1秒弹出一个消息框,一个一个出来,Sleep好像根本就没有执行,线程根本没有睡眠阻塞,我把 MessageBoxA(NULL,"","",MB_OK)换成printf就不行了,线程立即执行Sleep,这是为何?关键就在 MessageBoxA上,它实际上是一个模态对话框,既然是对话框它就有消息循环,它并没有开独立的线程,因此可以肯定还是原来的线程,原来 MessageBoxA的内部实现了GetMessage-->TranslateMessage-->DispatchMessage的循环,由于还是原来的线程,所以它GetMessage将还包括WM_TIMER,另外还有消息框自己的一些关于界面的消息,比如WM_PAINT,消息框 的出现只是在系统中又注册了一些需要的消息,就是消息框的关于界面事件的消息。WM_TIMER回调函数在那,于是调用之,于是又是一个消息框出来了,但 是一旦你点击最上面的OK键,那么程序立即向下进行,进入Sleep,开始睡眠,所有的消息框好像死掉一般,因为就一个线程,它睡眠了,消息循环不再进行,当然所有消息框的消息循环也不再进行,什么WM_PANIT之类的消息都将阻塞,于是消息框们都和死了一样。
通过以上论述,我想关于windows消息大致已经说清了,下面解决第二个问题,如何向SetTimer的回调函数传递自定义参数。再看TimerProc:
VOID CALLBACK TimerPro(HWND hwnd,UINT uMsg,UINT_PTR idEvent,DWORD dwTime);
看看MSDN关于第三个参数idEvent的解释,就是Timer的id,我们可以用SetTimer的返回值来作为自定义参数的指针,然后在 TimerProc回调函数中通过idEvent取出,强制转换为自己定义的类型,可是SetTimer的返回值并不是我们所能左右的啊,那么还是要从 MSG结构下手了:
typedef struct tagMSG { // msg
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
看看wParam和lParam,MSDN上说是额外参数,我们只需要重新设置值就可以了,于是我们要取wParam和lParam这两个参数与 TimerProc形参的交集,这个交集就是我们可以自定义的参数,经过测试,发现wParam其实就是SetTimer返回的id,也就是 TimerProc的形参idEvent,于是例子如下:
VOID CALLBACK TimerFunc(HWND hwnd,UINT uMsg,UINT_PTR idEvent,DWORD dwTime)
{
char * buf = (char*)idEvent;
printf( "%s\n", buf );//这里打印的就是"abcde"
}
DWORD CALLBACK AutoBakup( PVOID lpParam )
{
char * buf = "abcde";
MSG msg;
UINT id=SetTimer(NULL,1,1000,TimerFunc);
BOOL bRet;
while( ((bRet = GetMessage(&msg,NULL,0,0))!= 0) )
{
if(bRet==-1)
{
break;
}
else
{
TranslateMessage(&msg);
msg.wParam = (WPARAM)buf;//这里设置参数
DispatchMessage(&msg);
}
}
KillTimer(NULL,id);
return 0;
}
如果你说,这么把idEvent占了的话,真正的timer的id不就得不到了吗?唉,晕!如果能传递一个指针那么就没有设呢没不能传递了,X位机器上的X 位指针是可以指遍整个虚拟内存的,你可以把自定义参数包装成一个结构,该结构的一个字段指向真正的timer的id,一切..