Line By Line Text Graphics

In the good old days of line printers and 80x24 consoles, generating line-by-line text displays was easy. One just opened stdout, /dev/lp, LPT1:, /dev/console, or CON: and started writing characters. The device took care of everything.

With graphical windowing systems, it is not as easy. A programmer must use graphics commands to draw text strings at specific window/screen locations, or use widgets designed for that purpose. Even when widgets are available for onscreen display, the programmer must still resort to low-level graphics commands to generate hardcopy output.

When writing code to draw text output, some programmers will end up with code like this:

 // Display user information in two columns
 void DisplayUserInfo(User user, Graphics g, Font font, Rectangle bounds)
 {
     Brush brush = System.Drawing.Brushes.Black;
     int columnOffset = bounds.Left + (bounds.Width / 2);
     g.DrawString("User Name:", font, brush, origin);
     g.DrawString(user.Name, font, brush, origin.X + columnOffset, origin.Y);
     g.DrawString("Full Name:", font, brush, origin.X, origin.Y + font.Height);
     g.DrawString(user.FullName, font, brush, origin.X + columnOffset, origin.Y + font.Height);
     g.DrawString("E-mail:", font, brush, origin.X, origin.Y + 2 * font.Height);
     g.DrawString(user.EmailAddress, font, brush, origin.X + columnOffset, origin.Y + 2 * font.Height);
     g.DrawString("Phone:", font, brush, origin.X, origin.Y + 3 * font.Height);
     g.DrawString(user.PhoneNumber, font, brush, origin.X + columnOffset, origin.Y + 3 * font.Height);
 }
Such code is difficult to read and hard to maintain.

A useful technique is to define a line-printer-like class that keeps track of the current position within a boundary rectangle and which provides a WriteLine() method that makes it easy to generate line-by-line output. Here is a simple example:

    public class LineDisplay
    {
        private Graphics graphics;
        private Rectangle bounds;
        private Brush brush;
        private Font font;
        private PointF position;

public LineDisplay(Graphics graphics, Font font, Rectangle bounds) { this.graphics = graphics; this.bounds = bounds; this.brush = System.Drawing.Brushes.Black; this.font = font; this.position = new PointF(bounds.Left, bounds.Top); }

public void WriteLine(string text) { this.graphics.DrawString(text, this.font, this.brush, this.position); this.NewLine(); }

public void NewLine() { this.position.X = this.bounds.Left; this.position.Y += this.font.Height; } }
With such a class, one can write code like this:

 void DisplayUserInfo(User user, Graphics g, Font font, Bounds bounds)
 {
     LineDisplay firstColumn = new LineDisplay(g, font, bounds);
     firstColumn.WriteLine("User Name:");
     firstColumn.WriteLine("Full Name:");
     firstColumn.WriteLine("E-mail:");
     firstColumn.WriteLine("Phone:");

int columnWidth = bounds.Width / 2; int columnOffset = bounds.Left + columnWidth; Rectangle secondColumnbounds = new Rectangle(bounds.Left + columnOffset, bounds.Top, columnWidth, bounds.Height); LineDisplay secondColumn = new LineDisplay(g, font, secondColumnbounds); secondColumn .WriteLine(user.UserName); secondColumn .WriteLine(user.FullName); secondColumn .WriteLine(user.EmailAddress); secondColumn .WriteLine(user.PhoneNumber); }
This is easier to understand than the original example. If you do a lot of two-column printing, you might want to add something like this to your class:

    public class LineDisplay
    {
        // ...

public void WriteAtColumn(int x, string text) { this.graphics.DrawString(text, this.font, this.brush, x, this.position.Y); }

public void WriteTwoColumnLine(string leftText, string rightText) { this.WriteAtColumn(this.bounds.Left, leftText); this.WriteAtColumn(this.bounds.Left + (this.bounds.Width / 2), rightText); this.NewLine(); } }
And then the example becomes:

 void DisplayUserInfo(User user, Graphics g, Font font, Bounds bounds)
 {
     LineDisplay display = new LineDisplay(g, font, bounds);
     display.WriteTwoColumnLine("User Name:", user.UserName);
     display.WriteTwoColumnLine("Full Name:", user.FullName);
     display.WriteTwoColumnLine("E-mail:", user.EmailAddress);
     display.WriteTwoColumnLine("Phone:", user.PhoneNumber);
 }
This basic idea can be extended to support word wrapping and other advanced formatting features. For example, you could add a Write() method that updates the horizontal position as text is added to a line:

    public class LineDisplay
    {
        // ...

public void Write(string text) { SizeF textDisplaySize = this.graphics.MeasureString(text, this.font); this.graphics.DrawString(text, this.font, this.brush, this.position); this.position.X += textDisplaySize.Width; } }
and then add functionality to take care of automatically calling NewLine() before drawing past the right side of the boundary rectangle.

When using a class such as this to generate hardcopy output, it is useful to add features for determining page breaks and for adding header and footer text and page numbers to each page.


EditText of this page (last edited February 7, 2013) or FindPage with title or text search