Drydo's Blog

Teenager of the Internet

This blog hosted by:
http://blogs.vbcity.com
  Home :: Syndication  :: Login   Community Forums   :: vbCity.com   :: DevCity.NET  

Hey Ho and a bucket of prawns,

Wow - have I been having some fun lately. Loads of GDI, a bit of managed DirectX and messing around with DirectShow - all in the .NET environment and getting some pretty spanking results. Anyhoo, today's chunk of wisdom - if you can call it that - is about the PaintEventArgs ClipRectangle parameter.

Well, I won't go into a full and detailed explanation of the ClipRectangle - but to mention that this element informs the Paint Event / Overriden OnPaint method of the area on the 'canvas' that has been invalidated and requires redrawing. Now most GDI samples I see on the forum essentially redraw the entire object rather than selectively updating the damaged elements of the object and as you can imagine - this isnt particularly efficient...

Now with some GDI routines - it may be simply impossible to redraw an element of a highly complex GUI. However, it is certainly something that any developer should be aware of and the trade-offs of completely repainting an object as opposed to selective redrawing. As a demonstration I've created appropriate code to demonstrate the 'wrong' way of drawing an image onto a form and moving a circle forwards and backwards across the image. What happens is that each tick of the animation timer - the code invalidates the form and causes the entire form to repaint. OK, with double buffering and speedy processors - there doesn't seem to be anykind of delay. However, by increasing the size of the window - a jerkiness does appear, which admittedly could be solved by adjusting the animation interval and the movement increment...

Code Copy HideScrollFull
    Private _BG As Bitmap
    Private _AniTimer As Timers.Timer
    Private _LastDrawArea As Rectangle
    Private _BallPosition As New Rectangle(0, 50, 40, 40)
    Private _BallDirection As Boolean = True

    Private Const MOVEMENT_MODIFIER As Integer = 3
    Private Const ANIMATION_INTERVAL As Integer = 10 ' Milliseconds

    Private Sub Form_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' Use double buffering
        Me.SetStyle(ControlStyles.UserPaint, True)
        Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
        Me.SetStyle(ControlStyles.DoubleBuffer, True)

        ' Load the bitmap in
        Me._BG = ResizeImage(Image.FromFile("C:\test.jpg"))
        ' Lock the form
        Me.FormBorderStyle = FormBorderStyle.FixedSingle
        ' Initialise the animation timer
        _AniTimer = New Timers.Timer
        With _AniTimer
            .Enabled = False
            .Interval = ANIMATION_INTERVAL
            AddHandler _AniTimer.Elapsed, AddressOf AnimateTimer
            .Enabled = True
        End With
    End Sub

    Private Function ResizeImage(ByVal img As Bitmap) As Bitmap
        ' Resizes the specified image to the dimensions of the control
        Dim newImg As New Bitmap(Me.Width, Me.Height, Imaging.PixelFormat.Format32bppPArgb)
        Dim g As Graphics = Graphics.FromImage(newImg)
        g.InterpolationMode = Drawing.Drawing2D.InterpolationMode.High
        g.DrawImage(img, New Rectangle(0, 0, newImg.Width, newImg.Height))
        Return newImg
    End Function

    Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
        ' Draw the BG onto the form
        e.Graphics.DrawImage(Me._BG, New Rectangle(0, 0, Me.Width, Me.Height))

        ' Draw the ball
        e.Graphics.FillEllipse(New SolidBrush(Color.PaleVioletRed), Me._BallPosition)
    End Sub

    Private Sub AnimateTimer(ByVal sender As Object, ByVal e As Timers.ElapsedEventArgs)
        ' Determine the ball direction
        If Me._BallDirection Then
            ' Have we hit the edge
            If Me._BallPosition.X + Me._BallPosition.Width >= Me.Width Then
                ' Reverse direction
                Me._BallDirection = Not Me._BallDirection
                ' Call this routine again
                AnimateTimer(sender, e)
                Return
            Else
                ' We haven't so animate
                Me._BallPosition.X += MOVEMENT_MODIFIER
                ' Redraw
                Me.Invalidate()
            End If
        Else
            If Me._BallPosition.X = 0 Then
                ' Reverse direction
                Me._BallDirection = Not Me._BallDirection
                ' Call this routine again
                AnimateTimer(sender, e)
                Return
            Else
                ' Animate
                Me._BallPosition.X -= MOVEMENT_MODIFIER
                ' Redraw
                Me.Invalidate()
            End If
        End If
    End Sub
. . .

...However, what would the performance be if we simply updated the portion of the screen we needed to actually update rather than updating the entire screen. To do this, I've created a helper routine (CalculateReDrawArea) to calculate a rectangle that contains the area to refresh (the last area drawn + the new area to draw), call the invalidate routine of the form and pass through the rectangle that requires updating and adjusted the OnPaint method to only redraw the appropriate portion of the background image...

Code Copy HideScrollFull
    Private _BG As Bitmap
    Private _AniTimer As Timers.Timer
    Private _LastDrawArea As Rectangle
    Private _BallPosition As New Rectangle(0, 50, 40, 40)
    Private _BallDirection As Boolean = True

    Private Const MOVEMENT_MODIFIER As Integer = 3
    Private Const ANIMATION_INTERVAL As Integer = 10 ' Milliseconds

    Private Sub Form_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        ' Use double buffering
        Me.SetStyle(ControlStyles.UserPaint, True)
        Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
        Me.SetStyle(ControlStyles.DoubleBuffer, True)

        ' Load the bitmap in
        Me._BG = ResizeImage(Image.FromFile("C:\test.jpg"))
        ' Lock the form
        Me.FormBorderStyle = FormBorderStyle.FixedSingle
        ' Initialise the animation timer
        _AniTimer = New Timers.Timer
        With _AniTimer
            .Enabled = False
            .Interval = ANIMATION_INTERVAL
            AddHandler _AniTimer.Elapsed, AddressOf AnimateTimer
            .Enabled = True
        End With

    End Sub

    Private Function ResizeImage(ByVal img As Bitmap) As Bitmap
        ' Resizes the specified image to the dimensions of the control
        Dim newImg As New Bitmap(Me.Width, Me.Height, Imaging.PixelFormat.Format32bppPArgb)
        Dim g As Graphics = Graphics.FromImage(newImg)
        g.InterpolationMode = Drawing.Drawing2D.InterpolationMode.High
        g.DrawImage(img, New Rectangle(0, 0, newImg.Width, newImg.Height))
        Return newImg
    End Function

    Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
        ' Draw the BG onto the form
        e.Graphics.DrawImage(Me._BG, e.ClipRectangle, e.ClipRectangle, GraphicsUnit.Pixel)

        ' Draw the ball
        e.Graphics.FillEllipse(New SolidBrush(Color.PaleVioletRed), Me._BallPosition)

    End Sub

    Private Sub AnimateTimer(ByVal sender As Object, ByVal e As Timers.ElapsedEventArgs)
        ' Determine the ball direction
        If Me._BallDirection Then
            ' Have we hit the edge
            If Me._BallPosition.X + Me._BallPosition.Width >= Me.Width Then
                ' Reverse direction
                Me._BallDirection = Not Me._BallDirection
                ' Call this routine again
                AnimateTimer(sender, e)
                Return
            Else
                ' We haven't so animate
                Me._BallPosition.X += MOVEMENT_MODIFIER
                ' Redraw just the approrpriate portion
                Me.Invalidate(CalculateReDrawArea(Me._BallPosition))
            End If
        Else
            If Me._BallPosition.X = 0 Then
                ' Reverse direction
                Me._BallDirection = Not Me._BallDirection
                ' Call this routine again
                AnimateTimer(sender, e)
                Return
            Else
                ' Animate
                Me._BallPosition.X -= MOVEMENT_MODIFIER
                ' Redraw just the approrpriate portion
                Me.Invalidate(CalculateReDrawArea(Me._BallPosition))
            End If
        End If
    End Sub

    Private Function CalculateReDrawArea(ByVal RectToInvalidate As Rectangle) As Rectangle
        ' Then calculate the total area to invalidate ( dependant on the direction)

        ' Return object
        Dim retRect As Rectangle
        If Me._BallDirection Then
            retRect = New Rectangle(_LastDrawArea.X, RectToInvalidate.Y, RectToInvalidate.Width + (RectToInvalidate.X - _LastDrawArea.X), RectToInvalidate.Height)
        Else
            retRect = New Rectangle(RectToInvalidate.X, RectToInvalidate.Y, RectToInvalidate.Width + (_LastDrawArea.X - RectToInvalidate.X), RectToInvalidate.Height)
        End If

        ' Record the area to be draw
        Me._LastDrawArea = RectToInvalidate

        ' Return the invalidate area
        Return retRect
    End Function
. . .

Now the animation performs much faster. And to test the performance rates, increase the size of the form.  Note: to check the portions of the form being updated in all its Flashy goodness - comment out the line...

Code Copy
Me.SetStyle(ControlStyles.DoubleBuffer, True)

Have fun and the next thing I'll probably blog up is the performance differences between the custom and in-built double-buffering in GDI+.

M

posted on Wednesday, August 10, 2005 12:10 PM