正文

第一课 Windows程序内部运行原理及SDK编程实现(二)2007-09-20 20:48:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/lym51/29559.html

分享到:

产生一个窗口前,也必须设计好窗口(指定窗口的那些特征)。窗口的特性是由一个WNDCLASS结构体进行定义的。参看MSDN,WNDCLASS定义如下:

typedef struct _WNDCLASS {

    UINT    style;

    WNDPROC lpfnWndProc;

    int     cbClsExtra;

    int     cbWndExtra;

    HANDLE  hInstance;

    HICON   hIcon;

    HCURSOR hCursor;

    HBRUSH  hbrBackground;

    LPCTSTR lpszMenuName;

    LPCTSTR lpszClassName; 

} WNDCLASS;

style成员指定了这一类型窗口的样式。比较典型的取值有:

CS_NOCLOSE,这一类型的窗口没有关闭按钮,请实验体会。

CS_VREDRAW,当改变窗口的垂直方向上的高度时,将引发窗口重画。窗口的重画过程好比汽车重新喷漆一样,汽车车身上原有的文字与图案,如"http://www.it315.org"的字样将被擦除。同样,当窗口重画时,窗口上原有的文字和图形将被擦除。如果没有指定该值,当垂直方向上拉动窗口时,窗口不会重画,窗口上原有的文字和图形将被保留。

CS_HREDRAW,当改变窗口的水平方向上的宽度时,将引发窗口重画。

CS_DBLCLKS,设置该,可以接受到用户双击的消息。

其他的设置值请参阅MSDN,在一些特殊的场合可能要用到这些置。

提示:

在我们的程序中经常要用到一类变量,这个变量里的每一位(bit)都对应某一种特性。当该变量的某位为1时,表示有该位对应的那种特性,当该位为0时,即没有那位所对应的特性。当变量中的某几位同时为1时,就表示同时具有那几种特性的组合。一个变量中的哪一位代表哪种意义,不容易记忆,所以我们经常根据特征的英文拼写的大写去定义一些宏,该宏所对应的数值中仅有与该特征相对应的那一位(bit)为1,其余的bit都为0。我们再次使用goto definition就能发现CS_VREDRAW=0x0001,CS_HREDRAW=0x0002, CS_DBLCLKS =0x0008,CS_NOCLOSE=0x0200。他们的共同点就是只有一位为1,其余位都为0。如果我们希望某一变量的数值即有CS_VREDRAW特性,又有CS_HREDRAW特性,我们只需使用(|)操作符将他们相组合,如style=CS_VREDRAW| CS_HREDRAW | CS_NOCLOSE。如果我们希望在某一变量原有的几个特征上去掉其中一个特征,用(&~)就能够实现,如在刚才的style的基础上去掉CS_NOCLOSE特征,可以用style & ~CS_NOCLOSE实现。

lpfnWndProc成员指定了这一类型窗口的过程函数,也称回调函数。回调函数的原理是这样的,当应用程序收到给某一窗口的消息时(还记得前面讲过的消息通常与窗口相关的吗?),就应该调用某一函数来处理这条消息。这一调用过程不用应用程序自己来实施,由操作系统完成,但回调函数本身的代码由应用程序完成。对一条消息,操作系统到底调用应用程序中的哪个函数(回调函数)来处理呢?操作系统调用的就是接受消息的窗口所属的类型中的lpfnWndProc成员指定的函数。每一种不同类型的窗口都有自己专用的回调函数,该函数就是通过pfnWndProc成员指定的。汽车厂家生产汽车好比应用程序创建窗口,用户使用汽车好比操作系统管理窗口,某种汽车在销售前就指定好了修理站(类似回调函数),当用户的汽车出现故障后(类似窗口收到消息),汽车用户(类似操作系统)自己直接找到修理站去修理,不用厂家(类似应用程序)亲自将车送到修理站去修理,但修理站还得由厂家事先建造好。

