27.4 唤醒一个线程
27.4.1 线程的挂起与唤醒
(1)当线程调用GetMessage或WaitMessage,而消息队列中又没有消息出现时,线程会被挂起。
(2)当消息被“Post”(也可以是线程间的“Send”)到消息队列时,相应的Wake标志位会被设置,以表明该线程可被调度。
27.4.2 查询队列的状态:DWORD GetQueueStatus(UINT fuFlags)
(1)fuFlags参数,可以是下列标志或组合,表示要查询的哪个(些)标志位的状态
要查询的标志位 |
队列中的消息 |
QS_KEY |
如果有一条WM_KEYUP、WM_KEYDOWN、WM_SYSKEYUP或WM_SYSKEYDOWN在消息队列中,则该标志被设置。 |
QS_MOUSEMOVE |
一条WM_MOUSEMOVE在“Input -Queue”队列中(只要队列中存在一个未处理的WM_MOUSEMOVE消息,这个标志被设置,当最后一条WM_MOUSEMOV消息被从队列删除时,标志被关闭)。 |
QS_MOUSEBUTTON |
一条鼠标消息(如WM_?BUTTON*消息等) |
QS_MOUSE |
相当于QS_MOUSEMOVE|QS_MOUSEBUTTON的组合 |
QS_INPUT |
相当于QS_MOUSE|QS_KEY的组合 |
QS_PAINT |
一条WM_PAINT消息(该线程创建的窗口中,只要窗口存在无效区时,该位被设置。只有当线程建立的所有窗口都有效时,这个标志才关闭)。 |
QS_TIMER |
一条WM_TIMER消息(当定时器报时,QS_TIMER被设置,当GetMessage返回WM_TIMER消息后,该标志被关闭) |
QS_HOTKEY |
一条WM_HOTKEY消息 |
QS_POSTMESSAGE |
post的消息(不同于硬件输入事件)。如果队列在期望的消息过滤范围内没有post的消息,该标志被关闭。只要Posted-Message队列中有一条消息,该标志就被设置。 |
QS_ALLPOSTMESSAGE |
Post的消息(不同于硬件输入事件)。如果Posted-Message队列中完全没有消息,这个标志被清除。 |
QS_ALLEVENTS |
相当于QS_INPUT|QS_POSTMESSAGE|QS_TIMER|QS_PAINT|QS_HOTKEY |
QS_QUIT |
调用了PostQuitMessage函数,该标志被设置,但系统并不向线程添加WM_QUIT消息。(这个标志是未公开的,只供系统内部使用,GetQueueStatus不返回这个标志的状态) |
QS_SENDMESSAGE |
由另一个线程发送的消息,即Send-Message队列中有消息时被设置,没有时被清除。 |
(2)返回值:
①高位字表示队列里当前消息的类型,即如果调用GetMessage将取出来的那条消息。(如WM_SIZE、WM_DESTROY等)。但这与fuFlags指定的查询状态有关。它总是返回是所想要的标志集的子集。例如,对下面的调用:BOOL fPaintMsgWaiting = HIWORD(GetQueueStatus(QS_TIMER)) & QS_PAINT; fPaintMsgWaiting的值总是FALSE,不论队列中是否有一个WM_PAINT消息在等待,因为GetQueueStatus的参数中没有将QS_PAINT指定为一个标志。
②低位字表示下个消息的类型,即上次调用GetQueuestatus,GetMessage或PeekMessBge以来加入队列并仍然在队列里的消息的类型。
③QS_标志出现在返回值里,并不保证以后调用函数GetMessage或PeekMessage会返回一个消息。GetMesssge和PeekMesssge执行某些内部过滤会导致消息被内部处理。因此,GetQueueStatus的返回值只能被看作是否调用GetMessage或PeekMessage的提示。
27.4.3 从线程的队列中提取消息的算法
(1)调用GetMessage(或PeekMessage)获取下一条消息的算法过程
①如果QS_SENDMESSAGE标志被设置,系统向相应的窗口发送消息。Get/PeekMessage函数在内部进行这种处理,并且在窗口过程处理完消息之后不返回到线程。相反,Get/PeekMessage函数会等待其他消息以便处理(可以是任何的消息,包括本线程或其他线程send或post过来的消息)。
②系统查看“posted-message”队列,如果“posted-message”队列有消息,Get/PeekMessage会填充MSG结构体,然后返回。在线程的消息循环中通常会调用DispatchMessage,将这个消息分派到相应的窗口过程中去处理。
③如果QS_QUIT标志被设置,Get/PeekMessage会返回一条WM_QUIT消息(在消息的wParam参数中指出了退出代码),并关闭QS_QUIT标志。
④如果在线程的虚拟“Input-Queue”有消息,Get/PeekMessage会返回硬件输入消息(如键盘或鼠标消息)。
⑤如果QS_PAINT标志被设置,Get/PeekMessage会返回一条WM_PAINT消息给相应的窗口。
⑥如果QS_TIMER标志被设置,Get/PeekMessage会返回一条WM_TIMER消息。
(2)为什么 “posted-message”队列在“虚拟Input-Queue”队列之前被检查?
①由于某个硬件事件可能引起其他的软件事件,所以系统要等用户读取下一个硬件事件之前,先处理这些软件事件。
②比如,调用TranslateMessage函数。这个函数会检查是否有一个WM_KEYDOWN(或WM_SYSKEYDOW硬件消息)从“Input-Queue”队列中被取出。如果有,系统会检查虚键信息能否转化为等价的字符,当可以转换时TranslateMessage会调用PostMessage将一个WM_CHAR(或WM_SYSCHAR)的软件消息投递到“posted-message”队列。下次调用GetMessage时,系统首先会检查“posted-message”队列,将这个WM_CHAR(或WM_SYSCHAR)消息取出。直到这个队列为空,再去检查“input-queue”队列。所以才会生成WM_KEYDOWN→WM_CHAR→WM_KEYUP这样的消息序列。
(3)为什么QS_QUIT的检测在检查“post-message”队列之后。原因有两个:
使用QS_QUIT标志可以让线程在结束消息循环前,处理完所有“posted-message”队列中的消息,比如下面代码:
case WM_CLOSE: PostQuitMessage(0);//虽然比后面的PostMessage更早调用,但不会更早退出! PostMessage(hwnd,WM_USER,0,0);// 会先取出“posted-message”队列中的 //WM_USER,队列空时再检查QS_QUIT标志。 Break;
(4)为什么PostQuitMessage只设置QS_QUIT而不将WM_QUIT消息投递到“posted-message”队列?
①调用PostQuitMessage类似于(但不相同)调用PostThreadMessage(,WM_QUIT,…)。后者会将消息添加到“posted-message”队列的尾端,并使消息在系统检查“input-queue”前被处理。但前者只会设置QS_QUIT标志位,而不会将WM_QUIT消息放入“posted-queue”队列。因为当低内存时,post一个消息到“posted-message”可能会失败,如果一个程序想退出,即使是低内存时也应该被允许,但postQuitMessage函数调用不会失败(其返回值为void),因为它只改变QS_QUIT标志。
②尽管PostQuitMessage不会向消息队列投递WM_QUIT消息,但当系统检测到QS_QUIT标志被设置时,会先填充MSG结构体,将uMsg字段设为WM_QUIT,然后设置GetMessage的返回值为FALSE。由于系统内部自动填充了这个带有WM_QUIT信息的MSG结构体,让人感觉GetMessage好象从posted-message队列取出了一条WM_QUIT消息。但实际上这条消息并不是从“posted-message”队列中取出的,而是系统伪造的一个MSG结构体。
(5)WM_PAINT优先级低于键盘消息,WM_TIMER的优先级比WM_PAINT更低(防止用定器时在短时间内重复的WM_PAINT)。
(6)Get/PeekMessage只检查唤醒标志和调用线程的消息队列。这意味不能从其他线程的消息队列中取得消息,包括同一进程内的其他线程的消息。
27.4.4 利用内核对象或队列状态标志唤醒线程(与UI相关或无关的线程)
(1)当内核对象触发或某种消息到达时唤醒线程,既可以兼顾让线程运行一个长时间的操作时,又可以响应界面操作,防止界面出现一种“假死”的现象。
(2)MsgWaitForMultipleObjects(Ex)函数
参数 |
描述 |
DWORD nCount |
要等待的句柄的数量 |
PHANDLE phObjects |
内核对象的句柄数组 |
BOOL fWaitAll |
该参数MsgWaitForMultipleObjects才有的。 TRUE表示所有事件(内核对象和指定消息)触发;FALSE表示内核对象或消息任一被触发时,线程被唤醒。 |
DWORD dwMilliseconds |
要等待的毫秒数 |
DWORD dwWakeMask |
何时触发事件,即要等待哪些消息到达,与GetQueueStatus的参数一致。带QS_前缀的一个或多个常数。 |
DWORD dwFlags |
该参数MsgWaitForMultipleObjectsEx才有。可以是以下标志的组合,如果不要指定任何标志,可以设为0 ①MWMO_WAITALL:等待所有内核对象触发且指定消息到达。如果不使用该标志,则只有任一内核对象触发或消息到达,线程就被唤醒。 ②MWMO_ALERTABLE:让线程处于可警告状态 ③MWMO_INPUTAVAILABLE:只要队列中有指定的消息就被唤醒(不一定要等指定的、新的消息到达才唤醒) |
返回值 |
内核对象:WAIT_OBJECT_0~WAIT_OBJECT_nCount-1 消息触发:WAIT_OBJECT_nCount 超时:WAIT_TIMEOUT …… |
(3)使用MsgWaitForMultipleObjects(Ex)的几个注意点
①nCount的最大值为MAXIMUM_WAIT_OBJECTS-1,即63。
②如果fWaitAll为FALSE,当任一内核对象或指定的消息到达时,函数会立即返回。如果fWaitAll为TRUE时,函数必须等到所有内核对象被触发且指定的消息到达时才返回。
③默认下,当调用这两个函数时,它们都会检查是否有指定的新的消息的到来,比如现在队列是有两个键盘消息,如果现在用带QS_INPUT标志的参数调用MsgWaitForMultipleOjbects(Ex)函数,线程会被唤醒。当第1个键盘消息被取出并处理完后,如果继续调用MsgWaitForMultipleOjbects(Ex)函数,则线程会挂起。此时虽然队列中还有一个键盘消息,但他不是新的。为了解决这个问题,在MsgWaitForMultipleOjbectsEx函数中增加了一个MWMO_INPUTAVAILABLE标志,只要队列中有指定消息,函数就立即返回,但这个标志只能在MsgWaitForMultipleOjbectsEx中使用。
【使用MsgWaitForMultipleOjbectsEx的消息循环】
BOOL fQuit = FALSE; //是否要退出消息循环 while(!fQuit){ //当内核对象触发或UI消息到达时唤醒线程 DWORD dwResult = MsgWaitForMultipleObjectsEx(1,&hEvent, INFINITE,QS_ALLEVENTS,//所有事件 MWMO_INPUTAVAILABLE); //只有消息队列还有消息,就返回 switch(dwResult) { case WAIT_OBJECT_0://内核对象被触发 break; case WAIT_OBJECT_0+1://消息出现在消息队列中 //分派消息 MSG msg; //与GetMessage不同,PeekMessage的返回值表示有无获取到消息。 //GetMessage:如果函数取得WM_QUIT之外的其他消息,返回非零值. // 如果函数取得WM_QUIT消息,返回值是零 //PeekMessage:TRUE表示获取到一条消息。FALSE表示队列中无消息 while (PeekMessage(&MSG,NULL,0,0,PM_REMOVE)) { if (msg.message == WM_QUIT) { fQuit = TRUE;//退出消息循环 }else{ //翻译和分派消息 TranslateMessage(&msg); DispatchMessage(&msg); } }//队列己空 break; } }//循环结束
27.5 利用窗口消息在进程之间传送数据
27.5.1 WM_SETTEXT/WM_GETTEXT消息的发送过程
(1)WM_SETTEXT消息的处理过程
①当调用SendMessage时,函数会检查是否要发送WM_SETTEXT消息。如果是,就将以零结尾的字符串从调用进程的地址空间放入到一个内存映像文件(可在进程间共享)。
②然后再发送消息到共他进程的线程。
③当接收线程己准备好处理WM_SETTEXT消息时,它在自己的地址空间中找到上述内存映射文件(该文件包含要发送的文本),并让lParam指向那个文本,然后消息被分派到指定的窗口过程去进行处理。
④处理完消息之后,内存映像文件被删除。
(2)WM_GETTEXT消息的处理过程
①SendMessage时,系统检测到WM_GETTEXT消息时,实际上会发送两个消息。首先系统向那个窗口发送WM_GETTEXTLENGTH消息,以获得文本的长度。
②系统利用这个长度创建一个内存映像文件,用于在两个进程间共享。然后再发送消息来填充它。
③然后转到调用SendMessage的进程,从内存映像文件中复制文本到指定的缓冲区(由lParam参数指定)
④最后SendMessage函数返回。
27.5.2 WM_COPYDATA消息发送过程
(1)COPYDATASTRUCT结构体:
typedef struct tagCOPYDATASTRUCT { ULONG_PTR dwData;//备用的数据项,可以放任何值 DWORD cbData; //要向另一个进程发送的字节数 PVOID lpData; //指向要发送的第1个字节(地址在发送进程的地址空间中) } COPYDATASTRUCT;
(2)WM_COPYDATA消息的处理过程
①当SendMessage看到要发送一个WM_COPYDATA消息时,会建立一个内存映像文件,大小是cbData字节。
②从发送进程的地址空间中将数据复制到这个内存映像文件。
③然后向目标窗口发送消息
④接收线程在处理这个消息时,lParam参数己指定接收进程地址空间中的一个COPYDATASTRUCT结构体。这个结构的lpData成员指向了接收进程地址空间中的共享内存映像文件的视图。
(3)使用WM_COPYDATA消息,应注意的3个重要问题。
①只能SendMessage这个消息,而不能PostMessage。因为在接收消息的窗口过程处理完消息后,系统必须释放内存映像文件。但因PostMessage是立即返回的,它不知道这个消息何时被处理完,也就不能释放这个内存块。
②系统从另外的进程地址空间中复制数据要花费一些时间。所以不应让发送程序中运行的其他线程在SendMessage返回前修改这个内存块。
③WM_COPYDATA消息,可以实现16位到32位之间的通信,也能实现32位到64位之间的通信。但Win98以下没有WM_COPYDATA消息和COPYDATASTRUCT结构本的定义,须参考msdn自己定义。
【27_CopyData程序】演示如何使用WM_COPYDATA消息从一个程序向另一个程序发送一个数据块。
/************************************************************************ Module: CopyData.cpp Notices: Copyright(c) 2008 Jeffrey Ritcher & Christophe Nasarre ************************************************************************/ #include "../../CommonFiles/CmnHdr.h" #include "resource.h" #include <malloc.h> #include <tchar.h> ////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnCopyData(HWND hWnd,HWND hWndFrom,PCOPYDATASTRUCT pcds){ Edit_SetText(GetDlgItem(hWnd,pcds->dwData?IDC_DATA2:IDC_DATA1), (PTSTR)pcds->lpData); return (TRUE); } ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hWnd,HWND hWndFocus,LPARAM lParam){ chSETDLGICONS(hWnd,IDI_COPYDATA); //初始化编辑框 Edit_SetText(GetDlgItem(hWnd,IDC_DATA1),TEXT("测试数据")); Edit_SetText(GetDlgItem(hWnd,IDC_DATA2),TEXT("其他测试数据")); return (TRUE); } ////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hWnd,int id,HWND hWndCtrl,UINT codeNotify){ switch(id){ case IDCANCEL: EndDialog(hWnd,id); break; case IDC_COPYDATA1: case IDC_COPYDATA2: if (codeNotify !=BN_CLICKED) break; //获取相应的文本框 HWND hWndEdit = GetDlgItem(hWnd, (id == IDC_COPYDATA1)?IDC_DATA1:IDC_DATA2); //准备一个COPYDATASTRUCT结构体,其内容会被Copy到 //一个内存映像文件中 COPYDATASTRUCT cds; //指示哪个数据域的内容要被发送(0=ID_DATA1,1=ID_DATA2) cds.dwData =(DWORD)((id == IDC_COPYDATA1)?0:1); //获取文本的长度(字节) cds.cbData = (Edit_GetTextLength(hWndEdit)+1)*sizeof(TCHAR); //在栈中分配一个内容以保存字符串内容 cds.lpData = _alloca(cds.cbData); //将编辑框里的字符串存到cbs.lpData中 Edit_GetText(hWndEdit,(PTSTR)cds.lpData,cds.cbData); //获取应用程序的标题 TCHAR szCaption[100]; GetWindowText(hWnd,szCaption,sizeof(szCaption)/sizeof(szCaption[0])); //枚举所有具有相同标题的顶层窗口 HWND hWndT = NULL; do { hWndT = FindWindowEx(NULL,hWndT,NULL,szCaption); if (hWndT != NULL) { FORWARD_WM_COPYDATA(hWndT,hWnd,&cds,SendMessage); } } while (hWndT != NULL); break; } } ////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam){ switch(uMsg){ chHANDLE_DLGMSG(hWnd,WM_INITDIALOG,Dlg_OnInitDialog); chHANDLE_DLGMSG(hWnd,WM_COMMAND,Dlg_OnCommand); chHANDLE_DLGMSG(hWnd,WM_COPYDATA,Dlg_OnCopyData); } return (FALSE); } ////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hInstExe,HINSTANCE hPrev,PTSTR lpCmdLIne,int nShowCmd){ DialogBox(hInstExe,MAKEINTRESOURCE(IDD_COPYDATA),NULL,Dlg_Proc); return 0; }
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 27_CopyData.rc 使用 // #define IDD_COPYDATA 101 #define IDC_DATA1 1001 #define IDC_DATA2 1002 #define IDC_COPYDATA1 1003 #define IDC_COPYDATA2 1004 #define IDI_COPYDATA 1005 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 103 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1004 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//27_CopyData.rc
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_COPYDATA DIALOGEX 0, 0, 239, 45 STYLE DS_SETFONT | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "CopyData应用程序" FONT 9, "宋体", 400, 0, 0x86 BEGIN LTEXT "数据1:",IDC_STATIC,13,9,29,8 LTEXT "数据2:",IDC_STATIC,12,26,29,8 EDITTEXT IDC_DATA1,42,6,63,14,ES_AUTOHSCROLL EDITTEXT IDC_DATA2,42,23,63,14,ES_AUTOHSCROLL PUSHBUTTON "将数据1发送到其他窗口",IDC_COPYDATA1,115,6,110,14 PUSHBUTTON "将数据2发送到其他窗口",IDC_COPYDATA2,115,23,110,14 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO BEGIN IDD_COPYDATA, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 232 TOPMARGIN, 3 BOTTOMMARGIN, 40 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_COPYDATA ICON "CopyData.ico" #endif // 中文(简体,中国) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
27.6 Windows如何处理ANSI/Unicode字符串
27.6.1 问题的由来
(1)对于某些消息(如WM_SETTEXT),窗口过程可能要求传入ANSI或Unicode字符串。这取决于注册窗口类时所使用的函数。如果调用RegisterClassA,系统认为窗口过程要求所有字符串和字符都属于ANSI。而用RegisterClassW注册窗口类时,则系统认为窗口过程需要Unicode字符串。
(2)IsWindowsUnicode(hwnd)可用来判断窗口过程需要使用哪种字符(串)类型
(3)如果要将一个ANSI字符串发送WM_SETTEXT给一个要求Unicode串的窗口过程,则系统在发送消息之前,会自动地转换字符串而无法我们干预。如果将Unicode字符串发送WM_SETTEXT给一个要求ANSI串的窗口过程,就需要我们自己来转换。
27.6.2 窗口子类化可能出现的问题(假设父类的窗口过程要求Unicode)
(1)子类窗口过程要求的字符类型:
调用SetWindowLongPtrA/W并将nIndex设为GCLP_WNDPROC,dwNewLong设为子类窗口过程的地址。如果调用SetWindowLongPtrA表示子类的窗口过程要求是ANSI字符串(可以调用IsWindowUnicode来判断子窗口过程验证是否是ANSI类型的)。相反如果调用SetWindowLongPtrW,则表示子类的窗口过程要求Unicode字符串。
(2)子类窗口过程中如何知道父类的窗口过程使用的是哪种类型?
当调用SetWindowLongPtr来派生子类时,系统会检查子类与父类窗口过程要求的类型是否一致。如果一致,SetWindowLongPtr就直接返回原窗口过程(父类)的地址,如果不一致,SetWindowLongPtr返回一个内部子系统数据结构的句柄,这个数据结构包含原窗口过程的地址及一个用来指示父窗口过程要求ANSI或Unicode的数值。
(3)子类窗口过程的设计
因SetWindowLongPtr的返回值可能是原窗口过程,也可能是系统内部一个数据结构,所以子类中不能直接通过这个返回值来调用原窗口过程,而应该用CallWindowProc,因为这个函数会查看传入的地址是一个窗口过程的地址还是某个内部数据结构的地址。如果传入的是一个窗口过程,则调用原先(父类)的窗口过程,不需要执行字符转换。否则,根据内部数据结构指示的类型进行转换,再调用原窗口过程。
【子类窗口过程的设计】
//子类化 WNDPROC wndprocPrev = SetWindowLongPtr(hwnd,GWL_WNDPROC,SubClassWndProc); …… //子类窗口过程 LRESULT SubClassWndProc(hwnd,uMsg,wParam,lParam){ …… //子类处理一些自己感兴趣的消息 //对不感兴趣的消息,调用父类窗口过程处理 //return CallWindowsProcA(wndprocPrev,hwnd,uMsg,wParam,lParam); //子类使用ANSI时 return CallWindowsProcW(wndprocPrev,hwnd,uMsg,wParam,lParam); //子类Unicode时 //以下调用父类窗口过程的方法是错误的,因为wndprocPrev可能是原窗口过程,也可能是系统 //内部的数据结构 //return wndprocPrev(hwnd,uMsg,wParam,lParam);//错误,直接使用SetWindowLongPtr返回值。 }