190

我有一个控件,我必须对其进行大量修改。我想在这样做时完全防止它重绘 - SuspendLayout 和 ResumeLayout 是不够的。如何暂停控件及其子控件的绘制?

4

10 回答 10

323

在我之前的工作中,我们努力让我们丰富的 UI 应用程序能够立即流畅地绘制。我们使用的是标准的 .Net 控件、自定义控件和 devexpress 控件。

经过大量谷歌搜索和反射器使用后,我遇到了 WM_SETREDRAW win32 消息。这确实会在您更新控件时停止绘图,并且可以将 IIRC 应用于父/包含面板。

这是一个非常简单的类,演示如何使用此消息:

class DrawingControl
{
    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, Int32 wMsg, bool wParam, Int32 lParam);

    private const int WM_SETREDRAW = 11; 
    
    public static void SuspendDrawing( Control parent )
    {
        SendMessage(parent.Handle, WM_SETREDRAW, false, 0);
    }

    public static void ResumeDrawing( Control parent )
    {
        SendMessage(parent.Handle, WM_SETREDRAW, true, 0);
        parent.Refresh();
    }
}

对此有更全面的讨论 - google for C# 和 WM_SETREDRAW,例如

C# 抖动

暂停布局

对于它可能关心的人来说,这是 VB 中的一个类似示例:

Public Module Extensions
    <DllImport("user32.dll")>
    Private Function SendMessage(ByVal hWnd As IntPtr, ByVal Msg As Integer, ByVal wParam As Boolean, ByVal lParam As IntPtr) As Integer
    End Function

    Private Const WM_SETREDRAW As Integer = 11

    ' Extension methods for Control
    <Extension()>
    Public Sub ResumeDrawing(ByVal Target As Control, ByVal Redraw As Boolean)
        SendMessage(Target.Handle, WM_SETREDRAW, True, IntPtr.Zero)
        If Redraw Then
            Target.Refresh()
        End If
    End Sub

    <Extension()>
    Public Sub SuspendDrawing(ByVal Target As Control)
        SendMessage(Target.Handle, WM_SETREDRAW, False, IntPtr.Zero)
    End Sub

    <Extension()>
    Public Sub ResumeDrawing(ByVal Target As Control)
        ResumeDrawing(Target, True)
    End Sub
End Module
于 2009-01-28T14:15:31.890 回答
53

以下是 ng5000 的相同解决方案,但不使用 P/Invoke。

public static class SuspendUpdate
{
    private const int WM_SETREDRAW = 0x000B;

    public static void Suspend(Control control)
    {
        Message msgSuspendUpdate = Message.Create(control.Handle, WM_SETREDRAW, IntPtr.Zero,
            IntPtr.Zero);

        NativeWindow window = NativeWindow.FromHandle(control.Handle);
        window.DefWndProc(ref msgSuspendUpdate);
    }

    public static void Resume(Control control)
    {
        // Create a C "true" boolean as an IntPtr
        IntPtr wparam = new IntPtr(1);
        Message msgResumeUpdate = Message.Create(control.Handle, WM_SETREDRAW, wparam,
            IntPtr.Zero);

        NativeWindow window = NativeWindow.FromHandle(control.Handle);
        window.DefWndProc(ref msgResumeUpdate);

        control.Invalidate();
    }
}
于 2011-06-10T04:36:33.073 回答
19

我通常使用 ngLink's answer的一些修改版本。

public class MyControl : Control
{
    private int suspendCounter = 0;

    private void SuspendDrawing()
    {
        if(suspendCounter == 0) 
            SendMessage(this.Handle, WM_SETREDRAW, false, 0);
        suspendCounter++;
    }

    private void ResumeDrawing()
    {
        suspendCounter--; 
        if(suspendCounter == 0) 
        {
            SendMessage(this.Handle, WM_SETREDRAW, true, 0);
            this.Refresh();
        }
    }
}

