[C# Helper]
Index Books FAQ Contact About Rod
[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

[C# 24-Hour Trainer]

[C# 5.0 Programmer's Reference]

[MCSD Certification Toolkit (Exam 70-483): Programming in C#]

Title: Draw "illuminated text" in C#


This example lets you draw "illuminated text" where each paragraph begins with an oversized letter.

In some illuminated manuscripts, the first letter of a paragraph is drawn in a larger font than the remaining text. This example does something similar. (Although in a real illuminated manuscript that initial character would probably be decorated with ornate images and scroll work. This example is more an exercise in formatting than a truly useful tool.)

The following code shows how the program controls the text drawing.

// The text to draw. private string[] paragraphs = { "Once there were three bats...", "The first bat comes home one night ...", ... }; // Draw the text. private void picWriting_Paint(object sender, PaintEventArgs e) { // Get the available space. const int paragraph_spacing = 10; const int margin = 5; RectangleF rect = new RectangleF(margin, margin, picWriting.ClientSize.Width - 2 * margin, picWriting.ClientSize.Height - 2 * margin); // Make the fonts. using (Font lead_font = new Font("Times New Roman", 30, FontStyle.Bold)) { using (Font body_font = new Font("Times New Roman", 12)) { // Draw the text. foreach (string paragraph in paragraphs) DrawIlluminatedText(e.Graphics, 50, 55, lead_font, Brushes.Green, Pens.Green, body_font, Brushes.Black, ref rect, paragraph_spacing, paragraph); } } }

The program first defines an array of strings to hold the text's paragraphs. When the picWriting PictureBox receives a Paint event, its event handler creates a RectangleF representing the area in which the text should be drawn. It then creates fonts to use for each paragraph's lead character and for the body of the paragraphs. The event handler then loops through the paragraphs and calls the following DrawIlluminatedText method for each.

// Draw an illuminated paragraph. private void DrawIlluminatedText(Graphics gr, float min_lead_width, float min_lead_height, Font lead_font, Brush lead_brush, Pen lead_pen, Font body_font, Brush body_brush, ref RectangleF rect, int paragraph_spacing, string paragraph) { // Get the lead character. string ch = paragraph.Substring(0, 1); paragraph = paragraph.Substring(1); // Size the lead character. SizeF lead_size = gr.MeasureString(ch, lead_font); if (lead_size.Width < min_lead_width) lead_size.Width = min_lead_width; if (lead_size.Height < min_lead_height) lead_size.Height = min_lead_height; // Make a StringFormat to align and trim the text. using (StringFormat string_format = new StringFormat()) { // Stop drawing each line at a word boundary. string_format.Trimming = StringTrimming.Word; // See how much space is available for the side text. SizeF side_size = new SizeF( rect.Width - lead_size.Width, lead_size.Height); // See how much side text will fit // allowing a partial line at the end. int chars_fitted, lines_filled; side_size = gr.MeasureString(paragraph, body_font, side_size, string_format, out chars_fitted, out lines_filled); // Get the side text. string side_text = paragraph.Substring(0, chars_fitted); paragraph = paragraph.Substring(chars_fitted); // Draw only complete lines. string_format.FormatFlags = StringFormatFlags.LineLimit; // See how much space the side text needs. side_size.Height += 1000; side_size = gr.MeasureString(side_text, body_font, side_size, string_format, out chars_fitted, out lines_filled); if (side_size.Height < min_lead_height) side_size.Height = min_lead_height; // Use at least that much height for the lead character. if (lead_size.Height < side_size.Height) lead_size.Height = side_size.Height; // Illuminate the lead character. gr.DrawRectangle(lead_pen, rect.X, rect.Y, lead_size.Width, lead_size.Height); // Draw the lead character. RectangleF lead_rect = new RectangleF( rect.X, rect.Y, lead_size.Width, lead_size.Height); string_format.Alignment = StringAlignment.Center; string_format.LineAlignment = StringAlignment.Center; gr.DrawString(ch, lead_font, lead_brush, lead_rect, string_format); string_format.Alignment = StringAlignment.Near; string_format.LineAlignment = StringAlignment.Near; // Get the area available for the side text. RectangleF side_rect = new RectangleF( rect.X + lead_size.Width, rect.Y, side_size.Width, side_size.Height); // Draw the side text. gr.DrawString(side_text, body_font, body_brush, side_rect, string_format); // Remove the space used by the side text. rect.Y += lead_size.Height; rect.Height -= lead_size.Height; // Draw the rest of the paragraph. gr.DrawString(paragraph, body_font, body_brush, rect, string_format); // See how much space that used. SizeF rect_size = new SizeF(rect.Width, rect.Height); SizeF size = gr.MeasureString(paragraph, body_font, rect_size); // Remove the used space. rect.Y += size.Height + paragraph_spacing; rect.Height -= size.Height + paragraph_spacing; } }

This method takes as parameters:

  • The Graphics object on which to draw
  • The minimum width and height that should be used for the lead character
  • The font and brush that should be used for the lead character
  • The pen that should be used to draw a box around the lead character
  • The font and brush that should be used for the remaining text
  • A RectangleF where the paragraph should be drawn
  • Spacing to add after the paragraph
  • The paragraph's text

The code starts by pulling the initial character off of the paragraph. It uses the Graphics object's MeasureString method to measure the character and sets the lead_size structure to hold the larger of the character's size and the minimum required size.

Next, the code creates a StringFormat object to use while drawing text. It sets the object's Trimming property to Word so any text is clipped to the nearest word. That prevents the Drawstring method from drawing part of a word at the end of a line of text.

The code then calculates the amount of space available to the right of the initial character. It uses MeasureString again to see how much of the paragraph's remaining text will fit in that area. The method then extracts the text that will fit in that area.

Next, the code determines how much space is actually required by the text to the right of the initial character. (For example, suppose that area is 100 x 50 pixels and can hold 120 characters. Those characters might actually only use 100 x 45 pixels. This step measures the space actually needed so we don't waste the 5 extra pixels.)

The results look better if the initial character's area has the same height as the text to its right. If that area's height is currently less than the height of the text to the right, the code increases it to match.

Finally, the code starts producing output. It first draws a rectangle around the initial character's area. It then draws the initial character centered in that area.

The method then draws the text to the right of the initial character and removes the space used by that text from the RectangleF representing the area in which all of the text must fit.

Finally, the method draws any remaining text and removes the area used by that text from the total space available. That RectangleF is passed into the method by reference, so the change returns to the calling code and the next paragraph is drawn in whatever space remains.

You can modify the code if you like to omit the rectangle around the initial character, fill that rectangle with color, display an image in that rectangle, or produce other effects. My next post will show how to make those sorts of changes flexibly.

Download the example to experiment with it and to see additional details.

© 2009-2023 Rocky Mountain Computer Consulting, Inc. All rights reserved.