I’ve always had a fascination with graphics applications that allow you to twist and manipulate text and images—not so much from the end-user perspective, but more from the angle of a programmer who’s always seen this type of programming as near guru level. To that end, my previous few articles have focused on the GDI+ engine, which essentially makes many tasks such as rendering 3D text more accessible to those of us who don’t have either the time or the inclination to become gurus in the world of graphics programming.
This article brings to a close this mini-series on text-manipulation via GDI+ by illustrating how to use the TranslateTransform and ScaleTransform methods of the Graphics class to draw reflected text.
Steps for Drawing Reflected Text
First, implement a handler for the PictureBox control’s Paint method. Whenever you need to display new text, simply call the PictureBox control’s Invalidate method, which in turn calls its paint method. The following steps and code refer to work within the paint method.
Note: The first few steps are the same as those in previous articles, as they pertain to tasks that are common to any text drawing. These tasks include obtaining a presentation space to draw on, instantiating a font object, measuring the text to display, and so on. I describe these steps here again as I don’t want assume that the reader has read all of the articles in this series. If you have read those articles, simply skip to Step #5, where the commonality between most of the examples ends.
- Obtain the Graphics object for the picture control.
You could acquire this object via the PictureBox object’s CreateGraphics method. However, that Graphics object would be volatile in that it would be collected by the GC (Garbage Collector) when the object goes out of scope. Therefore, you need to use the Graphics object that is passed to the Paint method (via the PaintEventArgs::Graphics member):
Graphics* g = e->Graphics;
- Instantiate the Font object based on the user-supplied font size.
This application uses hard-coded values for the font typeface (Times New Roman) as well as the font size (40) and style (regular):
System::Drawing::Font* font = new System::Drawing::Font("Times New Roman", 40, FontStyle::Regular);
- Obtain the size of the text to be rendered.
As discussed in a previous article, the Graphics::MeasureString method is a convenient method for measuring a string, given the presentation space in which you will draw it and the font that you will use:
SizeF textSize = g->MeasureString(textToDisplay, font);
- Clear the PictureBox Box control.
Initialize the PictureBox control using the PictureBox::Clear method and specifying the desired color. Here, I use a hard-coded value of Color::White simply because it’s easier to view the demo’s black and gray text on a white background:
- Calcualte where the text will be rendered on the PictureBox control.
- Prepend the translation to the transformation matrix of the PictureBox control.
Shortly, you’ll need to scale the Graphics so that the reflected text is drawn inverted. However, scaling affects the entire graphics object, not just the text you wish to render. Therefore, you need to reposition the origin of the Graphics object (normally based at 0,0) to the x,y location where you wish to draw the text:
- Calculate the true height of the text being rendered.
You need the reflected text to be drawn starting below the original text. Although you got the text’s height from the Measurestring method, that value includes spacing for descenders and whitespace. In this case, you want only the height from the baseline. This involves getting the cell ascent of the font family being used. (You can retrieve the font family from the Font object via it’s FontFamily property.) However, the cell ascent value is in design units, which must be converted to pixels and then scaled for the font size. After much testing and research, this is the best formula I’ve found for doing this, where the final value (represented by cy) is the height of the text to be drawn).
This formula is optimized for characters that are drawn above the baseline. If you use characters that are drawn below the baseline (i.e., characters with descents), you’ll see overlap in the descending parts of the characters between the original and reflected text. To remedy this, tweak the calculation by incorporating the cell descent (retrieved via the FontFamily::GetCellDescent method):
int lineAscent = font->FontFamily->GetCellAscent(font->Style); int lineSpacing = font->FontFamily->GetLineSpacing(font->Style); Single lineHeight = font->GetHeight(g); Single cy = lineHeight * lineAscent / lineSpacing;
- Draw the original text.
Note that the x and y positions are at 0 and 0. This is due to your having called TranslateTransform as described earlier:
g->DrawString(textToDisplay, font, Brushes::Black, 0, 0);
- Apply the necessary scaling factor to the graphic object’s transformation matrix.
To reflect text, you use the ScaleTransform method with a value of -1 in order to invert the text without distorting it:
- Draw the reflected text.
Once the scaling factor has been established, you simply draw the reflected text. Note the y value is set to the negative of the text height multiplied by two so that it displays below the original text:
g->DrawString(textToDisplay, font, Brushes::Gray, 0, -(cy*2));
Here, you need to determine the x and y coordinates of the text you will draw. I center the text horizontally, so that’s pretty self-explanatory. However, to ensure that the totality of both the original and reflected strings (and not just the first string) is centered, vertically I need to multiply the text’s height by 1.5:
Single x = (picText->Width - textSize.Width) / 2; Single y = (picText->Height - (textSize.Height*1.5)) /2;
Saving and Restoring the GraphicsState
The previous section showed how to use the ScaleTransform method to produce reflected text. In terms of the order in which the text was drawn, the code first drew the original string, scaled the text in the exact opposite direction (in the y direction), and then drew the text’s “reflection”. However, what if you needed to perform these steps in reverse? In other words, draw the reflection first. You could scale the text twice—once to draw the reflection and then back to the normal scaling. Or you could save the current state of the Graphics object, set the scaling factor, and then when finished drawing on that scale, restore the original graphics state to draw the normal text. Here’s how that would look:
// Save the graphics state GraphicsState* prevGraphicsState = g->Save(); // Scale to draw reflected text g->ScaleTransform(1.0F, -1.0F); // Draw the reflected text // Restore the saved graphics state g->Restore(prevGraphicsState); // Draw the normal text
Download the Code
To download the accompanying source code for the demo, click here.
About the Author
The founder of the Archer Consulting Group (ACG), Tom Archer has been the project lead on three award-winning applications and is a best-selling author of 10 programming books as well as countless magazine and online articles.