Come sospendo la pittura per un controllo e i suoi figli?


184

Ho un controllo al quale devo apportare grandi modifiche. Vorrei impedirgli completamente di ridisegnare mentre lo faccio - SuspendLayout e ResumeLayout non sono sufficienti. Come sospendo la pittura per un controllo e i suoi figli?


3
qualcuno può spiegarmi cosa sta disegnando o dipingendo qui in questo contesto? (sono nuovo a .net) almeno fornire un link.
Mr_Green,

1
È davvero un peccato (o risibile) che .Net sia stato fuori per oltre 15 anni, e questo è ancora un problema. Se Microsoft impiegasse tanto tempo a risolvere problemi reali come lo sfarfallio dello schermo, ad esempio Ottieni malware per Windows X , questo sarebbe stato risolto molto tempo fa.
1717

@jww Lo hanno riparato; si chiama WPF.
philu

Risposte:


304

Nel mio lavoro precedente abbiamo avuto difficoltà a far sì che la nostra ricca app UI dipingesse istantaneamente e senza intoppi. Stavamo usando controlli .Net standard, controlli personalizzati e controlli devexpress.

Dopo un sacco di utilizzo su Google e sul riflettore mi sono imbattuto nel messaggio win32 WM_SETREDRAW. Questo interrompe veramente il disegno dei controlli mentre li aggiorni e può essere applicato, IIRC al pannello padre / contenitore.

Questa è una classe molto semplice che dimostra come usare questo messaggio:

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

Ci sono discussioni più approfondite su questo: google per C # e WM_SETREDRAW, ad es

C # Jitter

Sospensione dei layout

E a chi può interessare, questo è un esempio simile 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

45
Che grande risposta e un grande aiuto! Ho creato metodi di estensione SuspendDrawing e ResumeDrawing per la classe Control, in modo da poterli chiamare per qualsiasi controllo in qualsiasi contesto.
Zach Johnson,

2
Aveva un controllo simile ad un albero che aggiornava correttamente un nodo solo riorganizzando i suoi figli se lo facessi crollare e poi lo espandeva, il che portava a uno sfarfallio brutto. Questo ha funzionato perfettamente per aggirarlo. Grazie! (Abbastanza divertente, il controllo ha già importato SendMessage e definito WM_SETREDRAW, ma in realtà non lo ha utilizzato per nulla. Ora lo fa.)
neminem

7
Questo non è particolarmente utile. Questo è esattamente ciò che la Controlclasse base per tutti i controlli WinForms fa già per i metodi BeginUpdatee EndUpdate. Inviare il messaggio da soli non è meglio che usare quei metodi per fare il lavoro pesante per te, e sicuramente non può produrre risultati diversi.
Cody Grey

13
@Cody Grey - TableLayoutPanels non ha BeginUpdate, ad esempio.
TheBlastOne,

4
Fai attenzione se il tuo codice rende possibile chiamare questi metodi prima che il controllo sia stato visualizzato: la chiamata a Control.Handleforzerà la creazione della maniglia della finestra e potrebbe influire sulle prestazioni. Ad esempio, se si spostava un controllo su un modulo prima che fosse visualizzato, se lo si chiama in SuspendDrawinganticipo, lo spostamento sarà più lento. Probabilmente dovrebbe avere if (!parent.IsHandleCreated) returncontrolli in entrambi i metodi.
Oatsoda,

53

La seguente è la stessa soluzione di ng5000 ma non 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();
    }
}

L'ho provato, ma nella fase intermedia tra Suspend e Resume, è solo invalidato. Può sembrare strano, ma può solo mantenere il suo stato prima di sospenderlo nello stato transitorio?
utente

2
Sospendere un controllo significa che non verrà eseguito alcun disegno nell'area di controllo e si potrebbero ottenere gli avanzi estratti da altre finestre / controlli in quella superficie. Puoi provare a utilizzare un controllo con DoubleBuffer impostato su true se desideri che lo "stato" precedente venga disegnato fino a quando il controllo non viene ripreso (se ho capito cosa intendevi), ma non posso garantire che funzionerà. Ad ogni modo penso che ti stia perdendo il punto di usare questa tecnica: ha lo scopo di evitare all'utente di vedere un disegno progressivo e lento di oggetti (meglio vederli apparire tutti insieme). Per altre esigenze, utilizzare altre tecniche.
Ceztko,

4
Bella risposta, ma sarebbe molto meglio mostrare dove Messagee dove NativeWindow; cercare documentazione per una classe chiamata Messagenon è poi così divertente.
darda,

1
@pelesl 1) utilizzare l'importazione automatica dello spazio dei nomi (fare clic sul simbolo, ALT + Maiusc + F10), 2) il comportamento previsto non è rappresentato dai bambini non dipinti da Invalidate (). È necessario sospendere il controllo parent solo per un breve periodo di tempo e riprenderlo quando tutti i figli rilevanti sono invalidati.
ceztko,

2
Invalidate()non funziona bene come Refresh()se non seguito da uno comunque.
Eugene Ryabtsev,

16

Di solito uso una versione leggermente modificata della risposta di 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();
        }
    }
}

Ciò consente di sospendere / riprendere le chiamate da annidare. Devi assicurarti di abbinare ciascuno SuspendDrawinga ResumeDrawing. Quindi, probabilmente non sarebbe una buona idea renderli pubblici.


4
Ciò contribuisce a mantenere entrambe le chiamate in equilibrio: SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }. Un'altra opzione è implementarla in una IDisposableclasse e racchiudere la parte del disegno in una usingdichiarazione. La maniglia verrebbe passata al costruttore, che sospenderebbe il disegno.
Olivier Jacot-Descombes il

Vecchia domanda, lo so, ma l'invio di "false" non sembra funzionare nelle versioni recenti di c # / VS. Ho dovuto cambiare falso in 0 e vero in 1.
Maury Markowitz,

@MauryMarkowitz DllImportdichiara wParamcome bool?
Ozgur Ozcitak,

Ciao, il downgrade di questa risposta è stato un incidente, scusa!
Geoff,

13

Per aiutare a non dimenticare di riattivare il disegno:

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

utilizzo:

SuspendDrawing(myControl, () =>
{
    somemethod();
});

1
Che dire di quando action()genera un'eccezione? (Usa un tentativo / finalmente)
David Sherret,

8

Una bella soluzione senza usare l'interoperabilità:

Come sempre, abilita semplicemente DoubleBuffered = true sul tuo CustomControl. Quindi, se si dispone di contenitori come FlowLayoutPanel o TableLayoutPanel, derivare una classe da ciascuno di questi tipi e nei costruttori, abilitare il doppio buffering. Ora, usa semplicemente i tuoi contenitori derivati ​​anziché i contenitori Windows.Forms.

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

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

2
Questa è certamente una tecnica utile - che uso abbastanza spesso per ListViews - ma in realtà non impedisce che si verifichino ridisegni; accadono ancora fuori dallo schermo.
Simon

4
Hai ragione, risolve il problema dello sfarfallio, non il problema di ridisegno fuori dallo schermo in particolare. Quando stavo cercando una soluzione allo sfarfallio, mi sono imbattuto in diversi thread correlati come questo, e quando l'ho trovato, potrei non averlo pubblicato nel thread più rilevante. Tuttavia, quando la maggior parte delle persone vuole sospendere la pittura, probabilmente si riferiscono alla pittura sullo schermo, che è spesso un problema più ovvio della pittura fuori schermo ridondante, quindi penso ancora che altri spettatori potrebbero trovare utile questa soluzione in questo thread.
Eugenio De Hoyos,

Sostituisci OnPaint.

6

Sulla base della risposta di ng5000, mi piace usare questa estensione:

        #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

Uso:

using (this.BeginSuspendlock())
{
    //update GUI
}

4

Ecco una combinazione di ceztko e ng5000 per portare una versione di estensioni VB che non 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

4
Sto lavorando con un'app WPF che utilizza winforms mescolati con i moduli wpf e mi occupo dello sfarfallio dello schermo. Sono confuso su come dovrebbe essere sfruttato questo codice - questo andrebbe nella finestra di winform o wpf? O questo non è adatto alla mia situazione particolare?
nocarrier il

3

So che questa è una vecchia domanda, a cui è già stata data una risposta, ma ecco la mia opinione su questo; Ho riformulato la sospensione degli aggiornamenti in un IDisposable - in questo modo posso racchiudere le dichiarazioni che voglio eseguire in una usingdichiarazione.

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

2

Questo è ancora più semplice, e forse confuso, poiché posso vedere un sacco di muscoli GDI su questo thread , ed è ovviamente solo una buona misura per determinati scenari. YMMV

Nel mio scenario, utilizzo quello che chiamerò UserControl "Parent" - e durante l' Loadevento, rimuovo semplicemente il controllo da manipolare dalla .Controlscollezione del Parent e il ParentOnPaint si occupa di dipingere completamente il bambino controllare in qualsiasi modo speciale .. portando completamente offline le capacità di pittura del bambino.

Ora, passo la mia routine di pittura per bambini a un metodo di estensione basato su questo concetto di Mike Gold per la stampa di moduli Windows .

Qui ho bisogno di un sottoinsieme di etichette per rendere perpendicolare al layout:

semplice diagramma del tuo IDE di Visual Studio

Quindi, esento il controllo figlio dall'essere dipinto, con questo codice nel ParentUserControl.Loadgestore eventi:

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

Quindi, nello stesso ParentUserControl, dipingiamo il controllo da manipolare da zero:

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

Dopo aver ospitato ParentUserControl da qualche parte, ad esempio un Windows Form, sto scoprendo che Visual Studio 2015 esegue correttamente il rendering del modulo in fase di progettazione e in fase di esecuzione: ParentUserControl ospitato in un Windows Form o forse in un altro controllo utente

Ora, poiché la mia particolare manipolazione ruota il controllo figlio di 90 gradi, sono sicuro che tutti i punti caldi e l'interattività sono stati distrutti in quella regione, ma il problema che stavo risolvendo era tutto per un'etichetta del pacchetto che doveva essere visualizzata in anteprima e stampata, che ha funzionato bene per me.

Se ci sono modi per reintrodurre i punti caldi e il controllo del mio controllo volutamente orfano - mi piacerebbe conoscerlo un giorno (non per questo scenario, ovviamente, ma ... solo per imparare). Certo, WPF supporta tale follia OOTB .. ma .. hey .. WinForms è ancora così divertente, vero?


-4

O semplicemente usa Control.SuspendLayout()e Control.ResumeLayout().


9
Layout e pittura sono due cose diverse: il layout è il modo in cui i controlli figlio sono disposti nel loro contenitore.
Larry,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.