Làm thế nào để tôi đình chỉ vẽ tranh cho một điều khiển và con của nó?


184

Tôi có một điều khiển mà tôi phải sửa đổi lớn. Tôi muốn ngăn chặn hoàn toàn việc vẽ lại trong khi tôi làm điều đó - SuspendLayout và ResumeLayout là không đủ. Làm thế nào để tôi đình chỉ vẽ tranh cho một điều khiển và con của nó?


3
ai đó có thể vui lòng giải thích cho tôi những gì đang vẽ hoặc vẽ ở đây trong bối cảnh này? (tôi mới biết đến .net) ít nhất cung cấp một liên kết.
Mr_Green

1
Thật đáng xấu hổ (hoặc đáng cười) rằng .Net đã ra mắt được hơn 15 năm và đây vẫn là một vấn đề. Nếu Microsoft dành nhiều thời gian để khắc phục các sự cố thực sự như màn hình nhấp nháy như nói, Nhận phần mềm độc hại Windows X , thì điều này đã được khắc phục từ lâu.
jww

@jww Họ đã sửa nó; nó được gọi là WPF.
philu

Câu trả lời:


304

Ở công việc trước đây, chúng tôi đã vật lộn để có được ứng dụng UI phong phú của mình để vẽ ngay lập tức và trơn tru. Chúng tôi đã sử dụng các điều khiển .Net tiêu chuẩn, điều khiển tùy chỉnh và điều khiển phát hiện.

Sau rất nhiều lần sử dụng Google và phản xạ, tôi đã thấy thông báo win32 WM_SETREDRAW. Điều này thực sự dừng các điều khiển vẽ trong khi bạn cập nhật chúng và có thể được áp dụng, IIRC cho bảng điều khiển cha / chứa.

Đây là một lớp rất đơn giản thể hiện cách sử dụng thông điệp này:

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

Có nhiều cuộc thảo luận về vấn đề này - google cho C # và WM_SETREDRAW, vd

C # Jitter

Bố trí treo

Và với người mà nó có thể quan tâm, đây là ví dụ tương tự trong 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
Thật là một câu trả lời tuyệt vời và một sự giúp đỡ to lớn! Tôi đã tạo các phương thức mở rộng SuspendDrawing và ResumeDrawing cho lớp Control, vì vậy tôi có thể gọi chúng cho bất kỳ điều khiển nào trong bất kỳ ngữ cảnh nào.
Zach Johnson

2
Có một điều khiển giống như cây sẽ chỉ làm mới một nút đúng cách sắp xếp lại các con của nó nếu bạn bị sụp đổ sau đó mở rộng nó, dẫn đến nhấp nháy xấu xí. Điều này làm việc hoàn hảo để có được xung quanh nó. Cảm ơn! (Thật thú vị, điều khiển đã nhập SendMessage và đã xác định WM_SETREDRAW, nhưng thực tế không sử dụng nó cho bất cứ điều gì. Bây giờ thì có.)
neminem

7
Điều này không đặc biệt hữu ích. Đây chính xác là những gì Controllớp cơ sở cho tất cả các điều khiển WinForms đã làm cho các phương thức BeginUpdateEndUpdate. Tự gửi tin nhắn không tốt hơn là sử dụng các phương pháp đó để thực hiện công việc nặng nhọc cho bạn và chắc chắn không thể tạo ra kết quả khác nhau.
Cody Grey

13
@Cody Grey - Chẳng hạn như TableLayoutPanels không có BeginUpdate.
TheBlastOne

4
Hãy cẩn thận nếu mã của bạn cho phép gọi các phương thức này trước khi điều khiển được hiển thị - lệnh gọi Control.Handlesẽ buộc điều khiển cửa sổ được tạo và có thể ảnh hưởng đến hiệu suất. Ví dụ nếu bạn đang di chuyển một điều khiển trên biểu mẫu trước khi được hiển thị, nếu bạn gọi này SuspendDrawingtrước, di chuyển của bạn sẽ chậm hơn. Có lẽ nên có if (!parent.IsHandleCreated) returnkiểm tra trong cả hai phương pháp.
yến mạch

53

Sau đây là cùng một giải pháp của ng5000 nhưng không sử dụng P / Gọi.

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

Tôi đã thử nó, nhưng trong giai đoạn trung gian giữa Đình chỉ và Tiếp tục, nó chỉ không hợp lệ. Nghe có vẻ lạ, nhưng liệu nó có thể giữ trạng thái trước khi tạm dừng ở trạng thái thoáng qua không?
người dùng

2
Tạm dừng một điều khiển có nghĩa là không có bản vẽ nào được thực hiện trong khu vực điều khiển và bạn có thể nhận được phần còn lại được vẽ từ các cửa sổ / điều khiển khác trong bề mặt đó. Bạn có thể thử sử dụng điều khiển với DoubleBuffer được đặt thành true nếu bạn muốn "trạng thái" trước đó được rút ra cho đến khi điều khiển được nối lại (nếu tôi hiểu ý của bạn), nhưng tôi không thể đảm bảo rằng nó sẽ hoạt động. Dù sao, tôi nghĩ rằng bạn đang thiếu điểm sử dụng kỹ thuật này: điều đó có nghĩa là để tránh người dùng nhìn thấy một bản vẽ các vật thể tiến bộ và chậm chạp (tốt hơn để thấy tất cả chúng xuất hiện cùng nhau). Đối với các nhu cầu khác, sử dụng các kỹ thuật khác.
ceztko

4
Câu trả lời hay, nhưng sẽ tốt hơn nhiều nếu chỉ ra ở đâu MessageNativeWindowđang ở đâu ; tìm kiếm tài liệu cho một lớp có tên Messagekhông thực sự thú vị.
darda

1
@pelel 1) sử dụng nhập không gian tên tự động (nhấp vào biểu tượng, ALT + Shift + F10), 2) trẻ em không được vẽ bởi Unlimitedate () là hành vi dự kiến. Bạn chỉ nên tạm dừng kiểm soát cha mẹ trong một khoảng thời gian ngắn và tiếp tục điều khiển khi tất cả các con có liên quan bị vô hiệu.
ceztko

2
Invalidate()không hoạt động tốt như Refresh()trừ khi theo sau bởi một.
Eugene Ryabtsev

16

Tôi thường sử dụng một phiên bản sửa đổi nhỏ của câu trả lời của 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();
        }
    }
}

Điều này cho phép tạm dừng / tiếp tục các cuộc gọi được lồng nhau. Bạn phải chắc chắn để khớp từng SuspendDrawingvới a ResumeDrawing. Do đó, có lẽ sẽ không phải là một ý tưởng tốt để công khai chúng.


4
Điều này giúp giữ cho cả hai cuộc gọi cân bằng : SuspendDrawing(); try { DrawSomething(); } finally { ResumeDrawing(); }. Một lựa chọn khác là thực hiện điều này trong một IDisposablelớp và bao gồm phần vẽ trong một phần using. Tay cầm sẽ được chuyển cho nhà xây dựng, sẽ tạm dừng vẽ.
Olivier Jacot-Descombes

Câu hỏi cũ, tôi biết, nhưng gửi "sai" dường như không hoạt động trong các phiên bản gần đây của c # / VS. Tôi đã phải đổi sai thành 0 và đúng thành 1.
Maury Markowitz

@MauryMarkowitz bạn có DllImporttuyên bố wParambool?
Ozgur Ozcitak

Xin chào, hạ thấp câu trả lời này là một tai nạn, xin lỗi!
Geoff

13

Để giúp không quên vẽ lại:

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

sử dụng:

SuspendDrawing(myControl, () =>
{
    somemethod();
});

1
Thế còn khi action()ném ngoại lệ? (Sử dụng thử / cuối cùng)
David Sherret

8

Một giải pháp tốt đẹp mà không cần sử dụng interop:

Như mọi khi, chỉ cần bật DoubleBuffered = true trên CustomControl của bạn. Sau đó, nếu bạn có bất kỳ vùng chứa nào như FlowLayoutPanel hoặc TableLayoutPanel, hãy lấy một lớp từ mỗi loại này và trong các hàm tạo, kích hoạt bộ đệm đôi. Bây giờ, chỉ cần sử dụng các Container có nguồn gốc của bạn thay vì các Container của Windows.Forms.

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

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

2
Đó chắc chắn là một kỹ thuật hữu ích - một kỹ thuật mà tôi sử dụng khá thường xuyên cho ListViews - nhưng nó không thực sự ngăn cản việc vẽ lại xảy ra; chúng vẫn xảy ra ngoài màn hình
Simon

4
Bạn đã đúng, nó giải quyết vấn đề nhấp nháy, không phải là vấn đề vẽ lại ngoài màn hình cụ thể. Khi tôi đang tìm kiếm một giải pháp cho sự nhấp nháy, tôi đã tìm thấy một số chủ đề liên quan như thế này, và khi tôi tìm thấy nó, tôi có thể đã không đăng nó trong chủ đề phù hợp nhất. Tuy nhiên, khi hầu hết mọi người muốn đình chỉ vẽ tranh, có lẽ họ đang đề cập đến việc vẽ trên màn hình, đây thường là một vấn đề rõ ràng hơn so với vẽ ngoài màn hình, vì vậy tôi vẫn nghĩ rằng những người xem khác có thể thấy giải pháp này hữu ích trong chủ đề này.
Eugenio De Hoyos

Ghi đè OnPaint.

6

Dựa trên câu trả lời của ng5000, tôi thích sử dụng tiện ích mở rộng này:

        #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

Sử dụng:

using (this.BeginSuspendlock())
{
    //update GUI
}

4

Đây là sự kết hợp giữa ceztko và ng5000 để mang đến một phiên bản mở rộng VB không sử dụng 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
Tôi đang làm việc với một ứng dụng WPF sử dụng winforms xen kẽ với các hình thức wpf và xử lý hiện tượng nhấp nháy màn hình. Tôi bối rối về cách mã này nên được sử dụng - điều này sẽ đi trong cửa sổ winform hoặc wpf? Hay điều này không phù hợp với hoàn cảnh cụ thể của tôi?
nocarrier

3

Tôi biết đây là một câu hỏi cũ, đã được trả lời, nhưng đây là ý kiến ​​của tôi về vấn đề này; Tôi đã cấu trúc lại việc đình chỉ các bản cập nhật thành IDis Dùng - theo cách đó tôi có thể gửi các câu lệnh tôi muốn chạy trong một usingcâu lệnh.

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

Điều này thậm chí còn đơn giản hơn và có lẽ là hacky - vì tôi có thể thấy rất nhiều cơ GDI trên luồng này , và rõ ràng chỉ phù hợp với một số tình huống nhất định. YMMV

Trong kịch bản của tôi, tôi sử dụng cái mà tôi sẽ gọi là UserControl "Parent" - và trong Loadsự kiện, tôi chỉ cần loại bỏ quyền điều khiển khỏi .Controlsbộ sưu tập của Parent và Parent OnPaintsẽ chăm sóc hoàn toàn cho đứa trẻ kiểm soát theo bất cứ cách nào đặc biệt .. hoàn toàn lấy khả năng vẽ của trẻ em nhé.

Bây giờ, tôi đưa ra thói quen vẽ con của tôi cho một phương pháp mở rộng dựa trên khái niệm này từ Mike Gold để in các mẫu cửa sổ .

Ở đây tôi cần một bộ nhãn phụ để hiển thị vuông góc với bố cục:

sơ đồ đơn giản của Visual Studio IDE của bạn

Sau đó, tôi miễn kiểm soát trẻ em khỏi bị vẽ, với mã này trong ParentUserControl.Loadtrình xử lý sự kiện:

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

Sau đó, trong cùng ParentUserControl, chúng tôi vẽ điều khiển được thao tác từ đầu lên:

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

Khi bạn lưu trữ ParentUserControl ở đâu đó, ví dụ: Biểu mẫu Windows - Tôi thấy rằng Visual Studio 2015 của tôi hiển thị biểu mẫu chính xác tại Giờ thiết kế cũng như thời gian chạy: ParentUserControl được lưu trữ trong Biểu mẫu Windows hoặc có thể điều khiển người dùng khác

Bây giờ, do thao tác cụ thể của tôi xoay điều khiển trẻ 90 độ, tôi chắc chắn tất cả các điểm nóng và tính tương tác đã bị phá hủy trong khu vực đó - nhưng, vấn đề tôi đang giải quyết là tất cả cho nhãn gói cần xem trước và in, mà làm việc tốt cho tôi

Nếu có nhiều cách để giới thiệu lại các điểm nóng và kiểm soát đối với sự kiểm soát mồ côi có chủ đích của tôi - tôi rất muốn tìm hiểu về điều đó một ngày nào đó (tất nhiên không phải cho kịch bản này, nhưng .. chỉ để tìm hiểu). Tất nhiên, WPF hỗ trợ OOTB điên rồ như vậy .. nhưng .. này .. WinForms vẫn còn rất nhiều niềm vui, phải không?


-4

Hoặc chỉ sử dụng Control.SuspendLayout()Control.ResumeLayout().


9
Bố cục và Vẽ tranh là hai điều khác nhau: bố cục là cách các điều khiển con được sắp xếp trong thùng chứa của chúng.
Larry
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.