4.9. Fonts and Text
As we've seen, fonts are represented in AWT by the java.awt.Font class. While you can continue to use fonts in Java 1.2 exactly as you did in Java 1.1, Java 2D has added a number of powerful new features related to fonts and text rendering that you may want to take advantage of.
4.9.1. Available Fonts
Java 1.0 and Java 1.1 support only a small set of fonts, specified by logical font names. Although these logical fonts are guaranteed to be available on every platform, they are not guaranteed to look the same on every platform. In addition, the lack of variety severely limits the design choices available to developers. The fonts and their logical names were listed earlier in Table 4-3.
Java 1.2 allows an application to use any font installed on the native system and refer to that font by its physical font name, instead of a logical font name. A physical font name is the actual name of a font, such as "Century Gothic" or "Lucida Sans Bold." To request a specific font, simply pass its physical name to the Font() constructor. The Font() constructor always returns a valid Font object, even if the font you have requested does not exist. If you need to check whether you got the font you requested, call the getFontName() method of the returned font.
If you want to be sure that a font exists on the host system before attempting to use it, you should first query the system to find out what fonts are installed. You can do this with methods of the java.awt.GraphicsEnvironment object. The code looks like this:
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); Font allfonts = env.getAllFonts();
The getAllFonts() method returns an array of Font objects that represents all of the fonts installed on the system. Each Font object in this array represents a font that is one point high, so you have to scale the font (using deriveFont() as explained shortly) before using it. Also, in the initial release of Java 1.2 at least, the getAllFonts() method can take prohibitively long to return (65 seconds on my Windows 95 system).
Another GraphicsEnvironment method, getAvailableFontFamilyNames(), returns an array of font family names instead of an array of Font objects:
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); String familynames = env.getAvailableFontFamilyNames();
This method returns relatively quickly and is therefore safer to call than getAllFonts(). Note that this method returns font family names (e.g., "Lucida Sans"), not actual font face names (e.g., "Lucida Sans Oblique"). The good news is that you can get away with specifying a font family name instead of a font face name when you call the Font() constructor.
4.9.2. Font Attributes
In Java 1.2, the Font class has a new constructor that is passed a java.util.Map object that contains a set of font attributes. These attributes specify the desired characteristics of the font; the Font() constructor tries to return a Font that matches the attributes. Typically, you use a java.util.Hashtable or java.util.Hashmap to hold your attribute values. The attribute names or keys are constants defined in java.awt.font.TextAttribute. The important constants are FAMILY, SIZE, WEIGHT, and POSTURE. The TextAttribute class also defines commonly used values for the WEIGHT and POSTURE attributes.
4.9.3. Transforming Fonts
The Font class defines several deriveFont() methods that allow you to use a Font object to create related Font objects. deriveFont() is typically used to return a new Font object that represents an existing font at a different size or in a different style. For example:
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); String familynames = env.getAvailableFontFamilyNames(); Font regularFont = new Font("Century Schoolbook", Font.PLAIN, 12); Font bigFont = regularFont.deriveFont(18.0f); Font boldFont = regularFont.deriveFont(Font.BOLD); Font bigBoldFont = regularFont.deriveFont(Font.BOLD, 24.0f);
When you are passing a point size to deriveFont(), be sure to explicitly specify a float value, such as the 18.0f constant in the preceding code, so that you do not inadvertently call the version of deriveFont() that takes an integer-style constant.
You can also derive a transformed version of a Font object by passing in an arbitrary java.awt.geom.AffineTransform object. This technique allows you to arbitrarily rotate or skew any font, as we'll discuss later in the chapter.
4.9.4. Text-Rendering Hints
The java.awt.RenderingHints class defines two hints that apply particularly to text drawing. The first controls antialiasing. Antialiasing is a technique used to make the jagged edges of shapes, such as the glyphs of a font, look smoother. It is implemented using translucent colors and compositing: when the edge of a shape only partially covers a pixel, the color used to draw that pixel is given an alpha- transparency value that corresponds to the amount of coverage. If a fully covered pixel is drawn with an opaque color, a pixel that is only one-quarter covered is drawn with an alpha value of .25. As you can imagine, antialiasing can be computationally intensive. However, the smoothing effect it achieves is significant and is particularly useful when drawing small amounts of text at large point sizes.
The first text-related rendering hint simply requests antialiasing for text. If you want text to be antialiased, set the KEY_TEXT_ANTIALIASING hint to VALUE_ TEXT_ANTIALIAS_ON. There is also a more general hint, KEY_ANTIALIASING. Java 2D defines a separate hint for text so that you can choose independently whether to request antialiasing for text and other graphics.
The second text-related rendering hint controls the low-level positioning of characters of text. When Java 2D renders the shape of an individual font glyph, it caches the rendered pixels for reuse. This technique dramatically speeds up text display. However, the cached rendering is useful only if the glyph is always drawn at an integral pixel position. By default, therefore, most implementations of Java 2D adjust character spacing so that the origin of each character falls evenly on an integer-pixel coordinate. If you want to be able to position text at arbitrary floating-point positions, without forcing each character to the nearest device pixel, set the KEY_FRACTIONALMETRICS hint to VALUE_FRACTIONALMETRICS_ON. Note, however, that the visual effect of setting this hint is rarely worth the computational overhead it requires.
4.9.5. Measuring Text and Fonts
Sometimes you need to obtain measurement information about a font or measure text before you can draw text. For example, to horizontally center a string of text, you must be able to figure out how wide it is. To correctly draw multiple lines of text, you need to be able to query the baseline position and the interline spacing for the font. In Java 1.0 and Java 1.1, you obtained this information with the FontMetrics class (as described near the beginning of the chapter).
Java 2D provides another way to measure the width of a string of text. The Font class defines several getStringBounds() methods that return the width and height of a specified string as a Rectangle2D object. These methods allow widths to be returned as floating-point numbers instead of integers and are therefore more accurate than the stringWidth() method of FontMetrics. Each variant of getStringBounds() allows you to specify a string of text in a different way. What these methods have in common, however, is that they must all be passed a FontRenderContext object. This object contains information needed to accurately measure text. It includes information about whether antialiasing and fractional metrics are being used, for example. You can obtain an appropriate FontRenderContext by calling the getFontRenderContext() method of a Graphics2D object.
The Java 1.2 Font class also defines a set of getLineMetrics() methods that are similar to the getStringBounds() methods. Each method takes a FontRenderContext object and returns a java.awt.font.LineMetrics object that contains various vertical metrics for the font. LineMetrics is similar to the older FontMetrics, except that it returns precise float values instead of approximate int values. getHeight() returns the line height of the font. This value is the sum of the values returned by getAscent(), getDescent(), and getLeading(). Ascent is the amount of space above the baseline, descent is the space below the baseline, and leading space is the empty interline spacing for the font. Other LineMetrics methods return values that allow you to correctly underline and strike through text.
The following code shows how you can obtain important metrics for a string of text, so that you can center it in a box:
Graphics2D g; // Initialized elsewhere Font f; // Initialized elsewhere String message = "Hello World!"; // The text to measure and display Rectangle2D box; // The display box: initialized elsewhere // Measure the font and the message FontRenderContext frc = g.getFontRenderContext(); Rectangle2D bounds = f.getStringBounds(message, frc); LineMetrics metrics = f.getLineMetrics(message, frc); float width = (float) bounds.getWidth(); // The width of our text float lineheight = metrics.getHeight(); // Total line height float ascent = metrics.getAscent(); // Top of text to baseline // Now display the message centered horizontally and vertically in box float x0 = (float) (box.getX() + (box.getWidth() - width)/2); float y0 = (float) (box.getY() + (box.getHeight() - lineheight)/2 + ascent); g.setFont(f); g.drawString(message, x0, y0);
The getLineMetrics() methods all require a string to be specified, just as the getStringBounds() methods do. This is because a single font may have different font metrics for glyphs in different writing systems. If you pass a string of Latin text, you may get a different LineMetrics object than you would if you supplied a string of Chinese text, for example. If you pass in a string that mixes text from several distinct writing systems, you get line metrics for only a prefix of that string. The LineMetrics.getNumChars() method returns the length of this prefix.
4.9.6. Advanced Text Drawing
The easiest way to display text in an application is to use a Swing component such as a JLabel, JTextField, JTextArea, or JEditorPane. Sometimes, however, you have to draw text explicitly, such as when you are implementing a custom Swing component.
The easiest way to draw text is with the drawString() method of Graphics or Graphics2D. drawString() is actually a more complex method than you might think. It works by first taking the characters of a string and converting them to a list of glyphs in a font. There is not always a one-to-one correspondence between characters and glyphs, however, and font encodings usually do not match the Unicode encoding used for characters. Next, the method must obtain the measurements of each glyph in the list of glyphs and position it individually. Only after these steps can the method actually perform the requested string drawing operation.
If you are drawing a string repeatedly, you can optimize this process by first converting the string of characters into a java.awt.font.GlyphVector. This converts characters to glyphs and calculates the appropriate position for each glyph. Then, to draw the string, you simply pass the resulting glyph vector to the drawGlyphVector() method of a Graphics2D object. Your code might look like this:
Graphics2D g; Font f; GlyphVector msg = f.createGlyphVector(g.getFontRenderContext(), "Hello"); g.drawGlyphVector(msg, 100.0f, 100.0f);
This technique is useful only if you expect to be drawing the same string repeatedly. The optimization occurs because the string is converted to glyphs only once, instead of being converted each time you call drawString().
The GlyphVector class has a number of methods that are useful for other purposes. Once you have created a GlyphVector, you can call getOutline() to obtain a Shape that represents the original string or getGlyphOutline() to get the Shape of a single glyph. You can also call getGlyphMetrics() to obtain a GlyphMetrics object that contains detailed metrics for an individual glyph.
Two other methods, setGlyphPosition() and setGlyphTransform(), are designed to let you set the position and transform for individual glyphs. For example, you might use setGlyphPosition() to increase the interletter spacing of a glyph in a GlyphVector in order to implement fill-justification. In the initial release of Java 1.2, however, these methods are not implemented. If you want to handle the low-level layout of glyphs, one approach is to implement your own subclass of the abstract GlyphVector class.
A GlyphVector object can represent only glyphs from a single font; the default implementation represents only glyphs that appear on a single line of text. If you want to represent a single line of multifont text, you can use a java.awt. font.TextLayout object. And if you want to work with multiline text, you can use java.awt.font.LineBreakMeasurer to break a paragraph of multifont text into multiple TextLayout objects, each representing a single line of text.
TextLayout is a powerful class for displaying multifont text. It supports bidirectional text layout, such as when left-to-right English text is mixed with right-to-left Hebrew or Arabic text or when right-to-left Hebrew letters are mixed with left-to-right Hebrew numbers. Once you've created a TextLayout object, you can draw the text it represents by calling its draw() method, specifying a Graphics2D object and a position.
The TextLayout object does more than simply draw text. Once the text is drawn, it also provides methods that applications can use to allow a user to interact with the text. If the user clicks on the text, the TextLayout has a method that allows you to determine which character was clicked on. If you want to highlight portions of the text, you can tell the TextLayout the first and last characters to be highlighted, and it returns a Shape that represents the region to be highlighted. Similarly, if you want to display an insertion cursor within the text, you can specify the character position, and the TextLayout returns a Shape that you can draw to display the cursor. Although these methods may seem trivial, they in fact handle all the nontrivial complexities of multifont and bidirectional text, making TextLayout a powerful class for certain applications.
You can create a TextLayout object by specifying a String, a Font, and a FontRenderContext. However, a TextLayout created in this way can represent only single-font text. To display multifont text, you must use a java.text.AttributedCharacterIterator to represent the text. The attributes associated with the text should be java.awt.font.TextAttribute constants, such as TextAttribute.FONT. The easiest way to create an AttributedCharacterIterator is to create a java.text.AttributedString, specify attributes with its addAttribute() method, and then get an iterator for it with its getIterator() method. The java.text API is covered in Java in a Nutshell, not in this book.
4.9.7. Text Art with Font Glyphs
As I mentioned earlier, the GlyphVector class allows you to obtain a Shape object that represents the outline of a single glyph or a string of glyphs. This is a powerful feature of Java 2D that allows you to produce sophisticated text art. The Shape object returned by the getOutline() or getGlyphOutline() method of GlyphVector can be used in the same way that you use any other Shape object. Use the draw() method of Graphics2D to draw the outline of the glyph or glyphs. Use fill() to fill the glyphs with an arbitrary Paint. You can transform the glyph shapes by scaling, rotating, and skewing them and you can even use them to perform clipping and hit detection.
Copyright © 2001 O'Reilly & Associates. All rights reserved.