原文地址:http://www.newxing.com/Tech/Program/VC/QQ_169.html
源代碼下載地址:http://www.newxing.com/Code/VC/jiemian/84.html
一、觀察
模仿前最重要的一步就是觀察,經過半天對QQ的擺弄和摸索,總結出了以下一些特點:
1、窗口開始粘附時,檢測的是鼠標座標與桌面邊界的距離,特別地,粘附在下面的時候,檢測的是與任務欄的距離;
2、在向上移動窗口時,窗口邊界永遠不會超出桌面上面邊界;
3、窗口是個 TopMost 風格;
4、當窗口粘附在上面、左邊或右邊並顯示時,你把鼠標移動到最頂端,光標變成改變窗口大小的圖標,而單單是把窗口的top座標設置爲0是不行的;
5、粘附在下面的時候,當處於移動狀態,那麼窗口的底邊是與任務欄頂邊對齊的,但從隱藏到顯示的時候,窗口的底端是與屏幕底邊對齊的;
6、隱藏後顯露出來的那條線可能是一個Border,但肯定的是絕不包含Client區域;
7、關於響應鼠標的進入與移出窗口,絕對不是WM_MOUSEMOVE、WM_MOUSELEAVE。證明:你以及其慢的速度接觸隱藏狀態的QQ邊界,你會發現幾乎是“一觸即發”,你又以及其慢的速度移出顯示狀態的QQ,你會發現它的收縮反而不是“一觸即發”的,而是離邊緣10象素左右。而WM_MOUSEMOVE,WM_MOUSELEAVE,只有在進入、移出Client區域才響應,明顯和QQ不同,其實從第6點也可以知道;
8、粘附在兩邊的時候,高度會調整爲桌面上邊界到任務欄下邊界的距離;
9、在“拖動時顯示窗口內容”模式下(桌面屬性-外觀-效果),粘附在兩邊的拖動出來時;如果收縮之前高度比收縮後小則回覆原來高度,在非“拖動時顯示窗口內容”模式下,光柵會回覆原來高度,但釋放左鍵時,高度卻是收縮時調整後的高度,一開始我以爲這是個BUG,但我編寫時同樣出現這個問題,發現這兩種模式會影響WM_MOVING參數的意義;
10、粘附在兩邊的時候當你設置任務欄自動隱藏,QQ窗口會自動調整高度充滿屏幕高度;
11、窗口顯示或隱藏不是一瞬間的,這點在第9點提到的兩種模式下,會有所不同;
12、任務欄並不顯示QQ窗口;
二、編寫代碼
觀察完畢,就開始編寫了。
首先新建一個基於對話框的MFC程序,命名爲QQHideWnd,在對話框屬性的styles頁把border改爲Resizing,你也可同時把Entended styles 的 tool window 鉤上,對於這點我在程序了動態修改了。
在QQHideWndDlg.h頭文件添加以下成員函數:
-
protected:
-
-
void FixMoving(UINT fwSide, LPRECT pRect);
-
-
void FixSizing(UINT fwSide, LPRECT pRect);
-
-
void DoShow();
-
-
void DoHide();
-
-
BOOL SetWindowPos(const CWnd* pWndInsertAfter,
-
LPCRECT pCRect, UINT nFlags = SWP_SHOWWINDOW);
繼續添加成員變量:
-
private:
-
BOOL m_isSizeChanged;
-
BOOL m_isSetTimer;
-
-
INT m_oldWndHeight;
-
INT m_taskBarHeight;
-
INT m_edgeHeight;
-
INT m_edgeWidth;
-
-
INT m_hideMode;
-
BOOL m_hsFinished;
-
BOOL m_hiding;
-
增加消息響應,需要注意的是有些消息你只有把右下角的 Filter for message設置爲window才能看到。
WM_ NCHITTEST
WM_MOVING
WM_CREATE
WM_TIMER
然後來到對應的cpp文件,在頭部定義一些宏:
-
-
#define HM_NONE 0 //不收縮
-
#define HM_TOP 1 //向上收縮
-
#define HM_BOTTOM 2 //向下收縮
-
#define HM_LEFT 3 //向左收縮
-
#define HM_RIGHT 4 //向右收縮
-
-
#define CM_ELAPSE 200 //檢測鼠標是否離開窗口的時間間隔
-
#define HS_ELAPSE 5 //隱藏或顯示過程每步的時間間隔
-
#define HS_STEPS 10 //隱藏或顯示過程分成多少步
-
-
#define INTERVAL 20 //觸發粘附時鼠標與屏幕邊界的最小間隔,單位爲象素
-
#define INFALTE 10 //觸發收縮時鼠標與窗口邊界的最小間隔,單位爲象素
-
#define MINCX 200 //窗口最小寬度
-
#define MINCY 400 //窗口最小高度
然後在構造函數初始化成員變量
-
m_isSizeChanged = FALSE;
-
m_isSetTimer = FALSE;
-
m_hsFinished = TRUE;
-
m_hiding = FALSE;
-
-
m_oldWndHeight = MINCY;
-
m_taskBarHeight = 30;
-
m_edgeHeight = 0;
-
m_edgeWidth =0;
-
m_hideMode = HM_NONE;
完成了一些初始的工作,那麼就開始進入關鍵的函數實現了。首先是在OnCreate做些窗口的初始化和獲得一些系統信息。
【代碼一】
-
int CQQHideWndDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
-
{
-
if (CDialog::OnCreate(lpCreateStruct) == -1)
-
return -1;
-
-
-
CWnd* p;
-
p = this->FindWindow("Shell_TrayWnd",NULL);
-
if(p != NULL)
-
{
-
CRect tRect;
-
p->GetWindowRect(tRect);
-
m_taskBarHeight = tRect.Height();
-
}
-
-
-
ModifyStyleEx(WS_EX_APPWINDOW, WS_EX_TOOLWINDOW);
-
-
-
-
-
m_edgeHeight = GetSystemMetrics(SM_CYEDGE);
-
m_edgeWidth = GetSystemMetrics(SM_CXFRAME);
-
-
return 0;
-
}
接着如何知道鼠標進入或移出窗口呢?在前面我已經證明了WM_MOUSEMOVE和WM_MOUSELEAVE不符合我們的要求,於是我用了WM_ NCHITTEST這個消息,你可以看到我在這個消息響應函數中用了兩個SetTimer,一個用於檢測鼠標是否離開,一個用於伸縮過程,不管你喜歡不喜歡,要達到第7點和第11點,這個是必須的,考慮的效率問題,在不需要的時候關閉這些Timer就好了。
【代碼二】
-
UINT CQQHideWndDlg::OnNcHitTest(CPoint point)
-
{
-
-
CString str;
-
str.Format("Mouse (%d,%d)",point.x,point.y);
-
GetDlgItem(IDC_CURSOR)->SetWindowText(str);
-
if(m_hideMode != HM_NONE && !m_isSetTimer &&
-
-
point.x < GetSystemMetrics(SM_CXSCREEN) + INFALTE)
-
{
-
SetTimer(1,CM_ELAPSE,NULL);
-
m_isSetTimer = TRUE;
-
-
m_hsFinished = FALSE;
-
m_hiding = FALSE;
-
SetTimer(2,HS_ELAPSE,NULL);
-
}
-
return CDialog::OnNcHitTest(point);
-
}
然後再OnTimer中:
【代碼三】
-
void CQQHideWndDlg::OnTimer(UINT nIDEvent)
-
{
-
-
if(nIDEvent == 1 )
-
{
-
POINT curPos;
-
GetCursorPos(&curPos);
-
-
CString str;
-
str.Format("Timer On(%d,%d)",curPos.x,curPos.y);
-
GetDlgItem(IDC_TIMER)->SetWindowText(str);
-
-
CRect tRect;
-
-
GetWindowRect(tRect);
-
-
tRect.InflateRect(INFALTE,INFALTE);
-
-
if(!tRect.PtInRect(curPos))
-
{
-
KillTimer(1);
-
m_isSetTimer = FALSE;
-
GetDlgItem(IDC_TIMER)->SetWindowText("Timer Off");
-
-
m_hsFinished = FALSE;
-
m_hiding = TRUE;
-
SetTimer(2,HS_ELAPSE,NULL);
-
}
-
}
-
-
if(nIDEvent == 2)
-
{
-
if(m_hsFinished)
-
KillTimer(2);
-
else
-
m_hiding ? DoHide() : DoShow();
-
}
-
CDialog::OnTimer(nIDEvent);
-
}
暫時不管OnTimer中的DoHide(); DoShow();
先來看看核心的函數之一的 FixMoving,該函數在OnMoving中被調用,FixMoving通過檢測鼠標位置和窗口位置來決定窗口的收縮模式,並修正粘附邊界時窗口的位置,從而達到像移動QQ時出現的效果。
【代碼四】
-
void CQQHideWndDlg::FixMoving(UINT fwSide, LPRECT pRect)
-
{
-
POINT curPos;
-
GetCursorPos(&curPos);
-
INT screenHeight = GetSystemMetrics(SM_CYSCREEN);
-
INT screenWidth = GetSystemMetrics(SM_CXSCREEN);
-
INT height = pRect->bottom - pRect->top;
-
INT width = pRect->right - pRect->left;
-
-
if (curPos.y <= INTERVAL)
-
{
-
pRect->bottom = height - m_edgeHeight;
-
pRect->top = -m_edgeHeight;
-
m_hideMode = HM_TOP;
-
}
-
else if(curPos.y >= (screenHeight - INTERVAL - m_taskBarHeight))
-
{
-
pRect->top = screenHeight - m_taskBarHeight - height;
-
pRect->bottom = screenHeight - m_taskBarHeight;
-
m_hideMode = HM_BOTTOM;
-
}
-
else if (curPos.x < INTERVAL)
-
{
-
if(!m_isSizeChanged)
-
{
-
CRect tRect;
-
GetWindowRect(tRect);
-
m_oldWndHeight = tRect.Height();
-
}
-
pRect->right = width;
-
pRect->left = 0;
-
pRect->top = -m_edgeHeight;
-
pRect->bottom = screenHeight - m_taskBarHeight;
-
m_isSizeChanged = TRUE;
-
m_hideMode = HM_LEFT;
-
}
-
else if(curPos.x >= (screenWidth - INTERVAL))
-
{
-
if(!m_isSizeChanged)
-
{
-
CRect tRect;
-
GetWindowRect(tRect);
-
m_oldWndHeight = tRect.Height();
-
}
-
pRect->left = screenWidth - width;
-
pRect->right = screenWidth;
-
pRect->top = -m_edgeHeight;
-
pRect->bottom = screenHeight - m_taskBarHeight;
-
m_isSizeChanged = TRUE;
-
m_hideMode = HM_RIGHT;
-
}
-
else
-
{
-
if(m_isSizeChanged)
-
{
-
-
pRect->bottom = pRect->top + m_oldWndHeight;
-
m_isSizeChanged = FALSE;
-
}
-
if(m_isSetTimer)
-
{
-
if(KillTimer(1) == 1)
-
m_isSetTimer = FALSE;
-
}
-
m_hideMode = HM_NONE;
-
GetDlgItem(IDC_TIMER)->SetWindowText("Timer off");
-
}
-
}
收縮模式和位置決定後,剩下的工作就由最後兩個核心函數完成了:實現收縮的DoHide(),實現伸展的DoShow()。在這兩個過程中m_hsFinished,m_hiding 這兩個變量起到很重要的控制作用。由於伸縮過程沒完成時,hsFinished始終爲FALSE,所以Timer 2 不會關閉,於是在OnTimer中會重複調用這兩個函數之一,在這兩個函數體內,窗口位置有規律地遞減或遞增就可以達到QQ的“抽屜”效果了,有趣的是即使伸縮過程還沒完成,你也可以在這個過程中改變m_hiding這個值來決定他是伸還是縮,正如QQ一樣。你可以把Timer
2 的事件間隔調大一點,然後在窗口伸縮時,鼠標來回地進出窗口就會很容易看到這樣有趣的效果(還沒縮進去又被拉了出來,或者還沒拉出來又縮進去了)。
【代碼五】
-
void CQQHideWndDlg::DoHide()
-
{
-
if(m_hideMode == HM_NONE)
-
return;
-
-
CRect tRect;
-
GetWindowRect(tRect);
-
-
INT height = tRect.Height();
-
INT width = tRect.Width();
-
-
INT steps = 0;
-
-
switch(m_hideMode)
-
{
-
case HM_TOP:
-
steps = height/HS_STEPS;
-
tRect.bottom -= steps;
-
if(tRect.bottom <= m_edgeWidth)
-
{
-
-
tRect.bottom = m_edgeWidth;
-
m_hsFinished = TRUE;
-
}
-
tRect.top = tRect.bottom - height;
-
break;
-
case HM_BOTTOM:
-
steps = height/HS_STEPS;
-
tRect.top += steps;
-
if(tRect.top >= (GetSystemMetrics(SM_CYSCREEN) - m_edgeWidth))
-
{
-
tRect.top = GetSystemMetrics(SM_CYSCREEN) - m_edgeWidth;
-
m_hsFinished = TRUE;
-
}
-
tRect.bottom = tRect.top + height;
-
break;
-
case HM_LEFT:
-
steps = width/HS_STEPS;
-
tRect.right -= steps;
-
if(tRect.right <= m_edgeWidth)
-
{
-
tRect.right = m_edgeWidth;
-
m_hsFinished = TRUE;
-
}
-
tRect.left = tRect.right - width;
-
tRect.top = -m_edgeHeight;
-
tRect.bottom = GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;
-
break;
-
case HM_RIGHT:
-
steps = width/HS_STEPS;
-
tRect.left += steps;
-
if(tRect.left >= (GetSystemMetrics(SM_CXSCREEN) - m_edgeWidth))
-
{
-
tRect.left = GetSystemMetrics(SM_CXSCREEN) - m_edgeWidth;
-
m_hsFinished = TRUE;
-
}
-
tRect.right = tRect.left + width;
-
tRect.top = -m_edgeHeight;
-
tRect.bottom = GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;
-
break;
-
default:
-
break;
-
}
-
-
SetWindowPos(&wndTopMost,tRect);
-
}
【代碼六】
-
void CQQHideWndDlg::DoShow()
-
{
-
if(m_hideMode == HM_NONE)
-
return;
-
-
CRect tRect;
-
GetWindowRect(tRect);
-
INT height = tRect.Height();
-
INT width = tRect.Width();
-
-
INT steps = 0;
-
-
switch(m_hideMode)
-
{
-
case HM_TOP:
-
steps = height/HS_STEPS;
-
tRect.top += steps;
-
if(tRect.top >= -m_edgeHeight)
-
{
-
-
tRect.top = -m_edgeHeight;
-
m_hsFinished = TRUE;
-
}
-
tRect.bottom = tRect.top + height;
-
break;
-
case HM_BOTTOM:
-
steps = height/HS_STEPS;
-
tRect.top -= steps;
-
if(tRect.top <= (GetSystemMetrics(SM_CYSCREEN) - height))
-
{
-
tRect.top = GetSystemMetrics(SM_CYSCREEN) - height;
-
m_hsFinished = TRUE;
-
}
-
tRect.bottom = tRect.top + height;
-
break;
-
case HM_LEFT:
-
steps = width/HS_STEPS;
-
tRect.right += steps;
-
if(tRect.right >= width)
-
{
-
tRect.right = width;
-
m_hsFinished = TRUE;
-
}
-
tRect.left = tRect.right - width;
-
tRect.top = -m_edgeHeight;
-
tRect.bottom = GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;
-
break;
-
case HM_RIGHT:
-
steps = width/HS_STEPS;
-
tRect.left -= steps;
-
if(tRect.left <= (GetSystemMetrics(SM_CXSCREEN) - width))
-
{
-
tRect.left = GetSystemMetrics(SM_CXSCREEN) - width;
-
m_hsFinished = TRUE;
-
}
-
tRect.right = tRect.left + width;
-
tRect.top = -m_edgeHeight;
-
tRect.bottom = GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;
-
break;
-
default:
-
break;
-
}
-
-
SetWindowPos(&wndTopMost,tRect);
-
}
-
-
BOOL CQQHideWndDlg::SetWindowPos(const CWnd* pWndInsertAfter, LPCRECT pCRect, UINT nFlags)
-
{
-
return CDialog::SetWindowPos(pWndInsertAfter,pCRect->left, pCRect->top,
-
pCRect->right - pCRect->left, pCRect->bottom - pCRect->top, nFlags);
-
}
到此,程序終於完成了。在我的源代碼中還有對WM_SIZING的處理和定義了與之相關的宏,這些主要是控制窗口在調整大小時不能超過最小的寬度和高度,與QQ的自動伸縮無關,所以不在這裏提及了。
三、結束語
雖然還不能算是完美的模仿,但效果已經非常非常的接近了。也許有人會奇怪爲什麼要用Tool Window 風格,這是因爲,這樣在任務欄中不會顯示窗口。從QQ的標題欄高度也可以判斷出他也是這種風格,但這樣一來就不能擁有最小化、最大化按鍵了。實際上QQ的最大化、最小化和關閉按鍵都是用DC畫上去的。如何在Caption上增加按鍵,外國一些開源網站有源代碼,我下載並看了一下,發現裏面有個知識點很有趣,那就是更改消息路由,有興趣的可以去下載來學習一下。