在Windows API編程中,WM_PAINT是Windows窗口的一個重要消息,應用程序就是通過響應 這個消息來完成窗口的繪制。
The WM_PAINT message is generated by the system and should not be sent by an application.The system sends this message when there are no other messages in the application's message queue
注意:WM_PAINT 消息是由系統產生,非要等應用程序的消息隊列為空時才發送WM_PAINT消息 。
其實 系統會在很多的不同的機制下發送WM_PAINT消息,比如調用UpdateWindow函數,第一次創建 窗口,改變了窗口的大小,最大化,最小化等等。這些動作的產生都是有系統來控制的,應 用程序只是接收消息,並處理消息。
當Window檢測到窗口被覆蓋的地方需要恢復的時 候,它會向用戶程序發送一個WM_PAINT消息,消息中包括了需要恢復的區域,然後由用戶程 序來決定如何恢復被覆蓋的內容。窗口過程收到WM_PAINT消息後,並不代表整個客戶區都需 要被刷新,有可能客戶區被覆蓋的區域只有一小塊,這個區域叫做“無效區域” ,程序只需要更新這個區域。與WM_TIMER消息類似,WM_PAINT消息也是一個低級別的消息, 雖然它不會像WM_TIMER消息一樣被丟棄,但Windows總是在消息循環空的時候才把WM_PAINT放 入其中,實際上,Windows為每個窗口維護一個“繪圖信息結構”,無效區域的坐 標就在其中,每當消息循環空的時候,如果Windows發現存在一個無效區域,就會放入一個 WM_PAINT消息。
無效區域的坐標並不附帶在WM_PAINT消息的參數中,在程序中有其他 方法可以獲取,WM_PAINT消息只是通知程序有個區域需要更新而已,所以Windows也不會同時 將兩條WM_PAINT消息放入消息循環中,當Windows要放入一條WM_PAINT消息的時候,如果發現 已經存在一個無效區域了,那麼它只需要把新舊兩個無效區域合並計算出一個無效區域就可 以了,消息循環中還是只需要一條WM_PAINT消息。
如果程序在WM_PAINT消息中對客戶 區刷新完畢後工作並沒有結束,如果不使無效區域變得有效,Windows會在下一輪消息循環中 繼續放入一個WM_PAINT消息,而不是根據程序是否執行了刷新過程,所以程序也可以不去刷 新客戶區,而是簡單地用一個ValidateRect函數直接讓客戶區變得有效,以此來“欺騙 ”Windows已經沒有無效區域了,當Windows檢查“繪圖信息結構”的時候發 現沒有了無效區域,也就不會繼續發送WM_PAINT消息了。
那麼“繪圖信息結構 ”怎麼獲取呢?BeginPaint函數的第二個參數是一個繪圖信息結構的緩沖區地址, windows會在這裡返回繪圖信息結構,結構中包含了無效區域的位置和大小,繪圖信息結構的 定義如下:
typedef struct tagPAINTSTRUCT { // ps
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT;
其中hdc字段是窗口的設備環境 句柄,rcPaint字段是一個RECT結構,它指定了無效區域矩形的對角頂點,fErase字段如果為 非零值,表示Windows在發送WM_PAINT消息前已經使用背景色擦除了無效區域,後面3個字段 是Windows內部使用的,應用程序不必去理會他們。
摘自《Windows環境下32位匯編語 言程序設計》
大多數Windows程序在WinMain中進入消息循環之前的初始化期間都要呼 叫函數UpdateWindow。Windows利用這個機會給窗口消息處理程序發送第一個WM_PAINT消息。 這個消息通知窗口消息處理程序:必須繪制顯示區域。此後,窗口消息處理程序應在任何時 刻都准備好處理其它WM_PAINT消息,必要的話,甚至重新繪制窗口的整個顯示區域。在發生 下面幾種事件之一時,窗口消息處理程序會接收到一個WM_PAINT消息:
在使用者移動 窗口或顯示窗口時,窗口中先前被隱藏的區域重新可見。
使用者改變窗口的大小(如 果窗口類別樣式有著CS_HREDRAW和CS_VREDRAW位旗標的設定)。
程序使用 ScrollWindow或ScrollDC函數滾動顯示區域的一部分。
程序使用InvalidateRect或 InvalidateRgn函數刻意產生WM_PAINT消息。
在某些情況下,顯示區域的一部分被臨 時覆蓋,Windows試圖保存一個顯示區域,並在以後恢復它,但這不一定能成功。在以下情況 下,Windows可能發送WM_PAINT消息:
Windows擦除覆蓋了部分窗口的對話框或消息框 。
菜單下拉出來,然後被釋放。
顯示工具提示消息。
在某些情況下, Windows總是保存它所覆蓋的顯示區域,然後恢復它。這些情況是:
鼠標光標穿越顯 示區域。
圖標拖過顯示區域。
處理WM_PAINT消息要求程序寫作者改變自己向 顯示器輸出的思維方式。程序應該組織成可以保留繪制顯示區域需要的所有信息,並且僅當 「響應要求」-即Windows給窗口消息處理程序發送WM_PAINT消息時才進行繪制。如果程序在 其它時間需要更新其顯示區域,它可以強制Windows產生一個WM_PAINT消息。這看來似乎是在 屏幕上顯示內容的一種捨近求遠的方法。但您的程序結構可以從中受益。
1. 系統何 時發送WM_PAINT消息?
系統會在多個不同的時機發送WM_PAINT消息:當第一次創建一 個窗口時,當改變窗口的大小時,當把窗口從另一個窗口背後移出時,當最大化或最小化窗 口時,等等,這些動作都是由 系統管理的,應用只是被動地接收該消息,在消息處理函數中 進行繪制操作;大多數的時候應用也需要能夠主動引發窗口中的繪制操作,比如當窗口顯示 的數據改變的時候,這一般是通過InvalidateRect和 InvalidateRgn函數來完成的。 InvalidateRect和InvalidateRgn把指定的區域加到窗口的Update Region中,當應用的消息 隊列沒有其他消息時,如果窗口的Update Region不為空時,系統就會自動產生WM_PAINT消息 。
系統為什麼不在調用Invalidate時發送WM_PAINT消息呢?又為什麼非要等應用消息 隊列為空時才發送WM_PAINT消息呢?這是因為系統把在窗口中的繪制操作當作一種低優先級 的操作,於是盡 可能地推後做。不過這樣也有利於提高繪制的效率:兩個WM_PAINT消息之間 通過InvalidateRect和InvaliateRgn使之失效的區域就會被累加起來,然後在一個WM_PAINT 消息中一次得到 更新,不僅能避免多次重復地更新同一區域,也優化了應用的更新操作。像 這種通過InvalidateRect和InvalidateRgn來使窗口區域無效,依賴於系統在合適的時機發送 WM_PAINT消息的機 制實際上是一種異步工作方式,也就是說,在無效化窗口區域和發送 WM_PAINT消息之間是有延遲的;有時候這種延遲並不是我們希望的,這時我們當然可以在無 效化窗口區域後利用SendMessage 發送一條WM_PAINT消息來強制立即重畫,但不如使用 Windows GDI為我們提供的更方便和強大的函數:UpdateWindow和RedrawWindow。 UpdateWindow會檢查窗口的Update Region,當其不為空時才發送WM_PAINT消息; RedrawWindow則給我們更多的控制:是否重畫非客戶區和背景,是否總是發送WM_PAINT消息 而不管Update Region是否為空等。
2. BeginPaint
今天在處理WM_PAINT消息 時產生了一個低級的錯誤,並搞的我花了快一個小時才找到原因。我在處理消息時,沒有使 用BeginPaint和EndPaint這對函數,結果我其余的消息彈不出來,窗口拖動時,不停閃爍( 其實那就是重繪)。後來還是在MSDN上找到了答案,現將原話貼出來。(在MSDN的The WM_PAINT Message標題中)
BeginPaint sets the update region of a window to NULL. This clears the region, preventing it fromgenerating subsequent WM_PAINT messages. If an application processes a WM_PAINT message but does not call BeginPaint or otherwise clear the update region, the application continues to receive WM_PAINT messages as long as the region is not empty. In all cases, an application must clear the update region before returning from the WM_PAINT message.
BeginPaint函數的作用就是將窗口需要重繪的區域設置為空(也就是 Update Region置空)。在正常情況下,我們接收到了WM_PAINT消息後,窗口的Update Region都是非空的(如果為空就不需要發送WM_PAINT消息了)。而當你響應這個消息的時候 又不調用BeginPaint來清空,窗口的Update Region就一直是非空的,系統就會一直發送 WM_PAINT消息。這樣就形成了一個處理WM_PAINT消息的死循環。這就是我出現錯誤的原因, 低級錯誤。
BeginPaint和WM_PAINT消息緊密相關。試一試在WM_PAINT處理函數中不寫 BeginPaint會怎樣?程序會像進入了一個死循環一樣達到驚人的CPU占用率,你會發現程序總 在處理一個接 一個的WM_PAINT消息。這是因為在通常情況下,當應用收到WM_PAINT消息時, 窗口的Update Region都是非空的(如果為空就不需要發送WM_PAINT消息了),BeginPaint的 一個作用就是把該Update Region置為空,這樣如果不調用BeginPaint,窗口的Update Region就一直不為空,如前所述,系統就會一直發送WM_PAINT消息。
BeginPaint和 WM_ERASEBKGND消息也有關系。當窗口的Update Region被標志為需要擦除背景時, BeginPaint會發送WM_ERASEBKGND消息來重畫背景,同時在其返回信息裡有一個標志表明窗口 背景是否被重畫過。當我們用InvalidateRect和InvalidateRgn來把指定區域加到Update Region中時,可以設置該區域是否需要被擦除背景,這樣下一個BeginPaint就知道是否需要 發送WM_ERASEBKGND消息了。
當然關於 WM_PAINT消息還有很多的知識需要學習。另外 要注意的一點是,BeginPaint只能在WM_PAINT處理函數中使用,並且在調用了BeginPaint函 數後,不要忘記了調用EndPaint函數,他們可是一對的。
3.重畫函數 InvalidateRect,UpdateWindow, RedrawWindow的區別
InvalidateRect是通過線程 的消息隊列來發送刷新消息,是最常用的。
UpdateWindow是直接調用窗口函數立即響 應刷新消息,使窗口刷新消息優先被響應(消息隊列中如果沒有WM_PAINT消息就什麼都不執 行),一般是在ShowWindow之後調用。
RedrawWindow相當於先調用InvalidateRect, 緊接著又調用UpdateWindow,此外RedrawWindow還提供了一些前兩者沒法做到的功能。
補充幾點:
1.WM_Paint 是一個被動消息,不能通過普通的方法簡單的 sendmessage WM_paint 了事這是不行的;但通過消息由程序員引發不是不可能;通過幾個特殊 的常數可以做到,不過要到delphi下找
2.sendmessage 可以將消息發送到消息隊列;但 windows會自動判斷是否存在無效的畫圖區域;如果存在無效的畫圖區域,則可能會重畫,反之 則棄用該消息.
3.可以使用 InvalidateRect 等幾個APi將屏幕上任意一個個矩形區域 設置為無效區域,在UpdateWindow後調用後,windows會自動查找是否存在無效,並重畫,該矩形 區域;