Terrific Transformations

f you read my DevX article “Beautify Your UIs with Perfect Pen and Brush Control,” then you know all about how to draw lines that are thick or thin; dashed, striped, or solid; beveled, mitered, or rounded; and that have standard or customized end caps. You also know how to fill areas with solid colors, hatch patterns, linear color gradients, and path color gradients.

The Pen and Brush objects that you use to draw and fill these kinds of objects correspond fairly directly to the pens and brushes that you would use to draw on a piece of paper or paint on a canvas. They allow you to produce remarkable results such as text that shades from one color to another and lines with customized arrows at their end points.

While most developers understand the connection between Pens and Brushes and pens and brushes, they don’t realize there are also ways to affect the paper and canvas. Transformations allow you to move, rotate, and stretch the canvas.

Actually, you can use transformations to control each drawing operation that you perform, so you can apply different transformations to different parts of the drawing. It’s as if you can make a pile of transparent canvases?all moved, rotated, and stretched in any way you like.

While this may sound a bit abstract (in fact, it would be an interesting way to make an abstract painting), transformations have some very practical applications. For example, suppose you write a bunch of code that draws a graph and a pie chart. Then you discover that you need to add some text and numbers to the form and when you do so, the picture is too big. Given enough time and caffeine, you could rewrite all of your code to make the new text fit.

But then suppose you learn that you need to put the same drawing in a printed document. The printer’s page doesn’t have the same size and resolution as a form on the screen so you’ll need to reformat the results to fit better. You may also need to move the drawing so you can add more text to fill the page.

Transformations can simplify all of this extra work. By using a relatively simple transformation, you can easily move, stretch, and rotate the original image to make room for more text or to resize it to fit on a printed page.

This article explains how basic transformations work. It then shows how you can use them to display graphics in an area normally, centered, or sized to fit the area.

Transformation Methods
GDI+ graphics represents a transformation using a 3-by-3 matrix. I won’t bore you with the details of exactly what values are in the matrix’s nine entries because you probably won’t find it all that interesting. (Unless you’re a math major like I was. In that case, you may find it quite interesting to see how the entries represent translation, scaling, and rotation. You can learn the details in my book Visual Basic Graphics Programming or in any other good graphics programming book.)

The Graphics object provides three methods for adding transformations to the output: TranslateTransform, ScaleTransform, and RotateTransform.

TranslateTransform
The Graphics object’s TranslateTransform method takes two parameters that specify the amount by which you want to translate the resulting drawing in the X and Y directions. For example, the following code executes in a Paint event handler. It uses the Graphics object’s DrawRectangle method to draw a red 200 x 100 pixel rectangle.

It then adds a transformation to the Graphics object to move future drawing 30 pixels to the right (in the X direction) and 50 pixels down (in the Y direction). It finishes by drawing the exact same rectangle, although this time in green:

?
Figure 1. TranslateTransform Results: The TranslateTransform method moves a drawing:
   e.Graphics.DrawRectangle(Pens.Red, 10, 10, 200, 100)   e.Graphics.TranslateTransform(30, 50)   e.Graphics.DrawRectangle(Pens.Green, 10, 10, 200, 100)

The output of the program TranslateRectangle (available for download in both Visual Basic and C# versions) in Figure 1 shows the result. Notice that the green rectangle is the same size as the red one, just moved down and to the right. (This program and the others shown in this article are available for download in Visual Basic and C# versions.)

ScaleTransform
The Graphics object’s ScaleTransform method takes two parameters that tell how much future drawing operations should be stretched in the X and Y directions. The following Paint event handler code draws a red square. It then uses ScaleTransform to make future drawing stretch horizontally by a factor of 1.5 and vertically by a factor of 3. It finishes by drawing the same square again, this time in green:

?
Figure 2. ScaleTransform Results: The ScaleTransform method stretches a drawing vertically and horizontally:
   e.Graphics.DrawRectangle(Pens.Red, 10, 10, 50, 50)   e.Graphics.ScaleTransform(1.5, 3)   e.Graphics.DrawRectangle(Pens.Green, 10, 10, 50, 50)

Figure 2 shows the result. Because the call to ScaleTransform used different arguments for the vertical and horizontal scale factors, the green rectangle is no longer a square.

Notice also that the green rectangle’s upper left corner has moved. The X and Y coordinates of all of the rectangle’s corners have been stretched by the scale factors so the upper left corner has moved from (10, 10) to (10 * 1.5, 10 * 3) = (15, 30).

Finally notice that even the green rectangle’s pen has been scaled. The vertical lines are scaled by a factor of 1.5 so they look the same as before. The horizontal lines, however, are scaled by a factor of 3 so they are noticeably thicker than the original lines.

When you use big scale factors, the lines are scaled greatly and may look really ugly. To compensate, you can make a custom Pen with a really thin line (for example, a width of 0.05 if you are scaling by a factor of 20). Alternatively you can make a Pen with line width 0, which GDI+ always draws one pixel wide, even when it is scaled.

You can use negative scale factors in the call to ScaleTransform. The following code scales the drawing’s X coordinate by a factor of -1. That’s the same as flipping the result horizontally around the Y-axis and is exactly what you’d get if you held the drawing up to a mirror:

   e.Graphics.ScaleTransform(-1, 1)

RotateTransform
The Graphics object’s RotateTransform method takes one parameter that indicates the angle in degrees clockwise that future drawing should be rotated around the origin (0, 0). The following code draws a red rectangle and an orange line from the origin to the rectangle’s upper left corner. It then uses ScaleTransform to rotate future drawing by 30 degrees and redraws the rectangle and line in green and blue respectively:

?
Figure 3. RotateTransform Results: The RotateTransform method rotates a drawing.
   e.Graphics.DrawRectangle(Pens.Red, 20, 20, 200, 100)   e.Graphics.DrawLine(Pens.Orange, 0, 0, 20, 20)   e.Graphics.RotateTransform(30)   e.Graphics.DrawRectangle(Pens.Green, 20, 20, 200, 100)   e.Graphics.DrawLine(Pens.Blue, 0, 0, 20, 20)

Figure 3 shows the result. Notice that the blue line still starts at the origin (0, 0), the same place the orange line started. All rotation is around the origin so that’s the only point in the drawing that looks the same both before and after the rotation. This little detail is important when you want to rotate around some point other than the origin, and I’ll say more about it later.

Translation, scaling, and rotation are mathematics, not magic. With a little work, you could write code to transform the corners of a rectangle and draw them without bothering with the Graphics object’s transformation methods. You could use the same code to transform any drawing made up of straight lines. With more work, you might even be able to “manually” transform curves such as ellipses and arcs.

However, transforming text and images is a lot tougher. You can’t simply transform the corners of an image and expect the interior points to be transformed, as well. It takes some special algorithms to transform every point in an image smoothly and, while the calculations are interesting, they’re time consuming.

?
Figure 4. Transform All Output: Transformation methods apply to both images and text.

Fortunately, when you use the Graphics object’s transformation methods, any images or text that you draw are transformed as well. The TransformImage program shown in Figure 4 uses transformations to rotate both the image and text.

Composing Transformations
One of the coolest features of transformations (actually the whole reason for implementing them as matrices) is that you can easily combine them to represent a series of graphical operations. To represent a series of transformations represented by matrices as a single matrix, you simply multiply together the individual transformation matrices.

For example, suppose you want to translate, scale, rotate, and translate a set of drawing commands. Suppose the transformation matrices T1, S, R, and T2 represent these operations. Then you can represent this complicated series of transformations with a single transformation matrix T that is the product of the matrices representing the separate transformations:

   T = T1 * S * R * T2

The Graphics object’s transformation methods automatically add the new transformation to any previously selected one. For example, the following code prepares a Graphics object named gr to draw with a translation, scaling, and rotation:

   gr.TranslateTransform(100, 100)   gr.ScaleTransform(30)   gr.RotateTransform(30)

Unfortunately, for some bizarre reason these routines add their transformations before any previously defined transformations. In this example, that means the Graphics object will actually first rotate the drawing, then scale it, and finally translate the result, the opposite of what you might expect.

In other words, what you should remember from this is that the order of transformations is very important (in general, a rotation followed by a translation is not the same as a translation followed by a rotation) so it’s important to build the combined transformation in the correct order.

While the default behavior for these methods is to have operations invoked last occur first (this is so counterintuitive that it still weirds me out), you can change this behavior by adding a final argument to the method calls. In the following code, the new parameters tell the Graphics object to apply the transformations in the order: translate, scale, rotate:

   gr.TranslateTransform(100, 100, Drawing2D.MatrixOrder.Append)   gr.ScaleTransform(30, Drawing2D.MatrixOrder.Append)   gr.RotateTransform(30, Drawing2D.MatrixOrder.Append)

Usually when I run som transformation code and don’t see the image I expect to see, it’s because I forgot that new transformations are added after existing ones by default and I didn’t include the Append parameter. If things look wrong?particularly if you see no drawing at all?recheck your code and make sure the transformations are being applied in the order you want.

Common Transformations
A particularly common graphic operation is to rotate or stretch a drawing around a point other than the origin. Deriving the mathematics for this operation directly would be hard but building the transformation out of simpler ones is relatively easy.

For example, suppose you want to rotate a drawing 30 degrees around the point (100, 200). You can build this transformation by first translating the point (100, 200) to the origin, rotating 30 degrees around the origin, and then translating the point that’s still at the origin back to (100, 200). The following code shows how you could do this for the Graphics object gr:

   gr.TranslateTransform(-100, -200, Drawing2D.MatrixOrder.Append)   gr.RotateTransform(30, Drawing2D.MatrixOrder.Append)   gr.TranslateTransform(100, 200, Drawing2D.MatrixOrder.Append)

The RotateAround method shown in the following code makes this type of combined transformation easier. It takes the center and angle of rotation as parameters and builds the combined transformation for you:

   ' Rotate around the indicated point.   Private Sub RotateAround(ByVal gr As Graphics, _    ByVal X As Single, ByVal Y As Single, ByVal degrees As Single)       ' Translate to center the rectangle at the origin.       gr.TranslateTransform(-X, -Y, Drawing2D.MatrixOrder.Append)          ' Rotate.       gr.RotateTransform(degrees, Drawing2D.MatrixOrder.Append)          ' Translate the result back to its original position.       gr.TranslateTransform(X, Y, Drawing2D.MatrixOrder.Append)   End Sub

The following event handler code shows how a program might use this method. The code draws a rectangle, calls the RotateAround method to rotate around the rectangle’s center, and then draws the same rectangle again:

?
Figure 5. Rotating Around a Point: The RotateAround method makes it easy to rotate around an arbitrary point.
   ' Draw the original rectangle.   e.Graphics.DrawRectangle(Pens.Red, 50, 50, 100, 100)      ' Rotate 30 degrees around the rectangle's center.   RotateAround(e.Graphics, 100, 100, 30)      ' Draw the rectangle.   e.Graphics.DrawRectangle(Pens.Green, 50, 50, 100, 100)

Figure 5 shows the result. Compare this to Figure 3, which shows a rectangle rotated around the origin.

Using a similar transformation compositing technique, you can create a ScaleAround method that translates a point to the origin, scales, and then translates the origin back to its original position. For example, ScaleAround would let you scale an object around its center, making it bigger or smaller without moving it.

Centering Pictures
Some other useful transformations involve drawing an object inside a given area. Common ways you might want to draw the object include:

?
Figure 6. Sizing Content to Fit: Program FitToBoxes uses transformations to fit smiley faces into PictureBoxes in different ways.
  • Drawing the object as it is without any transformations
  • Drawing the object centered in the area
  • Drawing the object stretched to fill the area
  • Drawing the object as large as possible within the area without stretching it

The example program FitToBoxes (download here), shown in Figure 6, demonstrates each of these alternatives. The program’s DrawSmiley subroutine draws a smiley face in the rectangle -20 <= x <= 20, -20 <= y <= 20. The series of images in the program's top row shows the smiley untransformed, centered in its display area, stretched to fill the display area, and enlarged as much as possible without stretching in a relatively tall and thin display area. The bottom picture shows the smiley enlarged without stretching in a relatively short and wide display area. Program FitToBoxes includes several functions that make using these common transformations easier. Each function returns a transformation matrix that the program can assign to a Graphics object’s Transform property. For example, the following code shows how the program draws the centered smiley face:

   Dim world_rect As New RectangleF(-20, -20, 40, 40)   e.Graphics.Transform = _       CenterWorld(world_rect, PictureBox2.ClientRectangle)   DrawSmiley(e.Graphics)

This code makes a RectangleF to define the area occupied by the smiley face in its native or world coordinate system. It calls the CenterWorld method to build a transformation that maps the world coordinate rectangle onto the device coordinate rectangle where the smiley face should be displayed. CenterWorld returns a matrix representing the necessary transformation, which the code assigns to the Graphics object’s Transform property. The code then simply calls the DrawSmiley method to draw the smiley face; the transformation centers the result.

The following code shows the CenterWorld function. It first calculates how far from the left and top edges of the device coordinate rectangle the world coordinates must be positioned to center the result. It creates a new Matrix object and calls its Translate method to make the Matrix represent the necessary translation. The function then returns the Matrix:

   ' Make a transformation that centered the world coordinate    ' rectangle in the device coordinate rectangle.   Private Function CenterWorld(ByVal world_rect As RectangleF, _    ByVal device_rect As RectangleF) As Matrix       ' Find the necessary left and top margins.       Dim l_margin As Integer = _           device_rect.Left - world_rect.Left + _           (device_rect.Width - world_rect.Width)  2       Dim t_margin As Integer = _           device_rect.Top - world_rect.Top + _           (device_rect.Height - world_rect.Height)  2          ' Make the transformation.       Dim result As New Matrix()       result.Translate(l_margin, t_margin)       Return result   End Function

The following code shows the StretchToFitWorld function, which returns a transformation matrix that stretches a world coordinate rectangle to fit a device coordinate rectangle. It first defines an array of PointF objects that contain the coordinates of the device rectangle’s upper left, upper right, and lower left corners. It then creates a Matrix object, passing its constructor the world coordinate rectangle and the coordinate array. This version of the Matrix class’s constructor automatically creates a transformation mapping the world rectangle onto the rectangle defined by the array of three points. (Note that it can also map the world rectangle onto a skewed parallelogram if the three corner points don’t define a rectangle.)

   ' Make a transformation that maps a world coordinate    ' rectangle to device coordinate rectangle.   Private Function StretchToFitWorld( _    ByVal world_rect As RectangleF, _    ByVal device_rect As RectangleF) As Matrix       Dim pts() As PointF = { _           New PointF(device_rect.Left, device_rect.Top), _           New PointF(device_rect.Right, device_rect.Top), _           New PointF(device_rect.Left, device_rect.Bottom) _       }       Return New Matrix(world_rect, pts)   End Function

The ScaleToFitWorld method shown in the following code is the trickiest of these transformation-building methods. It starts by comparing the height-to-width aspect ratios of the world and device rectangles to determine whether the scaled world rectangle will be limited by the device rectangle’s height or its width.

For example, the smiley face in the top right picture in Figure 6 is limited by the display area’s width while the picture at the bottom in Figure 6 is limited by its display area’s height.

Based on the rectangles’ relative aspect ratios, the code calculates the scale factor that will make the world rectangle as tall or wide as possible.

The code creates a new Matrix object and then builds a combined transformation to map the world rectangle to an enlarged and centered position on the device rectangle. It starts by translating the world rectangle to the origin and then scaling it by the calculated scale factor.

Next the code must translate the result so it is centered in the device rectangle. It calculates the world rectangle’s scaled size and determines where it must translate the rectangle to center it properly. It adds the needed translation transformation and returns the resulting Matrix object (see Listing 1).

These transformation functions are particularly useful when printing. Depending on your application, you might want to center a picture on the printed page. You might also want to enlarge the picture to fit the page, either with or without stretching it. The CenterWorld, StretchToFitWorld, and ScaleToFitWorld functions not only make these operations easy, but also let you center or fit an image to a specific portion of the page so you can add other graphics and text around it.

Inverting Transformations
When you need to fit a picture to a printed page, the transformation functions described in the previous section are just about all you need. You create the proper transformation, draw on the paper, and you’re done.

When you transform a drawing on the screen, however, you sometimes need to reverse the transformation. For example, suppose you want to let the user interact with a bar chart or map that you’ve drawn on the screen. If you use transformations to scale the image to fit the form, then the point where the user clicks doesn’t necessarily match up with the original drawing coordinates. In other words, the user’s click is in device coordinates but the image data is in world coordinates.

To draw the picture, you build a transformation that maps from world coordinates to device coordinates. To see where the user clicked, you need a transformation that maps back from device coordinates to world coordinates. Fortunately each of the transformations used in this article have simple inverses: the inverse of a translation by (td, ty) is a translation by (-tx, -ty); the inverse of a scaling by (sx, sy) is a scaling by (1/sx, 1/sy); and the inverse of a rotation by theta degrees is a rotation by ?theta degrees.

?
Figure 7. Translating from Device Coordinates to World Coordinates: The example program ‘BullsEye” maps user mouse clicks from device coordinates to world coordinates.

To invert a series of transformations, you just apply the inverse transformations in reverse order. For example, the inverse of TranslateTransform(10, 20), ScaleTransform(-2, 4), RotateTransform(30) is RotateTransform(-30), ScaleTransform(-1/2, 1/4), TranslateTransform(-10, -20).

While you could build this transformation yourself easily enough, the Matrix class provides an Invert method that makes the process even easier. Simply create a Matrix object that represents the Graphics object’s transformation and call its Invert method. Then you can use the inverted Matrix’s TransformPoints method to map points back from device coordinates to world coordinates.

The example program BullsEye, shown in Figure 7, uses transformations to draw the same bull’s-eye in three differently shaped PictureBoxes. When the user clicks on a picture, the program uses an inverted transformation to find the point that the user clicked in world coordinates. It then tells the user which region was clicked in the form’s caption.

When BullsEye starts, it builds the transformations it will need later to map the bull’s-eye drawing onto the PictureBoxes. It also inverts those transformations and saves the results so it can map user clicks to world coordinates.

The following code shows how the program builds the transformations for the first PictureBox. The code for the other PictureBoxes is similar so I’ve omitted it to save space:

   Private m_WtoD1 As Matrix ' World to device   Private m_DtoW1 As Matrix ' Device to world      ' Make transforms.   Private Sub Form1_Load(ByVal sender As System.Object, _    ByVal e As System.EventArgs) Handles MyBase.Load       Dim world_rect As New RectangleF(-1, -1, 2, 2)       Dim device_rect As RectangleF          ' PictureBox1.       device_rect = PictureBox1.ClientRectangle       device_rect.Width -= 1       device_rect.Height -= 1       m_WtoD1 = StretchToFitWorld(world_rect, device_rect)       m_DtoW1 = m_WtoD1.Clone       m_DtoW1.Invert()          '...   End Sub

This code builds rectangles to represent the world and device coordinates (it subtracts a bit from the PictureBox’s client rectangle so the picture doesn’t get covered by the control’s border at the edges) and calls the StretchToFitWorld method to make a transformation that stretches the world coordinates to fit the PictureBox.

The code then uses the transformation matrix’s Clone method to make a copy of the matrix and calls the copy’s Invert method to make the inverse transformation.

The following code shows how the program responds to mouse clicks on PictureBox1:

   Private Sub PictureBox1_MouseClick(ByVal sender As Object, _    ByVal e As System.Windows.Forms.MouseEventArgs) _    Handles PictureBox1.MouseClick       ' Draw the point.       Using gr As Graphics = PictureBox1.CreateGraphics()           gr.FillEllipse(Brushes.Blue, e.X - 2, e.Y - 2, 5, 5)       End Using          ' Get the point in device coordinates.       Dim ptf() As PointF = {New PointF(e.X, e.Y)}          ' Translate into world coordinates.       m_DtoW1.TransformPoints(ptf)          ' See where the user clicked.       Dim dist As Single = ptf(0).X * ptf(0).X + ptf(0).Y * ptf(0).Y       Select Case dist           Case Is < 0.25 * 0.25               Me.Text = "Bulls eye!"           Case Is < 0.5 * 0.5               Me.Text = "50 points"           Case Is < 0.75 * 0.75               Me.Text = "20 points"           Case Is < 1 * 1               Me.Text = "10 points"           Case Else               Me.Text = "Miss"       End Select   End Sub

The code starts by marking the point that the user clicked. It then makes an array of PointF objects containing that point. It uses the inverse transformation matrix m_DtoW1 to transform the point from device coordinates back into world coordinates. Finally the code checks the point's distance from the origin to see how far the point is from the bull's-eye in world coordinates and displays an appropriate message in the form's title bar.

Transformations let you change the way the Graphics object's drawing methods work, translating, scaling, and rotating the results without changing the drawing code. That lets you make adjustments and reuse code by executing the same drawing code with different transformations. It lets you easily center a picture, possibly while enlarging it with or without stretching.

Most importantly, transformations let you draw in world coordinates that make sense for your application without worrying about how the final result will be mapped onto the device coordinates used by the screen. Inverse transformations let you translate device coordinates back into world coordinates if you need to let users interact with the drawing.

Taken together, these techniques add an entirely new element to your graphics programming that simple Pens and Brushes don't provide on their own.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: