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?
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?
Respostas:
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
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
Control
classe base de todos os controles WinForms já faz para os métodos BeginUpdate
e 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.
Control.Handle
forç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 SuspendDrawing
antes, sua movimentação será mais lenta. Provavelmente deve ter if (!parent.IsHandleCreated) return
verificações nos dois métodos.
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();
}
}
Message
e onde NativeWindow
estão; pesquisar na documentação por uma classe chamada Message
não é realmente tão divertido.
Invalidate()
não funciona tão bem quanto, a Refresh()
menos que seja seguido por um de qualquer maneira.
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 SuspendDrawing
um com um ResumeDrawing
. Portanto, provavelmente não seria uma boa ideia divulgá-las.
SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }
. Outra opção é implementar isso em uma IDisposable
classe e incluir a parte do desenho em uma using
declaração. O identificador seria passado para o construtor, o que suspenderia o desenho.
DllImport
declara wParam
como bool
?
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();
});
action()
lança uma exceção? (Utilize um try / finally)
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;
}
}
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
}
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
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 using
instruçã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();
}
}
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 Load
evento, simplesmente removo o controle a ser manipulado da .Controls
coleçã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:
Em seguida, isento o controle filho de ser pintado, com este código no ParentUserControl.Load
manipulador 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:
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?