循序渐进Windows 编程(三)

第三天课程 — 标准的 Windows 程序

  本章和下一章的内容主要是分析一个典型的 Windows 程序的底层结构。本章介绍下列内容:

  • WinMain 函数
  • RegisterClass 函数
  • CreateWindow 函数
  • 消息循环
  • WndProc 函数
  • WINDOWS.H 文件
  • 匈牙利命名法

3.1 具有 GUI 功能的第一个应用程序


  现在给你们展示一个完整的 Windows 程序,这个程序设计的既灵活又耐用(见程序清单 3.1)。这是 一个多模块程序,说明了一个标准的 Windows 程序所有的主要组成部分。现在,我们集中精力准备运行它。 记住,它是由几个模块组成的。

  • 程序清单 3.1  Window1 程序
    ///
    // Program Name:Window1
    // Programmer: skyline
    // Description: Example Windows program 
    ///
    
    #define STRICT 
    #define WIN32_LEAN_AND_MEAN
    #include&ltwindows.h>
    #include&ltwindowsx.h>
    #pragma warning (disable:4068)
    #pragma warning (disable:4100)
    static char szAppName[]="skyline";
    
    LRESULT CALLBACK WndProc(HWND hwnd,UINT Message,
    	WPARAM wParam,LPARAM lParam);
    BOOL Register(HINSTANCE hInst);
    HWND Create(HINSTANCE hInst,int nCmdShow);
    
    //
    // The WinMain is the program entry point.    
    //
    #pragma argsused                                         
    int WINAPI WinMain(HINSTANCE hInst,HINSTANCE hPrevInstance,
    	LPSTR lpszCmdParam,int nCmdShow)
    {
        MSG Msg;
    
        if(! hPrevInstance)
        if(! Register(hInst)) 
    	return FALSE; 
    
        if(! Create(hInst,nCmdShow))
     	return FALSE;
     	
        while(GetMessage(&Msg,NULL,0,0))
        {
    	TranslateMessage(&Msg);
    	DispatchMessage(&Msg);              
        }
        return Msg.wParam;
    }  
    
    
    // Register the window
    
    BOOL Register(HINSTANCE hInst)
    {
        WNDCLASS WndClass;
    
        WndClass.style = CS_HREDRAW|CS_VREDRAW;
        WndClass.lpfnWndProc = WndProc;
        WndClass.cbClsExtra = 0;
        WndClass.cbWndExtra = 0;
        WndClass.hInstance = hInst;
        WndClass.hIcon = LoadIcon(NULL,IDI_APPLICATION);
        WndClass.hCursor = LoadCursor(NULL,IDC_ARROW);
        WndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
        WndClass.lpszMenuName = NULL;
        WndClass.lpszClassName = szAppName;
     
        return (RegisterClass(&WndClass)!=0);
    }
    
    
    // Create the Window and show it.
    
    HWND Create(HINSTANCE hInst,int nCmdShow)
    {                               
        HWND hwnd = CreateWindowEx (0,szAppName,szAppName,
    		WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,
    		CW_USEDEFAULT,CW_USEDEFAULT,
    		NULL,NULL,hInst,NULL);
        if(hwnd == NULL)
    	return FALSE;
    
        ShowWindow(hwnd,nCmdShow);
        UpdateWindow(hwnd);
    
        return hwnd;
    }
    
    #define skyline_DefProc DefWindowProc
    void skyline_OnDestroy(HWND hwnd);
                    
    //
    // The window proc is where message get processed
    //
    
    LRESULT CALLBACK WndProc(HWND hwnd,UINT Message,
    		WPARAM wParam,LPARAM lParam)  
    { 
        switch(Message)
        {
      	HANDLE_MSG(hwnd,WM_DESTROY,skyline_OnDestroy);
    
          default:
      	return skyline_DefProc(hwnd,Message,wParam,lParam);
        }
    }
    
    #pragma argsused
    
    void skyline_OnDestroy(HWND hwnd)
    {
        PostQuitMessage(0);
    }
    

  Window1 程序是由上述所有文件进行编译后产生的。该程序创建一个常见窗口的样本,Windows 环境 的下的所有用户对这种窗口都十分熟悉。
  如果你还没有准备好,你应该花足够的时间来看看 Window1 的动作。当编译这个程序时,要记住它是 由几个不同的模块放到一起组成这个完整的程序的。就像由一个换档器、方向盘和传动机构以及其它机器零 件才能构成一辆汽车一样,在 Windows 程序中的每个模块都为创建最终的可执行文件作出自己的贡献。
  当你建立程序并能运行程序的时候,你应当花几分钟时间把它试运行一下。试试把窗口最大化、最小化 的操作,再试着用鼠标拉拽它的边界来改变它的形状,这样给自己一点时间来理解一下已录入并编译好的代 码的功能。

3.2 WinMain 函数和 WndProc 的概念性介绍


  下面是 Window1 程序的主函数:

  • #pragma argsused                                         
    int WINAPI WinMain(HINSTANCE hInst,HINSTANCE hPrevInstance,
    	LPSTR lpszCmdParam,int nCmdShow)
    {
        MSG Msg;
    
        if(! hPrevInstance)
        if(! Register(hInst)) 
    	return FALSE; 
    
        if(! Create(hInst,nCmdShow))
     	return FALSE;
     	
        while(GetMessage(&Msg,NULL,0,0))
        {
    	TranslateMessage(&Msg);
    	DispatchMessage(&Msg);              
        }
        return Msg.wParam;
    }
    

  正如前面所说的那样,WinMain 函数起到标准 DOS 程序中 Main() 函数的作用。用 C 或 C++ 编写的 Windows 应用程序总是从调用 WinMain 函数开始的。
  WinMain 函数分成三个部分。第一部分是调用 Register 函数,第二部分是调用 Create 函数,第三部 分处理程序消息的一个 While 循环。在我详细讲解之前,反复记住这三个步骤:Register、Create、进入消 息循环,这可能对你是有帮助的。
  现在花一点时间理解一下下面的比喻:
  Register 函数可以看作是购买一辆汽车之前做的纸面准备工作。你和推销商讨论你要购买哪一“类”汽车。 你告诉他想买带移动操纵杆的旅行车 Volvo 或想买 Mercedes 车。Register 函数允许你描述你要创建的窗 口类型并指明是否有特殊的颜色和图标等等。
  而 Create 函数的动作类似于打开某个特定汽车的车门,坐在前座然后点火。现在,汽车正式归你所有。 你以买下这辆车——不是特定的一类车,而是一辆具体指定的车辆。也就是说,就在此时,你创建了由 Register 函数描述的窗口的一个特定的实例。
  还可以进一步完善这个比喻。你可以把消息循环和 WndProc 想象成类似于拥有汽车和维护汽车所要做的事 情,也就是你使用和照料汽车的这些事情。简单地说,在程序运行时它们负责管理窗口。
  这种比喻并非没有缺点,但在即将进行的讨论中,它可以作为帮助你理解的一个具体形象。
  registering(注册)一个窗口的动作对许多 DOS 程序员来讲完全是陌生的。在 DOS 环境中没有与此对 应的程序动作。但要把程序放到 Windows 环境中却要说:“喂,我的主窗口在屏幕上出现之前,我要向你注册, 这样你才能知道它是哪一类的窗口,它要做什么。”
  再回到买车的比喻上来。可以说注册过程与你想买车时进行的描述是的相同的。你会说:“我想买这类车, 我想买四轮驱动的 Toyata 车或我想买带空调的 Chery 汽车”。此时你并未谈论一种汽车的具体实例,只是 谈论汽车的一般类型和你想要的功能。当然,当你创建一个窗口时,你不是要指出是否需要动力舵或精美的立 体声音响,而是要指明要什么样的图标、哪一种菜单、窗口的颜色时什么,以及给窗口指定一个什么样的名字 等等。
  小结一下,Register 过程对你要用的窗口类注册。而实际要创建的窗口是由 CreateWindow 过程指定 的。

  附注:
  Register 过程的存在意味着一个窗口是一个存在着的独立的实体,它有自己的管理者——通常是程序自 己。此外它还可以与 Windows,也就是操作系统交流。

  在正常注册之后,下一步就是建立窗口,在这个过程确立窗口的外观、标题和样式,此时窗口本身成为现 实,而程序将接管对这个实体的控制。
  这和买一辆具体的汽车的动作看起来很相似。你在单据上签名成为一辆具体的车辆的拥有者,此时你就可 以坐在驾驶盘后面把车开走。在这个阶段,程序变成了窗口的拥有者并对其负责。你也不会再谈论某一类汽车, 而只谈论一辆具体的汽车。
  在 WinMain 中有一些 if 语句,如果这些句子中有哪些句子失败了,函数就返回 FALSE,这表示窗口 没有建立起来。也就是说,如果调用 Register 或 Create 失败,函数 WinMain 也立即夭折。这就像你与 汽车推销商谈判破裂,没有购买汽车空手离开一样。发生这种罕见的情况时,应用程序会在不做明显表示的情 况下被简单地剥夺了运行权利。
  如果这个应用程序的前一个实例仍然存在的话,这些 if 语句确保这个窗口类不必第二次注册了。也就是 说,如果你到汽车推销商那儿买第二量同样的汽车,你就用不着对你感兴趣的汽车类型或你想要包括的功能再 进行描述了。你可以直接到 Create 那儿去“买”一个具体窗口。
  你暂且不必为这些 if 语句工作的准确方式去多操心,我会在本章后面对这些代码进行深入地讨论。
  在 WinMain 中最后一部分是消息循环。你可以继续用汽车这个比喻。可以把消息循环看成是汽车的窗口, WndProc 看作是司机的座位和控制系统。从那儿司机可以看到任何进来的新消息并将车辆作相应的调整。
  为了对消息循环和 WndProc 有一个完整的理解,你应该理解到 Windows 在一定程度上是面向对象的。 这意味着,每个窗口有它自己的实例,就像汽车是一个对象,它也有自己的实例。
  每个窗口都可以当作独立存在的独立对象来看待。正因为如此,它们不是由主程序或操作系统直接操纵, 而是由操作系统发送消息给窗口,由窗口自己“决定”对这些消息应该做些什么。
  也许我给出一个大多数读者都熟悉的例子会有助于对问题的理解。在某一天结束时,我们常常会决定关闭 程序管理器或类似于程序管理器的替代程序以关闭 Windows。在发生这种情况时,Windows 就会向桌面上的 所有窗口发送消息问它们是否准备好可以关闭。
  WndProc 常常会回答:“等等,我还没有准备好,这儿还有文件没有保存。”为此,WndProc 弹出一个 窗口,询问是否保存文件,还是要撤消整个操作。如果你选择了 Yes 钮,WndProc 就会保存文件并通知操 作系统可以着手关机。如果选择了 No 按钮,文件并未保存,但 Windows 还是关闭了。如果选择 Cancel, 文件没有保存,而你的程序告诉 Windows 还应该继续打开着。
  在许多这种过程中,一个窗口就像是一个具有自己生存期的自主对象。它完全知道要通知程序管理器做哪 些必要的事情。更另人惊叹的是它知道如何与用户联系要求把松散的东西绑紧以免发生其他事情。这是一种典 型的面向对象的行为,尽管从结构上看底层的代码并不像真正的 OOP。

  注意事项:
  ·应该记住,第一次在屏幕上出现的窗口类要进行注册。在一个窗口注册,就告诉操作环境有关 这类窗口的一些特性,例如:
  ·窗口类的名字
  ·窗口的图标
  ·与窗口相联的光标
  ·窗口的背景颜色
  ·和窗口相联系的菜单名字

  不要忘记在调用 WinMain 的早期阶段就要创建一个窗口。在调用 CreateWindow 同时,可以定义窗口 的标题和窗口尺寸以及执行其他重要的初始化工作步骤。
  不要忘记建立正常的消息循环。它是驱动你的程序的引擎。一个窗口如果没有消息循环就象一辆汽车没有 窗户。如果没有消息循环,窗口就不知道周围发生的事情,其结果程序必然垮掉,就象司机看不到外面肯定会 出事故一样。
  从概念上讲,这些就是对 WinMain 和 WndProc 的概述。现在到了改变焦点放大精美图片的时候了,在 这里你会看到事情真相的细节,而这些正是大多数真正的程序设计人员每天都要关心的内容。

3.3 调用 WinMain 函数


  WinMain 函数是程序的入口点,当它被调用就启动了程序,这个例程结束时,程序也就结束了。
  再看一下 WinMain 函数的开头部分:
  Int WINAPI WinMain(HINSTANCE hInst,HINSTANCE hPrevInstance,
          LPSTR lpszCmdParam,int nCmdShow);
  尽管在第一章曾对 WinMain 作了简单介绍,但对四个参数的许多内容还需在此做进一步说明。
  ·第一个参数 hInst 是用来标识程序的唯一数字。在 WIN16 中,它在整个机器中是唯一的。在 WIN32 中,它只在各自的地址空间中是唯一的,因为每个 WIN32 进程都有它自己的地址空间。该数的唯一性已经没 有特定的意义。在 WIN32 中,这个数代表内存的基地址,.EXE 文件从此处开始加载(在 WIndows 9x 中, 这个数是 4MB 或 0x00400000)。
  ·第二个参数 hPrevInstance 是一个唯一的数字,它与本程序的前一个实例相关。如果桌面上没有其它 实例,hPrevInstance 就置为 NULL。在 WIN32 中,当前的地址空间中不会有应用程序的其它实例在运行, 所以这个参数通常总是置为为 NULL。包含这个参数只是有助于将代码从 WIN16 转移到 WIN32。
  ·第三个参数 lpszCmdParam 是一个字符串,它包含从命令行传来的任何信息。这是一个 ANSI 字符串 而不是 Unicode 字符串。如果你需要的是 Unicode 字符串,必须用下面某个函数得到:
  LPSTR GetCommandLine(void);
  LPWSTR * CommandLineToArgvw(LPWSTR lpCmdLine,LPINT pArgc);
  lpszCmdParam 并不包含作为该函数第一个成员的当前可执行文件的名字。如果你需要这个名字,你可以 调用上述两个函数得到或直接访问 _argz 或 _argv 来得到。如果你想得到环境变量,可以调用 GetEnvironmentVariable 和 SetEnvironmentVariable 函数。但你应当清楚地知道,通过环境变量与应 用程序通信并不是 Windows 推荐的工作方式。
  ·第四个参数 nCmdShow 定义第一次打开主窗口的状态。比如窗口按最大化或最小化或简单地以默认的大 小与外观出现。
  传给 WinMain 的第一、二个参数的类型是 HINSTANCE。这个类型只是一个句柄或唯一数,Windows 用 来标识桌面上的一个特定对象(如果你定义为 STRICT,那么 HINSTANCE 就变成一个指针,系统要对它做认 真检查。在 WIN16 中,它是一个 16 位(近)指针,在 WIN32 中,它是一个 32 位指针,指向一个整型 量。)

  附注:
  Windows 对每个窗口赋予一个唯一的句柄,DOS 为每个文件分配一个唯一的句柄。这两种方式很相似。 如果觉得这种类比有助与理解某些问题,你可以把 HINSTANCE 看成是对窗口指定的“文件句柄”(DOS 程 序员可能认为 DOS 给每个应用程序赋予一个唯一的 PSP 更像 Windows 为窗口指定句柄。文件和句柄的比 喻可能有点牵强,但对更多的程序设计人员来讲会更熟悉一些。用这种比喻对你们来讲是很有帮助的)。

  传给程序的第一个参数用以标识应用程序的当前实例。第二个参数代表前一个实例(如果它存在的话)。 在 WIN16 中,每个实例可以看作只是一个唯一的数字,这个数字是赋给每个特定的对象用来标识的。在 WIN32中,第一个参数的唯一性不成问题,而第二个参数总是置为 NULL。
  每个 WIN32 应用程序都有自己的地址空间。也就是说,它在自己的虚存为 4GB 的计算机中运行。应用 程序一启动,操作系统就为它建立一个人工环境,它看起来和感觉上都像一个有 4GB 内存的完整的计算机。 4GB 地址空间是虚拟存储区域,它要映射到实际存在的存储空间中,它可以是在内存中,甚至是在你的硬盘 中。有关这些内容如果你没有弄清楚也不必焦急,只要做你能做的事并继续阅读下去。虽然你对 WIN32 内存 管理有些一般的认识是有好处的,但你也可以不比理解细节。
  在分给每个应用程序的 4GB 地址空间中没有其它应用程序运行。如果另一个应用程序被启动,它会得到 它自己的 4GB 空间,即使它是第一个程序的第二个实例。这样,每一个应用程序都相信它是系统中唯一运行 的程序。
  对这种结构,在一个程序的地址空间中,只有(至少在理论上)唯一的一个 HINSTANCE。在这个地址空 间中不会有 hPrevInstance,因为该应用程序的任何潜在的第二个副本将运行在另外一个完全不同的 4GB虚 地址空间中。这两个应用程序将彼此不知对方存在、不借助特殊手段不能直接通信(这些特殊手段包括内存映 像文件、OLE 自动机和一些特殊技术)。
  16 位的 Windows 就完全不同了。关键是 16 位 Windows 是一个伪多任务操作系统,所启动的程序都 在一个地址空间内。这在 Windows 95 中也是如此,也就是说每个在 Windows 95 中启动的 16 位应用程 序,根据默认规则,它与系统中运行的其它 16 位应用程序在相同的地址空间内。
  尽管有这些限定,16 位 Windows 仍然可以完全支持在同一个时刻存在同一个程序的多个副本。为了做 到这一点,它为每一个运行的程序分配一个唯一的数字。这个数字是由 WinMain 的第一个参数传给你的。因 此,在任何 WIN16 程序中第一件要做的事是建立一个“数”(hInst)与程序的当前实例联系起来,并缺定 是否存在程序的以前实例(hPievInstance)。
  如果程序不存在以前的实例,hPievInstance 置为 NULL。因此一,在 16 位 Windows 中可能要检查 程序是否有任何兄弟存在。只要键入下列代码便可做到这一点:
  if(hPrevInstance)
    DoSomething();
  此处的 DoSomething 只在有有以前的实例时才会被调用。有时 16 位程序员希望一个时刻只能有一个 程序的副本,那么可以用下列代码来防止建立第二个实例:
  if(hPrevInstance)
    return FALSE;
  当然,如果你真的确切地按上述二行代码来防止第二个实例的出现,你的用户可能会花许多时间来单击你 的应用程序的图标,而且会很奇怪为什么没有动静。看来应该在返回 NULL 之前先弹出一个消息框来解释发 生了什么情况才更好些:

  • if(hPrevInstance)
    {
        MessageBox(0,"Only 1 copy allowed!","Notice",MB_OK);
        return FALSE;
    }
    

  当然,在 WIN32 应用程序中,这段代码没有什么意义,因为 hPrevInstance 总是置为 NULL。还可以 用另外一种方法来确定是否有正在运行的 32 位应用程序的另一个副本,那就是调用 FindWindow 函数。

3.4 匈牙利命名法和 WINDOWS.H 文件


  在进一步讨论 WinMain 之前,我要离开正题讨论匈牙利命名法和围绕着 WINDOWS.H 的各种文件(比 如 WINDEF.H 文件)。尽管这样安排有点让人讨厌,但如果你希望理解 WinMain 的语法成分,你却需要这些 信息。
  在继续进行之前,我应该介绍一下匈牙利命名法的奇特思想。也就是在变量名前习惯地加一个字母来帮助 标识变量的类型。例如,在 hPrevInstance 中的“h”表示这个 hPrevInstance 是 HINSTANCE 类型或 HANDLE 类型。同样,在 nCmdShow 中的“n”代表它是一个整型量。
  这种习惯在传统 Windows 的各个方面都广泛使用,在 DOS 中也偶尔露面。尽管匈牙利命名法会让一些 新人感到混乱,而且由于 STRICT 选项的出现在某种程度上降低了它的价值,但它仍不失为那些细心、老练 的程序设计人员手中一种有用的工具。

  附注:
  有关匈牙利命名法的一点有意思的说明是它的名字的由来。这种命名技术是由一位能干的 Microsoft 程 序员 Charles Simonyi 提出的,他出生在匈牙利。在 Microsoft 公司中和他一起工作的人被教会使用这 种约定。这对他们来说一切都很正常。但对那些 Simonyi 领导的项目组之外的人来说却感到很奇特,他们认 为这是死板的表达方式,甚至说代有这样奇怪的外观是因为它是用匈牙利文写的。从此这种命名方式就被叫做 匈牙利命名法。

  我使用匈牙利命名法是因为它现在已经成为 Windows 编码传统的一种固定部分了。不过我让你自己来判 断它是否有魅力。
  下面列出匈牙利命名法中常用的小写字母的前缀(表3.1):

  表3.1  WINDOWS.H 和/或 WNIDEF.H 中的 Windows 的类型定义

前  缀  |     类  型            |    Windows 类型

b(或f)     |    整型                     |     BOOL
    by(或b)    |    无符号字符               |     BYTE
    c          |    字符                     |
    dw         |    无符号长整型             |     DWORD
    fn         |    函数                     |
    h          |    无符号整型(UINT)       |     HANDLE
    i          |    整型                     |
    l          |    长整型                   |     LONG
    lp         |    长指针                   |
    n          |    整型或短整型             |
    s          |    字符串型                 |
    sz         |    以null做结尾的字符串型   |
    w          |    整型或短整型             |     WORD

  注意上表的第三列,它列出了在 Windows 程序中常常要用到的各种新类型。这些类型存在的理由与 HINSTANCE 类型存在的理由是一样的。不要被 Windows 代码的这些外观弄糊涂,这点小玩意儿或者说困难 其实没什么,对它们的最简单的具体认识应该是,它们只不过是 C 程序员用了多年的旧类型换了新名字而已。 若定义了 STRICT,它们常常转换为指针类型。例如,如果没有定义 STRICT。HINSTANCE 说明为 DWORD, 而定义了 STRICT 之后,它就被说明为指向一个结构的指针。如果定义 STRICT,HINSTANCE 指针类型就会 由编译程序进行细致的类型检查,这样有助于你写出清晰的可转换的代码。
  所有引用手册的最终来源是 WINDOWS.H 和在它当中引用的文件。在每个 Windows 程序的开头都会引用 WINDOWS.H 文件。在 WINDOWS.H 内部或在它引用的一个文件中,你会发现很多函数和类型的说明和定义来 自 Windows 环境。下列是特别重要的代码分支,包含一些方便使用的定义:

/** Simple types & common helper macros **/

typedef int                     BOOL;
#define FALSE                   0
#define TRUE                    1

typedef unsigned char           BYTE;
typedef unsigned short          WORD;
typedef unsigned long           DWORD;

typedef unsigned int            UINT;

#if define STRICT
typedef signed long             LONG;
#else
#define LONG long

#define LOBYTE(w)               ((BYTE)(w))
#define HIBYTE(w)               ((BYTE)((UINT)(w)>>8))

#define LOWORD(l)               ((WORD)(l))
#define HIWORD(l)               ((WORD)((UINT)(l)>>16))

  花一些时间仔细研究一下这些代码能帮你搞清楚很多 Windows 程序设计中有趣的外貌。
  有关匈牙利命名法和 WINDOWS.H 的难以理解的题外话就到此为止。围绕着 WINDOWS.H 的文件复合体正 是解释着许多难点的地方,花几个小时对这些文件内容研究一番是值得的。

3.5 再谈 WinMain 函数


  在学习了匈牙利命名法和 WINDOWS.H 复合体部分内容后,你可能会发现,现在可以理解 WinMain 函数 的第三个参数的意义了:
  int WINAPI WinMain(HINSTANCE hInst,HINSTANCE hPrevInstance,
      LPSTR lpszCmdParam,int nCmdShow)
  标识符 lpszCmdParam 的前缀用来标识这个参数是一个长指针,它指向一个用 null 终止的字符串。尤 其是该字符串恰好包含启动时传给程序的命令行。
  WinMain 的最后一个参数 nCmdShow 用来设计程序的主窗口的开始状态,比如最大化还是最小化或是正 常状态。
  当 nCmdShow 置为 SW_SHOWMAXIMIZED 时,程序的主窗口以最大状态出现。反之,当 nCmdShow 置为 SW_SHOWMINIMIZED 时,窗口状态以最小化开始。窗口状态的默认值是 SW_SHOWNORMAL.
  WinMain 头部还有最后一个重要部分我尚未讨论到。这就是字 WINAPI,它在 WINDOWS.H 中的定义如 下:

  #define WINAPI       far_pascal
  #define WINAPI       stdcall
  #define CALLBACK     far_pascal

  其中包含了对 CALLBACK 的定义,因为它显然是有关系的。还要注意 WINAPI 在 WINDOWS.H 复合体中 其他各个地方被重新定义。在以前 16 位 Windows 程序中,使用字 FAR_PASCAL,而现在程序员使用 WINAPI。 选择使用这个新字是为了与操作系统,例如 Windows NT 兼容。在 Windows NT 中不用这个关键字 FAR。
  所有标记为 CALLBACK 或 WINAPI 的过程在 WIN16 中都自动说明为按 Pascal 调用习惯,在 WIN32 中 是按 _stdcall 调用(_stdcall 是既不遵循 cdecl 调用习惯,也不遵循 Pascal 调用习惯)。标准的 16 位 Pascal 调用习惯强制执行这样的规则:当参数传给这个过程时,它们被推进栈,参数进栈的顺序是按参数 排列次序从左边开始,依次放入,到最右边为止。传统的 C 程序用的是相反的方式。但 16 位 Windows 用的 是 Pascal 调用习惯,因为它比较快。_stdcall 调用方式试图使用两种系统最好的特性。

