這可能實現嗎?秘密在于在控件之前搶先截獲Windows消息。這可以通過使用一個叫做WindowProc的TControl屬性來實現,這個屬性實質上指向控件的Windows消息事件處理器(event handler)。
為了展示這一技術,我們將創建一個LinkedLabel控件,可以將它連接到任何TControl控件并且動態改變它的行為。TLinkedLabel由TLabel繼承而來,附加4個公開的屬性:
Associate —— 將被改變行為的相連控件
CapsLock —— 當這個Boolean屬性被設置為True時,特定類型的控件將把小寫鍵盤輸入作為大寫來處理。這個屬性并不對所有控件有效,因為并不是所有的控件都以相同的方式相應WM_CHAR消息。經測試Edit,MaskEdit,Memo,和RichEdit控件都對CapsLock屬性有響應,但是ComboBox則不響應。很明顯,CapsLock屬性對于很多其他控件(如Button、CheckBox等)只有很小的影響,或者沒有影響。
Gap —— LinkedLabel與相連控件的距離
OnTop —— 這個Boolean屬性決定LinkedLabel出現在相連控件的左側還是頂端。
另外,TlinkedLabel將保持自身和相連控件的Enabled和Visible屬性相一致。它也會保持自身和相連控件的距離和角度,也就是說,當你移動LinkedLabel時,其關聯也會隨之移動,反之亦然。
我們來看一下TLinkedLabel類的聲明,如圖1所示。
unit LinkedLabel;
interface
uses
Messages, Classes, Controls, StdCtrls;
type
TLinkedLabel = class(TLabel)
private
// 相連控件.
FAssociate: TControl;
// 將 FAssociate 置為全大寫模式
FCapsLock: Boolean;
// 標簽與關聯控件之間的距離
FGap: Integer;
// 標簽在關聯控件頂端時為true
FOnTop: Boolean;
// 保存 FAssociate.WindowProc的原始值
FOldWinProc: TWndMethod;
// 用于防止無限更新循環
FUpdating: Boolean;
protected
procedure Adjust(MoveLabel: Boolean);
procedu SetGap(Value: Integer);
procedure SetOnTop(Value: Boolean);
procedure SetAssociate(Value: TControl);
procedure NewWinProc(var Message: TMessage);
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
procedure WndProc(var Message: TMessage); override;
public
constructor Create(AOwner :TComponent); override;
destructor Destroy; override;
published
property Associate: TControl
read FAssociate write SetAssociate;
property CapsLock: Boolean
read FCapsLock write FCapsLock;
property Gap: Integer read FGap write SetGap default 8;
property OnTop: Boolean read FOnTop write SetOnTop;
end;
現在讓我們來仔細看看這個控件中的不同方法,先由構造器(constructor)開始。首先說明一下,當創建一個新對象時,與它相關聯的所有內存都被清空。這個動作將會自動把Fassociate和FoldWinProc設置為nil,將FcapsLock、FonTop、Fupdating設置為False。所有這些都不需要在構造器中明確的初始化它們。因此,唯一需要我們在構造器中設置的就是Gap的默認值。
implementation
constructor TLinkedLabel.Create(AOwner: TComponent);
begin
inherited;
FGap := 8;
end;
現在我們來看一下Adjust方法,它負責安排LinkedLabel或者關聯控件的放置(取決于MoveLabel參數的取值)。正如你將在代碼中看到的,LinkedLabel與相關控件的實際位置取決于Gap和OnTop屬性(見圖2)。雖然我們在OnTop中只提供了兩種可能的選擇,不過可以很容易的對其編程以提供更多的可能性。不過,把TlinkedLabel武裝到牙齒(原文是“add a lot of "bells and whistles"”,譯者注)并不是本文的重點,這項任務就委托給讀者們來完成吧。
procedure TLinkedLabel.Adjust(MoveLabel: Boolean);
var
dx, dy: Integer;
begin
if (Assigned(FAssociate)) then begin
if (FOnTop) then
begin
dx := 0;
dy := Height + FGap;
end
else
begin
dx := Width + FGap;
dy := (Height - FAssociate.Height) div 2;
end;
if (MoveLabel) then
begin
Left := FAssociate.Left - dx;
Top := FAssociate.Top - dy;
end
else
begin
FAssociate.Left := Left + dx;
FAssociate.Top := Top + dy;
end;
end;
end;
現在,我們來完成Gap和OnTop屬性的set方法(見圖3),以便當Gap或者Onop屬性被修改時我們可以改變LinkedLabel的位置。
procedure TLinkedLabel.SetGap(Value: Integer);
begin
if (FGap $#@60;$#@62; Value) then
begin
FGap := Value;
Adjust(True);
end;
end;
procedure TLinkedLabel.SetOnTop(Value: Boolean);
begin
if (FOnTop $#@60;$#@62; Value) then
begin
FOnTop := Value;
Adjust(True);
end;
end;
現在是SetAssociate方法
procedure TLinkedLabel.SetAssociate(Value: TControl);
begin
if (Value $#@60;$#@62; FAssociate) then begin
if (Assigned(FAssociate)) then
FAssociate.WindowProc := FOldWinProc;
FAssociate := Value;
if (Assigned(Value)) then
begin
Adjust(True);
Enabled := FAssociate.Enabled;
Visible := FAssociate.Visible;
FOldWinProc := FAssociate.WindowProc;
FAssociate.WindowProc := NewWinProc;
end;
end;
end;
為了便于理解,我們需要詳細的討論一下WindowProc屬性。WindowProc被定義為TwndMethod類型。TwndMethod可以在Controls單元中找到,定義如下:
TWndMethod = procedure(var Message: TMessage) of object;
注意,FoldWinProc同樣被定義為TwndMethod,并且NewWinProc方法擁有與TwndMethod相同的參數結構。這就允許我們將FoldWinProc指向WindowProc的當前值,并把WindowProc重定向到NewWinProc方法。如果WindowProc只是另一個事件屬性的話,我們為什么需要使用FoldWinProc呢?因為WindowProc與其它事件屬性的不同之處在于WindowProc指向一個已經存在的事件處理器。如果我們只是簡單的將WindowProc指向我們的方法,這個控件將不能再對任何Windows消息產生響應。為了解決這個問題,我們在把WindowProc指向NewWinProc之前把FoldWinProc設置為WindowProc的當前值。
在NewWinProc中,我們通過FoldWinProc調用原先的消息處理器(message handler),并且處理特定的Windows消息。因為我們修改了關聯控件的WindowProc值,因此要在把關聯改變到一個新的控件之前恢復它從前的取值。
避免把關聯控件的WindowProc屬性指向一個不再存在的例程也同樣重要。如同我們所見的,在析構器中調用SetAssociate(nil)將會把WindowProc恢復為初始值。
destructor TLinkedLabel.Destroy;
begin
SetAssociate(nil);
inherited;
end
另外,我們也不希望關聯到一個不再存在控件。通過覆蓋Notification方法,我們可以知道關聯組件何時被銷毀,從而重置關聯的指針:
procedure TLinkedLabel.Notification(AComponent: TComponent;
Operation: TOperation);
begin
if ((Operation = opRemove) and
(AComponent = FAssociate)) then
SetAssociate(nil);
end;
現在我們來看NewProc方法。這里,我們只是尋找發送給關聯控件的特定Windows消息。認識到這一點是很重要的:雖然方法通過關聯控件調用,但它實際上是LinkedLabel的一部分,例如,Self=LinkedLabel,而不是關聯控件。這對為一個按鈕創建onclick事件處理器來說也是一樣的,onclick事件處理器是作為按鈕父窗體的一部分,而不是擴充Tbutton類的新方法。
procedure TLinkedLabel.NewWinProc(var Message: TMessage);
var
Ch: Char;
begin
if (Assigned(FAssociate) and (not FUpdating)) then begin
FUpdating := True;
try
case(Message.Msg) of
WM_CHAR:
if (FCapsLock) then begin
Ch := Char(TWMKey(Message).CharCode);
if (Ch $#@62;= ’a’) and (Ch $#@60;= ’z’) then
TWMKey(Message).CharCode := ord(UpCase(Ch));
end;
CM_ENABLEDCHANGED:
Enabled := FAssociate.Enabled;
CM_VISIBLECHANGED:
Visible := FAssociate.Visible;
WM_SIZE, WM_MOVE, WM_WINDOWPOSCHANGED:
Adjust(True);
end;
finally
FUpdating := False;
end;
end;
FOldWinProc(Message);
end;
如果你檢查一下這個例程,就會發現我們并沒有花多少力氣去處理Windows消息。我們只注意幾個特定的消息,然后就讓關聯通過調用FOldWinProc正常的處理它們。在處理WM_CHAR消息的時候,我們對消息的一部分做了改變,讓控件認為我們按下的是大寫字母鍵。
最后,我們關心一下兩個不同的消息,以確定關聯控件是否被移動了。這樣做的原因在于從TwinControl繼承的控件會在它們被移動時接到WM_MOVE消息,而此時其它的可視控件(如一個標簽)則會收到WM_WINDOWPOSCHANGED消息。程序也檢查了WM_SIZE消息,原因是如果OnTop屬性為False,則LinkedLabel的位置會隨控件的高度而變化。
我們這個控件的最后一個方法是:當LinkedLabel被改變時,要在關聯的什么地方作修改?當然我們不使用覆蓋Tlabel的現存方法來實現它,而是要用修改關聯行為的相同技術來做。注意我們不是重新定向WindowsProc屬性,而是覆蓋了WndProc方法。為什么把它們叫做相同的技術呢?如果你看一下TControl的構造器,你可以發現WindowProc會被初始化以指向WndProc方法。所以從本質上講,我們覆蓋的是同一種方法,不過做得更“干凈”,也不用去保存WindowProc的初始值。
procedure TLinkedLabel.WndProc(var Message: TMessage);
begin
if (Assigned(FAssociate) and (not FUpdating)) then begin
FUpdating := True;
try
case(Message.Msg) of
CM_ENABLEDCHANGED: FAssociate.Enabled := Enabled;
CM_VISIBLECHANGED: FAssociate.Visible := Visible;
WM_WINDOWPOSCHANGED: Adjust(False);
end;
finally
FUpdating := False;
end;
end;
inherited;
end;
對于剛剛完成的控件還有最后一點需要注意。你也許發現NewWinProc和WndProc中都使用了Fupdating。這個變量被用來通知LinkedLabel和它的關聯控件其它控件正在發生改變。如果你忽略了這一步,很容易造成一個無限的更新循環,或者其它無法預料的結果。下面是一個事件流程,顯示為什么需要Fupdating變量。
用戶把 LinkedLabel 拖動到一個新位置。
WndProc 接收到一個 WM_WINDOWPOSCHANGED 消息,并且觸發 Adjust(False) 來移動關聯控件。
作為對關聯控件調整的一部分,Adjust 把FAssociate.Left設置為新值。
FAssociate 觸發 WM_MOVE 消息,指出它已經改變了位置。
NewWinProc 監測到 WM_MOVE 消息并調用 Adjust(True) 以修改 LinkedLabel 的位置配合關聯控件的移動。
如你所見,在關聯控件試圖移動LinkedLabel之前我們沒有什么機會改變關聯控件的Top屬性來配合LinkedLabel的新位置。通過使用Fupdating變量,關聯控件不會注意到WM_MOVE消息,也不會試圖調用Adjust來重新布置LinkedLabel。
一對問題
在這篇文章中我沒有提及TlinkedLabel的一對問題。下面是對它們的大致說明:
如果你把兩個或者兩個以上LinkedLabel關聯到同一個控件然后釋放它們之中的一個或者幾個,就可能導致各種各樣的問題。你可能會打斷到其它LinkedLabel的關聯,甚至可能導致被關聯控件的WindowProc指向一個并不存在的歷程。
如果你把 LinkedLabel 關聯到另一個窗體上的控件,那么Notification 方法在那個控件被銷毀時不會被調用。當控件被關聯時調用 FreeNotification 可以解決這個問題,但這并沒有真正指出問題所在。真正的問題在于我們允許它被關聯在其它窗體的控件上。其實我們真正想實現的是把LinkedLabel與擁有相同Parent的控件相關聯。雖然這么做并不難,不過要只在對象查看器的Associate屬性下拉列表中顯示符合條件的控件也需要一些小技巧。
結論
其實結論也沒多少東西。替換現存控件的WindowProc確實有它的局限性,不過這畢竟是一種非常有用的技術。我想不出什么其它合適的方法來創建一個像TlinkedLabel這樣的控件,讓關聯控件在被移動時也一并移動LinkedLabel。我可不想去嘗試并且列出這種技術其它可能的用法,因為這種可能性是無限的,它只會被一個程序員的靈活性所局限。
標簽:
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn