WinForms 控件重繪流程:
閃爍本質:步驟 2 清空畫布與步驟 3 繪制內容之間有可見時間間隔,頻繁重繪 (大數據 TreeView、ListView、DataGridView) 時尤為明顯。雙緩衝雖能減少撕裂,但**無法阻止背景清除階段的白屏**。
| 方案 | 原理 | 效果 | 適用場景 | 副作用 |
|---|---|---|---|---|
1. 雙緩衝 (DoubleBuffered=true) | 離屏位圖繪制後一次性 Blit | ★★★☆☆ 減少撕裂,**不解決背景清除閃爍** | 中小數據量、簡單繪制 | 記憶體+CPU 開銷 |
2. SetStyle(AllPaintingInWmPaint | UserPaint | OptimizedDoubleBuffer) | 接管整個繪制流程,繞過 WM_ERASEBKGND | ★★★★☆ 徹底解決,**但需自繪所有內容** | 自繪控件 (OwnerDraw) | 開發工作量大,易出 Bug |
3. BeginUpdate/EndUpdate | 批量操作鎖定重繪 | ★★☆☆☆ 僅適合初始化批量添加,**不適合頻繁增量更新** | 初始化載入數萬節點 | 鎖定期間界面凍結 |
| 4. **抑制 WM_ERASEBKGND (推薦)** | 重寫 WndProc 忽略 0x0014 消息 | ★★★★★ **徹底消除背景清除閃爍**,保留標準繪制邏輯 | **所有標準控件、大數據場景** | 極少 (需手動觸發背景重繪時調用 Invalidate()) |
public class FlickerFreeTreeView : TreeView {
protected override void WndProc(ref Message m) {
// 0x0014 = WM_ERASEBKGND
if (m.Msg == 0x0014) {
// 返回 1 表示「已處理」,阻止 DefWindowProc 填充背景
m.Result = (IntPtr)1;
return;
}
base.WndProc(ref m);
}
}
// 通用擴展方法:任意控件一鍵無閃爍
public static class ControlExtensions {
public static void EnableFlickerFree(this Control ctrl) {
// 針對非自繪控件:抑制背景擦除 + 開啟雙緩衝 = 完美組合
ctrl.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
// 透過反射掛鉤 WndProc (或繼承控件重寫)
// 這裡提供繼承模板,實際專案建議建立 FlickerFreeXxx 系列控件
}
}
// 無閃爍 TreeView
public class FlickerFreeTreeView : TreeView {
public FlickerFreeTreeView() {
DoubleBuffered = true; // 配合抑制背景擦除
}
protected override void WndProc(ref Message m) {
if (m.Msg == 0x0014) { m.Result = (IntPtr)1; return; }
base.WndProc(ref m);
}
}
// 無閃爍 ListView
public class FlickerFreeListView : ListView {
public FlickerFreeListView() {
DoubleBuffered = true;
// ListView 需額外設定
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
}
protected override void WndProc(ref Message m) {
if (m.Msg == 0x0014) { m.Result = (IntPtr)1; return; }
base.WndProc(ref m);
}
}
// 無閃爍 DataGridView (繼承難度大,建議用雙緩衝反射)
public static class DataGridViewHelper {
public static void EnableDoubleBuffer(this DataGridView dgv) {
var prop = typeof(DataGridView).GetProperty("DoubleBuffered",
BindingFlags.Instance | BindingFlags.NonPublic);
prop?.SetValue(dgv, true, null);
}
}
| 錯誤做法 | 後果 | 正確做法 |
|---|---|---|
| 只開 `DoubleBuffered` 不抑制背景擦除 | 大數據仍閃爍白屏 | **組合使用**:抑制 WM_ERASEBKGND + DoubleBuffered |
| 用 `SetStyle(UserPaint)` 但忘記 `AllPaintingInWmPaint` | 背景仍被擦除,自繪內容被覆蓋 | 三合一:`UserPaint | AllPaintingInWmPaint | OptimizedDoubleBuffer` |
| 在 `OnPaintBackground` 返回不調用 base | 同抑制 WM_ERASEBKGND,但不如 WndProc 徹底 | 推薦 WndProc 方案,攔截更早 |
| 頻繁調用 `Invalidate()` 觸發重繪 | CPU 100%、閃爍加劇 | 合併無效區域、`Update()` 立即重繪、節流 |
WPF 採用**保留模式渲染** (視覺樹 + 合成器),**天然無閃爍**,無需手動干預。新專案強烈建議遷移 WPF/WinUI 3/MAUI,徹底告別 GDI+ 繪制痛點。
WinForms 閃爍的**根治之道**是:**抑制 `WM_ERASEBKGND` (0x0014) + 開啟 `DoubleBuffered`**。通過繼承標準控件封裝 `FlickerFreeXxx` 系列,專案中全量替換,一次解決,永久免維護。配合虛擬化、懶加載、佈局暫停,大數據界面同樣絲滑流暢。