Как приостановить рисование для элемента управления и его дочерних элементов?

184

У меня есть контроль, который я должен сделать большие изменения. Я хотел бы полностью предотвратить его перерисовку, пока я делаю это - SuspendLayout и ResumeLayout недостаточно. Как приостановить рисование для элемента управления и его дочерних элементов?

Саймон
источник
3
Может кто-нибудь, пожалуйста, объясните мне, что рисует или рисует здесь в этом контексте? (Я новичок в .net), по крайней мере, предоставить ссылку.
Mr_Green
1
Это действительно позор (или смехотворный), что .Net существует уже более 15 лет, и это все еще проблема. Если бы Microsoft потратила столько же времени на исправление реальных проблем, как мерцание экрана, как, скажем, Get Windows X malware, то это было бы исправлено давно.
jww
@jww Они это исправили; это называется WPF.
Филу

Ответы:

304

На моей предыдущей работе мы изо всех сил пытались заставить наше богатое приложение пользовательского интерфейса рисовать быстро и плавно. Мы использовали стандартные элементы управления .Net, пользовательские элементы управления и элементы управления devexpress.

После долгих поисков и использования рефлектора я наткнулся на сообщение win32 WM_SETREDRAW. Это действительно останавливает рисование элементов управления, пока вы обновляете их, и может применяться 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 для C # и WM_SETREDRAW, например

C # джиттер

Приостановка макетов

И к кому это может относиться, это аналогичный пример в 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
источник
45
Какой отличный ответ и огромная помощь! Я создал методы расширения SuspendDrawing и ResumeDrawing для класса Control, чтобы я мог вызывать их для любого элемента управления в любом контексте.
Зак Джонсон
2
Имел древовидный элемент управления, который корректно обновлял бы узел только после перестановки его потомков, если вы свернули, а затем развернул его, что привело к уродливому мерцанию. Это отлично сработало, чтобы обойти это. Спасибо! (Довольно забавно, что элемент управления уже импортировал SendMessage и определил WM_SETREDRAW, но на самом деле не использовал его ни для чего. Теперь это так.)
neminem
7
Это не особенно полезно. Это именно то , что Controlбазовый класс для всех элементов управления WinForms уже делает для BeginUpdateи EndUpdateметодов. Самостоятельная отправка сообщения не лучше, чем использование этих методов для тяжелой работы за вас, и, конечно, не может привести к другим результатам.
Коди Грей
13
@Cody Grey - TableLayoutPanels, например, не имеют BeginUpdate.
TheBlastOne
4
Будьте осторожны, если ваш код позволяет вызывать эти методы до отображения элемента управления - вызов Control.Handleзаставит создать дескриптор окна и может повлиять на производительность. Например, если вы перемещали элемент управления в форме до ее отображения, если вы вызываете это SuspendDrawingзаранее, ваш ход будет медленнее. Вероятно, должны иметь if (!parent.IsHandleCreated) returnпроверки в обоих методах.
oatsoda
53

Следующее является тем же решением 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();
    }
}
ceztko
источник
Я пробовал это, но на промежуточной стадии между Suspend и Resume, это просто делает недействительным. Это может звучать странно, но может ли оно просто удерживать свое состояние перед тем, как приостановиться в переходном состоянии?
пользователь
2
Приостановка управления означает, что в области управления вообще не будет выполняться рисование, и вы можете получить остатки от других окон / элементов управления на этой поверхности. Вы можете попытаться использовать элемент управления с DoubleBuffer, установленным в значение true, если вы хотите, чтобы предыдущее «состояние» отображалось до возобновления элемента управления (если я понял, что вы имели в виду), но я не могу гарантировать, что он будет работать. В любом случае, я думаю, что вы упускаете смысл использования этой техники: она предназначена для того, чтобы пользователь не видел прогрессивное и медленное рисование объектов (лучше видеть, что все они появляются вместе). Для других нужд используйте другие методы.
ceztko
4
Хороший ответ, но было бы намного лучше показать, где Messageи где NativeWindow; поиск документации по названному классу Messageне так уж интересен.
Дарда
1
@pelesl 1) использовать автоматический импорт пространства имен (нажмите на символ, ALT + Shift + F10), 2) дочерние объекты, не окрашенные Invalidate (), являются ожидаемым поведением. Вы должны приостановить родительский контроль только на короткий промежуток времени и возобновить его, когда все соответствующие дочерние элементы станут недействительными.
ceztko
2
Invalidate()не работает так же хорошо, как Refresh()если бы не последовал один в любом случае.
Евгений Рябцев
16

Я обычно использую немного измененную версию ответа 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();
        }
    }
}

Это позволяет приостановить / возобновить вызовы, чтобы быть вложенными. Вы должны убедиться, что соответствует каждому SuspendDrawingс ResumeDrawing. Следовательно, было бы неплохо сделать их публичными.

