Drawing Text

Just as you might find yourself with the need to draw arbitrary shapes to forms, be they handwritten or not, you might also be called upon to output text. Presenting output text to the user in a WFC component environment is easy. You just place an Edit or RichEdit control somewhere on the form, mark it single or multiline, and you're ready to write to it whatever text you want. In an environment ruled by the Graphics object and the paint event, displaying this text means drawing the text on the window. Drawing text is a little harder than simply writing it.

Drawing a Single Line

As long as all you are trying to do is draw a simple line of text, using a single font all written at one time, the problem isn't too difficult. It's still useful to divide this app up into the task of drawing a single line and the task of drawing multiple lines.

Drawing in a fixed location

The following simple program, HelloWorld, draws a single line of text—"Hello, world"—in a fixed location on the form. In the Forms Designer, set the form's title to match the name of the program and set Form1_paint() to handle the form's paint event. The remaining code appears as follows:

import com.ms.wfc.app.*;
import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.wfc.html.*;
public class Form1 extends Form
{
 public Form1()
 {
 // Required for Visual J Plus Plus Form Designer support
 initForm();
 }
 private void Form1_paint(Object source, PaintEvent e)
 {
 Graphics g = e.graphics;
 g.drawString("Hello, world", 80, 40);
 }
 /**
 * NOTE: The following code is required by the Visual J Plus Plus form
 * designer. It can be modified using the form editor. Do not
 * modify it using the code editor.
 */
 Container components = new Container();
 private void initForm()
 {
 // …created by the Forms Designer…
 }
 /**
 * The main entry point for the app. * …
 */
 public static void main(String args[])
 {
 app.run(new Form1());
 }
}


This program is very simple. Each time the paint event is generated, the Form1_paint() method invokes the drawString(String, x, y) method to draw the string "Hello, world". The upper left corner of the string is 80 pixels from the left edge of the form's drawing area (the x offset), and 40 pixels from the top (the y offset). This is demonstrated in the edited output from the program shown in Figure 9-4.

Screenshot

Screenshot-4. The edited "Hello, world" output, showing the meaning of the x and y offsets.

Drawing in a variable location

If you look closely at Figure 9-4, you'll notice that the "Hello, world" string appears approximately in the center of the form window and is appropriately sized for that window. This was no accident; I resized the window to make this so. However, suppose we want the text to be centered in the display window and be appropriately sized no matter what size the window is.

The following additions to HelloWorld.java do exactly that.

import com.ms.wfc.app.*;
import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.wfc.html.*;
public class Form1 extends Form
{
 public Form1()
 {
 // Required for Visual J Plus Plus Form Designer support
 initForm();
 }
 String outString = "Hello, world";
 Point center = new Point();
 Point offset = new Point();
 Point outOffset = new Point();
 private void Form1_paint(Object source, PaintEvent e)
 {
 Graphics g = e.graphics;
 // calculate the proper position for the string
 // based on the size of the form and the
 // size of the string in the current font--
 // calculate the center of the form
 Rectangle formSize = this.getDisplayRect();
 center.x = formSize.width / 2;
 center.y = formSize.height / 2;
 // to center the string, back up half the length
 // of the string and half of its height
 Point stringSize = g.getTextSize(outString);
 offset.x = stringSize.x / 2;
 offset.y = stringSize.y / 2;
 // now output the string at the center
 // minus the offset
 outOffset.x = center.x - offset.x;
 outOffset.y = center.y - offset.y;
 g.drawString(outString, outOffset);
 }
 
 private void Form1_resize(Object source, Event e)
 {
 // find the size of the display area of the form
 Rectangle size = this.getDisplayRect();
 // get a font size that's about 1/3
 // the vertical size of the form
 int fontHeight = size.height / 3;
 Font oldFont = this.getFont();
 Font newFont = new Font(oldFont, fontHeight, FontSize.PIXELS);
 // make this new font (if there is one) the default
 // font for the form
 if (newFont != null)
 {
 this.setFont(newFont);
 }
 }
 /**
 * NOTE: The following code is required by the Visual J Plus Plus form
 * designer. It can be modified using the form editor. Do not
 * modify it using the code editor.
 */
 Container components = new Container();
 private void initForm()
 {
 // …created by the Forms Designer…
 }
 /**
 * The main entry point for the app. * …
 */
 public static void main(String args[])
 {
 app.run(new Form1());
 }
}


From the Forms Designer, we set the Form1_resize() method to handle the form's resize event. This method's job is to create a font that is proportional in size to the window. To do this, Form1_resize() first calls getDisplayRect() to find out how big the form's display area is. It then creates newFont, which is like the old font in every way except that its height is one-third the height of the form's display area. (One-third was chosen after a very short trial of different values and isn't based on any human response data. In other words, pick whatever size factor you prefer.) Form1_resize() then sets this new font to be the default font for the form.

The added code in the Form1_paint() method makes that method considerably more complicated than the previous version was. It must calculate the x and y offsets to use in displaying the string, based on the size of the form's displayable area and the size of the string in the current font.

Form1_paint() starts by getting the form's displayable area. It then calculates the coordinates of the center of the screen by dividing the height and width by 2. The result is stored in the variable center.

NOTE
You might wonder why most of the local variables of Form1_paint() are created as data members of the class even though they are only used locally to the method. Methods such as Form1_paint() are called often, and they need to execute as fast as possible. Allocating variables such as center once at the beginning of the program and reusing them, rather than reallocating and then abandoning them every time the method is called, can save a considerable amount of execution time.

The Form1_paint() method must then find out how large the "Hello, world" string is in the current font. Fortunately, the Graphics class provides just such a method in getTextSize(). Given a string, this method returns a Point representing the height and width (in pixels) of the string in the current font. Since we want the center of the string to align with the center of the display area, Form1_paint() must divide this stringSize value by 2 as well. The result is stored in offset.

Subtracting offset from center gives the coordinates of where Form1_paint() should draw the string so that it's correctly centered both vertically and horizontally. Figure 9-5 demonstrates graphically how this works.

Screenshot

Screenshot-5. A diagram demonstrating the offsets that Form1_paint() uses to center the "Hello, world" text in the form window.

Figures 9-6 and 9-7 show the output of this program with various window sizes. These figures demonstrate that both the font sizing and centering algorithms work as desired.

Screenshot

Screenshot-6. A small form window results in a small font.

Screenshot

Screenshot-7. A large form window results in a large, heavy font that is still centered.

ISN'T THERE AN EASIER WAY TO POSITION TEXT?


When drawing text, you'll often be tempted to take this shortcut: "There are 10 characters in my output string and each character is x pixels wide, therefore the string is 10 times x pixels wide." Unfortunately, this logic doesn't work.

The problem with this solution lies in one of its assumptions. In general, each character isn't x pixels wide. Most fonts are called proportional fonts, meaning each character has a different width. By just reading this page, it should be obvious to you that capital W is given considerably more space than lower- case i. Other characters fall somewhere in between.

This being true, how does the Graphics.getTextSize() method calculate the width of a given string? Each font provides a table that lists the width of each character. The getTextSize() method iterates through the string you give it, adding up the widths of each character until it arrives at the width of the entire string.

There are a few fonts in which all characters are allocated the same width. These fonts are called monospace or nonproportional fonts and include such fonts as Courier. Monospace fonts are used in apps where you want the characters from different rows to line up in columns; most commonly this is in code listings. (Look closely at the code listings in this tutorial and you'll notice that the characters from different rows align.)

Both proportional fonts and monospace fonts allocate the same amount of vertical space to each character. This isn't to say that the character uses the entire vertical space. Clearly, a capital letter uses more space than a little letter such as an a. However, we as readers really want the bottoms of characters on the same row to line up across the page. (Characters that have descenders, like g, p, q, and y, represent a special case).

Thus, whether the font is monospace or proportional, we can still calculate the vertical offset by multiplying the number of lines times the vertical size of a single line.

Drawing Multiple Lines

You'll often be called upon to draw multiple lines of text. There are two cases that arise. The simpler case is that of drawing multiple rows of left-justified text. A more complicated case involves drawing multiple columns.

Left-justified text

Drawing multiple rows of left-justified text is just a matter of keeping straight what line you're drawing to and how tall your font is. Let me use a previous example program to demonstrate.

In the ConnectTheDots program presented at the beginning of this chapter, it would be interesting to display the coordinates of each dot. The information the app needs to draw in ANSI text from the coordinates of the dots is available in the paints list already. The following modifications, shown in boldface, to the Form1_paint() method in ConnectTheDots is therefore all that's needed to implement this change.

/**
 * Handle repaints of the form by drawing lines
 * between each of the mouse points in the draw list
 * and drawing the coordinates of the mouse points as
 * text.
 */
private void Form1_paint(Object source, PaintEvent e)
{
 // first draw the kaleidoscope
 Graphics g = e.graphics;
 int length = points.getSize();
 for (int i = 0; i < length; i++)
 {
 for (int j = 0; j < i; j++)
 {
 Point p1 = (Point)points.getItem(i);
 Point p2 = (Point)points.getItem(j);
 g.drawLine(p1, p2);
 }
 }
 // now output the coordinates of the points as text:
 // get the height of the current font
 String s = "Number of points = " + length;
 Point size = g.getTextSize(s);
 int height = size.y;
 // loop through the points stored in the list, drawing
 // each as a String;
 // start at vertical offset 10 and increment by the // font height plus 2 every line thereafter
 int y = 10;
 g.drawString(s, 10, y);
 for (int k = 0; k < length; k++)
 {
 Point p = (Point)points.getItem(k);
 y += (height + 2);
 g.drawString(p.toString(), 10, y);
 }
}


The kaleidoscope section of Form1_paint() is unchanged from the earlier version. We have added a section to draw, in text format, the coordinates stored in the points list. This section begins by constructing a string s consisting of the phrase "Number of points = x" where x is the number of members in points. The method then uses the Graphics.getTextSize() method to find the height of this string. This value is stored in the variable height.

Since Form1_paint() doesn't change fonts, it makes the assumption that the height of this line is the same as the height of every subsequent line. (See the previous sidebar "Isn't There an Easier Way to Position Text?".)

Once this height has been captured, Form1_paint() displays the string at location {10, 10}. It then iterates through the list points calling getItem() to return each subsequent Point object. At each pass through the list, Form1_paint() increments the vertical offset y by height + 2, which is the height of a line in the current font plus a small increment of 2, to increase the interline spacing to improve the appearance. Finally, the method outputs the Point object in String format at the calculated vertical offset. Using the same horizontal offset of 10 ensures that all the lines are left-justified.

Screenshot-8 demonstrates the result of this addition to ConnectTheDots.

Screenshot

Screenshot-8. Additions to Form1_paint() result in the textual display of the mouseDown points.

Concatenating text

Occasionally the programmer is asked to concatenate two strings on the display. Obviously the easiest approach is to concatenate the strings before drawing them; however, in some cases this isn't possible.

To demonstrate how to approach such problems, let's add one more level of complexity to the ConnectTheDots program. Clearly, if the user clicks many dots, the vertical string of text will eventually trail off the bottom of the window. To avoid this, suppose we make the restriction that each column of text should contain no more than six rows. The coordinates of the seventh point should appear on the first row but begin a new column. Rather than align the second column at a specific location as we did with the first column, however, we want the seventh string to display immediately to the right of the first string.

The following update to Form1_paint(), again shown in boldface, implements this new requirement. (By the way, I'm not suggesting that this is the best solution, but merely that this solution demonstrates how to concatenate strings on the display.)

/**
 * Handle repaints of the form by drawing lines
 * between each of the mouse points in the draw list
 * and drawing the coordinates of the mouse points as
 * text.
 */
int[] horizontalOffset = new int[6];
private void Form1_paint(Object source, PaintEvent e)
{
 // first draw the kaleidoscope
 Graphics g = e.graphics;
 int length = points.getSize();
 for (int i = 0; i < length; i++)
 {
 for (int j = 0; j < i; j++)
 {
 Point p1 = (Point)points.getItem(i);
 Point p2 = (Point)points.getItem(j);
 g.drawLine(p1, p2);
 }
 }
 // clear out the horizontal offset of each row --
 // start the first row 10 pixels from the left edge
 int maxRows = horizontalOffset.length;
 for (int i = 0; i < maxRows; i++)
 {
 horizontalOffset[i] = 10;
 }
 // now output the coordinates of the points as text;
 // get the height of the current font
 String s = "Number of points = " + length;
 Point size = g.getTextSize(s);
 int height = size.y;
 g.drawString(s, 10, 10);
 // loop through the points stored in the list, drawing
 // each as a String;
 // start at an initial vertical offset just beyond
 // the first string, an index of 0 and a rowNumber of 0
 int initialVO = 15 + height;
 for (int k = 0, rowNumber = 0, verticalOffset = initialVO;
 k < length;
 k++, rowNumber++, verticalOffset += (height + 2))
 {
 // if the row number exceeds the maximum number of rows…
 if (rowNumber >= maxRows)
 {
 // then reset the row number and vertical offset
 rowNumber = 0;
 verticalOffset = initialVO;
 }
 // get the point, and convert it to a string
 Point p = (Point)points.getItem(k);
 s = p.toString();
 // draw the string at the proper horizontal and
 // vertical offsets for this row
 g.drawString(s, horizontalOffset[rowNumber],
 verticalOffset);
 // update the horizontal offset by the width of this
 // string so the next column will appear in the proper
 // place
 horizontalOffset[rowNumber] += g.getTextSize(s).x;
 }
}


This new version of Form1_paint() maintains an array of six integers named horizonalOffset, representing the current horizontal offset of each row. Before beginning calculations, the method initializes each member of the horizontalOffset array to 10. This will force each row to begin 10 pixels from the left edge of the display.

Before beginning to display the Point objects stored in the points list, Form1_paint() calculates an initial vertical offset and stores it in the variable initialVO. This will become the offset of the first row.

The method then enters the same for loop as before, this time initializing the rowNumber to 0, the current verticalOffset to initialVO, and the index k to 0. Within the for loop, the program tests whether the rowNumber exceeds the maximum number of rows, in our case 6. If it does, the program resets the rowNumber to 0 and resets the verticalOffset back to the initial vertical offset, initialVO. This has the effect of moving output to the top row of the next column.

Once the program has determined the proper row and column to begin the next string, it retrieves the correct Point object from the points list and converts it to a text String as before. The program then draws the string beginning at the calculated horizontal and vertical offsets.

Before repeating the loop, the program increments the horizontalOffset of this rowNumber by the length in pixels of the current string. The repeat clause of the for loop increments the rowNumber by one, the verticalOffset by the height of a row (plus 2), and the index k by one.

As you can see, the key addition to this program is the array horizontalOffset. If you consider just one row, say row 0, horizontalOffset[0] starts off with the value 10. After drawing the first string, horizontalOffset[0] is equal to 10 plus the length of the first string in pixels. This value is used as the initial horizontal displacement for the seventh string. After drawing the seventh string, horizontalOffset[0] is equal to 10 plus the length of the first string plus the length of the seventh string. This cycle repeats for column after column. The results can be seen in Figure 9-9.

Java Click to view at full size.

Screenshot-9. The multicolumn version of ConnectTheDots, demonstrating the results of using getTextSize() to concatenate strings on the display. Comments