Windows程序内部运行原理及SDK编程实现

来源:互联网 发布:国际阿里云通用流量包 编辑:程序博客网 时间:2024/04/30 11:10

转自:http://blog.163.com/wu_w_s/blog/static/3521321320095822341257/

 

我接触的大多数自学过VC++程序开发的人们都有一个共同的感慨,VC++入门太难了,没有一到两年的工夫,是学不会VC++的。但是,我们的教学实际证明,绝大多数有C语言基础和英语过四级的人们,都可以在半月内学好VC++的,并迅速进行运用的。  
  这是什么原因造成如此巨大的反差呢?原因如下:市面上很难找到一本结合初学者的实际学习疑惑,采用循序渐进的方式,将VC++编程的各种技术透彻地展现到读者面前的。一般的书籍都是简单地教导你怎样使用VC++的操作界面,怎样一步步地编写一个特定功能的小程序,没有分析为什么要这样做,也没总结这样做是一个什么样的机理,有哪些注意事项,能够怎样进行举一反三。这样的结果是导致初学者照着书能够编写该程序,但一撇开书本,就什么都不知道了。  
  再者就是书的章节安排不合理,经常是本末倒置,对于一个小学没上小学的学生,上来就教其学习大学的课程,只会导致初学者死记硬背,满脑子的问号,弄得一头雾水。  
  我们结合实际开发中总结出来的经验与心得,通过在长期的教学中收集到的学员的问题的总结,分析,以及最终如何找到有效的方式来说明讲清该问题。编写了本书。  
   
   
  为了理解Visual   C++应用程序开发过程,先要理解Windows程序的运行机制。因为   Visual   C++是   Windows   开发语言,需要明白在   Windows   环境下编程和在其它环境下编程的一些根本性的差别。  
  Windows   的工作方式:  
  全面地讨论   Windows   的内部工作机制将需要整整一本书的容量,没有必要深入了解所有的技术细节。但是对于windows程序运行的一些根本性的概念,是一个Visual   C++程序员所必须掌握的知识。  
   
  一,Windows应用程序,操作系统,计算机硬件之间的相互关系。  
   
  我们这样解释上面的图例,向下的箭头1表示操作系统能够操纵输出设备,以执行特定的功能,如让声卡发出声音,让显卡画出图形。向上的箭头2表示操作系统能够感知输入设备状态的变化,如鼠标移动,键盘按下,并且能够知道鼠标移动的具体位置,键盘按下的哪个字符。这就是操作系统个计算机硬件之间的交互关系,应用程序开发者通常不需知道其具体实现细节。  
  1.关于API  
  向下的箭头3表示应用程序可以通知操作系统执行某个具体的动作,如操作系统能够控制声卡发出声音,但其并不知道何时发出何种声音,得由应用程序告诉操作系统该发出什么样的声音。这个关系好比有个机器人能够完成行走的功能,如果人们不告诉它往哪个方向上走,机器人是不会主动行走。这里的机器人就是操作系统,人们就是应用程序。应用程序是如何通知操作系统执行某个功能的呢?有编程知识的读者都应该知道,在应用程序中要完成某个功能,都是以函数调用的形式实现的,同样,应用程序也是以函数调用的方式来通知操作系统执行相应功能的,操作系统所能够完成的每一个特殊功能通常都有一个函数与其对应,也就是说,操作系统把它所能完成的功能以函数的形式提供给应用程序使用,应用程序对这些函数的调用叫系统调用,这些函数的集合是Windows操作系统提供给应用程序编程的接口(Application   Programming   Interface),简称Windows   API。如CreateWindow就是一个API函数,应用程序中调用这个函数,操作系统就会按照该函数提供的参数信息产生一个相应的窗口。大家不妨看看lesson1中的源程序,体会一下在程序中是如何调用这个CreateWindow   API函数的,关于这个函数的详细解释,请参阅MSDN(微软开发编程的开发系统)。  
  顺便提一下,对于一个真正的程序员来说,不可能死记硬背每一个API函数及其各参数的详细信息。通常都是只记住其英文拼写,有时甚至是凭着语意拼读出来的,如显示窗口用ShowWindow,退出Windows操作系统用ExitWindows等等,API函数的正确拼写格式及各参数的祥尽信息都是在MSDN迅速检索到的,没必要刻意去死记这些信息,等用的次数多了,这些信息也就在不知不觉中掌握了,但一定要具备在需要的时候能够从帮助系统中检索想要的信息的能力,这样就能做到事半功倍。学习VC++,一定要有一套真实的练习环境,学会查阅帮助系统,决不能纸上谈兵,照着书本亦步亦趋,否则就真的是没有一两年的时间,是学不好VC++的了。  
   
   
  注意:请不要将这里的API与java   API以及其他API混淆。API正如其语义一样,已成为一种被广泛使用的专业术语。如果某个系统或某个设备提供给某种应用程序对其进行编程操作的函数,类,组件等的集合,就称作该系统的API。曾经有学员问我这样的问题,Java   API与windows   API有何关系,是不是指java也可以调用windows里的API?读者现在应该明白这个问题了,不需我来回答了吧?  
  2.关于消息  
  向上的箭头4表示操作系统能够将输入设备的变化上传给应用程序。如用户在某个程序活动时按了一下键盘,操作系统马上能够感知到这一事件,并且能够知道用户按下的是哪一个键,操作系统并不决定对这一事件如何作出反应,而是将这一事件转交给应用程序,由应用程序决定如何对这一事件作出反应。好比有个蚊子叮了我们一口,我们的神经末梢(相当于操作系统)马上感知到这个事件,并传递给了我们的大脑(相当于应用程序),我们的大脑最终决定如何对这一事件作出反应,如将蚊子赶走,或是将蚊子拍死。对事件作出反应的过程就是消息响应,由水平箭头5表示。  
  操作系统是怎样将感知到的事件传递给应用程序的呢?这是通过消息机制来实现的。操作系统将每个事件都包装成一个称为消息的结构体MSG来传递给应用程序的,参看MSDN,MSG结构定义如下:  
  typedef   struct   tagMSG   {           //   msg      
          HWND       hwnd;              
          UINT       message;  
          WPARAM   wParam;  
          LPARAM   lParam;  
          DWORD     time;  
          POINT     pt;  
  }   MSG;  
  看不懂这种定义的读者,请赶快复习C语言,其基本意义是定义一个struct   tagMSG的结构体,并在以后的应用中用MSG来代替struct   tagMsg。该结构体中各成员变量的作用如下:  
  第一个成员变量hwnd即代表消息所属的窗口,一个消息一般都是与某个窗口相联系的,如在某个活动窗口中按下键盘,该键盘消息就是发给该窗口的,在VC中,用HWND变量类型来标识窗口。有关窗口的知识,在稍后有详细解释。  
  第二个成员变量message代表消息代号,无论是键盘按下,还是鼠标移动,都是用一个数字来表示的,不同的数值对应不同的消息。由于数值不便于记忆,在VC中将消息对应的数值定义为WM_xxx宏的形式,xxx对应某种消息的英文拼写的大写,如鼠标移动消息为WM_MOUSEMOVE,键盘按下消息为WM_KEYDOWN,输入一个字符消息为WM_CHAR等等。我们在程序中一般以WM_xxx宏的形式来使用消息。  
  提示:如果想知道WM_xxx消息对应的具体数值,请在程序中选中WM_xxx,单击右键,在弹出菜单中选择goto   definition即可看到该宏的具体定义。跟踪,查看某个变量的定义,使用此方法非常有效。  
  第三个,四个成员变量分别为wParam,lParam,用于对消息进行补充说明,如message成员表示字符消息,但没有说明输入的是哪个字符,这就需要用其他变量对其进行补充说明。wParam,lParam代表的意义,随消息的不同而异。读者可用goto   definition功能查看WPARAM,LPARAM的定义,发现它们分别为unsigned   int和long,并不是什么神秘莫测的变量类型。VC++中之所以要这样做,是希望从变量定义的类型上,就能区分出变量的用途。对于同一种变量类型,可按其用途细分定义成多种其他的形式。这种概念在VC++中被广泛使用,也是导致初学者困惑的一个因素。  
  最后两个变量分别代表发出消息的时间和鼠标的当前位置,这里没有什么需要特殊解释的。  
   
  明白了消息,我们再来看看消息队列。如上面的图例所示,每个Windows程序都有一个消息队列。队列是一个先进先出的缓冲区,通常是一个某种变量类型的数组。消息队列里的每一个元素即一条消息,操作系统将生成的每个消息按先后顺序放进消息队列,第一条消息放入第一格,第二条消息放入第二格,依次类推...。应用程序总是取走队列里的第一条消息,消息取走后,第二条消息成为第一条,剩余的消息依次前移。应用程序取得消息后,便能够知道用户的操作和程序状态的变化。例如,应用程序从队列里取到了一条WM_CHAR消息,那一定是用户输入了一个字符,并且能够知道输入的是哪个字符。应用程序得到消息后,就要对消息进行处理,这即我们通常说的消息响应,消息响应是我们通过编码实现的,这也是Windows程序的主要代码区。在消息响应代码中,我们很可能又要调用操作系统提供的API函数,以便完成特定的功能。如果我们收到窗口的WM_CLOSE消息,我们可以调用DestroyWindow这个API函数来关闭该窗口,或是用MessageBox这个API函数来提示用户是否真的要关闭窗口。  
   
  通过上面的分析,我们可以想象到,要用VC++编写Windows程序,除了要具备良好的C语言功底外,还要求掌握掌握两点知识:1.不同的消息所代表的用户操作和程序状态,2.要让操作系统执行某个功能所对应的API函数。  
   
  3.关于句柄  
  在Windows编程中我们时刻接触到一个称为句柄(HANDLE)的东西。可以这样去理解句柄,Windows程序中产生的任何资源(要占用某一块或大或小的内存),如图标,光标,窗口,应用程序的实例(已加载到内存运行中的程序)。操作系统每产生一个这样的资源时,都要将它们放入相应的内存,并为这些内存指定一个唯一的标识号,这个标识号即该资源的句柄。操作系统要管理和操作这些资源,都是通过句柄来找到对应的资源的。按资源的类型,又可将句柄细分成图标句柄(HICON),光标句柄(HCURSOR),窗口句柄(HWND),应用程序实例句柄(HINSTANCE),等等各种类型的句柄。操作系统给每一个窗口指定的一个唯一的标识号即窗口句柄。  
   
  4.WinMain函数  
  WinMain是Windows程序的入口点函数,同dos程序的入口点函数main的作用相同,当WinMain函数结束或返回时,Windows应用程序结束。WinMain函数的原型如下:  
  int   WINAPI   WinMain(  
      HINSTANCE   hInstance,     //   handle   to   current   instance  
      HINSTANCE   hPrevInstance,     //   handle   to   previous   instance  
      LPSTR   lpCmdLine,             //   pointer   to   command   line  
      int   nCmdShow                     //   show   state   of   window  
  );  
  该函数接受四个参数,这些参数都是系统调用WinMain函数时,传递给应用程序的。  
  第一个参数hInstance表示该程序的当前运行的实例句柄。同一应用程序在同一计算机上可运行多份实例,每启动一个这样的实例,操作系统都要给该实例分配一个标识号,即实例句柄,随后系统调用程序中的WinMain函数,并将该实例句柄传递给参数hInstance。  
  第二个参数hPrevInstance表示当前实例的上一个正在运行的,由同一个应用程序所产生的实例的句柄,即当前实例的"哥哥"的句柄。如果该值为NULL,则表示当前实例是该程序正在运行的第一份实例,是“长子”,是“老大”。如果该值不为NULL,只能表示当前实例不是该程序正在运行的第一份实例,不是“长子”,不是“老大”,但到底是“老几”,就无从得知了。这个参数到底有什么作用呢?如果想让我们的程序只能有一份实例运行,不能同时有多份实例运行,我们可以在WinMain函数的开始部分加上如下代码实现。  
  if(hPrevInstance)   return   0;  
  大多数没有实际开发经验的读者都很难理解这段简单的代码。大家平时见到的if语句通常是if(x!=0)或if(x==0),其实if判断的是括号中的表达式的结果为“真”,还是为“假”。在C语言中,结果为0即“假”,非0即“真”。所以,我们也可以直接用if对某个变量的值进行判断,if(变量)代表“如果变量不等于0”,if(!变量)代表“如果变量等于0”。  
  我们再来看看if(hPrevInstance)   return   0;的作用,如果hPrevInstance为NULL(即0),说明当前运行的实例是程序的第一个实例,WinMain函数将不返回,程序正常向下运行。只要hPrevInstance不为NULL,说明已经有同样程序的实例在运行,WinMain函数将返回,当前实例启动后立马结束,这样就保证了只有程序的一个实例可以运行。这个过程好比“计划生育”,只能要一个孩子,如果第二个孩子已经出世,当他发现自己不是老大,属于计划之外,便进行“自杀”,在程序中对应的是return   0语句。顺便说一下:“计划生育”的比喻只是为了方便大家理解问题,并不是我们的现实生活中真的要如此做法。  
  第三个参数lpCmdLine是一个字符串,里面包含有传递给应用程序的参数串,如:双击C盘下的1.txt文件方式启动notepad.exe程序,传递给notepad.exe程序的参数串即"c:/1.txt",不包含应用程序名本身。要在VC开发环境中给应用程序传递参数,请选择菜单Project->Settings...,在弹出的Project   Settings对话框中选择Debug标签,在该标签页的Program   arguments编辑框中输入你想传递给应用程序的参数。我们在WinMain函数的入口点设置一运行断点,以调试方式启动程序运行至该断点处,将鼠标移动到参数lpCmdLine上,在弹出的黄色小浮框中便能观察到该变量的值。在我们的程序调试中,经常要用到这种方法查看变量的值和状态。  
  第四个参数nCmdShow指定的程序的窗口应该如何显示,如最大化,最小化,隐藏等。  
  WinMain函数前的修饰符WINAPI的解释,请参看下面关于__stdcall的讲解,我们使用goto   definition功能,发现WINAPI其实就是__stdcall。  
   
  Winmain函数的程序代码按功能划分主要有两部分:1.产生并显示程序的主窗口。窗口创建并显示后,用户便可以在窗口上进行各种操作了,用户的操作及程序状态的变化都以消息的形式放到了应用程序的消息队列中。2.从消息队列循环取走消息,并将消息派发到窗口过程函数中去处理。当消息循环取到一条WM_QUIT消息时,将结束循环,WinMain函数返回,结束整个程序的运行。  
  如果WinMain在消息循环之前返回,程序没有正常运行,返回值为0。如果在消息循环之后返回,返回值为WM_QIUT消息的wParam参数。  
   
  5.窗口  
  不妨简单地将窗口看做带有边界的矩形区域。除文字处理程序中的文档窗口或者弹出提示有约会信息的对话框等这些最普通的窗口外,实际上还有许多其它类型的窗口。命令按钮、文本框、选项按钮都是窗口。  
  一个通常的Windows程序都有窗口,通过窗口,用户可以对应用程序进行各种操作。反之,应用程序可以通过窗口收集用户的操作信息,如在窗口上移动鼠标,按下键盘。可以说,窗口是应用程序和用户之间交互的界面,沟通的桥梁,联系的纽带。所以窗口的编写与管理在Windows程序中占有重要的地位。  
   
  一个完整的窗口具有许多特征,包括光标(鼠标进入该窗口时的形状),图标,菜单,背景色等。产生窗口的过程类似汽车的生产过程,在生产汽车前,必须先在图纸设计好该车型(选择搭配汽车的各个部件),并要为这种新设计好的车型起个名称,如“奔驰200”。以后,便可以生产“奔驰200”这款汽车了,可以按照这个型号生产若干辆汽车,同一型号的车,可以具有不同的颜色。  
  产生一个窗口前,也必须设计好窗口(指定窗口的那些特征)。窗口的特性是由一个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,当改变窗口的垂直方向上的高度时,将引发窗口重画。窗口的重画过程好比汽车重新喷漆一样,汽车车身上原有的文字与图案,如"锐信培训中心"的字样将被擦除。同样,当窗口重画时,窗口上原有的文字和图形将被擦除。如果没有指定该值,当垂直方向上拉动窗口时,窗口不会重画,窗口上原有的文字和图形将被保留。  
  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环境,程序将崩溃,但win98和win2000却没有这种现象。

原创粉丝点击