Tengo un control al que tengo que hacer grandes modificaciones. Me gustaría evitar que se vuelva a dibujar por completo mientras hago eso: SuspendLayout y ResumeLayout no son suficientes. ¿Cómo suspendo la pintura para un control y sus hijos?
Tengo un control al que tengo que hacer grandes modificaciones. Me gustaría evitar que se vuelva a dibujar por completo mientras hago eso: SuspendLayout y ResumeLayout no son suficientes. ¿Cómo suspendo la pintura para un control y sus hijos?
Respuestas:
En mi trabajo anterior, tuvimos problemas para lograr que nuestra rica aplicación de interfaz de usuario pintara al instante y sin problemas. Estábamos usando controles estándar .Net, controles personalizados y controles devexpress.
Después de mucho googlear y usar el reflector, me encontré con el mensaje WM_SETREDRAW win32. Esto realmente detiene el dibujo de controles mientras los actualiza y se puede aplicar, IIRC al panel principal / que contiene.
Esta es una clase muy muy simple que demuestra cómo usar este mensaje:
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();
}
}
Hay discusiones más completas sobre esto: google para C # y WM_SETREDRAW, por ejemplo
Y a quien corresponda, este es un ejemplo similar en 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
Control
clase base para todos los controles WinForms ya hace para los métodos BeginUpdate
y EndUpdate
. Enviar el mensaje usted mismo no es mejor que usar esos métodos para hacer el trabajo pesado por usted, y ciertamente no puede producir resultados diferentes.
Control.Handle
forzará la creación del identificador de ventana y podría afectar el rendimiento. Por ejemplo, si movía un control en un formulario antes de que se mostrara, si llama a esto de SuspendDrawing
antemano, su movimiento será más lento. Probablemente debería tener if (!parent.IsHandleCreated) return
controles en ambos métodos.
La siguiente es la misma solución de ng5000 pero no usa 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();
}
}
Message
y dónde NativeWindow
están; buscar documentación para una clase llamada Message
no es realmente tan entretenido.
Invalidate()
no funciona tan bien como a Refresh()
menos que siga uno de todos modos.
Usualmente uso una pequeña versión modificada de la respuesta de 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();
}
}
}
Esto permite que las llamadas de suspensión / reanudación se aniden. Debes asegurarte de hacer coincidir cada uno SuspendDrawing
con un ResumeDrawing
. Por lo tanto, probablemente no sería una buena idea hacerlos públicos.
SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }
. Otra opción es implementar esto en una IDisposable
clase y encerrar la parte del dibujo en una using
declaración. El identificador se pasaría al constructor, que suspendería el dibujo.
DllImport
declaras wParam
como bool
?
Para ayudar a no olvidar volver a habilitar el dibujo:
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();
}
uso:
SuspendDrawing(myControl, () =>
{
somemethod();
});
action()
lanza una excepción? (Use un intento / finalmente)
Una buena solución sin usar interoperabilidad:
Como siempre, simplemente habilite DoubleBuffered = true en su CustomControl. Luego, si tiene contenedores como FlowLayoutPanel o TableLayoutPanel, obtenga una clase de cada uno de estos tipos y, en los constructores, habilite el almacenamiento en búfer doble. Ahora, simplemente use sus contenedores derivados en lugar de los contenedores Windows.Forms.
class TableLayoutPanel : System.Windows.Forms.TableLayoutPanel
{
public TableLayoutPanel()
{
DoubleBuffered = true;
}
}
class FlowLayoutPanel : System.Windows.Forms.FlowLayoutPanel
{
public FlowLayoutPanel()
{
DoubleBuffered = true;
}
}
Basado en la respuesta de ng5000, me gusta usar esta extensión:
#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
Utilizar:
using (this.BeginSuspendlock())
{
//update GUI
}
Aquí hay una combinación de ceztko y ng5000 para traer una versión de extensiones VB que no usa pinvoke
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
Sé que esta es una vieja pregunta, ya respondida, pero aquí está mi opinión sobre esto; Refactoré la suspensión de actualizaciones en un ID desechable, de esa manera puedo adjuntar las declaraciones que quiero ejecutar en una using
declaración.
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();
}
}
Esto es aún más simple y quizás hacky, ya que puedo ver una gran cantidad de músculo GDI en este hilo , y obviamente solo es una buena opción para ciertos escenarios. YMMV
En mi caso, utilizo lo que denominaré Load
Control de usuario "principal", y durante el evento, simplemente elimino el control para ser manipulado de la .Controls
colección principal y el elemento principalOnPaint
se encargan de pintar completamente al niño control de cualquier manera especial ... desconectando por completo las capacidades de pintura del niño.
Ahora, paso la rutina de pintura de mi hijo a un método de extensión basado en este concepto de Mike Gold para imprimir formularios de Windows .
Aquí necesito un subconjunto de etiquetas para representar perpendicular al diseño:
Luego, eximo el control infantil de ser pintado, con este código en el ParentUserControl.Load
controlador de eventos:
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
Luego, en el mismo ParentUserControl, pintamos el control para ser manipulado desde cero:
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
Una vez que aloje ParentUserControl en algún lugar, por ejemplo, un formulario de Windows, descubro que mi Visual Studio 2015 representa el formulario correctamente en tiempo de diseño y en tiempo de ejecución:
Ahora, dado que mi manipulación particular gira el control infantil 90 grados, estoy seguro de que todos los puntos calientes y la interactividad se han destruido en esa región, pero el problema que estaba resolviendo era todo por una etiqueta de paquete que necesitaba previsualizar e imprimir, que funcionó bien para mí
Si hay formas de reintroducir los puntos calientes y el control de mi control huérfano a propósito, me encantaría aprender sobre eso algún día (no para este escenario, por supuesto, pero ... solo para aprender). Por supuesto, WPF admite esa locura OOTB ... pero ... oye ... WinForms es muy divertido, ¿verdad?