VC 仿QQ窗口靠邊自動收縮隱藏效果

原文地址: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頭文件添加以下成員函數:

[cpp] view plain copy
  1. protected:  
  2.     //修正移動時窗口的大小  
  3.     void FixMoving(UINT fwSide, LPRECT pRect);  
  4.     //修正改改變窗口大小時窗口的大小  
  5.     void FixSizing(UINT fwSide, LPRECT pRect);  
  6.     //從收縮狀態顯示窗口  
  7.     void DoShow();  
  8.     //從顯示狀態收縮窗口  
  9.     void DoHide();  
  10.     //重載函數,只是爲了方便調用  
  11.     BOOL SetWindowPos(const CWnd* pWndInsertAfter,  
  12.                       LPCRECT pCRect, UINT nFlags = SWP_SHOWWINDOW);  

繼續添加成員變量:

[cpp] view plain copy
  1. private:  
  2.     BOOL m_isSizeChanged;   //窗口大小是否改變了   
  3.     BOOL m_isSetTimer;      //是否設置了檢測鼠標的Timer  
  4.       
  5.     INT  m_oldWndHeight;    //舊的窗口寬度  
  6.     INT  m_taskBarHeight;   //任務欄高度  
  7.     INT  m_edgeHeight;      //邊緣高度  
  8.     INT  m_edgeWidth;       //邊緣寬度  
  9.   
  10.     INT  m_hideMode;        //隱藏模式  
  11.     BOOL m_hsFinished;      //隱藏或顯示過程是否完成  
  12.         BOOL m_hiding;          //該參數只有在!m_hsFinished纔有效  
  13.                             //真:正在隱藏,假:正在顯示  

增加消息響應,需要注意的是有些消息你只有把右下角的 Filter for message設置爲window才能看到。
WM_ NCHITTEST
WM_MOVING
WM_CREATE
WM_TIMER

然後來到對應的cpp文件,在頭部定義一些宏:

[cpp] view plain copy
  1. //收縮模式  
  2. #define HM_NONE     0   //不收縮  
  3. #define HM_TOP      1   //向上收縮  
  4. #define HM_BOTTOM   2   //向下收縮  
  5. #define HM_LEFT     3   //向左收縮  
  6. #define HM_RIGHT    4   //向右收縮  
  7.   
  8. #define CM_ELAPSE   200 //檢測鼠標是否離開窗口的時間間隔  
  9. #define HS_ELAPSE   5   //隱藏或顯示過程每步的時間間隔  
  10. #define HS_STEPS    10  //隱藏或顯示過程分成多少步  
  11.   
  12. #define INTERVAL    20  //觸發粘附時鼠標與屏幕邊界的最小間隔,單位爲象素  
  13. #define INFALTE     10  //觸發收縮時鼠標與窗口邊界的最小間隔,單位爲象素  
  14. #define MINCX       200 //窗口最小寬度  
  15. #define MINCY       400 //窗口最小高度  

然後在構造函數初始化成員變量

[cpp] view plain copy
  1. m_isSizeChanged = FALSE;  
  2. m_isSetTimer = FALSE;  
  3. m_hsFinished = TRUE;  
  4.        m_hiding = FALSE;  
  5.   
  6. m_oldWndHeight = MINCY;  
  7. m_taskBarHeight = 30;  
  8. m_edgeHeight = 0;  
  9. m_edgeWidth  =0;  
  10. m_hideMode = HM_NONE;  

完成了一些初始的工作,那麼就開始進入關鍵的函數實現了。首先是在OnCreate做些窗口的初始化和獲得一些系統信息。
【代碼一】

[cpp] view plain copy
  1. int CQQHideWndDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)  
  2. {  
  3. if (CDialog::OnCreate(lpCreateStruct) == -1)  
  4. return -1;  
  5.   
  6. // TODO: Add your specialized creation code here //獲得任務欄高度  
  7. CWnd* p;  
  8. p = this->FindWindow("Shell_TrayWnd",NULL);  
  9. if(p != NULL)  
  10. {  
  11. CRect tRect;  
  12. p->GetWindowRect(tRect);  
  13. m_taskBarHeight = tRect.Height();  
  14. }  
  15.   
  16. //修改風格使得他不在任務欄顯示  
  17. ModifyStyleEx(WS_EX_APPWINDOW, WS_EX_TOOLWINDOW);  
  18. //去掉關閉按鍵(如果想畫3個按鍵的話)  
  19. //ModifyStyle(WS_SYSMENU,NULL);  
  20.   
  21. //獲得邊緣高度和寬度  
  22. m_edgeHeight = GetSystemMetrics(SM_CYEDGE);  
  23. m_edgeWidth = GetSystemMetrics(SM_CXFRAME);  
  24.   
  25. return 0;  
  26. }  

接着如何知道鼠標進入或移出窗口呢?在前面我已經證明了WM_MOUSEMOVE和WM_MOUSELEAVE不符合我們的要求,於是我用了WM_ NCHITTEST這個消息,你可以看到我在這個消息響應函數中用了兩個SetTimer,一個用於檢測鼠標是否離開,一個用於伸縮過程,不管你喜歡不喜歡,要達到第7點和第11點,這個是必須的,考慮的效率問題,在不需要的時候關閉這些Timer就好了。

【代碼二】

[cpp] view plain copy
  1. UINT CQQHideWndDlg::OnNcHitTest(CPoint point)  
  2. {  
  3. // TODO: Add your message handler code here and/or call default  
  4. CString str;  
  5. str.Format("Mouse (%d,%d)",point.x,point.y);  
  6. GetDlgItem(IDC_CURSOR)->SetWindowText(str);  
  7. if(m_hideMode != HM_NONE && !m_isSetTimer &&  
  8. //防止鼠標超出屏幕右邊時向右邊收縮造成閃爍  
  9. point.x < GetSystemMetrics(SM_CXSCREEN) + INFALTE)  
  10. //鼠標進入時,如果是從收縮狀態到顯示狀態則開啓Timer  
  11. SetTimer(1,CM_ELAPSE,NULL);  
  12. m_isSetTimer = TRUE;  
  13.   
  14. m_hsFinished = FALSE;  
  15. m_hiding = FALSE;  
  16. SetTimer(2,HS_ELAPSE,NULL); //開啓顯示過程  
  17. }  
  18. return CDialog::OnNcHitTest(point);  
  19. }  

然後再OnTimer中:

【代碼三】

[cpp] view plain copy
  1. void CQQHideWndDlg::OnTimer(UINT nIDEvent)  
  2. {  
  3. // TODO: Add your message handler code here and/or call default  
  4. if(nIDEvent == 1 )  
  5. {  
  6. POINT curPos;  
  7. GetCursorPos(&curPos);  
  8.   
  9. CString str;  
  10. str.Format("Timer On(%d,%d)",curPos.x,curPos.y);  
  11. GetDlgItem(IDC_TIMER)->SetWindowText(str);  
  12.   
  13. CRect tRect;  
  14. //獲取此時窗口大小  
  15. GetWindowRect(tRect);  
  16. //膨脹tRect,以達到鼠標離開窗口邊沿一定距離才觸發事件  
  17. tRect.InflateRect(INFALTE,INFALTE);  
  18.   
  19. if(!tRect.PtInRect(curPos)) //如果鼠標離開了這個區域  
  20. {  
  21. KillTimer(1); //關閉檢測鼠標Timer  
  22. m_isSetTimer = FALSE;  
  23. GetDlgItem(IDC_TIMER)->SetWindowText("Timer Off");  
  24.   
  25. m_hsFinished = FALSE;  
  26. m_hiding = TRUE;  
  27. SetTimer(2,HS_ELAPSE,NULL); //開啓收縮過程  
  28. }  
  29. }  
  30.   
  31. if(nIDEvent == 2)  
  32. {  
  33. if(m_hsFinished) //如果收縮或顯示過程完畢則關閉Timer  
  34. KillTimer(2);  
  35. else  
  36. m_hiding ? DoHide() : DoShow();  
  37. }  
  38. CDialog::OnTimer(nIDEvent);  
  39. }  


暫時不管OnTimer中的DoHide(); DoShow();

先來看看核心的函數之一的 FixMoving,該函數在OnMoving中被調用,FixMoving通過檢測鼠標位置和窗口位置來決定窗口的收縮模式,並修正粘附邊界時窗口的位置,從而達到像移動QQ時出現的效果。

【代碼四】

[cpp] view plain copy
  1. void CQQHideWndDlg::FixMoving(UINT fwSide, LPRECT pRect)  
  2. {  
  3. POINT curPos;  
  4. GetCursorPos(&curPos);  
  5. INT screenHeight = GetSystemMetrics(SM_CYSCREEN);  
  6. INT screenWidth = GetSystemMetrics(SM_CXSCREEN);  
  7. INT height = pRect->bottom - pRect->top;  
  8. INT width = pRect->right - pRect->left;  
  9.   
  10. if (curPos.y <= INTERVAL)  
  11. //粘附在上邊  
  12. pRect->bottom = height - m_edgeHeight;  
  13. pRect->top = -m_edgeHeight;  
  14. m_hideMode = HM_TOP;  
  15. }  
  16. else if(curPos.y >= (screenHeight - INTERVAL - m_taskBarHeight))  
  17. //粘附在下邊  
  18. pRect->top = screenHeight - m_taskBarHeight - height;  
  19. pRect->bottom = screenHeight - m_taskBarHeight;  
  20. m_hideMode = HM_BOTTOM;  
  21. }  
  22. else if (curPos.x < INTERVAL)  
  23. //粘附在左邊  
  24. if(!m_isSizeChanged)  
  25. {  
  26. CRect tRect;  
  27. GetWindowRect(tRect);  
  28. m_oldWndHeight = tRect.Height();  
  29. }  
  30. pRect->right = width;  
  31. pRect->left = 0;  
  32. pRect->top = -m_edgeHeight;  
  33. pRect->bottom = screenHeight - m_taskBarHeight;  
  34. m_isSizeChanged = TRUE;  
  35. m_hideMode = HM_LEFT;  
  36. }  
  37. else if(curPos.x >= (screenWidth - INTERVAL))  
  38. //粘附在右邊  
  39. if(!m_isSizeChanged)  
  40. {  
  41. CRect tRect;  
  42. GetWindowRect(tRect);  
  43. m_oldWndHeight = tRect.Height();  
  44. }  
  45. pRect->left = screenWidth - width;  
  46. pRect->right = screenWidth;  
  47. pRect->top = -m_edgeHeight;  
  48. pRect->bottom = screenHeight - m_taskBarHeight;  
  49. m_isSizeChanged = TRUE;  
  50. m_hideMode = HM_RIGHT;  
  51. }  
  52. else  
  53. //不粘附  
  54. if(m_isSizeChanged)  
  55. //如果收縮到兩邊,則拖出來後會變回原來大小  
  56. //在"拖動不顯示窗口內容下"只有光柵變回原來大小  
  57. pRect->bottom = pRect->top + m_oldWndHeight;  
  58. m_isSizeChanged = FALSE;  
  59. }  
  60. if(m_isSetTimer)  
  61. //如果Timer開啓了,則關閉之  
  62. if(KillTimer(1) == 1)  
  63. m_isSetTimer = FALSE;  
  64. }  
  65. m_hideMode = HM_NONE;  
  66. GetDlgItem(IDC_TIMER)->SetWindowText("Timer off");  
  67. }  
  68. }  

收縮模式和位置決定後,剩下的工作就由最後兩個核心函數完成了:實現收縮的DoHide(),實現伸展的DoShow()。在這兩個過程中m_hsFinished,m_hiding 這兩個變量起到很重要的控制作用。由於伸縮過程沒完成時,hsFinished始終爲FALSE,所以Timer 2 不會關閉,於是在OnTimer中會重複調用這兩個函數之一,在這兩個函數體內,窗口位置有規律地遞減或遞增就可以達到QQ的“抽屜”效果了,有趣的是即使伸縮過程還沒完成,你也可以在這個過程中改變m_hiding這個值來決定他是伸還是縮,正如QQ一樣。你可以把Timer 2 的事件間隔調大一點,然後在窗口伸縮時,鼠標來回地進出窗口就會很容易看到這樣有趣的效果(還沒縮進去又被拉了出來,或者還沒拉出來又縮進去了)。

【代碼五】

[cpp] view plain copy
  1. void CQQHideWndDlg::DoHide()  
  2. {  
  3. if(m_hideMode == HM_NONE)  
  4. return;  
  5.   
  6. CRect tRect;  
  7. GetWindowRect(tRect);  
  8.   
  9. INT height = tRect.Height();  
  10. INT width = tRect.Width();  
  11.   
  12. INT steps = 0;  
  13.   
  14. switch(m_hideMode)  
  15. {  
  16. case HM_TOP:  
  17. steps = height/HS_STEPS;  
  18. tRect.bottom -= steps;  
  19. if(tRect.bottom <= m_edgeWidth)  
  20. //你可以把下面一句替換上面的 ...+=|-=steps 達到取消抽屜效果  
  21. //更好的辦法是添加個BOOL值來控制,其他case同樣.  
  22. tRect.bottom = m_edgeWidth;  
  23. m_hsFinished = TRUE; //完成隱藏過程  
  24. }  
  25. tRect.top = tRect.bottom - height;  
  26. break;  
  27. case HM_BOTTOM:  
  28. steps = height/HS_STEPS;  
  29. tRect.top += steps;  
  30. if(tRect.top >= (GetSystemMetrics(SM_CYSCREEN) - m_edgeWidth))  
  31. {  
  32. tRect.top = GetSystemMetrics(SM_CYSCREEN) - m_edgeWidth;  
  33. m_hsFinished = TRUE;  
  34. }  
  35. tRect.bottom = tRect.top + height;  
  36. break;  
  37. case HM_LEFT:  
  38. steps = width/HS_STEPS;  
  39. tRect.right -= steps;  
  40. if(tRect.right <= m_edgeWidth)  
  41. {  
  42. tRect.right = m_edgeWidth;  
  43. m_hsFinished = TRUE;  
  44. }  
  45. tRect.left = tRect.right - width;  
  46. tRect.top = -m_edgeHeight;  
  47. tRect.bottom = GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;  
  48. break;  
  49. case HM_RIGHT:  
  50. steps = width/HS_STEPS;  
  51. tRect.left += steps;  
  52. if(tRect.left >= (GetSystemMetrics(SM_CXSCREEN) - m_edgeWidth))  
  53. {  
  54. tRect.left = GetSystemMetrics(SM_CXSCREEN) - m_edgeWidth;  
  55. m_hsFinished = TRUE;  
  56. }  
  57. tRect.right = tRect.left + width;  
  58. tRect.top = -m_edgeHeight;  
  59. tRect.bottom = GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;  
  60. break;  
  61. default:  
  62. break;  
  63. }  
  64.   
  65. SetWindowPos(&wndTopMost,tRect);  
  66. }  

