1、消息驱动模型
1. MyMessage MyMessage=packed record MessageID:longint; wParam :longint; lParam :longint; time :longint; pt:TPoint; End; //其中wParam各lParam可使用来封装事件任何额外的信息,例如鼠标的位置或是用户输入的字符等,而pt字段则可以储存鼠标的全域坐标等。 |
2、窗口运作模型
还要增加一个代表窗口的识别码。 MyMessage=packed record hwnd:HWND; MessageID:longint; wParam :longint; lParam :longint; time :longint; pt:TPoint; End; |
3、消息队列(Message Queue)
消息处理需要时间,为执行环境建立一个消息队列。 |
4、窗口记录
MyWindowClassInfo=packed record iWidth:integer iHeight:integer; lpfnWndProc:Pointer; hIcon:HICON; hCursor:HCURSOR; hbrBackground:HBRUSH; lpszMenuName:PAnsiChar; lpszClassName:PAnsiChar; hIconSm:HICON; end; |
5、要让窗口处理消息最简单的方法是让执行环境调用窗口提供的函数并且传递消息给此函数来处理。
lpfnWndProc字段就是一个Pointer类型的字段,代表应用程序可以把任何的函数地址指定给此字段以代表可以处理此窗口消息的函数。由于这个函数会由执行环境调用,因此这种函数也被称为回调函数(Callback Routine)。
必须定义回调函数的格式: function WindowProc(Window:HWND;AMessage:UNIT;WParam:WPARAM;LParam:LPARAM):LRESULT;stdcall;export;//主要作用是接受所有消息(包括其他窗体的) 这里的window,WParam,LParam就是前面消息数据结构MyMessage中的hwnd、wParam和lParam字段,代表发生事件的窗体ID以及窗口的辅助信息等。而AMessage则是代表事件的窗口消息值。 因此当应用程序遵照回调函数的原型提供了适当的处理窗口消息函数并指定给MyWindowClassInfo中的lpfnWndProc字段,那么应用程序就可以等待这个回调函数在窗口事件发生后自动由执行环境调用了。 |
6、消息处理
Case AMessage of WM_PAINT//重画窗口的消息: … WM_DESTROY//窗口消灭的消息: …
由于应用程序要判断回调函数中发生的消息ID值,因此执行环境当然必须为触发事件定义代表的消息ID以及每一个消息的意义。 unit Messages;
{$A-} {$WEAKPACKAGEUNIT}
interface
uses Windows;
{ Window Messages }
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; {$EXTERNALSYM WM_ACTIVATE} WM_ACTIVATE = $0006; {$EXTERNALSYM WM_SETFOCUS} WM_SETFOCUS = $0007; {$EXTERNALSYM WM_KILLFOCUS} WM_KILLFOCUS = $0008; {$EXTERNALSYM WM_ENABLE} WM_ENABLE = $000A; {$EXTERNALSYM WM_SETREDRAW} WM_SETREDRAW = $000B; {$EXTERNALSYM WM_SETTEXT} WM_SETTEXT = $000C; {$EXTERNALSYM WM_GETTEXT} WM_GETTEXT = $000D;
|
7、Microsoft Windows中开发原始Windows应用程序时要进行的步骤以及处理的数据结构和调用的函数:
Windows应用程序可以调用GetMessage从消息队列中取得要处理的消息,再调用TranslateMessage适当地将消息转换成可处理的格式,最后调用DispatchMessage即可由Windows自动调用消息处理回调函数。
|
8、Windows定义的消息数据结构
1.MSG是Microsoft Windows定义的最基本窗口消息数据结构,MSG定义如下: Typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG;
2. MW(Microsoft Windows)中也定义了WNDCLASSA数据结构来代表应用程序注册的窗口类。应用程序通过WNDCLASSA数据结构中填入适当的数据数值,例如窗口的格式、回调函数地址、窗口类名称、窗口使用的图像和窗口使用的菜单类名称等而能够让Windows创建应用程序想要的窗口。WNDCLASSA数据结构的定义如下: 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; 3. 为了能让MW能够顺利地调用到应用程序的回调函数,Windows当然必须规定回调函数的原型。原型: Typedef LRESULT (CALLBACK* WNDPROC)(HWND,UINT,WPARAM,LPARAM); 4. 当应用程序使用WNDCLASSA数据结构定义了窗口的格式之后,必须先向Windows注册此窗口类,之后应用程序才能够根据这个窗口类来创建窗口。要注册窗口类,应用程序只需要调用RegisterClass即可: WINAPI RegisterClass(IN CONST WNDCLASSA *lpWndClass); 5. 创建Windows窗口 在调用RegisterClass注册了窗口类之后,应用程序最后就可以调用CreateWindow来实际地创建窗口了。原型: function CreateWindow(lpClassName:PChar;lpWindowName:PChar;dwStyle:DWORD;X,Y,nWidth,nHeight:Integer;hWndParent:HWND;hMenu:HMENU;hInstance:HINST;lpParam:Pointer):HWND; 在应用程序调用了CreateWindow之后,窗口就会被创建。之后的剩下的工作就是让应用程序中的窗口消息处理函数根据发生的事件来处理窗口消息了。 |
9、数据结构和数据类型的转换
Pascal语法的: tagMSG = packed record hwnd: HWND; message: UINT; wParam: WPARAM; lParam: LPARAM; time: DWORD; pt: TPoint; end; //windows unit tagWNDCLASSA = packed record style: UINT; lpfnWndProc: TFNWndProc; cbClsExtra: Integer; cbWndExtra: Integer; hInstance: HINST; hIcon: HICON; hCursor: HCURSOR; hbrBackground: HBRUSH; lpszMenuName: PAnsiChar; lpszClassName: PAnsiChar; end; //windows unit |
10、Windows应用程序步骤:
·定义窗口类的内容,以决定窗口的创建格式以及回调函数 ·注册窗口类 ·创建窗口 ·进入窗口消息处理循环以便让回调函数处理窗口消息,直到应用程序结束为止 实例: program ObjectPascalWinHello;
uses Windows, Messages, SysUtils;
const AppName = 'ObjectPacalHello';
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, '使用Object Pascal撰写的Native Window程序', -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;//回调函数
{Register the window class}
function WinRegister: boolean; var WindowClass: WndClass; begin WindowClass.Style := cs_hRedRaw or cs_vRedraw; WindowClass.lpfnWndProc := TFNWndProc(@WindowProc); 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 := AppName;//终于找到了关联 result := RegisterClass(WindowClass) <> 0; //前面定义类的属性注册类 end; {create the window class}
function WinCreate: HWND; var hWindow: HWnd; begin hWindow := CreateWindow(AppName, 'Hello World Object Pascal program', 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;
result := hwindow; end;
var AMessage: TMsg; hWindow: HWnd; begin if not WinRegister then begin Messagebox(0, 'Register failed', nil, mb_ok); exit; end; hWindow := WinCreate; if longint(hwindow) = 0 then begin Messagebox(0, 'WinCreate failed', nil, mb_ok); exit; end; while GetMessage(aMessage, 0, 0, 0) do begin TranslateMessage(AMessage); DispatchMessage(AMessage); end; halt(aMessage.wParam);
end.
李维: 首先上面的应用程序通过调用WinRegister来注册应用程序之中的窗口类信息,当然在注册时也指定了处理窗口消息的函数指针是应用程序中的WindowProc函数[明白]。接着调用WinCreate以真正根据注册的窗口类来创建窗口[?关联在何处],最后进入等待窗口消息的循环,以等待处理任何发生的窗口消息并且把窗口消息分派到窗口消息函数中。 function CreateWindow(lpClassName: PChar; lpWindowName: PChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; var FPUCW: Word; begin FPUCW := Get8087CW; Result := _CreateWindowEx(0, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); Set8087CW(FPUCW); end; 疑问: 1.WinCreate和前面的WinRegister在哪关联? 经过仔细分析程序发现: function WinRegister: boolean; begin … WindowClass.lpszClassName := AppName; … end; function WinCreate: HWND; begin … hWindow := CreateWindow(AppName, 'Hello World Object Pascal program', ws_OverLappedWindow, cw_UseDefault, cw_UseDefault, cw_UseDefault, cw_UseDefault, 0, 0, system.MainInstance, nil); … end; 类名关联的。 2. WindowProc := DefWindowProc(Window, AMessage, WParam, LParam);用途? Windows定义了千百条消息,不是每一条消息应用程序都要处理,因此对于应用程序不须处理的窗口消息,应用程序可以调用DefWindowProc这个Windows API来让Windows操作系统本身处理这个触发的窗口消息。
分析四步骤: var AMessage: TMsg; hWindow: HWnd; begin if not WinRegister then begin Messagebox(0, 'Register failed', nil, mb_ok); exit; end; //完成注册、回调函数 hWindow := WinCreate; if longint(hwindow) = 0 then begin Messagebox(0, 'WinCreate failed', nil, mb_ok); exit; end;//窗体的创建 while GetMessage(aMessage, 0, 0, 0) do begin TranslateMessage(AMessage); DispatchMessage(AMessage); end;//消息的处理 halt(aMessage.wParam);
end.
消息处理: while GetMessage(aMessage, 0, 0, 0) do begin TranslateMessage(AMessage); DispatchMessage(AMessage); end;//消息的处理 while循环中调用GetMessage API以便检查消息队列是否有此应用程序需要处理的任何窗口消息。如果有的话[哪里去判断有无?只有个公用变量AMessage],就会进入TranslateMessage API把特定的窗口消息先转换成可供处理的格式,例如把键盘的虚拟键值转换为字符消息,最后调用DispatchMessage API来分派窗口消息给应用程序注册的回调函数。 [哪里去判断有无?只有个公用变量AMessage]? 难不成是只要注册了一有消息就去调用回调函数。[以后学习注意了,此问题未解决。] |
11、窗口回调程序设计的缺点
回调函数的: case AMessage of WM_PAINT: begin dc := BeginPaint(window, ps); GetClientRect(Window, r); DrawText(dc, '使用Object Pascal撰写的Native Window程序', -1, r, DT_SINGLELINE or DT_CENTER or DT_VCENTER); endPaint(Window, ps); exit; end; WM_Destroy: begin end; … 缺点: 1、程序员必须非常了解窗口消息 2、每个回调函数都非常类似,重复代码 3、Case逐一判断效率低下
改进: 以Hash Table的方式来储存窗口消息和消息处理函数,那么不但每一个消息找到它的处理时间趋于线性时间,而且每一个消息处理函数现在都可以独立成一个小程序,如此一来消息处理函数都比原来使用的函数小得多,因此编译器将会有机会产生更高效率的最佳化程序代码。
再改进: 如果我们把经常会调用到的消息处理函数储存在高速缓存(Cache)中,那么还可再提高执行效率,因为对于这些经常发生的事件可以直接在高速缓存中分派和处理,无须重复执行上面的消息分配流程。接近许多Windows Framework的做法了。 |
12、改善Windows应用程序的撰写
利用面向对象的方法 myWindow:=TMyWindow.create; myWindow.ApplicationName:=AppName; myWindow.WindowProcedure:=TFNWndProc(处理窗口消息的程序地址); myWindow.WinCreate; myWindow.MyRun;
思路清晰了,实现步骤: 1. 使用一个Object Pascal类来封闭一般的Windows程序设计繁琐的事项,例如窗口注册类结构等。并且提供数个方法和特性让程序员能够用它来创建窗口对象。如此一来程序员只须创建此封装类对象,再调用其中的演绎法中是设定特性值就可以轻易创建窗口了。 代码: program MyWindows;
uses Windows, Messages, SysUtils;
type TMyWindow = class(TObject) private WindowClass: WndClass; hWindow: HWND; AMessage: TMSG; FAppName: string; FWndProc: TFNWndProc;
function WinRegister: boolean; procedure CreateMyWindow; public constructor Create; destructor Destroy; override; procedure WinCreate; procedure MyRun; property ApplicationName: string read FAppName write FAppName; property WindowProcedure: TFNWndProc read FWndProc write FWndProc; end; var myWindow: TMyWindow; const AppName = 'MyClassDemo';
{ TMyWindow }
constructor TMyWindow.Create; begin
end;
procedure TMyWindow.CreateMyWindow; begin hWindow := CreateWindow(AppName, '面向对象方式设计窗口应用程序的范例', 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; destructor TMyWindow.Destroy; begin
inherited; end;
procedure TMyWindow.MyRun; begin while getMessage(AMessage, 0, 0, 0) do begin TranslateMessage(AMessage); DispatchMessage(AMessage); end; halt(AMessage.wParam); 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, '使用TMyWindow类别封装的Window程序。这导致了使用面向对象方式设计窗口应用程序', -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;
begin //主程序 myWindow := TMyWindow.Create; myWindow.ApplicationName := AppName; myWindow.WindowProcedure := TFNWndProc(@WindowProc); myWindow.WinCreate; try myWindow.MyRun; finally FreeAndNil(myWindow); end; end.
2. 如果再把上面的WinRegister和WinCreate这两个函数声明成虚拟方法: function WinRegister:Boolean;virtual; procedure WinCreate;virtual; 派生类通过重载这两个虚拟方法来改变如何注册和创建窗口的行为。VCL Framework也是使用虚拟方法机制来提供程序员重载注册窗口和创建窗口执行行为的。 3.仍然缺少封闭处理窗口消息的机制,因此程序员还要提供传统的程序函数来处理所有的窗口消息。 |