Using Matrix Transformations in System.Drawing
Stillwaters Consulting, Inc.
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)
Microsoft Visual Studio® .NET 2003
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
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 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
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()
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)
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)
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)
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.
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
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.
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.