这允许嵌套暂停/恢复调用。您必须确保将每个SuspendDrawingResumeDrawing. 因此,将它们公开可能不是一个好主意。

于 2009-11-24T13:04:07.487 回答
14

为了帮助不要忘记重新启用绘图:

public static void SuspendDrawing(Control control, Action action)
{
    SendMessage(control.Handle, WM_SETREDRAW, false, 0);
    action();
    SendMessage(control.Handle, WM_SETREDRAW, true, 0);
    control.Refresh();
}

用法:

SuspendDrawing(myControl, () =>
{
    somemethod();
});
于 2015-03-10T11:40:42.813 回答
10

根据 ng5000 的回答,我喜欢使用这个扩展:

        #region Suspend
        [DllImport("user32.dll")]
        private static extern int SendMessage(IntPtr hWnd, Int32 wMsg, bool wParam, Int32 lParam);
        private const int WM_SETREDRAW = 11;
        public static IDisposable BeginSuspendlock(this Control ctrl)
        {
            return new suspender(ctrl);
        }
        private class suspender : IDisposable
        {
            private Control _ctrl;
            public suspender(Control ctrl)
            {
                this._ctrl = ctrl;
                SendMessage(this._ctrl.Handle, WM_SETREDRAW, false, 0);
            }
            public void Dispose()
            {
                SendMessage(this._ctrl.Handle, WM_SETREDRAW, true, 0);
                this._ctrl.Refresh();
            }
        }
        #endregion

采用:

using (this.BeginSuspendlock())
{
    //update GUI
}
于 2016-10-28T16:07:53.410 回答
8

不使用互操作的一个很好的解决方案:

与往常一样,只需在您的 CustomControl 上启用 DoubleBuffered=true。然后,如果您有任何容器,如 FlowLayoutPanel 或 TableLayoutPanel,请从这些类型中的每一个派生一个类,并在构造函数中启用双缓冲。现在,只需使用您的派生容器而不是 Windows.Forms 容器。

class TableLayoutPanel : System.Windows.Forms.TableLayoutPanel
{
    public TableLayoutPanel()
    {
        DoubleBuffered = true;
    }
}

class FlowLayoutPanel : System.Windows.Forms.FlowLayoutPanel
{
    public FlowLayoutPanel()
    {
        DoubleBuffered = true;
    }
}
于 2010-03-10T22:53:44.513 回答
4

这里结合了 ceztko 和 ng5000 带来了一个不使用 pinvoke 的 VB 扩展版本

Imports System.Runtime.CompilerServices

Module ControlExtensions

Dim WM_SETREDRAW As Integer = 11

''' <summary>
''' A stronger "SuspendLayout" completely holds the controls painting until ResumePaint is called
''' </summary>
''' <param name="ctrl"></param>
''' <remarks></remarks>
<Extension()>
Public Sub SuspendPaint(ByVal ctrl As Windows.Forms.Control)

    Dim msgSuspendUpdate As Windows.Forms.Message = Windows.Forms.Message.Create(ctrl.Handle, WM_SETREDRAW, System.IntPtr.Zero, System.IntPtr.Zero)

    Dim window As Windows.Forms.NativeWindow = Windows.Forms.NativeWindow.FromHandle(ctrl.Handle)

    window.DefWndProc(msgSuspendUpdate)

End Sub

''' <summary>
''' Resume from SuspendPaint method
''' </summary>
''' <param name="ctrl"></param>
''' <remarks></remarks>
<Extension()>
Public Sub ResumePaint(ByVal ctrl As Windows.Forms.Control)

    Dim wparam As New System.IntPtr(1)
    Dim msgResumeUpdate As Windows.Forms.Message = Windows.Forms.Message.Create(ctrl.Handle, WM_SETREDRAW, wparam, System.IntPtr.Zero)

    Dim window As Windows.Forms.NativeWindow = Windows.Forms.NativeWindow.FromHandle(ctrl.Handle)

    window.DefWndProc(msgResumeUpdate)

    ctrl.Invalidate()

End Sub

End Module
于 2012-03-12T15:47:57.123 回答
3

我知道这是一个老问题,已经回答了,但这是我对此的看法;我将更新的暂停重构为 IDisposable - 这样我就可以将要运行的语句包含在using语句中。

class SuspendDrawingUpdate : IDisposable
{
    private const int WM_SETREDRAW = 0x000B;
    private readonly Control _control;
    private readonly NativeWindow _window;

    public SuspendDrawingUpdate(Control control)
    {
        _control = control;

        var msgSuspendUpdate = Message.Create(_control.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero);

        _window = NativeWindow.FromHandle(_control.Handle);
        _window.DefWndProc(ref msgSuspendUpdate);
    }

    public void Dispose()
    {
        var wparam = new IntPtr(1);  // Create a C "true" boolean as an IntPtr
        var msgResumeUpdate = Message.Create(_control.Handle, WM_SETREDRAW, wparam, IntPtr.Zero);

        _window.DefWndProc(ref msgResumeUpdate);

        _control.Invalidate();
    }
}
于 2015-01-05T23:56:50.853 回答
2

这甚至更简单,也许也很老套—​​—因为我可以在这个线程上看到很多 GDI 肌肉,而且显然只适合某些场景。YMMV

在我的场景中,我使用我称之为“父级”的用户控件——在Load事件期间,我只是从父级的.Controls集合中删除要操作的控件,父OnPaint级负责完全绘制孩子以任何特殊方式进行控制.. 使孩子的绘画能力完全离线。

现在,我将我的孩子绘画例程交给基于 Mike Gold 的这个概念的扩展方法,用于打印 windows 窗体

在这里,我需要一组标签来垂直于布局呈现:

Visual Studio IDE 的简单图表

ParentUserControl.Load然后,我在事件处理程序中使用以下代码使子控件免于绘制:

Private Sub ParentUserControl_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    SetStyle(ControlStyles.UserPaint, True)
    SetStyle(ControlStyles.AllPaintingInWmPaint, True)

    'exempt this control from standard painting: 
    Me.Controls.Remove(Me.HostedControlToBeRotated) 
End Sub

然后,在同一个 ParentUserControl 中,我们从头开始绘制要操作的控件:

Protected Overrides Sub OnPaint(e As PaintEventArgs)
    'here, we will custom paint the HostedControlToBeRotated instance...

    'twist rendering mode 90 counter clockwise, and shift rendering over to right-most end 
    e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
    e.Graphics.TranslateTransform(Me.Width - Me.HostedControlToBeRotated.Height, Me.Height)
    e.Graphics.RotateTransform(-90)
    MyCompany.Forms.CustomGDI.DrawControlAndChildren(Me.HostedControlToBeRotated, e.Graphics)

    e.Graphics.ResetTransform()
    e.Graphics.Dispose()

    GC.Collect()
End Sub

一旦您将 ParentUserControl 托管在某处,例如 Windows 窗体 - 我发现我的 Visual Studio 2015在设计时和运行时正确 呈现窗体:ParentUserControl 托管在 Windows 窗体或其他用户控件中

现在,由于我的特定操作将子控件旋转了 90 度,我确信该区域中的所有热点和交互性都已被破坏 - 但是,我要解决的问题都是需要预览和打印的包装标签,这对我来说很好。

如果有办法将热点和控制性重新引入我故意孤立的控制中 - 我很想有朝一日了解这一点(当然不是为了这种情况,但是......只是为了学习)。当然,WPF 支持这种疯狂的 OOTB.. 但是.. 嘿.. WinForms 仍然很有趣,对吧?

于 2016-01-08T17:13:27.940 回答
-4

或者只是使用Control.SuspendLayout()and Control.ResumeLayout()

于 2012-01-14T15:22:38.380 回答