麻豆小视频在线观看_中文黄色一级片_久久久成人精品_成片免费观看视频大全_午夜精品久久久久久久99热浪潮_成人一区二区三区四区

首頁 > 學院 > 開發設計 > 正文

VCL消息處理機制的內幕

2019-11-18 18:39:04
字體:
來源:轉載
供稿:網友
 

由VCL中的代碼理解VCL中的消息處理機制

Delphi,一個非常優秀的開發工具,擁有強大的可視化開發環境、面向組件的快速開發模式、優秀的VCL類庫、快速的代碼編譯器、強大的數據庫和WEB開發能力、還有眾多的第三方控件支持...(此處省略x千字,既然大家都知道了,不浪費口水了 ^_^)

說到VCL的優秀就不能不提到其對Windows消息及API的較全面和完美的封裝,正因為如此開發者在大多數情況下甚至不需理會Windows消息處理的細節,而只需要寫幾行事件驅動代碼即可!

但如果做為開發人員你還是想對此做些了解的話,那么就繼續,通過VCL代碼本身來體會VCL中的消息處理機制。

(以下代碼取自Delphi 6)

說到VCL中的消息處理就不能不提到Tapplication,Windows會為每一個當前運行的程序建立一個消息隊列,用來完成用戶與程序的交互,正是通過Application完成了對Windows消息的集中處理!

首先通過Application.Run進入消息循環進行消息的處理,其中調用了HandleMessage。

PRocedure TApplication.HandleMessage;
var
  Msg: TMsg;
begin
  if not ProcessMessage(Msg) then Idle(Msg);//這里先調用ProcessMessage處理,返回值為False調用Idle,就是在空閑時,即消息隊列中無消息等待處理時調用Idle。
end;

function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
  Handled: Boolean;
begin
  Result := False;
  if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then//查詢消息隊列中有無消息等待處理,參數PM_REMOVE使消息在處理完后會被刪除。
  begin
    Result := True;
    if Msg.Message <> WM_QUIT then//如果是WM_QUIT,終止進程,否則執行下面的代碼
    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);//將記錄Msg傳遞給Windows進行轉換
        DispatchMessage(Msg);//將記錄Msg回傳給Windows
      end;
    end
    else
      FTerminate := True;
  end;
end;

然后程序中的各個VCL對象又是如何接收到Windows消息的呢?這還要從窗體的創建開始!

首先找到TWinControl.CreateWnd中的
Windows.RegisterClass(WindowClass)//調用RegisterClass注冊一個窗體類

向上看
WindowClass.lpfnWndProc := @InitWndProc;//這里指定了窗口的消息處理函數的指針為@InitWndProc!

再找到function InitWndProc(HWindow: HWnd; Message, WParam, LParam: Longint): Longint;

發現了
CreationControl.FHandle := HWindow;
SetWindowLong(HWindow, GWL_WNDPROC,Longint(CreationControl.FObjectInstance));
沒有?

原來InitWndProc初次被調用時候,又使用API函數SetWindowLong指定處理消息的窗口過程為FObjectInstance。

回到TWinControl.Create
FObjectInstance := Classes.MakeObjectInstance(MainWndProc);
找到關鍵所在了,也許有些朋友對MakeObjectInstance這個函數很熟了,它的作用就是將一個成員過程轉換為標準過程。

繞了個圈子?為什么呢?很簡單,因為窗體成員過程包括一隱含參數傳遞Self指針,所以需要轉化為標準過程。

const
  InstanceCount = 313;//這個不難理解吧?314*13+10=4092,再大的話,記錄TInstanceBlock的大小就超過了下面定義的PageSize

type
  PObjectInstance = ^TObjectInstance;
  TObjectInstance = packed record
    Code: Byte;
    Offset: Integer;
    case Integer of
      0: (Next: PObjectInstance);
      1: (Method: TWndMethod);
  end;

type
  PInstanceBlock = ^TInstanceBlock;
  TInstanceBlock = packed record
    Next: PInstanceBlock;
    Code: array[1..2] of Byte;
    WndProcPtr: Pointer;
    Instances: array[0..InstanceCount] of TObjectInstance;
  end;

