¿Cómo suspendo la pintura para un control y sus hijos?

184

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?

Simón
fuente
3
¿Puede alguien explicarme qué es dibujar o pintar aquí en este contexto? (Soy nuevo en .net) al menos proporcionar un enlace.
Mr_Green
1
Es realmente una pena (o ridículo) que .Net haya estado fuera por más de 15 años, y esto sigue siendo un problema. Si Microsoft dedicó tanto tiempo a solucionar problemas reales como el parpadeo de la pantalla como, por ejemplo, Obtener malware de Windows X , entonces esto se habría solucionado hace mucho tiempo.
jww
@jww Lo arreglaron; Se llama WPF.
philu

Respuestas:

304

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

C # Jitter

Suspender diseños

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
ng5000
fuente
45
¡Qué gran respuesta y una tremenda ayuda! Hice los métodos de extensión SuspendDrawing y ResumeDrawing para la clase Control, por lo que puedo llamarlos para cualquier control en cualquier contexto.
Zach Johnson el
2
Tenía un control similar a un árbol que solo refrescaría un nodo correctamente al reorganizar a sus hijos si colapsabas y luego lo expandías, lo que conducía a un parpadeo feo. Esto funcionó perfectamente para evitarlo. ¡Gracias! (Sorprendentemente suficiente, el control ya importó SendMessage, y define WM_SETREDRAW, pero en realidad no lo utilizan para nada Ahora que lo hace..)
neminem
77
Esto no es particularmente útil. Esto es exactamente lo que la Controlclase base para todos los controles WinForms ya hace para los métodos BeginUpdatey 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.
Cody Gray
13
@Cody Gray - TableLayoutPanels no tiene BeginUpdate, por ejemplo.
TheBlastOne
44
Tenga cuidado si su código hace posible llamar a estos métodos antes de que se muestre el control; la llamada a Control.Handleforzará 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 SuspendDrawingantemano, su movimiento será más lento. Probablemente debería tener if (!parent.IsHandleCreated) returncontroles en ambos métodos.
Oatsoda
53

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();
    }
}
ceztko
fuente
Lo he intentado, pero en la etapa intermedia entre Suspender y Reanudar, es simplemente invalidar. Puede sonar extraño, pero ¿puede mantener su estado antes de suspenderlo en el estado transitorio?
usuario
2
Suspender un control significa que no se realizará ningún dibujo en el área de control y puede obtener sobras de otras ventanas / controles en esa superficie. Puede intentar usar un control con DoubleBuffer establecido en verdadero si desea que se dibuje el "estado" anterior hasta que se reanude el control (si entendí lo que quería decir), pero no puedo garantizar que funcione. De todos modos, creo que te estás perdiendo el punto de usar esta técnica: está destinado a evitar que el usuario vea un dibujo progresivo y lento de los objetos (mejor verlos aparecer juntos). Para otras necesidades, use otras técnicas.
ceztko
44
Buena respuesta, pero sería mucho mejor mostrar dónde Messagey dónde NativeWindowestán; buscar documentación para una clase llamada Messageno es realmente tan entretenido.
Darda
1
@pelesl 1) use la importación automática de espacio de nombres (haga clic en el símbolo, ALT + Shift + F10), 2) el comportamiento esperado es el de los niños que no están siendo pintados por Invalidate (). Debe suspender el control principal solo por un corto período de tiempo y reanudarlo cuando se invaliden todos los elementos secundarios relevantes.
ceztko
2
Invalidate()no funciona tan bien como a Refresh()menos que siga uno de todos modos.
Eugene Ryabtsev
16

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 SuspendDrawingcon un ResumeDrawing. Por lo tanto, probablemente no sería una buena idea hacerlos públicos.

Ozgur Ozcitak
fuente
44
Esto ayuda a mantener en equilibrio las dos llamadas: SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }. Otra opción es implementar esto en una IDisposableclase y encerrar la parte del dibujo en una usingdeclaración. El identificador se pasaría al constructor, que suspendería el dibujo.
Olivier Jacot-Descombes
Antigua pregunta, lo sé, pero enviar "falso" no parece funcionar en versiones recientes de c # / VS. Tuve que cambiar falso a 0 y verdadero a 1.
Maury Markowitz
@MauryMarkowitz, ¿tu DllImportdeclaras wParamcomo bool?
Ozgur Ozcitak
Hola, rechazar esta respuesta fue un accidente, lo siento.
Geoff
13

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();
});
Jonathan H
fuente
1
¿Qué pasa cuando action()lanza una excepción? (Use un intento / finalmente)
David Sherret
8

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;
    }
}
Eugenio De Hoyos
fuente
2
Esa es ciertamente una técnica útil, una que uso con bastante frecuencia para ListViews, pero en realidad no evita que ocurran los redibujos; Todavía ocurren fuera de la pantalla.
Simon
44
Tiene razón, resuelve el problema de parpadeo, no específicamente el problema de redibujado fuera de la pantalla. Cuando estaba buscando una solución para el parpadeo, me encontré con varios hilos relacionados como este, y cuando lo encontré, es posible que no lo haya publicado en el hilo más relevante. Sin embargo, cuando la mayoría de las personas quieren suspender la pintura, probablemente se estén refiriendo a la pintura en pantalla, que a menudo es un problema más obvio que la pintura redundante fuera de la pantalla, por lo que todavía creo que otros espectadores podrían encontrar esta solución útil en este hilo.
Eugenio De Hoyos
Anular OnPaint.
6

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
}
Koray
fuente
4

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
goughy000
fuente
44
Estoy trabajando con una aplicación WPF que usa winforms entremezclados con los formularios wpf y estoy lidiando con el parpadeo de la pantalla. Estoy confundido sobre cómo se debe aprovechar este código: ¿iría esto en la ventana winform o wpf? ¿O no es esto adecuado para mi situación particular?
nocarrier
3

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 usingdeclaració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();
    }
}
Scott Baker
fuente
2

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é LoadControl de usuario "principal", y durante el evento, simplemente elimino el control para ser manipulado de la .Controlscolecció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:

diagrama simple de su IDE de Visual Studio

Luego, eximo el control infantil de ser pintado, con este código en el ParentUserControl.Loadcontrolador 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: ParentUserControl alojado en un formulario de Windows o quizás otro control de usuario

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?

bkwdesign
fuente
-4

O simplemente use Control.SuspendLayout()y Control.ResumeLayout().

JustJoost
fuente
9
El diseño y la pintura son dos cosas diferentes: el diseño es cómo se organizan los controles secundarios en su contenedor.
Larry