使用RAD类型工具的人越来越多了,虽然我对于RAD类的工具向来不多作评议,但我还是常常使用的。所以我深深的知道这类工具虽然给我们带来了便利,使我们能不用将更多的精力放在界面上,但同时也将初学者紧紧的圈在了他所提供的控件和组件中。所以很多人并不能真正的了解windows的消息驱动原理以及windows的运作过程。本文中我们就一起来学习一下windows的运作过程,使我们对delphi这样一个优秀的编程工具有一个新的认识,并对windows下的程序编写有更深刻、透辟的了解和认识。
一、消息的定义
我们先从使用的角度看看windows的运作过程。我们都知道windows是一个多任务的平台,使用这个平台,我们可以一边工作,一边听歌曲,等等。所以对于这个操作平台可以想象到它除了一般操作系统所提供的对文件系统,内存系统等的管理之外,更重要的就是我们所熟知的消息驱动了,也就是说,要通过一定的方法和结构可以给每一个运行中的程序实例以及其中的每一个窗口传递其中所触发的事件。Windows中究竟是怎样做到的呢?让我们打开安装delphi的目录,在其中的source\rtl\Win\Windows.pas文件(或者在一个工程文件,找到uses,在其中找到Windows,然后按下Ctrl键,用鼠标点击单词),在其中的第18919行,我们可以看到这样一个结构的定义:
{ Message structure } PMsg = ^TMsg ; tagMSG = packed record hwnd : HWND ; message : UINT ; wParam : WPARAM ; lParam : LPARAM ; time : DWORD ; pt : TPoint ; end ;
{ $ EXTERNALSYM tagMSG } TMsg = tagMSG ; MSG = tagMSG ;
{ $ EXTERNALSYM MSG }
其中hwnd字段表示触发了消息的窗口的ID,由此可以保证消息正确的发送到每一个窗口去。 Message 表示消息的类型,其中更细致的解释要通过wParam和lParam一起来进行,不同的消息,wParam和lParam的值也就不相同。time用来记录消息触发的时间。Pt则表示触发的位置(毕竟window中有了鼠标)。我们也可以用同样的方法打开Messages文件。其中定义了windows中的绝大部分消息和结构。下面是我们截取的其中一部分。
const
{ $ EXTERNALSYM WM_NULL } WM_NULL = $0000 ;
{ $ EXTERNALSYM WM_CREATE } WM_CREATE = $0001 ;
{ $ EXTERNALSYM WM_DESTROY } WM_DESTROY = $0002 ;
{ $ EXTERNALSYM WM_MOVE } WM_MOVE = $0003 ;
{ $ EXTERNALSYM WM_SIZE } WM_SIZE = $0005 ; …… …… WM_APP = $8000 ;
{ NOTE : All Message Numbers below 0x0400 are RESERVED }
{ Private Window Messages Start Here }
{ $ EXTERNALSYM WM_USER } WM_USER = $0400 ;
…… ……
{ Dialog messages }
{ $ EXTERNALSYM DM_GETDEFID } DM_GETDEFID = ( WM_USER+0 ) ;
{ $ EXTERNALSYM DM_SETDEFID } DM_SETDEFID = ( WM_USER+1 ) ;
{ $ EXTERNALSYM DM_REPOSITION } DM_REPOSITION = ( WM_USER+2 ) ;
{ $ EXTERNALSYM PSM_PAGEINFO } PSM_PAGEINFO = ( WM_USER+100 ) ;
{ $ EXTERNALSYM PSM_SHEETINFO } PSM_SHEETINFO = ( WM_USER+101 ) ;
{ Button Notification Codes }
…… ……
可以看到,windows中每一个消息都对应着一个唯一的数值。当然我们也可以定义自己的消息,它的数值只要定义在WM_USER之后,保证和其中的定义不想重复即可。
二、消息的接收
消息到是有了,但怎样才能让程序以及窗口接收到呢?还是从使用的角度考虑,可以想象到,对于一个程序或窗口可以接收到来自鼠标、键盘等输入设备的消息,也可以接收到来自程序传递的消息,因为有了前面的tagMSG结构,我们就可以被动的在程序中接收消息了。这个功能的实现就是由下面的程序实现的(同样的这段程序来自于delphi的元代码):
function Tapplication . ProcessMessage ( var Msg : TMsg ) : Boolean; var Handled : Boolean ; begin
Result : = False ; if PeekMessage ( Msg , 0 , 0 , 0 , PM_REMOVE ) then begin Result : = True ; if Msg.Message <> WM_QUIT then begin Handled := False ; if Assigned ( FonMessage ) then FonMessage ( Msg , Handled ) ; if not IsHintMsg ( Msg ) and not Handled and not IsMDIMsg ( Msg ) and not IsKeyMsg ( Msg ) and not IsDlgMsg ( Msg ) then begin TranslateMessage ( Msg ) ; DispatchMessage ( Msg ) ; end ; end ; else FTerminate : = True ; end ; end ;
其中主要的是这几句:
if PeekMessage ( Msg , 0 , 0 , 0 , PM_REMOVE ) then begin TranslateMessage ( Msg ) ; DispatchMessage ( Msg ) ; end ;
但更常见的是:
while GetMessage ( Msg , 0 , 0 , 0 ) do begin TranslateMessage ( Msg ) ; DispatchMessage ( Msg ) ; End ;
PeekMessage和GetMessage都是从消息队列中得到发给程序的消息,只要有消息,就通过TranslateMessage ( Msg )和DispatchMessage ( Msg )两句将消息翻译为可处理的格式并分派给应用程序所注册的回调函数进行处理。
三、消息的处理
那么消息是怎样被处理的呢?回调函数又有什么作用呢?有了前面的知识,我们只要能对不同的消息进行正确的解释,就可以做到对消息的正确处理了。但前面我们提到了对于不同的消息传递的信息使不相同的,所以这是API编程中最麻烦的一部分了。
我们这里先给出一个常见而又必须的消息框架。
function WindowProc ( hwnd : HWnd ; Msg : UINT; Wparam : WPARAM ; Lparam : LPARAM ) : LRESULT ; stdcall ; export ; var dc : hdc ; rc : Trect ; ps : TpaintStruct ; begin
case Msg of WM_PAINT : Begin dc : = BeginPaint ( hwnd , ps ) ; …… …… EndPaint (hwnd, &ps) ; Exit ; end;
WM_COMMAND : …… …… WM_DESTROY :
Begin PostQuitMessage ( 0 ) ; Exit ; end ;
end;
Result : = DefWindowProc ( hWnd , Msg , wParam , lParam ) ;
end ;
在这个框架中,WindowProc就是我们前面提到的回调函数。它是windows程序设计中的重点。无论是从输入输出等硬件设备传来的消息,还是从软件传来的消息,都要保存到系统的消息队列中,这个消息队列有两种,一种是系统消息队列,主要是用来保存从输入输出等硬件设备传来的消息,另一种是每个程序窗口的窗口消息队列,主要保存每个窗口的发送来的消息。之后对消息的获取和分发工作,当然是由前面讲到的GetMessage 、 TranslateMessage和DispatchMessage三个函数来完成。至于消息的处理工作,则是由WindowProc函数来完成了。也就是说它是由系统在程序有消息到达时才调用的,所以我们称之为回调函数。
WindowProc是在注册窗口类时,注册的窗口消息处理函数,当然名字可以自己命名。其中的参数有hwnd : HWnd ; Msg : UINT; Wparam : WPARAM ; Lparam : LPARAM ,这也就是我们前面谈到的消息和窗体。
这里我们主要使用了三种消息:WM_PAINT , WM_COMMAND和WM_DESTROY ,但是我们可以随着程序而是用各种各样的消息。为了处理不同的消息,在程序中使用了分支结构,所以随着程序的规模越来越大,这个分支结构也会越来越庞大。
在这些消息中有两个点是最为重要的,其一是WM_DESTROY消息,它表示一个销毁窗口退出应用程序的消息。也是每个程序所必备的。对于这个消息的处理方式就是通过调用PostQuitMessage ( 0 )函数传递一个WM_QUIT消息,准备让由GetMessage 、 TranslateMessage和DispatchMessage三个函数组成的消息循环中的GetMessage取得。当消息循环中的收到WM_QUIT消息时,GetMessage会传回0,从而结束消息循环。进而释放各种资源,结束整个程序。另一个重要的地方是DefWindowProc函数。我们的程序无论多大都不可能将所有的消息都处理,所以我们必须有一个机制让不重要的不需要我们处理的消息,交给windows操作系统为我们处理,这个过程就是由DefWindowProc函数来实现的。
因此当我们按下窗口右上角的差号或者按下左上角系统菜单中的Close命令时,系统会送出WM_CLOSE消息。通常程序的窗口函数不拦截此消息,于是交由DefWindowProc函数来处理。DefWindowProc函数在受到WM_CLOSE消息后,调用DestroyWindow把窗口清除。DestroyWindow又会送出WM_DESTROY消息。程序又如前面讲到的一样来结束程序释放资源。
四、建立窗口类
知道了消息的传递和处理之后,我们来看看有关窗口的知识。
Windows带给我们的不仅是技术上的创新,更重要的是统一而又便捷的窗口。那么它是怎样创建的呢?这就要从窗口类tagWNDCLASSA说起了。
让我们先打开delphi目录下的source\rtl\Win\Windows.pas文件,18875行,可以看到这样一个结构:
tagWNDCLASSA = packed record style : UINT ; lpfnWndProc : TFNWndProc ; cbClsExtra : Integer ; cbWndExtra : Integer ; hInstance : HINST ; hIcon : HICON ; hCursor : HCURSOR ; hbrBackground : HBRUSH ; lpszMenuName : PansiChar ; lpszClassName : PansiChar ; end ;
其中存储了一个窗口的所有相关信息。style : UINT 表示窗口的风格;lpfnWndProc : TFNWndProc 表示窗口的消息处理函数;hInstance : HINST 表示窗口的一个应用实例;Icon : HICON 用来记录窗口的图标;hCursor : HCURSOR 记录窗口的光标;hbrBackground : HBRUSH 用来记录窗口的背景色;lpszMenuName : PansiChar 表示窗口中的菜单资源的名称; lpszClassName : PansiChar 记录窗口类的名称。
下面是一个例子:
WindowClass . style : = CS_VREDRAW + CS_HREDRAW + CS_DBLCLKS ; WindowClass . lpfnWndProc : = @DefWindowProc ; WindowClass . hCursor : = LoadCursor ( 0 , IDC_ARROW ) ; WindowClass . hbrBackground : = 0 ; WindowClass . hInstance : = Hinstance ; StrPCopy ( WinClassName , ClassName ) ;
五、注册窗口类
当我们按照程序的要求创建了这个窗口类之后,我们就可以在系统中注册它了。这就要用到function RegisterClass(const lpWndClass: TWndClass): ATOM; stdcall;这样一个函数了。他只有一个参数,就是我们先前说注册的窗口类。
六、创建窗口
有了前面几步,现在我们可以创建我们所注册的窗口,看看她的真面目了。
function CreateWindow ( lpClassName : Pchar ; lpWindowName : PChar ; dwStyle : DWORD ; X , Y , nWidth , nHeight : Integer ; hWndParent : HWND ; hMenu : HMENU ; hInstance : HINST ; lpParam : Pointer ) : HWND ;
这个函数可以帮助我们创建我们先前注册的窗口。其中的参数lpClassName : Pchar表示我们前面注册的窗口类的名称。lpWindowName : PChar 表示窗口的标题; dwStyle : DWORD 表示窗口的风格; X , Y , nWidth , nHeight : Integer ; 表示窗口的位置和宽度高度;
七、显示窗口
窗口创建了,但我们只有在调用function ShowWindow ( hWnd : HWND ; nCmdShow : Integer ) : BOOL ; stdcall ;函数之后才会显示出来。这个函数很简单,hWnd : HWND 表示窗口的句柄, nCmdShow : Integer则是窗口的显示方式。function UpdateWindow ( hWnd : HWND ) : BOOL ; stdcal l ;函数则会送出一个WM_PAINT消息,使窗体得到更新。
也许你会觉得很烦人,但这是所有windows程序的基础,即便是我们用delphi编程时,程序也都是这样运行的,只是delphi的创造者将一切都隐藏到了一个美丽外表之下。
下面我们用大家最常见的一个例子对前面的知识加以总结。在这个例子中,我们将在窗体中显示“ hello , world ! ” 。下面是程序及其运行效果:
program Project1;
{ $ APPTYPE CONSOLE } uses Windows , Messages ; { uses SysUtils ; }
var wClass : TWndClass; // 主窗口类 hInst , //应用程序句柄 Handle : HWnd ; // 主窗口 aMsg : TMsg ; //消息 RCT : TRect ; //区域 ps : TPaintStruct ; //显示 dc : hdc ; //设备上下文
//函数:WindowProc //作用:处理主窗口的消息 function WindowProc ( hWnd , Msg , wParam , lParam : Longint ) : Longint ; stdcall ; begin
WindowProc : = 0 ; case Msg of WM_PAINT : begin dc : =BeginPaint ( hWnd , ps ) ; GetClientRect ( hWnd , RCT ) ; DrawText ( dc , ' hello , world ! ' , -1 , RCT , Dt_SINGLELINE or DT_CENTER or DT_VCENTER ) ; EndPaint ( hWnd , ps ) ; Exit ; end ; WM_DESTROY : //结束应用程序 Begin PostQuitMessage ( 0 ) ; Exit ; end ; end ;
Result : = DefWindowProc ( hWnd , Msg , wParam , lParam ) ; //消息默认处理 end ;
//主窗口 begin
// hInst : = GetModuleHandle ( nil ) ; // 获得应用程序句柄 with wClass do //初始化窗口类 begin hInstance : = system.MainInstance ; Style : = CS_HREDRAW or CS_VREDRAW ; HIcon : = LoadIcon ( 0 , IDI_APPLICATION ) ; LpfnWndProc : = @WindowProc ; HbrBackground : = GetStockObject ( WHITE_BRUSH ) ; lpszClassName : = ' Sample Class ' ; hCursor : = LoadCursor ( 0 , IDC_ARROW ) ; end ;
RegisterClass ( wClass ) ; // 注册窗口类
//创建主窗口 Handle : = CreateWindow ( ' Sample Class ' , // 窗口类名 ' Windows API在Delphi中的应用 ' , //窗口标题 WS_OVERLAPPEDWINDOW or WS_VISIBLE , // 窗口风格 10 , //左边界坐标 10 , //上边界坐标 400 , // 宽度 300 , // 高度 0 , // 父窗口句柄 0 , //菜单句柄 system . MainInstance , // 应用程序实例 nil //创建窗口的附加参数 ) ;
if Handle <> 0 then begin
ShowWindow ( Handle , SW_SHOW ) ; UpdateWindow ( Handle ) ;
end ;
while ( GetMessage ( aMsg , Handle , 0 , 0 ) ) do //消息循环 begin TranslateMessage ( aMsg ) ; //翻译消息 DispatchMessage ( aMsg ) ; //发送消息 end ;
end .
效果如下图:

八、初步的封装, 面向过程的方式的编写
可以看到对于任何一个Windows程序的创建和运行都要经过上面的几个步骤,而且这些步骤又很有条理,所以我们又可以将不同的功能封装在几个命名规范且容易理解的函数之中。下面是修改后的程序代码:
program Project1;
{ $ APPTYPE CONSOLE }
uses Windows , Messages ; { uses SysUtils ; }
var wClass : TWndClass; // 主窗口类 hInst , //应用程序句柄 Handle : HWnd ; // 主窗口 aMsg : TMsg ; //消息 RCT : TRect ; //区域 ps : TPaintStruct ; //显示 dc : hdc ; //设备上下文
//函数:WindowProc //作用:处理主窗口的消息 function WindowProc ( hWnd , Msg , wParam , lParam : Longint ) : Longint ; stdcall ; begin
WindowProc : = 0 ;
case Msg of WM_PAINT : begin dc : =BeginPaint ( hWnd , ps ) ; GetClientRect ( hWnd , RCT ) ; DrawText ( dc , ' hello , world ! ' , -1 , RCT , Dt_SINGLELINE or DT_CENTER or DT_VCENTER ) ; EndPaint ( hWnd , ps ) ; Exit ; end ;
WM_DESTROY : //结束应用程序 Begin PostQuitMessage ( 0 ) ; Exit ; end ;
end ;
Result : = DefWindowProc ( hWnd , Msg , wParam , lParam ) ; //消息默认处理
end ;
//函数:WinRegister //作用:注册窗口类 function WinRegister : Boolean ; begin
with wClass do //初始化窗口类 begin hInstance : = system.MainInstance ; Style : = CS_HREDRAW or CS_VREDRAW ; HIcon : = LoadIcon ( 0 , IDI_APPLICATION ) ; LpfnWndProc : = @WindowProc ; HbrBackground : = GetStockObject ( WHITE_BRUSH ) ; lpszClassName : = ' Sample Class ' ; hCursor : = LoadCursor ( 0 , IDC_ARROW ) ; end ;
ReSult : = RegisterClass ( wClass ) <> 0 ; // 注册窗口类 end ;
//函数:WinCreate //作用:创建窗口 function WinCreate : HWnd ; begin
//创建主窗口 Handle : = CreateWindow ( ' Sample Class ' , // 窗口类名 ' Windows API在Delphi中的应用 ' , //窗口标题 WS_OVERLAPPEDWINDOW or WS_VISIBLE , // 窗口风格 10 , //左边界坐标 10 , //上边界坐标 400 , // 宽度 300 , // 高度 0 , // 父窗口句柄 0 , //菜单句柄 system . MainInstance , // 应用程序实例 nil //创建窗口的附加参数 ) ;
if Handle <> 0 then begin ShowWindow ( Handle , SW_SHOW ) ; UpdateWindow ( Handle ) ; end ;
Result : = Handle ; end ;
//主窗口 //程序的进入点 begin if not WinRegister then //调用函数:WinRegister注册窗口类 begin MessageBox ( 0 , ‘ Register failed ‘ , nil , MB_OK ) ; Exit ; end ;
Handle : = WinCreate ; //调用函数:WinCreate创建窗口
if longint ( Handle ) = 0 then begin MessageBox ( 0 , ‘ WinCreate failed ‘ , nil , MB_OK ) ; Exit ; end;
while ( GetMessage ( aMsg , Handle , 0 , 0 ) ) do //消息循环 begin TranslateMessage ( aMsg ) ; //翻译消息 DispatchMessage ( aMsg ) ; //发送消息 end ;
end .
九、进一步封装,面向对象的方法编写
更进一步的话,我们就可以考虑用面向对象的方法来编写。
可以看到如果我们要用上面的方法编写程序的话,工作量是十分繁重的,尤其在程序规模越来越大的情况下,出错的可能性也就会成指数性的增长。即使我们使用粘贴拷贝仍然要对程序进行修改,出错的可能人不会避免。而面向对象的编程方法可以有效的克服以上的不足,大大的提高了代码的利用率。所以下面我们就用面向对象的方法来看一看同样的程序我们应该怎样编写。
面向对象的核心就是要用类的方法来建立程序框架,然后用类的实例调用类的方法属性等手段来实现最终的效果。所以第一步我们首先又来建立一个窗口程序类,其中共有成员函数Create和 Destroy分别用来创建和销毁一个我们建立的窗口程序类的实例。共有成员函数WinCreate则调用私有成员函数WinRegister和 CreateMyWindow来注册和创建窗口。其中的属性变量ApplicationName 和 WindowProcedure 则是用来获得窗口程序类名和窗口消息处理函数的。下面是修改后的源代码:
//用面向对象的方法来编写 program TMyWindowClass;
uses Windows, Messages, SysUtils;
//创建窗口类 type TMyWindow = class ( TObject )
private
{ 定义私有变量 }
WindowClass : WndClass; hWindow : HWnd ; AMessage : TMsg ; FAppName : String ; FWndProc : TFNWndProc ; function WinRegister : Boolean ; virtual ;
procedure CreateMyWindow ;
public
{定义公有变量 }
constructor Create ; destructor Destroy ; override ; procedure WinCreate ; virtual ; procedure Run ;
{定义属性 }
property ApplicationName : String read FAppName write FAppName ; property WindowProcedure : TFNWndProc read FWndProc write FWndProc ;
end ;
const
AppName = ' MyClassshili ' ; var myWindow : tMyWindow ;
{ TMyWindow类中公有函数的实现 }
constructor TMyWindow . Create ; begin
end ;
destructor TMyWindow . Destroy ; begin inherited ; end ;
procedure TMyWindow . CreateMyWindow ; //创建窗口 begin hWindow : = CreateWindow ( AppName , ' hello , world ' , ws_OverlappedWindow , cw_UseDefault , cw_UseDefault , cw_UseDefault , cw_UseDefault , 0 , 0 , system . MainInstance , nil ) ;
if hWindow <> 0 then begin ShowWindow ( hWindow , CmdShow ) ; ShowWindow ( hWindow , SW_SHOW ) ; UpdateWindow ( hWindow ) ; end ;
end ;
procedure TMyWindow . WinCreate ; begin if WinRegister then begin CreateMyWindow ; end ;
end ;
function TMyWindow . WinRegister : Boolean ; //注册窗口类 begin WindowClass.Style : = cs_hRedraw or cs_vRedraw ; WindowClass . lpfnWndProc : = FWndProc ; WindowClass . cbClsExtra : = 0 ; WindowClass . cbWndExtra : = 0 ; WindowClass . hInstance : = system.MainInstance ; WindowClass . hIcon : = LoadIcon ( 0 , idi_Application ) ; WindowClass . hCursor : = LoadCursor ( 0 , idc_Arrow ) ; WindowClass . hbrBackground : = GetStockObject ( WHITE_BRUSH ) ; WindowClass . lpszMenuName : = nil ; WindowClass . lpszClassName : = PChar ( FAppName ) ; Result : = RegisterClass ( WindowClass ) <> 0 ; end ;
function WindowProc ( Window : HWnd ; Amessage : UINT ; WParam : WPARAM ; LParam : LPARAM ) : LRESULT ; stdcall ; export ;
//消息处理函数 var dc : hdc ; ps : TPaintStruct ; r : TRect ;
begin WindowProc : = 0 ; case AMessage of WM_PAINT : begin dc : = BeginPaint ( Window , ps ) ; GetClientRect ( Window , r ) ; DrawText ( dc , ' hello , world ! ! ' , -1 , r , DT_SINGLELINE or DT_CENTER or DT_VCENTER ) ; EndPaint ( Window , ps ) ; Exit ; end ;
wm_Destroy : begin PostQuitMessage ( 0 ) ; Exit ; end ; end ;
WindowProc : = DefWindowProc ( Window , AMessage , WParam , LParam ) ; end ;
procedure TMyWindow . MyRun ; begin while GetMessage ( AMessage , 0 , 0 , 0 ) do begin TranslateMessage ( AMessage ) ; DispatchMessage ( AMessage ) ; end ;
Halt ( AMessage . wParam ) ; end ;
//主程序,程序运行的进入口 begin
myWindow : = TMyWindow . Create ; myWindow . ApplicationName : = AppName ; myWindow . WindowProcedure : = TFNWndProc ( @WindowProc ) ; myWindow . WinCreate ;
try myWindow . MyRun ;
finally FreeAndNil ( myWindow ) ;
end ; end .
好了,就这些吧。相信有了这些基础应该在看delphi源代码时容易了许多,毕竟很多代码在此都很熟悉了。 |