3.6 注  册


  现在介绍复杂的 Register 函数:

  • BOOL Register(HINSTANCE hInst)
    {
        WNDCLASS WndClass;
    
        WndClass.style = CS_HREDRAW|CS_VREDRAW;
        WndClass.lpfnWndProc = WndProc;
        WndClass.cbClsExtra = 0;
        WndClass.cbWndExtra = 0;
        WndClass.hInstance = hInst;
        WndClass.hIcon = LoadIcon(NULL,IDI_APPLICATION);
        WndClass.hCursor = LoadCursor(NULL,IDC_ARROW);
        WndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
        WndClass.lpszMenuName = NULL;
        WndClass.lpszClassName = szAppName;
     
        return (RegisterClass(&WndClass)!=0);
    }
    

  这段代码向你介绍了 Windows 许多习惯之一 ——存在为数众多并且复杂的结构和说明。如果你有比 较好的参考书或者联机帮助系统,这也没有什么好怕的。
  也许有一天你可能希望记住 WNDCLASS 结构的所有字段,但你不需要马上去做这事,而应该先花一点 时间研究来一下参考手册或联机帮助文件,其中都列出了这些结构。
  或者你可以花几分钟的时间阅读一下 WINDOWS.H 以及相关的文件,这可能帮你找到有关 WNDCLASS 结构更多的消息:

  • typedef struct tagWNDCLASS 
    {
        UINT    style;
        WNDPROC lpfnWndProc; 
        int     cbClsExtra;
        int     cbWndExtra;
        HANDLE  hInstance; 
        HICON   hIcon;
        HCURSOR hCursor;
        HBRUSH  hbrBackground; 
        LPCTSTR lpszMenuName;
        LPCTSTR lpszClassName;
    }WNDCLASS;
    

  WNDCLASS 结构处于 Register 过程的核心地位。
  正如我前面所说的那样,填写 Register 过程的参数就像你决定你的新汽车需要哪些功能一样。现在你 有机会看一看注册一个新窗口时到底有哪些选项可供选择:
  ·style 字段:窗口的样式是什么?你只要改变 style 字段的内容就可以根本改变窗口的行为。你还 可以像前面所示的代码那样用“或”操作把几个样式连接起来。这个参数通常有两个样式:CS_VREDRAW 和 CS_HREDRAW。下面列出这个字段中可以指定的参数(更多的信息参看联机帮助)。

  • CS_BYTEALIGNCLIENT: 为了改善窗口的性能,按字节边界对齐窗口的列。
    CS_BYTEALIGNWINDOW: 在 x 方向对齐一个窗口的字节边界。使得在移动窗口或确定窗口大小的操作时可以提高其性能。
    CS_CLASSDC: 使一个类的所有窗口共享一个设备描述表(DC)。
    CS_DBLCLKS: 可得到双击消息。
    CS_GLOBALCLASS: 不管传送给 CreateWindow 或 CreateWindowEx 函数的 hInstance 参数值是什么,都允许应用程序建 立一个该类的窗口。如果不指明这个样式选项,传给 CreateWindow(或CreateWindowEx)的 hInstance 参数必须和传给 RegisterClass 函数的 hInstance 参数完全一样。
    CS_HREDRAW: 如果窗口的水平方向大小改变了,重新绘制整个窗口。
    CS_NOCLOSE: 关掉系统菜单中的 Close 命令。
    CS_OWNDC: 给一个类中每一个窗口一个唯一的设备描述表。
    CS_PARENTDC: 让子窗口使用父窗口的设备描述表来增加其性能。
    CS_SAVEBITS: 为了重新绘制窗口,把窗口当作位图保存。
    CS_VREDRAW: 如果窗口的纵向改变了大小,要重新绘制整个窗口。

  ·wndProc 字段:用这个字段指定窗口过程的名字和地址。
  ·cbClsExtra,cbWndextra:是否有与窗口类或实例相关的额外数据。目前你不必关心这两个字段, 它们将在以后的章节中深入介绍。
  ·hInstance:应用程序唯一的句柄。
  ·hIcon:如果有的话,指定窗口的图标。
  ·hCursor:如果有的话,指定窗口的光标。
  ·nbrBackground:窗口颜色。
  ·lpszMenuName:窗口的菜单名。
  ·lpszClassName:窗口类的名字。
  在 WINDOW1.CPP 中,style 域设置为 CS_HREDRAW 和 CS_VREDRAW。这两个常量指明无论窗口大 小在水平或垂直方向是否有了变化都要重新绘制窗口。这两个参数的出现确保了窗口内容对用户来说总是年 个清晰可见的。

  注意事项
  ·可以用正确的方式为窗口一次指定多个样式,用“或”操作符“|”把它们连接在一起。
  ·不要把窗口的两个样式选项用 "&" 或 "+" 操作符连接在一起。

  下一步是指定窗口过程,在一些老的代码中,WndProc 被说明为返回一个 long 类型的值。如果要把 老代码定义为 STRICT,这样就会细致地监视类型检查,将会强制你执行类型转换:
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  如果把 WndProc 说明为返回 LRESULT,就可以避免语法上一点点的混淆:
  WndClass.lpfnWndProc = WndProc;
  当然,即使你没有用 STRICT,大多数标准的 C 编译程序也能对上一行代码进行编译(甚至你把 WndProc 的返回值说明为 long 类型的)。
  WndClass 结构的下面两个字段目前尚不重要,所有 cbClsExtra 和 cbWndExtra 要做的事是给你 一个机会再设立一块存储区域用来存放与窗口相关的信息。不过目前尚不必关心这些深奥的设计结果。
  下一步是把你的应用程序的句柄赋给 hInstance,这很简单:
  WndClass.hInstance = hInst;
  hIcon,hCursor,hbrBackground 和 lpszMenuName 这几个域都用来描述窗口的一般特性。在以后 的课程中,我将介绍如何再一次给每一个特性做说明。但现在你可能想做几个试验来体会一下会发生些什么。 例如,不指定 IDI_APPLICATION 为你的图标,而用 IDI_EXCLAMATION 或 IDI_HAND 试试看:
  WndClass.hIcon = LoadIcon(NULL,IDI_HAND);
  在你运行你的应用程序后,把它最小化,这时你会看到一个不同的图标。
  也可以对光标做一些改动,例如用 IDC_WAIT,IDC_CROSS,或 IDC_IBEAM 代替 IDC_ARROW:
  WndClass.hCursor = LoadCursor(NULL,IDC_WAIT);
  你还可以改变应用程序的背景色。只要用 BLACK_BRUSH 或 GRAY_BRUSH 代替 WHITE_BRUSH 常量。 记住,所有这些常量都在 WINDOWS.H 中定义并在任何一本好的参考书中都会列出。
  现在还不是讨论菜单的时候,所以我对 lpszMenuName 域只简单地赋为 NULL。
  对 lpszClassNane 域简单解释几句,这个字段提供了为你的窗口命名的机会。这是一个非常重要的动 作,因为你的窗口是在这个名下注册的。这个类名有点像汽车的钥匙,没有它你就不能坐在司机的座位上发动 引擎。这就是说窗口在建立之前必须在一个特定的名字下注册。无论何时要创建一个窗口,如果你没有这把钥 匙——也就是你不知道这个类的名字,就根本没有希望去建立窗口。
  到此为止,已不需要对 WNDCLASS 结构作更深的介绍了。剩下的事情只是调用 RegisterClass 函数把 这个消息传送给 Windows 并把 WNDCLASS 结构的地址传给它:
  return (RegisterClass(&WndClass)!=0);
  注意,这里我没有把 RegisterClass 过程的值直接返回。而是返回一个布尔量来表示该值是否为 0。 如果该值为 0,则调用失败,也就返回 FALSE,否则返回 TRUE。
  1.Register 过程帮助你定义在屏幕上出现的窗口的类。所有窗口都是对象,每个对象都有一组特征。程 序员填好 WNDCLASS 结构就等于告诉 Windows,他所定义的对象或类时什么样子的。
  2.通常你完全可以不考虑 Register 的具体方法,而只要将这段程序粘贴你的程序中。甚至可以不必考 虑它的内容就可以调用它。在大多数情况下默认设置(如前所示)就会把事情都作好。在每一次编码时, Register 函数不是你要特别注意的事,它只是一系列提供给你的选择机会而已。