提示:lpfnWndProc成员的变量类型为WNDPROC,我们使用goto definition将发现WNDPROC是被如下定义的:

typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);读者不要被新的数据类型LRESULT,CAllBACK所吓倒,只要再次使用goto definition就知道他们的庐山真面目分别为long__stdcall。顺便帮助大家复习一下C语言的知识。首先是关于用typedef定义指向函数的指针类型的问题,其次是__stdcall修饰符的问题。typedef int (*PFUN)(int x,int y);这样就定义了一个函数指针类型PFUN。以后便可以用PFUN定义变量。应用如下:int add(int x,int y);PFUN pfun=add;int sum=pfun(3,5);能够赋值给pfun的函数原型必须严格与PFUN的定义相同。WNDPROC定义了指向窗口回调函数的指针类型,回调函数的格式必须与WNDPROC相同。

__stdcall__cdecl是两种不同的函数调用习惯,定义了参数的传递顺序、堆栈清除等。关于它们的详细信息请参看msdn。由于除了那些可变参数的API函数外,其余的API函数都是__stdcall习惯。由于VC++程序默认的编译选项是__cdecl,所以在VC++中调用这些__stdcall习惯的API函数,必须在声明这些函数的原型时加上__stdcall修饰符,以便对该函数的调用使用__stdcall习惯。我们曾有这样的经验,在Delphi(默认的编译选项是__stdcall)中编写的dll中的函数,在VC++中被调用时,总是造成程序崩溃,在函数的原型声明中加上__stdcall修饰符,便解决了这个问题。回调函数也必须是__stdcall调用习惯,在这里是用CALLBACK来标识的,否则,在NT4.0环境,程序将崩溃,但win98win2000却没有这种现象。

 

cbClsExtra,cbWndExtra这两个成员变量一般都被初始化为0,如需进一步了解这两个参数更详细的信息,请参看msdn

hInstance成员指定了提供回调函数的程序实例句柄。

hIcon成员指定了这一类型窗口的图标句柄,LoadIcon函数可以加载一个图标资源到内存中并返回系统分配给该图标的句柄。LoadIcon函数的详细信息请参阅msdn,但要注意的是,如果加载的是系统的标准图标,第一个参数必须为NULL,如果加载的是应用程序中自定义的图标,对于第二个参数要用MAKEINTRESOURCE宏转换。

 

VC++,对于自定义的菜单,图标,光标,对话框等都是以资源的形式进行管理的,它们的定义与描述存放在资源文件中(扩展名为.rc),资源文件是文本格式,读者可以用notepad.exe打开,阅读里面的信息。在VC++中是以“所见即所得”的方式打开资源文件的,在编辑窗口中看到的和编辑完后的结果即程序运行时的效果。对于每一个资源及资源中的子项都是用一个标识号来标识的,通常称为ID,同一个ID可以标识多个不同的资源。注意区别资源的ID号与句柄的区别,ID号是应用程序指定的,可以在资源还没在内存中产生前指定,也可在设计阶段就指定,基本上是固定的。而句柄则是资源在内存中产生时由操作系统临时安排的,每次产生的句柄可能都不一样,一个ID号标识的资源可在内存中产生多个实例句柄。资源文件中的ID标识符必须在"resouce.h"头文件中用宏定义成一个整数,这样程序中用到的一个ID号标识符实际上就是那个整数。

LoadIcon的第二个参数是LPCTSTR类型,用goto definition功能发现它实际被定义成CONST CHAR *,是字符串常量指针,而图标的ID是一个整数。参看msdn中的提示,对于这样的情况我们需用MAKEINTRESOURCE这个宏把资源ID标识符转换为需要的LPCTSTR类型。使用goto definition功能,或在MSDN中都可以看到MAKEINTRESOURCE的定义:

         #define MAKEINTRESOURCE(i)  (LPTSTR) ((DWORD) ((WORD) (i)))

