Advanced Graphics with .NET Compact Framework

Building managed applications for Windows Mobile devices that have a rich user interface is almost impossible if you restrict yourself to the primitive controls that ship with the .NET Compact Framework.  Luckily building your own custom controls isn’t that difficult.  You typically have to override the painting of the control to render the content the way you want it.  Unfortunately if you want to do some more complex rendering you will still run into problems because the Graphics object that is exposed by the OnPaint event is a significantly reduced subset of the desktop Graphics object.  The main things it misses are the ability to transform (scale, rotate and translate) the graphics.

Lets take an example, say you have a method that draws a cross:

Private Sub DrawCross(ByVal g As Graphics, 
                               ByVal size As Integer)
    g.DrawLine(mForegroundPen, -size, -size, size, size)
    g.DrawLine(mForegroundPen, size, -size, -size, size)
End Sub

As you can see from this method it draws the cross based around the origin.  This is great but the likelihood of you wanting to draw a cross at the origin is virtually 0 since only a quarter of it would be visible.  There are a couple of options in terms of positioning the cross.  Firstly, you can modify the DrawCross method to accept a third parameter that identifies the centre of the cross:

Private Sub DrawCross(ByVal g As Graphics, _
                               ByVal centre As Point, _
                               ByVal size As Integer)
    g.DrawLine(mForegroundPen, centre.X – size, centre.Y – size, _
                                           
centre.X + size, centre.Y + size)
    g.DrawLine(mForegroundPen, centre.X + size, centre.Y – size, _
                                            centre.X – size, centre.Y + size)
End Sub

Now all of a sudden the method has become significantly less readable and you can imagine how it would get if the rendering was more complex than just a cross. The second way to do this, and my preference, is to translate the centre of the graphics canvas.  This way the DrawCross method doesn’t change – it still things it’s drawing at the origin – just the canvas that you are drawing on does.  A way to visualise this is to imagine a pen suspended in mid air ready to draw a cross on the canvas below it.  What we want to do is reposition the canvas underneath so that when the pen draws the cross, the cross is actually at the required position in the canvas.  When we are done, we have to remember to reset the canvas so that other drawing is done at the right place.  We can do this as follows:

Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)
    MyBase.OnPaint(pe)

    Dim g As Graphics = pe.Graphics
   
g.Clear(Me.BackColor)
   
    g.TranslateTransform(Me.Width / 2, Me.Height / 2)
    DrawCross(g, 10)
    g.ResetTransform()
End Sub

As you can see it is clear from here what is going on – we are moving to the centre of the control, drawing the cross with size 10 and then resetting the canvas (just in case other methods are added).

Going back to the original discussion around the .NET Compact Framework you will notice that the TranslateTransform method doesn’t exist on the Graphics object.  I’m guessing that part of the reason for this is the lack of support from the underlying rendering engine but here’s quite a simple way to get around this issue (be warned though, calculations involved in doing these operations can quickly become CPU intensive which can make your application slow and consume battery power!).  You need to create a wrapper graphics class that is capable of doing the layout changes you want:

Public Class TranformGraphics
    Private mGraphics As Graphics
    Private mMatrix As TMatrix

    Public Sub New(ByVal g As Graphics)
        Me.mGraphics = g
    End Sub

    Public Sub DrawLine(ByVal pen As Pen, ByVal x1 As Integer, _
                                                        ByVal y1 As Integer, _
                                                        ByVal x2 As Integer, _
                                                        ByVal y2 As Integer) 
        Me.mGraphics.DrawLine(pen, ConvertedX(x1, y1), _
                                                ConvertedY(x1, y1), _
                                                ConvertedX(x2, y2), _
                                                ConvertedY(x2, y2))
    End Sub

    Private Function ConvertedX(ByVal x As Integer, _
                                           ByVal y As Integer) As Integer
        If Me.mMatrix Is Nothing Then Return x
        Return CInt(mMatrix.R1C1 * x + mMatrix.R1C2 * y + mMatrix.DX)
    End Function

    Private Function ConvertedY(ByVal x As Integer, _
                                           ByVal y As Integer) As Integer
        If Me.mMatrix Is Nothing Then Return y
        Return CInt(mMatrix.R2C1 * x + mMatrix.R2C2 * y + mMatrix.DY)
    End Function

    Public Sub ResetTransform()
        mMatrix = Nothing
    End Sub

    Public Sub TranslateTransform(ByVal dx As Single, ByVal dy As Single)
        Dim trans As New TMatrix(1, 0, 0, 1, dx, dy)
        If mMatrix Is Nothing Then
            mMatrix = trans
        Else
            mMatrix = mMatrix.Multiply(trans)
        End If
    End Sub
End Class

I’ve left the implementation of TMatrix to your imagination but the important method that you need to get right is the matrix multiplication:

Public Function Multiply(ByVal M1 As TMatrix) As TMatrix
    Dim M2 As TMatrix = Me

    Return New TMatrix(M2.R1C1 * M1.R1C1 + M2.R1C2 * M1.R2C1, _
                        M2.R1C1 * M1.R1C2 + M2.R1C2 * M1.R2C2, _
                        M2.R2C1 * M1.R1C1 + M2.R2C2 * M1.R2C1, _
                        M2.R2C1 * M1.R1C2 + M2.R2C2 * M1.R2C2, _
                        M2.R1C1 * M1.DX + M2.R1C2 * M1.DY + M2.DX, _
                        M2.R2C1 * M1.DX + M2.R2C2 * M1.DY + M2.DY)
End Function

There might be an easier way to do this with existing classes, so if you come across something please let me know.  Now back to our code – instead of using the Graphics.TranslateTransform (which doesn’t exist for the .NET CF), we can now use our newly created TransformGraphics.TranslateTransform:

Protected Overrides Sub OnPaint(ByVal pe As PaintEventArgs)
    MyBase.OnPaint(pe)

    Dim g As new TransformGraphics(pe.Graphics)
   
g.Clear(Me.BackColor)
   
    g.TranslateTransform(Me.Width / 2, Me.Height / 2)
    DrawCross(g, 10)
    g.ResetTransform()
End Sub

Private Sub DrawCross(ByVal g As TransformGraphics, 
                               ByVal size As Integer)
    g.DrawLine(mForegroundPen, -size, -size, size, size)
    g.DrawLine(mForegroundPen, size, -size, -size, size)
End Sub

Note we have still had to modify the DrawCross method so that it accepts a TransformGraphics but overall we haven’t really affected the readability of the code.  Please feel free to comment on how you get around the challenges of complex rendering using the .NET Compact Framework!

Leave a comment