Jak zawiesić malowanie dla kontrolki i jej dzieci?

184

Mam kontrolę, do której muszę wprowadzać duże modyfikacje. Chciałbym całkowicie uniemożliwić przerysowanie, gdy to robię - SuspendLayout i ResumeLayout nie wystarczą. Jak zawiesić malowanie dla kontrolki i jej dzieci?

Szymon
źródło
3
czy ktoś może mi wyjaśnić, co w tym kontekście rysuje lub maluje? (Jestem nowy w .net) przynajmniej podaj link.
Mr_Green
1
To naprawdę wstyd (lub śmiech), że .Net jest dostępny od ponad 15 lat, a to wciąż stanowi problem. Gdyby Microsoft poświęcił tyle czasu na naprawę prawdziwych problemów, jak migotanie ekranu, jak powiedzmy: Pobierz złośliwe oprogramowanie dla systemu Windows X , to dawno temu by to zostało naprawione.
jww
@jww Naprawili to; nazywa się WPF.
philu

Odpowiedzi:

304

W mojej poprzedniej pracy mieliśmy problemy z uzyskaniem, aby nasza bogata aplikacja UI malowała natychmiast i płynnie. Używaliśmy standardowych kontrolek .Net, niestandardowych i devexpress.

Po wielu problemach z przeglądaniem i używaniem reflektora natrafiłem na komunikat win32 WM_SETREDRAW. To naprawdę zatrzymuje rysowanie kontrolek podczas ich aktualizacji i można je zastosować, IIRC do panelu nadrzędnego / zawierającego.

To jest bardzo, bardzo prosta klasa pokazująca, jak korzystać z tego komunikatu:

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();
    }
}

Są pełniejsze dyskusje na ten temat - Google dla C # i WM_SETREDRAW, np

C # Jitter

Zawieszanie układów

I do kogo może to dotyczyć, jest to podobny przykład w 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
źródło
44
Cóż za wspaniała odpowiedź i ogromna pomoc! Stworzyłem metody rozszerzeń SuspendDrawing i ResumeDrawing dla klasy Control, więc mogę je wywoływać dla dowolnej kontroli w dowolnym kontekście.
Zach Johnson
2
Miał kontrolkę przypominającą drzewo, która odświeżałaby odpowiednio węzeł tylko przy przestawianiu swoich dzieci, jeśli się zwali, a następnie rozszerzy, co doprowadziło do brzydkiego migotania. To działało idealnie, aby obejść ten problem. Dzięki! (Co zabawne, formant już zaimportował SendMessage i zdefiniował WM_SETREDRAW, ale tak naprawdę nie wykorzystał go do niczego. Teraz to robi.)
neminem
7
To nie jest szczególnie przydatne. To jest dokładnie to, co Controlklasa podstawowa dla wszystkich kontrolek WinForm już robi dla metod BeginUpdatei EndUpdate. Samo wysłanie wiadomości nie jest lepsze niż użycie tych metod do wykonania ciężkiego podnoszenia i na pewno nie może przynieść różnych rezultatów.
Cody Gray
13
@Cody Gray - TableLayoutPanels nie ma na przykład BeginUpdate.
TheBlastOne
4
Zachowaj ostrożność, jeśli kod umożliwia wywołanie tych metod przed wyświetleniem formantu - wywołanie Control.Handleto wymusi utworzenie uchwytu okna i może wpłynąć na wydajność. Na przykład, jeśli przesuwałeś formant na formularzu przed jego wyświetleniem, jeśli wywołasz go SuspendDrawingwcześniej, Twój ruch będzie wolniejszy. Prawdopodobnie powinny mieć if (!parent.IsHandleCreated) returnkontrole w obu metodach.
oatsoda
53

Poniżej przedstawiono to samo rozwiązanie ng5000, ale nie używa 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
źródło
Próbowałem, ale na etapie pośrednim między zawieszeniem a wznowieniem jest to po prostu nieważne. Może to zabrzmieć dziwnie, ale czy może po prostu utrzymać swój stan przed zawieszeniem w stanie przejściowym?
użytkownik
2
Wstrzymaj kontrolkę oznacza, że ​​w obszarze kontrolnym nie zostanie w ogóle wykonany rysunek i możesz pobrać resztki z innych okien / kontrolek na tej powierzchni. Możesz spróbować użyć formantu z DoubleBuffer ustawionym na true, jeśli chcesz, aby poprzedni „stan” był rysowany, dopóki formant nie zostanie wznowiony (jeśli zrozumiałem, o co ci chodzi), ale nie mogę zagwarantować, że zadziała. W każdym razie myślę, że nie rozumiesz sensu używania tej techniki: ma ona na celu uniknięcie przez użytkownika oglądania postępującego i powolnego rysowania obiektów (lepiej widzieć wszystkie pojawiające się razem). W przypadku innych potrzeb użyj innych technik.
ceztko
4
Dobra odpowiedź, ale znacznie lepiej byłoby pokazać, gdzie Messagei gdzie NativeWindow; wyszukiwanie dokumentacji dla nazwanej klasy Messagenie jest wcale tak zabawne.
darda
1
@pelesl 1) użyj automatycznego importu przestrzeni nazw (kliknij symbol, ALT + Shift + F10), 2) oczekiwane zachowanie dzieci nie malowanych przez Invalidate (). Powinieneś zawiesić kontrolę rodzicielską tylko na krótki okres i wznowić ją, gdy wszystkie odpowiednie dzieci zostaną unieważnione.
ceztko
2
Invalidate()nie działa tak dobrze, jak i tak Refresh()po nim.
Eugene Ryabtsev,
16

Zwykle używam trochę zmodyfikowaną wersję ngLink za odpowiedź .

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();
        }
    }
}

Umożliwia to zagnieżdżanie / zawieszanie połączeń. Musisz upewnić się, że każdy SuspendDrawingz nich pasuje do znaku ResumeDrawing. Dlatego prawdopodobnie nie byłoby dobrym pomysłem upublicznienie ich.

Ozgur Ozcitak
źródło
4
Pomaga to utrzymać oba połączenia zbilansowany: SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }. Inną opcją jest zaimplementowanie tego w IDisposableklasie i zawarcie części rysunkowej w usinginstrukcji. Uchwyt zostanie przekazany do konstruktora, który zawiesi rysowanie.
Olivier Jacot-Descombes
Stare pytanie, wiem, ale wysyłanie „fałszu” nie działa w najnowszych wersjach c # / VS. Musiałem zmienić wartość false na 0 i true na 1.
Maury Markowitz
@MauryMarkowitz DllImportdeklaruje wParamjako bool?
Ozgur Ozcitak
Cześć, odrzucenie tej odpowiedzi to wypadek, przepraszam!
Geoff,
13

Aby nie zapomnieć o ponownym włączeniu rysunku:

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();
}

stosowanie:

SuspendDrawing(myControl, () =>
{
    somemethod();
});
Jonathan H.
źródło
1
A kiedy action()rzuca wyjątek? (Spróbuj / w końcu)
David Sherret
8

Ładne rozwiązanie bez użycia interop:

Jak zawsze, po prostu włącz DoubleBuffered = true w CustomControl. Następnie, jeśli masz jakieś kontenery, takie jak FlowLayoutPanel lub TableLayoutPanel, wyprowadź klasę z każdego z tych typów i w konstruktorach włącz podwójne buforowanie. Teraz wystarczy użyć pochodnych kontenerów zamiast Windows.Forms Containers.

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

class FlowLayoutPanel : System.Windows.Forms.FlowLayoutPanel
{
    public FlowLayoutPanel()
    {
        DoubleBuffered = true;
    }
}
Eugenio De Hoyos
źródło
2
Jest to z pewnością przydatna technika - którą często używam do wyświetlania listView - ale tak naprawdę nie zapobiega występowaniu przerysowań; wciąż zdarzają się poza ekranem.
Simon
4
Masz rację, to rozwiązuje problem migotania, a nie problem przerysowywania poza ekranem. Kiedy szukałem rozwiązania migotania, natknąłem się na kilka powiązanych wątków, takich jak ten, i kiedy go znalazłem, mogłem nie opublikować go w najbardziej odpowiednim wątku. Jednak gdy większość ludzi chce zawiesić malowanie, prawdopodobnie odnosi się to do malowania na ekranie, co często jest bardziej oczywistym problemem niż zbędne malowanie poza ekranem, więc nadal uważam, że inni widzowie mogą uznać to rozwiązanie za pomocne w tym wątku.
Eugenio De Hoyos,
Zastąp OnPaint.
6

