一个典型的SDK程序的框架如下:
1、RegeditClass 注册窗口类。
2、CreateWindow 根据窗口类,创建对应的窗口
3、窗口过程回调函数(WndProc)
4、ShowWindow 显示窗口
5、UpdateWindow 更新窗口
6、消息循环(GetMessage DispatchMessage等)
流程简述:
Windows程序是消息驱动或者说是消息响应机制。即应用程序需要事先准备好一个函数,我们称之为回调函数(WndProc),在某些条件下,由系统从内核中直接调用该函数,而应用程序通常不会主动调用该函数。显然在什么条件下调用窗口过程回调函数值得深究。实际上,这是windows对于用户与UI交互的一种设计,即将用户操作等动作抽象并封装成一个特殊的结构体MSG(如下所示),每种操作对应了唯一的标识值,我们称之为消息码message(例如后面经常遇到的WM_CREATE、WM_PAINT、WM_KEYDOWN、WM_MOUSEMOVE)等。当由于一些原因(例如用户通过键盘键入某一个按键),系统会产生键盘消息(结构体MSG对应的实例msg),然后通知应用程序来处理。而应用程序在回调函数中就可以接收到了这个消息,根据特定的消息码message来为该动作做出对应的响应。
typedefstruct tagMSG {
HWND hwnd;
UINT message; //消息码
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG,*LPMSG,*PMSG;
显然,一台正在“跑”的计算机中,有很多进程在操作系统上运行(我们可以把进程理解成磁盘上应用程序的实例,即磁盘上的计算器文件是一个应用程序,而一个计算器双击运行一次就产生一个用户看得见、“摸得着”的进程)。显然,每一个有UI界面的进程都可以并且必须能够响应用户的操作,即能响应消息。在一个时间段中,每个进程通常都不止一个消息待处理。一旦消息多了会累积,而每个消息都应有被处理的机会,那么自然要让消息排个队,并在适当的时机被取出处理。因此在windows的设计中,每个进程至少应有一个消息队列。当有需要进程处理的消息时,系统就将其投入到该进程的消息队列中。进程要做的事情就是“乖乖”在自己的程序中等待消息的到来,然后在上面所说的窗口过程回调函数中处理就好了。因此进程总是“被动的”,等待消息、处理消息。
上面为什么说一个进程至少有一个消息队列呢?实际上,消息队列应称之为线程消息队列,即线程才可以但不一定必须拥有一个消息队列。一个进程可以有多个线程,也可以有多个窗口,但通常只有主线程(即UI线程,负责创建窗口),才会被分配一个消息队列(实际是一种数据结构,用链表来存储消息)。而操作系统内部怎么知道该不该为一个线程分配一个消息队列呢,实际上,一开始线程是不会被分配消息队列,只要该线程调用了与UI相关的API(windows 提供的函数调用接口)时,windows内核就会为其分配消息队列了。
所以一个正常的应用程序需要响应各种消息,那么最重要的事就是写一个循环和对应的回调函数。
这个循环我们又叫消息循环:
while(GetMessage(&msg, NULL,0,0)) //从消息队列中取出消息
{
DispatchMessage(&msg);//将消息"派发"给窗口过程回调函数
}
这个回调函数我们称之为窗口回调函数(因为它与窗口是一一对应关系)。
由于窗口过程函数需要根据不同的消息码
message来做出不同的处理,因此窗口过程函数的主体通常是一个大大的switch-case:
switch(message)
{
case WM_COMMAND:
break;
case WM_NCPAINT:
break;
case WM_PAINT:
break;
case WM_DESTROY:
break;
case WM_CREATE:
break;
default:
returnDefWindowProc(hWnd, message, wParam, lParam); //其余消息交给DefWindowProc,由操作系统默认处理
}
而通常,一个带UI界面的进程至少得有一个窗口,所以需要调用CreateWindow这个API来创建窗口。创建一个窗口自然需要指定这个窗口需要什么风格啊,窗口有多大等等信息,其实更为重要的是要通过该函数告诉操作系统,你的这个窗口所对应的窗口过程函数是什么。因为从用户角度来讲,实际响应消息的"实体"应该是对应的窗口,而不该是线程。线程只是提供了一个数据结构来存储发送过来的所有消息,实际消息取出后,就应该根据消息中所指定的窗口(hwnd),找到该窗口所对应的窗口过程回调函数,然后就交由该函数来处理消息了。
CreateWindowExA(DWORD dwExStyle,
LPCSTR lpClassName, //需要指定“类名”
LPCSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam)
从CreateWindowEx中没有找到需要指明的窗口过程回调函数,但是却有一个"类名"的概念,这个“类”实际也是一种设计。我们知道对于界面而言,很多窗口的外观和作用是固定的,这相当于是工厂中已经生产好了的组件。因此对于这些窗口而言,其窗口回调、样式都是固定好了的,不用每次创建窗口时重复指定。因此,微软创建了一个“窗口类”的概念,类里面封装了一个窗口的窗口回调函数,样式啊等等,然后那些拥有固定外观和作用的窗口有着微软定制好了的“窗口类”,创建窗口时,直接指定这个“类名”,系统就知道了你要什么样的窗口了。
typedefstruct _WNDCLASSW {
UINT style;
WNDPROC lpfnWndProc; //在窗口类中指定窗口过程回调函数
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCWSTR lpszMenuName;
LPCWSTR lpszClassName;
} WNDCLASSW,*LPWNDCLASSW,*PWNDCLASSW;
当然,我们也可以自己定制自己的窗口类,这也就是框架第一步注册窗口类的由来:
ATOM WINAPI
RegisterClassW(CONST WNDCLASSW *lpWndClass)
{
WNDCLASSEXW Class;
if(lpWndClass == NULL)
return0;
RtlCopyMemory(&Class.style, lpWndClass,sizeof(WNDCLASSW));
Class.cbSize =sizeof(WNDCLASSEXW);
Class.hIconSm = NULL;
return RegisterClassExW(&Class);
}
最后,框架中还有两个API:ShowWindow和UpdateWindow需要解释。其实CreateWindow只是在内存中创建了一个窗口,即在内存中分配了各种数据结构,但是窗口的显示却需要ShowWindow和UpdateWindow来进一步处理,这两个API实际上在内部还是通过消息机制,向本进程的主窗口发送WM_PAINT等消息。因此,最终的显示仍是依赖窗口过程回调函数来完成。如果窗口过程回调中没有显式处理,那么就会在
DefWindowProc内部,由默认的系统内部来处理。
LRESULT WINAPI
DefWindowProcA(HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam)
{
BOOL Hook, msgOverride = FALSE;
LRESULT Result=0;
LoadUserApiHook();
Hook=BeginIfHookedUserApiHook();
if(Hook)
{
msgOverride =IsMsgOverride(Msg,&guah.DefWndProcArray);
if(msgOverride == FALSE)
{
EndUserApiHook();
}
}
/* Bypass SEH and go direct. */
if(!Hook||!msgOverride)
return RealDefWindowProcA(hWnd,Msg, wParam, lParam);
_SEH2_TRY
{
Result= guah.DefWindowProcA(hWnd,Msg, wParam, lParam);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
}
_SEH2_END;
EndUserApiHook();
returnResult;
}