���¼���ԭʼ��ҳ��ӡ
視窗和控件閃爍解決方案_可下人間_新浪部落格
對于MFC程式員來說做UI開發是痛苦的事情,不過大多數情況下我們都需要做這件事情,因為MFC自帶的控件實在是太簡陋了。這時候我們多半會涉及到自繪控件,随之而來的很可能就是視窗和控件的閃爍問題。這篇文章希望對MFC的視窗和控件閃爍問題做一個盡量全面的總結。
一、閃爍的原因
引起閃爍的原因很多,以至于網上有n多種解決閃爍問題的方法;如果你按照某一種方法做了仍然沒有解決你的問題,請不要認定這個方法有問題,而是你沒有對上号。如果你對這個解釋不滿意的話,我們就來深究一下到底是什麼引起了閃爍。從原理上講,閃爍是因為螢幕上連續的兩次或多次輸出畫面差别比較大引起的,這是最根本的原因。是以如果視窗繪制差别不大,即使重新整理再頻繁,也不會引起閃爍。但是差别較大的畫面輸出一定會引起閃爍嗎?還有一個因素要考慮進來,就是螢幕的重新整理頻率。根據顯示卡和顯示器的不同,螢幕的重新整理周期是不一樣的,雖然這個參數的差别對界面開發的影響幾乎可以忽略,但是如果你真的從思想上了解了這一點,你就會立即明白為什麼雙緩沖技術能夠幫助我們解決一部分閃爍問題。
二、再談閃爍的原因
雖然第一部分的描述對我們有一些啟發,但我們還是應該更深入一些!哪些情況下會導緻我們的視窗或控件輸出連續的差别較大的繪制界面呢?
1、繪制界面太複雜,一個重新整理周期内繪制不完,每次都輸出一部分繪制結果,導緻幾次重新整理閃爍。
我們的繪制過程都是通過很多個繪制語句組成的,如果這些語句加起來的時間大于一個重新整理周期,那麼就很可能引起閃爍。通常的解決辦法是去掉中間過程的重新整理,直到最後整體繪制完畢再一次性重新整理。是不是似曾相識,這就是雙緩沖技術的原理!但是有些情況是雙緩沖也無能為力的,後面再講。
2、繪制過程很簡單,但是需要頻繁重新整理。
這種情況下我們首先需要弄清楚頻繁重新整理的原因是什麼,不同的原因對應不同的解決辦法。但是歸根結底,我們還是為了減少重新整理的次數或者盡量去掉中間輸出差别較大的繪制輸出。
3、重新整理過程。
對于視窗或控件的界面顯示,windows系統有一套繪制和重新整理的規則,繪制或重新整理的時機選擇也是影響閃爍的重要因素。如果再與上面兩條結合起來,某些情況下引起閃爍的原因确實非常複雜。隻有我們分析出問題所在,才能用正确的方法解決之。
三、幾種消除閃爍的解決方案
1、盡量減少重複繪制
MFC的視窗和控件重新整理有一套很複雜的規則,如果我們能深入了解,正确應用的話就能避免一部分閃爍。比如盡量用 InvalidateRect() 函數代替 Invalidate() 函數,InvalidateRect() 函數隻重新整理界面上指定的區域,如果我們的界面上隻有一小部分需要頻繁重新整理,那麼用這個函數代替 Invalidate() 的話,解決閃爍問題的效果是非常明顯的。這個函數已經封裝到MFC的CWnd類中(也有API函數)。
void InvalidateRect(LPCRECT lpRect, BOOL bErase = TRUE);
其中,lpRect指向一個方形區域,該區域将被添加到需要更新的區域清單中,bErase指定重新整理時是否更新區域背景。
如果我們需要重新整理的區域是不規則的,比如是幾個區域的組合,或者是某區域中去掉一部分,這時候用 InvalidateRect() 不能滿足我們的需求,我們可以用 InvalidateRgn() 函數。
void InvalidateRgn (CRgn* pRgn, BOOL bErase = TRUE);
其中,pRgn指向需要重新整理的區域。下面是一段示例代碼:
Crect rectClient;
CRgn rgn1, rgn2;
GetClientRect(rectClient);
rgn1.CreateRectRgnIndirect(rectClient);
rgn2.CreateRectRgnIndirect(m_rectButton);
rgn1.CombineRgn(&rgn1, &rgn2, RGN_XOR);
InvalidateRgn(&rgn1, FALSE);
有的時候我們的視窗上有很多控件,如果是由我們負責控件重新整理(比如視窗設定了WS_CLIPCHILDREN風格),我們最好判斷不同情況下确實需要重新整理的控件,而不是簡單的将所有控件全部重新整理一遍,以此将閃爍的影響減小到最小。
2、正确選擇視窗重繪時機
Windows有很多重新整理和重繪的函數,但是他們的特性和運作方式不盡相同,我們需要了解調用這些函數的注意事項,否則很可能因為實際情況跟我們的預期不同而引起閃爍。
Windows系統是通過WM_PAINT消息來通知界面重繪的,該消息一般由系統自動産生,比如當視窗被建立、改變大小、最大化、移動、覆寫等等,另外當UpdateWindow等函數被調用時也會産生WM_PAINT消息。
當視窗重繪時,并不一定整個視窗區域都需要重新整理,而隻是需要更新的那一部分,這部分區域叫做“無效區域”。系統在發現消息隊列空閑時會檢查無效區域,如果存在就會發送WM_PAINT消息進行重新整理。
Invalidate()、InvalidateRect()、InvalidateRgn()這些函數都隻是産生無效區域,而并沒有發送WM_PAINT消息,也就是說我們調用這些 Invalidate() 函數時,并不一定會使視窗立即重新整理,而是要等到下次WM_PAINT消息進入到消息隊列時才行。如果要使重繪立即執行,可以調用 UpdateWindow() 函數或者 RedrawWindow() 函數強制重新整理。
Windows的視窗重繪時,會首先判斷是否需要重新整理背景,如果需要則首先重新整理視窗背景,然後進入OnPaint()函數進行視窗内容的繪制。這個過程中如果操作不當,也有可能引起閃爍。當我們遇到閃爍問題,可以從以上視窗繪制機制中查找是否某些步驟的操作引起了閃爍。比如我們在對一個CListCtrl控件進行頻繁操作時(比如添加多個項或者修改内容),可以先調用 SetRedraw(FALSE),在操作全部完成後,再調用 SetRedraw(TURE) 完成一次性重新整理。
3、控制視窗背景重新整理
Windows視窗背景重新整理預設情況下是系統幫你完成的,如果我們的視窗繪制内容和背景差别比較大,或者在重新整理背景和重新整理視窗繪制之間有一個明顯的時間間隔,就有可能引起閃爍。
這個時候我們可能要禁止系統預設的背景繪制,而在視窗繪制函數中自行處理背景。這時隻要重載 OnEraseBkgnd() 函數,并直接傳回TRUE就可以了,代碼如下:
BOOL CMyWnd::OnEraseBkgnd(CDC* pDC)
{
return TRUE;
// return
CWnd::OnEraseBkgnd(pDC); // 注釋掉預設語句
}
4、雙緩沖
也許你已經聽說過雙緩沖這種方法了,的确,多數情況下雙緩沖能很好的解決我們的視窗閃爍問題,尤其是涉及到視窗自繪的時候。雙緩沖的基本原理是首先将複雜的繪制結果輸出到記憶體DC上,然後再一次性輸出到真正的視窗DC,這樣就避免了由于繪制時間占用多個重新整理周期,而導緻一次繪制引起短時間多次輸出産生閃爍。雙緩沖方法結合上一個方法,可以解決大部分自繪視窗的閃爍問題。具體的雙緩沖示例代碼如下:
void CMyWnd::OnPaint()
{
CPaintDC dc(this);
CRect rectClient;
GetClientRect(&rectClient);
CDC dcMem;
CBitmap bmpMem;
dcMem.CreateCompatibleDC(&dc);
bmpMem.CreateCompatibleBitmap(&dc, rectClient.Width(), rectClient.Height());
dcMem.SelectObject(&bmp);
// 此處将繪制内容輸出到dcMem上
// dcMem.FillRect(rectClient, &brush);
dc.BitBlt(0, 0, rectClient.Width(), rectClient.Height(), &dcMem, 0, 0, SRCCOPY);
bmpMem.DeleteObject();
dcMem.DeleteDC();
}
5、合理設定WS_CLIPCHILDREN和WS_CLIPSIBLINGS風格
當我們的視窗界面有多層視窗組成時(比如包含多個控件的對話框),用到自繪視窗可能會經常碰到閃爍問題。因為多層視窗會涉及到很多遮擋,重繪時一般涉及到主視窗和子視窗等多個視窗,而這些視窗的重新整理可能不會在一個重新整理周期内完成,進而引起閃爍。這時我們可以通過設定WS_CLIPCHILDREN和WS_CLIPSIBLINGS這兩個視窗風格來控制重新整理行為。
Clip是裁剪的意思,兩個屬性的具體含義如下:
帶有WS_CLIPCHILDREN風格表示裁剪掉子視窗的區域,即當該視窗重繪時,它的子視窗區域不重新整理,而留給子視窗自己去重新整理;
帶有WS_CLIPSIBLINGS風格(隻用于子視窗)表示裁剪掉兄弟視窗的區域,即當該視窗重繪時,與兄弟視窗重疊的區域将不會被重新整理。
根據這些視窗行為,我們就能優化我們的界面重新整理,控制一些視窗的重新整理時機,或者減少重疊區域的重複重新整理。比如當對話框視窗放置了大量控件時,我們可以給對話框加上WS_CLIPCHILDREN風格來阻止一些不必要的重新整理。
6、多層次視窗調整大小
如果視窗包含很多子視窗,當我們調整視窗大小時,可能要同時調整子視窗的位置和大小。此時若使用 MoveWindow() 或 SetWindowPos() 等函數進行調整,由于這些函數會等視窗重新整理完才傳回,是以當有大量子視窗時,這個過程肯定會引起閃爍。
這時我們可以應用 BeginDeferWindowPos(),
DeferWindowPos() 和 EndDeferWindowPos() 三個函數解決。首先調用 BeginDeferWindowPos(),設定需要調整的視窗個數;然後用 DeferWindowPos() 移動視窗(并非立即移動視窗);最後調用 EndDeferWindowPos() 一次性完成所有視窗的調整。
7、拖動和調整大小時的虛線框
當以上方法無效或者實作起來過于複雜,有沒有更統一更簡潔的方法呢?可能你曾經注意到Windows作業系統有這樣一種視覺效果(右擊我的電腦-> 屬性-> 進階-> 設定 -> 視覺效果-> 自定義,去掉“拖拉時顯示視窗内容”選項),當你拖動和調整視窗大小時,并不是即時顯示視窗内容,而是出現一個虛線框,當調整結束時才一次性繪制最終界面。這時一個非常好的防止閃爍的方法,我們來看看怎麼實作這種效果。
比較複雜的方法是自己畫虛線框,響應WM_MOVING消息畫虛線框,響應WM_MOVE消息繪制視窗内容,不過這個方法的難度可想而知,具體内容可以檢視這個讨論帖 javascript:void(0)。
有沒有簡單的方法呢?調用 SystemParametersInfo 這個API函數可以改變系統“拖拉時顯示視窗内容”項的設定,但是如果我們設定以後,系統其他視窗的行為也将被改變。其實我們隻要判斷什麼時候需要繪制虛線框,此時調用SystemParametersInfo(SPI_SETDRAGFULLWINDOWS,
FALSE, NULL,
SPIF_SENDWININICHANGE),然後在拖動完畢需要繪制的時候調用 SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, TRUE,
NULL,
SPIF_SENDWININICHANGE) 恢複設定就可以了。當然如果希望完全不影響系統原來的設定,我們隻要每次都先查詢一下系統原設定,然後恢複設定就可以了。
具體處理過程是在CDialog的OnNcLButtonDown消息響應函數中,當使用者點選對話框的非客戶區時該函數會被調用,而我們移動視窗或者調整視窗大小都是要點選非客戶區(标題欄或邊框)觸發該消息。拖動過程中的處理是在CDialog::OnNcLButtonDown(nHitTest,
point)中完成的,是以,我們隻要按如下代碼實作即可:
void CMyDlg::OnNcLButtonDown(UINT nHitTest, CPoint
point)
{
//
1,查詢目前系統“拖動顯示視窗内容”設定
SystemParametersInfo(SPI_GETDRAGFULLWINDOWS,
0, &m_bDragFullWindow,
NULL);
2,如果需要修改設定,則在每次進入CDialog::OnNcLButtonDown預設處理之前修改
if(m_bDragFullWindow)
SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, FALSE, NULL, NULL);
3,預設處理,系統會自動繪制虛框
CDialog::OnNcLButtonDown(nHitTest,
point);
������
http://blog.sina.com.cn/s/blog_48f93b530100jonm.html
�Ķ�ģʽ
— An Arc90 Laboratory
Experiment