Delphi 的消息機制淺探(2)

補充知識:TWndMethod 概述
===============================================================================
寫這段基礎知識是因爲我在閱讀 MakeObjectInstance(MainWndProc) 這句時不知道究竟傳遞了什麼東西給 MakeObjectInstance。弄清楚了 TWndMethod 類型的含義還可以理解後面 VCL 消息系統中的一個小技巧。

  TWndMethod = procedure(var Message: TMessage) of object;

這句類型聲明的意思是:TWndMethod 是一種過程類型,它指向一個接收 TMessage 類型參數的過程,但它不是一般的靜態過程,它是對象相關(object related)的。TWndMethod 在內存中存儲爲一個指向過程的指針和一個對象的指針,所以佔用8個字節。TWndMethod類型的變量必須使用已實例化的對象來賦值。舉個例子:
  var
    SomeMethod: TWndMethod;
  begin
    SomeMethod := Form1.MainWndProc; // 正確。這時 SomeMethod 包含 MainWndProc
                                     // 和 Form1 的指針,可以用 SomeMethod(Msg)
                                     // 來執行。
    SomeMethod := TForm.MainWndProc; // 錯誤!不能用類引用。
  end;

  如果把 TWndMethod變量賦值給虛方法會怎樣?舉例:
  var
    SomeMethod: TWndMethod;
  begin
    SomeMethod := Form1.WndProc;  // TForm.WndProc 是虛方法
  end;

這時,編譯器實現爲 SomeMethod 指向 Form1 對象虛方法表中的 WndProc 過程的地址和 Form1 對象的地址。也就是說編譯器正確地處理了虛方法的賦值。調用 SomeMethod(Message) 就等於調用 Form1.WndProc(Message)。

在可能被賦值的情況下,對象方法最好不要設計爲有返回值的函數(function),而要設計爲過程(procedure)。原因很簡單,把一個有返回值的對象方法賦值給 TWndMethod 變量,會造成編譯時的二義性。

===============================================================================
⊙ VCL 的消息處理從 TWinControl.MainWndProc 開始
===============================================================================
通過對 Application.Run、TWinControl.Create、TWinControl.Handle 和 TWinControl.CreateWnd 的討論,我們現在可以把焦點轉向 VCL 內部的消息處理過程。VCL 控件的消息源頭就是 TWinControl.MainWndProc 函數。(如果不能理解這一點,請重新閱讀上面的討論。)

讓我們先看一下 MainWndProc 函數的代碼(異常處理的語句被我刪除):

procedure TWinControl.MainWndProc(var Message: TMessage);
begin
  WindowProc(Message);
end;

TWinControl.MainWndProc 以引用(也就是隱含傳地址)的方式接受一個 TMessage 類型的參數,TMessage 的定義如下(其中的WParam、LParam、Result 各有 HiWord 和 LoWord 的聯合字段,被我刪除了,免得代碼太長):

  TMessage = packed record
    Msg:    Cardinal;
    WParam: Longint;
    LParam: Longint;
    Result: Longint);
  end;

TMessage 中並沒有窗口句柄,因爲這個句柄已經在窗口創建之後保存在 TWinControl.Handle 之中。TMessage.Msg 是消息的 ID 號,這個消息可以是 Windows 標準消息、用戶定義的消息或 VCL 定義的 Control 消息等。WParam 和 LParam 與標準 Windows 回調函數中 wParam 和 lParam 的意義相同,Result 相當於標準 Windows 回調函數的返回值。

注意 MainWndProc 不是虛函數,所以它不能被 TWinControl 的繼承類重載。(思考:爲什麼 Borland 不將 MainWndProc 設計爲虛函數呢?)

MainWndProc 中建立兩層異常處理,用於釋放消息處理過程中發生異常時的資源泄漏,並調用默認的異常處理過程。被異常處理包圍着的是 WindowProc(Message)。WindowProc 是 TControl(而不是 TWinControl) 的一個屬性(property):

  property WindowProc: TWndMethod read FWindowProc write FWindowProc;

WindowProc 的類型是 TWndMethod,所以它是一個對象相關的消息處理函數指針(請參考前面 TWndMethod 的介紹)。在 TControl.Create 中 FWindowProc 被賦值爲 WndProc。

WndProc 是 TControl 的一個函數,參數與 TWinControl.MainWndProc 相同:

  procedure TControl.WndProc(var Message: TMessage); virtual;

原來 MainWndProc 只是個代理函數,最終處理消息的是 TControl.WndProc 函數。

那麼 Borland 爲什麼要用一個 FWindowProc 來存儲這個 WndProc 函數,而不直接調用 WndProc 呢?我猜想可能是基於效率的考慮。還記得上面 TWndMethod 的討論嗎?一個 TWndMethod 變量可以被賦值爲一個虛函數,編譯器對此操作的實現是通過對象指針訪問到了對象的虛函數表,並把虛函數表項中的函數地址傳回。由於 WndProc 是一個調用頻率非常高的函數(可能要用“百次/秒”或“千次/秒”來計算),所以如果每次調用 WndProc 都要訪問虛函數表將會浪費大量時間,因此在 TControl 的構造函數中就把 WndProc 的真正地址存儲在 WindowProc 中,以後調用 WindowProc 將就轉換爲靜態函數的調用,以加快處理速度。

===============================================================================
⊙ TWinControl.WndProc
===============================================================================
轉了層層彎,到現在我們纔剛進入 VCL 消息系統處理開始的地方:WndProc 函數。如前所述,TWinControl.MainWndProc 接收到消息後並沒有處理消息,而是把消息傳遞給 WindowProc 處理。由於 WindowProc 總是指向當前對象的 WndProc 函數的地址,我們可以簡單地認爲 WndProc 函數是 VCL 中第一個處理消息的函數,調用 WindowProc 只是效率問題。

WndProc 函數是個虛函數,在 TControl 中開始定義,在 TWinControl 中被重載。Borland 將 WndProc 設計爲虛函數就是爲了各繼承類能夠接管消息處理,並把未處理的消息或加工過的消息傳遞到上一層類中處理。

這裏將消息處理的傳遞過程和對象的構造函數稍加對比:

對象的構造函數通常會在第一行代碼中使用 inherited 語句調用父類的構造函數以初始化父類定義的成員變量,父類也會在構造函數開頭調用祖父類的構造函數,如此遞歸,因此一個 TWinControl 對象的創建過程是 TComponent.Create -> TControl.Create -> TWinControl.Create。

而消息處理函數 WndProc 則是先處理自己想要的消息,然後看情況是否要遞交到父類的 WndProc 中處理。所以消息的處理過程是 TWinControl.WndProc -> TControl.WndProc。

因此,如果要分析消息的處理過程,應該從子類的 WndProc 過程開始,然後纔是父類的  WndProc 過程。由於 TWinControl 是第一個支持窗口創建的類,所以它的 WndProc 是很重要的,它實現了最基本的 VCL 消息處理。

TWinControl.WndProc 主要是預處理一些鍵盤、鼠標、窗口焦點消息,對於不必響應的消息,TWinControl.WndProc 直接返回,否則把消息傳遞至 TControl.WndProc 處理。

從 TWinControl.WndProc 摘抄一段看看:

    WM_KEYFIRST..WM_KEYLAST:
      if Dragging then Exit;      // 注意:使用 Exit 直接返回

這段代碼的意思是:如果當前組件正處於拖放狀態,則丟棄所有鍵盤消息。

再看一段:
    WM_MOUSEFIRST..WM_MOUSELAST:
      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);
          // DefWindowProc 是 Win32 API 中缺省處理消息的函數
        Exit;
      end;

這裏的 IsControlMouseMsg 很關鍵。讓我們回憶一下:TControl 類的對象並沒有創建 Windows 窗口,它是怎樣接收到鼠標和重繪等消息的呢?原來這些消息就是由它的 Parent 窗口發送的。

在上面的代碼中,TWinControl.IsControlMouseMsg 判斷鼠標地址是否落在 TControl 類控件上,如果不是就返回否值。TWinControl 再調用 TControl.WndProc,TControl.WndProc 又調用了 TObject.Dispatch 方法,這是後話。

如果當前鼠標地址落在窗口上的 TControl 類控件上,則根據 TControl 對象的相對位置重新生成了鼠標消息,再調用 TControl.Perform 方法把加工過的鼠標消息直接發到 TControl.WndProc 處理。TControl.Perform 方法以後再談。

如果 TWinControl 的繼承類重載 WndProc 處鼠標消息,但不使用 inherited 把消息傳遞給父類處理,則會使從 TControl 繼承下來的對象不能收到鼠標消息。現在我們來做個試驗,下面 Form1 上的 TSpeedButton 等非窗口控件不會發生 onClick 等鼠標事件。

procedure TForm1.WndProc(var Message: TMessage); override;
begin
  case Message.Msg of
    WM_MOUSEFIRST..WM_MOUSELAST:
      begin
        DefWindowProc(Handle, Message.Msg, Message.WParam, Message.LParam);
        Exit; // 直接退出
      end;
  else
    inherited;
  end;
end;

TWinControl.WndProc 的最後一行代碼是:

  inherited WndProc(Message);

也就是調用 TControl.WndProc。讓我們來看看 TControl.WndProc 做了些什麼。

===============================================================================
⊙ TControl.WndProc
===============================================================================
TControl.WndProc 主要實現的操作是:
    響應與 Form Designer 的交互(在設計期間)
    在控件不支持雙擊的情況下把鼠標雙擊事件轉換成單擊
    判斷鼠標移動時是否需要顯示提示窗口(HintWindow)
    判斷控件是否設置爲 AutoDrag,如果是則執行控件的拖放處理
    調用 TControl.MouseWheelHandler 實現鼠標滾輪消息
    使用 TObject.Dispatch 調用 DMT 消息處理方法

TControl.WndProc 相對比較簡單,在此只隨便談談第二條。你是否有過這樣的使用經驗:在你快速雙擊某個軟件的 Button 時,只形成一次 Click 事件。所以如果你需要設計一個不管用戶用多快的速度點擊,都能生成同樣點擊次數 Click 事件的按鈕時,就需要參考 TControl.WndProc 處理鼠標消息的過程了。

TControl.WndProc 最後一行代碼是 Dispatch(Message),也就是說如果某個消息沒有被 TControl 以後的任何類處理,消息會被 Dispatch 處理。

TObject.Dispatch 是 Delphi VCL 消息體系中非常關鍵的方法。

===============================================================================
⊙ TObject.Dispatch
===============================================================================
TObject.Dispatch 是個虛函數,它的聲明如下:

  procedure TObject.Dispatch(var Message); virtual;

請注意它的參數雖然與 MainWndProc 和 WndProc 的參數相似,但它沒有規定參數的類型。這就是說,Dispatch 可以接受任何形式的參數。

Delphi 的文檔指出:Message參數的前 2 個字節是 Message 的 ID(下文簡稱爲 MsgID),通過 MsgID 搜索對象的消息處理方法。

這段話並沒有爲我們理解 Dispatch 方法提供更多的幫助,看來我們必須通過閱讀源代碼來分析這個函數的運作過程。

TObject.Dispatch 雖然是個虛方法,但卻沒有被 TPersistent、TComponent、TControl、TWinControl、TForm 等後續類重載( TCommonDialog 調用了 TObject.Dispatch,但對於整個 VCL 消息系統並不重要),並且只由 TControl.WndProc 調用過。所以可以簡單地認爲如果消息沒有在 WndProc 中被處理,則被 TObject.Dispatch 處理。

我們很容易查覺到一個很重要的問題:MsgID 是 2 個字節,而 TMessage.Msg 是 4 個字節,如果 TControl.WndProc 把 TMessage 消息傳遞給 Dispatch 方法,是不是會形成錯誤的消息呢?

要解釋這個問題,必須先了解 Windows 消息的規則。由於 Windows 操作系統的所有窗口都使用消息傳遞事件和信息,Microsoft 必須制定窗口消息的格式。如果每個程序員都隨意定義消息 ID 值肯定會產生混亂。Microsoft 把窗口消息分爲五個區段:

  0x00000000 至 WM_USER - 1             標準視窗消息,以 WM_ 爲前綴
  WM_USER    至 WM_APP  - 1             用戶自定義窗口類的消息
  WM_APP     至 0x0000BFFF              應用程序級的消息
  0x0000C000 至 0x0000FFFF              RegisterWindowMessage 生成的消息範圍
  0x00010000 至 0xFFFFFFFF              Microsoft 保留的消息,只由系統使用

  ( WM_USER = 0x00000400,  WM_APP = 0x00008000 )

發現問題的答案了嗎?原來應用程序真正可用的消息只有 0x00000000 至 0x0000FFFF,也就是消息 ID 只有低位 2 字節是有效的。(Borland 真是牛啊,連這也能想出來。)

由於 Intel CPU 的內存存放規則是高位字節存放在高地址,低位字節存放在低地址,所以 Dispatch 的 Message 參數的第一個內存字節就是 LoWord(Message.Msg)。下圖是 Message參數的內存存放方式描述:

        |        | + Memory
        |--------|
        | HiWord |
        |--------|
        | LoWord | <-- [EDX]
        |--------|
        |        |
        |--------|
        |        |
        |--------| - Memory
        [ 圖示:Integer 類型的 MsgID 在內存中的分配(見 Dispatch 彙編代碼) ]
        (爲了簡單起見,我用 Word 爲內存單位而不是 Byte,希望不至於更難看懂)

現在可以開始閱讀 TObject.Dispatch 的彙編代碼了(不懂彙編沒關係,後面會介紹具體的功能):

procedure TObject.Dispatch(var Message); virtual; 
asm
    PUSH    ESI            ; 保存 ESI
    MOV     SI,[EDX]       ; 把 MsgID 移入 SI (2 bytes)
                           ; 如果 MsgID 是Integer 類型,[EDX] = LoWord(MsgID),
                           ; 見上圖
    OR      SI,SI      
    JE      @@default      ; 如果 SI = 0,調用 DefaultHanlder
    CMP     SI,0C000H
    JAE     @@default      ; 如果 SI >= $C000,調用 DefaultHandler (注意這裏)
    PUSH    EAX            ; 保存對象的指針
    MOV     EAX,[EAX]      ; 找到對象的 VMT 指針
    CALL    GetDynaMethod  ; 調用對象的動態方法; 如果找到了動態方法 ZF = 0 ,
                           ; 沒找到 ZF = 1
                           ; 注:GetDynaMethod 是 System.pas 中的獲得動態方法地
                           ; 址的彙編函數
    POP     EAX            ; 恢復 EAX 爲對象的指針
    JE      @@default      ; 如果沒找到相關的動態方法,調用 DefaultHandler     
    MOV     ECX,ESI        ; 把找到的動態方法指針存入 ECX
    POP     ESI            ; 恢復 ESI
    JMP     ECX            ; 調用對象的動態方法

@@default:
    POP     ESI            ; 恢復 ESI
    MOV     ECX,[EAX]      ; 把對象的 VMT 指針存入 ECX,以調用 DefaultHandler
    JMP     DWORD PTR [ECX] + VMTOFFSET TObject.DefaultHandler
end;

TObject.Dispatch 的執行過程是:
    把 MsgID 存入 SI,作爲動態方法的索引值
    如果 SI >= $C000,則調用 DefaultHandler(也就是所有 RegisterWindowMessage
        生成的消息ID 會直接被髮送到 DefaultHandler 中,後面會講一個實例)
    檢查是否有相對應的動態方法
    找到了動態方法,則執行該方法
    沒找到動態方法,則調用 DefaultHandler

原來以 message 關鍵字定義的對象方法就是動態方法,隨便從 TWinControl 中抓幾個消息處理函數出來:

    procedure WMSize(var Message: TWMSize); message WM_SIZE;
    procedure WMMove(var Message: TWMMove); message WM_MOVE;

到現在終於明白 WM_SIZE、WM_PAINT 方法的處理過程了吧。不但是 Windows 消息,連 Delphi 自己定義的消息也是以同樣的方式處理的:

    procedure CMEnabledChanged(var Message: TMessage); message CM_ENABLEDCHANGED;
    procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED;

所以如果你自己針對某個控件定義了一個消息,你也可以用 message 關鍵字定義處理該方法的函數,VCL 的消息系統會自動調用到你定義的函數。

由於 Dispatch 的參數只以最前 2 個字節爲索引,並且自 MainWndProc 到 WndProc 到 Dispatch 都是以引用(傳遞地址)的方式來傳遞消息內容,你可以將消息的結構設置爲任何結構,甚至可以只有 MsgID —— 只要你在處理消息的函數中正確地訪問這些參數就行。

最關鍵的 Dispatch 方法告一段落,現在讓我們看看 DefaultHandler 做了些什麼?

===============================================================================
⊙ TWinControl.DefaultHandler
===============================================================================
DispatchHandler 是從 TObject 就開始存在的,它的聲明如下:

  procedure TObject.DefaultHandler(var Message); virtual;

從名字也可以看出該函數的大概目的:最終的消息處理函數。在 TObject 的定義中 DefaultHandler 並沒有代碼,DefaultHandler 是在需要處理消息的類(TControl)之後被重載的。

從上面的討論中已經知道 DefaultHandler 是由 TObject.Dispatch 調用的,所以 DefaultHandler 和 Dispatch 的參數類型一樣都是無類型的 var Message。

由於 DefaultHandler 是個虛方法,所以執行流程是從子類到父類。在 TWinControl 和 TControl 的 DefaultHandler 中,仍然遵從 WndProc 的執行規則,也就是 TWinControl 沒處理的消息,再使用 inherited 調用 TControl.DefaultHandler 來處理。

在 TWinControl.DefaultHandler 中先是處理了一些不太重要的Windows 消息,如WM_CONTEXTMENU、WM_CTLCOLORMSGBOX等。然後做了兩件比較重要的工作:1、處理 RM_GetObjectInstance 消息;2、對所有未處理的窗口消息調用 TWinControl.FDefWndProc。
下面分別討論。

RM_GetObjectInstance 是應用程序啓動時自動使用 RegisterWindowMessage API 註冊的 Windows 系統級消息ID,也就是說這個消息到達 Dispatch 後會無條件地傳遞給 DefaultHandler(見 Dispatch 的分析)。TWinControl.DefaultHandler 發現這個消息就把 Self 指針設置爲返回值。在 Controls.pas 中有個函數 ObjectFromHWnd 使用窗口句柄獲得 TWinControl 的句柄,就是使用這個消息實現的。不過這個消息是由 Delphi 內部使用,不能被應用程序使用。(思考:每次應用程序啓動都會調用 RegisterWindowMessage,如果電腦長期不停機,那麼 0xC000 - 0xFFFF 之間的消息 ID 是否會被耗盡?)

另外,TWinControl.DefaultHandler 在 TWinControl.FHandle 不爲 0 的情況下,使用 CallWindowProc API 調用 TWndControl.FDefWndProc 窗口過程。FDefWndProc 是個指針,它是從哪裏初始化的呢?跟蹤一下,發現它是在 TWinControl.CreateWnd 中被設置爲如下值:

    FDefWndProc := Params.WindowClass.lpfnWndProc;

還記得前面討論的窗口創建過程嗎?TWinControl.CreateWnd 函數首先調用 TWinControl.CreateParams 獲得待創建的窗口類的參數。CreateParams 把 WndClass.lpfnWndProc 設置爲 Windows 的默認回調函數 DefWindowProc API。但 CreateParams 是個虛函數,可以被 TWinControl 的繼承類重載,因此程序員可以指定一個自己設計的窗口過程。

所以 TWinControl.DefaultHandler 中調用 FDefWndProc 的意圖很明顯,就是可以在 Win32 API 的層次上支持消息的處理(比如可以從 C 語言寫的 DLL 中導入窗口過程給 VCL 控件),給程序員提供充足的彈性空間。

TWinControl.DefaultHandler 最後一行調用了 inherited,把消息傳遞給 TControl 來處理。

TControl.DefaultHandler 只處理了三個消息 WM_GETTEXT、WM_GETTEXTLENGTH、WM_SETTEXT。爲什麼要處理這個幾個看似不重要的消息呢?原因是:Windows 系統中每個窗口都有一個 WindowText 屬性,而 VCL 的 TControl 爲了模擬成窗口也存儲了一份保存在 FText 成員中,所以 TControl 在此接管這幾個消息。

TControl.DefaultHandler 並沒有調用 inherited,其實也沒有必要調用,因爲 TControl 的祖先類都沒有實現 DefaultHandler 函數。可以認爲 DefaultHandler 的執行到此爲止。

VCL 的消息流程至此爲止。

===============================================================================
⊙ TControl.Perform 和 TWinControl.Broadcast
===============================================================================
現在介紹 VCL 消息系統中兩個十分簡單但調用頻率很高的函數。

TControl.Perform 用於直接把消息送往控件的消息處理函數 WndProc。Perform 方法不是虛方法,它把參數重新組裝成一個 TMessage 類型,然後調用 WindowProc(還記得 WindowProc 的作用嗎?),並返回 Message.Result 給用戶。它的調用格式如下:

  function TControl.Perform(Msg: Cardinal; WParam, LParam: Longint): Longint;

Perform 經常用於通知控件某些事件發生,或得到消息處理的結果,如下例:

  Perform(CM_ENABLEDCHANGED, 0, 0);
  Text := Perform(WM_GETTEXTLENGTH, 0, 0);

TWinControl.Broadcast 用於把消息廣播給每一個子控件。它調用 TWinControl.Controls[] 數組中的所有對象的 WindowsProc 過程。

  procedure TWinControl.Broadcast(var Message);