之所以可以这样做,是因为字符串变量本身代表的就是一个字符数组的首地址,本身就是一个数字。所以字符串变量可以类型转换成整数,反之,一个整数也可以类型转换成字符串型。

 

hCursor成员指定了这一类型窗口的光标句柄,LoadCursor函数可以加载一个光标资源到内存中并返回系统分配给该光标的句柄。除了加载的是光标外,其特点与LoadIcon函数一样。

hbrBackground成员指定了这一类型窗口重画时所使用的刷子句柄。当窗口重画时会使用这里指定的刷子去刷新窗口背景。刷子是具有颜色和形状的,我们可以使用GetStockObject返回一个系统刷子,也可以直接使用msdn中提供的宏,如COLOR_WINDOWTEXT,还可以用CreateBrushIndirect函数产生具有一定形状的刷子。由于GetStockObject参数能够返回标准的刷子、笔、字体、调色板等图形设备对象,定义该函数时,是无法确定该函数到底返回的是刷子还是笔,所以该函数返回类型是HGDIOBJECT(图形设备对象的总称)。由于编译器的需要,在这里我们必须HGDIOBJECT转换成HBRUSH

顺便提示:在VC++开发Windows程序中,类型转换的频率非常高,在这有必要重点介绍一下。比如有个函数为“去叫一个人来帮忙”,定义该函数时,其返回值只能是“人”,但实际来的“人”,要么是“男人”,要么是“女人”。即使叫来的是一个“男人”,如果将该函数的返回值直接赋给一个“男人”类型的变量,编译时是没法确定返回的是“男人”,还是“女人”,将不会通过。只有我们写程序的人才知道运行时返回的是“男人”,还是“女人”,我们可以对返回值进行类型转换,以便编译器通过。在类型转换时,程序员要对转换完的后果负责,要确保在内存中存在的对象本身确实可以被看成那种要转换成的类型,如果来的是“女人”,我们将其转换成“男人”后,编译能够通过,但程序运行时将会出错。作者在编码和调试时,总是用意境的方式,仿佛看到变量或对象在内存中的真实布局和状态,以及是如何进行转换的,这样编码时比较容易一气呵成,极少犯错。

lpszMenuName成员指定了这一类型窗口的菜单。可见菜单本身不是一个窗口,同图标、光标一样,是窗口的一个元素。不少的人和书都错以为菜单也是一个窗口,其实我们用Spy++实用工具的Find Window功能就能够区分出桌面上的哪些元素为窗口,哪些不是。lpszMenuNameLPCTSTR类型,需用MAKEINTRESOURCE这个宏把资源ID标识符转换为lpszMenuName需要的LPCTSTR类型。

 

lpszClassName成员指定了这一类型窗口的名称,是字符串变量。与设计一辆新型汽车后,要为该汽车型号指定名称一样,设计了一种新型窗口后,也要为这种新型窗口起个名称。我们先将这里的名称指定成"http://www.it315.org",等会我们将看到如何使用这个名称。

 

设计完WNDCLASS后,需调用RegisterClass函数对其进行注册,以后便可以用CreateWindow函数产生这种类型的窗口窗口了。CreateWindow函数的定义如下:

HWND CreateWindow(

  LPCTSTR lpClassName,  // pointer to registered class name

  LPCTSTR lpWindowName, // pointer to window name

  DWORD dwStyle,        // window style

  int x,                // horizontal position of window

  int y,                // vertical position of window

  int nWidth,           // window width

  int nHeight,          // window height

  HWND hWndParent,      // handle to parent or owner window

  HMENU hMenu,          // handle to menu or child-window identifier

  HANDLE hInstance,     // handle to application instance

  LPVOID lpParam        // pointer to window-creation data

);

参数lpClassName即我们刚才在WNDCLASSlpszClassName成员指定的名称,在这里应该为"http://www.it315.org",表示要产生"http://www.it315.org"这一类型的窗口。产生窗口的过程是由操作系统完成的,如果在调用CreateWindow函数之前,还没有用RegisterClass函数注册过名称为"http://www.it315.org"的窗口类型,操作系统无法得知这种窗口类型的配置信息,窗口产生过程失败。

