Como suspiro a pintura de um controle e de seus filhos?

184

Eu tenho um controle que eu tenho que fazer grandes modificações. Eu gostaria de impedi-lo completamente de redesenhar enquanto faço isso - SuspendLayout e ResumeLayout não são suficientes. Como suspiro a pintura de um controle e de seus filhos?

Simon
fonte
3
alguém pode me explicar o que é desenho ou pintura aqui neste contexto? (eu sou novo no .net) pelo menos fornecer um link.
18712 Mr_Green
1
É realmente uma pena (ou risível) que o .Net esteja fora há mais de 15 anos, e isso ainda é um problema. Se a Microsoft dedicasse tanto tempo a consertar problemas reais, como a tela tremer como, por exemplo, obter malware para o Windows X , isso já teria sido corrigido há muito tempo.
JWW
@jww Eles consertaram; chama-se WPF.
philu 22/06

Respostas:

304

No meu trabalho anterior, lutamos para fazer com que nosso aplicativo de interface do usuário rico pintasse instantaneamente e sem problemas. Estávamos usando controles .Net padrão, controles personalizados e controles DevExpress.

Depois de muito uso no Google e no refletor, me deparei com a mensagem win32 do WM_SETREDRAW. Isso realmente interrompe o desenho dos controles enquanto você os atualiza e pode ser aplicado, IIRC ao painel pai / contendo.

Esta é uma classe muito, muito simples, demonstrando como usar esta mensagem:

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

Há discussões mais completas sobre isso - google for C # e WM_SETREDRAW, por exemplo

Tremulação C #

Suspender layouts

E para quem possa interessar, este é um exemplo semelhante no 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
fonte
44
Que grande resposta e uma tremenda ajuda! Eu criei os métodos de extensão SuspendDrawing e ResumeDrawing para a classe Control, para que eu possa chamá-los para qualquer controle em qualquer contexto.
Zach Johnson
2
Tinha um controle em forma de árvore que só atualizaria um nó adequadamente ao reorganizar seus filhos se você desmoronasse e o expandisse, o que levaria a tremendas oscilações. Isso funcionou perfeitamente para contornar isso. Obrigado! (Surpreendentemente o suficiente, o controle já importados SendMessage, e definido WM_SETREDRAW, mas não chegou a usá-lo para qualquer coisa Agora ele faz..)
neminem
7
Isso não é particularmente útil. Isso é exatamente o que a Controlclasse base de todos os controles WinForms já faz para os métodos BeginUpdatee EndUpdate. Enviar a mensagem você mesmo não é melhor do que usar esses métodos para fazer o trabalho pesado para você e certamente não pode produzir resultados diferentes.
Cody Gray
13
@ Gray Gray - TableLayoutPanels não tem BeginUpdate, por exemplo.
TheBlastOne
4
Tenha cuidado se o seu código possibilitar a chamada desses métodos antes que o controle seja exibido - a chamada Control.Handleforçará a criação do identificador da janela e poderá afetar o desempenho. Por exemplo, se você estava movendo um controle em um formulário antes que ele fosse exibido, se você chamar isso SuspendDrawingantes, sua movimentação será mais lenta. Provavelmente deve ter if (!parent.IsHandleCreated) returnverificações nos dois métodos.
oatsoda
53

A seguir está a mesma solução do ng5000, mas não 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
fonte
Eu tentei, mas no estágio intermediário entre Suspender e Continuar, é apenas inválido. Pode parecer estranho, mas pode apenas manter seu estado antes de suspender no estado transitório?
usuário
2
Suspender um controle significa que nenhum desenho será executado na área de controle e você poderá obter sobras de outras janelas / controles nessa superfície. Você pode tentar usar um controle com DoubleBuffer definido como true se desejar que o "estado" anterior seja desenhado até que o controle seja retomado (se eu entendi o que você quis dizer), mas não posso garantir que funcione. De qualquer forma, acho que você está perdendo o sentido de usar essa técnica: ela visa evitar que o usuário veja um desenho progressivo e lento dos objetos (é melhor ver todos eles aparecendo juntos). Para outras necessidades, use outras técnicas.
ceztko
4
Boa resposta, mas seria muito melhor mostrar onde Messagee onde NativeWindowestão; pesquisar na documentação por uma classe chamada Messagenão é realmente tão divertido.
Darda
1
@pelesl 1) use a importação automática de namespace (clique no símbolo, ALT + Shift + F10), 2) crianças que não estão sendo pintadas por Invalidate () é o comportamento esperado. Você deve suspender o controle pai apenas por um curto período de tempo e retomar quando todos os filhos relevantes forem invalidados.
Ceztko 04/04
2
Invalidate()não funciona tão bem quanto, a Refresh()menos que seja seguido por um de qualquer maneira.
Eugene Ryabtsev 15/12/2015
16

Normalmente, uso uma versão modificada da resposta do 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();
        }
    }
}

Isso permite que as chamadas de suspensão / retomada sejam aninhadas. Você deve certificar-se de combinar cada SuspendDrawingum com um ResumeDrawing. Portanto, provavelmente não seria uma boa ideia divulgá-las.

Ozgur Ozcitak
fonte
4
Isso ajuda a manter ambas as chamadas equilibrado: SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }. Outra opção é implementar isso em uma IDisposableclasse e incluir a parte do desenho em uma usingdeclaração. O identificador seria passado para o construtor, o que suspenderia o desenho.
Olivier Jacot-Descombes
Pergunta antiga, eu sei, mas o envio de "false" não parece funcionar nas versões recentes do c # / VS. Eu tive que mudar falsa a 0 e fiel a 1.
Maury Markowitz
@MauryMarkowitz seu DllImportdeclara wParamcomo bool?
Ozgur Ozcitak 22/09
Olá, voto negativo nesta resposta foi um acidente, desculpe!
Geoff
13

Para ajudar a não esquecer de reativar o desenho:

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
fonte
1
E quando action()lança uma exceção? (Utilize um try / finally)
David Sherret
8

Uma boa solução sem usar interoperabilidade:

Como sempre, basta ativar DoubleBuffered = true no seu CustomControl. Em seguida, se você tiver algum contêiner como FlowLayoutPanel ou TableLayoutPanel, obtenha uma classe de cada um desses tipos e nos construtores, ative o buffer duplo. Agora, basta usar seus contêineres derivados em vez dos contêineres 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
fonte
2
Essa é certamente uma técnica útil - que eu uso com frequência para ListViews - mas na verdade não impede que os redesenhos aconteçam; eles ainda acontecem fora da tela.
Simon
4
Você está certo, resolve o problema de tremulação, não o problema de redesenho fora da tela especificamente. Quando eu estava procurando uma solução para a cintilação, deparei-me com vários segmentos relacionados como este, e quando o encontrei, talvez não o tivesse postado no segmento mais relevante. No entanto, quando a maioria das pessoas deseja suspender a pintura, provavelmente está se referindo à pintura na tela, o que geralmente é um problema mais óbvio do que a pintura fora da tela redundante, então ainda acho que outros espectadores podem achar essa solução útil neste segmento.
Eugenio De Hoyos
Substituir OnPaint.
6

Com base na resposta do ng5000, gosto de usar esta extensão:

        #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

Usar:

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

Aqui está uma combinação de ceztko e ng5000 para trazer uma versão de extensões VB que não 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
fonte
4
Estou trabalhando com um aplicativo WPF que usa winforms misturados com os formulários wpf e lidando com a oscilação da tela. Estou confuso em como esse código deve ser aproveitado - isso iria na janela winform ou wpf? Ou isso não é adequado para minha situação específica?
Nocarrier
3

Eu sei que essa é uma pergunta antiga, já respondida, mas aqui está minha opinião sobre isso; Refatorei a suspensão das atualizações em um IDisposable - dessa forma, posso incluir as instruções que desejo executar em uma usinginstrução.

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

Isso é ainda mais simples e talvez hacky - como eu posso ver muitos dos músculos GDI nesse segmento , e obviamente é apenas uma boa opção para determinados cenários. YMMV

No meu cenário, uso o que chamarei de UserControl "pai" - e, durante o Loadevento, simplesmente removo o controle a ser manipulado da .Controlscoleção dos pais e as informações do pai.OnPaint cuidam da pintura completa da criança. controle de qualquer maneira especial. desligue totalmente os recursos de pintura da criança.

Agora, passo a rotina de pintura do meu filho para um método de extensão baseado neste conceito de Mike Gold para imprimir formulários do Windows .

Aqui estou precisando de um subconjunto de rótulos para renderizar perpendicularmente ao layout:

diagrama simples do seu IDE do Visual Studio

Em seguida, isento o controle filho de ser pintado, com este código no ParentUserControl.Loadmanipulador 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

Em seguida, no mesmo ParentUserControl, pintamos o controle a ser manipulado desde o início:

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

Depois de hospedar o ParentUserControl em algum lugar, por exemplo, um Windows Form - eu estou achando que meu Visual Studio 2015 renderiza o formulário corretamente no Design Time e no tempo de execução: ParentUserControl hospedado em um Windows Form ou talvez outro controle de usuário

Agora, como minha manipulação específica gira o controle infantil em 90 graus, tenho certeza de que todos os pontos de acesso e interatividade foram destruídos naquela região - mas o problema que resolvi foi para uma etiqueta de embalagem que precisava visualizar e imprimir, o que funcionou bem para mim.

Se existem maneiras de reintroduzir os pontos quentes e o controle no meu controle órfão proposital - eu adoraria aprender sobre isso algum dia (não para esse cenário, é claro, mas apenas para aprender). Claro, o WPF suporta tanta loucura OOTB .. mas ... ei ... WinForms ainda é muito divertido, certo?

bkwdesign
fonte
-4

Ou apenas use Control.SuspendLayout()e Control.ResumeLayout().

JustJoost
fonte
9
Layout e Pintura são duas coisas diferentes: layout é como os controles filho são organizados em seu contêiner.
Larry