一个完整的Windows程序框架
<上一节
下一节>
前面我们演示了带界面的Windows程序,但那仅仅是一个弹窗,调用MessageBox函数就可以实现,不是一个真正意义上的窗口。我们通常所说的窗口包含最大化、最小化、关闭按钮,也包含菜单、单选框、图像等各种控件。
一个完整的Windows程序框架:
对于初学者,这段代码“又臭又长”,难于理解,有点吓人。但这是一个Windows程序的基本框架,只不过不像C语言的框架那么简单,几行代码搞定。大家不要急于理解每行代码的含义,大部分代码直接拿来使用就可以。
注意最后的字段
窗口类仅仅是一个结构体,如果只是定义了结构体变量,那么在使用时并不能通过 lpszClassName 字段的值找到这个结构体。所以还要调用 RegisterClass() 来注册,让系统知道窗口类的名字,下次使用时才能找到。
作为简明教程,我们并不打算深入研究窗口类的每一个字段的含义,下面是对它们的简要说明:
A) CreateWindow 的第一个参数就是窗口类的名字,通过这个名字可以找到刚才注册的窗口类,然后再根据它来创建窗口。
B) 显示器上的坐标与数学中的不同,显示器的左上角是坐标原点,从原点向右是x轴,向下是y轴,都是正坐标,没有负数。如下图所示:
C) 参数 hInstance 是通过主函数 WinMain 传入的。
注意:通过 CreateWindows() 函数创建窗口后,仅仅是为窗口分配了内存空间,获得了句柄,但窗口并没有显示出来,所以还需要调用 ShowWindow() 函数来显示窗口。
而调用了 ShowWindow() 函数又仅仅是将窗口显示出来,但不会进行客户区的绘制,所以还需要调用 UpdateWindow() 函数,生成 VM_PAINT 消息,将客户区中的各种控件绘制出来,下面会讲解。
至此,一个窗口的创建工作就已经完成了。窗口的各种属性,在窗口类和 CreateWindow() 函数的参数中都进行了说明。
在《与windows编程有关的重要概念》一节中讲到了Windows的消息机制。Windows 会为每个应用程序维护一个消息队列,当有事件发生时,Windows会自动将这些事件转换为“消息”,并投递到消息队列。
在我们的程序中,可以通过一段“消息循环”代码来从消息队列中获取消息:
注意:GetMessage 的返回值永远为非零值,while 循环会一直进行下去。如果队列中没有消息,GetMessage 函数会等待,直到有消息进入。
获取到消息后,需要调用 TranslateMessage 函数对消息进行转换(翻译),然后再调用 DispatchMessage 函数将消息传给窗口过程去处理(调用窗口过程)。
窗口过程有一个参数 message,会传入发生的事件类型,常用的有:
WM_CREATE 和 WM_DESTROY 很容易理解,WM_PAINT 将在下节中详细讲解,它非常重要,不理解 WM_PAINT 可以说就没有学会Windows编程。
不同的消息往往需要进行不同的处理,所以一般通过 switch case 语句来匹配。
注意:你可以对获取到的消息进行处理,加入自己的业务逻辑;也可以不处理,让Windows自己看着办(默认处理方式)。窗口过程最后一条语句:
编写Windows应用程序的步骤:
有了代码模板,剩下的主要工作就是处理各种各样的事件了,也就是在窗口过程中编写代码。
一个完整的Windows程序框架:
#include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow ){ static TCHAR szClassName[] = TEXT("HelloWin"); //窗口类名 HWND hwnd; //窗口句柄 MSG msg; //消息 WNDCLASS wndclass; //窗口类 /**********第①步:注册窗口类**********/ //为窗口类的各个字段赋值 wndclass.style = CS_HREDRAW | CS_VREDRAW; //窗口风格 wndclass.lpfnWndProc = WndProc; //窗口过程 wndclass.cbClsExtra = 0; //暂时不需要理解 wndclass.cbWndExtra = 0; //暂时不需要理解 wndclass.hInstance = hInstance; //当前窗口句柄 wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION); //窗口图标 wndclass.hCursor = LoadCursor (NULL, IDC_ARROW); //鼠标样式 wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH); //窗口背景画刷 wndclass.lpszMenuName = NULL ; //窗口菜单 wndclass.lpszClassName= szClassName; //窗口类名 //注册窗口 RegisterClass(&wndclass); /*****第②步:创建窗口(并让窗口显示出来)*****/ hwnd = CreateWindow( szClassName, //窗口类的名字 TEXT("Welcome"), //窗口标题(出现在标题栏) WS_OVERLAPPEDWINDOW, //窗口风格 CW_USEDEFAULT, //初始化时x轴的位置 CW_USEDEFAULT, //初始化时y轴的位置 500, //窗口宽度 300, //窗口高度 NULL, //父窗口句柄 NULL, //窗口菜单句柄 hInstance, //当前窗口的句柄 NULL //不使用该值 ); //显示窗口 ShowWindow (hwnd, iCmdShow); //更新(绘制)窗口 UpdateWindow (hwnd); /**********第③步:消息循环**********/ while( GetMessage(&msg, NULL, 0, 0) ){ TranslateMessage(&msg); //翻译消息 DispatchMessage (&msg); //分派消息 } return msg.wParam; } /**********第④步:窗口过程**********/ LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){ HDC hdc; //设备环境句柄 PAINTSTRUCT ps; RECT rect; switch (message){ //窗口绘制消息 case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; DrawText( hdc, TEXT("你好,欢迎来到C语言中文网"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER ); EndPaint (hwnd, &ps) ; return 0 ; //窗口销毁消息 case WM_DESTROY: PostQuitMessage(0) ; return 0 ; } return DefWindowProc(hwnd, message, wParam, lParam) ; }运行结果:
对于初学者,这段代码“又臭又长”,难于理解,有点吓人。但这是一个Windows程序的基本框架,只不过不像C语言的框架那么简单,几行代码搞定。大家不要急于理解每行代码的含义,大部分代码直接拿来使用就可以。
1) 注册窗口类
在Windows中,调用 CreateWindow() 函数可以创建一个窗口(请看上面的代码)。窗口有很多属性,比如大小、位置、标题、背景颜色、鼠标样式、图标等,在创建窗口时都需要指定。这些属性比较多,超过10个,但是有一部分是通用的,不同的窗口,它们的值一般相同,Windows将这些通用的属性抽取出来,用一个结构体表示,就是上面代码中WNDCLASS(window class缩写):WNDCLASS wndclass; //定义窗口类 //为窗口类的各个字段赋值 wndclass.style = CS_HREDRAW | CS_VREDRAW; //窗口风格 wndclass.lpfnWndProc = WndProc; //窗口过程 wndclass.cbClsExtra = 0; //暂时不需要理解 wndclass.cbWndExtra = 0; //暂时不需要理解 wndclass.hInstance = hInstance; //当前窗口句柄 wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION); //窗口图标 wndclass.hCursor = LoadCursor (NULL, IDC_ARROW); //鼠标样式 wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH); //窗口背景画刷 wndclass.lpszMenuName = NULL ; //窗口菜单 wndclass.lpszClassName= szClassName; //窗口类名这个结构体,我们称之为窗口类。如果你有面向对象的编程经验,那么会很容易理解,没有的话也没关系,你可以认为,基于该结构体创建的窗口属于同一个类别,有很多属性是相同的。
注意最后的字段
lpszClassName
,它指明了当前窗口类的名字,将这个名字传递给 CreateWindow() 函数,就能根据该窗口类来创建窗口。也就是说,以后想使用窗口类,只要知道它的名字就可以(也就是字段lpszClassName
的值)。窗口类仅仅是一个结构体,如果只是定义了结构体变量,那么在使用时并不能通过 lpszClassName 字段的值找到这个结构体。所以还要调用 RegisterClass() 来注册,让系统知道窗口类的名字,下次使用时才能找到。
作为简明教程,我们并不打算深入研究窗口类的每一个字段的含义,下面是对它们的简要说明:
字段 | 说明 |
---|---|
style |
窗口风格。对于初学者,常用的取值为CS_HREDRAW | CS_VREDRAW ,表示当窗口大小改变时重绘窗口,这样才能保证文字始终处于窗口中间。style还有很多取值,这里不一一讲解,有兴趣的读者可以查看:WNDCLASS中style字段的取值(wndclass.style的取值) |
lpfnWndProc | 窗口处理过程,下面会详细讲解。 |
hInstance | 当前窗口句柄。 |
hIcon | 窗口图标。也就是程序运行时在左上角和任务栏看到的图标,需要通过LoadIcon函数加载。 |
hCursor | 鼠标样式。需要通过LoadCursor函数加载。 |
hbrBackground | 窗口背景画刷。也就是窗口背景的填充颜色,后面我们会讲解画笔、画刷和画布的概念。 |
lpszMenuName | 窗口菜单。也就是标题栏下方看到的多种多样的菜单,上面的程序没有菜单,所以值为 NULL。 |
lpszClassName | 窗口类的名字。每个窗口类的名字都是不同的,以便与其他窗口类区分。 |
2) 创建窗口
有了窗口类,就可以根据它来创建窗口了。创建窗口使用 CreateWindows() 函数,如下所示:hwnd = CreateWindow( szClassName, // 窗口类的名字 TEXT("Welcome"), //窗口标题(出现在标题栏) WS_OVERLAPPEDWINDOW, //窗口风格 CW_USEDEFAULT, //初始化时窗口x轴坐标 CW_USEDEFAULT, //初始化时窗口y轴坐标 500, //窗口宽度 300, //窗口高度 NULL, //父窗口句柄。这里没有父窗口,所以为 NULL NULL, //窗口菜单句柄。当前窗口没有菜单,所以为 NULL hInstance, //当前窗口的句柄,通过 WinMain 函数传入。 NULL //不使用该值 );几点说明:
A) CreateWindow 的第一个参数就是窗口类的名字,通过这个名字可以找到刚才注册的窗口类,然后再根据它来创建窗口。
B) 显示器上的坐标与数学中的不同,显示器的左上角是坐标原点,从原点向右是x轴,向下是y轴,都是正坐标,没有负数。如下图所示:
C) 参数 hInstance 是通过主函数 WinMain 传入的。
注意:通过 CreateWindows() 函数创建窗口后,仅仅是为窗口分配了内存空间,获得了句柄,但窗口并没有显示出来,所以还需要调用 ShowWindow() 函数来显示窗口。
而调用了 ShowWindow() 函数又仅仅是将窗口显示出来,但不会进行客户区的绘制,所以还需要调用 UpdateWindow() 函数,生成 VM_PAINT 消息,将客户区中的各种控件绘制出来,下面会讲解。
至此,一个窗口的创建工作就已经完成了。窗口的各种属性,在窗口类和 CreateWindow() 函数的参数中都进行了说明。
注意:在窗口类 wndclass 中指定的窗口样式以 CS 开头,是通用的;而在 CreateWindow 函数中指定的窗口样式以 WS 开头,只对当前窗口有效,详情请查看《CreateWindow窗口风格取值》。
3) 进行消息循环
在 UpdateWindow 函数被调用之后,新建的窗口在屏幕中就可以显示了。此时,程序必须能够接受用户的键盘或鼠标事件,例如按下回车键、右击鼠标等。在《与windows编程有关的重要概念》一节中讲到了Windows的消息机制。Windows 会为每个应用程序维护一个消息队列,当有事件发生时,Windows会自动将这些事件转换为“消息”,并投递到消息队列。
在我们的程序中,可以通过一段“消息循环”代码来从消息队列中获取消息:
while( GetMessage(&msg, NULL, 0, 0) ){ TranslateMessage(&msg); //翻译消息 DispatchMessage (&msg); //分派消息 }GetMessage 函数用来从消息队列中获取一条消息,并保存到 MSG 结构体变量中。作为简明教程,我们不再详细分析 getMessage 函数的各个参数,读者根据上面的代码“照猫画虎”就可以,不会影响你后续的学习。
注意:GetMessage 的返回值永远为非零值,while 循环会一直进行下去。如果队列中没有消息,GetMessage 函数会等待,直到有消息进入。
获取到消息后,需要调用 TranslateMessage 函数对消息进行转换(翻译),然后再调用 DispatchMessage 函数将消息传给窗口过程去处理(调用窗口过程)。
4) 窗口过程
所谓窗口过程,就是处理窗口事件的函数,也就是上面代码中最后的 WndProc 函数。GetMessage 每获取到一条消息,最终都会丢给窗口过程去处理。窗口过程有一个参数 message,会传入发生的事件类型,常用的有:
- WM_CREATE:窗口被创建。
- WM_PAINT:窗口需要更新或重绘。
- WM_WM_DESTROY:窗口被销毁(关闭)。
WM_CREATE 和 WM_DESTROY 很容易理解,WM_PAINT 将在下节中详细讲解,它非常重要,不理解 WM_PAINT 可以说就没有学会Windows编程。
不同的消息往往需要进行不同的处理,所以一般通过 switch case 语句来匹配。
注意:你可以对获取到的消息进行处理,加入自己的业务逻辑;也可以不处理,让Windows自己看着办(默认处理方式)。窗口过程最后一条语句:
return DefWindowProc(hwnd, message, wParam, lParam) ;它的作用就是让Windows自己处理应用程序没有处理的消息,必须要有该语句。
窗口过程在窗口类中指明,然后就不用管了,不需要我们显式调用。
最后的总结
上面讲到的,是开发一个Windows应用程序的基本流程,也是Windows应用程序的代码模板,你不需要记住每个细节,直接套用就可以。编写Windows应用程序的步骤:
- 注册窗口类
- 根据窗口类来创建窗口
- 进入无休止的消息循环
- 编写窗口过程
有了代码模板,剩下的主要工作就是处理各种各样的事件了,也就是在窗口过程中编写代码。
<上一节
下一节>