必备的Win32程序设计原理

【动态链接需要什么函数库】
  众所周知,Windows支持动态链接,换句话说,应该程序所调用的Windows API函数是在“执行时期”才链接上去的。

  Windows程序调用的函数可分为C Runtimes和Windows API两在部分,以下是C Runtimes所用到库文件及其说明:
  ■LIBC.LIB ---- 这是C Runtimes函数库的动态链接版本。
  ■MSVCRT.LIB ---- 这是C Runtimes函数库动态链接版本(如果链接这一函数库,你的程序执行进必须有MSVCRT40.DLL)。
  对于API函数则由系统的三大模块GDI32.DLL、USER32.DLL和DERNEL32.DLL提供。虽说动态链接是在执行期间才发生“链接”事实,但在连接时期,连接器仍需先为应用程序准备一些适当的信息,才能够在执行时期顺利“跳”到DLL中执行。这些适当的信息放在所谓的“import函数库”中,32位Windows的三大模块所对应的improt函数库分别是GDI32.LIB、USER32.LIB和KERNEL32.LIB。

  其实并不是.dll的都是动态链接函数库,.exe、.fon、.mod、.drv、.ocx也是所谓的动态链接函数库。

【需要什么头文件】
  所有Windows程序都必须包含windows.h,早期这是一个巨大的头文件(大约有5000行左右),VC++4.0已经把它切割为各个较小的文件,但还是以windows.h总括之。除非你十分清楚什么API操作需要什么头文件,否则为求便利,单单一个windows.h也就是了!

【以消息为基础(Message Based),以事件事件驱动(Event driven)】
  Windows程序的进行依靠外部发生的事件来驱动。换句话说,程序不断等待(利用一个while循环),等待任何可能的输入,再作适当的处理。
  消息队列有系统消息队列(只有一个)和程序消息队列(每个程序一个)之分,由硬件设备产生的消息(如键盘和鼠标等),放在系统消息队列中,而由系统或其它程序发送过来的消息,放在程序消息队列中。你不需要管它是从那个队列中来,只要知道它是用GetMessage取来的就可以了,程序的生命靠它来推动。所有GUI系统,名手UNIX的X Window和OS/2的Presentation Manager都是这样的。

【程序进入点】
  main是一般C程序的进入点,WinMain则是Windows程序的进入点。当Windows的“外壳”(Shell,如资源管理器)侦察到使用者要执行一个Windows程序,于是调用加载器把该程序加载,然后调用C startup code,再由C startup code调用WinMain,开始执行程序。
  WinMain函数有四个固定的参数,而且这四个参数都是由操作系统负责传递进来。

【窗口的诞生】
  一个程序窗口的产生及其显示在屏幕上,需要经过以下步骤:
  ■设置窗口类的样式WNDCLASS(也就是说,设定窗口的外观)。
  ■用API函数RegisterClass注册已经设置好的窗口类。
  ■用API函数CreateWindow产生一个窗口。
  ■用API函数ShowWindow显示窗口。

  在Windows3.x时代,窗口类只需注册一次,即可供同一程序的后续每一个实例(Instance)使用,之所以能够如此,是因为所有进程同在一个地址空间中,但在WindowsNT和Windows9x中略有变化,由于Win32程序的每个实例(Instance)有自己的地址空间,故共享同一窗口类已不可能。

【消息循环】
  初始化工作(包括窗口的产生和显示)完成后,WinMain进入所谓的消息循环:
while ( GetMessage(&msg, ...) )
{
  TranslateMessage ( &msg );    //翻译键盘消息
  DispatchMessage ( &msg );    //分派消息到相应的消息处理函数
}
其中TranslateMessage是为了将键盘消息中的按键代码翻译成Windows的虚拟键值。消息循环中的GetMessage是Windows3.x非强制性(Non-preemptive)多任务的关键,应用程序藉由此操作,提供了释放控制权的机会:“如果消息队列上没有属于我的消息,我就把机会让给别人”。通过程序之间彼此协调让步的方式,达到多任务能力。Windows9x和WindowsNT具备强制性(Preemptive)多任务能力,不再非靠GetMessage释放CPU控制权不可,但程序写法依然不变,因为应用程序仍然需要靠消息推动。

【窗口的生命中枢:窗口消息处理函数】
  窗口消息处理函数是被Windows系统所调用的(我们并没有在应用程序的任何地方调用此函数),所以这是一种call back函数,意思是指“在你的程序中,被Windows系统调用的函数,这些函数虽然由你设计,但是永远不会也不该被你调用,它们是为Windows系统准备的。

  窗口消息处理函数的形式相当一致,必然是:
LRESULT CALLBACK WndProc ( HWND hWnd, UINT message,
                                                             WPARAM wParam, LPARAM lParam )

  注意,无论什么消息,都必须处理,所以消息处理函数的最后必须调用DefWindowProc,这是Windows内部默认的消息处理函数。

【消息映射(Message Map)的雏形】
个人感想:
  终于开始说到一点MFC是怎样封装API的了,所谓的映射其实就是用指针指向对应的东西,看了前两章,我觉得这里是认识MFC的关键(并不是指整本书的关键啊),还是看看侯老师是怎样解释的吧!

  有没有可能把消息处理函数的内容设计得更模块化、更一般化些呢?下面是一种做法。请注意,做法只是MFC“消息映射表”的雏形,这里所采用结构和变量名称都与MFC相同,藉此让你先有个暖身。

  首先,定义一个MSGMAP_ENTRY的结构体和一个dim宏:
struct MSGMAP_ENTRY
{
  UINT    nMessage;                                        //消息message
  LONG    (*pfn)( HWND, UINT, WPARAM, LPARAM );            //对应的消息处理函数的指针
};
#define dim(x)    ( sizeof(x) / sizeof(x[0]) )                //这里我不太明白,为什么不是DIM呢?

  MSGMAP_ENTRY中的两个元素的意图就是以函数指针(第二个元素)所指的函数来处理nMessage消息,这正是面向对象观念中把“数据”和“处理数据的方法”封装起来的一种具体实现,只不过我们用的不是C++语言而已。

 

接下来,创建两个数组_messageEntries[]和_commandEntries[],把程序中欲处理的消息以及消息处理函数的关联性建立起来:
//消息与消息处理函数的对照表
struct MSGMAP_ENTRY        _messageEntries [] =
{
  //这是消息        这是消息处理函数
  WM_CREATE,        OnCreate,
  WM_PAINT,        OnPaint,
  WM_SIZE,        OnSize,
  WM_COMMAND,        OnCommand,
  WM_SETFOCUS,    OnSetFocus,
  WM_CLOSE,        OnClose,
  WM_DESTROY,        OnDestroy,
};

//Command-ID(菜单项)与处理函数的对照表
struct MSGMAP_ENTRY        _commandEntries [] =

{
  //这是Com-ID    这是命令处理函数
  IDM_ABOUT,        OnAbout,
  IDM_FILEOPEN,    OnFileOpen,
  IDM_SAVEAS,        OnSaveAs,
};

于是我们原来熟悉的WndProc函数就可以设计成:
LRESULT
CALLBACK    WndProc ( HWND hWnd, UIN message,
                                             WPARAM wParam, LPARAM lParam )
{
  int        i;

  for ( i=0; i<dim(_messageEntries); ++i )                //消息对照表
  {
    if ( message == _messageEntries [i].nMessage )        //查找对应的消息
      return ( (*_messageEntries[i].pfn)(hWnd, message, wParam, lParam) );
  }
  return DefWindowProc ( hWnd, message, wParam, lParam );
}

//专门处理WM_COMMAND
LONG

OnCommand ( HWND hWnd, UINT message,
                            WPARAM wParam, LPARAM lParam )
{
  int        i;
  for ( i=0; i<dim(_commandEntries); ++i )                //菜单项目对照表
  {
    if ( LOWORD(wParam) == _commandEntries [i].nMessage )//查找对应的菜单ID
      return ( (*_commandEntries[i].pfn)(hWnd, message, wParam, lParam) );
  }
  return DefWindowProc ( hWnd, message, wParam, lParam );
}

  这么,WndProc和OnCommand永远不必改变,每当有新的要处理的消息,只要在_messageEntries[]和_commandEntries[]两个数组中加上新元素,并针对新消息写新的处理函数即可。

【对话框的运行】
  Windows的对话框依其与父窗口的关系,分为两类:
  1.“令其父窗口无效,直至对话框结束”,这种称为Model对话框(也就是所谓的模式对话框)。
  2.“父窗口与对话框共同运行”,这种称为Modeless对话框(也就是所谓的非模式对话框)。

  为了做出一个对话框,程序员必须准备两样东西:
  1.对话框模板(Dialog template)。这是在RC文件中定义的一个对话框外貌,以各种方式决定对话框的大小、字形、内部有哪些控件、各在什么位置等等。
  2.对话框消息处理函数(Dialog procedure)。其形态类似于主窗口的消息处理函数,但是它通常只处理WM_INITDIALOG和WM_COMMAND两个消息。对话框中的各个控件也都是小小窗口,各有自己的消息处理函数,它们以消息与其管理者(父窗口,也就是对话框)沟通。而所有传来的消息都是WM_COMMAND。

  对话框处理过消息之后,应该传回TRUE,如果没有处理消息,则应该传回FALSE。这是因为你的对话框函数的上层还有一个系统提供的默认对话框处理函数,如果你传回FALSE,该默认对话框的处理函数就会接手处理。

【Windows程序的生与死】
  现在我以窗口的诞生和死亡,说明消息的发生与传递,以及应用程序的兴起与结束,其过程如下:
  1.程序初始化过程中调用CreateWindow,为程序建立了一个窗口,作为程序的屏幕舞台。CreateWindow产生窗口之后会送出WM_CREATE直接给窗口消息处理函数。
  2.在程序活着的过程中,不断以GetMessage从消息队列中抓取消息。如果这个消息是WM_QUIT,GetMessage会传回0而结束while循环,进而结束整个程序。
  3.DispatchMessage通过Windows USER模块的协助与监督,把消息分派至窗口消息处理函数。消息将在该处被判别并处理。
  4.程序不断进行2.和3.的操作。
  5.当使用者按下系统菜单的Close命令项时,系统送出WM_CLOSE。通常程序的窗口消息处理函数不拦截此消息,于是DefWindowProc处理它。
  6.DefWindowProc收到WM_CLOSE后,调用DestroyWindow把窗口消除。DestroyWindow本身又会送出WM_DESTROY.
  7.程序对WM_DESTROY的标准反应是调用PostQuitMessage。
  8.PostQuitMessage没什么其它操作,就只送出WM_QUIT消息,准备让消息循环中的GetMessage取得,如步骤2,结束消息循环。

  为什么结束一个程序复杂如斯?因为操作系统与应用程序职责不同,二者是相互合作的关系,所以必须各做各的事,并互以消息通知对方。你可以作一个小实验,在窗口函数中拦截WM_DESTROY,但不调用PostQuitMessage。你会发现当选择系统菜单中的Close时,屏幕上的窗口消失了(因为窗口摧毁及数据结构的释文是DefWindowProc调用DestroyWindow完成的),但是应用程序本身并没有结束(因为消息循环结束不了),它还留在内存中。

【空闲时间的处理:OnIdle】
  所谓空闲时间(Idle time),是指“系统中没有任何消息等待处理”的时间。空闲时间常常发生,不要认为你移动鼠标时产生一大堆的WM_MOUSEMOVE,事实上夹杂在每个WM_MOUSEMOVE之间就可能存在许多空闲时间。毕竟,计算机的速度超乎想象(这是几年前写的一句话,可想而知,现在的计算机速度就更不用说了)。
  后台工作最适宜在空闲时间完成。要处理空闲时间,可以以下列循环取代WinMain中传统的消息循环:
while ( TRUE )
{
  if ( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE )
  {
    if ( msg.message == WM_QUIT )
      break;
    TranslateMessage ( &msg );
    DispatchMessage ( &msg );
  }
  else
    OnIdle ();
}
  PeekMessage和GetMessage的性质不同。它们老师到消息队列中抓消息,如果抓不到,程序的主执行线程(Primary thread,是一个UI执行线程)会被操作系统挂起,当操作系统再次回来照顾这一执行线程时,发现消息队列中仍然是空的,这时候两个API函数的行为就有不同了:
  ■GetMessage会过门不入,于是操作系统再去照顾其它人。
  ■PeekMessage会取回控制权,使程序得以执行一段时间。于是上述消息循环就进入OnIdle函数中。

【Console程序】
  说到Windows程序,一定得有WinMain、消息循环、窗口函数。即使你只产生一个对话框(Dialog Box)或消息窗口(Message Box),也有隐藏的WindowsAPI内里的消息循环和窗口函数。
  过去那种单单纯纯的C/C++程序,有着简单的main和printf的好时光到哪里云了?
  其实在VC++中也可以写一个“像DOS”的程序,而且仍然可以调用部分的,不牵扯到图形使用接口(GUI)的Win32API函数,这种程序称为Console(控制台)程序。甚至你还可以在Console程序中使用部分的MFC类(与GUI没有关联的)。

  在BBS文坛上看到很多程序设计初学者,还没有学习C/C++,就想直接学习VC++,并不是他们好高骛远,而是他们以为VC++是一种特殊的C++语言,吃过苦头的过来人以为初学者所说的VC++ Programming是指MFC Programming,所以在吃一惊(没有一点C++基础就要学习MFC Programming,当然是大吃一惊)。

【Console程序与DOS程序的差别】
  不少人把DOS程序和Console程序混为一谈,这是不对的。
  在Windows环境下的DOS Box中,或是在Windows版本的各种C++编译器套件的集成开发环境(IDE)中,利用Windows编译器、链接器做出来的程序,都是所谓Win32。如果程序以main为进入点,调用C Runtime函数和“不牵扯GUI”的Win32API函数,那么就是一个Console程序,Console窗口将成为其标准输入和标准输出设备(cin和cout)。
  过去在DOS环境下开发的程序,称为DOS程序,它也是以main作为程序进入点,可以调用C Runtime函数,但,当然,不可能调用Win32API函数。

【MFC Console程序设计】
  我挑一个最单纯而无须与别人攀缠纠葛的MFC类,写一个40行的小程序。目的纯粹是为了做一个导人,并与Win32 console程序做一比较。

  我所挑选的两个单纯的MFC类是CStdioFile和CString。在MFC中,CFile用来处理正常的文件I/O操作。CStdioFile继承自CFile,一个CStdioFile对象代表以C Runtime函数fopen所开启的一个stream文件。
  CString是一个完全独立的类。

  我的例子用来计算小于100的所有费伯纳契数列(Fabonacci sequence)(这是台湾和港澳的说法,我们大陆不是叫这个名字,但我记不起叫什么数列了)。其数列的计算方法是:
  1.头两个数为1。
  2.接下来的每一个数是前两个数的和。

// File   : MFCCON.CPP
// Author : J.J.Hou / Top Studio
// Date   : 1997.04.06
// Goal   : Fibonacci sequencee, less than 100
// Build  : cl /MT mfccon.cpp  (/MT means Multithreading)

#include <afx.h>
#include <stdio.h>

int main()
{
  int lo, hi;
  CString str;
  CStdioFile fFibo;

  fFibo.Open("FIBO.DAT", CFile::modeWrite |
                              CFile::modeCreate | CFile::typeText);

  str.Format("%s/n", "Fibonacci sequencee, less than 100 :");
  printf("%s", (LPCTSTR) str);
  fFibo.WriteString(str);

  lo = hi = 1;

  str.Format("%d/n", lo);
  printf("%s", (LPCTSTR) str);
  fFibo.WriteString(str);

  while (hi < 100)
  {
    str.Format("%d/n", hi);
    printf("%s", (LPCTSTR) str);
    fFibo.WriteString(str);
    hi = lo + hi;
    lo = hi - lo;
  }

  fFibo.Close();
  return 0;
}

  在MFC Console程序中一定要指定多线程的C Runtime函数库,所以必须在编译时使用/MT选项。如果不做这项设定,会出现链接错误。

  VC++一共有六个C Runtime函数库产品供你选择:
  ■Single-Threaded (static)          libc.lib    898,826
  ■Multithreaded (static)           libcmt.lib     951,142
  ■Multithreaded DLL             msvcrt.lib   5,510,000
  ■Debug Single-Threaded (static)     libcd.lib       2,374,542
  ■Debug Multithreaded (static)       libcmtd.lib  2,949,190
  ■Debug Multithreaded DLL          msvcrtd.lib  803,418

  VC++编译器提供下列选项,让我们决定使用哪一个C Runtime函数库:
  ■/ML  Single-Threaded (static)
  ■/MT  Multithreaded (static)
  ■/MD  Multithreaded DLL (dynamic import library)
  ■/MLd      Debug Single-Threaded (static)
  ■/MTd      Debug Multithreaded (static)
  ■/MDd      Debug Multithreaded DLL (dynamic import library)

【进程与线程(Process and Thread)】
  OS/2、WindowsNT以及Windows9x都支持多线程,这带给PC程序员一种令人兴奋的感觉。然而它带来的不全然是便利,如果不谨慎小心地处理线程的同步问题,程序的错误以及除错所花的时间可能使你发誓再也不碰“线程”这种东西。
  我们习惯以进程(process)表示一个执行中的程序,并且认为它是CPU高度单位,事实上线程才是高度单位。

【核心对象】
  首先让我解释什么叫做“核心对象”(Kernel object)。你可以说核心对象是系统的一种资源,系统对象一旦产生,任何应用程序都可以开启并使用该对象。系统给予核心对象一个计数值(usage count)作为管理之用。

【一个进程的诞生与死亡】
  执行一个程序,必然就产生一个进程。如在shell(Windows的资源管理器)中双击某一个可执行文件(假设其为App.exe),执行起来的App进程其实是shell调用CreateProcess激活的。让我们看看整个流程:
  1.shell调用CreateProcess激活App.exe。
  2.系统产生一个“进程核心对象”,计数值为1。
  3.系统为此进程建立一个4GB地址空间。
  4.加载器将必要的代码加载到上述地址空间中。
  5.系统为此行程建立一个线程,称为主线程(primary thread),线程才是CPU时间的分配对象。
  6.系统调用C Runtime函数库的Startup code。
  7.Startup code调用App程序的WinMain函数
  8.App程序开始运行。
  9.使用者关闭App主窗口,使WinMain中的消息循环结束掉,于是WinMain结束。
  10.回到Startup code。
  11.回到系统,系统调用ExitProcess结束进程。

  可以说,通过这种方式执行起来的所有Windows程序,都是shell的子进程。本来母进程与子进程之间可以有某些关系存在,但shell在调用CreateProcess时已经把母子之间的脐带关系剪断了,因此它们事实上是独立个体。

【一个线程的诞生与死亡】
  执行程序代码,是线程的工作。当一个进程建立起来后,主线程也产生。所以每一个Windwos程序一开始就有了一个线程。我们可以调用CreateThread产生额外的线程,系统会帮我们完成下列事情:
  1.配置“线程对象”,其handle将成为CreateThread的返回值。
  2.设定计数值为1。
  3.配置线程的context。
  4.保留线程的堆栈。
  5.将context中的堆栈指针寄存器(SS)和指令指针寄存器(IP)设定妥当。

  当CreateThread成功时,系统为我们把一个线程应该有的东西都准备好。线程的主体在哪里呢?就在所谓的线程处理函数(就相当于窗口的消息处理函数的重要性)。线程与线程之间,不必考虑控制权释放的问题,因为Win32操作系统的特点是强制性多任务的。

  线程的结束有两种情况,一种是寿终正寝,一种是求得善终。前者是线程函数正常结束退出,那么线程也就自然而然终结了。这时候系统会调用ExitThread便有些善后清理工作(其实线程中也可以自行调用此函数以结束自己)。一是进程结束(自然也就导致线程的结束),二是别的线程强制以TerminateThread将它终结掉。不过,TerminateThread太过毒辣,若非必要还是少用为妙(请参考API手册)。

【线程优先级(Priority)】
  线程的优先级范围从0(最低)到31(最高)。当你产生线程时,并不是直接以数值指定其优先级,而是采用两个步骤。第一个步骤是指定“优先级等级(Priority Class)”给进程,第二个步骤是指定“相对优先级”给该进程所拥有的线程(如下图所示)。如果你不指定,系统默认给的是NORMAL_PRIORITY_CLASS,除非父进程是IDLE_PRIORITY_CLASS,那么子进程也会是IDLE_PRIORITY_CLASS。

等级 代码 优先级值
idle IDLE_PRIORITY_CLASS 4
normal NORMAL_PRIORITY_CLASS 9(前台)或7(后台)
high HIGH_PRIORITY_CLASS 13
realtime REALTIME_PRIORITY_CLASS 24


  ■“idle”等级只有在CPU时间被浪费掉时(也就是空闲时间)才执行。该等级最适合于系统监视软件,或屏幕保护软件。
  ■“normal”是默认等级,
  ■“high”等级是为了满足立即反应的需要。
  ■“realtime”等级几乎不会被一般的应用程序使用。就连系统中控制鼠标、键盘、驱动器状态重新扫描、Ctrl+Alt+Del等的线程都比"realtime"的优先级还低。这种等级使用在“如果还在某个时间范围内被执行的话,数据就要遗失”的情况。这个等级一定得在正确评估之下使用,如果你把这样的等级指定给一般的(并不会常常被阻塞的)线程,多任务环境恐怕会瘫痪,因为?如此高的优先级,其它线程再也没有机会被执行了。

  上述四种等级,每一个等级又映射到某一范围的优先级值:IDLE_最低,NORMAL_次之,HIGH_又次之,REALTIME_最高。在每一个等级之中,你可以使用SetThreadPriority设定精确的优先级,并且可以稍高或稍低于该等级的正常值,你可以把SetThreadPriority想象成一种微调操作。以下是SetThreadPriority的参数选项:

SetThreadPriority的参数 微调幅度
THREAD_PRIORITY_LOWEST -2
THREAD_PRIORITY_BELOW_NORMAL -1
THREAD_PRIORITY_NORMAL 不变
THREAD_PRIORITY_ABOVE_NORMAL +1
THREAD_PEIOEITY_HIGHEST +2


  除了以上五种微调,另外还可以指定两种微调常数:

SetThreadPriority的参数 面对任何等级
的调整结果:
面对"realtime"等
级的调整结果:
THREAD_PRIORITY_IDLE 1 16
THREAD_PRIORITY_TIME_CRITICAL 15 31



  以上的这些情况可以总结为:

优先等级 idle lowest  below normal normal above normal highest time critical
idle 1 2 3 4 5 6 15
normal(后台) 1 5 6 7 8 9 15
normal(前台) 1 7 8 9 10 11 15
high 1 11 12 13 14 15 15
realtime 16 22 23 24 25 26 31
Win32线程优先级

你可能感兴趣的:(Win32)