3.7 创 建 窗 口


  现在是考虑 Create 例程的时候了。记住,这是你定义一个特定窗口的程序的一部分。Register 过程 定义的是一个一般的窗口类,而 Create 过程定义的是一个窗口类的一个特定的实例:

  • BOOL Create(HINSTANCE hInst,int nCmdShow)
    {                               
        HWND hwnd = CreateWindowEx (0,szAppName,szAppName,
    		WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,
    		CW_USEDEFAULT,CW_USEDEFAULT,
    		NULL,NULL,hInst,NULL);
        if(hwnd == NULL)
    	return FALSE;
    
        ShowWindow(hwnd,nCmdShow);
        UpdateWindow(hwnd);
    
        return hwnd;
    }
    

  这个函数的核心是调用 CreateWindow,这个函数共有 11 个参数,现在只需要对这些参数作简单的介 绍:

  • LPCTSTR lpClassName:   注册窗口类名的地址。
    LPCTSTR lpWindowName:  窗口标题正文的地址,
    DWORD dwStyle:         窗口样式。
    int x:                 窗口的水平位置。
    int y:                 窗口的垂直位置。
    int nWidth:            窗口的宽度。
    int nHeight:           窗口的高度。
    HWND hWndParent:       父窗口的句柄。
    HMENU hMenu:           菜单或子窗口 ID 的句柄。
    HANDLE hInstance:      应用程序实例的句柄。
    LPVOID lpParam:        窗口建立的数据的地址。
    

  从第四到第七个参数确定了窗口的初始大小。若用 CW_USEDEFAULT 常量则表示由 Windows 来为应 用程序选择窗口的坐标。
  传给 CreateWindow 的第一个参数是类的名字。要记住,桌面上的每一个窗口都是一个具有一定自主 性的独立实体,这就要求每一个窗口都要有一个名字来标识它。程序的主窗口的名字按惯例就是程序本身的 名字——当然不是一定要这么做。
  传给 CreateWindow 的第二个参数将在以后的章节里讨论,那时将对注册和创建窗口的题材作更详细 的介绍。
  在对窗口过程和消息循环作简单介绍之前,先应注意一下下面两个函数,它们可以让窗口在屏幕上出现:
  ShowWindow(hwnd,nCmdShow);
  UpdateWindow(hwnd);
  ShowWindow 函数把 nCmdShow 传送给 Windows,通知它按某种特定状态显示窗口。下面是一些特定 状态:

  • SW_SHOWMINIMIXZED:     最小化。
    SW_SHOWMAXIMIXZED:     最大化。
    SW_SHOW:               正常显示。
    

  可以通过 WinMain 或 ShowWindow 的联机帮助信息找到可以传给 ShowWindow 的其它值。
  UpdateWindow 函数指示 Windows 发送一条 WM_PAINT 消息给新创建的窗口,以后将对它作进一步 的介绍。现在你只要知道 ShowWindow 和 UpdateWindow 两个函数都用 HWND 来作它们的第一个参数。 此处 HWND 是应用程序主窗口的句柄。这样,任何时候在桌面上可以打开很多窗口,但当前的目标只是要显 示刚刚建立的特定窗口。因此把句柄传给这个窗口是必要的,这样窗口才能知道需要显示的是哪个对象。

