程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> Delphi下QQ窗體自動隱藏探索

Delphi下QQ窗體自動隱藏探索

編輯:Delphi

騰訊QQ是當前流行的網絡聊天工具之一,由於它在應用設計上有很多獨特之處,所以也吸引了很多程序員對之進行研究和模仿。在這裡,我將利用Delphi對QQ的窗體自動隱藏效果提出自己的實現方法。

一、問題的提出

熟悉QQ使用的朋友都知道,當QQ窗體區域超出屏幕四邊時,窗體就會自動“消失”,只留下窗體一邊的小部分顯露在桌面上。當用鼠標移動到顯露部分之上,窗體就會在隱藏位置重新完整顯示;但當鼠標離開窗體區域後,窗體便會重新進入隱藏狀態。

對隱藏的全過程進行分析,可以得出兩點推測:第一,窗體隱藏的處理是與窗體移動過程有關;第二,窗體隱藏的觸發條件。

對第一點推測,可以通過對窗體移動時產生的Windows消息進行攔截處理加以實現。對第二點推測,如何去表示“窗體區域已經超出屏幕可視范圍”這一條件為實現的關鍵。

二、基本的分析

讓我們先留意一下Windows環境下窗體移動的過程與效果。當使用鼠標移動窗體的時候,窗體本身並沒有立刻隨鼠標的移動而發生位置的改變;相反,鼠標正在拖動的是一個大小與窗體一致的透明區域(確切的說一個虛線邊框的矩形),如圖一所示。當鼠標釋放矩形後,窗體本身才會在矩形最後停留的地方出現,從而完成整個移動的過程,如圖二所示。(注意:在Windows 2000及XP環境下,如果在顯示屬性中選中“拖動時顯示窗體內容”的顯示效果選項,則上述過程無法觀察。)

圖一 窗體移動過程 注意鼠標拖動的是一個矩形

圖二 窗體移動過程 拖動結束 窗體出現在矩形最後停留位置

對QQ窗體,其移動過程與上述無異,但卻有一處不同。當我們把矩形移動到屏幕四邊且已有部分超出時,矩形就會自動地停留在超出位置上並完整顯示。此時不論我們怎樣試圖把矩形再向超出方向上移動,矩形也只保持在該位置,如圖三所示。當釋放鼠標之後,窗體的隱藏效果也就出現了,如圖四所示。

圖三 QQ 窗體移動過程 矩形保持位置而不向超出方向移動

圖四 QQ 窗體移動過程 窗體在矩形保持位置處實現隱藏

從上述過程可以推斷,觸發隱藏條件後,即使仍處於移動過程但矩形本身卻已經被鎖定,因此對窗體位置的判斷是發生在移動過程中,也就是說我們要攔截處理的Windows消息是WM_MOVING。其次,在移動過程中首先發生位置變化的是矩形而不是窗體本身,因此實現隱藏的關鍵是對矩形參數的判斷與設置。

我們可以先留意一下WM_MOVING消息的語法結構:

WM_MOVING
WPARAM wParam
LPARAM lParam,

其中,WPARAM不被使用,而LPARAM則是一個指針,所指向的是一個RECT結構。RECT結構中包含了Left、Top、Right、Bottom四個參數,分別用於描述矩形的左上角與右下角,“該RECT記錄了窗體相對於屏幕的當前位置;當要改變拖動矩形的位置時,程序本身必須改變RECT結構中各成員變量的相關值”。由此可知,我們要處理的矩形其實已經在WM_MOVING消息中被提到,我們要處理的也就是LPARAM所指向的RECT結構的有關參數。

接下來我們要設置一個由隱藏條件激活的計時器,目的是監控鼠標相對窗體的位置。因為窗體隱藏後的隱現是靠鼠標激活的,所以若檢測到鼠標位於窗體之上,則說明窗體在顯示狀態;反之,窗體在隱藏狀態。我們只需在相關的判斷下加入對窗體Top和Left屬性的賦值即可實現隱現效果。

至此,有關自動隱藏效果的實現分析就基本完成了。不過還要注意一點,因為我們是在WM_MOVING消息的攔截處理中判斷隱藏條件,而通過計時器的OnTimer事件處理隱現效果。在此隱藏條件是否滿足在兩個過程中的傳遞將成為關鍵。同時我們要知道的不僅是隱藏條件是否滿足,還必須知道窗體是在屏幕的那一邊上發生隱藏。為此,我們需要定義一個集合去描述窗體隱藏的位置,例如:

type
  HidePosKind = (hpTop,hpLeft,hpBottom,hpRight);
type
  THidePos = set of HidePosKind;

