Introduction
While working on a Button UserControl project, I had a requirement to write vertical text. I web-searched the usual suspects, but all I found out was how to draw a string rotated in its entirety either 90 or 270 degrees. What I wanted was to draw the string vertically but keep the characters upright. Having found nothing, I decided to do it myself, and wrote the VerticalText
class.
Problems encountered
My first attempt was reasonably successful, and not particularly difficult. However, the string was a little "spaced out" as the left-most string in this screenshot shows:
As you can see, the "i" 's may be forgiven for wondering if they are suffering from a personal hygiene problem, and the "s" at the bottom seems to be trying to escape to the next word...
For attempt number two, I've applied a TextSpread
value, discussed later, to reduce the height of each character. This is fine for the "s" and "i", but not so good for taller characters, or those with tails. When you have a tall character following a tailed character, such as the "gh", the problem is compounded, and the characters start to overlap.
To solve this problem, I developed a small routine which, depending on the character in question, would return a small offset, to be applied before or after the character was written. The results are the right-hand string, which to my eye at least looks about right.
The Code
The primary method in the VerticalText
class is Draw
. This method has three overloads, as follows. (The first two overloads ultimately call the last one.)
public void Draw(Graphics, string, Font, Brush, Rectangle);
public void Draw(Graphics, string, Font, Brush, Rectangle, StringFormat);
public void Draw(Graphics, string, Font, Brush, int, int);
The second overload calculates the text start co-ordinates, depending on the Alignment
and LineAlignment
properties of the StringFormat
.
public void Draw(Graphics g, string text, Font font, Brush brush,
Rectangle stringRect, StringFormat stringStrFmt)
{
int horOffset;
int vertOffset
switch (stringStrFmt.Alignment)
{
case StringAlignment.Center:
horOffset = (stringRect.Width / 2) - (int)(font.Size / 2) - 2;
break;
case StringAlignment.Far:
horOffset = (stringRect.Width - (int)font.Size - 2);
break;
default:
horOffset = 0;
break;
}
double textSize = this.Length(text, font);
switch (stringStrFmt.LineAlignment)
{
case StringAlignment.Center:
vertOffset = (stringRect.Height / 2) - (int)(textSize / 2);
break;
case StringAlignment.Far:
vertOffset = stringRect.Height - (int)textSize - 2;
break;
default:
vertOffset = 0;
break;
}
this.Draw(g, text, font, brush,
stringRect.X + horOffset,
stringRect.Y + vertOffset);
}
X
is determined by using the width of the rectangle, and the width of the font. Y
is based on the height of the rectangle, and the vertical length of the text string as calculated by the Length
method.
[Description("Length Method - returns vertical length of string")]
public int Length(string text, Font font)
{
char[] textChars = text.ToCharArray();
int len = new int();
for (int i = 0; i < text.Length; i++)
{
len += (int)(font.Height * textSpread);
len += ExtraSpaceAllowance(esaType.Either,
textChars[i], font);
}
len += 1;
return len;
}
The Length
method is public
, so it may be used by the calling code. This is useful for example when checking that the string you want to draw will actually fit where you want to draw it.
The third overload of the Draw
method, called by the first two, does the main job of drawing each individual character, starting at the position passed in x
and y
, by either the calling program or one of the previous two overloads. This snippet shows the important part:
Rectangle charRect = new Rectangle(x, y, (int)(font.Size * 1.5), font.Height);
for (int i = 0; i < text.Length; i++)
{
charRect.Offset(0, ExtraSpaceAllowance(esaType.Pre, textChars[i], font));
g.DrawString(textChars[i].ToString(),font,brush, charRect, charStrFmt);
charRect.Offset(0, (int)(font.Height * textSpread));
charRect.Offset(0, ExtraSpaceAllowance(esaType.Post, textChars[i],font));
}
A small rectangle is positioned at the supplied x and y co-ordinates, with a width and height based on the font. Another StringFormat
instance is used, this time to position each character centrally within the rectangle. We then loop through each character in the string, now represented as the textChars
Array
. Before and after each character is drawn, we call the ExtraSpaceAllowance
method.
If you've been paying attention, you'll remember that we bumped into ExtraSpaceAllowance
in the Length
method, although we weren't formally introduced. This method accepts an esaType
, a char
and a Font
as parameters. The esaType
tells us whether to process "tall" characters (esaType.Pre
), "tailed" characters (esaType.Post
), or either. Once we know what we're doing, we look for the passed character in the appropriate string of qualifying characters. If we find it, we return an int
giving the amount of extra pixels to add. This is set to one fifth of the height of the font - this figure was arrived at through trial and error, and it seems to work OK.
private int ExtraSpaceAllowance(esaType type, char ch, Font font)
{
if (textSpread >= 1) return 0;
int offset = 0;
if (type == esaType.Pre | type == esaType.Either)
{
if (" bdfhijkltABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".IndexOf(ch) > 0)
{
offset += (int)(font.Height * .2);
}
}
if (type == esaType.Post | type == esaType.Either)
{
if (" gjpqyQ".IndexOf(ch) > 0)
{
offset += (int)(font.Height * .2);
}
}
return offset;
}
The final important thing to mention is the textSpread
value, which can be seen above in the third overload of the Draw
method. This is a double
which is used to determine the spacing of the string. Ignoring the effect of ExtraSpaceAllowance
for now, if textSpread
is set to 1, then each character will be positioned below the previous one by the exact height of the font. If textSpread
is 0.5, then the characters' start positions will be half as far apart. The default for textSpread
is 0.75, and it is exposed as a public
property TextSpread
.
Using the code
I haven't included a demo app or a DLL in the downloads section - all the demo does is display the screenshot at the top of the article, and the amount of code here hardly seemed worth assembling it into a separate file. Just copy the source for VerticalString into your project, and by all means create a DLL if you feel the need.
Unresolved issues?
If you cast a critical eye over the strings drawn by this class, you'll notice that although the vertical spacing is pretty good, certain letters of the alphabet seem to have a mind of their own when it comes to horizontal alignment. Look at the "Test String" 's on the screenshot at the top of the article - most of the characters are lined up quite nicely in a straight vertical line, but one or two are a little out of kilter. For example, the lower-case "s" and "t" seem strangely drawn to the right and the left respectively, and when one is above the other the problem is particularly apparent.
Maybe someone would like to write a new method similar to ExtraSpaceAllowance
, to change the horizontal offset of charRect
for certain characters. I thought about it, but was daunted by the fact that initial testing revealed this tendency for certain characters to list to port or starboard to be inconsistent across fonts - you have been warned!