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...
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...
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...
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