概述(三)

1.7、应用程序使用的一些术语
  本节介绍Winodws应用程序使用的一些术语及其相关概念,在后面的章节中介绍有关的内容时,再对其中的概念进行详细的讨论。

  1.7.1 模块
  在Windows中,术语“模块”一般是指任何能被装入内存中运行的可执行代码和数据的集合。更明确地讲,模块指的就是一个.EXE文件(又称为应用程序模块),或一个动态链接库(DLL — Dynamic Linking Library,又被称为动态链接库模块或DLL模块),或一个设备驱动程序,也可能是一个程序包含的能被另一个程序存取的数据资源。模块一词也被用于特指自包含的一段程序。例如,一个可单独编译的源文件,或该源文件被编译器处理之后所生成的目标程序。当制作一个程序时,模块一词用于指被连接在一起的许多模块中的某个模块。
  Windows本身由几个相关的模块组成,Windows API函数就是在Windows启动时装入内存中的几个动态链接库模块实现的。其中的三个主要模块是USER.EXE(用于窗口管理等)、KERNEL.EXE(用于内存管理的多任务调度)和GDI.EXE(图形设备接口,用于图形输出等)。

  1.7.2 应用程序
  一个Windows应用程序是被Windows调用或在Windows下运行的一个程序,这个程序可以调用静态连接库(也就是C的运行时间库)中的函数和DLL的函数,它也可以启动其它的应用程序。一个应用程序在运行时的输入被Windows捕获,并以消息的形式传送到应用程序的活动窗口上。一个应用程序的输出也是通过Windows进行的,所有的输出首先被送给Windows。许多MS-DOS应用程序基本上占据整个计算机,并认为所有的计算机资源只属于该应用程序,应用程序告诉相对被动的MS-DOS应做什么。在一个Windows应用程序中,Windows自身是非常主动的,并且和应用程序协同得非常紧密。Windows管理着计算机的所有资源,并调度这些资源,使它们可为正在Windows上运行的所有应用程序共享。

  1.7.3 任务和实例
  Windows将运行的应用程序实例作为不同的任务。当一个应用程序的多个实例在运行时,它们也被Windows当作不同的任务。Windows为一个模块的每一个实例都装入一个缺省数据段,但可执行代码只能装入一次。也就是说,同一个模块的实例共享相同的代码,但有自己私用的数据段。
  对每一个模块、任务或实例,Windows分别使用一个句柄来标识它。在窗口对象的私有数据存储区存储有一个应用程序的任务句柄、实例句柄和模块句柄。任务句柄被Windows的任务调度程序用于进行任务调度。通过模块句柄,Windows可以知道一个模块当前有多少实例正在运行。同一个模块的不同实例有相同的模块句柄,但有不同的任务和实例句柄。当Windows由于内存管理的需要而废弃了一个实例的代码段时,通过模块句柄,Windows可以从模块中重新装入这个实例所需的代码。

  1.7.4 动态链接库
  DLL是一种有别于MS-DOS应用程序所使用的库模块(例如 C的运行时间库)的一种特殊的库模块,它含有可能 被其它应用程序调用的函数。一个DLL在运行时被动态地连接到一个应用程序中或另一个DLL中,而不是在制作应用程序时静态地连接到应用程序中的(这种方法是在制作MS-DOS应用程序中使用的方法,它们在Windows应用程序中仍然可以继续被使用)。使用DLL的好处在于,当有多个应用程序使用同一个DLL并且同时在Windows中运行时,该DLL在内存中只有一个实例。

  1.7.5 应用程序设计接口
  应用程序设计接口(API)是应用程序用于操作周围环境的一组函数调用接口。Windows API大约有600多个函 数,学习Windows程序设计的许多工作就是学习如何使用这些API。

  1.7.6 Windows下的函数
  在进行Windows应用程序设计中,程序员除了需要知道有关一个函数的常用信息(例如函数的名字,近函数或远函数,返回类型以及应如何调用)之外,同时还要知道更多的内容:一个回调函数、引出函数或是一个引入函数。
  引出函数:这个术语与一个函数如何在一个模块中说明而在另一个模块中被调用有关。引出函数是在一个模块中定义而在这个模块之外被调用的一种函数;或是被Windows或是被另一个模块调用。这些函数必须以一种特定的方式进行说明,并被编译器作特殊的处理。这样,当它们被调用时,它们会被正确地束定到合适的数据段上。DLL为其它模块提供要被调用的函数,因此,每个DLL一般都带有一个DLL库,以便应用程序可以合法地调用DLL中的函数。DLL库由DLL中每个引出函数的入口点组成。整个Windows API就是由构成Windows环境的不同的模块所引出的函数组成,这些API函数的入口点在一个名为IMPORT.LIB的DLL库中说明。
  引入函数:在DLL中引出的函数若要能为一个模块调用,必须在这个模块中将这个函数说明为引入函数。由此可见引出函数和引入函数表达的是从两种角度处理同一个函数的术语:引出模块中的一个函数使得这个函数能被其它模块调用;调用引出函数的模块通过引入这个函数才能调用它。在制作Windows应用程序时,连接器自动包含一个名为IMPORT.LIB的库文件。这个文件允许应用程序调用Windows API中的函数。这个文件被称为引入库。引入库提供了应用程序与一个到多个DLL中可被这个应用程序调用的函数之间的连接。
  回调函数:回调函数是一种特殊的引出函数,是由 Windows环境直接调用的函数。一个应用程序至少要有一个回调函数。当一条消息要交给应用程序处理时,Windows调用这个回调函数。这个函数对应于一个活动窗口,被称为这个窗口的窗口函数。因为许多应用程序至少建立一个窗口,并且Windows需要向这个窗口发送消息,所以,处理消息的函数必须由Windows调用。在请求Windows枚举它所维护的对象时,例如字体或窗口,Windows也要调用应用程序中的回调函数。当向Windows提出这样的请求时,就必须向Windows提供回调函数的地址。
  由于引出函数是在不同的模块中被调用的,也就是说,调用者的代码段与被调用的引出函数的代码段不在同一个段中,因此,在所开发的Windows应用程序中,引出函数都被说明为远函数。为了程序运行的效率原因,引出函数都使用Pascal调用约定,这种调用约定不同于C调用约定的地方在于:
最左边的参数先入栈:Pascal调用约定的参数进入栈的顺序是函数调用中最左边的参数先入栈。C的调用约定与此相反,它采用最右边的参数先入栈。
被调用的函数负责从展中清除参数:Pascal调用约定的函数在返回时负责清除栈中的参数;C调用约定的函数不作这种工作,而由调用者来作;这样,当程序中调用了大量的使用C调用约定的函数时,为清除栈中的参数,在程序中要额外地增加许多代码。
全局标识符不保持原来的大小写(一般被为大写形式),也不在标识符前面加下划线。
  为便于程序开发活动,在Windows.h中定义了两个类型名,用于在程序说明引出函数:
类型    说明
WINAPI    等价于FAR PASCAL,说明该函数是一个引出函数,这个类型名只用于在DLL中说明引出函数,或在应用程序中对DLL中的引出函数进行函数说明时。
CALLBACK    等价于FAR PASCAL,说明该函数是一个回调函数,它常被用在应用程序模块中说明一个窗口函数或其它种类的回调函数。



1.8、事件和消息
  在Windows中,用户或系统中所发生的任何活动被当作事件来处理,例如,用户按下了鼠标按钮,就产生一鼠标事件。对于所发生的每一个事件,Windows将其转换成消息的形式放在一个称为消息队列的内存区中,然后由Windows的消息发送程序选择适合的对象,将消息队列中的消息发送到欲接受消息的对象上。Windows的消息可分为四种类型:
  (1)输入消息:对键盘和鼠标输入作反应。这类输入消息首先放在系统消息队列中,然后Windows将它们送入应用程序的消息队列,使消息得到处理。
  (2)控制消息:用来与Windows的特殊控制对象,例如,对话框、列表框、按钮等进行双向通信。这类消息一般不通过应用程序的消息队列,而是直接发送到控制对象上。
  (3)系统消息:对程式化的事件或系统时钟中断作出反应。有些系统消息,例如大部分DDE消息(程序间进行动态数据交换时所使用的消息)要通过Windows的系统消息队列。而有些系统消息,例如窗口的创建及删除等消息直接送入应用程序的消息队列。
  (4)用户消息:这些消息是程序员创建的,通常,这些消息只从应用程序的某一部分进入到该应用程序的另一部分而被处理,不会离开应用程序。用户消息经常用来处理选单操作:一个用户消息与选单中的一选项相对应,当它在应用程序队列中出现时被处理。
  Windows应用程序通过执行一段称为消息循环的代码来轮询应用程序的消息队列,从中检索出该程序要处理的消息,并立即将检索到的消息发送到有关的对象上。典型的Windows应用程序的消息循环的形式为:

  MSG  msg;
  while (GetMessage(&msg, NULL, 0, 0L))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  函数GetMessage从应用程序队列中检索出一条消息,并将它存于具有MSG类型的一个变量中,然后交由函数TranslateMessage对该消息进行翻译,紧接着,函数DispatchMessage将消息发送到适当的对象上。有关这三个函数的更多的细节在1.10节里介绍.

