Bagaimana cara menangguhkan lukisan untuk kontrol dan anak-anaknya?

184

Saya memiliki kontrol yang harus saya modifikasi. Saya ingin benar-benar mencegahnya menggambar ulang sementara saya melakukan itu - SuspendLayout dan ResumeLayout tidak cukup. Bagaimana cara menangguhkan lukisan untuk kontrol dan anak-anaknya?

Simon
sumber
3
dapatkah seseorang menjelaskan padaku apa yang menggambar atau melukis di sini dalam konteks ini? (Saya baru mengenal .net) setidaknya memberikan tautan.
Mr_Green
1
Benar-benar memalukan (atau menggelikan) bahwa .Net telah keluar selama lebih dari 15 tahun, dan ini masih menjadi masalah. Jika Microsoft menghabiskan banyak waktu untuk memperbaiki masalah nyata seperti layar berkedip seperti, Dapatkan malware Windows X , maka ini akan diperbaiki sejak lama.
jww
@ jww Mereka memperbaikinya; itu disebut WPF.
philu

Jawaban:

304

Di pekerjaan saya sebelumnya, kami berjuang untuk mendapatkan aplikasi UI kami yang kaya untuk melukis secara instan dan lancar. Kami menggunakan kontrol .Net, kontrol kustom, dan kontrol devexpress standar.

Setelah banyak menggunakan googling dan reflektor saya menemukan pesan win32 WM_SETREDRAW. Ini benar-benar berhenti menggambar kontrol sementara Anda memperbaruinya dan dapat diterapkan, IIRC ke panel induk / yang mengandung.

Ini adalah kelas yang sangat sangat sederhana yang menunjukkan cara menggunakan pesan ini:

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

Ada diskusi yang lebih lengkap tentang ini - google untuk C # dan WM_SETREDRAW, mis

C # Jitter

Tata Letak yang Menangguhkan

Dan kepada siapa yang berkepentingan, ini adalah contoh serupa di 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
sumber
45
Sungguh jawaban yang luar biasa dan bantuan yang luar biasa! Saya membuat metode ekstensi SuspendDrawing dan ResumeDrawing untuk kelas Kontrol, jadi saya bisa memanggil mereka untuk kontrol apa pun dalam konteks apa pun.
Zach Johnson
2
Memiliki kontrol seperti pohon yang hanya akan menyegarkan node dengan benar pada pengaturan ulang anak-anaknya jika Anda pingsan kemudian memperluasnya, yang menyebabkan kedipan buruk. Ini bekerja dengan sempurna untuk mengatasinya. Terima kasih! (Cukup mengejutkan, kontrol sudah mengimpor SendMessage, dan mendefinisikan WM_SETREDRAW, tetapi tidak benar-benar menggunakannya untuk apa pun. Sekarang ya.)
neminem
7
Ini tidak terlalu berguna. Inilah yang Controlkelas dasar untuk semua kontrol WinForms sudah melakukan untuk BeginUpdatedan EndUpdatemetode. Mengirim pesan sendiri tidak lebih baik daripada menggunakan metode-metode itu untuk melakukan pekerjaan berat untuk Anda, dan tentu saja tidak dapat menghasilkan hasil yang berbeda.
Cody Grey
13
@Cody Grey - TableLayoutPanels tidak memiliki BeginUpdate, misalnya.
TheBlastOne
4
Hati-hati jika kode Anda memungkinkan untuk memanggil metode ini sebelum kontrol ditampilkan - panggilan ke Control.Handleakan memaksa pegangan jendela dibuat dan dapat memengaruhi kinerja. Misalnya jika Anda memindahkan kontrol pada formulir sebelum ditampilkan, jika Anda memanggil ini SuspendDrawingsebelumnya, gerakan Anda akan lebih lambat. Mungkin harus if (!parent.IsHandleCreated) returndiperiksa di kedua metode.
oatsoda
53

Berikut ini adalah solusi yang sama dari ng5000 tetapi tidak menggunakan 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
sumber
Saya sudah mencobanya, tetapi pada tahap menengah antara Menangguhkan dan Melanjutkan, itu hanya tidak valid. Ini mungkin terdengar aneh, tetapi bisakah itu hanya menahan keadaannya sebelum ditunda dalam keadaan sementara?
pengguna
2
Menangguhkan kontrol berarti bahwa tidak ada gambar yang akan dilakukan sama sekali di area kontrol dan Anda mungkin mendapatkan sisa sisa gambar dari jendela / kontrol lain di permukaan itu. Anda dapat mencoba menggunakan kontrol dengan DoubleBuffer disetel ke true jika Anda ingin "negara" sebelumnya ditarik sampai kontrol dilanjutkan (jika saya mengerti apa yang Anda maksud), tetapi saya tidak dapat menjamin bahwa itu akan berfungsi. Pokoknya saya pikir Anda kehilangan titik menggunakan teknik ini: itu dimaksudkan untuk menghindari pengguna melihat gambar objek yang progresif dan lambat (lebih baik untuk melihat semuanya muncul bersama-sama). Untuk kebutuhan lain, gunakan teknik lain.
ceztko
4
Jawaban yang bagus, tetapi akan jauh lebih baik untuk menunjukkan di mana Messagedan di mana NativeWindow; mencari dokumentasi untuk kelas bernama Messagetidak benar-benar menghibur.
darda
1
@pelesl 1) menggunakan impor namespace otomatis (klik pada simbol, ALT + Shift + F10), 2) anak-anak yang tidak dilukis oleh Invalidate () adalah perilaku yang diharapkan. Anda harus menangguhkan kontrol orang tua hanya untuk rentang waktu singkat dan melanjutkannya ketika semua anak yang relevan tidak valid.
ceztko
2
Invalidate()tidak bekerja sebagus Refresh()kecuali diikuti oleh satu lagi pula.
Eugene Ryabtsev
16

Saya biasanya menggunakan versi modifikasi dari jawaban 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();
        }
    }
}

Ini memungkinkan penangguhan / melanjutkan panggilan untuk disarangkan. Anda harus memastikan untuk mencocokkan masing SuspendDrawing- masing dengan a ResumeDrawing. Oleh karena itu, mungkin bukan ide yang baik untuk mempublikasikannya.

Ozgur Ozcitak
sumber
4
Hal ini membantu untuk menjaga kedua panggilan seimbang: SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }. Pilihan lain adalah untuk mengimplementasikan ini di IDisposablekelas dan untuk melampirkan bagian gambar di- usingpernyataan. Pegangan akan diteruskan ke konstruktor, yang akan menangguhkan gambar.
Olivier Jacot-Descombes
Pertanyaan lama, saya tahu, tetapi mengirimkan "false" tampaknya tidak berfungsi di versi terbaru dari c # / VS. Saya harus mengubah false menjadi 0 dan benar menjadi 1.
Maury Markowitz
@MauryMarkowitz tidak Anda DllImportdeclare wParamsebagai bool?
Ozgur Ozcitak
Hai, menolak jawaban ini adalah kecelakaan, maaf!
Geoff
13

Untuk membantu dengan tidak lupa mengaktifkan kembali gambar:

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

pemakaian:

SuspendDrawing(myControl, () =>
{
    somemethod();
});
Jonathan H
sumber
1
Bagaimana dengan saat action()melempar pengecualian? (Gunakan percobaan / akhirnya)
David Sherret
8

Solusi yang bagus tanpa menggunakan interop:

Seperti biasa, cukup aktifkan DoubleBuffered = true pada CustomControl Anda. Kemudian, jika Anda memiliki wadah seperti FlowLayoutPanel atau TableLayoutPanel, dapatkan kelas dari masing-masing jenis ini dan di konstruktor, aktifkan buffering ganda. Sekarang, cukup gunakan Wadah asal Anda dan bukan Wadah Windows.

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

class FlowLayoutPanel : System.Windows.Forms.FlowLayoutPanel
{
    public FlowLayoutPanel()
    {
        DoubleBuffered = true;
    }
}
Eugenio De Hoyos
sumber
2
Itu tentu teknik yang berguna - yang sering saya gunakan untuk ListViews - tetapi itu sebenarnya tidak mencegah redraws terjadi; mereka masih terjadi di luar layar.
Simon
4
Anda benar, itu memecahkan masalah berkedip-kedip, bukan masalah menggambar ulang di luar layar khusus. Ketika saya mencari solusi untuk kedipan ini, saya menemukan beberapa utas terkait seperti ini, dan ketika saya menemukannya, saya mungkin tidak mempostingnya di utas yang paling relevan. Namun, ketika kebanyakan orang ingin menangguhkan lukisan, mereka mungkin merujuk pada lukisan di layar, yang seringkali merupakan masalah yang lebih jelas daripada lukisan di luar layar yang redundan, jadi saya masih berpikir bahwa pemirsa lain mungkin menganggap solusi ini bermanfaat dalam utas ini.
Eugenio De Hoyos
Override OnPaint.
6

Berdasarkan jawaban ng5000, saya suka menggunakan ekstensi ini:

        #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

Menggunakan:

using (this.BeginSuspendlock())
{
    //update GUI
}
Koray
sumber
4

Berikut ini adalah kombinasi dari ceztko's dan ng5000's untuk menghadirkan versi ekstensi VB yang tidak menggunakan 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
sumber
4
Saya bekerja dengan aplikasi WPF yang menggunakan winforms yang dicampur dengan formulir wpf, dan berurusan dengan layar berkedip. Saya bingung bagaimana kode ini harus ditingkatkan - apakah ini akan masuk ke jendela winform atau wpf? Atau ini tidak cocok untuk situasi khusus saya?
nocarrier
3

Saya tahu ini adalah pertanyaan lama, sudah dijawab, tetapi inilah pendapat saya tentang ini; Saya refactored penangguhan pembaruan menjadi IDisposable - dengan cara itu saya bisa melampirkan pernyataan yang ingin saya jalankan dalam usingpernyataan.

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
sumber
2

Ini bahkan lebih sederhana, dan mungkin hacky - karena saya dapat melihat banyak otot GDI di utas ini , dan jelas hanya cocok untuk skenario tertentu. YMMV

Dalam skenario saya, saya menggunakan apa yang akan saya sebut sebagai UserControl "Induk" - dan selama Loadacara tersebut, saya cukup menghapus kontrol-yang-akan-dimanipulasi dari .Controlskoleksi Induk , dan IndukOnPaint mengurus sepenuhnya melukis anak itu kontrol dengan cara khusus apa pun .. sepenuhnya mengambil kemampuan melukis anak offline.

Sekarang, saya menyerahkan rutinitas cat anak saya ke metode ekstensi berdasarkan konsep ini dari Mike Gold untuk mencetak formulir windows .

Di sini saya membutuhkan sub-set label untuk merender tegak lurus terhadap tata letak:

diagram sederhana dari Visual Studio IDE Anda

Kemudian, saya membebaskan kontrol anak dari dicat, dengan kode ini di ParentUserControl.Loadpengendali acara:

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

Kemudian, di ParentUserControl yang sama, kami melukis kontrol-untuk-dimanipulasi dari bawah ke atas:

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

Setelah Anda meng-host ParentUserControl di suatu tempat, misalnya Formulir Windows - Saya menemukan bahwa Visual Studio 2015 saya merender formulir dengan benar pada Waktu Desain serta runtime: ParentUserControl dihosting di Formulir Windows atau mungkin kontrol pengguna lainnya

Sekarang, karena manipulasi khusus saya memutar kontrol anak 90 derajat, saya yakin semua hot spot dan interaktivitas telah dihancurkan di wilayah itu - tetapi, masalah yang saya pecahkan adalah semua untuk label paket yang perlu melihat dulu dan mencetak, yang bekerja dengan baik untuk saya.

Jika ada cara untuk memperkenalkan kembali hot spot dan kontrol-ke kontrol yatim sengaja saya - saya ingin belajar tentang itu suatu hari nanti (bukan untuk skenario ini, tentu saja, tapi .. hanya untuk belajar). Tentu saja, WPF mendukung kegilaan OOTB .. tapi .. hei .. WinForms masih sangat menyenangkan, oke?

bkwdesign
sumber
-4

Atau cukup gunakan Control.SuspendLayout()dan Control.ResumeLayout().

JustJoost
sumber
9
Layout dan Lukisan adalah dua hal yang berbeda: tata letak adalah bagaimana kontrol anak diatur dalam wadah mereka.
Larry