點擊TButton后的執行OnClick和OnMouseDown兩個事件的過程(其實是通過WM_COMMAND執行程序員的代碼)


問題的來源:在李維的《深入淺出VCL》一書中提到了點擊TButton會觸發WM_COMMAND消息,正是它真正執行了程序員的代碼。也許是我比較笨,沒有理解他說的含義。但是后來經過追蹤代碼和仔細分析,終於明白了整個過程。結論是,自己對Win32的不夠了解,其實觸發按鈕就是靠這個WM_COMMAND消息,而且VC里也是這樣做的。

現象:有沒有發現TButton既有OnClick,又有OnMouseDown,它們之間是什么區別和聯系是什么呢?普通的按鈕點擊到底是哪個事件執行了程序員的代碼,又是如何執行的呢?且看我的分析過程:

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    m_tag: integer;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  tag:=100;
end;

procedure TForm1.Button1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  m_tag:=200;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  ShowMessage(intToStr(tag));
  ShowMessage(intToStr(m_tag));
end;

點擊Button1后,再點擊Button2,發現tag和m_tag兩個值都被賦值了。看來用鼠標點擊Button是一箭雙雕啊,會同時觸發OnClick和OnMouseDown事件。至於這兩個事件哪個會先執行,則要看產生消息的先后順序。至於到底誰先誰后,我想了好多辦法:用SPY++觀察不行,因為Button1和Form1是兩個不同的句柄;在Application.Run里觀察消息也不行,因為實在Application運行以后是太多消息了,沒法調試。也許修改VCL源代碼並同時加上case WM_COMMAND和case WM_LBUTTONDOWN后看先截住誰。不過后來我想了一個好辦法,就是在這兩個事件里加上記錄時間的選項,這樣簡單方便,實在是不用什么高深技術。通過代碼測試,我發現還是OnClick會被先執行:

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    m_tag: integer;
    m_time_click: TTime;
    m_time_mousedown: TTime;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  tag:=100;
  m_time_click:=now;
end;

procedure TForm1.Button1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  m_tag:=200;
  m_time_mousedown:=now;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  ShowMessage(intToStr(tag));
  ShowMessage(intToStr(m_tag));
  if m_time_click>m_time_mousedown then ShowMessage('m_time_click is first');
end;

 

於是接下去自然應該分析OnClick的執行過程。不過說實話,正面分析有點難,但我還是硬着頭皮上吧(其實我本人是知道答案后反推整個過程的)。如下:

1.程序員改寫的OnClick事件,那么我們可以發現OnClick是TControl的事件:
property OnClick: TNotifyEvent read FOnClick write FOnClick stored IsOnClickStored;
同時可以了解一下什么是TNotifyEvent?按住Ctrl點擊鼠標就可以找到它的定義:
TNotifyEvent = procedure(Sender: TObject) of object;
就是說是一個函數指針。從這個原理上來猜,就是VCL框架讓這個函數指針指向了程序員定義的那個函數,才使得程序員定義的函數自動被融入到VCL框架內得以正確執行。

2.然后在TControl里搜索,是誰在調用。發現有不少地方都調用了FOnClick事件。不過我們這個例子里,沒有什么action在起作用,所以只能是TControl.Click函數在調用它,代碼如下:

procedure TControl.Click;
begin
{ Call OnClick if assigned and not equal to associated action's OnExecute.
If associated action's OnExecute assigned then call it, otherwise, call
OnClick. }
if Assigned(FOnClick) and (Action <> nil) and (@FOnClick <> @Action.OnExecute) then
FOnClick(Self) 
else if not (csDesigning in ComponentState) and (ActionLink <> nil) then
ActionLink.Execute(Self)
else if Assigned(FOnClick) then
FOnClick(Self); // 這里,會執行函數指針指向的內容
end;

再看它的定義,發現是一個動態函數:

procedure TControl.Click; dynamic;
於是下一步就該研究,是誰調用Click函數了。

