Как мне приостановить рисование для элемента управления и его дочерних элементов?
Вопрос
У меня есть элемент управления, в который я должен внести большие изменения.Я бы хотел полностью предотвратить его перерисовку, пока я это делаю - SuspendLayout и ResumeLayout недостаточно.Как мне приостановить рисование для элемента управления и его дочерних элементов?
Решение
На моей предыдущей работе мы боролись с тем, чтобы наше приложение с богатым пользовательским интерфейсом рисовало мгновенно и плавно.Мы использовали стандартные .Сетевые элементы управления, пользовательские элементы управления и элементы управления devexpress.
После долгих поисков в Google и использования reflector я наткнулся на сообщение WM_SETREDRAW win32.Это действительно останавливает рисование элементов управления во время их обновления и может быть применено IIRC к родительской / содержащей панели.
Это очень-очень простой класс, демонстрирующий, как использовать это сообщение:
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();
}
}
Есть более подробные обсуждения по этому поводу - google for C # и WM_SETREDRAW, например
И кого это может касаться, это аналогичный пример в 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
Другие советы
Ниже приведено то же самое решение для ng5000, но не использует 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();
}
}
Обычно я использую немного измененную версию nglink's ответ.
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();
}
}
}
Это позволяет приостанавливать / возобновлять вызовы вложенными.Вы должны убедиться, что каждый из них соответствует SuspendDrawing
с помощью ResumeDrawing
.Следовательно, вероятно, было бы не очень хорошей идеей предавать их огласке.
Чтобы помочь не забыть повторно включить рисование:
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();
}
использование:
SuspendDrawing(myControl, () =>
{
somemethod();
});
Хорошее решение без использования взаимодействия:
Как всегда, просто включите DoubleBuffered=true в вашем CustomControl.Затем, если у вас есть какие-либо контейнеры, такие как FlowLayoutPanel или TableLayoutPanel, создайте класс из каждого из этих типов и в конструкторах включите двойную буферизацию.Теперь просто используйте ваши производные контейнеры вместо окон.Формирует контейнеры.
class TableLayoutPanel : System.Windows.Forms.TableLayoutPanel
{
public TableLayoutPanel()
{
DoubleBuffered = true;
}
}
class FlowLayoutPanel : System.Windows.Forms.FlowLayoutPanel
{
public FlowLayoutPanel()
{
DoubleBuffered = true;
}
}
Основываясь на ответе ng5000, мне нравится использовать это расширение:
#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
Использование:
using (this.BeginSuspendlock())
{
//update GUI
}
Вот комбинация ceztko и ng5000 для создания версии расширений VB, которая не использует 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
Я знаю, что это старый вопрос, на который уже дан ответ, но вот мой взгляд на это;Я преобразовал приостановку обновлений в IDisposable - таким образом, я могу вложить инструкции, которые я хочу выполнить, в 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();
}
}
Это еще проще, и, возможно, халтурный - поскольку я вижу много GDI-мускулов в этой теме, и является очевидно, что это хорошо подходит только для определенных сценариев. ЯММВ
В моем сценарии я использую то, что я буду называть "родительским" UserControl - и во время Load
событие, я просто удаляю элемент управления, которым нужно манипулировать, из родительского .Controls
коллекция, и родительский OnPaint
заботится о том, чтобы полностью раскрасить дочерний элемент управления каким-либо особым образом..полностью отключите возможности ребенка рисовать в автономном режиме.
Теперь я передаю свою дочернюю процедуру рисования методу расширения, основанному на этом концепция от Майка Голда для печати Windows forms.
Здесь мне нужен подмножество меток для рендеринга перпендикулярно к макету:
Затем я освобождаю дочерний элемент управления от рисования, используя этот код в 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
Затем, в том же ParentUserControl , мы рисуем элемент управления, которым нужно манипулировать, с нуля:
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
Как только вы разместите ParentUserControl где-нибудь, напримерформа Windows - я обнаружил, что моя Visual Studio 2015 отображает форма корректно как во время разработки, так и во время выполнения:
Теперь, поскольку моя конкретная манипуляция поворачивает дочерний элемент управления на 90 градусов, я уверен, что все горячие точки и интерактивность были уничтожены в этой области - но проблема, которую я решал, касалась только этикетки упаковки, которую нужно было предварительно просмотреть и распечатать, что у меня отлично получилось.
Если есть способы вернуть горячие точки и контроль к моему намеренно утраченному контролю - я бы хотел когда-нибудь узнать об этом (не для этого сценария, конечно, но..просто чтобы научиться).Конечно, WPF поддерживает такое безумие OOTB..но..эй..WinForms - это все еще так весело, правда?
Или просто используйте Control.SuspendLayout()
и Control.ResumeLayout()
.