不過,類似的集合在Delphi本身就已經存在,譬如TAnchors集合。TAnchors集合原來是用於指明一個控件如何錨定於其父類控件的位置,我們在這裡則借用來描述窗體對屏幕的隱藏位置。

在TAnchors集合中也包含了四個值,其定義如下:

type TAnchorKind = (akTop, akLeft, akRight, akBottom);
type TAnchors = set of TAnchorKind;

在代碼的實現中,我們將定義一個TAnchors類型的全局變量FAnchors去描述窗體隱藏的位置。

三、初步的實現

首先我們定義一個過程對WM_MOVING消息進行攔截處理,代碼如下:

..
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;

在該過程中,我們通過對矩形參數Left、Top、Right、Bottom的判斷確定窗體所處位置是否符合隱藏條件,判斷結果存放在全局變量Fanchors之中。當觸發隱藏時,在Fanchors中將至少有一個值而不多於兩個值。(為什麼呢?)

判斷條件的設置似乎和我們一般的理解有點不同。以Left參數的判斷為例,在判斷了Max(0, Left)之後還為什麼一定要與Screen.Width-Width 的值再作比較呢?這其實是為了對一些較為極端的情況(例如窗體的寬度大於屏幕寬度)所作的偽處理,大家如果有興趣的可自己試驗一下這些極端的效果。當然,如果我們的窗體限制了寬、高的最大值,那麼判斷也就可以簡化為我們最初的理解。

最後需要注意的是,代碼中出現的Left、Top、Right、Bottom都是RECT的參數,而Width和Height才是窗體Form1的屬性。

接下來我們要處理TTimer的OnTimer事件了。在WMMOVING過程中,當Fanchors不為空時,TTimer啟動;反之,TTimer關閉。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;

在這裡,我們首先定義一個常量cOffset去表示窗體隱藏後顯露部分的大小。然後我們利用WindowFromPoint這個Windows API函數檢測鼠標是否位於窗體之上。接下來的判斷就是處理在顯示和隱藏狀態下窗體Left 和Top 屬性值的設置。注意,針對Fanchors中存在不同值的情況,窗體Left和Top的設置是各不相同的,但是這些設置只有順序上的差異而並沒有優先級別的差異。(為什麼要提到這一點呢?)

最後需要注意的是:在本事件中Top、Left、Width和Height都是窗體Form1的屬性值。

好了,有關窗體隱藏的核心代碼已經介紹完畢了,不過要達到預期效果,窗體Form1在創建時還必須做一些准備工作,代碼如下:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Timer1.Enabled := False;
  Timer1.Interval := 200;
  FormStyle := fsStayOnTop;
end;

這裡的代碼相對簡單,不過值得指出的是對Form1的FormStyle 屬性的設置。FormStyle 為fsStayOnTop時可保證Form1始終位於最前顯示。從效果角度看,當系統工具欄為“總在最前顯示”時是最為明顯的,因為若窗體移動到系統工具欄上時也不會被其所遮蓋。

四、進一步完善

上面的代碼已經基本實現了窗體的自動隱藏效果,但是我在介紹代碼的時候有兩個問題是被提出但沒有被解答的。

首先是為什麼觸發隱藏時Fanchors中將至少有一個值而不多於兩個值呢?注意代碼中對Fanchors的賦值是通過四個判斷進行的, 那麼如果觸發隱藏的話,Fanchors中將毫無疑問會有一個值存在,但這種情況是針對隱藏發生在屏幕的四邊而言。當窗體被推入到屏幕的四角時,那麼Fanchors中便將會有兩個值存在。那此時窗體會隱藏到什麼地方呢?

實際的效果告訴我們,窗體會被隱藏到屏幕的四角上。此時若我們試圖讓窗體重新顯示,你便會發現窗體在不斷的閃爍。為什麼呢?這就是第二個問題提出的原因了。因為對窗體顯示或隱藏的處理是根據Fanchors中的值作出的。當Fanchors中有兩個值的時候,就將會引發對窗體屬性的兩次設置。而因為設置語句只有順序差異而沒有優先級差異,那麼OnTimer事件中每次都會對窗體進行兩次的屬性值設置,從而導致我們看到閃爍的顯示效果。

怎麼去解決這個問題呢?我們再觀察一下QQ的處理。在2003 II版的QQ裡面,窗體的隱藏效果作了一定的調整:當窗體在屏幕左右兩邊隱藏時,它會自動充滿屏幕的左右兩邊且高度不可改變;當窗體脫離屏幕兩邊的隱藏區域後,窗體的大小會恢復為隱藏前的大小,如圖五所示。(注意:窗體並非是完全充滿屏幕的兩邊。QQ在處理這個效果時可能只注意了系統工具欄總在最前顯示且位於屏幕下方的情況,所以其充滿的區域也只是屏幕頂端到系統工具欄上方的一段空間,如圖六所示。)這樣的處理可以令窗體即使被推入到屏幕四角,也可以保證只會對其中的一個隱藏方向進行處理,從而避免了前面出現的閃爍現象。

圖五 Q Q 窗體自動充滿屏幕兩邊

圖六 Q Q 窗體自動充滿屏幕兩邊的漏洞

結合前面的分析,要實現如上的效果還是從攔截WM_MOVING消息入手。重寫後的WMMOVING過程如下:

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;

在新的代碼中,我們首先使用了三個新定義的全局變量,分別是:

Lst_Height : Integer; //記錄窗體隱藏前的高度
Lst_Width : Integer; //記錄窗體隱藏前的寬度
Rec_Position : Boolean; //是否啟動窗體寬高記錄標志

然後加入了三個判斷代碼塊。

在第一個判斷中首先判定窗體在移動前是否位於屏幕左右兩邊的隱藏區域。若為真,則判斷窗體是否從隱藏區域向屏幕中央移動(注意,存在此判斷的原因是因為我們還可能將窗體往屏幕兩邊推動)。若再為真,則恢復窗體隱藏前的大小;反之,強制設置矩形的Top和Bottom值並退出消息的處理。

第二個判斷在於記錄窗體的寬高值。Rec_Position 是記錄窗體寬高的標志,它的值在第三個判斷中進行設置。若窗體在移動前位於屏幕兩邊的隱藏區域,則Rec_Position為True,此時窗體的高度已經固定,記錄已經無意義。所以只在Rec_Position為False時才需要記錄窗體的寬高。

第三個判斷位於Fanchors值設置之後。它根據窗體的位置對矩形的顯示效果進行判斷處理。判斷也是基於窗體是否位於屏幕兩邊進行,為True則設置矩形的高度並設置Rec_Position的值為True。

在第三個判斷中使用了一個新定義的過程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用於計算矩形高度,計算後的結果通過Cur_Top和Cur_Bottom兩個全局變量傳遞給矩形的Top和Bottom參數。

在該過程中使用了一個Windows API 函數SHAppBarMessage。SHAppBarMessage 的作用是向系統傳遞系統工具欄消息,其函數原型為:

WINSHELLAPI UINT APIENTRY SHAppBarMessage(DWORD dwMessage,PAPPBARDATA pData);

其中dwMessage 是發送給系統的工具欄消息; pData是指向PAPPBARDATA結構的指針,PAPPBARDATA結構返回的內容依據發出的消息而定。

在過程中,我們首先傳遞ABM_GETSTATE參數去獲取系統工具欄的狀態是自動隱藏還是總在最前顯示。

然後我們再利用ABM_GETTASKBARPOS參數去獲取系統工具欄的位置,此時AppBarData的返回值中將會是系統工具欄的位置ABE_TOP 、ABE_LEFT、ABE_RIGHT、ABE_BOTTOM四者之一。最後我們利用系統工具欄自身的拖動矩形參數計算出工具欄的高度。

使用了SetBarHeight令窗體在屏幕兩邊隨系統工具欄的位置和高度的改動而發生相應的變化。當然,你也可以直接給Cur_Top和Cur_Bottom這兩個變量設置固定值以實現QQ效果。在測試中,Cur_Top可以是1,而Cur_Bottom 則是Screen.Width-30(Windows系統工

具欄的高度在默認情況下是30,這是不隨分辨率改變的)。

由於要使窗體在屏幕兩邊的高度與位置可以隨系統工具欄的位置和高度的改動而發生相應的變化,因此OnTimer事件中的處理也要相應的改動,主要是顯示窗體的時候要注意對窗體Top和Height屬性的設置必須跟隨與系統工具欄的位置和高度相協調,代碼如下:

..
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;
..

最後,為了保證窗體在屏幕兩邊隱藏後高度保持不變,我們再添加一個WMSizing過程對WM_Sizing消息進行攔截處理。WMSizing過程的代碼如下:

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消息的語法結構與WM_MOVING消息相似,也包含了一個對矩形的指針。通過該指針我們可以對矩形的Top、Left、Right和Bottom參數進行設置,從而保證矩形高度不受用戶操作影響。

至此,一個窗體自動隱藏的程序就基本完成了,其實際效果已經和QQ相當接近了。運行效果如圖七至圖十所示。當然,從實際運行效果看還存在著一些小瑕疵,並且代碼中並沒有對窗體在隱藏後的寬度設置上進行處理,或者讀者可以考慮繼續進行完善此程序。

圖七 窗體向屏幕右方移動

圖八 窗體充滿屏幕右方

圖九 窗體位置隨工具欄位置變化

圖十 窗體離開屏幕右方恢復隱藏前大小

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved