一個磁性表單VCL組件的具體實現
副標題:
作者:佚名 文章來源:大富翁 點擊數:52 更新時間:2005-2-25
文:陳達軍
日期:2003-10-29
記得《程式員》雜誌中曾有一篇文章介紹過如何?磁性表單,該篇文章總結了網上相關的討論文章,分析了實現磁性表單的基本原理,並最終給出了一個可行的解決方案。但文中介紹的具有磁性吸附能力的主表單只能是應用程式的主表單,不具有靈活性,且文中給出的範例程式碼是以C++ Bulider來實現的。現在我們就在Delphi中嘗試一下如何以VCL組件的形式來實現該功能,并力求在功能上做些延伸。文中疏漏之處,還請高手指教。
分析
討論之前我們先來定義兩個名詞。這裡所指的主表單是指具有磁性吸附作用的表單,而子表單是指被主表單吸附並能與主表單粘在一起的表單,子表單之間不具備吸附性。打個比方,磁鐵能吸附一個或多個鐵塊,而兩個鐵塊之間是不具有磁性作用的(如果鐵塊被磁化那就另當別論了)。下面我們就來分析一下實現磁性表單的大致過程。
一、子表單粘貼到主表單的過程
使用者在移動一個表單的過程中,表單會接收到什麼訊息呢?我們來看看MSDN上的一段說明:
The WM_MOVING message is sent to a window that the user is moving. By processing this message, an application can monitor the position of the drag rectangle and, if needed, change its position.
原來在表單移動的過程中,會不斷產生WM_MOVING標準的系統訊息!!!該訊息的LPARAM參數是一個指向TRect結構的指標,而TRect結構中存放著視窗當前位置矩形地區資訊。由此不難推斷出我們需要截獲視窗的WM_MOVING訊息,在Delphi中截獲視窗訊息的方法有很多種,這裡我們採用替換原視窗函數的方式,用自訂訊息處理函數來截獲此訊息。
再來看看MSDN上關於WM_MOVE訊息的解釋:
The WM_MOVE message is sent after a window has been moved.
看來WM_MOVE訊息也應當是我們的囊中之物,用來調整相關視窗的最終位置。
在自訂訊息處理函數中,我們不斷地檢測子表單與主視窗的距離,若小於了某個設定的值,則將該子表單粘貼到主表單。但如何獲知主表單呢?因為只有獲得了主表單,我們才能將兩者做位置地區比較,決定子表單是否能被吸附到主表單上。前面所說一文中採用了Application->MainForm的方式,這樣就限制了主表單只能是應用程式的主表單。我們採取的做法是:定義一個全域變數來儲存主表單所關聯的TMagnetServer對象,在建立第一個TMagnetServer對象時用該對象給此全域變數賦值,當然每個應用程式中也只能有一個TMagnetServer對象。
二、子表單從主表單移開的過程
子表單從主表單移開的過程與上述粘貼過程非常相似,具體實現時兩者可放在一起處理。在自訂訊息處理函數中,不斷地檢測子表單與主視窗的距離,若大於了某個設定的值,則將該子表單與主表單斷開。
三、主視窗關聯子表單移動的過程
主視窗關聯子表單移動即指在主表單移動的過程中所有粘貼到該主表單的子表單都跟著主表單的移動而移動,並保持相對位置不變。同理,主表單在移動的過程也會接收到WM_MOVING訊息,我們可以截獲此訊息,並逐一改變其所關聯的所有子表單的位置。
設計
Delphi作為RAD(Rapid Application Development)工具而深受廣大程式員的喜愛,豐富的VCL庫可謂功不可沒。由上述分析可知,我們定義了三個類來具體實現之,以適應"一拖即用"的組件化快速應用開發需要。
TMagnetForm 類作為TMagnetServer和TMagnetClient類的基類,主要實現替換原視窗函數,截獲特定視窗訊息的功能。因為磁性表單組件在運行時具有不可見度,所以該類直接從TComponent類繼承。注意:TMagnetForm類的DoMoving和DoMove函數我們都定義為了virtual、abstract類型,充分利用虛函數的優點,實現根據繼承類的不同而在運行時動態調用不同的函數。TMagnetForm類的相關說明如表1所示。
名 稱 類 型 說 明
OldWindowProc TWndMethod 預設訊息處理函數
DoMoving Procedure virtual abstract WM_MOVING訊息處理函數
DoMove Procedure virtual abstract WM_MOVE訊息處理函數
表1
TMagnetServer 類實現了磁性主表單具備的功能。Delphi中的TList類為我們存放子表單列表提供了一個很好的容器類。當有子表單粘貼到主表單時,就將該子表單的TMagnetClient對象添加到鏈表中,反之,則從鏈表中移除該對象。在主表單移動的過程,調用AdjustFormPos 函數,逐一調整子表單的位置。並且可以通過MagnetStyles 集合類型屬性來控制主表單的粘貼樣式。TMagnetServer類的相關說明如表2所示。
名 稱 類 型 說 明
FActive Boolean 決定主表單是否允許粘貼
FDistance Integer 產生磁性引力的距離
FForm TScrollingWinControl 關聯的視窗
FMagnetStyles TMangerStyles 主表單粘貼樣式
FMagnetClientList TList 關聯的子表單鏈表
AdjustFormPos procedure 調整關聯子表單位置
UnionOtherForm procedure 具體調整過程
DoMove procedure override 重載基類同名函數
DoMoving procedure override 重載基類同名函數
表2
TMagnetClient 類用來封裝磁性子表單的功能。FMagnetServer 成員變數用來儲存子表單關聯的主表單的TMagnetServer對象。TMagnetClient類的相關說明如表3所示。
名 稱 類 型 說 明
FEnabled Boolean 用來防止重複操作
FForm TScrollingWinControl 關聯的視窗
FMagnetServer TMagnetServer 粘貼到的主表單
FXPos Integer 子表單粘貼時最終Left位置
FYPos Integer 子表單粘貼時最終Top位置
AttachToForm procedure 粘貼到主表單或移開過程
DoMove procedure override 重載基類同名函數
DoMoving procedure override 重載基類同名函數
表3
編碼
經過了上面詳細的分析設計,磁性表單組件的具體實現過程並不困難。下面我們就摘取部分關鍵代碼來做一下說明,具體的實現過程請參考附例代碼。
var
MainServer: TMagnetServer; // 定義一個全域變數
// 定義新的表單訊息處理函數
procedure TMagnetForm.NewWndProc(var Message: TMessage);
begin
if Message.Msg = WM_MOVING then
begin
DoMoving(Message);
Message.Result := 1;
end;
if Message.Msg = WM_MOVE then
begin
DoMove(Message);
Message.Result := 1;
end;
OldWindowProc(Message);
end;
// TmagnetForm建構函式,確保每個表單只能擁有一個TMagnetServer或TMagnetClient組件
constructor TMagnetForm.Create(AOwner: TComponent);
var
nIndex: Integer;
begin
for nIndex := 0 to TScrollingWinControl(AOwner).ComponentCount - 1 do
begin
if (UpperCase(TScrollingWinControl(AOwner).Components[nIndex].ClassName) = 'TMAGNETCLIENT') or
(UpperCase(TScrollingWinControl(AOwner).Components[nIndex].ClassName) = 'TMAGNETSERVER') then
begin
Alert('該表單已經擁有一個 TMagnetServer 或 TMagnetClient 組件!');
Exit;
end;
end;
inherited Create(AOwner);
end;
Alert()函數是一個用來顯示警告資訊的自訂函數。定義如下:
// 顯示警告資訊
procedure Alert(const Msg: string);
begin
Application.MessageBox(PChar(Msg), PChar(Application.Title), MB_ICONWARNING or Mb_OK);
end;
// TMagnetServer類的建構函式
constructor TMagnetServer.Create(AOwner: TComponent);
begin
// 確保每個應用程式的TMagnetServer組件唯一
if Assigned(MainServer) then
begin
Alert('每個應用程式只能擁有一個 TMagnetServer 組件!');
Exit;
end;
inherited Create(AOwner);
// 初始化成員變數
FMagnetClientList := TList.Create;
FForm := AOwner as TScrollingWinControl;
FDistance := DEFAULT_DISTANCE;
FActive := True;
FMagnetStyles := [msTop, msBottom, msLeft, msRight];
MainServer := Self; // 將自己賦值給上面定義的 MainServer 全域變數
// 替換視窗訊息處理函數
if not(csDesigning in ComponentState) then
begin
OldWindowProc := FForm.WindowProc;
FForm.WindowProc := NewWndProc;
end;
end;
// 移動被粘貼在一起的其他表單
procedure TMagnetServer.AdjustFormPos(Rect: PRect);
var
Dx, Dy: Integer;
nIndex: Integer;
MagnetClient: TMagnetClient;
begin
if not Assigned(FForm) then Exit;
if not FActive then Exit;
Dx := Rect^.Left - FForm.Left;
Dy := Rect^.Top - FForm.Top;
FForm.Left := Rect^.Left;
FForm.Top := Rect^.Top;
// 逐一調整主表單關聯的所有子表單位置
for nIndex := 0 to FMagnetClientList.Count - 1 do
begin
MagnetClient := FMagnetClientList[nIndex];
UnionOtherForm(MagnetClient, Dx, Dy);
end;
end;
// 具體的調整子表單位置過程
procedure TMagnetServer.UnionOtherForm(MagnetClient: TMagnetClient; Dx, Dy: Integer);
var
Fx, Fy: Integer;
FormTemp: TScrollingWinControl;
Begin
if not Assigned(MagnetClient) then Exit;
FormTemp := MagnetClient.FForm;
if (MagnetClient.FEnabled) then
begin
MagnetClient.FEnabled := False;
Fx := FormTemp.Left;
Fy := FormTemp.Top;
SetWindowPos(FormTemp.Handle, FForm.Handle,
Fx + Dx, Fy + Dy,
FormTemp.Width, FormTemp.Height,
SWP_NOSIZE or SWP_NOACTIVATE);
MagnetClient.FEnabled := True;
end;
end;
// 將表單粘貼到主表單上或重新從主表單移開過程
procedure TMagnetClient.AttachToForm(MagnetServer: TMagnetServer; Rect: PRect; Distance: Integer);
var
RectServer: TRect;
IsPasted: Boolean;
begin
if not Assigned(FForm) then Exit;
if not Assigned(MainServer) then Exit;;
if not Assigned(MainServer.FForm) then Exit;
GetWindowRect(MainServer.FForm.Handle, RectServer);
FXPos := Rect^.Left;
FYPos := Rect^.Top;
IsPasted := False;
// 上下方向判斷
if (Mid(RectServer.Left, Rect^.Left, RectServer.Right) or Mid(Rect^.Left, RectServer.Left, Rect^.Right)) then
begin
if (DistanceIn(Rect^.Top, RectServer.Bottom, Distance) and (msBottom in MainServer.FMagnetStyles)) then
begin
// 下粘貼
FYPos := RectServer.Bottom;
IsPasted := True;
end
else if (DistanceIn(Rect^.Bottom, RectServer.Top, Distance) and (msTop in MainServer.FMagnetStyles)) then
begin
// 上粘貼
FYPos := RectServer.Top - (Rect^.Bottom - Rect^.Top);
IsPasted := True;
end;
end;
// 左右方向判斷
if (Mid(RectServer.Top, Rect^.Top, RectServer.Bottom) or Mid(Rect^.Top, RectServer.Top, Rect^.Bottom)) then
begin
if (DistanceIn(Rect^.Left, RectServer.Right, Distance) and (msRight in MainServer.FMagnetStyles)) then
begin
// 右粘貼
FXPos := RectServer.Right;
IsPasted := True;
end
else if (DistanceIn(Rect^.Right, RectServer.Left, Distance) and (msLeft in MainServer.FMagnetStyles)) then
begin
// 左粘貼
FXPos := RectServer.Left - (Rect^.Right - Rect^.Left);
IsPasted := True;
end;
end;
// 判斷最終是粘貼到主表單還是從主表單移開
if IsPasted and MainServer.Active then
begin
if not Assigned(FMagnetServer) then
begin
MainServer.FMagnetClientList.Add(Self);
FMagnetServer := MainServer;
end;
end else begin
if Assigned(FMagnetServer) then
begin
MainServer.FMagnetClientList.Remove(Self);
FMagnetServer := nil;
end;
end;
end;
// 將我們定義的TMagnetServer和TMagnetClient組件註冊到Delphi中
procedure Register;
begin
RegisterComponents('Magnet', [TMagnetServer, TMagnetClient]);
end;
通過了以上分析、設計和編碼的過程,我們完成了一個磁性視窗組件的製作,使用時只要在表單上放上一個相關磁性表單組件即可,將磁性表單組件化必將使應用開發時更加便捷。下面我們就來建立一個應用程式,看看效果如何。
測試
圖1
將我們剛才建立的磁性表單組件安裝到Delphi環境中,並建立一個應用程式,建立三個表單,分別放上一個TMagnetServer和TMagnetClient組件,並設定視窗為合適的大小,編譯器產生可執行檔,運行效果1所示。以上所有代碼在Delphi6下測試通過。