1.2.2 一个简单的Windows应用程序
相对一个简单的DOS程序来说一个简单的Windows应用程序要很长。下面的例子中只实现了一个特别简单的Windows程序,这个程序在桌面上显示一个简单的窗口,它没有菜单栏、工具栏、状态栏,只是在窗口中输出一段简单的字符串。虽然程序如此简单,但是也要编写100行左右的代码。考虑到初学的读者,这里将一部分一部分地逐步介绍代码中的细节,以减少代码的长度,从而方便初学者的学习。
1.Windows窗口应用程序的主函数—WinMain()
在DOS时代,或编写Windows下的命令行的程序,要使用C语言编写代码的时候都是从main()函数开始的。而在Windows下编写有窗口的程序时,要用C语言编写窗口程序就不再从main()函数开始了,取而代之的是WinMain()函数。
既然Windows应用程序的主函数是WinMain(),那么就从了解WinMain()函数的定义开始学习Windows应用程序的开发。WinMain()函数的定义如下:
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow );
该函数的定义取自MSDN中,在看到WinMain()函数的定义后,很直观地会发现WinMain函数的参数比main()函数的参数变多了。从参数个数上来说,WinMain()函数接收的信息更多了。下面来看每个参数的含义。
hInstance是应用程序的实例句柄。保存在磁盘上的程序文件是静态的,当被加载到内存中时,被分配了CPU、内存等进程所需的资源后,一个静态的程序就被实例化为一个有各种执行资源的进程了。句柄的概念随上下文的不同而不同,句柄是操作某个资源的“把手”。当需要对某个实例化进程操作时,需要借助该实例句柄进行操作。这里的实例句柄是程序装入内存后的起始地址。实例句柄的值也可以通过GetModuleHandle()参数来获得(注意系统中没有GetInstanceHandle()函数,不要误以为是hInstance就会有GetInstance×××()类的函数)。
注:句柄这个词在开发Windows程序时是非常常见的一个词。“句柄”一词的含义随上下文的不同而所有改变。比如,磁盘上的程序文件被加载到内存中后,就创建了一个实例句柄,这个实例句柄是程序装入内存后的“起始地址”,或者说是“模块的起始地址”。而在前面介绍的FindWindow()函数和SendMessage()函数中也提到了“句柄”这个词,而这时的“句柄”相当于某个资源的“把手”或“面板”。
拿SendMessage()函数举例来说,句柄相当于一个操作的面板,对句柄发送的消息相当于面板上的各个开关按键,消息的附加数据,相当于给开关按键送的各种参数,这些参数根据按键的不同而不同。
hPrevInstance是同一个文件创建的上一个实例的实例句柄。这个参数是Win16平台下的遗留物,在Win32下已经不再使用了。
lpCmdLine是主函数的参数,用于在程序启动时给进程传递参数。比如在“开始”菜单的“运行”中输入“notepad c:\boot.ini”,这样就通过记事本打开了C盘下的boot.ini文件。C:\Boot.ini文件是通过WinMain()函数的lpCmdLine参数传递给notepad.exe程序的。
nCmdShow是进程显示的方式,可以是最大化显示、最小化显示,或者是隐藏等显示方式(如果是启动木马程序的话,启动方式当然要由自己进行控制)。
主函数的参数都介绍完了。编写Windows的窗口程序,需要主函数中应该完成哪些操作是下面要讨论的内容。
2.WinMain()函数中的流程
编写Windows下的窗口程序,在WinMain()主函数中主要完成的任务是注册一个窗口类,创建一个窗口并显示创建的窗口,然后不停地获取属于自己的消息并分发给自己的窗口过程,直到收到WM_QUIT消息后退出消息循环结束进程。这是主函数中程序的执行脉络,程序中将注册窗口类、创建窗口的操作封装为自定义函数。
代码如下:
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { MSG Msg; BOOL bRet; // 注册窗口类 MyRegisterClass(hInstance); // 创建窗口并显示窗口 if ( !InitInstance(hInstance, SW_SHOWNORMAL) ) { return FALSE; } // 消息循环 // 获取属于自己的消息并进行分发 while( (bRet = GetMessage(&Msg, NULL, 0, 0)) != 0 ) { if ( bRet == -1 ) { // handle the error and possibly exit break; } else { TranslateMessage(&Msg); DispatchMessage(&Msg); } } return Msg.wParam; }
在代码中,MyRegisterClass()和InitInstance()是两个自定义的函数,分别用来注册窗口类,创建窗口并显示更新创建的窗口。后面的消息循环部分用来获得消息并进行消息分发。它的流程如图1-7所示的“主程序”部分。
代码中主要是3个函数,分别是GetMessage()、TranslateMessage()和DispatchMessage()。这3个函数是Windows提供的API函数。GetMessage()的定义如下:
BOOL GetMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax );
该函数用来获取属于自己的消息,并填充MSG结构体。有一个类似于GetMessage()的函数是PeekMessage(),它可以判断消息队列中是否有消息,如果没有消息,可以主动让出CPU时间给其他进程。关于PeekMessage()函数的使用,请参考MSDN:
BOOL TranslateMessage(CONST MSG *lpMsg);
该函数是用来处理键盘消息的。它将虚拟码消息转换为字符消息,也就是将WM_KEYDOWN消息和WM_KEYUP消息转换为WM_CHAR消息,将WM_SYSKEYDOWN消息和WM_SYSKEYUP消息转换为WM_SYSCHAR消息:
LRESULT DispatchMessage(CONST MSG *lpmsg);
该函数是将消息分发到窗口过程中。
3.注册窗口类的自定义函数
在WinMain()函数中,首先调用了MyRegisterClass()这个自定义函数,需要传递进程的实例句柄hInstance作为参数。该函数完成窗口类的注册,分为两步:第一步是填充WNDCLASSEX结构体,第二步是调用RegisterClassEx()函数进行注册。该函数相对简单,但是,该函数中稍微复杂的是WNDCLASSEX结构体的成员较多。
代码如下:
ATOM MyRegisterClass(HINSTANCE hInstance) { WNDCLASSEX WndCls; // 填充结构体为0 ZeroMemory(&WndCls, sizeof(WNDCLASSEX)); // cbSize是结构体大小 WndCls.cbSize = sizeof(WNDCLASSEX); // lpfnWndProc是窗口过程地址 WndCls.lpfnWndProc = WindowProc; // hInstance是实例句柄 WndCls.hInstance = hInstance; // lpszClassName是窗口类类名 WndCls.lpszClassName = CLASSNAME; // style是窗口类风格 WndCls.style = CS_HREDRAW | CS_VREDRAW; // hbrBackground是窗口类背景色 WndCls.hbrBackground = (HBRUSH)COLOR_WINDOWFRAME + 1; // hCursor是鼠标句柄 WndCls.hCursor = LoadCursor(NULL, IDC_ARROW); // hIcon是图标句柄 WndCls.hIcon = LoadIcon(NULL, IDI_QUESTION); // 其他 WndCls.cbClsExtra = 0; WndCls.cbWndExtra = 0; return RegisterClassEx(&WndCls); }
在代码中,WNDCLASSEX结构体的成员都介绍了。WNDCLASSEX中最重要的字段是lpfnWndProc,它将保存的是窗口过程的地址。窗口过程是对各种消息进程处理的“汇集地”,也是编写Windows应用程序的重点部分。代码中的函数都比较简单,主要涉及LoadCursor()、LoadIcon()和RegisterClassEx()这3个函数。由于这3个函数使用简单,通过代码就可以进行理解,这里不做过多介绍。
注册窗口类(提到窗口类,你是否想到了FindWindow()函数的第一个参数呢?)的重点是在后面的代码中可以根据该窗口类创建该种类型的窗口。代码中,在定义窗口类时指定了背景色、鼠标指针、窗口图标等,那么使用该窗口类创建的窗口都具有相同的窗口类型。
4.创建主窗口并显示更新
注册窗口类后,根据该窗口类创建具体的主窗口并显示和更新窗口。
代码如下:
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { HWND hWnd = NULL; // 创建窗口 hWnd = CreateWindowEx(WS_EX_CLIENTEDGE, CLASSNAME, "MyFirstWindow", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); if ( NULL == hWnd ) { return FALSE; } // 显示窗口 ShowWindow(hWnd, nCmdShow); // 更新窗口 UpdateWindow(hWnd); return TRUE; }
在调用该函数时,需要给该函数传递实例句柄和窗口显示方式两个参数。这两个参数的第1个参数通过WinMain()函数的参数hInstance指定,第2个参数可以通过WinMain()函数的第3个参数指定,也可以进行自定义指定。程序中的调用代码如下:
InitInstance(hInstance, SW_SHOWNORMAL);
在创建主窗口时调用了CreateWindowEx()函数,先来看看它的函数原型:
HWND CreateWindowEx( DWORD dwExStyle, LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam );
CreateWindowEx()中的第2个参数是lpClassName,由注释可以知道是已经注册的类名。这个已经注册的类名就是WNDCLASSEX结构体的lpszClassName字段。
5.处理消息的窗口过程
按照如图1-7所示的流程,WinMain()主函数的部分已经都实现完成了。接下来看程序中关键的部分—窗口过程。从WinMain()主函数中看出,在WinMain()主函数中没有任何地方直接调用窗口过程,只是在注册窗口类时指定了窗口过程的地址。那么窗口类是由谁进行调用的呢?答案是由操作系统进行调用的。原因有二,首先窗口过程的地址是由系统维护的,注册窗口类时是将“窗口过程的地址”向操作系统进行注册。其次是除了应用程序本身会调用自己的窗口过程外,其他应用程序也会调用自己的窗口过程,比如前面的例子中调用SendMessage()函数发送消息后,需要系统调用目标程序的窗口过程来完成相应的动作。如果窗口过程由自己调用,那么窗口就要自己维护窗口类的信息,进程间消息的通信会非常繁琐,也会无形中增加系统的开销。
窗口过程的代码如下:
LRESULT CALLBACK WindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hDC; RECT rt; char *pszDrawText = "Hello Windows Program."; switch (uMsg) { case WM_PAINT: { hDC = BeginPaint(hwnd, &ps); GetClientRect(hwnd, &rt); DrawTextA(hDC, pszDrawText, strlen(pszDrawText),&rt, DT_CENTER | DT_VCENTER | DT_SINGLELINE); EndPaint(hwnd, &ps); break; } case WM_CLOSE: { if ( IDYES == MessageBox(hwnd, "是否退出程序", "MyFirstWin", MB_YESNO) ) { DestroyWindow(hwnd); PostQuitMessage(0); } break; } default: { return DefWindowProc(hwnd, uMsg, wParam, lParam); } } return 0; }
在WinMain()函数中,通过调用RegisterClassEx()函数进行了窗口类的注册,通过调用CreateWindowEx()函数创建了窗口,并且GetMessage()函数不停地获取消息,但是在主函数中没有对被创建的窗口做任何处理。那是因为真正对窗口行为的处理全部放在了窗口过程中。当WinMain()函数中的消息循环得到消息以后,通过调用DispatchMessage()函数将消息派发(实际不是由DispatchMessage()函数直接派发)给了窗口过程,从而由窗口过程对消息进行处理。
窗口过程的定义是按照MSDN上给出的形式进行定义的,MSDN上的定义形式如下:
LRESULT CALLBACK WindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam );
WindowProc是窗口过程的函数名,这个函数名可以随意改变,但是该窗口过程的函数名必须与WNDCLASSEX结构体中lpfnWndProc的成员变量的值一致。函数的第1个参数hwnd是窗口的句柄,第2个参数uMsg是消息值,第3个和第4个参数是对于消息值的附加参数。这4个参数的类型与SendMessage()函数的参数相对应。
上面WindowProc()窗口过程中只对两个消息进行了处理,分别是WM_PAINT和WM_CLOSE。这里为了演示因此只简单处理了两个消息。Windows中有上千种消息,那么多的消息不可能全部都由程序员自己去处理,程序员只处理一些程序中需要的消息,其余的消息就交给了DefWindowProc()函数进行处理。DefWindowProc()函数实际上是将消息传递给了操作系统,由操作系统来处理程序中没有处理的消息。比如,在调用CreateWindow()函数时,系统会发送消息WM_CREATE给窗口过程,但是这个消息可能对程序的功能并不需要进行特殊的处理,因此直接交由DefWindowProc()函数让系统进行处理。
DefWindowProc()函数的定义如下:
LRESULT DefWindowProc( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam );
该函数的4个参数跟窗口过程的参数相同,只要将窗口过程的参数依次传递给DefWindowProc()函数就可以完成该函数的调用。在switch分支结构中的default位置直接调用DefWindowProc()函数就可以了。
WM_CLOSE消息是关闭窗口时发出的消息,在这个消息中需要调用DestoryWindow()函数来销毁窗口,并且调用PostQuitMessage()来退出消息循环,使程序退出。对于WM_PAINT消息,这里不进行介绍,涉及的几个API函数可以参考MSDN进行了解。
有的资料在介绍消息循环时会给出一个建议,就是把需要经常处理的消息放到程序靠上的位置,而将不经常处理的消息放到程序靠下的位置,从而提高程序的效率。其实,在窗口过程中往往会使用switch结构对消息进行判断(如果使用if和else结构进行消息的判断,那么常用的消息是要放到前面),而switch结构在编译器进行编译后会进行优化处理,从而大大提高程序的运行效率。关于switch结构的优化,我们将在其他章节进行介绍。