|
windows sdk编程系列文章 ---- 简单窗口程序
网站:http://edu.teamsourcing.com.cn
理论:
Windows 程序中,在写图形用户界面时需要调用大量的标准 Windows Gui 函数。其实这对用户和程序员来说都有好处,对于用户,面对的是同一套标准的窗口,对这些窗口的操作都是一样的,所以使用不同的应用程序时无须重新学习操作。对程序员来说,这些 Gui 源代码都是经过了微软的严格测试,随时拿来就可以用的。当然至于具体地写程序对于程序员来说还是有难度的。为了创建基于窗口的应用程序,必须严格遵守规范。作到这一点并不难,只要用模块化或面向对象的编程方法即可。
下面我就列出在桌面显示一个窗口的几个步骤:
得到您应用程序的句柄(对于C程序,可选);
得到命令行参数(如果您想从命令行得到参数,可选);
注册窗口类(必需,除非您使用 Windows 预定义的窗口类,如 MessageBox 或 dialog box;
产生窗口(必需);
在桌面显示窗口(必需,除非您不想立即显示它);
刷新窗口客户区;
进入无限的获取窗口消息的循环;
如果有消息到达,由负责该窗口的窗口回调函数处理;
如果用户关闭窗口,进行退出处理。
相对于单用户的 DOS 下的编程来说,Windows 下的程序框架结构是相当复杂的。但是 Windows 和 DOS 在系统架构上是截然不同的。Windows 是一个多任务的操作系统,故系统中同时有多个应用程序彼此协同运行。这就要求 Windows 程序员必须严格遵守编程规范,并养成良好的编程风格。
例子:(见光盘FirstWindow)
#include "Windows.h"
#include "tchar.h"
HWND hWinMain;
TCHAR szClassName[] = _T("MyClass");
TCHAR szCaptionMain[] = _T("My First Window!");
TCHAR szText[] = _T("Win32 program, Simple and powerful !");
WNDCLASSEX stdWndClass;
//如果有消息到达,由负责该窗口的窗口回调函数处理
LRESULT CALLBACK ProcWinMain( HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
)
{
switch(Msg)
{
//如果用户关闭窗口,进行退出处理
case WM_CLOSE:
{
DestroyWindow(hWinMain);
PostQuitMessage(NULL);
}
break;
default:
return DefWindowProc(hWnd, Msg, wParam, lParam );
}
return 0;
}
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow
)
{
MSG stMsg;
WNDCLASSEX stdWndClass;
RtlZeroMemory(&stdWndClass, sizeof(stdWndClass));
stdWndClass.hCursor = LoadCursor(0,IDC_ARROW);
stdWndClass.cbSize = sizeof(stdWndClass);
stdWndClass.style = CS_HREDRAW|CS_VREDRAW;
stdWndClass.lpfnWndProc = ProcWinMain;
stdWndClass.hbrBackground = (HBRUSH)COLOR_WINDOW;
stdWndClass.lpszClassName = szClassName;
stdWndClass.hInstance = hInstance;
//注册窗口
RegisterClassEx(&stdWndClass);
//产生窗口
hWinMain = CreateWindowEx(WS_EX_CLIENTEDGE,szClassName,szCaptionMain, WS_OVERLAPPEDWINDOW,100,100,600,400,NULL,NULL,hInstance,NULL);
if(!hWinMain)
return 0;
//在桌面显示窗口
ShowWindow(hWinMain,SW_SHOWNORMAL);
//刷新窗口客户区
UpdateWindow(hWinMain);
//进入无限的获取窗口消息的循环
while(GetMessage(&stMsg,NULL,0,0))
{
TranslateMessage(&stMsg);
DispatchMessage(&stMsg);
}
return stMsg.wParam;
}
分析:
看到一个简单的 Windows 程序有这么多行,您是不是有点想撤? 但是您必须要知道的是上面的大多数代码都是模板而已,模板的意思即是指这些代码对差不多所有标准 Windows 程序来说都是相同的。在写 Windows 程序时您可以把这些代码拷来拷去,当然把这些重复的代码写到一个库中也挺好。其实真正要写的代码集中在 WinMain 中。这和一些 C 编译器一样,无须要关心其它杂务,集中精力于 WinMain 函数。唯一不同的是 C 编译器要求您的源代码有必须有一个函数叫 WinMain。否则 C 无法知道将哪个函数和有关的前后代码链接。相对C,汇编语言提供了较大的灵活性,它不强行要求一个叫 WinMain 的函数。
下面我们开始分析,您可得做好思想准备,这可不是一件太轻松的活。
头文件: windows.h是编写windows程序必须要包含的,因为其中包含大量要用到的常量和结构的定义, windowd.h还没有包含windows所有的常量和结构定义,对于程序中我们用到的在windows.h定义之外的,我们可以通过察看msdn,找到该结构和函数所在的头文件和库文件,包含进来就行。tchar.h定义了我们用得宏 _T(x).
我们的程序调用的API在 user32.dll (譬如:CreateWindowEx, RegisterWindowClassEx) 和 kernel32.dll (ExitProcess)中的函数,所以必须链接这两个库。接下来我如果问:您需要把什么库链入您的程序呢 ? 答案是:先查到您要调用的函数在什么库中,然后包含进来。在VC6的settings中已经包含了常用的lib.如图所示。因此,我们在代码中就无需显式的加载用到的库。
显式加载的方法是在代码中加入一句,例如:
#pragma comment(lib,"kernel32.lib“)
我们在前面曾经讲过,windows提供的API函数封装在几个DLL中,调用DLL中的这些API函数,有两种方法,我们现在用的就是其中一种,即静态加载办法。还有一种是动态加载,我们将在后面讲DLL的时候,再给大家介绍。
windows应用程序中必须要有WinMain函数,这个函数是由C编译器需要的,代码编译后,在程序运行时,该函数由C运行库调用,传入4个参数。用户可以把它看作是程序的入口。该函数共有4个参数:应用程序的实例句柄,该应用程序的前一实例句柄,命令行参数串指针和窗口如何显示。Win32 没有前一实例句柄的概念,所以第二个参数总为0。之所以保留它是为了和 Win16 兼容的考虑,在 Win16下,如果 hPrevInst 是 NULL,则该函数是第一次运行。函数声明如下:
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow
) ;
特别注意:WIN32下的实例句柄实际上是您应用程序在内存中的线性地址。即HINSTANCE hInstance。
如果你要处理命令行,可以从 LPSTR lpCmdLine参数中得到程序传入的命令行串。
WINAPI 是一个宏,#define WINAPI __stdcall
#define CALLBACK __stdcall
这个是一种函数调用约定。在windows API中几乎所有的函数,都是采用WINAPI这种函数约定。
这种调用约定称为标准调用约定。指定了调用一个函数时,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。
还有几种调用约定在这里也稍作比对介绍。
_cdecl c调用约定, 按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。_cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。
_fastcall调用约定是"人"如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字 (DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
_fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。
关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...C/C++ Code Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、 /Gd和/Gr。缺省状态为/Gd,即__cdecl。
WinMain函数中用的RtlZeroMemory,是一个宏。
#define RtlZeroMemory(Destination,Length) memset((Destination),0,(Length))
在WinMain中主要概念就是窗口类(window class),一个窗口类就是一个有关窗口的规范,这个规范定义了几个主要的窗口的元素,如:图标、光标、背景色、和负责处理该窗口的函数。您产生一个窗口时就必须要有这样的一个窗口类。如果您要产生不止一个同种类型的窗口时,最好的方法就是把这个窗口类存储起来,这种方法可以节约许多的内存空间。
如果您要定义自己的创建窗口类就必须:在一个 WINDCLASS 或 WINDOWCLASSEXE 结构体中指明您窗口的组成元素,然后调用 RegisterClass 或 RegisterClassEx ,再根据该窗口类产生窗口。对不同特色的窗口必须定义不同的窗口类。 WINDOWS有几个预定义的窗口类,譬如:按钮、编辑框等。要产生该种风格的窗口无须预先再定义窗口类了,只要包预定义类的类名作为参数调用 CreateWindowEx 即可。
WNDCLASSEX 中最重要的成员莫过于lpfnWndProc了。前缀 lpfn 表示该成员是一个指向函数的长指针。在 Win32中由于内存模式是 FLAT 型,所以没有 near 或 far 的区别。每一个窗口类必须有一个窗口过程,当 Windows 把属于特定窗口的消息发送给该窗口时,该窗口的窗口类负责处理所有的消息,如键盘消息或鼠标消息。由于窗口过程差不多智能地处理了所有的窗口消息循环,所以您只要在其中加入消息处理过程即可。对于WNDCLASSEX的定义见WINUSER.H。可以看出为了使用不同的字符集,定义了两个结构体。
后面有一个宏,会根据编译选项来决定使用哪个结构。
typedef struct tagWNDCLASSA {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCSTR lpszMenuName;
LPCSTR lpszClassName;
} WNDCLASSA, *PWNDCLASSA, NEAR *NPWNDCLASSA, FAR *LPWNDCLASSA;
typedef struct tagWNDCLASSW {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCWSTR lpszMenuName;
LPCWSTR lpszClassName;
} WNDCLASSW, *PWNDCLASSW, NEAR *NPWNDCLASSW, FAR *LPWNDCLASSW;
#ifdef UNICODE
typedef WNDCLASSW WNDCLASS;
typedef PWNDCLASSW PWNDCLASS;
typedef NPWNDCLASSW NPWNDCLASS;
typedef LPWNDCLASSW LPWNDCLASS;
#else
typedef WNDCLASSA WNDCLASS;
typedef PWNDCLASSA PWNDCLASS;
typedef NPWNDCLASSA NPWNDCLASS;
typedef LPWNDCLASSA LPWNDCLASS;
#endif // UNICODE
cbSize:WNDCLASSEX 的大小。我们可以用sizeof(WNDCLASSEX)来获得准确的值。
style:从这个窗口类派生的窗口具有的风格。
lpfnWndProc:窗口处理函数的指针。
cbClsExtra:指定紧跟在窗口类结构后的附加字节数。
cbWndExtra:指定紧跟在窗口事例后的附加字节数。如果一个应用程序在资源中用CLASS伪指令注册一个对话框类时,则必须把这个成员设成DLGWINDOWEXTRA。
hInstance:本模块的事例句柄。
hIcon:图标的句柄。
hCursor:光标的句柄。
hbrBackground:背景画刷的句柄。
lpszMenuName:指向菜单的指针。
lpszClassName:指向类名称的指针。
hIconSm:和窗口类关联的小图标。如果该值为NULL。则把hCursor中的图标转换成大小合适的小图标。
注册窗口类后,我们将调用CreateWindowEx来产生实际的窗口。请注意该函数有12个参数。
我们来仔细看一看这些的参数:
dwExStyle:附加的窗口风格。相对于旧的CreateWindow这是一个新的参数。在9X/NT中您可以使用新的窗口风格。您可以在Style中指定一般的窗口风格,但是一些特殊的窗口风格,如顶层窗口则必须在此参数中指定。如果您不想指定任何特别的风格,则把此参数设为NULL。
lpClassName:(必须)。ASCIIZ形式的窗口类名称的地址。可以是您自定义的类,也可以是预定义的类名。像上面所说,每一个应用程序必须有一个窗口类。
lpWindowName:ASCIIZ形式的窗口名称的地址。该名称会显示在标题条上。如果该参数空白,则标题条上什么都没有。
dwStyle:窗口的风格。在此您可以指定窗口的外观。可以指定该参数为零,但那样该窗口就没有系统菜单,也没有最大化和最小化按钮,也没有关闭按钮,那样您不得不按Alt+F4 来关闭它。最为普遍的窗口类风格是 WS_OVERLAPPEDWINDOW。 一种窗口风格是一种按位的掩码,这样您可以用“or”把您希望的窗口风格或起来。像 WS_OVERLAPPEDWINDOW 就是由几种最为不便普遍的风格或起来的。
X,Y: 指定窗口左上角的以像素为单位的屏幕坐标位置。缺省地可指定为 CW_USEDEFAULT,这样 Windows 会自动为窗口指定最合适的位置。
nWidth, nHeight: 以像素为单位的窗口大小。缺省地可指定为 CW_USEDEFAULT,这样 Windows 会自动为窗口指定最合适的大小。
hWndParent: 父窗口的句柄(如果有的话)。这个参数告诉 Windows 这是一个子窗口和他的父窗口是谁。这和 MDI(多文档结构)不同,此处的子窗口并不会局限在父窗口的客户区内。他只是用来告诉 Windows 各个窗口之间的父子关系,以便在父窗口销毁是一同把其子窗口销毁。在我们的例子程序中因为只有一个窗口,故把该参数设为 NULL。
hMenu: WINDOWS菜单的句柄。如果只用系统菜单则指定该参数为NULL。回头看一看WNDCLASSEX 结构中的 lpszMenuName 参数,它也指定一个菜单,这是一个缺省菜单,任何从该窗口类派生的窗口若想用其他的菜单需在该参数中重新指定。其实该参数有双重意义:一方面若这是一个自定义窗口时该参数代表菜单句柄,另一方面,若这是一个预定义窗口时,该参数代表是该窗口的 ID 号。Windows 是根据lpClassName 参数来区分是自定义窗口还是预定义窗口的。
hInstance: 产生该窗口的应用程序的实例句柄。
lpParam: (可选)指向欲传给窗口的结构体数据类型参数的指针。如在MDI中在产生窗口时传递 CLIENTCREATESTRUCT 结构的参数。一般情况下,该值总为零,这表示没有参数传递给窗口。可以通过GetWindowLong 函数检索该值。
hWinMain = CreateWindowEx(WS_EX_CLIENTEDGE,szClassName,szCaptionMain, WS_OVERLAPPEDWINDOW,100,100,600,400,NULL,NULL,hInstance,NULL);
调用CreateWindowEx成功后,窗口句柄在eax中。我们必须保存该值以备后用。我们刚刚产生的窗口不会自动显示,所以必须调用 ShowWindow 来按照我们希望的方式来显示该窗口。接下来调用 UpdateWindow 来更新客户区.
ShowWindow(hWinMain,SW_SHOWNORMAL);
UpdateWindow(hWinMain);
这时候我们的窗口已显示在屏幕上了。但是它还不能从外界接收消息。所以我们必须给它提供相关的消息。我们是通过一个消息循环来完成该项工作的。每一个模块仅有一个消息循环,我们不断地调用 GetMessage 从 Windows 中获得消息。GetMessage 传递一个 MSG 结构体给 Windows ,然后 Windows 在该函数中填充有关的消息,一直到 Windows 找到并填充好消息后 GetMessage 才会返回。在这段时间内系统控制权可能会转移给其他的应用程序。这样就构成了Win16 下的多任务结构。如果 GetMessage 接收到 WM_QUIT 消息后就会返回 FALSE,使循环结束并退出应用程序。TranslateMessage 函数是一个是实用函数,它从键盘接受原始按键消息,然后解释成 WM_CHAR,在把 WM_CHAR 放入消息队列,由于经过解释后的消息中含有按键的 ASCII 码,这比原始的扫描码好理解得多。如果您的应用程序不处理按键消息的话,可以不调用该函数。DispatchMessage 会把消息发送给负责该窗口过程的函数。
while(GetMessage(&stMsg,NULL,0,0))
{
TranslateMessage(&stMsg);
DispatchMessage(&stMsg);
}
如果消息循环结束了,退出码存放在 MSG 中的 wParam中, 我们把它返回给windows。尽管目前Windows 没有利用到这个结束码,但我们最好还是遵从 Windows 规范已防意外。
return stMsg.wParam;
LRESULT CALLBACK ProcWinMain( HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
是我们的窗口处理函数。您可以随便给该函数命名。其中第一个参数 hWnd 是接收消息的窗口的句柄。uMsg 是接收的消息。注意 uMsg 不是一个 MSG 结构,其实上只是一个 DWORD 类型数。Windows 定义了成百上千个消息,大多数您的应用程序不会处理到。当有该窗口的消息发生时,Windows 会发送一个相关消息给该窗口。其窗口过程处理函数会智能的处理这些消息。wParam 和 lParam 只是附加参数,以方便传递更多的和该消息有关的数据。
LRESULT CALLBACK ProcWinMain( HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
)
{
switch(Msg)
{
case WM_CLOSE:
{
DestroyWindow(hWinMain); //注销窗口
PostQuitMessage(NULL); //退出消息循环
}
break;
default:
return DefWindowProc(hWnd, Msg, wParam, lParam );
}
return 0;
}
回调函数的调用约定是CALLBACK ,即也是采用标准调用约定。回调函数的类型已经在窗口类中有定义。
LRESULT CALLBACK WindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
回调函数中如果是我们感兴趣的消息则加以处理,处理完后,再 return 0,否则必须调用DefWindowProc,即把该窗口过程接收到的参数传递给缺省的窗口处理函数。所有消息中您必须处理的是 WM_DESTROY,当您的应用程序结束时 Windows 把这个消息传递进来,当您的应用程序解说到该消息时它已经在屏幕上消失了,这仅是通知您的应用程序窗口已销毁,您必须自己准备返回 Windows 。在此消息中您可以做一些清理工作,但无法阻止退出应用程序。如果您要那样做的话,可以处理 WM_CLOSE 消息。在处理完清理工作后,您必须调用 PostQuitMessage,该函数会把 WM_QUIT 消息传回您的应用程序,而该消息会使得 GetMessage 返回0,然后会结束消息循环并退回 WINDOWS。您可以在您的程序中调用 DestroyWindow 函数,它会发送一个 WM_DESTROY 消息给您自己的应用程序,从而迫使它退出。 |
|