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 TMatrixPublic Sub New(ByVal g As Graphics)
Me.mGraphics = g
End SubPublic 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 SubPrivate 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 FunctionPrivate 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 FunctionPublic Sub ResetTransform()
mMatrix = Nothing
End SubPublic 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 = MeReturn 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 SubPrivate 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!