注意 Broadcast 的參數是無類型的。雖然如此,在 Broadcast 函數體中會把消息轉換爲 TMessage 類型,也就是說 Broadcast 的參數必須是 TMessage 類型。那麼爲什麼要設計爲無類型的消息呢?原因是 TMessage 有很多變體(Msg 和 Result 字段不會變,WParam 和 LParam 可設計爲其它數據類型),將 Broadcast 設計爲無類型參數可以使程序員不用在調用前強制轉換參數,但調用時必須知道這一點。比如以下字符消息的變體,是和 TMessage 兼容的:

  TWMKey = packed record
    Msg: Cardinal;
    CharCode: Word;
    Unused: Word;
    KeyData: Longint;
    Result: Longint;
  end;

===============================================================================
⊙ TWinControl.WMPaint
===============================================================================
上面在討論 TWinControl.WndProc 時提到,TControl 類控件的鼠標和重繪消息是從 Parent TWinControl 中產生的。但我們只發現了鼠標消息的產生,那麼重繪消息是從哪裏產生出來的呢?答案是TWinControl.WMPaint:

    procedure TWinControl.WMPaint(var Message: TWMPaint); message WM_PAINT;

在 TWinControl.WMPaint 中建立了雙緩衝重繪機制,但我們目前不關心這個,只看最關鍵的代碼:

    if not (csCustomPaint in ControlState) and (ControlCount = 0) then
      inherited                 // 注意 inherited 的實現
    else
      PaintHandler(Message);    

這段代碼的意思是,如果控件不支持自繪製並且不包含 TControl 就調用 inherited。
inherited 是什麼呢?由於 TWinControl.WMPaint 的父類 TControl 沒有實現這個消息句柄,Delphi 生成的彙編代碼竟然是:call Self.DefaultHandler。(TWinControl.DefaultHandler 只是簡單地調用 TWinControl.FDefWndProc。)

如果條件爲否,那麼將調用 TWinControl.PaintHandler(不是虛函數)。PaintHandler 調用 BeginPaint API 獲得窗口設備環境,再使用該設備環境句柄爲參數調用 TWinControl.PaintWindow。在 TWinControl 中 PaintWindow 只是簡單地把消息傳遞給 DefaultHandler。PaintWindow 是個虛函數,可以在繼承類中被改寫,以實現自己需要的繪製內容。PaintHandler 還調用了 TWinControl.PaintControls 方法。PaintControls 使用 Perform 發送 WM_PAINT 消息給 TWinControl 控件包含的所有 TControl 控件。

這樣,TControl 控件才獲得了重繪的消息。

讓我們設計一個 TWinControl 的繼承類作爲練習:

TMyWinControl = class(TWinControl)
  protected
    procedure PaintWindow(DC: HDC); override;
  public
    constructor Create(AOwner: TComponent); override;
  end;

constructor TMyWinControl.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ControlState := ControlState + [csCustomPaint];
  // 必須通知 WMPaint 需要畫自己
end;

procedure TMyWinControl.PaintWindow(DC: HDC);
var
  Rect: TRect;
begin
  Windows.GetClientRect(Handle, Rect);
  FillRect(DC, Rect, COLOR_BTNSHADOW + 1);
  SetBkMode(DC, TRANSPARENT);
  DrawText(DC, 'Hello, TMyWinControl', -1, Rect, DT_SINGLELINE or DT_VCENTER
    or DT_CENTER);
end;

上面實現的 TMyWinControl 簡單地重載 PaintWindow 消息,它可以包含 TControl 對象,並能正確地把它們畫出來。如果你確定該控件不需要包含 TControl 對象,你也可以直接重載 WMPaint 消息,這就像用 C 語言寫普通的 WM_PAINT 處理函數一樣。

===============================================================================
⊙ 以 TWinControl 爲例描述消息傳遞的路徑
===============================================================================
下圖描述一條消息到達後消息處理函數的調用路徑,每一層表示函數被上層函數調用。

TWinControl.FObjectInstance
|-TWinControl.MainWndProc
      |-TWinControl.WindowProc
          |-TWinControl.WndProc
              |-TControl.WndProc
                  |-TObject.Dispatch
                      |-Call DMT messages
                      |-TWinControl.DefaultHandler
                          |-TControl.DefaultHandler

注:
如前文所述,上圖中的 WindowProc 是個指針,所以它在編譯器級實際上等於 WndProc,而不是調用 WndProc,圖中爲了防止與消息分枝混淆特意區分成兩層。
TObject.Dispatch 有兩條通路,如果當前控件以 message 關鍵字實現了消息處理函數,則呼叫該函數,否則調用 DefaultHandler。
有些消息處理函數可能在中途就已經返回了,有些消息處理函數可能會被遞歸調用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章