Using Matrix Transformations in System.Drawing
Brian Connolly
Stillwaters Consulting, Inc.
May 2004
Download the Matrix.msi sample file.
Summary: This article shows you how to define a System.Drawing.Drawing2D.Matrix transformation on a Graphics object, and how you can then draw shapes and text using application coordinates and have the Graphics object automatically translate these shapes to device coordinates. (9 printed pages)
Applies to:
Microsoft Visual Studio® .NET 2003
Contents
Introduction
The Sample Function
Constructing the Graphic Bitmap
Creating the Graphic Object
Defining Pen Sizes
Defining the Application Coordinate Transform
Drawing the Grid Lines and Axes
Drawing the Function
Defining the Text Transform for Labels
Conclusion
Introduction
In this article, I create a simple application to graph a mathematical function. The function uses an [x,y] coordinate system around the [0,0] origin. The function will be drawn on a System.Drawing.Graphic object that creates a display bitmap for a Windows Form panel. I use a mathematical function as an example because it shows how to address all of the typical problems relating application coordinate shapes to graphic bitmap shapes:
- Higher coordinate values in mathematical functions move upwards and to the right, whereas the default coordinate system of a System.DrawingGraphic bitmap object has higher coordinate values moving downward and to the right.
- Mathematical functions are real-valued and bitmaps have discrete coordinates.
- The (x)-axis scale of a mathematical diagram can be different from the (y)-axis scale.
- Text requires special handling, and I'll describe an approach that allows text to be drawn using application coordinates.
The Sample Function
The following interface defines the methods and properties we need to draw a mathematical function.
Public Interface IFunction
ReadOnly Property [XMin]() As Double
ReadOnly Property [XMax]() As Double
ReadOnly Property [YMin]() As Double
ReadOnly Property [YMax]() As Double
Function GetY(ByVal X As Double) As Double
Function ToString() As String
End Interface
The properties define the (x) and value ranges for the function diagram. The GetY() function must return a Y value for every (x) value in the (x) value range. Note that the GetY() function can return a (y) value that is outside the range of YMin and Ymax. In this case, the function line for those values won't appear on the diagram.
In this example, I'll be working with the exponential function between [-5,-5] and [5,5].
Private mXMin As Double = -5.0
Private mXMax As Double = +5.0
Private mYMin As Double = mXMin
Private mYMax As Double = mXMax
Public Function GetY(ByVal X As Double) As Double _
Implements IFunction.GetY
Return Math.Exp(X)
End Function
Public Function ToString() As String _
Implements IFunction.ToString
Return " Y = Exp(X)"
End Function
We'll use application coordinates to create the graph in Figure 1.

Figure 1. Graph based on application coordinates
Constructing the Graphic Bitmap
The Windows Form will use an instance of the class FunctionDiagram to construct a bitmap for the function display panel. An instance of FunctionDigram is created by passing the instance of iFunction and the pixel coordinates of the panel.
Dim lFunctionDiagram As FunctionDiagram = New FunctionDiagram _
(Me.panelFunctionDiagram.Width, _
Me.panelFunctionDiagram.Height, mIFunction)
Me.panelFunctionDiagram.BackgroundImage = _
lFunctionDiagram.ChartBitmap
Me.Text = "Application Coordinate Drawing for" _
+ mIFunction.ToString()
Creating the Graphic Object
We'll draw the shapes needed for our diagram by drawing on a System.Drawing.Graphics object named mGraphics. mGraphics is created from a bitmap of the proper dimensions, and it is filled with a white background.
mWidthPixels = WidthPixels
mHeightPixels = HeightPixels
mBitmap = New Bitmap _
(WidthPixels, HeightPixels, PixelFormat.Format16bppRgb555)
mGraphics = Graphics.FromImage(mBitmap)
mGraphics.FillRectangle _
(New SolidBrush(Color.White), 0, 0, WidthPixels, WidthPixels)
Defining Pen Sizes
In the diagram, we'll show a background grid, the (x)- and (y)- axes with labels, and the function itself. The first step is to determine the size of the diagram in our application ((x) and (y)) coordinates, and then define pen sizes with respect to the diagram size. The pen used to draw the (x)- and (y)- axes will be 1/100th of the (x) and (y) ranges.
mXAxisPensize = (mMaxY - mMinY) / 100.0
mYAxisPensize = (mMaxX - mMinX) / 100.0
mFunctionPenSize = Math.Min(mXAxisPensize, mYAxisPensize)
Defining the Application Coordinate Transform
The default coordinate system of the mGraphics object has [0,0] in its upper left corner and [width, height] in the lower right, where width and height correspond to the pixel ranges of the bitmap that was used to create it. In this example, we use an application coordinate system from [-5,-5] to [5,5], where higher coordinate values are upwards and to the right of lower coordinate values. If we do nothing to the mGraphics object and graph our sample function Y=exp(X), half of the diagram won't be shown at all because negative (x) coordinates don't exist in the mGraphics space. The part that does show will be squeezed into a few pixels at the upper left.
We need to prepare mGraphics so that we can draw shapes on it using our application coordinate system. The Graphics Transform property can be set to a System.Drawing.Drawing2D.Matrix instance. A matrix represents an affine transformation, which is a combination of a linear transformation matrix and an offset matrix. The 2X2 linear transformation matrix can accomplish rotations, reflections, and scale changes. The 1X2 offset matrix is applied after the linear transformation, and shifts the resulting shape by adding fixed amounts to its coordinates. A System.Drawing.Drawing2D.Matrix object represents the entire affine transformation.
In our case, we define a Matrix transform to transform [x,y] coordinates to the [width, height] coordinates of the mGraphics instance. There is a simple method that can be used to create the matrix we need. First, create a System.Drawing.RectangleF containing three corners of the [x,y] coordinate system. Second, create an array of System.Drawing.PointF that contain three corners of the Graphic instance coordinates. Then, use the matrix constructor to automatically create a transformation matrix. The steps to do this are shown below:
Dim rectPlotF = New RectangleF _
(mMinX, mMaxY, (mMaxX - mMinX), -(mMaxY - mMinY))
Dim bmpcorners(2) As PointF
bmpcorners(0) = New PointF(0, 0)
bmpcorners(1) = New PointF(WidthPixels, 0)
bmpcorners(2) = New PointF(0, HeightPixels)
' the graphic object will now implicitly convert (x,y)
' to pixel coordinates
mTransform = New Matrix(rectPlotF, bmpcorners)
mGraphics.Transform = mTransform
Figure 2 illustrates the Matrix transform.

