【问题标题】:How do I suspend painting for a control and its children?如何暂停控件及其子控件的绘制?
【发布时间】:2010-10-04 00:17:59
【问题描述】:

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

【问题讨论】:

  • 有人能解释一下在这种情况下什么是绘画或绘画吗? (我是 .net 的新手)至少提供一个链接。
  • .Net 已经出现超过 15 年真的很遗憾(或可笑),这仍然是一个问题。如果微软在解决屏幕闪烁等实际问题上花费的时间与 Get Windows X 恶意软件一样多,那么这个问题早就解决了。
  • @jww 他们确实修复了它;它被称为 WPF。

标签: c# .net winforms paint


【解决方案1】:

在我之前的工作中,我们一直在努力让我们丰富的 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# Jitter

Suspending Layouts

对于谁来说,这是一个类似的 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

【讨论】:

  • 多么棒的答案和巨大的帮助!我为 Control 类创建了 SuspendDrawing 和 ResumeDrawing 扩展方法,因此我可以在任何上下文中为任何控件调用它们。
  • 有一个树状控件,如果你折叠然后展开它,它只会在重新排列其子节点时正确刷新节点,这会导致难看的闪烁。这完美地解决了它。谢谢! (有趣的是,控件已经导入了 SendMessage,定义了 WM_SETREDRAW,但实际上并没有将它用于任何事情。现在它可以了。)
  • 这不是特别有用。这正是 所有 WinForms 控件的Control 基类已经为BeginUpdateEndUpdate 方法所做的。自己发消息不比用那些方法替你做繁重的工作更好,当然也不能产生不同的结果。
  • @Cody Gray -- 例如,TableLayoutPanel 没有 BeginUpdate。
  • 如果您的代码可以在控件显示之前调用这些方法,请小心 - 对 Control.Handle 的调用将强制创建窗口句柄并可能影响性能。例如,如果您在显示之前在窗体上移动控件,如果您事先调用此SuspendDrawing,您的移动速度会更慢。可能应该在这两种方法中都有if (!parent.IsHandleCreated) return 检查。
【解决方案2】:

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

【讨论】:

  • 我试过了,但是在暂停和恢复之间的中间阶段,它只是无效。听起来可能很奇怪,但它可以在暂态暂停之前保持其状态吗?
  • 暂停控件意味着在控件区域中根本不会执行任何绘图,并且您可能会从该表面中的其他窗口/控件中得到剩余的绘图。如果您希望在控件恢复之前绘制先前的“状态”(如果我理解您的意思),您可以尝试使用将 DoubleBuffer 设置为 true 的控件,但我不能保证它会起作用。无论如何,我认为您错过了使用这种技术的重点:它旨在避免用户看到对象的渐进和缓慢绘制(最好看到所有对象一起出现)。对于其他需求,请使用其他技术。
  • 不错的答案,但最好显示MessageNativeWindow 的位置;搜索名为Message 的类的文档并不是那么有趣。
  • @pelesl 1) 使用自动命名空间导入(单击符号,ALT+Shift+F10),2) 不被 Invalidate() 绘制的子项是预期的行为。您应该只暂停父控件一小段时间,并在所有相关子控件无效时恢复它。
  • Invalidate() 不如Refresh() 好用,除非后面跟着一个。
【解决方案3】:

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

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

【讨论】:

  • 这有助于保持两个呼叫平衡:SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }。另一种选择是在IDisposable 类中实现这一点,并将绘图部分包含在using 语句中。句柄将被传递给构造函数,该构造函数将暂停绘图。
  • 老问题,我知道,但在最新版本的 c#/VS 中发送“false”似乎不起作用。我不得不将 false 更改为 0,将 true 更改为 1。
  • @MauryMarkowitz 是否将您的 DllImport 声明为 wParambool
  • 嗨,否决这个答案是个意外,对不起!
【解决方案4】:

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

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

【讨论】:

  • action() 抛出异常怎么办? (使用 try/finally)
【解决方案5】:

根据 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
}

【讨论】:

    【解决方案6】:

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

    与往常一样,只需在您的 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;
        }
    }
    

    【讨论】:

    • 这当然是一种有用的技术——我经常在 ListViews 中使用它——但它实际上并不能阻止重绘的发生;它们仍然发生在屏幕外。
    • 你是对的,它解决了闪烁问题,而不是专门解决离屏重绘问题。当我在寻找闪烁的解决方案时,我遇到了几个像这样的相关线程,当我找到它时,我可能没有将它发布在最相关的线程中。但是,当大多数人想要暂停绘画时,他们可能指的是在屏幕上绘画,这通常比多余的屏幕外绘画更明显的问题,所以我仍然认为其他观众可能会发现这个解决方案对这个线程有帮助。
    • 覆盖 OnPaint。
    【解决方案7】:

    这里结合了 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
    

    【讨论】:

    • 我正在使用 WPF 应用程序,该应用程序使用与 wpf 表单混合的 winforms,并处理屏幕闪烁。我对应该如何利用这段代码感到困惑——这会出现在 winform 或 wpf 窗口中吗?还是这不适合我的特殊情况?
    【解决方案8】:

    我知道这是一个老问题,已经回答了,但这是我对此的看法;我将更新的暂停重构为 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();
        }
    }
    

    【讨论】:

      【解决方案9】:

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

      在我的场景中,我使用我称之为“父级”用户控件 - 在 Load 事件期间,我只需从父级的 .Controls 集合中删除要操作的控件,然后父母的OnPaint 负责以任何特殊方式完全绘制子控件。完全使子控件的绘制功能脱机。

      现在,我将我的孩子绘制例程交给基于此concept from Mike Gold for printing windows forms 的扩展方法。

      这里我需要一个标签子集来呈现垂直到布局:

      然后,我在 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 在设计时和运行时正确呈现 窗体

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

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

      【讨论】:

        【解决方案10】:

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

        【讨论】:

        • 布局和绘画是两个不同的东西:布局是子控件在其容器中的排列方式。
        猜你喜欢
        • 2013-01-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-09-26
        相关资源
        最近更新 更多