在微軟的.NET 的Forms窗口控件中,比如Treeview和ListView,僅僅是對通用控件的簡單封裝,因此他們不正常的引發Paint事件。 微軟所發布內容中,能看到的唯一建議就是設置控件的ControlStyles.UserPaint類型,然後自己為控件做所有的繪圖操作。 (譯注:老外提供了一個TreeViewWithPaint控件類,派生自TreeView類,提供了Paint事件的掛接。)
一、為了解決這個問題,我們在類內部使用了一個基於Bitmap類的Graphics對象。當任何窗口重新定義大小時候,對象都會重建。
//Recreate internal graphics object protected override void OnResize( System.EventArgs e ) { if( internalBitmap == null || internalBitmap.Width != Width || internalBitmap.Height != Height ) { if( Width != 0 && Height != 0 ) { DisposeInternal(); internalBitmap = new Bitmap( Width, Height ); internalGraphics = Graphics.FromImage( internalBitmap ); } } }
二、重寫窗口過程
當控件收到了WM_PAINT消息時候,將執行下面的三個步驟:
1. 通過一個內部的WM_PRINTCLIENT消息,讓原來的控件過程把圖象畫到內部的Graphics對象上。
//Draw Internal Graphics IntPtr hdc = internalGraphics.GetHdc(); Message printClientMessage = Message.Create( Handle, WM_PRINTCLIENT, hdc, IntPtr.Zero ); DefWndProc( ref printClientMessage ); internalGraphics.ReleaseHdc( hdc );
2. 使用內部的Graphics對象建立PaintEventArgs參數,引發用戶的OnPaint()函數。
//Add the missing OnPaint() call OnPaint( new PaintEventArgs( internalGraphics, Rectangle.FromLTRB( updateRect.left, updateRect.top, updateRect.right, updateRect.bottom ) ) );
3. 把內部Graphics對象的位圖拷貝到屏幕的Graphics設備上。
//Draw Screen Graphics screenGraphics.DrawImage( internalBitmap, 0, 0 ); WM_ERASEBKGND消息被過濾掉,什麼都不做。 case WM_ERASEBKGND: //removes flicker return;
三、所提供的代碼和測試程序能使用Paint事件在TreeNode在被選中的時候,在其邊框上畫個黃色的邊框。但是,其實對於我實際要用的項目來說,需要添加背景圖的功能沒有實現。而這裡離我們的目的還有一步之遙,我們對前文繪圖過程2和3之間加一個步驟:
Bitmap temp = new Bitmap(internalBitmap, internalBitmap.Size); // 建立一個臨時的位圖temp,保存前面繪好的界面 temp.MakeTransparent(Color.White); // 設置白色為透明色 internalGraphics.FillRectangle(Brushes.White, 0, 0, this.Bounds.Width, this.Bounds.Height); // 在原來的內部位圖對象上,用白色重畫背景 if (image != null) // 如果設置了背景圖,就在內部對象上畫背景 internalGraphics.DrawImage (image, 0, 0, image.Width, image.Height); internalGraphics.DrawImage(temp, 0, 0, temp.Width, temp.Height);// 把前面繪好的界面按白色為透明色復合到內部位圖上 screenGraphics.DrawImage( internalBitmap, 0, 0 ); // 把合成的臨時位圖刷到屏幕上
其實,這裡還存在一個問題:在處理WM_PAINT消息時候,通常的做法是使用BeginPaint和Endpaint函數來操作DC畫圖的,當樹結點展開或者折疊時候,我們收到WM_PAINT消息,並由消息得到的刷新區域或者說刷新矩形。關鍵就是在於,這裡的刷新區域不是整個客戶區,背景圖會出現重疊的部分而變形。
解決方法:考慮使用GetDC和ReleaseDC操作,可以避開刷新區域的限制,我們可以把整個客戶區重畫,而實現背景圖的完整性。這裡要非常注意的是:BeginPaint和Endpaint函數會自動把需要刷新的區域設為有效,而GetDC和ReleaseDC函數不會,所以我們要自己增加兩個操作GetUpdateRect和ValidateRect,也就是自己把需要刷新的區域設置為有效。否則:會不停的得到WM_PAINT消息,和死循環一樣,CPU占用達到100%。
圖一 測試程序
四、結束語
由於使用了Win32的API函數,因此附加了一個Win32內部類,導入了自己需要的函數。