【代碼六】

[cpp] view plain copy
  1. void CQQHideWndDlg::DoShow()  
  2. {  
  3. if(m_hideMode == HM_NONE)  
  4. return;  
  5.   
  6. CRect tRect;  
  7. GetWindowRect(tRect);  
  8. INT height = tRect.Height();  
  9. INT width = tRect.Width();  
  10.   
  11. INT steps = 0;  
  12.   
  13. switch(m_hideMode)  
  14. {  
  15. case HM_TOP:  
  16. steps = height/HS_STEPS;  
  17. tRect.top += steps;  
  18. if(tRect.top >= -m_edgeHeight)  
  19. //你可以把下面一句替換上面的 ...+=|-=steps 達到取消抽屜效果  
  20. //更好的辦法是添加個BOOL值來控制,其他case同樣.  
  21. tRect.top = -m_edgeHeight;  
  22. m_hsFinished = TRUE; //完成顯示過程  
  23. }  
  24. tRect.bottom = tRect.top + height;  
  25. break;  
  26. case HM_BOTTOM:  
  27. steps = height/HS_STEPS;  
  28. tRect.top -= steps;  
  29. if(tRect.top <= (GetSystemMetrics(SM_CYSCREEN) - height))  
  30. {  
  31. tRect.top = GetSystemMetrics(SM_CYSCREEN) - height;  
  32. m_hsFinished = TRUE;  
  33. }  
  34. tRect.bottom = tRect.top + height;  
  35. break;  
  36. case HM_LEFT:  
  37. steps = width/HS_STEPS;  
  38. tRect.right += steps;  
  39. if(tRect.right >= width)  
  40. {  
  41. tRect.right = width;  
  42. m_hsFinished = TRUE;  
  43. }  
  44. tRect.left = tRect.right - width;  
  45. tRect.top = -m_edgeHeight;  
  46. tRect.bottom = GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;  
  47. break;  
  48. case HM_RIGHT:  
  49. steps = width/HS_STEPS;  
  50. tRect.left -= steps;  
  51. if(tRect.left <= (GetSystemMetrics(SM_CXSCREEN) - width))  
  52. {  
  53. tRect.left = GetSystemMetrics(SM_CXSCREEN) - width;  
  54. m_hsFinished = TRUE;  
  55. }  
  56. tRect.right = tRect.left + width;  
  57. tRect.top = -m_edgeHeight;  
  58. tRect.bottom = GetSystemMetrics(SM_CYSCREEN) - m_taskBarHeight;  
  59. break;  
  60. default:  
  61. break;  
  62. }  
  63.   
  64. SetWindowPos(&wndTopMost,tRect);  
  65. }  
  66.   
  67. BOOL CQQHideWndDlg::SetWindowPos(const CWnd* pWndInsertAfter, LPCRECT pCRect, UINT nFlags)  
  68. {  
  69. return CDialog::SetWindowPos(pWndInsertAfter,pCRect->left, pCRect->top,  
  70. pCRect->right - pCRect->left, pCRect->bottom - pCRect->top, nFlags);  
  71. }  

到此,程序終於完成了。在我的源代碼中還有對WM_SIZING的處理和定義了與之相關的宏,這些主要是控制窗口在調整大小時不能超過最小的寬度和高度,與QQ的自動伸縮無關,所以不在這裏提及了。

三、結束語

雖然還不能算是完美的模仿,但效果已經非常非常的接近了。也許有人會奇怪爲什麼要用Tool Window 風格,這是因爲,這樣在任務欄中不會顯示窗口。從QQ的標題欄高度也可以判斷出他也是這種風格,但這樣一來就不能擁有最小化、最大化按鍵了。實際上QQ的最大化、最小化和關閉按鍵都是用DC畫上去的。如何在Caption上增加按鍵,外國一些開源網站有源代碼,我下載並看了一下,發現裏面有個知識點很有趣,那就是更改消息路由,有興趣的可以去下載來學習一下。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章