Ho un controllo al quale devo apportare grandi modifiche. Vorrei impedirgli completamente di ridisegnare mentre lo faccio - SuspendLayout e ResumeLayout non sono sufficienti. Come sospendo la pittura per un controllo e i suoi figli?
Ho un controllo al quale devo apportare grandi modifiche. Vorrei impedirgli completamente di ridisegnare mentre lo faccio - SuspendLayout e ResumeLayout non sono sufficienti. Come sospendo la pittura per un controllo e i suoi figli?
Risposte:
Nel mio lavoro precedente abbiamo avuto difficoltà a far sì che la nostra ricca app UI dipingesse istantaneamente e senza intoppi. Stavamo usando controlli .Net standard, controlli personalizzati e controlli devexpress.
Dopo un sacco di utilizzo su Google e sul riflettore mi sono imbattuto nel messaggio win32 WM_SETREDRAW. Questo interrompe veramente il disegno dei controlli mentre li aggiorni e può essere applicato, IIRC al pannello padre / contenitore.
Questa è una classe molto semplice che dimostra come usare questo messaggio:
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();
}
}
Ci sono discussioni più approfondite su questo: google per C # e WM_SETREDRAW, ad es
E a chi può interessare, questo è un esempio simile in 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 per tutti i controlli WinForms fa già per i metodi BeginUpdate
e EndUpdate
. Inviare il messaggio da soli non è meglio che usare quei metodi per fare il lavoro pesante per te, e sicuramente non può produrre risultati diversi.
Control.Handle
forzerà la creazione della maniglia della finestra e potrebbe influire sulle prestazioni. Ad esempio, se si spostava un controllo su un modulo prima che fosse visualizzato, se lo si chiama in SuspendDrawing
anticipo, lo spostamento sarà più lento. Probabilmente dovrebbe avere if (!parent.IsHandleCreated) return
controlli in entrambi i metodi.
La seguente è la stessa soluzione di ng5000 ma non 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 dove NativeWindow
; cercare documentazione per una classe chiamata Message
non è poi così divertente.
Invalidate()
non funziona bene come Refresh()
se non seguito da uno comunque.
Di solito uso una versione leggermente modificata della risposta di 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();
}
}
}
Ciò consente di sospendere / riprendere le chiamate da annidare. Devi assicurarti di abbinare ciascuno SuspendDrawing
a ResumeDrawing
. Quindi, probabilmente non sarebbe una buona idea renderli pubblici.
SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }
. Un'altra opzione è implementarla in una IDisposable
classe e racchiudere la parte del disegno in una using
dichiarazione. La maniglia verrebbe passata al costruttore, che sospenderebbe il disegno.
DllImport
dichiara wParam
come bool
?
Per aiutare a non dimenticare di riattivare il disegno:
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();
}
utilizzo:
SuspendDrawing(myControl, () =>
{
somemethod();
});
action()
genera un'eccezione? (Usa un tentativo / finalmente)
Una bella soluzione senza usare l'interoperabilità:
Come sempre, abilita semplicemente DoubleBuffered = true sul tuo CustomControl. Quindi, se si dispone di contenitori come FlowLayoutPanel o TableLayoutPanel, derivare una classe da ciascuno di questi tipi e nei costruttori, abilitare il doppio buffering. Ora, usa semplicemente i tuoi contenitori derivati anziché i contenitori Windows.Forms.
class TableLayoutPanel : System.Windows.Forms.TableLayoutPanel
{
public TableLayoutPanel()
{
DoubleBuffered = true;
}
}
class FlowLayoutPanel : System.Windows.Forms.FlowLayoutPanel
{
public FlowLayoutPanel()
{
DoubleBuffered = true;
}
}
Sulla base della risposta di ng5000, mi piace usare questa estensione:
#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
Uso:
using (this.BeginSuspendlock())
{
//update GUI
}
Ecco una combinazione di ceztko e ng5000 per portare una versione di estensioni VB che non 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
So che questa è una vecchia domanda, a cui è già stata data una risposta, ma ecco la mia opinione su questo; Ho riformulato la sospensione degli aggiornamenti in un IDisposable - in questo modo posso racchiudere le dichiarazioni che voglio eseguire in una using
dichiarazione.
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();
}
}
Questo è ancora più semplice, e forse confuso, poiché posso vedere un sacco di muscoli GDI su questo thread , ed è ovviamente solo una buona misura per determinati scenari. YMMV
Nel mio scenario, utilizzo quello che chiamerò UserControl "Parent" - e durante l' Load
evento, rimuovo semplicemente il controllo da manipolare dalla .Controls
collezione del Parent e il ParentOnPaint
si occupa di dipingere completamente il bambino controllare in qualsiasi modo speciale .. portando completamente offline le capacità di pittura del bambino.
Ora, passo la mia routine di pittura per bambini a un metodo di estensione basato su questo concetto di Mike Gold per la stampa di moduli Windows .
Qui ho bisogno di un sottoinsieme di etichette per rendere perpendicolare al layout:
Quindi, esento il controllo figlio dall'essere dipinto, con questo codice nel ParentUserControl.Load
gestore eventi:
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
Quindi, nello stesso ParentUserControl, dipingiamo il controllo da manipolare da 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
Dopo aver ospitato ParentUserControl da qualche parte, ad esempio un Windows Form, sto scoprendo che Visual Studio 2015 esegue correttamente il rendering del modulo in fase di progettazione e in fase di esecuzione:
Ora, poiché la mia particolare manipolazione ruota il controllo figlio di 90 gradi, sono sicuro che tutti i punti caldi e l'interattività sono stati distrutti in quella regione, ma il problema che stavo risolvendo era tutto per un'etichetta del pacchetto che doveva essere visualizzata in anteprima e stampata, che ha funzionato bene per me.
Se ci sono modi per reintrodurre i punti caldi e il controllo del mio controllo volutamente orfano - mi piacerebbe conoscerlo un giorno (non per questo scenario, ovviamente, ma ... solo per imparare). Certo, WPF supporta tale follia OOTB .. ma .. hey .. WinForms è ancora così divertente, vero?