.NET Framework 的 TreeView 與 ListView 只是 Win32 通用控制項的簡易封裝,預設不會引發 Paint 事件。微軟官方建議的解法是設定 ControlStyles.UserPaint 並自行處理所有繪圖工作,但這會導致需重寫整個控制項的繪製邏輯。
採用內部 Bitmap + Graphics 雙重緩衝策略,攔截 WM_PAINT 訊息,透過 WM_PRINTCLIENT 讓原生控制項繪製到記憶體位圖,再合成背景圖片,最後一次性輸出到螢幕。
protected override void OnResize(EventArgs e)
{
base.OnResize(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);
}
}
}
protected override void WndProc(ref Message m)
{
const int WM_PAINT = 0x000F;
const int WM_PRINTCLIENT = 0x0318;
const int WM_ERASEBKGND = 0x0014;
if (m.Msg == WM_ERASEBKGND)
{
// 過濾背景擦除,減少閃爍
return;
}
if (m.Msg == WM_PAINT)
{
// 步驟 1:讓原生控制項畫到內部 Graphics
IntPtr hdc = internalGraphics.GetHdc();
Message printMsg = Message.Create(Handle, WM_PRINTCLIENT, hdc, IntPtr.Zero);
DefWndProc(ref printMsg);
internalGraphics.ReleaseHdc(hdc);
// 步驟 2:建立 PaintEventArgs 觸發使用者 OnPaint
RECT updateRect;
GetUpdateRect(Handle, out updateRect, false);
OnPaint(new PaintEventArgs(internalGraphics, Rectangle.FromLTRB(updateRect.Left, updateRect.Top, updateRect.Right, updateRect.Bottom)));
// 步驟 3:合成背景圖片(關鍵新增邏輯)
if (backgroundImage != null)
{
// 建立臨時位圖保存原始繪製內容
using (Bitmap temp = new Bitmap(internalBitmap, internalBitmap.Size))
{
temp.MakeTransparent(Color.White); // 白色設為透明
// 重畫白色背景
internalGraphics.FillRectangle(Brushes.White, 0, 0, Width, Height);
// 繪製背景圖
internalGraphics.DrawImage(backgroundImage, 0, 0, backgroundImage.Width, backgroundImage.Height);
// 將原始內容(透過白色透明)合成回去
internalGraphics.DrawImage(temp, 0, 0, temp.Width, temp.Height);
}
}
// 步驟 4:輸出到螢幕
using (Graphics screenGraphics = CreateGraphics())
{
screenGraphics.DrawImage(internalBitmap, 0, 0);
}
return;
}
base.WndProc(ref m);
}
使用 GetDC/ReleaseDC 取代 BeginPaint/EndPaint 可繞過更新區域限制,繪製完整客戶區。但必須手動呼叫 GetUpdateRect 與 ValidateRect 將更新區域標記為已驗證,否則會持續收到 WM_PAINT 導致 CPU 100%。
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
private static extern bool GetUpdateRect(IntPtr hWnd, out RECT lpRect, bool bErase);
[DllImport("user32.dll")]
private static extern bool ValidateRect(IntPtr hWnd, ref RECT lpRect);

圖一:測試程式執行畫面,TreeView 節點選取時顯示黃色邊框,背景圖片完整呈現無重疊變形。
WM_PRINTCLIENT 捕獲原生繪製 + 雙重緩衝合成MakeTransparent 實現透明疊加GetDC/ReleaseDC + 手動 ValidateRect 防止死迴圈