Wie setze ich das Malen für ein Steuerelement und seine Kinder aus?

184

Ich habe ein Steuerelement, an dem ich große Änderungen vornehmen muss. Ich möchte dabei vollständig verhindern, dass es neu gezeichnet wird - SuspendLayout und ResumeLayout reichen nicht aus. Wie setze ich das Malen für ein Steuerelement und seine Kinder aus?

Simon
quelle
3
Kann mir bitte jemand erklären, was hier in diesem Zusammenhang Zeichnen oder Malen ist? (Ich bin neu bei .net) Zumindest einen Link bereitstellen.
Mr_Green
1
Es ist wirklich eine Schande (oder lächerlich), dass .Net seit über 15 Jahren nicht mehr verfügbar ist, und dies ist immer noch ein Problem. Wenn Microsoft so viel Zeit damit verbracht hätte, echte Probleme wie das Flackern des Bildschirms zu beheben, wie beispielsweise Windows X- Malware abzurufen, wäre dies vor langer Zeit behoben worden.
JWW
@jww Sie haben es behoben; es heißt WPF.
Philu

Antworten:

304

Bei meinem vorherigen Job hatten wir Probleme damit, dass unsere umfangreiche UI-App sofort und reibungslos malt. Wir verwendeten Standard-.NET-Steuerelemente, benutzerdefinierte Steuerelemente und Devexpress-Steuerelemente.

Nach vielem googeln und Reflektornutzung stieß ich auf die Win32-Nachricht WM_SETREDRAW. Dadurch wird das Zeichnen von Steuerelementen wirklich gestoppt, während Sie sie aktualisieren, und es kann IIRC auf das übergeordnete / enthaltende Bedienfeld angewendet werden.

Dies ist eine sehr sehr einfache Klasse, die zeigt, wie diese Nachricht verwendet wird:

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

Es gibt ausführlichere Diskussionen dazu - Google für C # und WM_SETREDRAW, z

C # Jitter

Layouts anhalten

Und wen es betrifft, ist dies ein ähnliches Beispiel in 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
quelle
44
Was für eine großartige Antwort und eine enorme Hilfe! Ich habe die Erweiterungsmethoden SuspendDrawing und ResumeDrawing für die Control-Klasse erstellt, damit ich sie für jedes Steuerelement in jedem Kontext aufrufen kann.
Zach Johnson
2
Hatte ein baumartiges Steuerelement, das einen Knoten nur dann richtig aktualisiert, wenn seine untergeordneten Elemente neu angeordnet wurden, wenn Sie zusammenbrachen und ihn dann erweiterten, was zu hässlichem Flackern führte. Dies funktionierte perfekt, um es zu umgehen. Vielen Dank! (Amüsanterweise importierte das Steuerelement SendMessage bereits und definierte WM_SETREDRAW, verwendete es jedoch nicht für irgendetwas. Jetzt schon.)
neminem
7
Dies ist nicht besonders nützlich. Genau das macht die ControlBasisklasse für alle WinForms-Steuerelemente bereits für die Methoden BeginUpdateund EndUpdate. Das Senden der Nachricht selbst ist nicht besser als das Verwenden dieser Methoden, um das schwere Heben für Sie zu erledigen, und kann mit Sicherheit keine unterschiedlichen Ergebnisse erzielen.
Cody Gray
13
@Cody Gray - TableLayoutPanels haben beispielsweise kein BeginUpdate.
TheBlastOne
4
Seien Sie vorsichtig, wenn Ihr Code das Aufrufen dieser Methoden ermöglicht, bevor das Steuerelement angezeigt wurde. Der Aufruf von Control.Handleerzwingt das Erstellen des Fensterhandles und kann die Leistung beeinträchtigen. Wenn Sie beispielsweise ein Steuerelement in einem Formular SuspendDrawingverschoben haben, bevor es angezeigt wurde , ist Ihre Verschiebung langsamer , wenn Sie dies zuvor aufrufen . Wahrscheinlich sollten if (!parent.IsHandleCreated) returnbeide Methoden überprüft werden.
Oatsoda
53

Das Folgende ist die gleiche Lösung von ng5000, verwendet jedoch nicht 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
quelle
Ich habe es versucht, aber in der Zwischenphase zwischen Suspend und Resume ist es nur ungültig. Es mag seltsam klingen, aber kann es einfach seinen Zustand halten, bevor es im Übergangszustand suspendiert wird?
Benutzer
2
Das Anhalten eines Steuerelements bedeutet, dass im Kontrollbereich überhaupt keine Zeichnung ausgeführt wird und möglicherweise Reste von anderen Fenstern / Steuerelementen in dieser Oberfläche gezeichnet werden. Sie können versuchen, ein Steuerelement zu verwenden, bei dem DoubleBuffer auf true gesetzt ist, wenn der vorherige "Status" gezeichnet werden soll, bis das Steuerelement wieder aufgenommen wird (wenn ich verstanden habe, was Sie gemeint haben), aber ich kann nicht garantieren, dass es funktioniert. Wie auch immer, ich denke, Sie verpassen den Sinn dieser Technik: Sie soll verhindern, dass der Benutzer ein progressives und langsames Zeichnen von Objekten sieht (besser, wenn alle zusammen erscheinen). Verwenden Sie für andere Anforderungen andere Techniken.
Ceztko
4
Schöne Antwort, aber es wäre viel besser zu zeigen, wo Messageund wo NativeWindow; Das Durchsuchen der Dokumentation nach einer Klasse namens Messageist nicht wirklich unterhaltsam.
Darda
1
@pelesl 1) Verwenden Sie den automatischen Namespace-Import (klicken Sie auf das Symbol, ALT + Umschalt + F10). 2) Kinder, die nicht von Invalidate () gezeichnet werden, sind das erwartete Verhalten. Sie sollten das übergeordnete Steuerelement nur für eine kurze Zeitspanne aussetzen und fortsetzen, wenn alle relevanten untergeordneten Elemente ungültig sind.
Ceztko
2
Invalidate()funktioniert nicht so gut wie Refresh()wenn nicht einer folgt.
Eugene Ryabtsev
16

Normalerweise verwende ich eine etwas modifizierte Version von ngLinks Antwort .

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

Dadurch können Anrufe unterbrochen / fortgesetzt werden. Sie müssen sicherstellen, dass jedes SuspendDrawingmit einem übereinstimmt ResumeDrawing. Daher wäre es wahrscheinlich keine gute Idee, sie öffentlich zu machen.

Ozgur Ozcitak
quelle
4
Dies hilft, beide Anrufe ausgeglichen zu halten : SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }. Eine andere Möglichkeit besteht darin, dies in einer IDisposableKlasse zu implementieren und den Zeichnungsteil in eine usingAnweisung einzuschließen. Das Handle würde an den Konstruktor übergeben, der das Zeichnen aussetzen würde.
Olivier Jacot-Descombes
Alte Frage, ich weiß, aber das Senden von "false" scheint in neueren Versionen von c # / VS nicht zu funktionieren. Ich musste false auf 0 und true auf 1 ändern.
Maury Markowitz
@MauryMarkowitz DllImportdeklariert Ihr wParamals bool?
Ozgur Ozcitak
Hallo, diese Antwort herunterzustimmen war ein Unfall, sorry!
Geoff
13

Um nicht zu vergessen, dass das Zeichnen wieder aktiviert werden kann:

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

Verwendung:

SuspendDrawing(myControl, () =>
{
    somemethod();
});
Jonathan H.
quelle
1
Was ist, wenn action()eine Ausnahme ausgelöst wird? (Verwenden Sie einen Versuch / endlich)
David Sherret
8

Eine schöne Lösung ohne Interop:

Aktivieren Sie wie immer einfach DoubleBuffered = true auf Ihrem CustomControl. Wenn Sie dann Container wie FlowLayoutPanel oder TableLayoutPanel haben, leiten Sie eine Klasse von jedem dieser Typen ab und aktivieren Sie in den Konstruktoren die doppelte Pufferung. Verwenden Sie jetzt einfach Ihre abgeleiteten Container anstelle der Windows.Forms-Container.

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

class FlowLayoutPanel : System.Windows.Forms.FlowLayoutPanel
{
    public FlowLayoutPanel()
    {
        DoubleBuffered = true;
    }
}
Eugenio De Hoyos
quelle
2
Das ist sicherlich eine nützliche Technik - eine, die ich ziemlich oft für ListViews verwende -, aber sie verhindert nicht, dass die Neuzeichnungen stattfinden. Sie passieren immer noch außerhalb des Bildschirms.
Simon
4
Sie haben Recht, es löst das Problem des Flackerns und nicht speziell das Problem des Neuzeichnens außerhalb des Bildschirms. Als ich nach einer Lösung für das Flackern suchte, stieß ich auf mehrere verwandte Themen wie dieses, und als ich sie fand, habe ich sie möglicherweise nicht im relevantesten Thread veröffentlicht. Wenn die meisten Leute das Malen jedoch aussetzen möchten, beziehen sie sich wahrscheinlich auf das Malen auf dem Bildschirm, was oft ein offensichtlicheres Problem ist als das redundante Malen außerhalb des Bildschirms. Daher denke ich immer noch, dass andere Betrachter diese Lösung in diesem Thread hilfreich finden könnten.
Eugenio De Hoyos
OnPaint überschreiben.
6

Basierend auf der Antwort von ng5000 verwende ich gerne diese Erweiterung:

        #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

Verwenden:

using (this.BeginSuspendlock())
{
    //update GUI
}
Koray
quelle
4

Hier ist eine Kombination aus ceztko und ng5000, um eine VB-Erweiterungsversion zu bringen, die kein Pinvoke verwendet

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
quelle
4
Ich arbeite mit einer WPF-App, die Winforms verwendet, die mit den wpf-Formularen gemischt sind, und mit Bildschirmflimmern umgeht. Ich bin verwirrt darüber, wie dieser Code genutzt werden soll - würde dies in das Winform- oder Wpf-Fenster gehen? Oder ist das nicht für meine spezielle Situation geeignet?
Nocarrier
3

Ich weiß, dass dies eine alte Frage ist, die bereits beantwortet wurde, aber hier ist meine Meinung dazu. Ich habe die Aussetzung von Updates in ein IDisposable umgestaltet. Auf diese Weise kann ich die Anweisungen, die ich ausführen möchte, in eine usingAnweisung einschließen .

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
quelle
2

Dies ist noch einfacher und vielleicht hacky - da ich in diesem Thread viel GDI-Muskel sehen kann und offensichtlich nur für bestimmte Szenarien gut geeignet ist. YMMV

In meinem Szenario verwende ich das, was ich als "Eltern" -Benutzersteuerung bezeichne - und während des LoadEreignisses entferne ich einfach das zu manipulierende Steuerelement aus der .ControlsSammlung der Eltern, und die Eltern OnPaintkümmern sich darum, das Kind vollständig zu malen Kontrolle auf welche Art und Weise auch immer. Die Malfähigkeiten des Kindes werden vollständig offline geschaltet.

Jetzt übergebe ich meine Kindermalroutine an eine Erweiterungsmethode, die auf diesem Konzept von Mike Gold zum Drucken von Windows-Formularen basiert .

Hier benötige ich eine Untergruppe von Beschriftungen, um senkrecht zum Layout zu rendern :

einfaches Diagramm Ihrer Visual Studio IDE

Dann befreie ich das untergeordnete Steuerelement mit diesem Code im ParentUserControl.LoadEreignishandler vom Zeichnen:

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

Dann malen wir in demselben ParentUserControl das zu manipulierende Steuerelement von Grund auf neu:

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

Sobald Sie das ParentUserControl irgendwo gehostet haben, z. B. in einem Windows-Formular, stelle ich fest, dass mein Visual Studio 2015 das Formular sowohl zur Entwurfszeit als auch zur Laufzeit korrekt wiedergibt : ParentUserControl, das in einem Windows Form oder einem anderen Benutzersteuerelement gehostet wird

Jetzt, da meine spezielle Manipulation die Kindersteuerung um 90 Grad dreht, bin ich sicher, dass alle Hotspots und Interaktivität in dieser Region zerstört wurden - aber das Problem, das ich gelöst habe, war alles für ein Paketetikett, das in der Vorschau angezeigt und gedruckt werden musste. was für mich gut geklappt hat.

Wenn es Möglichkeiten gibt, die Hot Spots und die Kontrolle wieder in meine absichtlich verwaiste Kontrolle einzuführen, würde ich gerne eines Tages davon erfahren (natürlich nicht für dieses Szenario, aber ... nur um zu lernen). Natürlich unterstützt WPF solche Verrücktheit OOTB .. aber .. hey .. WinForms macht immer noch so viel Spaß, oder?

bkwdesign
quelle
-4

Oder verwenden Sie einfach Control.SuspendLayout()und Control.ResumeLayout().

JustJoost
quelle
9
Layout und Malen sind zwei verschiedene Dinge: Layout ist, wie untergeordnete Steuerelemente in ihrem Container angeordnet sind.
Larry