COMPONENT JAVAAWT Styled Text Component

STYLEDTEXTAREA IS A non-Swing, AWT-based custom component that can run on applets in a Web browser. Unlike the "plain" Abstract Windowing Toolkit (AWT) TextArea, which can only display text in one specific color, font, and size, StyledTextArea can display text in different fonts, colors, and sizes at the same time. The StyledTextArea component supports text wrapping and has one method—append—but can easily be extended to cater to your needs.

One classic problem that has not been perfectly resolved in Java is the applet GUI. On one hand, AWT is considered primitive and inadequate, and developers are encouraged to use Swing components that are richer, more powerful, and look the same everywhere. On the other hand, many Java programmers still stick with AWT for applet development because Swing applets can only run in browsers that have the appropriate version of Java Plug-in installed. To run Swing applets without plug-ins, you need to use a JDK 1.2-compliant browser. However, the only JDK 1.2-compliant browser available today is the Applet Viewer utility provided with the Java 2 SDK. In the real world, where browsers mean Microsoft Internet Explorer or Netscape Navigator, Applet Viewer is only used on development machines. If your users are using JDK 1.1.x-compliant browsers and you insist on using Swing applets, you have to worry about how your users can download the Swing JAR file. In many cases, downloading the correct Plug-in version to make your browser "swing" is not a guarantee that your Swing applet will work as expected.

That's the main reason why AWT is still heavily being used, especially in applets, despite the current release of newer JDKs that include Swing. However, using AWT means you have to live with a somewhat limited set of components. One missing feature in AWT is the ability to display text colorfully, with different types of fonts and sizes.

Unlike Swing text components, with which you can easily display styled text such as HTML text using a JEditorPane or its subclass JTextPane, AWT has much less rich components. In fact, you only have the TextArea and TextField components, which in most cases are not sufficient. For example, if you want to create a chat application that can let users choose their message colors, you have to build your own component. Or, if you need a list box that can display items in various fonts, there is no ready-to-use component to employ. Therefore, there is a compelling reason to develop a custom component that enables you to display richer text. I present how to build StyleTextArea, which gives you the ability to display styled text. Listing 1 shows the complete code for StyledTextArea.

Canvas Component Basics
The brain of the StyledTextArea component is the Canvas component that it extends. A Canvas component represents a blank rectangular area of the screen onto which the application can draw or from which the application can trap input events from the user. An application must subclass the Canvas class to get useful functionality such as the ability to create custom components. The paint method must be overridden to perform custom graphics on the canvas.

Creating a custom component by extending the Canvas component is easy. For example, here is the simplest form of a new class called MyNewComponent that subclasses Canvas:


public class MyNewComponent extends Canvas {
  public MyNewComponent() {
  }
}
By instantiating the MyNewComponent class from, say, your applet, with the following code, you create a blank rectangular area of width width and height height:


  MyNewComponent newComponent;
  .
  .
  .
  //instantiate the new component
  this.setLayout(null);
  newComponent = new MyNewComponent();
  add(newComponent, null);
  newComponent.setBounds(0,0,width,height);
You can then draw geometrical shapes as you want or display text by overriding the paint method. In short, you can do with your new component anything you can with the Graphics class. For example, the following code draws a green rectangle of width 100 and height 50 starting from coordinates (0,0):


public class MyNewComponent extends Canvas {
  public MyNewComponent() {
  }

  public void paint(Graphics g) {
    g.setColor(Color.green);
    g.drawRect(0,0, 100,50);
  }
}
StyledTextArea
The StyledTextArea component extends the Canvas component and is embedded in a ScrollPane component to support scrolling. When instantiating the StyledTextArea component, you must also instantiate the ScrollPane component and add the StyledTextArea component to it. The initial width of the StyledTextArea component is determined when you instantiate the class. This value remains unchanged during the life of the StyledTextArea object. The initial height of the StyledTextArea is zero, and as you add lines of text using the append method, the height of StyledTextArea component must grow too. The height must be equal to the sum of the heights of the fonts used for each individual line. Consequently, every time you add a new line, you need to increase the height of StyledTextArea.

As just mentioned, each appended text is treated as a separate line. Three Vector object variables—lines, fonts, and colors (see Listing 1)—are used to store information on each line of text. lines stores the string of the text, fonts stores the Font object used with the text, and colors stores the Color object of each line of text. The Vector object provides the addElement method to add a new element to the Vector. The following three lines add the myString String to the lines Vector object, the myFont Font to the fonts Vector object, and the myColor Color to the colors Vector object:


    lines.addElement(myString);
    fonts.addElement(myFont);
    colors.addElement(myColor);
When calling the append method, you basically pass these three arguments: myString, myFont, and myColor.

Drawing the Text
Before we move to dissecting the code, let's have a look at how you can draw strings of text on your custom component.

You can use the Graphics class's drawBytes(), drawChars(), or drawString() methods to draw strings on your StyledTextArea component. For example, the following code draws the string "Cool components" to the screen:


g.drawString("Cool components", x, y);
For the text drawing methods, x and y are integers that specify the position of the lower left corner of the text. To be precise, the y coordinate specifies the baseline of the text—the line that most letters rest on—which doesn't include room for the tails (descenders) on letters such as "j."

Figure 1
Figure 1. The baseline, ascender line, and descender line of text.

Figure 1 shows an example of text with the baseline, as well as the ascender and descender lines.

It is clear now that when calling the append method to add a line of text, you must increase your StyledTextArea component's height by the height of the font used in the added text. Java comes with a class you can use to deal with font metrics—appropriately named FontMetrics. The FontMetrics class is very useful in our component for displaying the text correctly. The following are some of the FontMetrics methods related to the font height that you should be familiar with:

  • getAscent(), getMaxAscent(): The getAscent() method returns the number of pixels between the ascender line and the baseline. Generally, the ascender line represents the typical height of capital letters. Specifically, the ascent and descent values are chosen by the font's designer to represent the correct text "color," or density of ink, so that the text appears as the designer planned. The ascent typically provides enough room for almost all of the characters in the font, except perhaps for accents on capital letters. The getMaxAscent() method accounts for these exceptionally tall characters.
  • getDescent(), getMaxDescent(): The getDescent() method returns the number of pixels between the baseline and the descender line. In most fonts, all characters fall within the descender line at their lowest point. Just in case, though, you can use the getMaxDescent() method to get a distance guaranteed to encompass all characters.
  • getHeight(): Returns the number of pixels normally found between the baseline of one line of text and the baseline of the next line of text. Note that this includes an allowance for leading (pronounced LEDDing).
  • getLeading(): Returns the suggested distance (in pixels) between one line of text and the next. Specifically, the leading is the distance between the descender line of one line of text and the ascender line of the next line of text.
The font size (returned by the Font class's getSize() method) is an abstract measurement. Theoretically, it corresponds to the ascent plus the descent. Practically, however, the font designer decides exactly how tall, say, a 12-point font is. For example, 12-point Times is often slightly shorter than 12-point Helvetica. Font size is usually measured in points, which are approximately 1/72 of an inch.

When you append a line of text in your StyledTextArea component, you use the getHeight() method to get the number of pixels you must increase your StyledTextArea component height by. Therefore, you need to set the Font of your StyledTextArea component using the setFont method and obtain the FontMetrics object from your StyledTextArea, as given in the following code:


    setFont(font);
    FontMetrics fontMetrics = getFontMetrics(font);
    textHeight += fontMetrics.getHeight();
You might think that now that you know the height of the whole text, you can set the new height for your StyledTextArea component using the following line of code:


    setBounds(0, 0, width, textHeight);
However, recall that the y coordinate refers to the baseline of the text. This means if you have characters that occupy the descender line, such as y and j, they will get truncated. Instead, the correct new height of the component is textHeight + fontMetrics.getDescent(). Hence:


    setBounds(0, 0, width, textHeight + fontMetrics.getDescent());
Text Wrapping
Another feature of StyledTextArea is text wrapping. When you call the append method and pass to it a string that is longer than the width of your StyledTextArea object, the string has to be broken. Text wrapping makes sure that the string is broken at the blank space nearest to the component width. To achieve this purpose, you again have to turn to FontMetrics to obtain the string width.

The following list shows the methods that FontMetrics provides to return information about the horizontal size of a font's characters. These methods take into account the spacing around each character. More precisely, each method returns not the number of pixels taken up by a particular character (or characters), but the number of pixels by which the current point will be advanced when that character (or characters) is shown. We call this the advance width to distinguish it from the character or text width.

  • getMaxAdvance(): Returns the advance width (in pixels) of the widest character in the font.
  • bytesWidth(byte[], int, int): Returns the advance width of the text represented by the specified array of bytes. The first integer argument specifies the starting offset of the data within the byte array. The second integer argument specifies the maximum number of bytes to check.
  • charWidth(int), charWidth(char): The advance width of the specified character.
  • charsWidth(char[], int, int): The advance width of the string represented by the specified character array.
  • stringWidth(String): The advance width of the specified string.
  • getWidths(): The advance width of each of the first 256 characters in the font.
In the StyledTextArea component, the string width in pixels is obtained using the following code, where str is the string appended to StyledTextArea:


    int stringWidth = fontMetrics.stringWidth(str);
Text wrapping means that str cannot be greater than the drawing area width, which in this case is the StyledTextArea width minus the left margin and the right margin (the right margin is assumed to be equal to the left margin in the code). If the stringWidth is greater than the width of the drawing area, it must be broken into two substrings. The first substring has width less than the drawing area width, and the second substring will be passed to the append method again. This recursive action could mean that a long string ends up being a few smaller strings.

In the case of a very long word, breaking happens in the character that makes the first string narrow enough to fit in the drawing area.

The code for text wrapping is given here:


    int stringLength = str.length();
    int stringWidth = fontMetrics.stringWidth(str);
    int position = stringLength;

    char ch = 0;
    boolean loop=true;
    while ((stringWidth > (width-2*leftMargin)) && (position>0)) {
      while (ch!=' ' && position>0) {
        ch = str.charAt(--position);
      }
      // a very long word
      if (position==0) {
        position = stringLength;
        while (stringWidth > width-2*leftMargin) {
          stringWidth = fontMetrics.stringWidth(str.substring (0, position--));
        }
      }
      else {
        stringWidth = fontMetrics.stringWidth(str.substring (0, position));
        ch = 0;
      }
    }

    lines.addElement(str.substring(0, position));
    if (position!=stringLength)
      append(str.substring(position).trim(), font, color);
Finally, the code calls the repaint method to display the latest content of the StyledTextArea component.

Figure 2
Figure 2. StyledTextArea in Netscape Navigator.

Figure 3
Figure 3. StyledTextArea in Internet Explorer.

Conclusion
StyledTextArea currently only has one method, the append method. However, this component provides the framework. You can extend its functionality and add your own methods, such as the ones to insert and delete a line of text. It can be used both in applets and applications. Listing 2 is the code you need to use the StyledTextArea component from an applet.

The greatest challenge that I faced during the StyledTextArea component development was to make it work on Web browsers. This means the component has to work on both Internet Explorer and Netscape Navigator. After exhaustive and repeated testing and debugging, I managed to make the StyledTextArea browser-friendly. Figures 2 and 3 show the StyledTextArea in Netscape Navigator and Internet Explorer, respectively.