深度探索WM_PAINT消息

轉自:http://blog.csdn.net/sdn_009/article/details/3293501


【引言】:這是以前在作VC/EVC開發時候遇到的一個鬱悶的問題的思考,剛好最近在VC#開發時候遇到了類似的問題,因此就總結出來,期望能夠給遇到同樣問題的開發者一些幫助和提示。

 

問題緣起

       半年前,在作Mobile GPS項目(移動手持設備上的GPS/GIS項目)的時候,爲了實現PDA上的地圖下載功能,我們將GIS地圖數據轉化爲XML文件,在經過相關技術的優化終於做到了將XML的地圖解析並繪製到PDA屏幕上。但是問題出現了:當在響應一個繪製地圖的菜單(Button)項目的時候,地圖繪製出來了,但是當有新窗體在上面經過(模擬器上運行)或者是觸摸筆擦寫PDA屏幕的時候,畫好的地圖被擦寫掉了!

       很顯然地,爲了調試這個Bug,我在VC下面去重複這樣的實現(當然是簡單的模擬),問題還是存在。但是將直接繪圖操作放在OnPaint()中則不會出現這樣的問題,但是這不能解決應用問題(圖像應該是一步步繪製的,並且在不同的時候有不同的顯示)。

原因揭密

       這個問題的原因是Windows對WM_PAINT消息的處理,WM_PAINT消息的相關知識這裏不羅嗦(可以參考相關資料),需要強調和說明的是:

1)  應用程序第一次運行的時候WM_PAINT會被自動調用,也就是說你在OnPaint()中實現的繪圖操作會被調用;

2)  當窗體的大小被調整(例如最小化、拉伸等),或者被遮擋後又顯示出來(問題中的情況)的時候會調用OnPaint()函數。

於是上面的問題的原因就是:當窗體被遮擋後,WM_PAINT消息使得窗體重畫,但是我們的繪畫邏輯不是放在OnPaint()中實現(是在菜單項或者Button的響應裏面實現的),因此調用後不能獲得對應的繪圖顯示。

解決方案

       問題出現了,項目還要做,客戶的需求還是要滿足,因此就得想辦法解決了。在弄懂了整個問題的原因後,解決方案也就慢慢又了眉目:既然是因爲系統調用OnPaint引起的,那就在OnPaint和繪圖邏輯間做一個權衡:

1)  將繪圖過程放在OnPaint中實現,當然可以是在另外函數中實現,在這裏調用即可;2)  菜單(Button)只控制繪圖邏輯:設置一個BOOL變量m_bDraw來標誌是否進行繪圖,初次可以設計爲FALSE。菜單和Button的響應邏輯中只是修改這個BOOL變量,例如標誌爲TRUE,提示繪製圖形;這個問題看上去已經解決了,但是這樣實現後,發現當菜單項(Button)點擊後,圖像沒有繪製出來。一思量,確實應該是這樣:因爲菜單項(Button)邏輯只是修改了m_bDraw的值,並沒有去顯示調用OnPaint消息。於是你會發現,當你有上面問題中的操作(例如最小化、遮擋等)時候,圖像如期而至(因爲這會出發WM_PAINT消息)。

       知道原因,解決方案就出來了:在菜單項(Button)響應邏輯中除了改變m_bDraw的狀態,還要發送WM_PAINT消息,以調用OnPaint函數。於是,只需要在2)中響應邏輯中除了改變BOOL變量值,再加上出發WM_PAINT消息的操作,你可以有以下3種選擇:1)  直接發送WM_PAINT消息,PostMessage(),SendMessage()函數發送WM_PAINT消息。使用以上兩函數發送WM_PAINT消息,能將WM_PAINT消息發送到WINDOWS程序消息隊列中,當WINDOWS將WM_PAINT消息發送給具體的消息處理函數時,如果窗口的無效區域爲空則WINDOWS將不理睬該消息。若存在無效區域,則調用窗口處理函數處理。要注意的這裏需要存在無效區域,因此要調用2)中的函數使得窗體(或者部分)無效,其處理過程與2)相同,將WM_PAINT消息送入消息處理隊列。與3)不同的是WM_PAINT並不立即處理;2)  調用相應的API實現WM_PAINT消息的發送:Invalidate(),InvalidateRect(),InvalidateRgn():以上函數將窗口的特定區域標定爲無效,當WINDOWS檢測到窗口中存在無效區域時將向消息隊列發送WM_PAINT 消息。我當時用的就是Invalidate()函數;

3)  UpdateWindow():該函數調用後WINDOWS將向窗口發送一個非隊列化的WM_PAINT消息,它不經過消息循環而直接發送給了窗口消息處理函數。如果窗口無效區域不存在,WINDOWS將不理睬該消息。注意這裏因爲要使得窗口無效區不存在,因此還是調用Invalidate(),InvalidateRect(), InvalidateRgn()函數,和2)中不同的是這裏的WM_PAINT消息會被立即處理,而2)中是加入消息處理隊列。

簡單起見,你可以使用2)中方案進行問題解決。

問題擴展

上面提到的都是在VC6.0和EVC 3.0下面的問題和解決方案,最近在做VC#開發的時

候,也遇到同樣的問題,其原因和上面當然是一樣的。解決方案也類似,只不過VC(EVC)中繪圖使用CDC(或其子類CPaintDC、CClientDC等)而VC#使用Graphics,解決方案也差不多,不同的是:顯示發送WM_PAINT消息的方法略有不同:在上面的發送WM_PAINT消息的方案2)中,你可以是調用以下的API之一:

1)  Invalidate():一共有7個重載的函數體,可以根據實際情況調用(相當於是上面的3個API功能);

2)  Update():注意這個操作和解決方案中的3)相似,因此必須還要調用1)中的Invalidate,與1)不同的是這裏WM_PAINT消息理解被觸發;

3)  Refresh():這個操作相當於Invalidate(true)+ Update(),因此也可以取得同樣的效果。

需要說明的是:當你在VC#的Windows Form下面測試上面的問題時候,你會發現另外

一個鬱悶的事情:當窗體大小被調整的時候,看上去似乎圖像被重畫了N次,但是每次都不徹底。問題的原因是:當窗體被擴大時,Windows只繪製那些新近顯露出來的矩形區域,而假定原有的矩形區域無需重繪。這個問題可以通過設置Form風格來處理,在Form的構造函數裏InitializeComponent()後面加上改變更個語句:SetStyle(ControlStyles.ResizeRedraw,true);一切就OK了。當然如果你要問爲什麼默認的不是這種風格,那是因爲這種做法的效率很低下(想想就知道了)的原因。

幾點說明

       本文在VC 6.0和VS 2003中均進行了測試,你可以通過以下方式構造測試程序:

1)  VC 6.0下,你可以新建一個基於Dialog或者Single Document的MFC程序,在前者你可以通過響應一個Button,後者通過一個簡單的菜單響應,繪製一個橢圓,看實際的問題和解決方案的可行性;

2)  在VS 2003中,新建一個VC#的Windows應用程序即可,同樣通過響應一個Button繪製橢圓,以獲得1)中效果。

發佈了1 篇原創文章 · 獲贊 13 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章