騰訊QQ是當(dāng)前流行的網(wǎng)絡(luò)聊天工具之一,由于它在應(yīng)用設(shè)計(jì)上有很多獨(dú)特之處,所以也吸引了很多程序員對(duì)之進(jìn)行研究和模仿。在這里,我將利用Delphi對(duì)QQ的窗體自動(dòng)隱藏效果提出自己的實(shí)現(xiàn)方法。
一、問(wèn)題的提出
熟悉QQ使用的朋友都知道,當(dāng)QQ窗體區(qū)域超出屏幕四邊時(shí),窗體就會(huì)自動(dòng)“消失”,只留下窗體一邊的小部分顯露在桌面上。當(dāng)用鼠標(biāo)移動(dòng)到顯露部分之上,窗體就會(huì)在隱藏位置重新完整顯示;但當(dāng)鼠標(biāo)離開(kāi)窗體區(qū)域后,窗體便會(huì)重新進(jìn)入隱藏狀態(tài)。
對(duì)隱藏的全過(guò)程進(jìn)行分析,可以得出兩點(diǎn)推測(cè):第一,窗體隱藏的處理是與窗體移動(dòng)過(guò)程有關(guān);第二,窗體隱藏的觸發(fā)條件是窗體的區(qū)域已經(jīng)移動(dòng)到屏幕的可視范圍之外。
對(duì)第一點(diǎn)推測(cè),可以通過(guò)對(duì)窗體移動(dòng)時(shí)產(chǎn)生的Windows消息進(jìn)行攔截處理加以實(shí)現(xiàn)。對(duì)第二點(diǎn)推測(cè),如何去表示“窗體區(qū)域已經(jīng)超出屏幕可視范圍”這一條件成為實(shí)現(xiàn)的關(guān)鍵。
二、基本的分析
讓我們先留意一下Windows環(huán)境下窗體移動(dòng)的過(guò)程與效果。當(dāng)使用鼠標(biāo)移動(dòng)窗體的時(shí)候,窗體本身并沒(méi)有立刻隨鼠標(biāo)的移動(dòng)而發(fā)生位置的改變;相反,鼠標(biāo)正在拖動(dòng)的是一個(gè)大小與窗體一致的透明區(qū)域(確切的說(shuō)一個(gè)虛線邊框的矩形)。當(dāng)鼠標(biāo)釋放矩形后,窗體本身才會(huì)在矩形最后停留的地方出現(xiàn),從而完成整個(gè)移動(dòng)的過(guò)程。(注意:在Windows 2000及XP環(huán)境下,如果在顯示屬性中選中“拖動(dòng)時(shí)顯示窗體內(nèi)容”的顯示效果選項(xiàng),則上述過(guò)程無(wú)法觀察。)
對(duì)QQ窗體,其移動(dòng)過(guò)程與上述無(wú)異,但卻有一處不同。當(dāng)我們把矩形移動(dòng)到屏幕四邊且已有部分超出時(shí),矩形就會(huì)自動(dòng)地停留在超出位置上并完整顯示。此時(shí)不論我們?cè)鯓釉噲D把矩形再向超出方向上移動(dòng),矩形也只保持在該位置。當(dāng)釋放鼠標(biāo)之后,窗體的隱藏效果也就出現(xiàn)了。
從上述過(guò)程可以推斷,觸發(fā)隱藏條件后,即使仍處于移動(dòng)過(guò)程但矩形本身卻已經(jīng)被鎖定,因此對(duì)窗體位置的判斷是發(fā)生在移動(dòng)過(guò)程中,也就是說(shuō)我們要攔截處理的Windows消息是WM_MOVING。其次,在移動(dòng)過(guò)程中首先發(fā)生位置變化的是矩形而不是窗體本身,因此實(shí)現(xiàn)隱藏的關(guān)鍵是對(duì)矩形參數(shù)的判斷與設(shè)置。
我們可以先留意一下WM_MOVING消息的語(yǔ)法結(jié)構(gòu):
WM_MOVING
WPARAM wParam
LPARAM lParam,
其中,WPARAM不被使用,而LPARAM則是一個(gè)指針,所指向的是一個(gè)RECT結(jié)構(gòu)。RECT結(jié)構(gòu)中包含了Left、Top、Right、Bottom四個(gè)參數(shù),分別用于描述矩形的左上角與右下角,“該RECT記錄了窗體相對(duì)于屏幕的當(dāng)前位置;當(dāng)要改變拖動(dòng)矩形的位置時(shí),程序本身必須改變RECT結(jié)構(gòu)中各成員變量的相關(guān)值”。由此可知,我們要處理的矩形其實(shí)已經(jīng)在WM_MOVING消息中被提到,我們要處理的也就是LPARAM所指向的RECT結(jié)構(gòu)的有關(guān)參數(shù)。
接下來(lái)我們要設(shè)置一個(gè)由隱藏條件激活的計(jì)時(shí)器,目的是監(jiān)控鼠標(biāo)相對(duì)窗體的位置。因?yàn)榇绑w隱藏后的隱現(xiàn)是靠鼠標(biāo)激活的,所以若檢測(cè)到鼠標(biāo)位于窗體之上,則說(shuō)明窗體在顯示狀態(tài);反之,窗體在隱藏狀態(tài)。我們只需在相關(guān)的判斷下加入對(duì)窗體Top和Left屬性的賦值即可實(shí)現(xiàn)隱現(xiàn)效果。
至此,有關(guān)自動(dòng)隱藏效果的實(shí)現(xiàn)分析就基本完成了。不過(guò)還要注意一點(diǎn),因?yàn)槲覀兪窃赪M_MOVING消息的攔截處理中判斷隱藏條件,而通過(guò)計(jì)時(shí)器的OnTimer事件處理隱現(xiàn)效果。在此隱藏條件是否滿足在兩個(gè)過(guò)程中的傳遞將成為關(guān)鍵。同時(shí)我們要知道的不僅是隱藏條件是否滿足,還必須知道窗體是在屏幕的那一邊上發(fā)生隱藏。為此,我們需要定義一個(gè)集合去描述窗體隱藏的位置,例如:
type
HidePosKind = (hpTop,hpLeft,hpBottom,hpRight);
type
THidePos = set of HidePosKind;
不過(guò),類似的集合在Delphi本身就已經(jīng)存在,譬如TAnchors集合。TAnchors集合原來(lái)是用于指明一個(gè)控件如何錨定于其父類控件的位置,我們?cè)谶@里則借用來(lái)描述窗體對(duì)屏幕的隱藏位置。
在TAnchors集合中也包含了四個(gè)值,其定義如下:
type TAnchorKind = (akTop, akLeft, akRight, akBottom);
type TAnchors = set of TAnchorKind;
在代碼的實(shí)現(xiàn)中,我們將定義一個(gè)TAnchors類型的全局變量FAnchors去描述窗體隱藏的位置。
三、初步的實(shí)現(xiàn)
首先我們定義一個(gè)過(guò)程對(duì)WM_MOVING消息進(jìn)行攔截處理,代碼如下:
……
private
FAnchors: TAnchors;
procedure WMMOVING(var Msg: TMessage); message WM_MOVING;
……
uses Math,type;
procedure TForm1.WMMOVING(var Msg: TMessage);
begin
inherited;
with PRect(Msg.LParam)^ do
begin
Left := Min(Max(0, Left), Screen.Width - Width);
Top := Min(Max(0, Top), Screen.Height - Height);
Right := Min(Max(Width, Right), Screen.Width);
Bottom := Min(Max(Height, Bottom), Screen.Height);
FAnchors := [];
if Left = 0 then Include(FAnchors, akLeft);
if Right = Screen.Width then Include(FAnchors, akRight);
if Top = 0 then Include(FAnchors, akTop);
if Bottom = Screen.Height then Include(FAnchors, akBottom);
Timer1.Enabled := FAnchors <> [];
end;
end;
在該過(guò)程中,我們通過(guò)對(duì)矩形參數(shù)Left、Top、Right、Bottom的判斷確定窗體所處位置是否符合隱藏條件,判斷結(jié)果存放在全局變量Fanchors之中。當(dāng)觸發(fā)隱藏時(shí),在Fanchors中將至少有一個(gè)值而不多于兩個(gè)值。(為什么呢?)
判斷條件的設(shè)置似乎和我們一般的理解有點(diǎn)不同。以Left參數(shù)的判斷為例,在判斷了Max(0, Left)之后還為什么一定要與Screen.Width – Width的值再作比較呢?這其實(shí)是為了對(duì)一些較為極端的情況(例如窗體的寬度大于屏幕寬度)所作的偽處理,大家如果有興趣的可自己試驗(yàn)一下這些極端的效果。當(dāng)然,如果我們的窗體限制了寬、高的最大值,那么判斷也就可以簡(jiǎn)化為我們最初的理解。
最后需要注意的是,代碼中出現(xiàn)的Left、Top、Right、Bottom都是RECT的參數(shù),而Width和Height才是窗體Form1的屬性。
接下來(lái)我們要處理TTimer的OnTimer事件了。在WMMOVING過(guò)程中,當(dāng)Fanchors不為空時(shí),TTimer啟動(dòng);反之,TTimer關(guān)閉。OnTimer事件的代碼如下:
procedure TForm1.Timer1Timer(Sender: TObject);
const
cOffset = 2;
begin
if WindowFromPoint(Mouse.CursorPos) = Handle then
begin
if akLeft in FAnchors then Left := 0;
if akTop in FAnchors then Top := 0;
if akRight in FAnchors then Left := Screen.Width - Width;
if akBottom in FAnchors then Top := Screen.Height - Height;
end else
begin
if akLeft in FAnchors then Left := -Width + cOffset;
if akTop in FAnchors then Top := -Height + cOffset;
if akRight in FAnchors then Left := Screen.Width - cOffset;
if akBottom in FAnchors then Top := Screen.Height - cOffset;
end;
end;
在這里,我們首先定義一個(gè)常量cOffset去表示窗體隱藏后顯露部分的大小。然后我們利用WindowFromPoint這個(gè)Windows API函數(shù)檢測(cè)鼠標(biāo)是否位于窗體之上。接下來(lái)的判斷就是處理在顯示和隱藏狀態(tài)下窗體Left和Top屬性值的設(shè)置。注意,針對(duì)Fanchors中存在不同值的情況,窗體Left和Top的設(shè)置是各不相同的,但是這些設(shè)置只有順序上的差異而并沒(méi)有優(yōu)先級(jí)別的差異。(為什么要提到這一點(diǎn)呢?)
最后需要注意的是:在本事件中Top、Left、Width和Height都是窗體Form1的屬性值。
好了,有關(guān)窗體隱藏的核心代碼已經(jīng)介紹完畢了,不過(guò)要達(dá)到預(yù)期效果,窗體Form1在創(chuàng)建時(shí)還必須做一些準(zhǔn)備工作,代碼如下:
procedure TForm1.FormCreate(Sender: TObject);
begin
Timer1.Enabled := False;
Timer1.Interval := 200;
FormStyle := fsStayOnTop;
end;
這里的代碼相對(duì)簡(jiǎn)單,不過(guò)值得指出的是對(duì)Form1的FormStyle屬性的設(shè)置。FormStyle為fsStayOnTop時(shí)可保證了Form1始終位于最前顯示。從效果角度看,當(dāng)系統(tǒng)工具欄為“總在最前顯示”時(shí)是最為明顯的,因?yàn)槿舸绑w移動(dòng)到系統(tǒng)工具欄上時(shí)也不會(huì)被其所遮蓋。
四、進(jìn)一步完善
上面的代碼已經(jīng)基本實(shí)現(xiàn)了窗體的自動(dòng)隱藏效果,但是我在介紹代碼的時(shí)候有兩個(gè)問(wèn)題是被提出但沒(méi)有被解答的。
首先是為什么觸發(fā)隱藏時(shí)Fanchors中將至少有一個(gè)值而不多于兩個(gè)值呢?注意代碼中對(duì)Fanchors的賦值是通過(guò)四個(gè)判斷進(jìn)行的,那么如果觸發(fā)隱藏的話,F(xiàn)anchors中將毫無(wú)疑問(wèn)會(huì)有一個(gè)值存在,但這種情況是針對(duì)隱藏發(fā)生在屏幕的四邊而言。當(dāng)窗體被推入到屏幕的四角時(shí),那么Fanchors中便將會(huì)有兩個(gè)值存在。那此時(shí)窗體會(huì)隱藏到什么地方呢?
實(shí)際的效果告訴我們,窗體會(huì)被隱藏到屏幕的四角上。此時(shí)若我們?cè)噲D讓窗體重新顯示,你便會(huì)發(fā)現(xiàn)窗體在不斷的閃爍。為什么呢?這就是第二個(gè)問(wèn)題提出的原因了。因?yàn)閷?duì)窗體顯示或隱藏的處理是根據(jù)Fanchors中的值作出的。當(dāng)Fanchors中有兩個(gè)值的時(shí)候,就將會(huì)引發(fā)對(duì)窗體屬性的兩次設(shè)置。而因?yàn)樵O(shè)置語(yǔ)句只有順序差異而沒(méi)有優(yōu)先級(jí)差異,那么OnTimer事件中每次都會(huì)對(duì)窗體進(jìn)行兩次的屬性值設(shè)置,從而導(dǎo)致我們看到閃爍的顯示效果。
怎么去解決這個(gè)問(wèn)題呢?我們?cè)儆^察一下QQ的處理。在2003 II版的QQ里面,窗體的隱藏效果作了一定的調(diào)整:當(dāng)窗體在屏幕左右兩邊隱藏時(shí),它會(huì)自動(dòng)充滿屏幕的左右兩邊且高度不可改變;當(dāng)窗體脫離屏幕兩邊的隱藏區(qū)域后,窗體的大小會(huì)恢復(fù)為隱藏前的大小。(注意:窗體并非是完全充滿屏幕的兩邊。QQ在處理這個(gè)效果時(shí)可能只注意了系統(tǒng)工具欄總在最前顯示且位于屏幕下方的情況,所以其充滿的區(qū)域也只是屏幕頂端到系統(tǒng)工具欄上方的一段空間。)這樣的處理可以令窗體即使被推入到屏幕四角,也可以保證只會(huì)對(duì)其中的一個(gè)隱藏方向進(jìn)行處理,從而避免了前面出現(xiàn)的閃爍現(xiàn)象。
結(jié)合前面的分析,要實(shí)現(xiàn)如上的效果還是從攔截WM_MOVING消息入手。重寫(xiě)后的WMMOVING過(guò)程如下:
procedure TForm1.WMMOVING(var Msg: TMessage);
begin
inherited;
with PRect(Msg.LParam)^ do
begin
if (akLeft in FAnchors) or (akRight in FAnchors) then
begin
if (Left > 0) and (Right < Screen.Width) then
begin
if rec_Position then
begin
Bottom := top + Lst_Height;
Right := Left + Lst_Width;
Height := Lst_Height;
Width := Lst_Width;
end;
end else
begin
SetBarHeight;
Top := Cur_Top;
Bottom := Cur_Bottom;
exit;
end;
end;
Left := Min(Max(0, Left), Screen.Width - Width);
……
if not Rec_Position then
begin
Lst_Height := form1.Height;
Lst_Width := form1.width;
end;
FAnchors := [];
……
if (akLeft in FAnchors) or (akRight in FAnchors) then
begin
Rec_Position := True;
SetBarHeight;
Top := Cur_Top;
Bottom := Cur_Bottom;
end else
Rec_Position := False;
Timer1.Enabled := FAnchors <> [];
end;
end;
在新的代碼中,我們首先使用了三個(gè)新定義的全局變量,分別是:
Lst_Height : Integer; //記錄窗體隱藏前的高度
Lst_Width : Integer; //記錄窗體隱藏前的寬度
Rec_Position : Boolean; //是否啟動(dòng)窗體寬高記錄標(biāo)志
然后加入了三個(gè)判斷代碼塊。
在第一個(gè)判斷中首先判定窗體在移動(dòng)前是否位于屏幕左右兩邊的隱藏區(qū)域。若為真,則判斷窗體是否從隱藏區(qū)域向屏幕中央移動(dòng)(注意,存在此判斷的原因是因?yàn)槲覀冞可能將窗體往屏幕兩邊推動(dòng))。若再為真,則恢復(fù)窗體隱藏前的大。环粗,強(qiáng)制設(shè)置矩形的Top和Bottom值并退出消息的處理。
第二個(gè)判斷在于記錄窗體的寬高值。Rec_Position是記錄窗體寬高的標(biāo)志,它的值在第三個(gè)判斷中進(jìn)行設(shè)置。若窗體在移動(dòng)前位于屏幕兩邊的隱藏區(qū)域,則Rec_Position為True,此時(shí)窗體的高度已經(jīng)固定,記錄已經(jīng)無(wú)意義。所以只在Rec_Position為False時(shí)才需要記錄窗體的寬高。
第三個(gè)判斷位于Fanchors值設(shè)置之后。它根據(jù)窗體的位置對(duì)矩形的顯示效果進(jìn)行判斷處理。判斷也是基于窗體是否位于屏幕兩邊進(jìn)行,為True則設(shè)置矩形的高度并設(shè)置Rec_Position的值為True。
在第三個(gè)判斷中使用了一個(gè)新定義的過(guò)程SetBarHeight,其代碼如下:
procedure TForm1.SetBarHeight;
var
AppBarData : TAPPBARDATA;
begin
AppBarData.cbSize := SIZEOF(AppBarData);
If SHAppBarMessage(ABM_GETSTATE,AppBarData) AND ABS_AUTOHIDE) <> 0 then
begin
Cur_Top := 1;
Cur_Bottom := Screen.Height - 1;
end else
begin
SHAppBarMessage(ABM_GETTASKBARPOS,AppBarData);
case AppBarData.uEdge of
ABE_TOP : begin
Cur_Top := AppBarData.rc.Bottom + 1;
Cur_Bottom := Screen.Height - 1;
end;
ABE_LEFT : begin
Cur_Top := 1;
Cur_Bottom := Screen.Height - 1;
end;
ABE_RIGHT : begin
Cur_Top := 1;
Cur_Bottom := Screen.Height - 1;
end;
ABE_BOTTOM : begin
Cur_Top := 1;
Cur_Bottom:=Screen.Height -(AppBarData.rc.Bottom - AppBarData.rc.Top) - 1;
end;
end;
end;
end;
SetBarHeight用于計(jì)算矩形高度,計(jì)算后的結(jié)果通過(guò)Cur_Top和Cur_Bottom兩個(gè)全局變量給傳遞矩形的Top和Bottom參數(shù)。
在該過(guò)程中使用了一個(gè)Windows API函數(shù)SHAppBarMessage。SHAppBarMessage的作用是向系統(tǒng)傳遞系統(tǒng)工具欄消息,其函數(shù)原型為:
WINSHELLAPI UINT APIENTRY SHAppBarMessage(DWORD dwMessage, PAPPBARDATA pData);
其中dwMessage是發(fā)送給系統(tǒng)的工具欄消息;pData是指向PAPPBARDATA結(jié)構(gòu)的指針,PAPPBARDATA結(jié)構(gòu)返回的內(nèi)容依據(jù)發(fā)出的消息而定。
在過(guò)程中,我們首先傳遞ABM_GETSTATE參數(shù)去獲取系統(tǒng)工具欄的狀態(tài)是自動(dòng)隱藏還是總在最前顯示。然后我們?cè)倮肁BM_GETTASKBARPOS參數(shù)去獲取系統(tǒng)工具欄的位置,此時(shí)AppBarData的返回值中將會(huì)是系統(tǒng)工具欄的位置ABE_TOP、ABE_LEFT、ABE_RIGHT、ABE_BOTTOM四者之一。最后我們利用系統(tǒng)工具欄自身的拖動(dòng)矩形參數(shù)計(jì)算出工具欄的高度。
使用了SetBarHeight令窗體在屏幕兩邊隨系統(tǒng)工具欄的位置和高度的改動(dòng)而發(fā)生相應(yīng)的變化。當(dāng)然,你也可以直接給Cur_Top和Cur_Bottom這兩個(gè)變量設(shè)置固定值以實(shí)現(xiàn)QQ效果。在測(cè)試中,Cur_Top可以是1,而Cur_Bottom則是Screen.Width – 30(Windows系統(tǒng)工具欄的高度在默認(rèn)情況下是30,這是不隨分辨率改變的)。
由于要使窗體在屏幕兩邊的高度與位置可以隨系統(tǒng)工具欄的位置和高度的改動(dòng)而發(fā)生相應(yīng)的變化,因此OnTimer事件中的處理也要相應(yīng)的改動(dòng),主要是顯示窗體的時(shí)候要注意對(duì)窗體Top和Height屬性的設(shè)置必須跟隨與系統(tǒng)工具欄的位置和高度相協(xié)調(diào),代碼如下:
……
if akLeft in FAnchors then
begin
Left := -Width + cOffset;
SetBarHeight;
Top := Cur_Top;
Height := Cur_Bottom;
end;
if akRight in FAnchors then
begin
Left := Screen.Width - cOffset;
SetBarHeight;
Top := Cur_Top;
Height := Cur_Bottom;
end;
……
最后,為了保證窗體在屏幕兩邊隱藏后高度保持不變,我們?cè)偬砑右粋(gè)WMSizing過(guò)程對(duì)WM_Sizing消息進(jìn)行攔截處理。WMSizing過(guò)程的代碼如下:
procedure TForm1.WMSizing(var Msg: TMessage);
begin
inherited;
if (akRight in FAnchors) then
begin
with PRect(Msg.LParam)^ do
begin
Left := Screen.Width - Width;
Top := Cur_Top;
Right := Screen.Width;
Bottom := Cur_Bottom
end;
end else if (akLeft in FAnchors) then
begin
with PRect(Msg.LParam)^ do
begin
Left := 0;
Top := Cur_Top;
Right := Width;
Bottom := Cur_Bottom;
end;
end;
end;
WM_Sizing消息的語(yǔ)法結(jié)構(gòu)與WM_MOVING消息相似,也包含了一個(gè)對(duì)矩形的指針。通過(guò)該指針我們可以對(duì)矩形的Top、Left、Right和Bottom參數(shù)進(jìn)行設(shè)置,從而保證矩形高度不受用戶操作影響。
至此,一個(gè)窗體自動(dòng)隱藏的程序就基本完成了,其實(shí)際效果已經(jīng)和QQ相當(dāng)接近了。當(dāng)然,從實(shí)際運(yùn)行效果看還存在著一些小瑕疵,并且代碼中并沒(méi)有對(duì)窗體在隱藏后的寬度設(shè)置上進(jìn)行處理,或者大家可以考慮繼續(xù)進(jìn)行完善此程序。