1.9、窗口对象
  对Windows用户和程序员而言,窗口对象(简称窗口)是一类非常重要的对象。尤其对程序员,窗口的定义和创建以及对窗口的处理过程最能直观地反映出Windows中面向对象的程序设计的四个基本机制(类、对象、方法、和消息)。

  1.9.1 窗口类
  如前所述,在程序中创建对象,必须先定义对象所属的类。在Windows中,窗口类是在类型为WNDCLASS的结构变量中定义的,在Windows.h中,结构类型WNDCLASS的说明为:

  typedef struct tagWNDCLASS {
     DWORD style;         /* 窗口风格 */
     WNDPROC *lpfnWndProc;    /* 窗口函数 */
     int cbClsExtra;       /* 类变量占用的存储空间 */
     int cbWndExtra;       /* 实例变量占用的存储空间 */
     HINSTANCE hinstance;    /* 定义该类的应用程序实例的句柄 */
     HICON hicon;        /* 图标对象的句柄 */
     HCURSOR hCursor;      /* 光标对象的句柄 */
     HBRUSH hbrBackground;    /* 用于擦除用户区的刷子对象的句柄 */
     LPCSTR lpszMenuName;    /* 标识选单对象的字符串 */
     LPCSTR lpszClassName;    /* 标识该类的名字的字符串 */
  } WNDCLASS;

  WNDCLASS类型有十个域,它描述了该类的窗口对象所具有的公共特征和方法。在程序中可以定义任意多的窗口类,每个类的窗口对象可以具有不同的特征。lpszClassName是类的名字,在创建窗口对象时用于标识该窗口对象属于哪个类。lpfnWndProc是指向函数的一个指针,所指向的函数应具有下述的函数原型:

  LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,LPARAM lParam);

  该函数被称为窗口函数,其中定义了处理发送到该类的窗口对象的消息的方法。窗口函数是一个回调函数,因此在定义窗口函数时要使用CALLLBACK类型进行说明。参数hWnd是一个窗口对象的句柄。通过该句柄,一个窗口函数可以检测出当前正在处理哪个窗口对象的消息。参数message是消息标识符,参数wParam和lParam是随同消息一起传送来的参数,随着消息的不同,这两个参数所表示的含义也不大相同,在定义消息时对这两个参数的含义一同进行定义。
  域hIcon、hCursor和hbrBackground分别定义窗口变成最小时所显示的图标对象的句柄,当光标进入该类的窗口对象的显示区域时所显示的光标对象的句柄,当需要擦除用户区域显示的消息时所使用的刷子对象的句柄(该刷子作用的结果形成窗口用户区的背景色)。
  域style规定窗口的风格,它可用下列常量经位或运算之后形成:
类型    说明
CS_HREDRAW    如果窗口的水平尺寸被改变,则重画整个窗口
CS_VREDRAW    如果窗口的垂直尺寸被改变,则重画整个窗口
CS_BYTEALIGNCLIENT    在字节边界上(在X方向上)定位用户区域的位置
CS_BYTEALIGNWINDOW    在字节边界上(在X方向上)定位窗口的位置
CS_DBLCLKS    当连续两次按动鼠标键时向窗口发送该事件的消息
CS_GLOBALCLASS    定义该窗口类是一个全局类。全局类由应用程序或库建立,并且所有的应用程序均可使用全局类
CS_NOCLOSE    禁止系统选单中的Close选项


  还有其他一些常量,在后面的章节中介绍有关内容时再进行讨论。
  域lpszMenuName指向一个以‘/0’字符(称为空字符或NULL字符)结尾的字符串,用于标识该窗口类的所有对象所使用的缺省选单对象。如果该域为NULL,则表示没有缺省选单。
  域hInstance用于标识定义该窗口类的应用程序的实例句柄。每一个窗口类需要一个实例句柄来区分注册窗口类的应用程序或DLL,该实例句柄用于确定类属。当注册窗口类的应用程序或DLL被终止时,窗口类被删除。
  WNDCLASS类型规定了该类窗口对象的基本数据表示和处理消息的窗口函数,但是,在有些应用程序中,单有这些是不够的。因此,该类型提供了两个域cbClsExtra及cbWndExtra,指示系统分配额外的存储空间用于存储一些附加数据。其中cbClsExtra定义可以为该类的所有对象共用的数据占用的存储空间的大小(以字节计);而cbWndExtra用于定义该类的每个对象私用的数据占用的存储空间的大小(以字节计),一个对象可以在该私有存储空间中存储一些数据,但该类的其他对象不能访问到这个对象所存储的这些私用数据。而在公用存储空间中所存的数据可被该类的所有对象访问到。函数SetClassWord/SetClassLong和GetClassWord/GetClassLong用于访问公用数据,函数SetWindowWord/SetWindowLong和函数GetWindowWord/GetWindowLong用于访问特定对象的私用数据,这些函数在“窗口对象”一章讨论。
  当程序员设置了WNDCLASS变量的各个域之后,使用函数RegisterClass向Windows注册这个类,至此,完成了定义一个窗口类的过程。函数RegisterClass的原型为:

  BOOL RegisterClass(LPWNDCLASS lpWndClass);

  该函数唯一的一个参数是指向WNDCLASS类型的变量的指针。函数返回非零,表示注册成功,否则注册失败。不能向Windows注册具有相同名字(lpszClassName域指向相同的两个字符串)的两个类,否则第二次注册失败并被忽略。下面是定义和注册窗口类的程序示例说明:

  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( BLACK_BRUSH);
  wndclass.lpszMenuName = NULL;
  wndclass.lpszClassName = "Window";

  if (!RegisterClass(&wndclass))
    ...  / * 处理类注册错误 * /

  其中,WndProc是一个窗口函数名,变量hInstance存储着当前程序实例的句柄。Windows预定义了一些图标、光标和刷子对象,函数LoadIcon返回预定义的应用程序图标的句柄,该图标由第二个参数IDI_APPLICATION来定义。函数LoadCursor返回标准箭头光标(IDC_ARROW)的句柄,函数GetStockObject返回库存对象中一个白色刷子(WHITE_BRUSH)的句柄。

  1.9.2 创建窗口对象
  在上一节中,我们介绍了窗口类的定义方法,窗口的某些特征(如窗口的颜色等)属于窗口类中定义的,并由该窗口 类的所有实例共享。在注册了窗口类之后,程序员使用函数CreateWindow创建窗口,得到窗口类的一个实例(一个窗口对象)的句柄。一个窗口可以是一个重叠式窗口,或是一个弹出式窗口,或是一个隶属窗口,或是一个子窗口,这在使用CreateWindow函数时指定。每一个子窗口都有一个父窗口,每一个隶属窗口都有一个拥有者,这个拥有者是另一个窗口对象,弹出式窗口是一种特殊的窗口,这些内容在“窗口对象”一章介绍。

  表1-1 CreateWindow 函数用 途    创建一个重叠窗口、弹出式窗口、隶属窗口或子窗口
原 型        HWND CreateWindow(     
  LPCSTR lpClassName,    类名,指定该窗口所属的类。
  LPCSTR lpWindowName,    窗口的名字,即在标题栏中显示的文本。
  DWORD dwStyle,    该窗口的风格,在后面详细介绍。
  int x,    窗口左上角相对于屏幕左上角的初始X坐标。
  int y,    窗口左上角相对于屏幕左上角的初始Y坐标。
  int nWidth,    窗口的宽度。
  int nHeight,    窗口的高度。
  HWND hWndParent,    一个子窗口的父窗口的句柄,或隶属窗口的拥有者窗口的句柄,若没有拥有或者父窗口,则为NULL。
  HMENU hMenu,    选单句柄,如果为NULL,则使用类中定义的选单。如果建立的是一个子窗口,该参数是一个子窗口标识符,使用此标识符来区分多个窗口。
  HINSTANCE hInstance,    创建窗口对象的应用程序的实例句柄。
  VOID FAR * lpParam    创建窗口时指定的额外参数。
);     


返回值    返回值是标识所创建的窗口对象的句柄,如果返回值为NULL,则窗口没有被创建。


  函数CreateWindow的第三个参数指定窗口的风格,表1-2是在Windows.h中定义的一些常用到的风格常量,通过将这些常量使用位运算组合在一起,形成所要求的窗口风格。

  表1-2 窗口风格 类型    说明
WS_BORDER    创建一个有边框的窗口
WS_CAPTION    创建一个有标题栏的窗口
WS_CHILDWINDOW(or WS_CHILD)    创建一个子窗口(不能与WS_POPUP一起使用)
WS_CLIPCHILDREN    当在父窗口内绘制时,把子窗口占据的区域剪切在外,即不在该区域内绘图
WS_CLIPSIBLINGS    裁剪相互有关系的子窗口,不在被其它子窗口覆盖的区域内绘图,仅与WS_CHILD一起使用
WS_DISABLED    创建一个初始被禁止的窗口
WS_DLGFRAME    创建一个有双边框但无标题的窗口
WS_HSCROLL    创建一个带水平滚动杠的窗口
WS_VSCROLL    创建一个带垂直滚动杠的窗口
WS_ICONIC    创建一个初始为图标的窗口,仅可以与WS_OVERLAPPEDWINDOWS一起使用
WS_MAXIMIZE    创建一个最大尺寸的窗口
WS_MINIMIZE    创建一个最小尺寸的窗口(即图标)
WS_MAXIMIZEBOX    创建一个带有极大框的窗口
WS_MINIMIZEBOX    创建一个带有极小框的窗口
WS_OVERLAPPED    创建一个重叠式窗口,重叠式窗口带有标题和边框
WS_POPUP    创建一个弹出式窗口,不能与WS_CHILD一起使用
WS_SYSMENU    窗口带有系统选单框,仅用于带标题栏的窗口
WS_THICKFRAME    创建一个边框的窗口,使用户可以直接缩放窗口
WS_VISIBLE    创建一个初始可见的窗口



  在Windows.h中,还定义了风格WS_OVERLAPPEDWINDOW和WS_POPUPWINDOW。其中,WS_OVERLAPPEDWINDOW由下面的宏进行定义:

  #define WS_OVERLAPPEDWINDOW(
    WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU |
    WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
  )

  WS_POPUPWINDOW定义为:

  #define WS_POPUPWINDOW ( WS_BORDER | WS_POPUP | WS_SYSMENU )

  但是,在使用WS_POPUPWINDOW时,必须组合WS_CAPTION ,否则不能使系统选单(WS_SYSMENU)在窗口上可见。另外 两个窗口风格是WS_GROUP和WS_TABSTOP,这两个窗口风格的意义在介绍对话框时进行介绍,在介绍对话框时,还将介绍其它窗口风格。
  CreateWindow函数的x和y参数是窗口左上角相对于屏幕左上角的坐标。这两个参数可以使用常量CW_USEDFAULT,用于表示使用缺省位置。缺省时,Windows显示各个重叠窗口的位置在水平方向的垂直方向上均与屏幕左上角有一个相应的偏移值。nWindth和nHeight参数也可以使用常量CW_USEDEFAULT来指定,这时,Windows使用缺省的窗口尺寸。缺省的窗口尺寸在水平方向延伸到屏幕的右边界,在垂直方向延伸到屏幕底部显示图标区域的上方。
  下面的程序说明在Windows程序中创建一个窗口对象的基本方法,所创建的窗口对象所属的类为在1.9.1节定义的“Window”窗口类。

  HWND hWnd;

  hWnd = CreateWindow(
    "Windows",
    "Sample Program",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT,CW_USEDEFAULT,
    CW_USEDEFAULT,CW_USEDEFAULT,
    NULL,   // 没有父窗口
    NULL,   // 使用类选单
    hInstance, // 变量hInstance中存储有当前程序实例的句柄
    NULL,   // 没有额外数据
  };

  其中所使用的符号“//”是C++语言新增加的单行注释符,它表示从“//”开始到它所在的行的结尾所有内容都是注释。

  1.9.3 窗口函数
  在前面两小节中,我们介绍了定义类和创建对象的过程。本节介绍窗口对象如何接收和处理所有影响窗口的事件(如击键或按动鼠标键)的消息。一个窗口对象所接受到的消息的响应是由该对象的方法决定的,这些方法被定义在一个称为窗口函数的函数中。同一类的所有对象共用同一个窗口函数。窗口函数决定着对象如何用内部方法对消息作出响应,例如,如何在屏幕上画出窗口自身。
  一个最简单的窗口函数为:

  LRESULT CALLBACK WndProc(HWND hwnd, UNIT message, WPARAM wParam, LPARAM lParam)
  {
    return DefWindowProc (hwnd, message, wParam, lParam);
  }

  该窗口函数通过调用Windows的函数DefWindowProc(缺省窗口函数),让Windows的缺省窗口函数来处理所有发送到窗口对象上的消息。
  当用户操作屏幕上的一个窗口对象时(例如用户改变了屏幕上窗口对象的位置或大小)或发生其它事件时,该事件的消息被存于应用程序的消息队列中, 消息循环首先从该队列中检索出该消息,然后将消息发送到某个对象上。发送过程由Windows来控制,Windows根据消息结构中的hWnd域所指示的消息发送的目标对象,调用该对象所在类的窗口函数完成消息的发送工作。窗口函数根据消息的种类 ,选择执行一段代码(方法),对消息进行处理,并通过return语句回送一个处理结果或状态。消息循环、Windows和窗口函数协同配合,完成一条消息的发送和处理。在处理完一条消息之后,如果应用程序队列中还有其他消息,继续进行上述处理过程,否则,应用程序在消息循环处理进行等待。

  1.9.4 处理消息
  窗口对象接收到的每条消息由参数message来标识,随同该消息一传递过来的其它数据由参数wParam和lParam给出。wParam用于十六位的数据,而lParam用于32位的数据。
  在窗口函数中,使用switch语句来判断窗口函数接收到什么消息,通过执行相应的语句对消息进行处理。当处理完一条消息时,窗口函数要返回一个值,表示消息的处理结果,许多消息返回0值,有些要求返回其它的值,这由具体的消息决定。窗口函数不打算处理的消息必须交由DefWindowProc()进行处理,并且函数必须返回DefWindowProc()的返回值。
  窗口函数的基本结构为:

  LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
  {
    变量说明语句
    初始化语句
    switch (message)
    {
      case 消息 1:
        处理“消息 1”的语句序列
        return 表达式 1;

      case 消息 2:
        处理“消息 2”的语句序列
        return 表达式 2;

         .......

      case 消息 n:
        处理“消息 n”的语句序列
        return 表达式 n;
    }
    return DefWindowProc(hwnd, message, wParam, lParam);
  }

  Windows为预定义的每种消息都指定了一个以WM(Window Message)为前缀的标识符常量。下面的窗口函数处理一条WM_DESTROY消息。

  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);
  }

  在1.11节,我们结合对Windows程序结构的介绍再详细解释窗口是如何处理WM_DESTROY消息的。

1.10、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应用程序逻辑结构的比较示意说明

1.11、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”(显示窗口),这两个参数的含义在后面介绍。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值