Como faço para suspender a pintura para um controle e de seus filhos?
Pergunta
Eu tenho um controle que eu tenho que fazer grandes modificações. Eu gostaria de impedi-lo completamente de redesenhar enquanto eu fazer isso - SuspendLayout e ResumeLayout não são suficientes. Como faço para suspender a pintura para um controle e seus filhos?
Solução
No meu trabalho anterior, lutou com a obtenção de nosso aplicativo UI rico para pintar instantaneamente e sem problemas. Estávamos usando controles .NET padrão, controles personalizados e controles DevExpress.
Depois de um monte de googling e uso refletor me deparei com a mensagem win32 WM_SETREDRAW. Isso realmente deixa de controles de desenho enquanto você atualizá-los e pode ser aplicado, IIRC para pai / contendo o painel.
Esta é uma classe muito simples que demonstra 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();
}
}
Existem discussões mais aprofundadas sobre isso -. Google para C # e WM_SETREDRAW, por exemplo
E para quem possa interessar, este é exemplo semelhante em 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
Outras dicas
Segue-se a mesma solução de ng5000 mas não usa P / invocação.
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();
}
}
Eu costumo usar um pouco modificado versão do de ngLink resposta .
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();
}
}
}
Isto permite suspender / resumir as chamadas sejam aninhados. Você deve certificar-se para corresponder cada SuspendDrawing
com um ResumeDrawing
. Por isso, não seria provavelmente uma boa idéia para torná-los públicos.
Para ajuda com não esquecendo de permitir 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();
});
Uma boa solução sem o uso de interoperabilidade:
Como sempre, simplesmente permitir DoubleBuffered = true no seu CustomControl. Então, se você tiver quaisquer recipientes como FlowLayoutPanel ou TableLayoutPanel, derivar uma classe de cada um desses tipos e nos construtores, ative o buffer duplo. Agora, basta usar o seu derivado Containers em vez dos Windows.Forms Containers.
class TableLayoutPanel : System.Windows.Forms.TableLayoutPanel
{
public TableLayoutPanel()
{
DoubleBuffered = true;
}
}
class FlowLayoutPanel : System.Windows.Forms.FlowLayoutPanel
{
public FlowLayoutPanel()
{
DoubleBuffered = true;
}
}
Com base na resposta das ng5000, eu 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
Use:
using (this.BeginSuspendlock())
{
//update GUI
}
Aqui é uma combinação de ceztko de e ng5000 de trazer uma versão 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 isto é uma questão antiga, já respondeu, mas aqui é a minha opinião sobre isso; I reformulado a suspensão de atualizações em um IDisposable - de que maneira eu posso colocar as declarações que deseja executar em um comunicado using
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();
}
}
Este é ainda mais simples, e talvez hacky -. Quanto eu posso ver um monte de músculo GDI sobre este tópico , e , obviamente, só um bom ajuste para determinados cenários YMMV
Em meu cenário, eu uso o que eu vou referir como um "pai" UserControl - e durante o evento Load
, eu simplesmente remover o controle-a-ser-manipulada da coleção .Controls
do Pai, e OnPaint
do Pai leva cuidar da pintura completamente o controle filho de qualquer maneira especial .. levando plenamente as capacidades de pintura da criança offline.
Agora, eu entregar a minha rotina pintura criança a um método de extensão baseado fora este conceito de Mike ouro para Windows Forms impressão.
Aqui eu estou precisando de um sub-conjunto de etiquetas para tornar perpendicular para o layout:
Então, eu isentar o controle filho de ser pintado, com este código no manipulador de eventos ParentUserControl.Load
:
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, na mesma ParentUserControl, pintamos o controle-a-ser-manipulado a partir do 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
Depois de sediar a algum lugar ParentUserControl, por exemplo, um Windows Form - Eu estou achando que o meu Visual Studio 2015 torna a forma corretamente em tempo de design, bem como tempo de execução:
Agora, desde os meus particulares gira manipulação o controle filho de 90 graus, eu tenho certeza que todos os pontos quentes e interatividade foi destruído naquela região - mas, o problema que eu estava resolvendo foi tudo por um rótulo da embalagem que precisava prévia e imprimir, que funcionou muito bem para mim.
Se existem formas de reintroduzir os pontos quentes e controle de-ness para meu controle propositadamente órfãos - Eu adoraria aprender sobre isso um dia (não para este cenário, é claro, mas .. apenas para aprender). Claro, WPF suporta tais OOTB loucura .. mas .. hey .. WinForms é muito divertido ainda, amiright?
Ou Control.SuspendLayout()
uso justo e Control.ResumeLayout()
.