Як призупинити малювання для контролю та його дітей?


184

У мене є контроль, який я повинен робити великі модифікації. Я хотів би повністю запобігти її перемальовуванню, поки я це роблю - SuspendLayout та ResumeLayout недостатньо. Як призупинити малювання для контролю та його дітей?


3
хтось, будь ласка, пояснить мені, що тут малює чи малює в цьому контексті? (я новачок у .net) принаймні надайте посилання.
Mr_Green

1
Дуже прикро (або смішно), що. Net вже понад 15 років, і це все ще проблема. Якби Microsoft витратила стільки часу на виправлення справжніх проблем, таких як мерехтіння екрана, як скажімо, Завантажте шкідливе програмне забезпечення Windows X , це було б виправлено давно.
jww

@jww Вони це виправили; це називається WPF.
філу

Відповіді:


304

На моїй попередній роботі ми боролися з тим, щоб наш багатий додаток інтерфейсу малював миттєво та гладко. Ми використовували стандартні елементи управління .Net, спеціальні елементи керування та елементи devexpress.

Після безлічі використання гуглів та рефлекторів я натрапив на повідомлення 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 для 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

44
Яка чудова відповідь та величезна допомога! Я створив методи розширення 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();
    }
}

Я спробував це, але на проміжній стадії між Suspend і Resume це просто недійсне. Це може здатися дивним, але чи може він просто утримати свій стан, перш ніж призупинити перехідний стан?
користувач

2
Призупинити контроль означає, що в контрольній області взагалі не буде виконуватися жодне малювання, і ви можете отримати залишки, намальовані з інших вікон / елементів керування на цій поверхні. Ви можете спробувати використати елемент керування з DoubleBuffer, встановленим на істинне, якщо ви хочете, щоб попереднє "стан" було зроблено до відновлення контролю (якщо я зрозумів, що ви мали на увазі), але я не можу гарантувати, що він буде працювати. У всякому разі, я думаю, що ви не вистачаєте сенсу використання цієї методики: це означає, щоб користувач не бачив прогресивний і повільний малюнок об'єктів (краще, щоб усі вони з’являлися разом). Для інших потреб використовуйте інші прийоми.
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.
Maury Markowitz

@MauryMarkowitz DllImportзаявляє wParamяк bool?
Ozgur Ozcitak

Привіт, заборонити цю відповідь було випадково, вибачте!
Джефф

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()кидає виняток? (Використовуйте спробу / нарешті)
David Sherret

8

Приємне рішення без використання interop:

Як завжди, просто увімкніть 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

4
Я працюю з програмою WPF, яка використовує форми winme, змішані з формами wpf, і маю справу з мерехтінням екрана. Я розгублений у тому, як слід використовувати цей код - чи буде це діяти у вікні winform чи wpf? Або це не підходить для моєї конкретної ситуації?
nocarrier

3

Я знаю, що це старе питання, вже відповів, але ось я сприймати це; Я відновив призупинення оновлень на ідентифікатор - таким чином я можу долучити до заяви заяви, які хочу запустити 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піклується про те, щоб повністю малювати дитину керуйте будь-яким особливим способом .. повністю переймаючи можливості малювання дитини офлайн.

Тепер я передаю розпорядок малювання фарбою дитині метод розширення, заснований на цій концепції від Майка Голда для друку віконних форм .

Тут мені потрібен підмножина міток, щоб зробити перпендикулярно макет:

проста схема вашої ID 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, я виявляю, що мій Visual Studio 2015 надає форму правильно в час проектування, а також під час виконання: ParentUserControl розміщений у формі Windows або, можливо, іншому контролі користувача

Тепер, оскільки моя особлива маніпуляція повертає контроль над дитиною на 90 градусів, я впевнений, що всі «гарячі точки» та інтерактивність були зруйновані в цьому регіоні - але проблема, яку я вирішувала, полягала в тому, що це етикетка пакета, яка потрібна для попереднього перегляду та друку, що добре спрацювало для мене.

Якщо є способи повернути гарячі точки та контроль до мого цілеспрямовано осиротілого контролю - я хотів би дізнатися про це коли-небудь (не для цього сценарію, звичайно, але .. просто для того, щоб дізнатися). Звичайно, WPF підтримує таку божевільність OOTB .. але .. ей .. WinForms все ще так весело, аміре?


-4

Або просто використовувати Control.SuspendLayout()і Control.ResumeLayout().


9
Макет і живопис - це дві речі: макет - це те, як дочірні елементи розташовані в контейнері.
Ларрі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.