参数lpWindowName指定产生的窗口实例上显示的标题文字。

参数dwStyle指定产生的窗口实例的样式,就象同一型号的汽车可以有不同的颜色一样,同一型号的窗口也可以有不同的外观样式。要注意区别WNDCLASS中的style成员与参数dwStyle,前者是针对一个大类,后者是针对个别。

参数x,y,nWidth,nHeight指定了窗口左上角的x,y坐标,窗口的宽度,高度。如果x被设置成CW_USEDEFAULT,系统将窗口的左上角设置为确省值,参数y将被忽略。如果nWidth被设置成CW_USEDEFAULT,系统将窗口的大小设置为确省值,参数nHeight将被忽略。

参数lpWindowName指定了窗口的父窗口句柄。窗口之间可以组合成父子关系,子窗口必须具有WS_CHILD样式,当父窗口被破坏,隐藏,移动,显示时,也会破坏,隐藏,移动,显示子窗口。当lpWindowNameNULL时,桌面就成为当前窗口的父窗口。

参数lpWindowName指定了窗口的菜单或子窗口句柄。

参数hInstance指定了窗口所属的应用程序的句柄。

参数lpParam可以为窗口附加补充信息。

如果窗口创建成功,函数将返回系统为该窗口分配的句柄,否则,返回NULL

 

6.消息循环

通常的消息循环代码如下:

       MSG msg;

       while(GetMessage(&msg,NULL,0,0))

       {

           TranslateMessage(&msg);

           DispatchMessage(&msg);

       }

GetMessage函数从应用程序消息队列中取走一条消息,该函数的原型如下:

BOOL GetMessage(

  LPMSG lpMsg,         // address of structure with message

  HWND hWnd,           // handle of window

  UINT wMsgFilterMin,  // first message

  UINT wMsgFilterMax   // last message

);

参数lpMsg是接受消息的变量的指针。

参数hWnd指定了接收属于哪个窗口的消息。

参数wMsgFilterMin,wMsgFilterMax指定了接受某一范围内的消息。

如果队列中没有满足条件的消息,该函数将一直等待,不会返回。除了WM_QUIT消息外,该函数返回非零值,对WM_QUIT消息,该函数返回零。也就是说,只有收到WM_QUIT消息,上述代码才能退出while循环,程序才有可能结束运行。

TranslateMessage函数对取道的消息进行转换。用户按动一下某个键,系统将发出WM_KEYDOWN, WM_KEYUP,并且参数中提供的是该键的虚拟扫描码。但有时用户按动一下某个键, 我们想得到一条表示用户输入了某个字符的消息,并在消息补充参数中提供字符的编码。TranslateMessage能将可行的WM_KEYDOWN, WM_KEYUP消息对转换成一条WM_CHAR消息,将可行的WM_SYSKEYDOWN, or WM_SYSKEYUP消息对转换成一条WM_SYSCHAR消息,并将转换后得到的新消息投递到程序的消息队列中。转换过程不会影响原来的消息,只在消息队列中增加新消息。

DispatchMessage函数将取道的消息传递到窗口的回调函数中去处理。可以理解成该函数通知操作系统,让操作系统去调用窗口的回调函数来处理收到的消息。

所有Windows程序的消息处理代码基本上都是相同的,如没特殊需要,可以照搬照抄上述代码。

顺便提示:从队列中取消息还有PeekMessage函数,PeekMessage函数有两种取消息的方式,第一种与GetMessage一样,从队列中直接取走消息,第二种是只取消息的一个副本,并不将消息从队列中取走。无论哪种方式,PeekMessage都不会因队列中没有满足条件的消息而阻塞,当取到满足条件的消息,该函数返回非零值反之,返回零。向队列中发送消息有PostMessageSendMessage,PostMessage函数发送消息后立即返回,而SendMessage需等到发送的消息被处理完后才能返回。还有一个PostThreadMessage用于向线程发送消息,关于线程,请在以后的章节再学,但我们也因此想到消息不一定总是与窗口相关的,也就是说,对于某些消息,其对应的MSG结构中的hwnd可以为NULL

 

7.回调函数

回调函数的原型为:

LRESULT CALLBACK WindowProc(

  HWND hwnd,      // handle to window

  UINT uMsg,      // message identifier

  WPARAM wParam,  // first message parameter

  LPARAM lParam   // second message parameter

);

我们可以将函数名WindowProc改为我们喜欢的名称,如DefWndProc,该函数的四个参数对应消息的窗口句柄,消息码,消息码的两个补充参数。

在该函数内部是一个庞大的switch语句,用于对各种感兴趣的消息进行处理。我们分析程序中的代码。

LRESULT CALLBACK DefWndProc( HWND hWnd,UINT Msg,WPARAM wParam, LPARAM lParam)

{

       HDC hDC;

       switch(Msg)

       {

       case WM_CHAR:

              char str[20];

              sprintf(str,"the char code is %d",wParam);

              MessageBox(hWnd,str,"www.it315.org",MB_OKCANCEL);

              break;

       case WM_LBUTTONDOWN:

              MessageBox(hWnd,"mouse click","www.it315.org",MB_OK);

              hDC=GetDC(hWnd);

              TextOut(hDC,LOWORD(lParam),HIWORD(lParam),

                         "http://www.it315.org",strlen("http://www.it315.org"));

              ReleaseDC(hWnd,hDC);

              break;

       case WM_CLOSE:

              if(IDOK==MessageBox(NULL,"真的要要退出吗?",

                     "http://www.it315.org",MB_OKCANCEL|MB_ICONQUESTION))

              {

                     DestroyWindow(hWnd);

              }

              break;

       case WM_PAINT:

              PAINTSTRUCT ps;

              hDC=BeginPaint(hWnd,&ps);//WM_PAINT里必须用这个函数

              TextOut(hDC,0,0,"http://www.it315.org",strlen("http://www.it315.org"));

              EndPaint(hWnd,&ps);

              break;

       case WM_DESTROY:

              PostQuitMessage(0);

              break;

       default:

              return(DefWindowProc(hWnd,Msg, wParam,lParam ));

       }

       return (0);

}

 

当用户在窗口上按下一个字符键,程序将得到一条WM_CHAR消息,参看msdn,在其wParam参数中含有字符的码值。使用程序中的代码,我们可以随时获得某个字符的码值,不用为此去专门查找书籍。MessageBox函数可以弹出一个显示信息的对话框,如果我们按下BackSpace键,在这里将弹出"the char code is 8"消息。

当用户在窗口上按下鼠标左按钮时,程序弹出一条消息框对收到的WM_LBUTTONDOWN进行响应,以证明按下鼠标左按钮动作与WM_LBUTTONDOWN消息的这种对应关系,另外程序还将在窗口上鼠标所按下的位置写上一串文字。当我们要在窗口上写字、绘图,并不直接在这些设备上操作,而是在一个称为设备描述表(Device Contexts,简称DC)的资源上操作的。使用DC,程序不用为图形的显示与打印输出分别单独处理了。无论是打印还是显示,我们都是在DC上操作,然后由DC映射到这些设备上。使用DC,我们不用担心程序在不同的硬件上执行的差异性,DC能够为我们装载合适的设备驱动程序,实现程序的图形操作与底层设备无关。

GetDC函数能够返回同窗口相关连的DC的句柄。

TextOut函数在当前DC上指定的位置输出一个字符串,当按下鼠标左按钮,消息WM_LBUTTONDOWN的补充参数lParam中含有鼠标位置的x,y坐标,其中的低16位含有x坐标,高16位含有y坐标,可以分别通过LOWORD,HIWORD宏获得。