題外話:為什么要搜索Controls單元?因為它第一次定義了OnClick事件,所以嫌疑最大。如果它那里只定義不處理,那也有是可能的。不過我的整個例子只涉及到TForm和TButton,除了這兩個類本身要研究,還有就是它們的父類要研究,那樣就縮小研究范圍、一共只有幾個類了,一定可以找到OnClick事件的來龍去脈。好在我們在TControl里就發現它了,那樣就變得更簡單了。關於這點完全是我自己的心得,別的文章可以把原理講的更透徹,但是對於類似我這樣的白痴產生更源頭上的問題,只有我這樣有相同的疑惑和經歷才會講到這一點。其實關於VCL我還有大量的疑惑沒有解決(當然是在仔細研讀了VCL代碼基礎上產生的大量問題,很多都是細節里的細節),如果哪位高手願意與我探討,我將不甚感激。

3. 很明顯,對於一個動態函數,一旦其子類有覆蓋函數,那么就會執行子類的覆蓋函數。沒有就拉倒,變得更簡單了。搜索TButton及其父類TButtonControl,我們果然在TButton里發現了它的覆蓋函數:procedure Click; override; 即:

procedure TButton.Click;
var
Form: TCustomForm;
begin
Form := GetParentForm(Self);
if Form <> nil then Form.ModalResult := ModalResult;
inherited Click;
end;

有趣的是,我們發現這個覆蓋函數僅僅改寫父窗體的狀態,它本身並不真正執行程序員事件,還是要inherited Click;也就是TControl.Click來執行程序員的事件。猜測這么做是因為放在TControl里可以讓圖形控件也擁有Click的能力。

4. 我們繼續搜索,發現在TControl.WMLButtonUp函數里調用Click;函數(注意,到這步分析錯了,大家不要往下看了,稍后糾正)

procedure TControl.WMLButtonUp(var Message: TWMLButtonUp);
begin
inherited;
if csCaptureMouse in ControlStyle then MouseCapture := False;
if csClicked in ControlState then
begin
Exclude(FControlState, csClicked);
if PtInRect(ClientRect, SmallPointToPoint(Message.Pos)) then // API
Click; // 類函數
end;
DoMouseUp(Message, mbLeft);
end;

正是它響應了WM_LBUTTONUP消息。注意啊,OnClick事件只有放開鼠標的時候才執行,否則是不會執行的;而且因為PtInRect函數判斷的關系,按下按以后,鼠標是不能移到Button1范圍之外,否則也不會執行Click函數,不信可以試試。

5. 至此就可以明白,只要鼠標點擊Button1,鼠標就會產生WM_LBUTTONUP消息並發送給Button1,VCL內建的消息循環必然會找到TControl.WMLButtonUp從而執行Click函數。但是它會先執行TButton.Click;函數,這個Click函數做了兩件事情:先通知祖先Form(一定是Form,而不是別的窗口,而且也不必是直接的父窗口,可以是間接的。所以Form里放一個TPanel,TPanel里放一個TButton也還是可以找到這個Form,這樣間接的按鈕照樣通過改變ModalResult照樣關閉一個Form)的ModalResult屬性狀態被改變了,然后執行要inherited Click;也就是TControl.Click;這個函數里面有這句:if Assigned(FOnClick) then FOnClick(Self); 如果FOnClick有值了,或者說不再是一個空指針了,那么它就會調用函數指針FOnClick執行的那個函數,而且還是帶一個Self參數的函數。那么FOnClick是否有值了如何判斷呢?簡單呀,程序員雙擊Button1后,在Unit1.dfm里以下內容:

object Button1: TButton
Left = 256
Top = 72
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 0 OnClick = Button1Click // 這里,函數指針連接了程序員的函數!
OnMouseDown = Button1MouseDown end

也就是說OnClick已經指向了程序員定義的函數(其實是IDE為自動產生,程序員填寫執行內容的函數。通過手動賦值OnClick = Func1就可以指向另一個指定函數一點問題沒有)。

OnClick的分析過程到此結束。OnMouseDown的分析過程,且聽下回分解(我會繼續編輯此文,而不是另開博文)。

--------------------------------------------------------------------------

有興趣的還可以參考一下這篇文章,圖文並茂,挺好的:
http://ymg97526.blog.163.com/blog/static/173658160201131021911946/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM