Comment suspendre la peinture pour un contrôle et ses enfants?


184

J'ai un contrôle auquel je dois apporter de grandes modifications. Je voudrais l'empêcher complètement de se redessiner pendant que je fais cela - SuspendLayout et ResumeLayout ne suffisent pas. Comment suspendre la peinture pour un contrôle et ses enfants?


3
quelqu'un peut-il m'expliquer ce qu'est le dessin ou la peinture ici dans ce contexte? (Je suis nouveau sur .net) au moins fournir un lien.
Mr_Green

1
C'est vraiment dommage (ou risible) que .Net soit sorti depuis plus de 15 ans, et cela reste un problème. Si Microsoft passait autant de temps à résoudre de vrais problèmes tels que le scintillement de l'écran qu'à obtenir un malware Windows X , cela aurait été corrigé il y a longtemps.
jww

@jww Ils l'ont résolu; ça s'appelle WPF.
philu

Réponses:


304

Lors de mon travail précédent, nous avons eu du mal à faire en sorte que notre riche application d'interface utilisateur peigne instantanément et en douceur. Nous utilisions des contrôles .Net standard, des contrôles personnalisés et des contrôles devexpress.

Après beaucoup d'utilisation de Google et de réflecteur, je suis tombé sur le message WM_SETREDRAW win32. Cela arrête vraiment le dessin des contrôles pendant que vous les mettez à jour et peut être appliqué, IIRC au panneau parent / contenant.

Il s'agit d'une classe très très simple montrant comment utiliser ce message:

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

Il y a des discussions plus complètes à ce sujet - google pour C # et WM_SETREDRAW, par exemple

C # Jitter

Suspendre des mises en page

Et à qui cela peut concerner, voici un exemple similaire 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

45
Quelle belle réponse et une aide formidable! J'ai créé des méthodes d'extension SuspendDrawing et ResumeDrawing pour la classe Control, je peux donc les appeler pour n'importe quel contrôle dans n'importe quel contexte.
Zach Johnson

2
Avait un contrôle en forme d'arbre qui ne rafraîchirait correctement un nœud qu'en réorganisant ses enfants si vous le réduisiez puis le développiez, ce qui entraînait un scintillement horrible. Cela a parfaitement fonctionné pour le contourner. Merci! (Assez amusant, le contrôle a déjà importé SendMessage et défini WM_SETREDRAW, mais ne l'a pas utilisé pour quoi que ce soit. Maintenant, il le fait.)
neminem

7
Ce n'est pas particulièrement utile. Ceci est exactement ce que la Controlclasse de base pour tous les contrôles WinForms fait déjà pour la BeginUpdateet EndUpdateméthodes. Envoyer le message vous-même n'est pas mieux que d'utiliser ces méthodes pour faire le gros du travail à votre place, et ne peut certainement pas produire des résultats différents.
Cody Gray

13
@Cody Gray - Les TableLayoutPanels n'ont pas BeginUpdate, par exemple.
TheBlastOne

4
Soyez prudent si votre code permet d'appeler ces méthodes avant que le contrôle n'ait été affiché - l'appel à Control.Handleforcera la création du handle de fenêtre et pourrait affecter les performances. Par exemple, si vous déplaciez un contrôle sur un formulaire avant qu'il ne soit affiché, si vous appelez cela SuspendDrawingau préalable, votre déplacement sera plus lent. Devrait probablement avoir des if (!parent.IsHandleCreated) returncontrôles dans les deux méthodes.
oatsoda

53

Ce qui suit est la même solution que ng5000 mais n'utilise pas 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();
    }
}

Je l'ai essayé, mais à l'étape intermédiaire entre la suspension et la reprise, c'est juste invalide. Cela peut sembler étrange, mais peut-il simplement conserver son état avant de se suspendre à l'état transitoire?
utilisateur du

2
Suspendre un contrôle signifie qu'aucun dessin ne sera effectué dans la zone de contrôle et que vous pouvez obtenir des restes dessinés à partir d'autres fenêtres / contrôles de cette surface. Vous pouvez essayer d'utiliser un contrôle avec DoubleBuffer défini sur true si vous souhaitez que le précédent «état» soit dessiné jusqu'à ce que le contrôle soit repris (si j'ai bien compris ce que vous vouliez dire), mais je ne peux pas garantir qu'il fonctionnera. Quoi qu'il en soit, je pense que vous manquez l'intérêt d'utiliser cette technique: elle est destinée à éviter à l'utilisateur de voir un dessin progressif et lent des objets (mieux vaut les voir tous apparaître ensemble). Pour d'autres besoins, utilisez d'autres techniques.
ceztko

4
Bonne réponse, mais il vaudrait mieux montrer où Messageet où NativeWindowsont; rechercher de la documentation pour une classe nommée Messagen'est pas vraiment divertissant.
darda

1
@pelesl 1) utiliser l'importation automatique des espaces de noms (cliquez sur le symbole, ALT + Shift + F10), 2) les enfants non peints par Invalidate () sont le comportement attendu. Vous ne devez suspendre le contrôle parent que pendant une courte période et le reprendre lorsque tous les enfants concernés sont invalidés.
ceztko

2
Invalidate()ne fonctionne pas aussi bien qu'à Refresh()moins d' être suivi par un de toute façon.
Eugene Ryabtsev

16

J'utilise généralement une version légèrement modifiée de la réponse 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();
        }
    }
}

Cela permet d'imbriquer les appels de suspension / reprise. Vous devez vous assurer de faire correspondre chacun SuspendDrawingavec un ResumeDrawing. Par conséquent, ce ne serait probablement pas une bonne idée de les rendre publics.


4
Cela aide à garder les appels équilibrés: SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }. Une autre option consiste à implémenter ceci dans une IDisposableclasse et à entourer la partie du dessin dans une usingdéclaration. Le handle serait passé au constructeur, ce qui suspendrait le dessin.
Olivier Jacot-Descombes

Ancienne question, je sais, mais l'envoi de "faux" ne semble pas fonctionner dans les versions récentes de c # / VS. J'ai dû changer faux en 0 et vrai en 1.
Maury Markowitz

@MauryMarkowitz est-ce que vous DllImportdéclarez wParamcomme bool?
Ozgur Ozcitak

Salut, voter contre cette réponse était un accident, désolé!
Geoff

13

Pour vous aider à ne pas oublier de réactiver le dessin:

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

usage:

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

1
Et quand action()lance une exception? (Utilisez un essai / enfin)
David Sherret

8

Une belle solution sans utiliser l'interopérabilité:

Comme toujours, activez simplement DoubleBuffered = true sur votre CustomControl. Ensuite, si vous avez des conteneurs tels que FlowLayoutPanel ou TableLayoutPanel, dérivez une classe de chacun de ces types et dans les constructeurs, activez la double mise en mémoire tampon. Maintenant, utilisez simplement vos conteneurs dérivés au lieu des conteneurs Windows.Forms.

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

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

2
C'est certainement une technique utile - une technique que j'utilise assez souvent pour les ListViews - mais elle n'empêche pas réellement les redessins de se produire; ils se produisent toujours hors écran.
Simon

4
Vous avez raison, cela résout le problème de scintillement, pas spécifiquement le problème de redessiner hors écran. Lorsque je cherchais une solution au scintillement, je suis tombé sur plusieurs fils connexes comme celui-ci, et quand je l'ai trouvé, je ne l'aurais peut-être pas publié dans le fil le plus pertinent. Cependant, lorsque la plupart des gens veulent suspendre la peinture, ils font probablement référence à la peinture à l'écran, ce qui est souvent un problème plus évident que la peinture hors écran redondante.Je pense donc que d'autres téléspectateurs pourraient trouver cette solution utile dans ce fil.
Eugenio De Hoyos

Remplacez OnPaint.

6

Sur la base de la réponse de ng5000, j'aime utiliser cette extension:

        #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

Utilisation:

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

4

Voici une combinaison de ceztko et de ng5000 pour apporter une version d'extensions VB qui n'utilise pas 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
Je travaille avec une application WPF qui utilise des winforms mélangés avec les formulaires wpf et qui gère le scintillement de l'écran. Je ne sais pas comment ce code devrait être exploité - cela irait-il dans la fenêtre winform ou wpf? Ou n'est-ce pas adapté à ma situation particulière?
nocarrier

3

Je sais que c'est une vieille question à laquelle on a déjà répondu, mais voici mon point de vue à ce sujet; J'ai refactorisé la suspension des mises à jour dans un IDisposable - de cette façon, je peux inclure les déclarations que je veux exécuter dans une usingdéclaration.

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

C'est encore plus simple, et peut-être hacky - car je peux voir beaucoup de muscle GDI sur ce fil , et ce n'est évidemment qu'un bon choix pour certains scénarios.YMMV

Dans mon scénario, j'utilise ce que j'appellerai un UserControl "Parent" - et pendant l' Loadévénement, je supprime simplement le contrôle à manipuler de la .Controlscollection Parent et le Parent'sOnPaint s'occupe de peindre complètement l'enfant contrôler de quelque manière que ce soit ... en mettant pleinement hors ligne les capacités de peinture de l'enfant.

Maintenant, je confie la routine de peinture de mon enfant à une méthode d'extension basée sur ce concept de Mike Gold pour l'impression de formulaires Windows .

Ici, j'ai besoin d'un sous-ensemble d'étiquettes pour rendre perpendiculaire à la mise en page:

diagramme simple de votre IDE Visual Studio

Ensuite, j'exempte le contrôle enfant d'être peint, avec ce code dans le ParentUserControl.Loadgestionnaire d'événements:

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

Ensuite, dans le même ParentUserControl, nous peignons le contrôle à manipuler à partir de zéro:

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

Une fois que vous hébergez le ParentUserControl quelque part, par exemple un formulaire Windows - je constate que mon Visual Studio 2015 rend le formulaire correctement au moment du design ainsi qu'au moment de l'exécution: ParentUserControl hébergé dans un Windows Form ou peut-être un autre contrôle utilisateur

Maintenant, comme ma manipulation particulière fait pivoter le contrôle enfant de 90 degrés, je suis sûr que tous les points chauds et l'interactivité ont été détruits dans cette région - mais le problème que je résolvais concernait uniquement une étiquette d'emballage qui devait être prévisualisée et imprimée, qui a bien fonctionné pour moi.

S'il y a des moyens de réintroduire les points chauds et le contrôle dans mon contrôle volontairement orphelin - j'aimerais en savoir plus un jour (pas pour ce scénario, bien sûr, mais ... juste pour apprendre). Bien sûr, WPF prend en charge une telle folie OOTB .. mais .. hé .. WinForms est encore tellement amusant, non?


-4

Ou utilisez simplement Control.SuspendLayout()et Control.ResumeLayout().


9
La disposition et la peinture sont deux choses différentes: la disposition est la façon dont les contrôles enfants sont organisés dans leur conteneur.
Larry
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.