1. 程式人生 > >視窗和控制元件閃爍解決方案

視窗和控制元件閃爍解決方案

對於MFC程式設計師來說做UI開發是痛苦的事情,不過大多數情況下我們都需要做這件事情,因為MFC自帶的控制元件實在是太簡陋了。這時候我們多半會涉及到自繪控制元件,隨之而來的很可能就是視窗和控制元件的閃爍問題。這篇文章希望對MFC的視窗和控制元件閃爍問題做一個儘量全面的總結。

    一、閃爍的原因

    引起閃爍的原因很多,以至於網上有n多種解決閃爍問題的方法;如果你按照某一種方法做了仍然沒有解決你的問題,請不要認定這個方法有問題,而是你沒有對上號。如果你對這個解釋不滿意的話,我們就來深究一下到底是什麼引起了閃爍。從原理上講,閃爍是因為螢幕上連續的兩次或多次輸出畫面差別比較大引起的,這是最根本的原因。因此如果視窗繪製差別不大,即使重新整理再頻繁,也不會引起閃爍。但是差別較大的畫面輸出一定會引起閃爍嗎?還有一個因素要考慮進來,就是螢幕的重新整理頻率。根據顯示卡和顯示器的不同,螢幕的重新整理週期是不一樣的,雖然這個引數的差別對介面開發的影響幾乎可以忽略,但是如果你真的從思想上理解了這一點,你就會立即明白為什麼雙緩衝技術能夠幫助我們解決一部分閃爍問題。

    二、再談閃爍的原因

    雖然第一部分的描述對我們有一些啟發,但我們還是應該更深入一些!哪些情況下會導致我們的視窗或控制元件輸出連續的差別較大的繪製介面呢?

    1、繪製介面太複雜,一個重新整理週期內繪製不完,每次都輸出一部分繪製結果,導致幾次重新整理閃爍。

    我們的繪製過程都是通過很多個繪製語句組成的,如果這些語句加起來的時間大於一個重新整理週期,那麼就很可能引起閃爍。通常的解決辦法是去掉中間過程的重新整理,直到最後整體繪製完畢再一次性重新整理。是不是似曾相識,這就是雙緩衝技術的原理!但是有些情況是雙緩衝也無能為力的,後面再講。

    2、繪製過程很簡單,但是需要頻繁重新整理。

    這種情況下我們首先需要弄清楚頻繁重新整理的原因是什麼,不同的原因對應不同的解決辦法。但是歸根結底,我們還是為了減少重新整理的次數或者儘量去掉中間輸出差別較大的繪製輸出。

    3、重新整理過程。

    對於視窗或控制元件的介面顯示,windows系統有一套繪製和重新整理的規則,繪製或重新整理的時機選擇也是影響閃爍的重要因素。如果再與上面兩條結合起來,某些情況下引起閃爍的原因確實非常複雜。只有我們分析出問題所在,才能用正確的方法解決之。

    三、幾種消除閃爍的解決方案

    1、儘量減少重複繪製

    MFC的視窗和控制元件重新整理有一套很複雜的規則,如果我們能深入理解,正確應用的話就能避免一部分閃爍。比如儘量用 InvalidateRect() 函式代替 Invalidate() 函式,InvalidateRect() 函式只重新整理介面上指定的區域,如果我們的介面上只有一小部分需要頻繁重新整理,那麼用這個函式代替 Invalidate() 的話,解決閃爍問題的效果是非常明顯的。這個函式已經封裝到MFC的CWnd類中(也有API函式)。

  1. void InvalidateRect(LPCRECT lpRect, BOOL bErase = TRUE);  

    其中,lpRect指向一個方形區域,該區域將被新增到需要更新的區域列表中,bErase指定重新整理時是否更新區域背景。

    如果我們需要重新整理的區域是不規則的,比如是幾個區域的組合,或者是某區域中去掉一部分,這時候用 InvalidateRect() 不能滿足我們的需求,我們可以用 InvalidateRgn() 函式。

  1. void InvalidateRgn (CRgn* pRgn, BOOL bErase = TRUE);  

其中,pRgn指向需要重新整理的區域。下面是一段示例程式碼:

  1. Crect rectClient;  
  2. CRgn rgn1, rgn2;  
  3. GetClientRect(rectClient);  
  4. rgn1.CreateRectRgnIndirect(rectClient);  
  5. rgn2.CreateRectRgnIndirect(m_rectButton);  
  6. rgn1.CombineRgn(&rgn1, &rgn2, RGN_XOR);  
  7. 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就可以了,程式碼如下:

  1. BOOL CMyWnd::OnEraseBkgnd(CDC* pDC)  
  2. {  
  3.  return TRUE;  
  4.  // return CWnd::OnEraseBkgnd(pDC);  // 註釋掉預設語句
  5. }  

    4、雙緩衝

    也許你已經聽說過雙緩衝這種方法了,的確,多數情況下雙緩衝能很好的解決我們的視窗閃爍問題,尤其是涉及到視窗自繪的時候。雙緩衝的基本原理是首先將複雜的繪製結果輸出到記憶體DC上,然後再一次性輸出到真正的視窗DC,這樣就避免了由於繪製時間佔用多個重新整理週期,而導致一次繪製引起短時間多次輸出產生閃爍。雙緩衝方法結合上一個方法,可以解決大部分自繪視窗的閃爍問題。具體的雙緩衝示例程式碼如下:

  1. void CMyWnd::OnPaint()  
  2. {  
  3.        CPaintDC dc(this);  
  4.        CRect rectClient;  
  5.        GetClientRect(&rectClient);  
  6.        CDC dcMem;  
  7.        CBitmap bmpMem;  
  8.        dcMem.CreateCompatibleDC(&dc);  
  9.        bmpMem.CreateCompatibleBitmap(&dc, rectClient.Width(), rectClient.Height());  
  10.        dcMem.SelectObject(&bmp);  
  11.       // 此處將繪製內容輸出到dcMem上
  12.       // dcMem.FillRect(rectClient, &brush);
  13.       dc.BitBlt(0, 0, rectClient.Width(), rectClient.Height(), &dcMem, 0, 0, SRCCOPY);  
  14.       bmpMem.DeleteObject();  
  15.       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作業系統有這樣一種視覺效果(右擊我的電腦-> 屬性-> 高階-> 設定 -> 視覺效果-> 自定義,去掉“拖拉時顯示視窗內容”選項),當你拖動和調整視窗大小時,並不是即時顯示視窗內容,而是出現一個虛線框,當調整結束時才一次性繪製最終介面。這時一個非常好的防止閃爍的方法,我們來看看怎麼實現這種效果。

    有沒有簡單的方法呢?呼叫 SystemParametersInfo 這個API函式可以改變系統“拖拉時顯示視窗內容”項的設定,但是如果我們設定以後,系統其他視窗的行為也將被改變。其實我們只要判斷什麼時候需要繪製虛線框,此時呼叫SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, FALSE, NULL, SPIF_SENDWININICHANGE),然後在拖動完畢需要繪製的時候呼叫 SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, TRUE, NULL, SPIF_SENDWININICHANGE) 恢復設定就可以了。當然如果希望完全不影響系統原來的設定,我們只要每次都先查詢一下系統原設定,然後恢復設定就可以了。

    具體處理過程是在CDialog的OnNcLButtonDown訊息響應函式中,當用戶點選對話方塊的非客戶區時該函式會被呼叫,而我們移動視窗或者調整視窗大小都是要點選非客戶區(標題欄或邊框)觸發該訊息。拖動過程中的處理是在CDialog::OnNcLButtonDown(nHitTest, point)中完成的,因此,我們只要按如下程式碼實現即可:

  1. void CMyDlg::OnNcLButtonDown(UINT nHitTest, CPoint point)  
  2. {     
  3.    // 1,查詢當前系統“拖動顯示視窗內容”設定
  4.     SystemParametersInfo(SPI_GETDRAGFULLWINDOWS, 0, &m_bDragFullWindow, NULL);  
  5.    // 2,如果需要修改設定,則在每次進入CDialog::OnNcLButtonDown預設處理之前修改
  6.     if(m_bDragFullWindow)  
  7.          SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, FALSE, NULL, NULL);  
  8.    // 3,預設處理,系統會自動繪製虛框
  9.     CDialog::OnNcLButtonDown(nHitTest, point);  
  10.    // 4,預設處理完畢後,還原系統設定
  11.     if(m_bDragFullWindow)  
  12.          SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, TRUE, NULL, NULL);  
  13. }