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: NotInheritable Class Matrix Inherits MarshalByRefObject Implements IDisposable ' Constructors Public Sub New(...) ' various overloads ' Properties Property Elements() As Single() Property IsIdentity() As Boolean Property IsInvertiable() As Boolean Property OffsetX() As Single Property OffsetY() As Single ' Methods Sub Invert() Sub Multiply(...) Sub Reset() Sub Rotate(...) Sub RotateAt(...) Sub Scale(...) Sub Shear(...) Sub TransformPoints(...) Sub TransformVectors(...) Sub Translate(...) Sub VectorTransformPoints(pts As Point()) End Class 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]
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. ScalingUsing 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 Dim mymatrix As Matrix = New Matrix() mymatrix.Scale(g.DpiX, g.Dpiy) Dim rulerFont As Font = New Font("MS Sans Serif", 8.25F / g.DpiY) Dim blackPen As Pen = New Pen(Color.Black, 0) Dim rulerFontHeight As Single = rulerFont.GetHeight(g) ' Inches ' Specify units in inches Dim rulerRect As RectangleF = New RectangleF(0.0F, 0.0F, _ 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. We'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 FontsThe 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: Dim mymatrix As Matrix = New Matrix() mymatrix.Scale(-1, -1) g.Transform = mymatrix g.DrawString("Scale(-1, -1)", Me.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. RotationScaling 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): Dim i As Integer For i = 0 to 90 Step 10 Dim mymatrix As Matrix = New Matrix() mymatrix.Rotate(i) g.Transform = mymatrix g.DrawLine(Pens.Black, 0, 0, 250, 0) g.DrawString(i.ToString(), Me.Font, Brushes.Black, _ textRect, format) Next 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): Dim i As Integer For i = 0 to 90 Step 10 Dim mymatrix As Matrix = New Matrix() mymatrix.Rotate(i, New PointF(25, 25)) g.Transform = mymatrix g.DrawLine(Pens.Black, 25, 25, 275, 25) g.DrawString(i.ToString(), Me.Font, _ Brushes.Black, textRect, format) Next Figure 6.5. Line from (25, 25) to (275, 25) Rotated by Degrees 0 “90 at (25, 25)
TranslationInstead 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, 150, 150) 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: Sub DrawLabeledRect(g As Graphics, label As String) ' Always draw at (0,0) and let the client ' set the position using a transform Dim rect As RectangleF = New RectangleF(0, 0, 150, 150) Dim format As StringFormat = 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, Me.Font, Brushes.Black, rect, format) End Sub Sub TranslationForm_Paint(sender As Object, e As PaintEventArgs) Dim g As Graphics = e.Graphics ' Origin at (0,0) DrawLabeledRect(g, "Translate(0, 0)") ' Move origin to (150, 150) Dim mymatrix As Matrix = New Matrix() mymatrix.Translate(150, 150) g.Transform = mymatrix DrawLabeledRect(g, "Translate(150, 150)") End Sub 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 . ShearingShearing 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:
Here's the code that results in the middle sheared rectangle and text in Figure 6.7: Dim rect As RectangleF = New RectangleF(0, 0, 200, 50) mymatrix = New Matrix() mymatrix.Translate(200, 0) mymatrix.Shear(0.5, 0) ' Shear in x dimension only g.Transform = mymatrix g.DrawString("Shear(.5,0)", Me.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 TransformsIn 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: Dim mymatrix As Matrix = New Matrix() mymatrix.Translate(10, 20) ' Move origin to (10, 20) mymatrix.Scale(2, 3) ' Scale x/width and y/width by 2 and 3 Dim mymatrix As Matrix = New Matrix() mymatrix.Scale(2, 3) ' Scale x/width and y/width by 2 and 3 mymatrix.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: Dim mymatrix As Matrix = New Matrix() ' Starts as identity mymatrix.Rotate(...) ' Touched by inhuman hands If Not(mymatrix.IsIdentity) Then mymatrix.Reset() ' Back to identity Transformation HelpersIf 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: Dim mymatrix As Matrix = New Matrix() mymatrix.Shear(.5, .5) g.Transform = mymatrix ' works g.Transform.Shear(.5, .5) ' 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 NotInheritable Class Graphics Inherits MarshalByRefObject Implements IDisposable ... Sub ResetTransform() Sub RotateTransform() Sub ScaleTransform() Sub TranslateTransform() End Class These methods are handy for simplifying transformation code (although you notice that there's no ShearTransform method): ' No new Matrix object required g.TranslateTransform(200, 0) g.DrawString("(0,0)", Me.Font, Brushes.Black, 0, 0) Path TransformationsAs 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: Function CreateLabeledRectPath(label As String) As GraphicsPath Dim path As GraphicsPath = New GraphicsPath() ... ' Add rectangle and string return path End Function Sub PathTranslationForm_Paint(sender As Object, e As PaintEventArgs) Dim g As Graphics = e.Graphics ' Draw at (0,0) g.DrawPath(Pens.Black, path) ' Translate all points in path by (150, 150) Dim mymatrix As Matrix = New Matrix() mymatrix.Translate(150, 150) path.Translate(mymatrix) g.DrawPath(Pens.Black, path) End Sub 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 identify 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: Dim widenPen As Pen = New Pen(Color.Empty, 10) path.Widen(widenPen) g.DrawPath(Pens.Black, path) widenPen.Dispose() 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: Dim destPoints(3) As PointF destPoints(0) = New PointF(CInt(width/2), 0) destPoints(1) = New PointF(width, height) destPoints(2) = New PointF(0, CInt(height/2)) Dim srcRect As RectangleF = New RectangleF(0, 0, width, CInt(height/2)) path.Warp(destPoints, srcRect) g.DrawPath(Pens.Black, path) |