一些著名的共享軟件不但功能卓著,而且在程序界面的設計技巧上往往領導了一種時尚,WinAmp就是其中的一個代表。WinAmp有兩個絕活,一是可以更換窗體的外觀,也就是現在俗稱的給軟件換“皮膚”;另一個即是磁性窗體技巧。
磁性窗體即若干窗體靠近到一定距離以內時會互相粘在一起,或者說相互吸附在一起,然後在拖動主窗體時,粘在其上的其它窗體也一起跟著移動,好像變成了一個窗體。國內的MP3播放器新秀CDOK也實現了這種技巧,而且更絕,把幾個窗體粘在一起後,窗體沒有主從之分,拖動其中任意一個窗體都會使其它的窗體一起移動。在CSDN上有關怎樣設計磁性窗體的帖子非常多,說明這個技巧深得廣大程序員的青睐。
本文先把幾位網友的方法略加分析,然後給出我認為比較可行的實現方法和源代碼。
實現磁性窗體基本上分為兩步,第一步是實現當兩個窗體靠近到一定距離以內時實現窗體間的粘貼操作,第二步是移動窗體時,同時移動與它粘在一起的其它窗體。
實現窗體的粘貼
實現粘貼的難點在於什麼時候進行這個操作,假設有兩個窗體Form1和Form2,移動Form2向Form1靠近,當Form2與Form1的最近距離小於distance時粘貼在一起。顯然,應該在移動Form2的過程中進行判斷,問題是在程序的什麼位置插入判斷代碼呢?
CSDN上有人認為可以使用定時器,每隔一定的時間檢查各個窗體的位置。這種方法有著明顯的弊病,不說定時器要無謂地浪費系統資源,單單它的即時性就難以保證。如果縮短計時值,浪費的CPU資源就更多了,所以我也就不多說了。
合理的方法是利用系統產生的消息,但是利用什麼消息呢?窗體在移動時會產生WM_WINDOWPOSCHANGING和WM_MOVING消息,移動結束後會產生WM_WINDOWPOSCHANGED和WM_MOVE消息。WM_WINDOWPOSCHANGING和WM_WINDOWPOSCHANGED消息的參數lParam是結構WINDOWPOS的指針,WINDOWPOS定義如下:
typedef struct _WINDOWPOS {
HWND hwnd; // 窗口句炳
HWND hwndInsertAfter; // 窗口的Z順序
int x; // 窗口x坐標
int y; // 窗口的y坐標
int cx; // 窗口的寬度
int cy; // 窗口的高度
UINT flags; // 標志位,根據它設定窗口的位置
} WINDOWPOS;
可以看出,WM_WINDOWPOSCHANGED消息不僅僅在窗口移動時產生,而且在它的Z順序發生變化時也會產生,包括窗口的顯示和隱藏。所以我認為這個消息不是最佳選擇。
WM_MOVING和WM_MOVE消息的參數lParam是一個RECT結構指針,與WM_WINDOWPOSCHANGED消息相比較為單純,我采用的即是這個消息。下面我給出用C++ Builder寫的示例程序。
為了方便程序的閱讀,先定義了一個枚舉數據類型,表示窗體的粘貼狀態。同時定義了一個類,封裝了窗體粘貼相關的數據,其中的Enable是為了防止重復進行操作,方法是操作時設置Enable為否,操作結束時恢復為真,而在操作前檢查這個標志是否為否,否則直接返回。
圖2 窗體的粘貼狀態示例
// 窗體粘貼狀態,含義見圖2
enum enumAttachStyle
{
AS_NONE, // 沒有粘貼
AS_TOP,
AS_BOTTOM,
AS_T_TOP,
AS_LEFT,
AS_RIGHT,
AS_L_LEFT
};
// 處理窗體粘貼的類,為了簡化,采用了public聲明
class CFormAttachStyle
{
public:
bool Enabled; // 防止重復進行粘貼相關的操作
HWND AttachTo; // 被粘貼到哪個窗口
int XStyle; // 左右方向的粘貼狀態
int YStyle; // 上下方向的粘貼狀態
int xPos; // 粘貼到的x坐標
int yPos; // 粘貼到的y坐標
CFormAttachStyle() // 初使化數據
{
XStyle =AS_NONE;
YStyle =AS_NONE;
Enabled=true;
hAttachTo=NULL;
}
};
函數DistanceIn用於判斷兩個整數的距離是否在指定范圍內:
// 整數i1和i2的差的絕對值小於i3
bool DistanceIn(unsigned int i1,unsigned int i2,unsigned int i3)
{
if(i1>i2)
{ // 確保i2>=i1;
int t=i1;
i1=i2;
i2=t;
}
return i2-i1<=i3;
}
//---------------------------------------------------------------------------
// i1<=i2 bool Mid(unsigned int i1,unsigned int i2,unsigned int i3)
{
return ((i1<=i2) && (i2 }
//---------------------------------------------------------------------------
AttachToForm是處理窗體粘貼的關鍵函數,如果進行了粘貼,則保存粘貼到的窗體的句柄,並調整窗體的位置。在函數中使用了窗體的Tag屬性保存了一個CFormAttachStyle類的實例指針,原因將在稍後進行說明,參數distance表示可以進行粘貼的距離。窗口粘貼在上下、左右各有3種形式,都需要加以判斷。
// 把窗體My粘到主窗體上
bool AttachToForm(TForm *My, TForm *Form, RECT *r,int distance)
{
CFormAttachStyle *MyStyle=(CFormAttachStyle *)My->Tag;
if(MyStyle==NULL)return false; // 這個窗體不支持粘貼
//准備粘貼到的窗體的位置
RECT rMain;
GetWindowRect(Form->Handle,&rMain);
MyStyle->AttachTo=NULL;
MyStyle->yPos=r->top;
MyStyle->xPos=r->left;
// 上下方向判斷
MyStyle->YStyle=AS_NONE;
if( Mid(rMain.left,r->left,rMain.right)
|| Mid(r->left,rMain.left,r->right)
|| (MyStyle->XStyle!=AS_NONE))
{
if(DistanceIn(r->top,rMain.bottom,space))
{
MyStyle->YStyle=AS_BOTTOM;
MyStyle->yPos=rMain.bottom;
}else if(DistanceIn(r->top,rMain.top,space))
{
MyStyle->YStyle=AS_TOP;
MyStyle->yPos=rMain.top;
}else if(DistanceIn(r->bottom,rMain.top,space))
{
MyStyle->YStyle=AS_T_TOP;
MyStyle->yPos=rMain.top-(r->bottom-r->top);
}
}
// 左右方向判斷
MyStyle->XStyle=AS_NONE;
if( Mid(rMain.top,r->top,rMain.bottom)
|| Mid(r->top,rMain.top,r->bottom)
|| (MyStyle->YStyle!=AS_NONE))
{
if(DistanceIn(r->left,rMain.left,space))
{
MyStyle->XStyle=AS_LEFT;
MyStyle->xPos=rMain.left;
}else if(DistanceIn(r->left,rMain.right,space))
{
MyStyle->XStyle=AS_RIGHT;
MyStyle->xPos=rMain.right;
}else if(DistanceIn(r->right,rMain.left,space))
{
MyStyle->XStyle=AS_L_LEFT;
MyStyle->xPos=rMain.left-(r->right-r->left);
}
}
My->Left=MyStyle->xPos;
My->Top=MyStyle->yPos;
if(MyStyle->XStyle!=AS_NONE || MyStyle->YStyle!=AS_NONE)
{ // 粘貼成功
MyStyle->AttachTo= Form->Handle;
}
return bool(MyStyle->AttachTo);
}
函數Do_WM_MOVING在消息循環中處理WM_MOVING時調用,參數My為處理消息的窗體,Msg為消息參數。
// 處理WM_MOVING事件
void Do_WM_MOVING(TForm *My,TMessage &Msg)
{
CFormAttachStyle *MyStyle=(CFormAttachStyle *)My->Tag;
if(MyStyle && MyStyle->Enabled)
{
MyStyle->Enabled=false; // 防止重復操作
RECT *r=(RECT *)Msg.LParam ;
// 處理粘貼,這裡只對粘貼到主窗體進行判斷
TForm *FormApplication->MainForm;
AttachToForm(My,r,12); // 檢查是否可以粘貼窗體
MyStyle->Enabled=true; // 恢復操作狀態
}
Msg.Result=0; // 通知系統,消息已經處理
}
實現窗體的關聯移動
與處理窗體粘貼相比,關聯窗體的難度小一些。但是從CSDN上的帖子看,采用的方法都單調而且不佳,我都不推薦。
比較直觀的方法是使用窗體的MOUSEDOWN、MOUSEMOVE和MOUSEUP事件,先定義一個標志鼠標是否按下的變量:
bool bMouseDown;
在MOUSEDOWN事件中設置:
bMouseDown=true;
在MOUSEUP事件中設置:
bMouseDown=false;
在MOUSEMOVE事件中作如下處理:
if(bMouseDown)
{
// 移動當前窗體
……
// 計算窗體移動的位移
int dx;
int dy;
// 計算出dx和dy
……
// 移動其它粘貼到當前窗體的窗體
……
}
這個方法的最明顯的問題有兩個:1、鼠標在窗體上的控件上按下時,不能收到窗體的MOUSEDOWN和MOUSEUP事件,如果同時監控各個控件的事件,麻煩是相當大的。2、窗口標題欄的鼠標事件難以正常處理。
其實,同上一段落類似,處理窗體的WM_MOVING事件是比較好的方法。即在WM_MOVING事件中同步移動其它窗體。
移動其它窗體的方法也有多種,有人采用發送消息的方式,具體如下:
// dx和dy是當前窗體移動的距離
// hMove是要移動的窗體
// WM_MOVEFORM是自定義的消息
PostMessage(hMove, WM_MOVEFORM,dx,dy);
被移動的窗體處理WM_MOVEFORM消息時,移動自己到新的位置。
如果是VB、Delphi一類的語言,可以直接設置其Left和Top屬性。我采用的方法是使用API函數SetWindowPos,該函數重新設置指定窗口的位置。我的參考代碼如下:
// 移動被粘貼在一起的其它窗體
void UnionOtherForm(TForm *My,TForm *Form,int dx,int dy)
{
if(Form==NULL)return;
CFormAttachStyle *MyStyle=(CFormAttachStyle *)(Form->Tag);
if(MyStyle)
{
if(MyStyle->Enabled && MyStyle->AttachTo==My)
{
MyStyle->Enabled=false;
int X1=Form->Left;
int Y1=Form->Top;
SetWindowPos(Form->Handle,My->Handle,
X1+dx,Y1+dy,Form->Width,Form->Height,
SWP_NOSIZE|SWP_NOACTIVATE);
MyStyle->Enabled=true;
}
}
}
// 移動被粘貼在一起的其它窗體
void AdjuctFormPos(TForm *My,RECT *r)
{
// 調整窗口位置
int dy=r->top-My->Top;
int dx=r->left-My->Left;
My->Top=r->top;
My->Left=r->left;
// 逐一檢查創建的窗體
for(int i=0;iFormCount;i++)
{
TForm *Form=Screen->Forms[i];
if(Form!=My)
{
// 調整被吸附的窗口位置
UnionOtherForm(My,Form,dx,dy);
}
}
}
// 處理WM_MOVE事件
void Do_WM_MOVE(TForm *My,TMessage &Msg)
{
// 處理粘貼成功後的位置調整
CFormAttachStyle *MyStyle=(CFormAttachStyle *)My->Tag;
if(MyStyle && MyStyle->Enabled)
{
if(MyStyle->Enabled && MyStyle->AttachTo)
{ // 粘貼成功
My->Left=MyStyle->xPos;
My->Top=MyStyle->yPos;
}
}
Msg.Result=0; // 通知系統,消息已經處理
}
在這裡有一個C++ Builder編程的技巧,即使用Screen全局對象。如果在初使化需要使用粘貼功能的窗體時,把一個CFormAttachStyle實例的指針賦值給該窗體的Tag窗體,那麼除了處理它的WM_MOVING和WM_MOVE事件外,其它的操作都可以省略了。關鍵的代碼如下:
// 注:應把這個函數的聲明加到TForm1的類聲明中
void __fastcall TForm1::WndProc(TMessage &Msg)
{
TForm::WndProc(Msg);
switch(Msg.Msg)
{
case WM_MOVING: // 處理移動事件
{
Do_WM_MOVING(this,Msg);
break;
}
case WM_MOVE: // 處理移動事件
{
Do_WM_MOVE(this,Msg);
break;
}
}
}
void __fastcall TForm1::FormCreate(TObject *Sender)
{
// 建立磁性窗體特性類
CFormAttachStyle *AttachStyle=new CFormAttachStyle;
Tag=(int)AttachStyle;
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{ // 刪除CformAttachStyle實例
CFormAttachStyle *AttachStyle=(CFormAttachStyle *)Tag;
delete AttachStyle;
}
以下是主窗體處理WM_MOVING消息的代碼:
void __fastcall TfmMain::WndProc(TMessage &Msg)
{
TForm::WndProc(Msg);
switch(Msg.Msg)
{
case WM_MOVING: // 處理移動事件
{
AdjuctFormPos(this,(RECT *)(Msg.LParam));
break;
}
}
}
到此,實現磁性窗體的步驟基本上都介紹完了