Transforms


Page units are useful for specifying things conveniently and letting the Graphics object sort it out, but there are all kinds of effects that can't be achieved with such a simple transform. A transform is a mathematical function by which units are specified and then transformed into other units. So far, we've talked about transforming from page units to device units, but a more general-purpose transformation facility is provided via the Transform property of the Graphics object, which is an instance of the Matrix class from the System.Drawing.Drawing2D namespace:

 sealed class Matrix : MarshalByRefObject, IDisposable {   // Constructors   public Matrix(...); // various overloads   // Properties   public float[] Elements { get; }   public bool IsIdentity { get; }   public bool IsInvertible { get; }   public float OffsetX { get; }   public float OffsetY { get; }   // Methods   public void Invert();   public void Multiply(...);   public void Reset();   public void Rotate(...);   public void RotateAt(...);   public void Scale(...);   public void Shear(...);   public void TransformPoints(...);   public void TransformVectors(...);   public void Translate(...);   public void VectorTransformPoints(Point[] pts); } 

The Matrix class provides an implementation of a 3x3 mathematical matrix , which is a rectangular array of numbers . The specifics of what make up a matrix in math are beyond the scope of this book, but the Matrix class provides all kinds of interesting methods that let you use a matrix without doing any of the math. [3]

[3] Of course, as with all technology, understanding the underlying principles is always helpful. Martin Heller, my series editor, recommends Introduction to Computer Graphics , by James D. Foley, Andries Van Dam, and Steven K. Feiner (Addison-Wesley, 1993), for the details of matrix math as related to graphics programming.

The graphics transformation matrix is used to transform world coordinates , which is what units involved with graphics operations are really specified in. Graphical units are passed as world coordinates, transformed by the transformation matrix into page units, and finally transformed again from page units to display units. As you've seen, the default page units for the screen are pixels, and that's why no page unit conversion happens without our changing the page unit or the scale or both. Similarly, by default, the transformation matrix is the identity matrix , which means that it doesn't actually do any conversions.

Scaling

Using an instance of a Matrix object instead of page units, we could perform the simple scaling we did in the preceding example:

  // Set units to inches using a transform   Matrix matrix = new Matrix();   matrix.Scale(g.DpiX, g.DpiY);   g.Transform = matrix;  using( Font rulerFont = new Font("MS Sans Serif",  8.25f / g.DpiY  ) ) using( Pen blackPen = new Pen(Color.Black, 0) ) {   float rulerFontHeight = rulerFont.GetHeight(g); // Inches   // Specify units in inches   RectangleF rulerRect =     new RectangleF(     0, 0,     6.5f, rulerFontHeight * 1.5f);   // Draw in inches   g.DrawRectangle(     blackPen,     rulerRect.X, rulerRect.Y,     rulerRect.Width, rulerRect.Height);   ... } 

This code creates a new instance of the Matrix class, which defaults to the identity matrix. Instead of directly manipulating the underlying 3x3 matrix numbers, the code uses the Scale method to put the numbers in the right place to scale from inches to pixels using the dpi settings for the current Graphics object. This transformation is exactly the same result that we got by setting the page unit to inches and the page scale to 1, except for one detail: the font. Although the page unit and scale do not affect the size of fonts, the current transform affects everything, including fonts. This is why the point size being passed to the Font's constructor in the sample code is first scaled back by the current dpi setting, causing it to come out right after the transformation has occurred. I'd show you the result of using the transform instead of page units, but because it looks just like Figure 6.1, it'd be pretty boring.

Scaling Fonts

The fact that the world transform works with fonts as well as everything else makes scaling fonts an interesting use of the world transform all by itself. Usually, fonts are specified by height only, but using a transforms allows a font's height and width to be adjusted independently of each other, as shown in Figure 6.2.

Figure 6.2. Scaling Font Height Independently of Font Width

Notice that scaling can even be used in the negative direction, as shown on the far right of Figure 6.2, although you'll want to make sure you specify the rectangle appropriately:

 Matrix matrix = new Matrix();  matrix.Scale(-1, -1);  g.Transform = matrix; g.DrawString("Scale(-1, -1)", this.Font, Brushes.Black,  new RectangleF(-x - width, -y - height, width, height)  , format); 

Because scaling by “1 in both dimensions causes all coordinates to be multiplied by “1, to get a rectangle at the appropriate place in the window requires negative coordinates. Notice that the width and height are still positive, however, because a rectangle needs positive dimensions to have positive area.

Rotation

Scaling by a negative amount can look very much like rotation, but only in a limited way. Luckily, matrices support rotation directly, as in this code sample, which draws a line rotated along a number of degrees (as shown in Figure 6.3):

 for( int i = 0; i <= 90; i += 10 ) {   Matrix matrix = new Matrix();  matrix.Rotate(i);  g.Transform = matrix;  g.DrawLine(Pens.Black, 0, 0, 250, 0);  g.DrawString(     i.ToString(), this.Font, Brushes.Black, textRect, format); } 
Figure 6.3. Line from (0, 0) to (250, 0) Rotated by Degrees 0 “90

Notice that rotation takes place starting to the right horizontally and proceeding clockwise. Both shapes and text are rotated, as would anything else drawn into the rotated graphics object.

Rotate works well if you're rotating around graphical elements with origins at (0, 0), but if you're drawing multiple lines originating at a different origin, the results may prove unintuitive (although mathematically sound), as shown in Figure 6.4.

Figure 6.4. Line from (25, 25) to (275, 25) Rotated by Degrees 0 “90

To rotate more intuitively around a point other than (0, 0), use the RotateAt method (as shown in Figure 6.5):

 for( int i = 0; i <= 90; i += 10 ) {   Matrix matrix = new Matrix();  matrix.RotateAt(i, new PointF(25, 25));  g.Transform = matrix;   g.DrawLine(Pens.Black, 25, 25, 275, 25);   g.DrawString(     i.ToString(), this.Font, Brushes.Black, textRect, format); } 
Figure 6.5. Line from (25, 25) to (275, 25) Rotated by Degrees 0 “90 at (25, 25)

Translation

Instead of moving our shapes relative to the origin, as we did when drawing the lines, it's often handy to move the origin itself by translating the matrix (as demonstrated in Figure 6.6).

Figure 6.6. Rectangle(0, 0, 125, 125) Drawn at Two Origins

Translation is very handy when you've got a figure to draw that can take on several positions around the display area. You can always draw starting from the origin and let the translation decide where the figure actually ends up:

 void DrawLabeledRect(Graphics g, string label) {  // Always draw at (0, 0) and let the client   // set the position using a transform   RectangleF rect = new RectangleF(0, 0, 125, 125);  StringFormat format = new StringFormat();   format.Alignment = StringAlignment.Center;   format.LineAlignment = StringAlignment.Center;   g.DrawRectangle(Pens.Black, rect.X, rect.Y, rect.Width, rect.Height);   g.DrawString(label, this.Font, Brushes.Black, rect, format); } void TranslationForm_Paint(object sender, PaintEventArgs e) {   Graphics g = e.Graphics;  // Origin at (0, 0)   DrawLabeledRect(g, "Translate(0, 0)");   // Move origin to (150, 150)   Matrix matrix = new Matrix();   matrix.Translate(150, 150);   g.Transform = matrix;   DrawLabeledRect(g, "Translate(150, 150)");  } 

In fact, this technique can be used for any of the matrix transformation effects covered so far, in addition to the one yet to be covered: shearing .

Shearing

Shearing is like drawing on a rectangle and then pulling along an edge while holding the opposite edge down. Shearing can happen in both directions independently. A shear of zero represents no shear, and the "pull" is increased as the shear increases . The shear is the proportion of the opposite dimension from one corner to another. For example, the rectangle (0, 0, 200, 50) sheared 0.5 along the x dimension will have its top-left edge at (0, 0) but its bottom-left edge at (25, 50). Because the shear dimension is x, the top edge follows the coordinates of the rectangle, but the bottom edge is offset by the height of the rectangle multiplied by the shear value:

bottomLeftX = height * xShear = 50 * 0.5 = 25

Here's the code that results in the middle sheared rectangle and text in Figure 6.7:

 RectangleF rect = new RectangleF(0, 0, 200, 50); matrix = new Matrix();  matrix.Translate(200, 0);   matrix.Shear(.5f, 0f); // Shear in x dimension only  g.Transform = matrix; g.DrawString("Shear(.5, 0)", this.Font, Brushes.Black, rect, format); g.DrawRectangle(Pens.Black, rect.X, rect.Y, rect.Width, rect.Height); 
Figure 6.7. Drawing a Constant-Size Rectangle at Various Shearing Values

Combining Transforms

In addition to a demonstration of shearing, the preceding code snippet offers another interesting thing to notice: the use of two operations ”a translation and a shear ”on the matrix. Multiple operations on a matrix are cumulative. This is useful because the translation allows you to draw the sheared rectangle in the middle at a translated (0, 0) without stepping on the rectangle at the right (and the rectangle at the right is further translated out of the way of the rectangle in the middle).

It's a common desire to combine effects in a matrix, but be careful, because order matters. In this case, because translation works on coordinates and shear works on sizes, the two operations can come in any order. However, because scaling works on coordinates as well as sizes, the order in which scaling and translation are performed matters very much:

 Matrix matrix = new Matrix(); matrix.Translate(10, 20); // Move origin to (10, 20) matrix.Scale(2, 3); // Scale x/width and y/width by 2 and 3 Matrix matrix = new Matrix(); matrix.Scale(2, 3); // Scale x/width and y/width by 2 and 3 matrix.Translate(10, 20); // Move origin to (20, 60) 

If you find that you'd like to reuse a Matrix object but don't want to undo all the operations you've done so far, you can use the Reset method to set it back to the identity matrix. Similarly, you can check whether it's already the identity matrix:

 Matrix matrix = new Matrix(); // Starts as identity matrix.Rotate(...); // Touched by inhuman hands  if( !matrix.IsIdentity ) matrix.Reset(); // Back to identity  

Transformation Helpers

If you've been following along with this section on transformations, you may have been tempted to reach into the Graphics object's Transform property and call Matrix methods directly:

 Matrix matrix = new Matrix(); matrix.Shear(.5f, .5f);  g.Transform = matrix; // works   g.Transform.Shear(.5f, .5f); // compiles, but doesn't work  

Although the Transform property will return its Matrix object, it's returning a copy, so performing operations on the copy will have no effect on the transformation matrix of the graphics object. However, instead of creating Matrix objects and setting the Transform property all the time, you can use several helper methods of the Graphics class that affect the transformation matrix directly:

 // Transformation methods of the Graphics class sealed class Graphics : MarshalByRefObject, IDisposable {   ...  public void ResetTransform();   public void RotateTransform(...);   public void ScaleTransform(...);   public void TranslateTransform(...);  } 

These methods are handy for simplifying transformation code (although you'll notice that there's no ShearTransform method):

  // No new Matrix object required   g.TranslateTransform(200, 0);  g.DrawString("(0, 0)", this.Font, Brushes.Black, 0, 0); 

Path Transformations

As you've seen in previous chapters, GraphicsPath objects are very similar to Graphics objects, and the similarity extends to transformations. A GraphicsPath object can be transformed just as a Graphics object can, and that's handy when you'd like some parts of a drawing, as specified in paths, to be transformed but not others.

Because a path is a collection of figures to be drawn as a group , a transformation isn't a property to be set and changed; instead, it is an operation that is applied. To transform a GraphicsPath, you use the Transform method:

 GraphicsPath CreateLabeledRectPath(string label) {   GraphicsPath path = new GraphicsPath();   ... // Add rectangle and string   return path; } void PathTranslationForm_Paint(object sender, PaintEventArgs e) {   Graphics g = e.Graphics;   using( GraphicsPath path = CreateLabeledRectPath("My Path") ) {     // Draw at (0, 0)     g.DrawPath(Pens.Black, path);     // Translate all points in path by (150, 150)     Matrix matrix = new Matrix();     matrix.Translate(150, 150);  path.Transform(matrix);  g.DrawPath(Pens.Black, path);   } } 

In addition, GraphicsPath provides transformations that do flattening, widening, and warping via the Flatten, Widen, and Warp methods, respectively (as shown in Figure 6.8).

Figure 6.8. Path Flattening, Widening, and Warping

Each of these methods takes a Matrix object in case you'd like to, for example, translate and widen at the same time. Passing the identity matrix allows each of the specific operations to happen without an additional transformation. The Flatten method takes a flatness value; the larger the value, the fewer the number of points used along a curve, and therefore the more "flat." Figure 6.8 shows an ellipse flattened by 10:

 // Pass the identity matrix as the first argument to // stop any transformation except for the flattening  path.Flatten(new Matrix(), 10);  g.DrawPath(Pens.Black, path); 

The Widen method takes a Pen whose width is used to widen the lines and curves along the path. Figure 6.8 shows an ellipse widened by a pen of width 10:

  using( Pen widenPen = new Pen(Color.Empty /* ignored */, 10) ) {   path.Widen(widenPen);  g.DrawPath(Pens.Black, path);  }  

One of the overloads of the Widen method takes a flatness value, in case you'd like to widen and flatten simultaneously , in addition to the matrix that it also takes for translation.

The Warp method acts very like the skewing of an image discussed in Chapter 4: Drawing Basics. Warp takes, at a minimum, a set of points that define a parallelogram that describes the target, and a rectangle that describes a chunk of the source. It uses these arguments to skew the source chunk to the destination parallelogram. Figure 6.8 shows the top half of an ellipse skewed left:

  PointF[] destPoints = new PointF[3];   destPoints[0] = new PointF(width/2, 0);   destPoints[1] = new PointF(width, height);   destPoints[2] = new PointF(0, height/2);   RectangleF srcRect = new RectangleF(0, 0, width, height/2);   path.Warp(destPoints, srcRect);  g.DrawPath(Pens.Black, path); 


Windows Forms Programming in C#
Windows Forms Programming in C#
ISBN: 0321116208
EAN: 2147483647
Year: 2003
Pages: 136
Authors: Chris Sells

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net