我有一个控件,我必须对其进行大量修改。我想在这样做时完全防止它重绘 - SuspendLayout 和 ResumeLayout 是不够的。如何暂停控件及其子控件的绘制?
10 回答
在我之前的工作中,我们努力让我们丰富的 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,例如
对于它可能关心的人来说,这是 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
以下是 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();
}
}
我通常使用 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();
}
}
}
这允许嵌套暂停/恢复调用。您必须确保将每个SuspendDrawing
与ResumeDrawing
. 因此,将它们公开可能不是一个好主意。
为了帮助不要忘记重新启用绘图:
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();
});
根据 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
}
不使用互操作的一个很好的解决方案:
与往常一样,只需在您的 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;
}
}
这里结合了 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
我知道这是一个老问题,已经回答了,但这是我对此的看法;我将更新的暂停重构为 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();
}
}
这甚至更简单,也许也很老套——因为我可以在这个线程上看到很多 GDI 肌肉,而且显然只适合某些场景。YMMV
在我的场景中,我使用我称之为“父级”的用户控件——在Load
事件期间,我只是从父级的.Controls
集合中删除要操作的控件,父OnPaint
级负责完全绘制孩子以任何特殊方式进行控制.. 使孩子的绘画能力完全离线。
现在,我将我的孩子绘画例程交给基于 Mike Gold 的这个概念的扩展方法,用于打印 windows 窗体。
在这里,我需要一组标签来垂直于布局呈现:
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 仍然很有趣,对吧?
或者只是使用Control.SuspendLayout()
and Control.ResumeLayout()
。