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?
À mon emploi précédent, nous avions du mal à faire peindre notre application d'interface utilisateur riche 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 l'utilisation du 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.
C'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
Ce qui suit est la même solution de 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 de 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/reprendre les appels. 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.
Pour 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();
});
Une solution intéressante sans interop:
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 mise en mémoire tampon double. 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;
}
}
Basé sur 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
}
Voici une combinaison de Ceztko et de ng5000 pour apporter une version de VB extensions 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
Je sais que c'est une vieille question à laquelle j'ai déjà répondu, mais voici ce que je pense de ceci. J'ai refactoré la suspension des mises à jour dans un IDisposable - de cette façon, je peux inclure les instructions que je veux exécuter dans une instruction 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();
}
}
Ceci est encore plus simple, et peut-être hacky - car je peux voir beaucoup de GDI muscle sur ce fil, et est évidemment seulement un bon ajustement pour certains scénarios. YMMV
Dans mon scénario, j'utilise ce que j'appellerai un contrôle de parent "parent" - et lors de l'événement Load
, je supprime simplement le contrôle à manipuler de la .Controls
_ collection, et le OnPaint
du parent se charge de peindre complètement le contrôle enfant de quelque manière que ce soit .. complètement en mettant les capacités de peinture de l'enfant hors ligne.
Maintenant, je laisse ma routine Paint enfant à une méthode d’extension basée sur ceci concept de Mike Gold pour l’impression de formulaires Windows .
Il me faut ici un sous-ensemble d’étiquettes pour rendre perpendiculairement la mise en page:
Ensuite, j'exempte le contrôle enfant d'être peint, avec ce code dans le ParentUserControl.Load
gestionnaire 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 la base:
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 bien le formulaire correctement au moment de la conception et de l'exécution:
Maintenant, puisque 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. ce qui a bien fonctionné pour moi.
S'il existe des moyens de réintroduire les points chauds et le contrôle dans mon contrôle volontairement orphelin - j'aimerais en apprendre un jour à ce sujet (pas pour ce scénario, bien sûr, mais ... juste pour apprendre). Bien sûr, WPF supporte une telle folie OOTB .. mais .. hé .. WinForms est tellement amusant encore, amiright?