首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# WinForms 解決控制項閃爍:雙緩衝原理與 Invalidate 微調實戰

     編輯:關於C語言

現象與成因

自繪控制項(覆寫 `OnPaint`)頻繁重繪時,**畫面閃爍**是經典痛點。根本原因:

  1. `Refresh()` / `Invalidate()` 觸發重繪流程。
  2. Windows 先發送 `WM_ERASEBKGND`,**以背景色清空整個客戶區**(產生一幀「白畫面」)。
  3. 隨後觸發 `WM_PAINT`,繪製內容。
  4. 若刷新頻率高,**「清背景 → 畫內容」的間隙被肉眼捕捉** → 閃爍。

即便啟用 `DoubleBuffered = true`(雙緩衝),若**清背景與繪內容仍在同一幀分兩步**,閃爍依然可能發生(特別是大面積重繪)。

核心解決策略:三步走

  1. 手動建立 Graphics:用 `CreateGraphics()` 而非 `PaintEventArgs.Graphics`,掌握繪圖時機。
  2. 離屏繪圖:建立與控制項等大的 `Bitmap` 作為後備緩衝區,所有繪圖操作針對 `Graphics.FromImage(bitmap)`。
  3. 最小化無效區域:呼叫 `Invalidate(new Rectangle(0, 0, 1, 1))` **只標記 1x1 像素區域需重繪**,**抑制 `WM_ERASEBKGND`**(系統不會清除整個背景),在 `OnPaint` 中直接 `DrawImage` 整張位圖。

完整實作範例

using System;
using System.Drawing;
using System.Windows.Forms;

public class FlickerFreeControl : Control
{
    private Bitmap _backBuffer;      // 後備緩衝區
    private Graphics _backGraphics;  // 後備繪圖面
    private bool _disposed;

    public FlickerFreeControl()
    {
        // 關鍵:抑制系統自動清背景,由我們在 OnPaint 全權接管
        SetStyle(ControlStyles.AllPaintingInWmPaint | 
                 ControlStyles.UserPaint | 
                 ControlStyles.OptimizedDoubleBuffer, true);
        
        // 固定大小或隨控制項縮放時重建緩衝區
        this.Resize += (s, e) => RecreateBuffer();
    }

    private void RecreateBuffer()
    {
        if (ClientSize.Width == 0 || ClientSize.Height == 0) return;
        
        _backBuffer?.Dispose();
        _backGraphics?.Dispose();

        _backBuffer = new Bitmap(ClientSize.Width, ClientSize.Height);
        _backGraphics = Graphics.FromImage(_backBuffer);
        // 可設定高品質插值
        _backGraphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
        _backGraphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
    }

    /// 
    /// 遊戲迴圈/動畫計時器呼叫:更新邏輯 → 繪製到後備緩衝 → 觸發最小重繪
    /// 
    public void UpdateAndRender()
    {
        if (_backGraphics == null) RecreateBuffer();
        if (_backGraphics == null) return;

        // 1. 清除後備緩衝(自訂背景色/圖片)
        _backGraphics.Clear(BackColor);
        
        // 2. 在這裡繪製所有動態內容(圖形、文字、圖片...)
        DrawFrame(_backGraphics);

        // 3. 只讓 1x1 像素無效 → 觸發 OnPaint 但不清背景
        Invalidate(new Rectangle(0, 0, 1, 1));
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        // 直接把整張後備位圖畫上去,一步到位,無閃爍
        if (_backBuffer != null)
        {
            e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0);
        }
        // 不呼叫 base.OnPaint(e),避免預設清背景邏輯
    }

    protected override void OnPaintBackground(PaintEventArgs e)
    {
        // 關鍵:空實作,完全阻止系統清背景(WM_ERASEBKGND 不再處理)
    }

    private void DrawFrame(Graphics g)
    {
        // 範例:繪製移動的球
        // 實際專案替換為你的繪圖邏輯
    }

    protected override void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                _backGraphics?.Dispose();
                _backBuffer?.Dispose();
            }
            _disposed = true;
        }
        base.Dispose(disposing);
    }
}

關鍵屬性組合解析

**三者合用** 相當於告訴系統:「別管背景、別管雙緩衝、我全包了」,再搭配 `Invalidate(1x1)` 微調,達到**零閃爍**。

常見誤區對照

ControlStyles 旗標作用
`UserPaint`控制項自行繪製(覆寫 OnPaint),系統不再自動繪製。
`AllPaintingInWmPaint`抑制 `WM_ERASEBKGND`,所有繪製在 `WM_PAINT` 一次完成。
`OptimizedDoubleBuffer`啟用雙緩衝(內建),配合上兩者效果最佳。
誤區做法結果正確做法
只設 `DoubleBuffered = true`大面積重繪仍閃爍加上 `UserPaint` + `AllPaintingInWmPaint` + 手動緩衝區
`OnPaint` 內 `e.Graphics.Clear()`觸發清背景閃爍清背景在**後備 Graphics** 做,`OnPaint` 只 `DrawImage`
`Invalidate()` 無參數整個區域無效 → 觸發清背景`Invalidate(new Rectangle(0,0,1,1))` 最小區域
用 `PaintEventArgs.Graphics` 畫緩衝區無法跨幀保留內容用 `CreateGraphics()` 或 `Graphics.FromImage(bitmap)` 建立持久 Graphics

效能補充建議

  • **重建緩衝區** 僅在 `Resize` 且尺寸實際改變時執行。
  • **高 DPI** 環境下,`ClientSize` 為邏輯像素,`Bitmap` 需配合 `DeviceDpi` 建立對應物理像素大小,或設定 `SetStyle(ControlStyles.DpiAware, true)`。
  • **極高幀率(60FPS+)** 遊戲級需求,建議改用 **Direct2D (SharpDX/Vortice)** / **OpenGL (OpenTK/Silk.NET)** / **SkiaSharp** 繞過 GDI+ 限制。

總結

WinForms 閃爍本質是 **「清背景」與「畫內容」分離導致的視覺間隙**。解法核心三步:**手動緩衝區 + 抑制系統清背景 + 最小化無效區域**。掌握 `ControlStyles` 三大旗標、`OnPaintBackground` 空實作、`Invalidate(1x1)` 技巧,即可徹底根治 GDI+ 自繪控制項閃爍問題。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved