Set DoubleBuffered on child control

Answered Set DoubleBuffered on child control

  • 12 April 2012 11:29
     
     

    Hello,

    I would like to try setting the doublebuffered property of a child control of a form (named Canv). However, it tells me it is protected and therefore I cannot change it, unless I override it.

    I have had experiences with overriding properties in the past, but only with entire classes. Is it possible to do the same with this particular instance of the Panel, instead of creating a whole new custom class and using that instead?

    What I am ultimately trying to achieve is to draw ~100 controls onto the panel without having them flicker singularly into view. If there's any better way you know of, I'm happy to hear it! Just keep it in VB.NET please :P

    Thanks, Silvisoft.


    Just call me Silvi or LS... My site: here

Semua Balasan

  • 12 April 2012 12:14
     
     

    Bracket the code that creates and displays the controls with a .SuspendLayout and .ResumeLayout.

    See (especially the Note):
    http://msdn.microsoft.com/en-us/library/system.windows.forms.control.suspendlayout.aspx

  • 12 April 2012 12:20
     
     
    Acamar, that didn't work very well unfortunately. It still takes a long time to create, and every control appears individually on screen. I'm using a For Each loop to add the controls based on data from an array -- could that have any effect on the performance?

    Just call me Silvi or LS... My site: here

  • 12 April 2012 12:29
     
     
    Is the problem when adding them (one time) or when painting them (each time)? If possible, call Controls.AddRange instead of .Add

    Armin

  • 12 April 2012 12:32
     
     
    Is the problem when adding them (one time) or when painting them (each time)? If possible, call Controls.AddRange instead of .Add

    Armin

    Adding and painting, really. However if I could at least get a smooth adding it would be nice! Will AddRange make a difference? Because Controls.Clear() didn't exactly work smoothly either until I hid the control.

    Just call me Silvi or LS... My site: here

  • 12 April 2012 12:42
     
     

    I just tried it with 100 textboxes and it works just fine.  Perhaps your controls are more complex.

    For adding, create the controls, add them to a list of controls, then add them to the container's control collection in one statement using AddRange as described above.  This eliminates any delay as a result of creating and initialsing the controls, but the drawing delay will still be there.

    For drawing there is not much you can do.  But 100 controls seems excessive.  Is it possible to redesign the application so that it doesn't need that amount of controls?

  • 12 April 2012 12:49
     
     

    Actually I just went back and remembered there are 263 controls to be created on a page.. but the Labels work just fine. The problem are the PictureBoxes which I cannot remove, and those are 132. However only about 60 have to render at one time on an average-height screen.

    I will try using AddRange and see if it makes a small difference. As for redesigning the whole application, I have a couple options.

    Either I just don't use controls and manually paint the images (they are more or less geometric figures), but that would pose problems with catching the clicks, and it would be a pain to redraw every time the user moves the window off screen or scrolls up and down.

    The second option is to use colour-filled squares instead of images, which will definitely take less time, but it will look really ugly and unprofessional.


    Just call me Silvi or LS... My site: here

  • 12 April 2012 12:50
     
     
    Oh, also the PictureBoxes aren't actually picture boxes. They are UserControls with a BackgroundImage set to the image, and some added functionality (but nothing that displays on screen, just more code)

    Just call me Silvi or LS... My site: here

  • 12 April 2012 12:56
     
     

    Was about to suggest replacing the PictureBoxes by painting in OnPaint, but you did it yourself. So... Capturing clicks just requires subtracting an offset, or is there another issue? Sure, you have to use a loop to find the "control"/figure that has been clicked. Painting when scrolling: Take e.Cliprectangle in OnPaint into account to avoid redrawing all areas.

    EDIT: Just saw your latest message so this mesage of mine may have become inapproparite.


    Armin


  • 12 April 2012 14:04
     
     

    Was about to suggest replacing the PictureBoxes by painting in OnPaint, but you did it yourself. So... Capturing clicks just requires subtracting an offset, or is there another issue? Sure, you have to use a loop to find the "control"/figure that has been clicked. Painting when scrolling: Take e.Cliprectangle in OnPaint into account to avoid redrawing all areas.

    EDIT: Just saw your latest message so this mesage of mine may have become inappropriate.


    Armin


    Yeah, in general I'd rather not use the first option because I'd have to adapt a lot of code and a lot of math. It took me ages to figure it all out and I'd end up redoing it!

    Just call me Silvi or LS... My site: here

  • 12 April 2012 20:52
     
     

    Yeah, in general I'd rather not use the first option because I'd have to adapt a lot of code and a lot of math. It took me ages to figure it all out and I'd end up redoing it!

    I am sure that drawing directly to the form instead of using picture boxes would solve the display delay.    A lot of the math is exactly the same.     I'm not sure sure why Armin suggests a loop to discover the cliccked image - it is not a loop but simply the inverse of the calculation that was originally done to position the image.

    If all the images are drawn to a single bitmap as they are added, removed or changed, then there is only one large drawing to be done in the paint event, and scrolling becomes trivial.   You are trading RAM for CPU, which is a common way to speed things up.

  • 12 April 2012 21:03
     
     

    I'm not sure sure why Armin suggests a loop to discover the cliccked image - it is not a loop but simply the inverse of the calculation that was originally done to position the image.

    If you have the click position and want to find out on which of the painted areas has been clicked, what else can you write but a loop? Maybe I misunderstood something. :-)


    Armin

  • 12 April 2012 21:59
     
      Memiliki Kode

    If you have the click position and want to find out on which of the painted areas has been clicked, what else can you write but a loop? Maybe I misunderstood something. :-)

    The calculation of the clicked item would be done using the inverse of the calculation that was used to determine the area to be painted - a loop is not required.

    Public Class Form1
    
        Dim BMP As Bitmap = New Bitmap(800, 600, System.Drawing.Imaging.PixelFormat.Format32bppArgb)
        Dim RND As New Random
    
        Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
            BMP = New Bitmap(800, 600, System.Drawing.Imaging.PixelFormat.Format32bppArgb)
            Dim g As Graphics = Graphics.FromImage(BMP)
            For I As Integer = 0 To 9
                Dim c As Color = Color.FromArgb(255, RND.Next(1, 255), RND.Next(1, 255), RND.Next(1, 255))
                Dim Col As Integer = I Mod 4
                Dim Row As Integer = I \ 4
                g.FillRectangle(New SolidBrush(c), Col * 50, Row * 50, 50, 50)
            Next
            Me.Invalidate()
        End Sub
    
        Private Sub Form1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Click
            Dim X As Integer = Me.PointToClient(Cursor.Position).X - 100
            Dim y As Integer = Me.PointToClient(Cursor.Position).Y - 100
    
            X = X \ 50
            y = y \ 50
            Dim I As Integer = X + y * 4
            Label1.Text = New Point(X, y).ToString & "=" & I
    
            Dim g As Graphics = Graphics.FromImage(BMP)
            Dim c As Color = Color.FromArgb(255, RND.Next(1, 255), RND.Next(1, 255), RND.Next(1, 255))
            Dim Col As Integer = I Mod 4
            Dim Row As Integer = I \ 4
            g.FillRectangle(New SolidBrush(c), Col * 50, Row * 50, 50, 50)
            Me.Invalidate()
        End Sub
    
        Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
            e.Graphics.DrawImage(BMP, New Point(100, 100))
        End Sub
    End Class
    

  • 12 April 2012 22:14
     
     
    If you have pictureboxes within a container, you can place them anywhere. I read the thread again but I don't find where it's mentioned they are arranged in rows and columns.

    Armin

  • 12 April 2012 22:25
     
     

    It may require some redesign, but OP has indicated that the images are currently being positioned using math - if a calculation is involved in placing the controls then the inverse of that calculation can be used to locate the control position.  It doesn't have to be regular rows and columns.

    But even if a loop is required, it can be optimised by creating a list of regions corresponding to the images - the loop is then an iteration through this list for a hittest, which is going to be fast.  I have an application that does hittesting for thousands of irregular objects and the delay is only just noticeable on a typical business PC. 

  • 12 April 2012 23:21
     
     

    It may require some redesign, but OP has indicated that the images are currently being positioned using math - if a calculation is involved in placing the controls then the inverse of that calculation can be used to locate the control position.  It doesn't have to be regular rows and columns.

    But even if a loop is required, it can be optimised by creating a list of regions corresponding to the images - the loop is then an iteration through this list for a hittest, which is going to be fast.  I have an application that does hittesting for thousands of irregular objects and the delay is only just noticeable on a typical business PC. 

    Sometimes people make a big thing of nothing. If I make a suggestion based on the picture I have from the situation doesn't necessarily mean I contradict your point of view. Your message reads as if I said a loop was too slow, in fact I did not. The reason for mention a table-style layout was not my own limitation to regular rows and colors, but it was in reference to the example you wrote yourself doing just that.


    Armin

  • 14 April 2012 8:24
     
     

    The images are indeed arranged in regular rows and columns -- in fact only four rows, and maximum 33 columns. I could get the position of the images with math as suggested, but it would be hard to transfer other functions of the picture to object-less functions.

    I will experiment with a drawn version separately from my current product to see if there are any improvements. Will it be best to use lines and ellipses, or can I draw the images onto the Graphics element?


    Just call me Silvi or LS... My site: here

  • 14 April 2012 10:21
     
     

    Will it be best to use lines and ellipses, or can I draw the images onto the Graphics element?

    Drawing a line or ellipse is the same as drawing an image, so I am not sure what options you are referring to here. 

  • 14 April 2012 20:12
     
     
    using DrawLine and DrawEllipse as opposed to printing an image onto the graphics element is the difference.

    Just call me Silvi or LS... My site: here

  • 14 April 2012 21:47
     
     
    The only effective difference between DrawLine, DrawImage, DrawEllipse DrawString and the rest is the source of the pixels to be drawn.  This decision has nothing to do with the design of the solution - you can defer any consideration of this until the solution is in place.  You can even change the option on a per-item basis with no effect on the design.  You should choose the one that best suits the visual effect that you need, and only consider a change after you are able to show that such a change might deliver an additional speed improvement.
  • 15 April 2012 11:22
     
     Jawab Memiliki Kode

    You will not achieve what you want by adding multiple controls to the form. The flickering is caused by the fact that each child window not only paints itself, but also causes it's parent to repaint itself, this in turn causes each existing child of the parent to paint itself and so the more child controls that you add the worse the flicker becomes.

    The solution is to paint directly to the form.

    If this were my task I would create a simple class which will be responsible for it's own painting and hittesting.

    Here is a simple class which does just that:

    Imports System.ComponentModel
    Imports System.Drawing.Drawing2D
    
    Public Class LiteEllipse
    
        Private _parent As Control
        Private _location As Point
        Private _size As Size
        Private _borderColor As Color = Color.Red
        Private _fillColor As Color = Color.White
    
        Public Event Click As EventHandler
    
        Public Property Parent As Control
            Get
                Return _parent
            End Get
            Set(value As Control)
                _parent = value
                OnParentChanged(EventArgs.Empty)
            End Set
        End Property
    
        <Browsable(False)> _
        Public ReadOnly Property Bounds As Rectangle
            Get
                Return New Rectangle(Me.Location, Me.Size)
            End Get
        End Property
    
        Public Property Location As Point
            Get
                Return _location
            End Get
            Set(value As Point)
                If _location <> value Then
                    _location = value
                    InvalidateParent()
                End If
            End Set
        End Property
    
        Public Property Size As Size
            Get
                Return _size
            End Get
            Set(value As Size)
                If _size <> value Then
                    _size = value
                    InvalidateParent()
                End If
            End Set
        End Property
    
        Public Property BorderColor As Color
            Get
                Return _borderColor
            End Get
            Set(value As Color)
                If _borderColor <> value Then
                    _borderColor = value
                    InvalidateParent()
                End If
            End Set
        End Property
    
        Public Property FillColor As Color
            Get
                Return _fillColor
            End Get
            Set(value As Color)
                If _fillColor <> value Then
                    _fillColor = value
                    InvalidateParent()
                End If
            End Set
        End Property
    
        Public Sub New(parent As Control)
            Me.Parent = parent
        End Sub
    
        Private Sub OnParentChanged(Empty As EventArgs)
    
            If (Parent IsNot Nothing) Then
                AddHandler Parent.Click, AddressOf OnClick
                AddHandler Parent.Paint, AddressOf OnPaint
            End If
    
        End Sub
    
        Private Sub OnClick(sender As Object, e As EventArgs)
    
            If Me.Size = Size.Empty Then
                Return
            End If
    
            Dim dr As Rectangle = Me.Parent.DisplayRectangle
            Dim rc As Rectangle = Me.Bounds
    
            rc.Offset(dr.Left, dr.Top)
    
            Dim clickPath As GraphicsPath = New GraphicsPath()
    
            clickPath.AddEllipse(rc)
    
            If clickPath.IsVisible(Me.Parent.PointToClient(Cursor.Position)) Then
                RaiseEvent Click(Me, e)
            End If
    
        End Sub
    
        Private Sub OnPaint(sender As Object, e As PaintEventArgs)
    
            If (Me.Size <> Size.Empty) Then
    
                e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
    
                Dim dr As Rectangle = Me.Parent.DisplayRectangle
                Dim rc As Rectangle = Me.Bounds
    
                rc.Offset(dr.Left, dr.Top)
    
                Using fillBrush As New SolidBrush(Me.FillColor)
                    e.Graphics.FillEllipse(fillBrush, rc)
                End Using
    
                Using borderPen As New Pen(Me.BorderColor)
                    e.Graphics.DrawEllipse(borderPen, rc)
                End Using
    
            End If
    
        End Sub
    
        Private Sub InvalidateParent()
            If Me.Parent IsNot Nothing Then
                Me.Parent.Invalidate()
            End If
    
        End Sub
    
    End Class
    

    The problem here is that these classes cannot be added at design time because they are not designable classes. I would create a custom container control which had a collection property to add these classes if I required design time support, but that is something for another thread.

    Another problem is that these controls will not controll scrolling, so you will need to set the parents AutoScroll and AutoScrollMinSize youself to account for the position of the controls.

    Now you can easily add these classes at runtime as follows:

    Public Class Form1
    
        Private Ellipse1, Ellipse2 As LiteEllipse
    
        Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
    
            Me.Ellipse1 = New LiteEllipse(Me)
            Me.Ellipse2 = New LiteEllipse(Me)
    
            Me.Ellipse1.Location = New Point(10, 10)
            Me.Ellipse2.Location = New Point(250, 350)
    
            Me.Ellipse1.Size = New Size(50, 50)
            Me.Ellipse2.Size = New Size(50, 50)
    
            AddHandler Me.Ellipse1.Click, AddressOf EllipseClick
            AddHandler Me.Ellipse2.Click, AddressOf EllipseClick
    
            Me.AutoScroll = True
            Me.AutoScrollMinSize = New Size(310, 410)
    
        End Sub
    
        Private Sub EllipseClick(sender As Object, e As EventArgs)
    
            Dim ellipse As LiteEllipse = CType(sender, LiteEllipse)
    
            If ellipse IsNot Nothing Then
                ellipse.BorderColor = RandomColor()
                ellipse.FillColor = RandomColor()
            End If
    
        End Sub
    
        Public Function RandomColor() As Color
    
            Static rnd As New Random()
    
            Return Color.FromArgb(255, rnd.Next(0, 256), rnd.Next(0, 256), rnd.Next(0, 256))
    
        End Function
    
        Private Sub Form1_Scroll(sender As System.Object, e As System.Windows.Forms.ScrollEventArgs) Handles MyBase.Scroll
            Me.Invalidate()
        End Sub
    
    End Class
    

    Obviously, you will need to modify the class so as to suit your exact requirements and will likely need to add the class instances as an array rather than individually as I have done here (to keep the code a simple as possible for clarity).

    I haven't fully tested this example so there may be some calculations that need to be corrected, but it should get you started.


    Mick Doherty
    http://dotnetrix.co.uk
    http://glassui.codeplex.com

    • Ditandai sebagai Jawaban oleh SilviSoft 15 April 2012 11:25
    •  
  • 15 April 2012 11:25
     
     
    Thank you very much! I will try to adapt that code to fit my needs and I'm sure it will work. :)

    Just call me Silvi or LS... My site: here

  • 15 April 2012 12:33
     
     Jawab Memiliki Kode

    You're welcome.

    Depending upon how complex your painting is (in fact it's probably best to do it anyway) you may want to optimise the InvalidateParent() method as follows:

        Private Sub InvalidateParent()
            If Me.Parent IsNot Nothing Then
    
                Dim dr As Rectangle = Me.Parent.DisplayRectangle
                Dim rc As Rectangle = Me.Bounds
    
                rc.Offset(dr.Left, dr.Top)
                Me.Parent.Invalidate(rc)
    
            End If
    
        End Sub


    Mick Doherty
    http://dotnetrix.co.uk
    http://glassui.codeplex.com

    • Ditandai sebagai Jawaban oleh SilviSoft 15 April 2012 16:47
    •  
  • 15 April 2012 13:48
     
      Memiliki Kode

    You're welcome.

    Depending upon how complex your painting is (in fact it's probably best to do it anyway) you may want to optimise the InvalidateParent() method as follows:

        Private Sub InvalidateParent()
            If Me.Parent IsNot Nothing Then
    
                Dim dr As Rectangle = Me.Parent.DisplayRectangle
                Dim rc As Rectangle = Me.Bounds
    
                rc.Offset(dr.Left, dr.Top)
                Me.Parent.Invalidate(rc)
    
            End If
    
        End Sub


    Mick Doherty
    http://dotnetrix.co.uk
    http://glassui.codeplex.com

    Indeed, it works a lot better and doesn't flash as much!

    However I still have a problem with your code: when painting for the first time, the ellipse is cut off at the bounds of the FillEllipse, so part of the border is not drawn at all. Then if I scroll or move the form out of view and back, it draws the ellipse fully (including the border). Why is this?


    Just call me Silvi or LS... My site: here

  • 15 April 2012 19:26
     
      Memiliki Kode

    I'm not seeing that effect here, but it'll be GDI+ clipping or else a result of Graphics AntiAiias (not really my speciality). Does this happen when you don't set the Graphics Smoothing mode?

    I've played around a little with the code and have modified the OnPaint() method to only draw if the ClipRectangle (inflated by 1 pixel to allow for GDI+ clipping) intersects with the drawing bounds:

        Private Sub OnPaint(sender As Object, e As PaintEventArgs)
    
            If (Me.Size <> Size.Empty) Then
    
                e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
    
                Dim dr As Rectangle = Me.Parent.DisplayRectangle
                Dim rc As Rectangle = Me.Bounds
    
                Dim clipRect As Rectangle = e.ClipRectangle
    
                clipRect.Inflate(1, 1)
    
                rc.Offset(dr.Left, dr.Top)
    
                If clipRect.IntersectsWith(rc) Then
    
                    Using fillBrush As New SolidBrush(Me.FillColor)
                        e.Graphics.FillEllipse(fillBrush, rc)
                    End Using
    
                    Using borderPen As New Pen(Me.BorderColor)
                        e.Graphics.DrawEllipse(borderPen, rc)
                    End Using
    
                End If
    
            End If
    
        End Sub

    ...and have modifed the form to add 250 ellipses in a 50 x 50 grid:

    Public Class Form1
    
        Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
    
            Dim scrollSize As Size
    
            For x As Int32 = 0 To 49
    
                For y As Int32 = 0 To 49
    
                    Dim ellipse As New LiteEllipse(Me)
                    ellipse.Size = New Size(40, 40)
                    ellipse.Location = New Point(10 + (x * 50), 10 + (y * 50))
                    ellipse.BorderColor = Color.Red
                    ellipse.FillColor = Color.White
    
                    scrollSize = Size.Add(New Size(ellipse.Location), New Size(50, 50))
    
                    AddHandler ellipse.Click, AddressOf EllipseClick
    
                Next
    
            Next
    
            Me.DoubleBuffered = True
            Me.AutoScroll = True
            Me.AutoScrollMinSize = scrollSize
    
        End Sub
    
        Private Sub EllipseClick(sender As Object, e As MouseEventArgs)
    
            Dim ellipse As LiteEllipse = CType(sender, LiteEllipse)
    
            If ellipse IsNot Nothing Then
    
                If e.Button = Windows.Forms.MouseButtons.Right Then
                    ellipse.FillColor = Color.White
                    Return
                End If
    
                If ellipse.FillColor.Equals(Color.Blue) Then
                    ellipse.FillColor = Color.Red
                Else
                    ellipse.FillColor = Color.Blue
                End If
    
            End If
    
        End Sub
    
    End Class

    ...oh yes, I also modified the LiteEllipse class to use a MouseEventHandler so that we can determin which button was clicked:

        Public Event Click As MouseEventHandler
    
        Private Sub OnClick(sender As Object, e As MouseEventArgs)
    
            If Me.Size = Size.Empty Then
                Return
            End If
    
            Dim dr As Rectangle = Me.Parent.DisplayRectangle
            Dim rc As Rectangle = Me.Bounds
    
            rc.Offset(dr.Left, dr.Top)
    
            Dim clickPath As GraphicsPath = New GraphicsPath()
    
            clickPath.AddEllipse(rc)
    
            If clickPath.IsVisible(Me.Parent.PointToClient(Cursor.Position)) Then
                RaiseEvent Click(Me, e)
            End If
    
        End Sub


    Mick Doherty
    http://dotnetrix.co.uk
    http://glassui.codeplex.com