对于每次成功调用GetDC返回的DC句柄,在执行完所有的图形操作后,必须调用ReleaseDC释放该句柄所占用的资源。否则,会造成内存泄露。我曾经帮助一个叫王健的学员调试他的On-job项目程序,他发现他的程序所占用的内存总是以4k的增量向上增长,每运行几小时后,程序便因内存不足而崩溃了。后来发现就是因为程序在定时器中反复使用GetDC而没有使用ReleaseDC释放造成的。读者可以将例子程序中的ReleaseDC一句注释掉,编译后运行,在NT4.0/win2000下启动任务管理器,切换到进程标签,查看你的程序所使用的内存量,在程序中不断点击鼠标左按钮,程序将不断调用GetDC函数,你将发现你的程序占用的内存量不断向上增长,我们通常使用这样的方式来检测程序的内存泄露的。

当用户点击窗口上的关闭按钮时,系统将给应用程序发送一条WM_CLOSE消息,如果程序不做进一步处理,窗口是不会关闭的。我们在程序中利用一个选择对话框,如果用户确认真的要退出,程序调用DestroyWindow函数将窗口关闭。

窗口关闭后,系统将给应用程序发送一条WM_DESTROY消息,需要注意的是,主窗口的关闭,不代表应用程序结束,在WinMain函数中的消息循环代码中,GetMessage函数必须取到一条WM_QUIT,消息循环才能结束。要让程序正常退出,必须在WM_DESTROY消息响应代码中,调用PostQuitMessage函数向程序的消息队列中发送一条WM_QUIT消息,PostQuitMessage函数的参数值传递给WM_QUIT消息的wParam参数,通常用作WinMain函数的返回值。

当窗口第一次产生,移动,改变大小,从其他窗口后面切换到前面等情况都会导致窗口的重画。重画时将使用设计窗口类时指定的刷子粉刷窗口的背景,窗口上原有的文字和图形都将被擦除掉。要想让图形和文字总显示在窗口的表面,只能是在这些图形和文字被擦除后,立即又将它们画上去。这个过程对用户来说,是感觉不到的,他们只能感觉到这些图形和文字永远都和窗口一并存在。当系统粉刷完窗口的背景后,都会发送一条WM_PAINT消息,以便通知应用程序原有的图形和文字已被擦除,如果还想保留哪些图形和文字,请在此处加入处理代码。也就是说,我们在WM_PAINT消息响应中作出的图形和文字是“永远”存在的。对于WM_PAINT消息响应代码中要获得窗口的DC,只能使用BeginPaint函数,除此之外的消息响应代码中必须用GetDC获得窗口的DC,BeginPaint获得的DC最后必须用EndPaint释放。提醒:水平或垂直改变窗口的大小时,窗口是否重画,取决于WNDCLASS结构中style成员的设置中是否包含CS_VREDRAWCS_HREDRAW

 

DefWindowProc函数提供了对所有消息的缺省处理方式,对于大多数不想特殊处理的消息,程序都可以调用这个函数来处理,所以程序在switchdefault语句中调用此函数进行处理。

 

8.程序编写操作步骤与实验。

1) 首先启动VC++,在菜单中选择File->New,在弹出的窗体中选择Projects标签,然后在左侧选择Windows Application, 在右侧的Project name:文本框中为新建的工程起一个名字,VC++会为新建的工程在硬盘上建一个与工程同名的文件夹,这个文件夹放在Location:指定的路径下,你可以点击Location旁边的"..."按钮来改变路径。接下来一定要选择Create new workspace单选按钮,并勾选Platforms:中的Win32复选框。完成后的界面如图1-1所示:

2) 这一课对第一次接触Windows编程的读者来说,新的东西太多。但只要你把这些知识基本理解和掌握,学好VC++的日子离你就不太遥远了。惟有如此,你才可能精通VC++编程。


阅读(5504) | 评论(0)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

暂无评论
您需要登录后才能评论,请 登录 或者 注册