如何为控件及其子控件挂起绘制?

我有一个控制,我必须作出大的修改。我想完全防止它重新绘制,而我这样做-暂停布局和简历布局是不够的。如何为控件及其子控件挂起绘制?

117650 次浏览

在我以前的工作中,我们努力让我们的富 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();
}
}

有更全面的讨论-谷歌 C # 和 WM _ SETREDRAW,例如。

C # Jitter

暂停布局

对于那些可能关心这个问题的人,这是 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

我通常使用 ngLink 的 回答的一个修改版本。

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匹配。因此,将它们公开可能不是一个好主意。

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

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

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


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

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

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

下面是 ceztko 和 ng5000的组合,它们带来了一个不使用 pcall 的 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

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

帮助不忘重新启用绘画:

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

这是更简单的,也可能是粗糙的——因为我可以在这个帖子上看到很多 GDI 肌肉,是 显然只适合某些情况。 YMMV

在我的场景中,我使用我称之为“ Parent”的用户控件——在 Load事件中,我只是从 Parent 的 .Controls集合中移除了待操作的控件,而 Parent 的 OnPaint则负责以任何特殊的方式完全绘制子控件。.让孩子的绘画能力完全脱机。

现在,我将我的子类油漆例程传递给一个基于此 从麦克戈尔德的概念打印窗口形式的扩展方法。

在这里,我需要一个子集的标签渲染 垂直的的布局:

simple diagram of your 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 hosted in a Windows Form or perhaps other user control

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

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

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