var
  InstBlockList: PInstanceBlock;
  InstFreeList: PObjectInstance;

function StdWndProc(Window: HWND; Message, WParam: Longint;
  LParam: Longint): Longint; stdcall; assembler;
asm
        XOR     EAX,EAX
        PUSH    EAX
        PUSH    LParam
        PUSH    WParam
        PUSH    Message
        MOV     EDX,ESP  ;將堆棧中構造的記錄TMessage指針傳遞給EDX
        MOV     EAX,[ECX].Longint[4]  ;傳遞Self指針給EAX,類中的Self指針也就是指向VMT入口地址
        CALL    [ECX].Pointer  ;調用MainWndProc方法
        ADD     ESP,12
        POP     EAX
end;

function CalcJmpOffset(Src, Dest: Pointer): Longint;
begin
  Result := Longint(Dest) - (Longint(Src) + 5);
end;

function MakeObjectInstance(Method: TWndMethod): Pointer;
const
  BlockCode: array[1..2] of Byte = (
    $59,       { POP ECX }
    $E9);      { JMP StdWndProc }
  PageSize = 4096;
var
  Block: PInstanceBlock;
  Instance: PObjectInstance;
begin
  if InstFreeList = nil then
  begin
    Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);//分配虛擬內存,并指定這塊內存為可讀寫并可執行
    Block^.Next := InstBlockList;
    Move(BlockCode, Block^.Code, SizeOf(BlockCode));
    Block^.WndProcPtr := Pointer(CalcJmpOffset(@Block^.Code[2], @StdWndProc));
    Instance := @Block^.Instances;
    repeat
      Instance^.Code := $E8;  { CALL NEAR PTR Offset }
      Instance^.Offset := CalcJmpOffset(Instance, @Block^.Code);
      Instance^.Next := InstFreeList;
      InstFreeList := Instance;
      Inc(Longint(Instance), SizeOf(TObjectInstance));
    until Longint(Instance) - Longint(Block) >= SizeOf(TInstanceBlock);
    InstBlockList := Block;
  end;
  Result := InstFreeList;
  Instance := InstFreeList;
  InstFreeList := Instance^.Next;
  Instance^.Method := Method;
end;

(注:上面出現的那些16進制代碼其實就是些16進制的機器代碼 $59=Pop ECX $E8=Call $E9=Jmp)

以上代碼看起來有點亂,但綜合起來看也很好理解!MakeObjectInstance實際上就是構建了一個Block鏈表

其結構看看記錄TInstanceBlock的結構可知其結構如下:

Next//下一頁指針
Code//Pop ECX和Jmp
WndProcPtr//和StdWndProc間的地址偏移
Instances//接下來是314個Instance鏈表

Instance鏈表通過記錄TObjectInstance也很好理解其內容

Code//Call
Offset//地址偏移
Method//指向對象方法的指針(結合TMethod很好理解TWndMethod這類對象方法指針指向數據的結構)

好現在來把這個流程回顧一遍,Windows回調的是什么呢?其實是轉到并執行一段動態生成的代碼:先是執行Call offset ,根據偏移量轉去執行Pop ECX,當然由于在Call這之前會將下一條指令入棧,所以這里彈出的就是指向對象方法的指針。接下來就是執行jmp [StdWndProc],其中將堆棧中構造的記錄TMessage指針賦給了EDX,而根據上面的解釋結合TMethod去理解,很容易理解
MOV     EAX,[ECX].Longint[4]  ;傳遞Self指針給EAX,類中的Self指針也就是指向VMT入口地址
CALL    [ECX].Pointer  ;調用MainWndProc方法

現在終于豁然開朗了,Windows消息就是這樣被傳遞到了TWinControl.MainWndProc,相比MFC中的回調全局函數AfxWndProc來根據窗體句柄檢索對應的對象指針的方法效率要高的多!VCL比MFC優秀的又一佐證! ^_^

現在終于找到了VCL接收消息的方法MainWndProc

procedure TWinControl.MainWndProc(var Message: TMessage);
begin
  try
    try
      WindowProc(Message);//由于TControl創建實例時已經將FWindowProc指向WndProc,所以這里實際也就是調用WndProc
    finally
      FreeDeviceContexts;
      FreeMemoryContexts;//調用FreeDeviceContexts和FreeMemoryContexts是為了保證VCL線程安全
    end;
  except
    Application.HandleException(Self);
  end;
end;

這里也不能忽略了TWinControl.WndProc

procedure TControl.WndProc(var Message: TMessage);
var
  Form: TCustomForm;
  KeyState: TKeyboardState; 
  WheelMsg: TCMMouseWheel;
begin
  ...
  //省略以上的消息相關處理代碼,研究某些特定消息時可自行查看
  ...
  Dispatch(Message);//調用Dispatch處理
end;

接下來,先不急著查看Dispatch中的相應代碼。想想看,忘了什么?

上面只是繼承于TWinControl的有句柄的控件,那繼承于TGraphicControl的沒有句柄的控件是如何獲得并處理消息的?下面以鼠標消息為例:

TWinControl.WndProc中有下面的代碼:

  case Message.Msg of
  ...
    WM_MOUSEFIRST..WM_MOUSELAST://注1:下面再解釋這段
      if IsControlMouseMsg(TWMMouse(Message)) then
      begin
        { Check HandleAllocated because IsControlMouseMsg might have freed the
          window if user code executed something like Parent := nil. }
        if (Message.Result = 0) and HandleAllocated then
          DefWindowProc(Handle, Message.Msg, Message.wParam, Message.lParam);
        Exit;
      end;
  ...
  end;
  inherited WndProc(Message);//執行祖先類的WndProc方法

function TWinControl.IsControlMouseMsg(var Message: TWMMouse): Boolean;
var
  Control: TControl;
  P: TPoint;
begin
  if GetCapture = Handle then
  begin
    Control := nil;
    if (CaptureControl <> nil) and (CaptureControl.Parent = Self) then
      Control := CaptureControl;
  end else
    Control := ControlAtPos(SmallPointToPoint(Message.Pos), False);//這里通過ControlAtPos獲得了鼠標所在控件
  Result := False;
  if Control <> nil then
  begin
    P.X := Message.XPos - Control.Left;
    P.Y := Message.YPos - Control.Top;
    Message.Result := Control.Perform(Message.Msg, Message.Keys, Longint(PointToSmallPoint(P)));//調用Perform方法發送消息給對應的實例
    Result := True;
  end;
end;

property WindowProc: TWndMethod read FWindowProc write FWindowProc;

function TControl.Perform(Msg: Cardinal; WParam, LParam: Longint): Longint;
var
  Message: TMessage;
begin
  Message.Msg := Msg;
  Message.WParam := WParam;
  Message.LParam := LParam;
  Message.Result := 0;
  if Self <> nil then WindowProc(Message);//由于TControl創建實例時已經將FWindowProc指向WndProc,所以這里實際也就是調用WndProc
  Result := Message.Result;
end;

VCL中就是這樣將消息分發給了那些繼承于TGraphicControl的沒有句柄的圖形控件。

上面說的都是Windows消息(Windows Messages),似乎還應該說說兩條經常用到的VCL中自定義消息:CM_MOUSEENTER,CM_MOUSELEAVE(CM = Short of Control Message)

它們是如何被處理的呢?還是看上面的(if not ProcessMessage(Msg) then Idle(Msg);),這兩條不是Windows消息,所以會觸發Idle

procedure TApplication.Idle(const Msg: TMsg);
var
  Control: TControl;
  Done: Boolean;
begin
  Control := DoMouseIdle;//調用DoMouseIdle方法
  ...
end;

function TApplication.DoMouseIdle: TControl;
var
  CaptureControl: TControl;
  P: TPoint;
begin
  GetCursorPos(P);
  Result := FindDragTarget(P, True);//獲取當前鼠標所停留在的控件
  if (Result <> nil) and (csDesigning in Result.ComponentState) then
    Result := nil;
  CaptureControl := GetCaptureControl;
  if FMouseControl <> Result then//判斷以前記錄的鼠標指針所指向的控件和現在所指向的控件是否相同
  begin
    if ((FMouseControl <> nil) and (CaptureControl = nil)) or
      ((CaptureControl <> nil) and (FMouseControl = CaptureControl)) then
      FMouseControl.Perform(CM_MOUSELEAVE, 0, 0);//發送消息CM_MOUSELEAVE給以前記錄的鼠標指針所指向的控件
    FMouseControl := Result;//記錄當前鼠標指針所指向的控件
    if ((FMouseControl <> nil) and (CaptureControl = nil)) or
      ((CaptureControl <> nil) and (FMouseControl = CaptureControl)) then
      FMouseControl.Perform(CM_MOUSEENTER, 0, 0);//發送消息CM_MOUSEENTER給鼠標指針現在所在的控件
  end;
end;

function FindDragTarget(const Pos: TPoint; AllowDisabled: Boolean): TControl;
var
  Window: TWinControl;
  Control: TControl;
begin
  Result := nil;
  Window := FindVCLWindow(Pos);//這里返回的是TWinControl,是一個有句柄的控件
  if Window <> nil then
  begin
    Result := Window;
    Control := Window.ControlAtPos(Window.ScreenToClient(Pos), AllowDisabled);//鼠標所指向處可能還存在一繼承于TGraphicControl的圖形控件,而上面返回的只是其容器控件
    if Control <> nil then Result := Control;//如果存在就返回用ControlAtPos所得到的控件
  end;
end;

于是又轉到了上面的TControl.Perform

現在所有的問題又都集中到了Dispatch的身上,消息是如何觸發事件的處理方法的呢?

首先看條消息處理方法的申明:
procedure CMMouseEnter(var Message: TMessage); message CM_MOUSEENTER;

這實際可以認為是申明了一個動態方法,調用Dispatch實際上就是通過消息號在DMT(動態方法表)中找到相應的動態方法指針,然后執行

//上面已經提到了,寄存器EAX中是類的Self指針,即VMT入口地址,寄存器EDX中是指向記錄Message的指針
procedure TObject.Dispatch(var Message);
asm
    PUSH    ESI
    MOV     SI,[EDX]  ;消息號,也就是記錄TMessage中Msg的值,對應CM_MOUSEENTER就是$B013(45075)
    OR      SI,SI
    JE      @@default
    CMP     SI,0C000H
    JAE     @@default
    PUSH    EAX
    MOV     EAX,[EAX]  ;VMT入口地址
    CALL    GetDynaMethod  ;調用GetDynaMethod查找
    POP     EAX
    JE      @@default  ;在GetDynaMethod中如果找到會將標志位寄存器的值置為0,如果是1,表示未找到,執行跳轉
    MOV     ECX,ESI  ;傳遞指針給ECX
    POP     ESI
    JMP     ECX  ;跳轉到ECX所指向的位置,也就完成了通過消息號調用CMMouseEnter的過程

@@default:
    POP     ESI
    MOV     ECX,[EAX]
    JMP     dWord ptr [ECX].vmtDefaultHandler  ;如果此控件和它的祖先類中都沒有對應此消息號的處理方法,調用Defaulthandler
end;

procedure       GetDynaMethod;
{       function        GetDynaMethod(vmt: TClass; selector: Smallint) : Pointer;       }
asm
        { ->    EAX     vmt of class            }
        {       SI      dynamic method index    }
        { <-    ESI pointer to routine  }
        {       ZF = 0 if found         }
        {       trashes: EAX, ECX               }

        PUSH    EDI
        XCHG    EAX,ESI  ;交換EAX和ESI的值,這之后ESI中為VMT入口地址,EAX為消息號,即對應動態方法的代號
        JMP     @@haveVMT
@@outerLoop:
        MOV     ESI,[ESI]
@@haveVMT:
        MOV     EDI,[ESI].vmtDynamicTable  ;嘗試著將DMT的入口地址傳遞給EDI

        TEST    EDI,EDI  ;通過EDI是否為0來判斷是否存在DMT
        JE      @@parent  ;不存在跳轉到父類繼續
        MOVZX   ECX,word ptr [EDI]  ;取[EDI],即DMT的頭兩個字節的值傳遞給ECX,即動態方法的個數
        PUSH    ECX
        ADD     EDI,2  ;地址加2,即跳過DMT中存儲動態方法的個數的部分
        REPNE   SCASW  ;EAX與EDI指向的數據按字依次比較,直到找到(ZF=1)或ECX=0為止
        JE      @@found
        POP     ECX
