Comment suspendre la peinture pour un contrôle et ses enfants?
Question
J'ai un contrôle sur lequel je dois apporter d'importantes modifications. J'aimerais éviter complètement de le redessiner - SuspendLayout et ResumeLayout ne suffisent pas. Comment suspendre la peinture pour un contrôle et ses enfants?
La solution
À mon emploi précédent, nous avions du mal à faire peindre notre riche application d'interface utilisateur instantanément et en douceur. Nous utilisions des contrôles .Net, des contrôles personnalisés et des contrôles devexpress standard.
Après de nombreuses recherches sur Google et sur le réflecteur, je suis tombé sur le message WM_SETREDRAW win32. Cela arrête vraiment le dessin des contrôles lorsque vous les mettez à jour et peut être appliqué, IIRC au panneau parent / contenant.
Ceci est 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 de plus amples discussions à ce sujet - google pour C # et WM_SETREDRAW, par exemple.
Et à qui de droit, ceci est un exemple similaire dans 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
Autres conseils
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();
}
}
J'utilise généralement une version légèrement modifiée du fichier réponse .
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();
}
}
}
Ceci permet aux appels en suspension / reprise d'être imbriqués. Vous devez vous assurer de faire correspondre chaque SuspendDrawing
avec un ResumeDrawing
. Par conséquent, ce ne serait probablement pas une bonne idée de les rendre publics.
To help with not forgetting to reenable drawing:
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();
});
A nice solution without using interop:
As always, simply enable DoubleBuffered=true on your CustomControl. Then, if you have any containers like FlowLayoutPanel or TableLayoutPanel, derive a class from each of these types and in the constructors, enable double buffering. Now, simply use your derived Containers instead of the Windows.Forms Containers.
class TableLayoutPanel : System.Windows.Forms.TableLayoutPanel
{
public TableLayoutPanel()
{
DoubleBuffered = true;
}
}
class FlowLayoutPanel : System.Windows.Forms.FlowLayoutPanel
{
public FlowLayoutPanel()
{
DoubleBuffered = true;
}
}
Based on ng5000's answer, I like using this 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
Use:
using (this.BeginSuspendlock())
{
//update GUI
}
Here is a combination of ceztko's and ng5000's to bring a VB extensions version that doesn't use 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
I know this is an old question, already answered, but here is my take on this; I refactored the suspension of updates into an IDisposable - that way I can enclose the statements I want to run in a using
statement.
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();
}
}
This is even simpler, and perhaps hacky - as I can see a lot of GDI muscle on this thread, and is obviously only a good fit for certain scenarios. YMMV
In my scenario, I use what I'll refer to as a "Parent" UserControl - and during the Load
event, I simply remove the control-to-be-manipulated from the Parent's .Controls
collection, and the Parent's OnPaint
takes care of completely painting the child control in whatever special way.. fully taking the child's paint capabilities offline.
Now, I hand off my child paint routine to an extension method based off this concept from Mike Gold for printing windows forms.
Here I'm needing a sub-set of labels to render perpendicular to the layout:
Then, I exempt the child control from being painted, with this code in the ParentUserControl.Load
event handler:
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
Then, in the same ParentUserControl, we paint the control-to-be-manipulated from the ground up:
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
Once you host the ParentUserControl somewhere, e.g. a Windows Form - I'm finding that my Visual Studio 2015 renders the form correctly at Design Time as well as runtime:
Now, since my particular manipulation rotates the child control 90 degrees, I'm sure all the hot spots and interactivity has been destroyed in that region - but, the problem I was solving was all for a package label that needed to preview and print, which worked out fine for me.
If there are ways to reintroduce the hot spots and control-ness to my purposely orphaned control - I'd love to learn about that someday (not for this scenario, of course, but.. just to learn). Of course, WPF supports such craziness OOTB.. but.. hey.. WinForms is so much fun still, amiright?
Or just use Control.SuspendLayout()
and Control.ResumeLayout()
.