WINDOWS应用程序组织及实例
2010-09-27 20:04:29 来源:WEB开发网Windows应用程序的面向对象认识
面向对象作为一种方法学,要求将程序中的数据和操作(代码)归结到某些对象名下,将数据看作对象的属性,要改变这些属性,必须通过操作来进行。
进行面向对象的程序设计最好使用面向对象的语言,如C++,SamllTalk等。面向对象的语言的语言所起的作用,就是给程序员们提供一些进行面向对象的程序设计时必需的约束,使数据和操作的衔接有一种显式的描述,并进行一些技术性的事务管理。但是,如果我们能理解面向对象程序设计的原理和方法,即使不使用面向对象的语言,也能实现面向对象的程序设计。
Windows本身并不是一个面向对象的程序设计环境,但Windows的某些部分还是明显地受到面向对象的软件的概念的影响。从某种程度上说,在进行Windows程序设计时,程序员是在进行面向对象的程序设计。理解Windows的面向对象的思想和应用程序设计的面向对象方法对设计结构合理的应用程序会有很大的帮助。
前面已给出了对象的定义:每个对象包含有数据和代码,代码描述了对象可执行的一系列预定义的动作,而数据是对象私有的,它们由相关的可执行代码存取。预定义的动作和私有数据的结合称为封装。在C中,我们使用一个函数来封装一个对象的私有数据和动作,使用switch语句来定义预定义的动作,这些动作只存取为该函数本身所知道的数据。
Windows和Windows应用程序是怎样发送消息的呢?在Windows及其应用程序中,消息被表示为一个数据结构,并能在对象之间传递。发送消息等价于执行其参数表示消息数据的函数调用,参数之一是一个标识该消息的预定义的消息标识符,当一个对象接受到一条消息时,消息标识符决定该对象执行何种动作。消息传递是以函数调用的形式来实现的,这种调用可以发生在程序的任何地方。
Windows程序员必须清楚用消息引发动作的技术。不同的对象能以不同的动作响应同样的消息。这样,一个特定的消息可代表一个通用事件。例如,按键操作、移动鼠标或绘制用户区等;而任何一个特定的消息可以在不同的对象中引发不同的动作,例如,不同的窗口对象以不同的动作处理同样的WM_KEYDOWN、WM_MOUSEMOVE或WM_PAINT消息。
一个消息可以有一个对象发送到另一个对象,或由Windows发送到某个对象。例如,WM_KEY_DOWN之类的消息是由Windows产生的。有些消息在对象的窗口函数对其处理完毕后就消失了,而有些消息在处理时有产生新的消息:一个对象通过向其它对象或自己发送一条或多条消息来处理一条消息。这样,Windows应用程序的控制流程不象MS-DOS应用程序那样易于跟踪,程序的调试也比MS-DOS应用程序困难。
除了个别消息以外,对象接受消息的顺序是不可预知的,但对象处理每条消息所采取的动作是显式定义在窗口函数中的。对象并不显式地定义所有可能消息的动作,对于不显示处理的消息,都交由DefWindowProc进行缺省处理。
消息传递的途径很简单:从一个对象传递到另一对象,但由于DefWindowProc对有些消息提供了缺省处理,因此,程序员在设计程序时必须考虑在一个窗口函数中捕获某条消息时是否还应交给DefWindowProc函数作进一步的处理。DefWindowProc能处理所有的消息,但对大部消息只是简单地废弃之,不作具有实际意义的处理,在窗口函数捕获这些废弃消息是安全的;若要捕获其它消息,则必须了解DefWindowProc是怎样处理这条消息的,并在窗口函数的处理代码中能提供类似的处理(或将该消息交由DefWindowProc作进一步的处理)。
现在我们讨论窗口函数对对象的私有数据的处理问题。窗口类也说明了对象的私有数据,当调用CreateWindow创建一个窗口对象时,Windows为创建的窗口对象分配私有数据存储区,其中存储有窗口的实例句柄、父窗口句柄、窗口函数的地址和其它Windows用于管理窗口对象的数据。对这些私有数据的的操作只能使用GetWindowWord/GetWindowLong等函数。对于程序中说明的变量,如何在窗口函数中将它们与相关的对象衔接在一起就比较复杂,因为窗口函数为该类的所有对象共享,该类的所有对象在接收到消息时都执行相同的代码。
在过去,Windows推荐使用的程序设计语言是C,由于C语言不具备将一个对象的私有数据和操作这些私有数据的代码衔接在一起的语言成份(面向对象的语言的事务性工作之一就是为程序完成这个工作),这个工作只能由程序员来作。程序员心中必须清楚程序中所说明或分配的变量私有于哪个对象,并采用合适的数据结构来表示它们,以便程序在使用它们时,能根据不同的对象将它们区别开来。
有几种方法可用于区分对象的私有数据:
程序员编制额外的代码来判断一个对象应使用哪些数据。
使用窗口附加字节。
使用属性表。
当使用第一种方法时,程序实际是使用对象句柄作索引来检索与该对象相关的私有数据,Windows也使用这种方法使用句柄来检索一张表,这个表中存储着该句柄所标识的对象的私有数据。Windows的许多函数需要一个对象的句柄作为第一参数,其原因就是为区分对象的私有数据,以便使用相同的函数处理不同的对象(的数据)。
后两种方法与第一种方法本质是一样的(我们会将在后面的章节对其进行介绍),只是Windows提供了一些相关的函数来简化程序的工作。
由于C没有继承这种语言成分,因为,也就不能形成对象的等级结构。继承是面向对象语言的另一个重要成分。继承使得程序中的对象形成一个分层次的对象结构,低层次的对象可以将它不处理的消息发送到高层对象上进行缺省处理。由于在C中不能(或说很难)建立对象的这种等级结构,但为了简化应用程序的设计,又必须要求支持消息的缺省处理(否则应用程序要定义一个窗口对象可能接收到的所有消息的处理代码),因此只能使用DefWindowProc提供消息的缺省处理。这就要求对一个窗口对象所有消息的处理定义在一个函数中,就带来了定义窗口函数的返回值和参数类型时使用了一种较难为人理解的方法。因为不同的消息可以带有不同类型和个数的参数,并且返回数据的类型也不相同,Windows的设计者采用了一个折中的方法:为消息规定一个十六位的参数和一个32位的参数,将返回类型指定为LRESULT,这种类型的长度能容下C中所有预定义类型的数据。
由于不同类的窗口对象定义有自己的窗口函数,但C语言不具备根据接受消息的对象自动决定调用该对象的窗口函数的能力(在面向对象的语言中,这种能力被称为多态性)。因此,向不同的窗口对象发送消息时使用函数SendMessage对窗口函数作间接调用,由Windows根据该函数调用中所使用的对象标识符来调用该对象的窗口函数。
在程序设计中由于窗口函数的限制,需经常进行各种各样的数据类型转换。例如:
SendMessage(hwnd, WM_USER, (WPARAM)5, MAKELPARAM(89, 3267));
在这个例子中,为了组建一个LPARAM类型的数据,使用了宏MAKEPARAM。它将两个十六位的数据组装成一个32位的数据(低位字为MAKEPARAM的第一个参数,高位字为第二个参数)。当需要从一个LPARAM类型的数据中分离出低位字和高位字时,使用宏LOWORD和HIWORD。例如,处理上个例子中所发送的WM_USER消息的窗口函数的代码可能为:
WORD wStart = LOWORD(lParam);
WORD wStart = LOWORD(lParam);
宏MAKELRESULT与MAKELPARAM类似,它被用于装配LRESULT类型的数据。宏MAKELONG用于装配LONG类型的数据,当需要从LRESULT或LONG类型的数据中分离出高位字和低位字时,使用宏HIWORD和LOWORD。
基于上面的介绍,我们在设计Windows应用程序时,要明确程序中存在哪些对象,对象之间是如何通过消息传递程序控制的,哪些数据是对所有对象公有的, 哪些数据是私有于某一个对象的,公有数据和对象的私有数据必须是存储在静态生存期的变量中(局部生存期的变量在窗口函数返回后就消失了,不能在下次调用该函数时保存上次的值。换句话说,存储对象的数据的变量的生存期不应小于对象的生存期)。
由于Windows应用程序各个模块之间主要是通过消息传递控制,因此,Windows应用程序的逻辑结构就不同于MS-DOS应用程序的逻辑结构,如图1-1所示。从图1-1可以看出,Windows应用程序的各个模块通过消息传递被联系在一起,因此,如果正确地组织程序,程序的模块性和结构较MS-DOS应用程序要好。
图1-1 DOS应用程序与Windows应用程序逻辑结构的比较示意说明
Windows程序的组织
将1.9节介绍的程序按照C/C++语言的要求组织起来,就得到一个完整的Windows程序。一个Windows程序必须有一个名为WinMain的主函数。
// 1-1.c 代码片段
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int PASCAL WinMain(
HINSTANCE hInstance, // 应用程序的实例句柄
HINSTANCE hPrevInstance, // 该应用程序前一个实例的句柄
LPSTR lpszCmdLine, // 命令行参数串
int nCmdShow) // 程序在初始化时如何显示窗口
{
char szAppName[] = "Window";
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
if (!hPrevInstance) {
// 该实例是程序的第一个实例,注册窗口类
wndclass.style = CS_VREDRAW | CS_HREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;
if (!RegisterClass(&wndclass))
// 如果注册失败
return FALSE;
}
// 对每个实例,创建一个窗口对象
hwnd = CreateWindow(
szAppName,
"Sample Program",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
while(GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
WinMain函数是Windows应用程序开始执行时的入口点,它的返回类型为int。WinMain函数的作用十分类似于MS-DOS中的C应用程序的main函数。
WinMain带有四个参数。参数hInstance和hPrevInstance是程序的实例句柄。在Windows环境下,可以运行同一个程序的多个拷贝,每一个拷贝都是该应用程序的一个句柄,每个实例使用一个实例句柄进行标识。hInstance是标识当前程序的实例的句柄,它的值不会为NULL。如果在此之前Windows中已经运行了该程序的另一个实例,则这个实例的句柄由参数hPrevInstace给出。如果在运行该程序时,Windows环境中不存在该程序的另一个实例,则hPrevInstance为NULL。
我们曾经说过,对同一个类,不能向Windows注册一次以上。在这个程序中,通过判别hPrevInstance的值是否为NULL,来决定是否应向Windows注册窗口类。这样的程序逻辑保证了只在该程序的第一个实例中注册窗口类。
参数lpszCmdLine中包含有运行程序时传递给程序的命令行参数。例如,若以这样的命令运行该程序。Sample.exe Programming Windows。则lpszCmdLine将指向字符串“Programming Windows”。
最后一个参数nCmdShow是一个int类型的整数,用以说明在程序被装如内存时,Windows以何种方式显示这个程序的窗口。根据运行程序的方式不同,该参数被设置为SW_SHOWNORMAL或SW_SHOWMINNOACTIVE,SW的含义是“Show Window”(显示窗口),这两个参数的含义在后面介绍。
在程序Sample.CPP中,有几个函数我们未曾介绍。表1-3给出了这些函数的说明。
表1-3-1 ShowWindow 函数
用 途 | 显示或改变给定的窗口 | ||||||||
原 型 |
| ||||||||
返回值 | 返回该窗口更新前的窗口状态。对先前可见的窗口,其值为非零。对先前隐藏的窗口,其值为零。 |
显示方式(nCmdShow)可以是下列常量之一:
类型 | 说明 |
SW_HIDE | 隐藏该窗口(并是另一个窗口激活) |
SW_MINIMIZE | 使窗口变成图标(并激活窗口管理表的顶层窗口) |
SW_SHOW | 激活一个窗口,并根据其当前的尺寸和位置显示该窗口 |
SW_SHOWMAXIMIZED | 激活并以全屏方式显示一个窗口 |
SW_SHOWMINIMIZED | 激活并以图标方式显示一个窗口 |
SW_SHOWMINNOACTIVE | 以图标方式显示一个窗口,当前活动的窗口仍保持活动 |
SW_SHOWNA | 以当前状态显示一个窗口,当前活动的窗口仍保持活动 |
SW_SHOWNOACTIVE | 以最近的大小和位置显示一个窗口,当前活动的窗口仍保持活动 |
SW_SHOWNORMAL | 激活并显示一个窗口,若其为图标或全屏方式显示,则恢复为它的原始大小和位置 |
SW_RESTORE | 同SH_SHOWNORMAL |
表1-3-2 UpdateWindow 函数
用 途 | 若应用程序的消息队列中存在WM_PAINT消息(绘制用户区消息),则该函数使Windows立即调用窗口函数,向其传递WM_PAINT。否则该函数不作为任何动作。 | ||||||
原 型 |
| ||||||
返回值 | 无 |
表1-3-3 GetMessage 函数
用 途 | 从应用程序中的消息队列中检索一条消息。 | ||||||||||||
原 型 |
| ||||||||||||
返回值 | 在检索出WM_QUIT消息时,返回零值,在其它情况下返回非零值。 |
表1-3-4 DispatchMessage 函数
用 途 | 将消息发送到指定的窗口对象上(窗口函数被调用)。 | ||||||
原 型 |
| ||||||
返回值 | 若有一个WM_CHAR消息被放到应用程序的消息队列中,返回非零,否则返回零。该函数不改变lpMsg所指向的变量中存储的消息数据。 |
Windows的主函数都是首先以初始化(注册类、创建对象等)这一步开始,而且紧跟着就是消息循环运行这一步。这些步骤对所有的Windows应用程序都大同小异。Windows应用程序主要的不同点在窗口函数的定义上,由于一个应用程序所解决的任务不同,它的窗口函数对消息的处理方式也就不相同,因而每个应用程序需要定义不同的窗口函数。
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0):
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
这个窗口函数仅处理一条WM_DESTROY消息。这条消息是在用户关闭了屏幕上的窗口时,Windows发送给窗口对象的。该函数对这条消息的处理只是简单地调用Windows函数PostQuitMessage。表1-4给出了函数PostQuitMessage的说明。当主函数的消息循环中的GetMessage函数检索出WM_QUIT消息时,函数GetMessage返回零,这样,消息循环终止,程序也随之被终止。存储消息数据的变量msg的wParam域的值是在调用函数PostQuitMessage时所提供的实参的值。如果程序正常结束,调用PostQuitMessage函数时使用零作为该函数的参数,如果需要表示程序由于出现了异常或错误而必须终止时,使用非零值(一般使用-1)作为该函数的参数。在调用PostQuitMessage使用的参数值被主函数用语句:
return msg.wParam;
返回给Windows,供Windows或其它应用程序使用。因此,我们也称PostQuitMessage使用的参数为程序的退出码。
表1-4 PostQuitMessage 函数
用 途 | 通知Windows,应用程序希望中止。它一般用于响应WM_DESTROY消息。该函数将消息WM_QUIT消息放入应用程序的消息队列中。 | ||||||
原 型 |
| ||||||
返回值 | 无 |
小结
首先介绍了图形用户界面的优点和面向对象的程序设计方法。从某种意义上说,Windows是面向对象的,它主要建立在把窗口作为一个对象的概念上。窗口之间通过消息进行消息传递。
Windows支持直接操作技术。直接操作是对屏幕对象的操作,数据和函数的封装允许该对象自己响应它们接收到的消息。在用户界面上发生的任何事件被作为消息发送给窗口对象。程序员在设计程序时,只须关心一个对象要接受哪些消息和怎样处理这些消息。消息传递工作由Windows负责。因而,使用Windows操作环境可以极大地方便程序开发用户界面的工作,并使程序的结构合理、模块化程序高。更重要的是,支持直接操作技术的Windows支持用户进行有创造性的界面设计。
赞助商链接