@@parent:
        MOV     ESI,[ESI].vmtParent  ;嘗試獲得父類
        TEST    ESI,ESI  ;通過EDI是否為0來判斷是否存在父類
        JNE     @@outerLoop ;存在就跳轉到@@outerLoop進行查找
        JMP     @@exit ;退出

@@found:
        POP     EAX
        ADD     EAX,EAX
        SUB     EAX,ECX         { this will always clear the Z-flag ! }  ;這句的用途就上上面說到的將標志位ZF置0
        MOV     ESI,[EDI+EAX*2-4]  ;將獲得的方法指針傳遞給ESI,理解這句先要對DMT結構的內容做些了解

@@exit:
        POP     EDI
end;

在VCL中,DMT的結構是這樣的,前2個字節儲存了DMT中動態方法的個數n,然后是方法代號,共4*n字節,最后是方法指針,也是4*n字節!

這樣就很好理解了,EDI-4就是當前方法代號所在地址,EDI-4+4*n=EDI+EAX*2-4(因為已經執行了一句ADD EAX,EAX,所以EAX=2*n)所以,[EDI+EAX*2-4]就是所找到了相應方法指針。

結合下面的

TNotifyEvent = procedure(Sender: TObject) of object;

FOnMouseEnter: TNotifyEvent;

property OnMouseEnter: TNotifyEvent read FOnMouseEnter write FOnMouseEnter;

procedure TXXX.CMMouseEnter(var Message: TMessage);
begin
  inherited;
  if Assigned(FOnMouseEnter) then
    FOnMouseEnter(Self);
end;

在跳轉到CMMouseEnter執行后,判斷方法指針FOnMouseEnter是否是nil,如果不為空,就執行相應的事件處理方法!

通過以上的一個看似復雜的過程,我們這些用Delphi的開發人員只需要很簡單的在類似
procedure TFormX.XXXMouseEnter(Sender: TObject);
begin
  //
end;

(XXX.OnMouseEnter:=XXXMouseEnter;)

的過程中寫兩行簡單的代碼,就能很容易的實現所謂的事件驅動!

很多人也許只看中結果,并不在乎過程,從這不能簡單評論誰對誰錯,對于這些知識的了解是否有用,我們每個人都可以自己去體會~~~


上一篇:如何將幾個DBGRID里的內容導入同一個EXCEL表中?

下一篇:轉載:TThread類剖析

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
學習交流
熱門圖片

新聞熱點

疑難解答

圖片精選

網友關注

主站蜘蛛池模板: 免费一级a毛片在线播放视 日日草夜夜操 | 久久综合一区二区 | free台湾极品性hd | 在线观看免费视频麻豆 | 欧美大胆xxxx肉体摄影 | 成人黄色网战 | 999精品国产 | 羞羞视频免费观看入口 | 日韩电影av在线 | 黄片毛片一级 | 久章草在线观看 | 久草最新网址 | 成人在线观看免费视频 | 久久精品久久精品久久精品 | 久久精品无码一区二区三区 | 欧美日韩在线播放 | 久久色伦理资源站 | 做爰xxxⅹ性护士hd在线 | 国产va在线观看 | 7777奇米成人四色影视 | 久久久新| 国产精品久久久久久久久久了 | 日韩 综合| china对白普通话xxxx | 黄色特级毛片 | 一级精品 | 国产一级毛片在线看 | 国产91九色 | 娇妻被各种姿势c到高潮小说 | 九色免费视频 | 精品久久中文字幕 | 一级电影免费 | 99re66热这里只有精品8 | 91久久线看在观草草青青 | china对白普通话xxxx | 欧美一级美片在线观看免费 | 中文字幕涩涩久久乱小说 | 成人午夜免费看 | 亚洲欧美aⅴ | 牛牛a级毛片在线播放 | 亚洲日本韩国精品 |