3.8 消 息 循 环


  WinMain 函数最后几行代码如下:

  • while(GetMessage(&Msg,NULL,0,0))
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);              
    }
    

  如前所述,这段代码大概等同于一辆新汽车的窗口。消息循环是你的应用程序的“眼睛”,通过它你能 看到周围的事情。尤其是要通过它才能知道用户是否按下一个键或鼠标按钮,或者操作系统是否传达某些重 要消息,所有这些事件或动作都以消息的形式来传给你。
  在应用程序的生存期中,Windows 向它发送消息来报告用户或环境中的其它部分所采取的动作。这些消 息放入一个队列中,用 GetMessage 函数可以按需要从中取出消息。每当检索到一条消息,就把它放在一个 消息结构中,该结构如下:

  • typedef struct tagMSG
    {
        HWND   hwnd;
        UINT   message;
        WPARAM wParam;
        LPARAM lParam;
        DWORD  time;
        POINT  pt;
    }MSG;
    

  现在你还不需要对这个结构作深入了解,以后我会再介绍它。
  从消息对列检索到一条消息后,就会把它传给 TranslateMessage,再那儿对它作处理以使得消息更易 理解。例如,如果用户按下了一个功能键,消息以非常抽象的形式放在队列中。TranslateMessage 函数对 这样的消息作些处理使得你的应用程序能够十分容易地理解它。
  然后由 DispatchMessage 把这条消息传送给指定的窗口过程,在本例中是 Wndproc。在消息分发后, 如果队列中还有消息的话,就调用 GetMessage 函数从队列中再检索出另外一条消息。
  重要的是你要理解,这种循环或者类似的循环在应用程序的整个生存期中将周而复始地被执行,这个过程 永不停止。在每个 Windows 程序中它处于核心位置。

3.9 窗 口 过 程


  消息循环一圈圈反复轮回。在典型 Windows 程序的生存期中,这个过程以无休止循环方式重复着。它 先从操作环境中得到消息,再把消息传给窗口过程或 WndProc。
  下面是与 WndProc 有关的 Window1 的程序的一段代码:

  • LRESULT CALLBACK WndProc(HWND hwnd,UINT Message,
    	WPARAM wParam,LPARAM lParam);
    #define skyline_DefProc DefWindowProc
    void skyline_OnDestroy(HWND hwnd);
                    
    //
    // The window proc is where message get processed
    //
    
    LRESULT CALLBACK WndProc(HWND hwnd,UINT Message,
    		WPARAM wParam,LPARAM lParam)  
    { 
        switch(Message)
        {
      	HANDLE_MSG(hwnd,WM_DESTROY,skyline_OnDestroy);
    
          default:
      	return skyline_DefProc(hwnd,Message,wParam,lParam);
        }
    }
    
    #pragma argsused
    
    void skyline_OnDestroy(HWND hwnd)
    {
        PostQuitMessage(0);
    }
    

  这段代码的开头是一组说明和一个定义:

  LRESULT CALLBACK WndProc(HWND hwnd,UINT Message,
	WPARAM wParam,LPARAM lParam);
  #define skyline_DefProc DefWindowProc
  void skyline_OnDestroy(HWND hwnd);

  WndProc 和 skyline_OnDestroy 是程序运行时要调用的例程。这儿没有任何费解之处。skyline_DefProc 是一个定义,用来引用标准的 Windows API 调用 DefWindowProc。
  窗口过程的实际实现如下列代码所示:

  • LRESULT CALLBACK WndProc(HWND hwnd,UINT Message,
    		WPARAM wParam,LPARAM lParam)  
    { 
        switch(Message)
        {
      	HANDLE_MSG(hwnd,WM_DESTROY,skyline_OnDestroy);
    
          default:
      	return skyline_DefProc(hwnd,Message,wParam,lParam);
        }
    }
    

  这段代码处理任何发送给程序的消息。这些消息可能来自消息循环或者由系统直接送来。
  HANDLE_MSG 宏来自 WINDOWSX.H,在以后会对该宏作介绍。
  窗口过程可以很长,很复杂。但例子中的这个过程却很简单。它除了要响应把窗口曲调的命令外,不需 要处理其它任何消息。所有的其它消息都由默认的窗口过程处理,此处用的是 skyline_DefProc。
  上述代码的最后一段是 skyline_DefProc 例程:
  void skyline_OnDestroy(HWND hwnd)
  {
    PostQuitMessage(0);
  }
  对 PostQuitMessage 的调用通知 Windows,一个应用程序或线程准备终止运行。

3.10 疑 难 解 答

 

  • 什么是 WNDCLASS ?

  WNDCLASS 是一个结构,用来定义一个窗口类的主要特性。例如它的名字、图标和光标等。

  • register 函数的作用是什么?

  register 函数用来定义 WNDCLASS 结构的字段,并通过调用 RegisterClass 函数来注册这个与窗口 类相关的结构。

  • Create 函数的作用是什么?

  使用创建窗口的函数来创建一个程序的主窗口并使它们变成可见的。这些函数有 CreateWindow,ShowWindow 和 UpdateWindow。

  • ShowWindow 函数和 UpdateWindow 函数有何不同?

  ShowWindow 函数用来把一个窗口置为可见状态。它通常用来使一个窗口或为可见的,或为隐藏的。
  UpdateWindow 函数发送一条 WM_PAINT 消息给窗口,这样使窗口被重新绘制。

  • WndProc 的作用是什么?

  WndProc 用来让你定义一个与特定窗口有关的行为。尤其它给程序员一个机会来说明窗口如何处理任何 发送给它的消息。

  • 消息循环的作用是什么?

  消息循环的动作像一个守门员站在一个窗口过程的大门边。在消息被送到这个窗口过程之前,它可以用 来过滤、协调和修改消息。

  • DefWindowProc 的作用是什么?

  DefWindowProc 用来传送一个消息给 Windows。在有些情况下,有些程序员可能想把一条消息压下来 不把它传出去,要达到这个目的只要让它退出这个窗口过程,甚至也不把这个消息传送给 DefWindowProc。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值