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

C# WinForms 解決多控制項容器佈局切換時閃爍問題:WM_SETREDRAW 與雙重緩衝完整方案

     編輯:關於C#

閃爍根本原因

當容器 (Form/Panel/TabControl) 狀態改變 (Normal ↔ Maximized、Dock 改變、Visible 切換) 時,Windows 會重繪背景與所有子控制項。若子控制項眾多且未暫停重繪,會產生「背景與前景交替閃爍」的視覺瑕疵。

核心解法:WM_SETREDRAW 訊息暫停重繪

參考 ListView.BeginUpdate/EndUpdate 內部實作,透過發送 WM_SETREDRAW (0x0B) 訊息告知視窗管理器暫停/恢復重繪。

通用輔助類別實作


using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public static class ControlRedrawHelper
{
    private const int WM_SETREDRAW = 0x0B;

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

    /// 
    /// 暫停控制項重繪
    /// 
    public static void SuspendDrawing(Control control)
    {
        if (control.IsHandleCreated)
            SendMessage(control.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero);
    }

    /// 
    /// 恢復控制項重繪並立即失效重繪
    /// 
    public static void ResumeDrawing(Control control)
    {
        if (control.IsHandleCreated)
        {
            SendMessage(control.Handle, WM_SETREDRAW, new IntPtr(1), IntPtr.Zero);
            control.Invalidate(true); // 遞迴失效所有子控制項
        }
    }

    /// 
    /// 執行動作期間暫停重繪 (自動恢復)
    /// 
    public static void SuspendDrawingScope(Control control, Action action)
    {
        try
        {
            SuspendDrawing(control);
            action();
        }
        finally
        {
            ResumeDrawing(control);
        }
    }
}

使用範例:動態載入大量控制項


private void LoadUserControls()
{
    // 方式一:手動控制
    ControlRedrawHelper.SuspendDrawing(panelContainer);
    try
    {
        panelContainer.Controls.Clear();
        for (int i = 0; i < 100; i++)
        {
            var uc = new MyUserControl { Dock = DockStyle.Top };
            panelContainer.Controls.Add(uc);
        }
    }
    finally
    {
        ControlRedrawHelper.ResumeDrawing(panelContainer);
    }

    // 方式二:使用 Scope 簡潔寫法 (C# 8.0+ using 宣告)
    ControlRedrawHelper.SuspendDrawingScope(tabPageContent, () =>
    {
        tabPageContent.Controls.Clear();
        // 大量操作...
    });
}

進階:表單最大化/最小化切換閃爍解決


protected override void OnResize(EventArgs e)
{
    // 避免調整大小時閃爍
    if (WindowState == FormWindowState.Maximized || WindowState == FormWindowState.Normal)
    {
        ControlRedrawHelper.SuspendDrawing(this);
        base.OnResize(e);
        ControlRedrawHelper.ResumeDrawing(this);
    }
    else
    {
        base.OnResize(e);
    }
}

輔助方案:雙重緩衝 (Double Buffering)

適用於自繪控制項或頻繁重繪場景:


// 表單層級啟用 (需在建構子中)
this.DoubleBuffered = true;

// 或針對特定控制項 (需透過反射設定 Protected 屬性)
public static void EnableDoubleBuffering(Control control)
{
    var prop = typeof(Control).GetProperty("DoubleBuffered", 
        System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
    prop?.SetValue(control, true, null);
}

// 使用範例
EnableDoubleBuffering(dataGridView1);
EnableDoubleBuffering(panelContainer);

終極難題:最大化時工作列消失問題

當無邊框表單 (FormBorderStyle=None) 最大化且 TopMost=true 時,若使用 SuspendDrawing 可能導致工作列未重繪而消失。

解決方案:強制工作列重繪


[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

[DllImport("user32.dll")]
private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, uint flags);

private const uint RDW_INVALIDATE = 0x0001;
private const uint RDW_UPDATENOW = 0x0100;
private const uint RDW_ALLCHILDREN = 0x0080;

private void ForceTaskbarRedraw()
{
    IntPtr taskbar = FindWindow("Shell_TrayWnd", null);
    if (taskbar != IntPtr.Zero)
    {
        RedrawWindow(taskbar, IntPtr.Zero, IntPtr.Zero, 
            RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN);
    }
}

// 在 ResumeDrawing 後呼叫
ControlRedrawHelper.ResumeDrawing(this);
ForceTaskbarRedraw();

內建但隱藏的 BeginUpdateInternal

.NET Framework 內部 Control.BeginUpdateInternal() 實作相同邏輯,但標記為 internal 僅供 ListView, TreeView, DataGridView 等少數控制項使用。上述輔助類別實質上是將此功能通用化。

最佳實踐總結

  1. 大量控制項增刪/屬性變更:包裹在 SuspendDrawingScope 中。
  2. 表單狀態切換:在 OnResize 或 SizeChanged 中暫停重繪。
  3. 自繪控制項:啟用 DoubleBuffered + ResizeRedraw。
  4. 無邊框最大化:恢復重繪後呼叫 ForceTaskbarRedraw。
  5. 避免巢狀 Suspend:計數器機制已在 SendMessage 內部處理,但邏輯層面應避免巢狀呼叫。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved