重載TControl的WndProc方法
還是先談談VCL的繼承策略。VCL中的繼承鏈的頂部是TObject基類。一切的VCL組件和對象都繼承自TObject。
打開BCB幫助查看TControl的繼承關系:
TObject->TPersistent->TComponent->TControl
呵呵,原來TControl是從TPersistent類的子類TComponent類繼承而來的。TPersistent抽象基類具有使用流stream來存取類的屬性的能力。
TComponent類則是所有VCL組件的父類。
這就是所有的VCL組件包括您的自定義組件可以使用dfm文件存取屬性的原因『當然要是TPersistent的子類,我想您很少需要直接從TObject類來派生您的自定義組件吧』。
TControl類的重要性並不亞於它的父類們。在BCB的繼承關系中,TControl類的是所有VCL可視化組件的父類。實際上就是控件的意思吧。所謂可視化是指您可以在運行期間看到和操縱的控件。這類控件所具有的一些基本屬性和方法都在TControl類中進行定義。
TControl的實現在\Borland\CBuilder5\Source\Vcl\control.pas中可以找到。『可能會有朋友問你怎麼知道在那裡?使用BCB提供的Search -> Find in files很容易找到。或者使用第三方插件的grep功能。』
好了,進入VCL的源碼吧。說到這裡免不了要抱怨一下Borland。哎,為什麼要用pascal實現這一切.....:-(
TControl繼承但並沒有重寫TObject的Dispatch()方法。反而提供了一個新的方法就是xycleo提到的WndProc()。一起來看看Borland的工程師們是怎麼寫的吧。
procedure TControl.WndProc(var Message: TMessage);
var
Form: TCustomForm;
begin
//由擁有control的窗體來處理設計期間的消息
if (csDesigning in ComponentState) then
begin
Form := GetParentForm(Self);
if (Form <> nil) and (Form.Designer <> nil) and
Form.Designer.IsDesignMsg(Self, Message) then Exit;
end
//如果需要,鍵盤消息交由擁有control的窗體來處理
else if (Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST) then
begin
Form := GetParentForm(Self);
if (Form <> nil) and Form.WantChildKey(Self, Message) then Exit;
end
//處理鼠標消息
else if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST) then
begin
if not (csDoubleClicks in ControlStyle) then
case Message.Msg of
WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK:
Dec(Message.Msg, WM_LBUTTONDBLCLK - WM_LBUTTONDOWN);
end;
case Message.Msg of
WM_MOUSEMOVE: Application.HintMouseMessage(Self, Message);
WM_LBUTTONDOWN, WM_LBUTTONDBLCLK:
begin
if FDragMode = dmAutomatic then
begin
BeginAutoDrag;
Exit;
end;
Include(FControlState, csLButtonDown);
end;
WM_LBUTTONUP:
Exclude(FControlState, csLButtonDown);
end;
end
// 下面一行有點特別。如果您仔細的話會看到這個消息是CM_VISIBLECHANGED.
// 而不是我們熟悉的WM_開頭的標准Windows消息.
// 盡管Borland沒有在它的幫助中提到有這一類的CM消息存在。但很顯然這是BCB的
// 自定義消息。呵呵,如果您對此有興趣可以在VCL源碼中查找相關的內容。一定會有不小的收獲。
else if Message.Msg = CM_VISIBLECHANGED then
with Message do
SendDockNotification(Msg, WParam, LParam);
// 最後調用dispatch方法。
Dispatch(Message);
end;
看完這段代碼,你會發現TControl類實際上只處理了鼠標消息,沒有處理的消息最後都轉入Dispatch()來處理。
但這裡需要強調指出的是TControl自己並沒有獲得焦點Focus的能力。TControl的子類TWinControl才具有這樣的能力。我憑什麼這樣講?呵呵,還是打開BCB的幫助。很多朋友抱怨BCB的幫助實在不如VC的MSDN。毋庸諱言,的確差遠了。而且這個幫助還經常有問題。但有總比沒有好啊。
言歸正傳,在幫助的The TWinControl Branch 分支下,您可以看到關於TWinControl類的簡介。指出TWinControl類是所有窗體類控件的基類。所謂窗體類控件指的是這樣一類控件:
1. 可以在程序運行時取得焦點的控件。
2. 其他的控件可以顯示數據,但只有窗體類控件才能和用戶發生鍵盤交互。
3. 窗體類控件能夠包含其他控件(容器)。
4. 包含其他控件的控件又稱做父控件。只有窗體類控件才能夠作為其他控件的父控件。
5. 窗體類控件擁有句柄。
除了能夠接受焦點之外,TWinControl的一切都跟TControl沒什麼分別。這一點意味著TwinControl可以對許多的標准事件作出響應,Windows也必須為它分配一個句柄。並且與這個主題相關的最重要的是,這裡提到是由BCB負責來對控件進行重畫以及消息處理。這就是說,TwinControl封裝了這一切。
似乎扯的太遠了。但我要提出來的問題是TControl類的WndProc方法中處理了鼠標消息。但這個消息只有它的子類TwinControl才能夠得到啊!?
這怎麼可以呢... Borland是如何實現這一切的呢?這個問題實在很奧妙。為了看個究竟,再次深入VCL吧。
還是在control.pas中,TWinControl繼承了TControl的WndProc方法。源碼如下:
procedure TWinControl.WndProc(var Message: TMessage);
var
Form: TCustomForm;
KeyState: TKeyboardState;
WheelMsg: TCMMouseWheel;
begin
case Message.Msg of
WM_SETFOCUS:
begin
Form := GetParentForm(Self);
if (Form <> nil) and not Form.SetFocusedControl(Self) then Exit;
end;
WM_KILLFOCUS:
if csFocusing in ControlState then Exit;
WM_NCHITTEST:
begin
inherited WndProc(Message);
if (Message.Result = HTTRANSPARENT) and (ControlAtPos(ScreenToClient(
SmallPointToPoint(TWMNCHitTest(Message).Pos)), False) <> nil) then
Message.Result := HTCLIENT;
Exit;
end;
WM_MOUSEFIRST..WM_MOUSELAST:
//下面這一句話指出,鼠標消息實際上轉入IsControlMouseMsg方法來處理了。
if IsControlMouseMsg(TWMMouse(Message)) then
begin
if Message.Result = 0 then
DefWindowProc(Handle, Message.Msg, Message.wParam, Message.lParam);
Exit;
end;
WM_KEYFIRST..WM_KEYLAST:
if Dragging then Exit;
WM_CANCELMODE:
if (GetCapture = Handle) and (CaptureControl <> nil) and
(CaptureControl.Parent = Self) then
CaptureControl.Perform(WM_CANCELMODE, 0, 0);
else
with Mouse do
if WheelPresent and (RegWheelMessage <> 0) and
(Message.Msg = RegWheelMessage) then
begin
GetKeyboardState(KeyState);
with WheelMsg do
begin
Msg := Message.Msg;
ShiftState := KeyboardStateToShiftState(KeyState);
WheelDelta := Message.WParam;
Pos := TSmallPoint(Message.LParam);
end;
MouseWheelHandler(TMessage(WheelMsg));
Exit;
end;
end;
inherited WndProc(Message);
end;
鼠標消息是由IsControlMouseMsg方法來處理的。只有再跟到IsControlMouseMsg去看看啦。源碼如下:
function TWinControl.IsControlMouseMsg(var Message: TWMMouse): Boolean;
var
//TControl出現啦
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);
Result := False;
if Control <> nil then
begin
P.X := Message.XPos - Control.Left;
P.Y := Message.YPos - Control.Top;
file://TControl的Perform方法將消息交由WndProc處理。
Message.Result := Control.Perform(Message.Msg, Message.Keys, Longint(PointToSmallPoint(P)));
Result := True;
end;
end;
原來如此,TWinControl最後還是將鼠標消息交給TControl的WndProc來處理了。這裡出現的Perform方法在BCB的幫助裡可以查到是TControl類中開始出現的方法。它的作用就是將指定的消息傳遞給TControl的WndProc過程。
結論就是TControl類的WndProc方法的消息是由TwinControl類在其重載的WndProc方法中調用IsControlMouseMsg方法後使用Peform方法傳遞得到的。
由於這個原因,BCB和Delphi中的TControl類及其所有的派生類都有一個先天的而且是必須的限制。那就是所有的TControl類及其派生類的Owner必須是TwinControl類或者TWinControl的派生類。Owner屬性最早可以在TComponent中找到,一個組件或者控件是由它的Owner擁有並負責釋放其內存的。這就是說,當Owner從內存中釋放的時候,它所擁有的所有控件占用的內存也都被釋放了。Owner最好的例子就是Form。Owner同時也負責消息的分派,當Owner接收到消息的時候,它負責將應該傳遞給其所擁有的控件的消息傳遞給它們。這樣這些控件就能夠取得處理消息的能力。TImage就是個例子:你可以發現Borland並沒有讓TImage重載TControl的WndProc方法,所以TImage也只有處理鼠標消息的能力,而這種能力正是來自TControl的。
唧唧崴崴的說了一大堆。終於可以說處理消息的第二種方法就是重載TControl的WndProc方法了。例程如下:
void __fastcall TForm1::WndProc(TMessage &Message)
{
switch (Message.Msg)
{
case WM_CLOSE:
OnCLOSE(Message); // 處理WM_CLOSE消息的方法
break;
}
TForm::WndProc(Message);
}
乍看起來,這和上次講的重載Dispatch方法好象差不多。但實際上還是有差別的。差別就在先後次序上,從前面TControl的WndProc可以看到,消息是先交給WndProc來處理,最後才調用Dispatch方法的啦。
這樣,重載WndProc方法可以比重載Dispatch方法更早一點點得到消息並處理消息。
好了,這次就說到這裡。在您的應用程序裡還有沒有比這更早得到消息的辦法呢?有,下次再說。