Озгур Озцитак
источник
4
Это помогает держать оба вызова сбалансированного: SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }. Другой вариант - реализовать это в IDisposableклассе и заключить чертежную часть в using-словие. Дескриптор будет передан конструктору, который приостановит рисование.
Оливье Жако-Дескомб
Старый вопрос, я знаю, но отправка «false» не работает в последних версиях c # / VS. Мне пришлось изменить false на 0 и true на 1.
Мори Марковиц
@MauryMarkowitz делает ваш DllImportDECLARE , wParamкак bool?
Озгур Озцитак
Привет, занижение этого ответа было несчастным случаем, извините!
Джефф
13

Чтобы помочь не забыть включить рисунок:

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();
});
Джонатан Х
источник
1
Как насчет того, когда action()выдает исключение? (Используйте попытку / наконец-то)
Дэвид Шеррет
8

Хорошее решение без использования взаимодействия:

Как всегда, просто включите DoubleBuffered = true в CustomControl. Затем, если у вас есть какие-либо контейнеры, такие как FlowLayoutPanel или TableLayoutPanel, выведите класс из каждого из этих типов, а в конструкторах включите двойную буферизацию. Теперь просто используйте ваши производные контейнеры вместо контейнеров Windows.Forms.

class TableLayoutPanel : System.Windows.Forms.TableLayoutPanel
{
    public TableLayoutPanel()
    {
        DoubleBuffered = true;
    }
}

class FlowLayoutPanel : System.Windows.Forms.FlowLayoutPanel
{
    public FlowLayoutPanel()
    {
        DoubleBuffered = true;
    }
}
Эухенио Де Ойос
источник
2
Это, безусловно, полезный метод, который я использую довольно часто для ListViews, но на самом деле он не предотвращает перерисовку; они все еще случаются за кадром.
Симон
4
Вы правы, это решает проблему мерцания, а не проблему перерисовки за пределами экрана. Когда я искал решение проблемы мерцания, я наткнулся на несколько связанных тем, подобных этой, и когда я нашел его, я мог не разместить его в самой актуальной теме. Однако, когда большинство людей хотят приостановить рисование, они, вероятно, имеют в виду рисование на экране, что часто является более очевидной проблемой, чем избыточное рисование вне экрана, поэтому я все же думаю, что другие зрители могут найти это решение полезным в этой теме.
Эухенио Де Ойос
Переопределить OnPaint.
6

Исходя из ответа 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
}
Корай
источник
4

Вот комбинация 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
goughy000
источник
4
Я работаю с приложением WPF, которое использует winforms, смешанные с формами wpf, и занимаюсь мерцанием экрана. Я запутался в том, как этот код должен использоваться - это пойдет в окне winform или wpf? Или это не подходит для моей конкретной ситуации?
Нокарьер 18.12.12
3

Я знаю, что это старый вопрос, на который уже дан ответ, но вот мое мнение об этом; Я реорганизовал приостановку обновлений в 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();
    }
}
Скотт Бейкер
источник
2

Это даже проще и, может быть, хакерски - поскольку я вижу много мышц GDI в этой теме , и, очевидно, подходит только для определенных сценариев. YMMV

В моем сценарии я использую то, что я буду называть «Родительским» UserControl - и во время Loadсобытия я просто удаляю элемент управления, которым нужно манипулировать, из .Controlsколлекции Родителя и Родителя.OnPaint заботится о полной покраске дочернего элемента. контролировать каким-либо особым образом .. полностью отключить возможности рисования ребенка в автономном режиме.

Теперь я передаю свою дочернюю процедуру рисования методу расширения, основанному на этой концепции от Майка Голда, для печати оконных форм .

Здесь мне нужен поднабор меток для рендеринга перпендикулярно макету:

простая схема вашей Visual Studio IDE

Затем я освобождаю дочерний элемент управления от рисования с помощью этого кода в 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 Form - я обнаружил, что моя Visual Studio 2015 правильно отображает форму во время разработки, а также во время выполнения: ParentUserControl размещается в форме Windows или, возможно, другом пользовательском элементе управления

Теперь, так как мои особые манипуляции поворачивают дочерний элемент управления на 90 градусов, я уверен, что все горячие точки и интерактивность были уничтожены в этом регионе - но проблема, которую я решал, заключалась в том, чтобы наклеить этикетку на упаковку, которую нужно было предварительно просмотреть и напечатать, который работал хорошо для меня.

Если есть способы восстановить «горячие точки» и контроль для моего намеренно осиротевшего контроля - я бы с радостью узнал об этом когда-нибудь (конечно, не для этого сценария, но… просто для изучения). Конечно, WPF поддерживает такое сумасшествие ... OOTB ... но ... эй ... WinForms так весело, правда?

bkwdesign
источник
-4

Или просто используйте Control.SuspendLayout()и Control.ResumeLayout().

JustJoost
источник
9
Макет и рисование - это две разные вещи: макет - это то, как дочерние элементы управления расположены в их контейнере.
Ларри