W oparciu o odpowiedź ng5000 lubię używać tego rozszerzenia:

        #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

Posługiwać się:

using (this.BeginSuspendlock())
{
    //update GUI
}
Koray
źródło
4

Oto kombinacja ceztko i ng5000 w celu uzyskania wersji rozszerzeń VB, która nie używa 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
źródło
4
Pracuję z aplikacją WPF, która korzysta z formularzy WinFix zmieszanych z formularzami WPF i zajmuje się migotaniem ekranu. Jestem zdezorientowany, w jaki sposób należy wykorzystać ten kod - czy przejdzie to w oknie winform lub wpf? Czy to nie pasuje do mojej konkretnej sytuacji?
nocarrier 18.12.12
3

Wiem, że to stare pytanie, na które już udzielono odpowiedzi, ale oto moje zdanie na ten temat; Zawiesiłem aktualizację zawieszenia na IDisposable - w ten sposób mogę zawrzeć instrukcje, które chcę uruchomić w usinginstrukcji.

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();
    }
}
Scott Baker
źródło
2

Jest to jeszcze prostsze i być może hackerskie - ponieważ widzę w tym wątku dużo mięśni GDI i jest oczywiście dobre tylko dla niektórych scenariuszy. YMMV

W moim scenariuszu korzystam z tego, co nazywam „Kontrolą nadrzędną” UserControl - a podczas tego Loadwydarzenia po prostu usuwam kontrolę nad manipulacją z .Controlskolekcji Rodzica , a Rodzic OnPaintzajmuje się całkowitym malowaniem dziecka kontrolować w jakikolwiek specjalny sposób .. całkowicie wyłączając możliwości malowania dziecka w trybie offline.

Teraz przekazuję moją procedurę malowania dziecięcego metodzie rozszerzenia opartej na tej koncepcji autorstwa Mike'a Golda do drukowania formularzy Windows .

Potrzebuję podzestawu etykiet do renderowania prostopadłego do układu:

prosty schemat twojego Visual Studio IDE

Następnie wyłączam malowanie kontroli dzieci za pomocą tego kodu w ParentUserControl.Loadmodule obsługi zdarzeń:

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

Następnie w tym samym ParentUserControl malujemy kontrolkę, którą należy manipulować od podstaw:

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

Kiedy już gdzieś hostujesz ParentUserControl, np. Formularz Windows - stwierdzam, że mój Visual Studio 2015 poprawnie renderuje formularz w czasie projektowania, a także w czasie wykonywania: ParentUserControl hostowany w formularzu Windows lub innej kontroli użytkownika

Teraz, ponieważ moja szczególna manipulacja obraca kontrolę dziecka o 90 stopni, jestem pewien, że wszystkie gorące punkty i interaktywność zostały zniszczone w tym regionie - ale problem, który rozwiązałem, dotyczył etykiety opakowania, która wymagała podglądu i wydrukowania, dla mnie dobrze.

Jeśli istnieją sposoby na przywrócenie gorących punktów i kontroli mojej celowo osieroconej kontroli - chciałbym się kiedyś o tym dowiedzieć (nie dla tego scenariusza, oczywiście, ale ... tylko po to, aby się uczyć). Oczywiście, WPF popiera takie szaleństwo OOTB .. ale ... hej .. WinForm wciąż jest tak fajny, prawda?

bkwdesign
źródło
-4

Lub po prostu użyj Control.SuspendLayout()i Control.ResumeLayout().

JustJoost
źródło
9
Układ i malowanie to dwie różne rzeczy: układ to sposób, w jaki kontrolki podrzędne są rozmieszczone w pojemniku.
Larry