Export (0) Print
Expand All

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/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:

  1. bmpcorners are the same bitmap coordinates used to define the global mGraphics instance used in the application.
  2. 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.

Show:
© 2014 Microsoft