如何为控件及其子控件暂停绘画?


184

我有一个控件,我必须对其进行较大的修改。我想完全阻止它重绘,而SuspendLayout和ResumeLayout不够。如何为控件及其子控件暂停绘画?


3
有人可以在这种情况下解释我在这里绘画吗?(我是.net的新手)至少提供一个链接。
Mr_Green

1
.Net已经使用了15年以上,真是可耻(或可笑),但这仍然是一个问题。如果Microsoft将大量时间用于解决屏幕闪烁之类的实际问题,例如Get Windows X恶意软件,那么该问题将在很久以前得到解决。
jww '17

@jww他们确实修复了它;它称为WPF。
philu

Answers:


304

在我之前的工作中,我们一直在努力获取丰富的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, 0)
        If Redraw Then
            Target.Refresh()
        End If
    End Sub

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

    <Extension()>
    Public Sub ResumeDrawing(ByVal Target As Control)
        ResumeDrawing(Target, True)
    End Sub
End Module

44
多么好的答案和巨大的帮助!我为Control类制作了SuspendDrawing和ResumeDrawing扩展方法,因此可以在任何上下文中为任何控件调用它们。
扎克·约翰逊2009年

2
有一个类似树的控件,如果折叠然后展开它,则只有在重新排列节点的子节点时才能正确刷新节点,这会导致丑陋的闪烁。这样可以很好地解决它。谢谢!(有趣的是,该控件已经导入了SendMessage,定义了WM_SETREDRAW,但实际上并未将其用于任何事情。现在可以了。)
neminem 2011年

7
这不是特别有用。这正是所有 WinForms控件的Control基类已经对和方法进行的操作。自己发送消息并不比使用这些方法为您完成繁重的工作更好,而且肯定不会产生不同的结果。BeginUpdateEndUpdate
科迪·格雷

13
@Cody Gray –例如,TableLayoutPanels没有BeginUpdate。
TheBlastOne

4
请注意,如果您的代码允许在显示控件之前调用这些方法,则该调用Control.Handle将强制创建窗口句柄并可能影响性能。例如,如果您要在窗体上显示控件之前将其移动,则如果SuspendDrawing事先调用它,则移动速度会变慢。可能if (!parent.IsHandleCreated) return两种方法都应进行检查。
oatsoda 2014年

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();
    }
}

我已经尝试过了,但是在Suspend和Resume之间的中间阶段,它只是无效的。听起来可能很怪异,但是可以在暂挂为暂态之前保持其状态吗?
用户

2
暂停控件意味着将完全不在控件区域中执行任何绘制,并且您可能会从该表面中的其他窗口/控件中绘制剩余的内容。如果希望在恢复控件之前绘制以前的“状态”,则可以尝试使用DoubleBuffer设置为true的控件(如果我理解您的意思),但是我不能保证它会起作用。无论如何,我认为您没有使用此技术的要点:这是为了避免用户看到渐进且缓慢的对象绘制(更好地看到所有对象一起出现)。对于其他需求,请使用其他技术。
ceztko 2012年

4
好的答案,但是显示位置MessageNativeWindow位置会更好。在文档中搜索名为的类Message并不是真正有趣的事情。
darda 2014年

1
@pelesl 1)使用自动命名空间导入(单击符号,ALT + Shift + F10),2)预期的行为不是由Invalidate()绘制的子级。您应该仅将父控件暂停一小段时间,并在所有相关子控件都无效时恢复它。
ceztko 2014年

2
Invalidate()Refresh()除非跟着一个跟随,否则效果不佳。
Eugene Ryabtsev

16

我通常会使用ngLink的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();
        }
    }
}

这允许嵌套挂起/继续调用。您必须确保以满足每SuspendDrawing一个ResumeDrawing。因此,将其公开可能不是一个好主意。


4
这有助于使两个通话保持平衡:SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }。另一种选择是在一个IDisposable类中实现此功能,并将绘图部分包含在using-statement中。该句柄将传递给构造函数,该构造函数将暂停绘制。
Olivier Jacot-Descombes 2014年

我知道有个老问题,但是发送“ false”在最新版本的c#/ VS中似乎不起作用。我不得不改变假为0,真为1
莫里维茨

@MauryMarkowitz DllImport声明wParambool吗?
Ozgur Ozcitak

嗨,对这个答案不满意是一个意外,对不起!
Geoff

13

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

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();
});

1
那什么时候action()抛出异常呢?(尝试一下/最后尝试)
David Sherret

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;
    }
}

2
这当然是一种有用的技术-我经常在ListViews中使用的一种技术-但实际上并不能阻止重绘的发生。它们仍然发生在屏幕外。
西蒙2010年

4
没错,它可以解决闪烁问题,而不是专门解决屏幕外重画问题。当我正在寻找一种解决闪烁的方法时,我遇到了像这样的几个相关线程,当我发现它时,可能没有将其发布到最相关的线程中。但是,当大多数人想要暂停绘画时,他们可能是指在屏幕上绘画,这通常比多余的屏幕外绘画更明显,因此,我仍然认为其他观众可能会发现此解决方案对此线程有所帮助。
Eugenio De Hoyos,2010年

覆盖OnPaint。

6

基于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
}

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

4
我正在使用WPF应用程序,该应用程序将Winforms与WPF表单混合使用,并处理屏幕闪烁。我对应该如何利用此代码感到困惑-这将在winform或wpf窗口中进行吗?还是这不适合我的特定情况?
nocarrier

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();
    }
}

2

这甚至更简单,也可能很棘手-因为我可以在该线程上看到很多GDI肌肉,并且显然仅适合某些情况。青年汽车

在我的场景中,我使用的是我称为“父母”的UserControl-在Load活动期间,我只是从Parent的.Controls集合中删除了要操作的控件,而Parent 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 在设计时和运行时正确呈现了窗体Windows窗体中托管的ParentUserControl或其他用户控件

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

如果有办法将热点和控制力重新引入我有目的的孤立控制中-我很想有一天了解这一点(当然,不是针对这种情况,而是..学习)。当然,WPF支持这种疯狂的OOTB。但是..嘿。WinForms仍然很有趣,对吗?


-4

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


9
布局和绘画是两个不同的东西:布局是子控件在其容器中的排列方式。
拉里
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.