從 Java Swing 轉戰 C# WinForms 的開發者常會驚訝:Swing 只要設 setDoubleBuffered(true) 幾乎不會閃爍,但 WinForms 即使開啟 DoubleBuffered = true,在高頻重繪時依然會出現明顯閃爍。根本原因在於 Control.Refresh() 的運作機制:
Paint 事件繪製新圖像。即便使用雙緩衝(先畫到記憶體位圖再一次性 DrawImage),清屏動作依然發生在螢幕可見區域,故無法徹底解決。
Invalidate(Rectangle.Empty) 避開清屏關鍵在於 不讓系統執行背景清除。Control.Invalidate() 若傳入面積為零的矩形(如 new Rectangle(0, 0, 1, 1)),會觸發 Paint 事件,但 不會擦除背景,從而消除空白幀。
public partial class GamePanel : UserControl
{
private BufferedGraphics _buffer;
private readonly Timer _renderTimer = new Timer { Interval = 16 }; // ~60 FPS
public GamePanel()
{
// 1. 關鍵設定:禁止系統自動清背景
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer, true);
// 2. 建立緩衝區(尺寸隨控件調整)
Resize += (s, e) => RecreateBuffer();
_renderTimer.Tick += (s, e) => Invalidate(new Rectangle(0, 0, 1, 1));
_renderTimer.Start();
}
private void RecreateBuffer()
{
if (ClientSize.Width == 0 || ClientSize.Height == 0) return;
_buffer?.Dispose();
_buffer = BufferedGraphicsManager.Current.Allocate(
CreateGraphics(), ClientRectangle);
}
protected override void OnPaint(PaintEventArgs e)
{
// 3. 所有繪圖操作在緩衝區進行
var g = _buffer.Graphics;
g.Clear(BackColor); // 在記憶體中清背景,不閃爍
DrawGameFrame(g); // 業務繪圖邏輯
_buffer.Render(e.Graphics); // 一次性輸出到螢幕
}
private void DrawGameFrame(Graphics g)
{
// 實際遊戲/動畫繪圖代碼
}
protected override void Dispose(bool disposing)
{
if (disposing) _buffer?.Dispose();
base.Dispose(disposing);
}
}
| 技巧 | 作用 |
|---|---|
SetStyle(..., UserPaint | AllPaintingInWmPaint) | 接管 WM_PAINT,阻止預設背景擦除 |
Invalidate(new Rectangle(0,0,1,1)) | 觸發重繪但不清背景,消除空白幀 |
BufferedGraphics | 雙緩衝在記憶體完成合成,最後一次性 Blit 到螢幕 |
在 OnPaint 而非 CreateGraphics() 繪圖 | 確保繪圖與視窗更新同步,避免撕裂 |
Timer 裡直接呼叫 CreateGraphics() 繪圖 → 繪圖與視窗更新不同步,易撕裂。DoubleBuffered = true → 仍會先清背景再畫,高頻下仍閃爍。UserPaint + AllPaintingInWmPaint + 微小 Invalidate + BufferedGraphics → 徹底解決。此模式適用於:遊戲迴圈、即時圖表、視訊畫面、高頻動畫等所有「每幀全量重繪」場景。若為局部更新,可傳入實際髒矩形區域至 Invalidate(dirtyRect) 進一步降低 GPU 頻寬。