在 WinForms 開發中,畫面閃爍是困擾開發者的經典問題。其核心成因可歸納為兩大類:
當視窗大小、位置改變導致 Paint 事件觸發時,系統會逐個繪製圖元。每個圖元繪製完成即時顯示於螢幕,造成畫面局部不斷刷新,產生明顯閃爍。圖元越多、繪製越複雜,閃爍越嚴重。
解決關鍵:讓所有圖元在記憶體中繪製完成後,一次性呈現到螢幕。
滑鼠拖曳繪圖、圖元變形等操作會高頻觸發 Paint 事件。即便單次繪製已優化,但刷新間隔遠小於繪製耗時,畫面仍會閃爍。
解決關鍵:正確設定控制項樣式旗標,啟用系統級雙緩衝機制。
在控制項建構函式中呼叫 SetStyle,必須同時啟用以下三個旗標才能發揮最大效果:
this.SetStyle(
ControlStyles.OptimizedDoubleBuffer | // 雙緩衝
ControlStyles.ResizeRedraw | // 尺寸改變時重繪
ControlStyles.AllPaintingInWmPaint, // 禁止擦除背景,直接在 WM_PAINT 中繪製
true);
// 方式 1:屬性設定
this.DoubleBuffered = true;
// 方式 2:SetStyle 方法
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
適用於一般表單、簡單繪圖場景。.NET Framework 內部自動管理緩衝區。
BufferedGraphics(進階控制)透過 BufferedGraphicsManager 手動分配、繪製、渲染緩衝區,適合高效能、大量圖元場景:
// 1. 取得緩衝管理器
BufferedGraphicsContext context = BufferedGraphicsManager.Current;
// 2. 分配緩衝區(關聯目標繪圖面與區域)
BufferedGraphics buffer = context.Allocate(this.CreateGraphics(), this.DisplayRectangle);
// 3. 取得緩衝區的 Graphics 進行繪圖
Graphics g = buffer.Graphics;
// 4. 大量繪圖操作(範例:繪製 10,000 個隨機小圓)
Random rnd = new Random();
for (int i = 0; i < 10000; i++)
{
int x = rnd.Next(400);
int y = rnd.Next(400);
int w = rnd.Next(10);
int h = rnd.Next(10);
g.DrawEllipse(Pens.Blue, x, y, w, h);
}
// 5. 一次性渲染到螢幕
buffer.Render();
// 6. 釋放資源
buffer.Dispose();
// 在 OnPaint 中實現
protected override void OnPaint(PaintEventArgs e)
{
// 1. 建立與控制項同尺寸的離屏 Bitmap
using (Bitmap offscreen = new Bitmap(this.Width, this.Height))
using (Graphics g = Graphics.FromImage(offscreen))
{
g.Clear(this.BackColor);
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
// 2. 所有繪圖操作導向離屏 Bitmap
foreach (var drawObj in doc.DrawObjects)
{
if (e.ClipRectangle.IntersectsWith(drawObj.Bounds))
{
drawObj.Draw(g);
// 選取狀態下繪製追蹤點
if (drawObj.IsSelected && CurrentOperator == Operator.Transform)
drawObj.DrawTracker(g);
}
}
// 3. 一次性繪製到螢幕
using (Graphics screen = e.Graphics)
{
screen.DrawImage(offscreen, 0, 0);
}
}
}
BufferedGraphics 優化 OnPaintprotected override void OnPaint(PaintEventArgs e)
{
Rectangle clipRect = e.ClipRectangle;
BufferedGraphicsContext ctx = BufferedGraphicsManager.Current;
using (BufferedGraphics buffer = ctx.Allocate(e.Graphics, clipRect))
{
Graphics g = buffer.Graphics;
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.HighSpeed;
g.Clear(this.BackColor);
foreach (var obj in doc.DrawObjects)
{
if (clipRect.IntersectsWith(obj.Bounds))
{
obj.Draw(g);
if (obj.IsSelected && CurrentOperator == Operator.Transform)
obj.DrawTracker(g);
}
}
buffer.Render(e.Graphics); // 直接渲染到傳入的 Graphics
}
}
此方式記憶體佔用極低,且不易發生記憶體洩漏,是大型繪圖應用的推薦方案。
單純雙緩衝無法解決高頻滑鼠移動時的閃爍,需結合「靜態背景快照 + 動態前景繪製」策略:
private Bitmap _currentMap;
private bool _isFirstMove = true;
private Point _dragStart;
private void PictureBox_MouseDown(object sender, MouseEventArgs e)
{
if (zoom == 0 && e.Button == MouseButtons.Left)
{
_dragStart = e.Location;
_currentMap = new Bitmap(pictureBox.Image); // 保存當前靜態畫面
_isFirstMove = false;
}
}
private void PictureBox_MouseMove(object sender, MouseEventArgs e)
{
if (zoom == 0 && !_isFirstMove)
{
// 建立臨時畫布
using (Bitmap temp = new Bitmap(Width, Height))
using (Graphics g = Graphics.FromImage(temp))
{
g.Clear(Color.Transparent);
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
// 根據滑鼠位移繪製圖片
int offsetX = e.X - _dragStart.X;
int offsetY = e.Y - _dragStart.Y;
g.DrawImageUnscaled(_currentMap, offsetX, offsetY);
}
pictureBox.Image = temp; // 直接替換 Image,避免 BackgroundImage 閃爍
}
}
private void PictureBox_MouseUp(object sender, MouseEventArgs e)
{
if (zoom == 0)
{
// 計算實際邏輯座標並更新資料模型
Point worldPt = new Point(
Width / 2 + (_dragStart.X - e.X),
Height / 2 + (_dragStart.Y - e.Y));
map.Center = map.ImageToWorld(worldPt);
pictureBox.Image = map.GetMap(); // 重新生成最終畫面
_isFirstMove = true;
}
}
| 場景 | 推薦方案 | 關鍵點 |
|---|---|---|
| 一般表單、少量繪圖 | DoubleBuffered = true | 最簡單,無需額外代碼 |
| 大量圖元、高頻重繪 | BufferedGraphics (方案二/四) | 記憶體效率高,控制精準 |
| 自訂控制項開發 | 重寫 OnPaint + 方案三/四 | 完整掌控繪圖流程 |
| 滑鼠拖曳/動畫 | 靜態快照 + 動態前景 | 避免高頻完整重繪 |
核心原則:所有繪圖操作在記憶體完成,最後一次性呈現到螢幕。正確設定 ControlStyles 旗標是基礎,選擇適合場景的緩衝策略才是關鍵。