Title: Easily render rotated text in a WPF program using C#
The example Render text easily in a WPF program using C# shows how to make an extension method that makes rendering text easy in WPF. This example makes a similar extension method that helps you render rotated text.
This is a fairly confusing extension method, so I'm going to show the code in pieces.
Most of the extension method's parameters are straightforward, but I want to explain the last three before you read the code. The following code fragment shows the method's declaration with the last three parameters highlighted in blue.
// Draw rotated text at the indicated location.
public static void DrawRotatedString(
this DrawingContext drawing_context,
string text, double angle, string font_name,
double em_size, Brush brush,
Point origin, TextAlignment text_align,
VertAlignment valign, TextAlignment halign)
{
...
}
The text_align parameter determines how the text is aligned. It can take the values Left, Center, and Right. (The TextAlignment enumeration also provides the Justify value, but WPF seems to treat it like Left, so this example does, too.) The picture on the right shows the results produced by the different alignments. If you look at the picture at the top of the post, you'll see that Center alignment is selected, so those values are centered inside their red bounding boxes. (This is most obvious with the TOP CENTER entry because "TOP" and "CENTER" have such different lengths.)
The extension method's last two parameters tell how the result should be aligned with respect to the text's point of origin. The dashed blue rectangles in the picture at the top of this post show the bounding boxes needed to hold the rotated text. The method positions the text so the blue bounding boxes are appropriately aligned with respect to the text origin. For example, the TOP LEFT text is aligned so its bounding box is aligned to the text origin on its top and left.
The following code shows how the method gets ready to draw text.
Typeface typeface = new Typeface(font_name);
FormattedText formatted_text = new FormattedText(
text, CultureInfo.CurrentUICulture,
FlowDirection.LeftToRight,
typeface, em_size, brush);
formatted_text.TextAlignment = text_align;
This code creates the necessary Typeface and FormattedText objects. It sets the FormattedText object's TextAlignment property to align the text properly.
The basic idea of the code that follows is to draw the text at the origin (0, 0) and then apply transformations to make the text appear rotated and in the desired location. Transformations allow you to make the drawing system move, rotate, scale, and skew drawing components as you draw them.
This method uses transformations to: (1) translate the result to center the text at the origin (0, 0), (2) rotate the text, and finally (3) translate the text to its final destination.
The next piece of code creates the first transformation, which moves the text so it is centered at (0, 0).
// Make a transformation to center the text.
double width = formatted_text.Width -
formatted_text.OverhangLeading;
double height = formatted_text.Height;
TranslateTransform translate1 = new TranslateTransform();
translate1.Y = -height / 2;
if ((text_align == TextAlignment.Left) ||
(text_align == TextAlignment.Justify))
translate1.X = -width / 2;
else if (text_align == TextAlignment.Right)
translate1.X = width / 2;
else translate1.X = 0;
This code first calculates the text's width and height. The height is given by the FormattedText object's Height property. The width is given by formatted_text.Width - formatted_text.OverhangLeading. (I'll explain this further in my next post.)
The code then creates a TranslateTransform object and sets its Y property to -height / 2. That translates in the negative Y direction by half of the text's height to center the text vertically.
Next the code sets the transformation's X property to center the text horizontally. If the text is aligned on the left (or justified), then when it is drawn at (0, 0), the text appears to the right of that point. In this case, the code sets the transformation's X property to -width / 2 to move the text left and center it horizontally.
If the text is aligned on the right, then when it is drawn at (0, 0), the text appears to the left of that point. In that case, the code sets the transformation's X property to width / 2 to move the text right and center it horizontally.
Finally if the text is centered, the code sets the transformation's X property to 0 so it remains centered at (0, 0).
The next piece of code creates the second transformation, which rotates the text.
// Make a transformation to rotate the text.
RotateTransform rotate = new RotateTransform(angle);
This one is easy. The code just makes a RotateTransform object, passing its constructor the angle by which the code should be rotated.
The next piece of code creates the third transformation, which moves the text to its final location.
// Get the text's bounding rectangle.
Rect rect = new Rect(0, 0, width, height);
if (text_align == TextAlignment.Center) rect.X -= width / 2;
else if (text_align == TextAlignment.Right) rect.X -= width;
// Get the rotated bounding rectangle.
Rect rotated_rect = rotate.TransformBounds(rect);
// Make a transformation to center the
// bounding rectangle at the destination.
TranslateTransform translate2 =
new TranslateTransform(origin.X, origin.Y);
// Adjust the translation for the desired alignment.
if (halign == TextAlignment.Left)
translate2.X += rotated_rect.Width / 2;
else if (halign == TextAlignment.Right)
translate2.X -= rotated_rect.Width / 2;
if (valign == VertAlignment.Top)
translate2.Y += rotated_rect.Height / 2;
else if (valign == VertAlignment.Bottom)
translate2.Y -= rotated_rect.Height / 2;
To correctly position the text, the code needs to know how big the text is after it has been rotated. (See the dashed blue rectangles in the picture at the top of the post.) To figure that out, the code creates a Rect that represents the text's original (untransformed) drawing area. Depending on the text's alignment, the code adjusts the Rect's X property to center the rectangle horizontally. This makes the rectangle represent the area where the text will be drawn.
Next the code uses the rotation transformation's RotateBounds method to find a new rectangle that represents the bounds of the text's rotated bounding rectangle. (It has the dimensions of the dashed blue rectangles in the picture at the top of the post.)
The code then creates a TranslateTransform that moves a point at the origin to the desired destination point (origin.X, origin.Y). If you left things as they are at this point, the text and its red bounding box would be rotated and translated so it was centered at (origin.X, origin.Y). The code adjusts the final TranslateTransform to align the text properly with respect to that point.
The rest of the method is a lot easier to understand. The following code shows how the method applies the three transformations.
// Push transformations in reverse order. (Thanks Microsoft!)
drawing_context.PushTransform(translate2);
drawing_context.PushTransform(rotate);
drawing_context.PushTransform(translate1);
This code uses the DrawingContext's PushTransform method to add the transformations to its drawing pipeline. For some reason, Microsoft requires you add the transformations to the DrawingContext in reverse order, so this example adds the final translation first, the rotation second, and the initial translation last. (Microsoft drawing methods have required you to pass transformations in reverse order for many years. I've never understood why.)
After the transformations are in place, anything you draw will be transformed by them. The following code shows how the method draws the text and its red bounding box.
// Draw.
drawing_context.DrawText(formatted_text, new Point(0, 0));
// Draw a rectangle around the text. (For debugging.)
drawing_context.DrawRectangle(null,
new Pen(Brushes.Red, 1), rect);
// Remove the transformations.
drawing_context.Pop();
drawing_context.Pop();
drawing_context.Pop();
First the code calls DrawText to draw the text. It then calls DrawRectangle to draw the red bounding box. Both of those operations are translated, rotated, and translated again as desired.
Next the code calls the DrawingContext's Pop method to remove the three transformations. If you don't do that, the next time you call the extension method, the new transformations are added to the previous ones and you get some really weird and hard-to-debug results.
That's it for the important drawing code. The following code shows the last piece of the method, which draws the dashed blue bounding rectangle.
// Draw the rotated bounding rectangle. (For debugging.)
Rect transformed_rect =
translate2.TransformBounds(
rotate.TransformBounds(
translate1.TransformBounds(rect)));
Pen custom_pen = new Pen(Brushes.Blue, 1);
custom_pen.DashStyle = new DashStyle(
new double[] { 5, 5 }, 0);
drawing_context.DrawRectangle(null, custom_pen,
transformed_rect);
This method simply applies the TransfromBounds methods of the three transformations to the text's original bounding rectangle rect. The first translation returns the bounding rectangle centered at the origin. The rotation's TransformBounds method returns a rectangle big enough to hold the rotated centered rectangle. The final translation just moves the resized rectangle so it has the appropriate alignment with the text's drawing origin. The result is one of the blue dashed boxes shown in the picture at the top of this post.
Download the example program to see the complete method all in one piece.
This method's code is fairly confusing, but the method itself is quite easy to use. The following code shows how the example's main program draws the TOP LEFT sample.
point = new Point(x, y);
drawingContext.DrawEllipse(brush, pen, point, 3, 3);
drawingContext.DrawRotatedString("TOP\nLEFT", angle,
font_name, em_size,
Brushes.Black, point, text_align,
VertAlignment.Top, TextAlignment.Left);
The code defines the text's point of origin. It then calls the DrawingContext's DrawEllipse method to mark that point on the window.
Next the code calls the DrawRotatedString extension method. Values such as brush, pen, font_name, and the others are set earlier in the program. They're straightforward so they aren't shown here.
Download the example to experiment with it and to see additional details.
|