# Using Matrix Transformations in System.Drawing

**Visual Studio .NET 2003**

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/100^{th} 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/100^{th} 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/20^{th} 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.