Figure 2. mGraphics Transformation Matrix
Now we can create GraphicsPath instances the grid lines, axes, and the function itself, using the natural application coordinates. When we pass an instance of GraphicsPath to mGraphics.DrawPath, the coordinate transformation will happen automatically.
XGrid(Color.Gray, Color.Black, Color.Black, mYAxisPensize)
YGrid(Color.Gray, Color.Black, Color.Black, mXAxisPensize)
DrawFunction(Color.Red)
Drawing the Grid Lines and Axes
The following code draws the vertical grid lines, as well as the (y)- axis. AxisPoints provides a collection of integer values that contain 0. A vertical (y)-axis line is drawn at x=0, grid lines one-fourth the size of the axis are drawn at the other Axis Points.
Private Sub YGrid _
(ByVal GridColor As Color, ByVal AxisColor As Color, _
ByVal LabelColor As Color, ByVal LineWidth As Double)
Dim lLine As GraphicsPath
Dim lGridPen As New Pen(GridColor, LineWidth / 4.0)
Dim lAxisPen As New Pen(AxisColor, LineWidth)
For Each i As Integer In AxisPoints(mMinY, mMaxY)
lLine = New GraphicsPath
lLine.AddLine(New PointF(mMinX, i), New PointF(mMaxX, i))
If i = 0 Then
mGraphics.DrawPath(lAxisPen, lLine)
Else
mGraphics.DrawPath(lGridPen, lLine)
End If
DrawString(i.ToString, New PointF(0, i), LabelColor)
Next i
End Sub
We call a special DrawString function to label the axis points. This routine is needed to handle some special issues regarding text drawing and transforms, and it will be described later. A similar method is used to draw the horizontal grid lines and the (x)-axis.
Drawing the Function
The function is drawn as a set of line segments, each 1/100th the width of the coordinate system:
Private Sub DrawFunction(ByVal Color As Color)
Dim lPen As New Pen(Color, mFunctionPenSize)
Dim lLineSegment As GraphicsPath = New GraphicsPath
Dim lLastPoint As New PointF(mMinX, mIFunction.GetY(mMinX))
Dim lThisPoint As PointF
For lThisXValue As Double = mIFunction.XMin _
To mIFunction.XMax _
Step (mIFunction.XMax - mIFunction.XMin) / 100.0
lThisPoint = _
New PointF(lThisXValue, mIFunction.GetY(lThisXValue))
lLineSegment.AddLine(lLastPoint, lThisPoint)
lLastPoint = lThisPoint
Next lThisXValue
mGraphics.DrawPath(lPen, lLineSegment)
End Sub
Defining the Text Transform for Labels
Text requires special handling. While the Graphics class has a DrawString method, the coordinates and bounding rectangles that it allows you to specify are in bitmap coordinates. We want to position text using application coordinates.
FunctionDiagram.DrawSting shows how to do this task. DrawString is used to label the (x)- and (y)-axis, and the FunctionPoint parameter is in application coordinates:
Private Sub DrawString _
(ByVal Str As String, ByVal FunctionPoint As PointF, _
ByVal Color As Color)
Dim lEmsize As Integer = mHeightPixels / 20
Dim lTextPath = New GraphicsPath
lTextPath.AddString(Str, mFontFamily, mFontStyle, lEmsize, _
New Point(5, 5), mStringFormat)
Dim lRectF As RectangleF = New RectangleF( _
New PointF(0.0, 0.0), _
New SizeF(mWidthPixels, mHeightPixels))
Dim lbmpcorners(2) As PointF
lbmpcorners(0) = New PointF(FunctionPoint.X, FunctionPoint.Y)
lbmpcorners(1) = New PointF _
(FunctionPoint.X + mMaxX, FunctionPoint.Y)
lbmpcorners(2) = New PointF(FunctionPoint.X, _
FunctionPoint.Y - (mMaxY - mMinY))
Dim lTextTransform As Matrix = New Matrix _
(lRectF, lbmpcorners)
lTextPath.Transform(lTextTransform)
Dim lPen As New Pen(Color, -1)
mGraphics.DrawPath(lPen, lTextPath)
End Sub
First, a GraphicsPath instance, lTextPath, is created and we use its AddString method to draw the text. mEmsize is specified as the height of each character in pixels, and in this case it is set to 1/20th of the height of the diagram. The upper left corner of the leftmost character will begin at five pixels down from the upper left corner.
When AddString completes, lTextPath represents the characters as a set of points. The points use a default coordinate system where the upper left corner is [0,0], and increasing coordinate values move downwards and to the right. Now we need to transform those coordinates to our application coordinate system. To do this, we define a new Matrix transform between two rectangles:
- bmpcorners are the same bitmap coordinates used to define the global mGraphics instance used in the application.
- lRectF is real coordinate system that is identical to the application coordinate system, except it is shifted so that the FunctionPoint that was passed serves as its origin.
The matrix instance, lTextTransform, is constructed as a transformer between (1) and (2). Now when lTextPath.Transform is invoked, the path coordinates are transformed to application coordinates with the correct positioning with respect to FunctionPoint. Figure 3 illustrates the successive transformations.

Figure 3. Text Drawing with Application Coordinates
Now that the points of the character glyphs in lTextPath have been transformed to application coordinates, mGraphics.DrawPath(lPen, lTextPath) drawz them in the proper place in the diagram.
Conclusion
This article illustrates how Matrix transformations provide a very simple way to adjust your GDI+ work without getting into any of the low-level graphic issues. Using this method of adjusting for position and size is much easier and faster than doing the graphic calculations manually.