ฉันจะระงับการระบายสีเพื่อการควบคุมและลูก ๆ ของมันได้อย่างไร


184

ฉันมีการควบคุมซึ่งฉันต้องทำการปรับเปลี่ยนขนาดใหญ่ ฉันต้องการป้องกันไม่ให้วาดใหม่ทั้งหมดในขณะที่ทำเช่นนั้น - SuspendLayout และ ResumeLayout ไม่เพียงพอ ฉันจะระงับการระบายสีเพื่อการควบคุมและลูก ๆ ของมันได้อย่างไร


3
ใครช่วยอธิบายหน่อยได้ไหมว่าภาพวาดหรือภาพวาดในบริบทนี้คืออะไร (ฉันใหม่กับ. net) อย่างน้อยให้ลิงค์
Mr_Green

1
มันเป็นความอัปยศ (หรือน่าหัวเราะ) จริงๆที่. Net ออกมานานกว่า 15 ปีแล้วและนี่ก็ยังเป็นปัญหาอยู่ หาก Microsoft ใช้เวลามากในการแก้ไขปัญหาจริงเช่นหน้าจอกะพริบตามที่ได้รับมัลแวร์Windows Xดังนั้นนี่จะได้รับการแก้ไขเป็นเวลานานแล้ว
jww

@jww พวกเขาซ่อมมันแล้ว มันเรียกว่า WPF
philu

คำตอบ:


304

ที่งานก่อนหน้าของฉันเราพยายามต่อสู้กับการใช้แอพ UI ที่หลากหลายของเราในการวาดภาพได้อย่างราบรื่นและทันที เราใช้การควบคุมแบบ. Net มาตรฐาน, การควบคุมแบบกำหนดเองและการควบคุม devexpress

หลังจากการใช้งาน googling และ 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 สำหรับ C # และ WM_SETREDRAW เช่น

C # Jitter

เลย์เอาต์ที่ถูกระงับ

และผู้ที่อาจเกี่ยวข้องนี่เป็นตัวอย่างที่คล้ายกันใน 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 ดังนั้นฉันสามารถเรียกพวกมันสำหรับการควบคุมใด ๆ ในบริบทใด ๆ
Zach Johnson

2
มีการควบคุมแบบต้นไม้ที่จะรีเฟรชโหนดอย่างถูกต้องในการจัดเรียงลูกของมันใหม่หากคุณยุบแล้วขยายมันซึ่งนำไปสู่การกะพริบน่าเกลียด มันทำงานได้อย่างสมบูรณ์แบบที่จะหลีกเลี่ยง ขอบคุณ! (อย่างพอเพียงตัวควบคุมได้นำเข้า SendMessage และกำหนด WM_SETREDRAW แล้ว แต่ไม่ได้ใช้เพื่ออะไรเลยตอนนี้ก็ทำได้)
neminem

7
สิ่งนี้ไม่มีประโยชน์อย่างยิ่ง นี่คือสิ่งที่Controlชั้นฐานสำหรับการควบคุม WinForms ทั้งหมดทำแล้วสำหรับBeginUpdateและEndUpdateวิธีการ การส่งข้อความด้วยตัวคุณเองนั้นไม่ได้ดีไปกว่าการใช้วิธีการเหล่านั้นในการยกของหนักและแน่นอนว่าคุณจะไม่ได้ผลลัพธ์ที่แตกต่างกัน
Cody Gray

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ไม่ใช่ทุกสิ่งที่ให้ความบันเทิง
darda

1
@pelesl 1) ใช้การนำเข้าเนมสเปซอัตโนมัติ (คลิกที่สัญลักษณ์ ALT + Shift + F10) 2) เด็ก ๆ ที่ไม่ได้ทาสีโดย Invalidate () เป็นพฤติกรรมที่คาดหวัง คุณควรหยุดการควบคุมพาเรนต์สำหรับช่วงเวลาสั้น ๆ และกลับมาทำงานต่อเมื่อลูกที่เกี่ยวข้องทั้งหมดไม่ถูกต้อง
ceztko

2
Invalidate()ใช้งานไม่ได้ผลดีRefresh()เว้นแต่จะตามมาด้วยใครอยู่ดี
Eugene Ryabtsev

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-statement หมายเลขอ้างอิงจะถูกส่งผ่านไปยังตัวสร้างซึ่งจะระงับการวาด
Olivier Jacot-Descombes

คำถามเก่าฉันรู้ แต่การส่ง "false" ไม่ปรากฏใน c # / VS รุ่นล่าสุด ฉันต้องเปลี่ยน false เป็น 0 และ true เป็น 1
Maury Markowitz

@MauryMarkowitz คุณDllImportประกาศwParamว่าเป็นboolอย่างไร
Ozgur Ozcitak

สวัสดีการลงคะแนนคำตอบนี้เป็นอุบัติเหตุขออภัย!
Geoff

13

เพื่อช่วยในการไม่ลืมที่จะวาดภาพ reenable:

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

ทางออกที่ดีโดยไม่ใช้การเชื่อมต่อ:

และเช่นเคยเพียงเปิดใช้งาน DoubleBuffered = true ใน CustomControl ของคุณ จากนั้นหากคุณมีคอนเทนเนอร์เช่น FlowLayoutPanel หรือ TableLayoutPanel ให้หาคลาสจากแต่ละประเภทเหล่านี้และใน Constructor ให้เปิดใช้งานการกำหนดบัฟเฟอร์คู่ ตอนนี้เพียงแค่ใช้คอนเทนเนอร์ที่ได้รับของคุณแทน Windows.Forms Containers

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

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

2
นั่นเป็นเทคนิคที่มีประโยชน์ - ซึ่งฉันใช้บ่อยสำหรับ ListViews - แต่มันก็ไม่ได้ป้องกันการวาดซ้ำเกิดขึ้นจริง พวกเขายังคงเกิดขึ้นนอกจอ
Simon

4
คุณพูดถูกมันแก้ปัญหาริบหรี่ไม่ใช่เฉพาะปัญหาการเขียนแบบนอกหน้าจอ เมื่อฉันกำลังมองหาวิธีแก้ปัญหาการกะพริบฉันพบหลายหัวข้อที่เกี่ยวข้องเช่นนี้และเมื่อฉันพบมันฉันอาจไม่ได้โพสต์ไว้ในกระทู้ที่เกี่ยวข้องมากที่สุด อย่างไรก็ตามเมื่อคนส่วนใหญ่ต้องการที่จะระงับการทาสีพวกเขาอาจหมายถึงการวาดภาพบนหน้าจอซึ่งมักจะเป็นปัญหาที่ชัดเจนมากขึ้นกว่าการวาดภาพนอกจอซ้ำซ้อนดังนั้นฉันยังคงคิดว่าผู้ชมคนอื่น ๆ อาจพบว่า
Eugenio De Hoyos

แทนที่ 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's และ 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 ที่ใช้ winforms ที่ผสมกับรูปแบบ wpf และจัดการกับการกะพริบของหน้าจอ ฉันสับสนในการใช้ประโยชน์จากรหัสนี้ - จะไปในหน้าต่าง winform หรือ wpf หรือไม่ หรือสิ่งนี้ไม่เหมาะกับสถานการณ์เฉพาะของฉัน
nocarrier

3

ฉันรู้ว่านี่เป็นคำถามเก่าตอบแล้ว แต่นี่คือสิ่งที่ฉันทำ ฉัน refactored ระงับการปรับปรุงใน 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 - ฉันพบว่า Visual Studio 2015 ของฉันแสดงผลแบบฟอร์มอย่างถูกต้องในเวลาออกแบบและรันไทม์: ParentUserControl โฮสต์ในแบบฟอร์ม Windows หรือการควบคุมผู้ใช้อื่น ๆ

ตอนนี้เนื่องจากการควบคุมเฉพาะของฉันหมุนการควบคุมเด็ก 90 องศาฉันแน่ใจว่าจุดร้อนและการโต้ตอบทั้งหมดถูกทำลายในภูมิภาคนั้น - แต่ปัญหาที่ฉันแก้ไขคือทั้งหมดสำหรับฉลากแพคเกจที่ต้องการดูตัวอย่างและพิมพ์ ซึ่งทำงานได้ดีสำหรับฉัน

หากมีวิธีที่จะรื้อฟื้นฮอตสปอตและการควบคุมเพื่อควบคุมกำพร้ากำพร้าของฉัน - ฉันชอบที่จะเรียนรู้เกี่ยวกับสักวันหนึ่ง (ไม่ใช่สำหรับสถานการณ์นี้แน่นอน แต่ .. เพียงเพื่อเรียนรู้) แน่นอน WPF สนับสนุนความบ้าคลั่งเช่น OOTB .. แต่ .. เฮ้ .. WinForms ยังสนุกอยู่มาก


-4

หรือเพียงแค่ใช้และControl.SuspendLayout()Control.ResumeLayout()


9
เลย์เอาต์และการระบายสีเป็นสองสิ่งที่แตกต่างกัน: เลย์เอาต์เป็นวิธีที่การจัดเรียงตัวควบคุมลูกในคอนเทนเนอร์
Larry
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.