C++ OnDraw()和OnPaint() 之間有什麼關係

OnDraw()和OnPaint()好象兄弟倆,因爲它們的工作類似。

至於不見了的問題簡單,因爲當你的窗口改變後,會產生無效區域,這個無效的區域需要重畫。一般Windows回發送兩個消息WM_PAINT(通知

客戶區有變化)和WM_NCPAINT(通知非客戶區有變化)。非客戶區的重畫系統自己搞定了,而客戶區的重畫需要我們自己來完成。這就需要

OnDraw()或OnPaint()來重畫窗口。

OnDraw()和OnPaint()有什麼區別呢?
首先:
我們先要明確CView類派生自CWnd類。而OnPaint()是CWnd的類成員,同時負責響應WM_PAINT消息。OnDraw()是CVIEW的成員函數,並且沒有響

應消息的功能。這就是爲什麼你用VC成的程序代碼時,在視圖類只有OnDraw沒有OnPaint的原因。

其次:
要想在屏幕上繪圖或顯示圖形,首先需要建立設備環境DC。其實DC是一個數據結構,

它包含輸出設備(不單指你17寸的純屏顯示器,還包括打印機之類的輸出設備)的繪圖屬性的描述。MFC提供了CPaintDC類和CWindwoDC類來

實時的響應,而CPaintDC支持重畫。

當視圖變得無效時(包括大小的改變,移動,被遮蓋等等),Windows 將 WM_PAINT 消息發送給它。該視圖的 OnPaint 處理函數通過創建

CPaintDC 類的DC對象來響應該消息並調用視圖的 OnDraw 成員函數。通常我們不必編寫重寫的 OnPaint 處理成員函數。

///CView默認的標準的重畫函數
void CView::OnPaint()
{
     CPaintDC dc(this);
     OnPreparDC(&dc);
     OnDraw(&dc); //調用了OnDraw
}

既然OnPaint最後也要調用OnDraw,因此我們一般會在OnDraw函數中進行繪製。下面是一個典型的程序

///視圖中的繪圖代碼首先檢索指向文檔的指針,然後通過DC進行繪圖調用。
void CMyView::OnDraw( CDC* pDC )
{
     CMyDoc* pDoc = GetDocument();
     CString s = pDoc->GetData();   // Returns a CString
     CRect rect;

     GetClientRect( &rect );
     pDC->SetTextAlign( TA_BASELINE | TA_CENTER );
     pDC->TextOut( rect.right / 2, rect.bottom / 2,
                   s, s.GetLength() );
}

最後:
現在大家明白這哥倆之間的關係了吧。因此我們一般用OnPaint維護窗口的客戶區(例如我們的窗口客戶區加一個背景圖片),用OnDraw維護

視圖的客戶區(例如我們通過鼠標在視圖中畫圖)。當然你也可以不按照上面規律來,只要達到目的並且沒有問題,怎麼幹都成。

補充:
我們還可以利用Invalidate(),ValidateRgn(),ValidateRect()函數強制的重畫窗口,具體的請參考MSDN吧。


改變窗口大小,或者窗口被遮擋(去除遮擋)時,窗口自動產生WM_PAINT消息重繪有變化的區域(無效區)。
當你需要人工指定窗口重繪時,依此原理,可以指定窗口客戶區的某些區域無效,讓其重繪。
Invalidate指定整個客戶區無效;
InvalidateRect和InvalidateRgn分別指定某個矩形或某區域無效。
這三個函數向窗口發送了WM_PAINT消息進入消息隊列。當隊列較長時(前面還有很多消息未處理),就不能即時刷新了。
你可以在上面三個函數後加一句UpdateWindow(),這樣就可以立即刷新窗口了。

一款屬於自己的窗體基礎類庫,與MFC不同的是用這個類庫建立的窗體形狀是不規則的(用戶定義),且窗體上的所有控件都沒有handle(只是

一個用戶定義的region, 或者說叫hotspot),而是從一個抽象類CControl派生。由於所有可視部分都採用用戶貼圖(用GDI+),這樣就必然要

和WM_PAINT打交道。當年用VB編程的時候,對Paint事件就不甚瞭解,現在果然遇到了不小的問題……經過半天的研究,終於基本弄明白了

WM_PAINT的來龍去脈,在這裏總結一下。

先看官方教材,在MSDN裏的Platform SDK - Windows GDI一章中給出的WM_PAINT發送條件是"The WM_PAINT message is sent when the

system or another application makes a request to paint a portion of an application's window."那什麼時候系統或其他應用程序會

發送重畫請求呢?需要重畫的"portion"又有多大呢?再往下看,"The message is sent when the UpdateWindow or RedrawWindow

function is called, or by the DispatchMessage function when the application obtains a WM_PAINT message by using the

GetMessage or PeekMessage function. "這就說明這個WM_PAINT消息既可能由系統發送,也可能由應用程序人工發送。這個很好理解。當窗

口的一部分被其它窗口遮蓋而復原,或者從最小化狀態恢復到正常狀態時,系統自然會要求窗口重畫。系統重畫窗口的條件可以參見MSDN

\Platform SDK\Windows GDI\Painting and Drawing\When to Draw in a window. 而有時候我們需要窗口的某些部分作出改變,就得人工要

求窗口重畫(通過調用UpdateWindow和RedrawWindow)。比如我的基礎類庫裏窗口上沒有任何Windows意義上的控件(有hWnd),只是人爲定義某

一個區域是一個“按鈕”,當鼠標指向這個區域時加載hover圖像以獲得hottrack效果。這時操作系統自然不會認爲有重畫的必要,但程序卻

必須重畫,這時就得人工發送WM_PAINT消息了。注意不要傻乎乎地直接用SendMessage或PostMessage發送WM_PAINT,後面會解釋原因。

由於重畫很費時間和資源,並且也不是應用程序的“主業”,因此係統也知道要儘量減少重畫的次數。系統只在應用程序的消息隊列爲空的

時候才發送WM_PAINT,這就是爲什麼當程序死鎖時窗口圖像不會更新。同樣爲了減少重畫的工作量,Windows提出了update region的概

念,"The update region identifies the portion of a window that is out-of-date or invalid and in need of redrawing. The

system uses the update region to generate WM_PAINT messages for applications and to minimize the time applications spend

bringing the contents of their windows up to date. "也就是說,Windows會判斷窗口的哪些區域需要重畫,這個區域就是update

region. 比如原來在窗口上面的一個窗口現在挪走了,系統就把新露出來的區域定義爲update region(這個過程稱爲invalidate)。系統不斷

檢測一個窗口的update region是否爲空,當update region不爲空並且應用程序沒有消息要處理(消息隊列爲空)的時候,系統就通過

WM_PAINT告訴應用程序“現在沒事幹了?窗口的一部分需要重畫,你把這一部分重畫一下”。應用程序重畫了窗口之後,把update region重

新設置爲空(這個過程稱爲validate),如此不斷循環。如果消息隊列不爲空,系統就把update region不斷更新(採用取並集的方法),等消息

隊列爲空的時候一起處理。這就大大減少了重畫的次數。

這樣你或許就明白了爲什麼不能直接用SendMessage和PostMessage發送WM_PAINT的原因:由於沒有invalidate,系統認爲窗口沒有更新的必

要,於是就對發來的WM_PAINT消息不理不睬。解決方案就是——我們自己invalidate!相關的API就是InvalidateRect()和InvalidateRgn().

畫完了之後用ValidateRect()和ValidateRgn()告訴系統“我畫完了”就行了。可以把invalidate過程看成類似CombineRgn()取並集,把

validate過程看成取差集即可。還有一些相關的API: GetUpdateRect(), GetUpdateRgn(), ExcludeUpdateRgn(), 從名字就能猜出個大概,

各位可以自行去查MSDN.

在WM_PAINT消息處理過程中有兩個不得不提到的函數:BeginPaint()和EndPaint(). 只有WM_PAINT消息處理能使用這兩個函數。實際上默認

消息處理函數DefWindowProc()對WM_PAINT的處理方式就是:


case WM_PAINT:

    PAINTSTRUCT ps;

    BeginPaint (hWnd, &ps);

    EndPaint (hWnd, &ps);

    return 0;


BeginPaint()和EndPaint()之所以不可或缺,就是因爲它們實現了validate過程。BeginPaint()的主要任務之一就是validate. 如果在

WM_PAINT的消息處理中直接return 0,update region就始終不爲空,系統就會不停地發送WM_PAINT消息。EndPaint()負責釋放BeginPaint()

返回的DC,做好善後工作(比如重新顯示BeginPaint()隱藏起來的光標)。

最後還有一點需要額外說明:用WM_PAINT處理重畫是異步(asynchronous)的。也就是說,在invalidate之後窗口並不會立即重畫而是等到消

息隊列爲空時再重畫,這樣就有一個時間差。這個事件差有時短到不被注意,但有時就是個大問題(尤其是當程序需要執行耗費時間的任務,

如串口I/O)。這時可以採用同步重畫法,直接用GetDC()獲得hDC執行重畫操作。如果非要使用WM_PAINT來同步重畫(個人比較喜歡這種方法,

和重畫有關的代碼就應該在WM_PAINT的處理程序裏嘛),可以使用UpdateWindow()和RedrawWindow(). 這兩個API函數會直接把WM_PAINT送進

窗口的消息隊列而不是應用程序的消息隊列,這樣就不用等到最後了。注意前者當update region不爲空時纔會發送WM_PAINT,後者的控制選

項更爲豐富。

對於窗口程序,一般有個特點:窗口大部分的區域保持不變,只有不分區域需要重新繪製。如果將整個窗口全部刷新的畫,就做了許多不必

要的工作,因而,MFC採用了一套基於無效區的處理機制。在分析無效區處理之前,我們要明白一個現實,現在的機器還不夠牛,如果夠牛的

話,我們乾脆將整個窗口不斷的重新繪製好了。事實上即使夠牛也不行,對於一個單線程程序,通過一個while循環不斷的刷新窗口,程序也

無法相應其他消息(除非使用多線程),看來使用無效區的處理機制還是有其必然性的。

     VC程序是基於消息機制的,你所做的任何操作,比如點擊鼠標,拖動窗口,首先進入系統的消息隊列。這裏的系統消息隊列包括多個程

序的消息,系統再將消息發送給相應的程序。既然是隊列,這就有一個先進先出的問題,屏幕上的無效區更新消息出現的頻率就會特別高。

比如當左上角更新的消息還沒有處理,右下角更新的消息已經過來了。爲了避免多次處理WM_PAINT消息,系統就將這些窗口更新消息合併到

一條,只是將無效區範圍變成包括這兩次更新無效區範圍在內的矩形區域。這樣就減少了WM_PAINT消息的處理次數,提高了效率。

     那麼,在OnPaint消息處理函數中,又是怎樣實現更新無效區的呢?首先,要明白MFC中所有繪圖操作都是基於設備描述表(Device

Context,簡稱DC)的,具體信息可參看任何一本VC教材。DC中包含了繪圖設備的各種信息,對於屏幕繪圖,其實就是有一塊內存(顯存),

專門用來存放要顯示到屏幕上的信息,顯示器以85HZ的頻率(我以前的顯示器)將其內容刷新的屏幕上。這裏就到了關鍵點,顯示器的刷新

是將顯存中的內容完全更新到顯示器上,不存在無效區處理的問題,那麼,無效區的處理一定發生在DC的繪圖處理上。事實確實如此,當程

序調用OnPaint消息時,首先將無效區範圍傳遞給DC,DC在進行繪圖操作時,就只更新無效區範圍內的信息,其他地方的不管,這就提高了效

率。

     現在你明白OnPaint的處理是怎麼一回事了吧?這裏還想說一下Invalidate和UpdateWindow的區別。Invalidate在消息隊列中加入一條

WM_PAINT消息,其無效區爲整個客戶區。而UpdateWindow直接發送一個WM_PAINT消息,其無效區範圍就是消息隊列中WM_PAINT消息(最多隻

有一條)的無效區。效果很明顯,調用Invalidate之後,屏幕不一定馬上更新,因爲WM_PAINT消息不一定在隊列頭部,而調用UpdateWindow

會使WM_PAINT消息馬上執行的,繞過了消息隊列。如果你調用Invalidate之後想馬上更新屏幕,那就加上UpdateWindow()這條語句

(專家門診 Visual c++ 開發答疑 p97)

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