« April 2024 »
S M T W T F S
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
You are not logged in. Log in

Programming Tips
Tuesday, 15 November 2005
Drawing Boxes Around Characters and Strings
Mood:  chatty
Topic: Java
There are at least two ways to answer this question, and we'll look at both. 
The Font class offers a getStringBounds() method that returns the bounding rectangle of a String.
The Graphics class offers a getFontMetrics() method that allows you to get the string width and character dimensions
for a finer level of detail. 
Of course, there is also the option of just setting the border of a JLabel to draw the box for you. We'll look at all these here. 

All three programs take a string from the command line to draw with a bounded box. If no string is provided, the word "Money" 
is drawn, a word that offers a combination of uppercase and lowercase letters as well as a descender (the character "y," 
which extends below the baseline). 

1.     Using Font.getStringBounds()

The first way to draw a bounding box is sufficient for most cases in which you are explicitly drawing the characters. Given the 
graphics context passed into your paintComponent() method, get its font rendering context, which contains internal details 
about the font, and then ask that what the bounding rectangle is for your message. Then, draw the rectangle. 

      String message = ...;
      Graphics2D g2d = (Graphics2D)g;
      FontRenderContext frc = g2d.getFontRenderContext();
      Shape shape = theFont.getStringBounds(message, frc);
      g2d.draw(shape);

Just be sure to coordinate the location of the drawn string with the rectangle, as demonstrated by the following program: 

import javax.swing.*;
import java.awt.*;
import java.awt.font.*;

public class Bounds {

  static class BoxedLabel extends JComponent {
    Font theFont = new Font("Serif", Font.PLAIN, 100);
    String message;

    BoxedLabel(String message) {
      this.message = message;
    }

    public void paintComponent(Graphics g) {
      super.paintComponent(g);
      Insets insets = getInsets();
      g.translate(insets.left, insets.top);
      int baseline = 150;

      g.setFont(theFont);
      FontMetrics fm = g.getFontMetrics();

      // Center line
      int width = getWidth() - insets.right;
      int stringWidth = fm.stringWidth(message);
      int x = (width - stringWidth)/2;

      g.drawString(message, x, baseline);

      Graphics2D g2d = (Graphics2D)g;
      FontRenderContext frc = g2d.getFontRenderContext();
      Shape shape = theFont.getStringBounds(message, frc);
      g.translate(x, baseline);
      g2d.draw(shape);
      g.translate(-x, -baseline);
      g.translate(-insets.left, -insets.top);
    }
  }

  public static void main(String args[]) {
    final String message;
    if (args.length == 0) {
      message = "Money";
    } else {
      message = args[0];
    }
    Runnable runner = new Runnable() {
      public void run() {
        JFrame frame = new JFrame("Bounds");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JComponent comp = new BoxedLabel(message);
        frame.add(comp, BorderLayout.CENTER);
        frame.setSize(400, 250);
        frame.setVisible(true);
      }
    };
    EventQueue.invokeLater(runner);
  }
}

2.    Using Graphics.getFontMetrics()

The second mechanism demonstrates a finer level of detail that may or may not be necessary. What if you want to draw a box 
around each character of a string? With a proportional font, each character is a different width, so you can't just calculate font 
size multiplied by number of characters to get the width of the whole string. Instead, you work with the FontMetrics class to get 
the metrics of each character involved. 

      g.setFont(theFont);
      FontMetrics fm = g.getFontMetrics();

Some information you get is specific to the font, and not at the character level. When you draw a string, you specify the 
baseline -- the line where the bottom of the character is to be drawn. The space above the line for the tallest character, 
including diacritical marks like umlauts, is called the ascent. Below the line for descenders (such as the letter "y") is called 
descent. Space reserved for between lines of characters is called leading, pronounced like the metal, 
not like taking charge of a group. The height is the sum of these three. 

      int ascent  = fm.getAscent();
      int descent = fm.getDescent();
      int leading = fm.getLeading();
      int height  = fm.getHeight();

There are also values for the maximum of the three sizes, though this may be the same as the non-max value. 

      int maxAdv  = fm.getMaxAdvance();
      int maxAsc  = fm.getMaxAscent();
      int maxDes  = fm.getMaxDescent();

As far as width goes, you can ask for the width of the whole string: 

      int stringWidth = fm.stringWidth(message);

So, in the case of the bounding box of a string, you'd take its width and height to create the rectangle. 

      int stringWidth = fm.stringWidth(message);
      int height  = fm.getHeight();

To work at the character level, the FontMetrics class has a charWidth() method. 
By looping through your string, you can increase the x coordinate of your box drawing to draw the next box. 
charX += fm.charWidth(message.charAt(i));

The following program demonstrates the use of both sets of methods to box off the whole string and each character. 

import javax.swing.*;
import java.awt.*;

public class Metrics {

  static class BoxedLabel extends JComponent {
    Font theFont = new Font("Serif", Font.PLAIN, 100);
    String message;

    BoxedLabel(String message) {
      this.message = message;
    }

    public void paintComponent(Graphics g) {
      super.paintComponent(g);
      Insets insets = getInsets();
      g.translate(insets.left, insets.top);
      int baseline = 150;

      g.setFont(theFont);
      FontMetrics fm = g.getFontMetrics();

      int ascent  = fm.getAscent();
      int descent = fm.getDescent();
      int height  = fm.getHeight();
      int leading = fm.getLeading();
      int maxAdv  = fm.getMaxAdvance();
      int maxAsc  = fm.getMaxAscent();
      int maxDes  = fm.getMaxDescent();

      // Center line
      int width = getWidth() - insets.right;
      int stringWidth = fm.stringWidth(message);
      int x = (width - stringWidth)/2;

      g.drawString(message, x, baseline);

      drawHLine(g, x, stringWidth, baseline);
      drawHLine(g, x, stringWidth, baseline-ascent);
      drawHLine(g, x, stringWidth, baseline-maxAsc);
      drawHLine(g, x, stringWidth, baseline+descent);
      drawHLine(g, x, stringWidth, baseline+maxDes);
      drawHLine(g, x, stringWidth, baseline+maxDes+leading);

      int charX = x;
      for (int i=0; i <= message.length(); i++) {
        drawVLine(g, charX, baseline-ascent, baseline+maxDes+leading);
        if (i != message.length()) {
          charX += fm.charWidth(message.charAt(i));
        }
      }
    }

    void drawHLine(Graphics g, int x, int width, int y) {
      g.drawLine(x, y, x+width, y);
    }

    void drawVLine(Graphics g, int x, int y, int height) {
      g.drawLine(x, y, x, height);
    }
  }

  public static void main(String args[]) {
    final String message;
    if (args.length == 0) {
      message = "Money";
    } else {
      message = args[0];
    }
    Runnable runner = new Runnable() {
      public void run() {
        JFrame frame = new JFrame("Metrics");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JComponent comp = new BoxedLabel(message);
        frame.add(comp, BorderLayout.CENTER);
        frame.setSize(400, 250);
        frame.setVisible(true);
      }
    };
    EventQueue.invokeLater(runner);
  }
}

One thing to mention about this program is that had the font been italic, the character might not have been bound to within 
the drawn box. If you need absolute control over the bounds of a string, consider looking at the TextLayout class, which 
allows you to get the outline shape of a string for further manipulation. 

3.    Setting Border of JLabel 

The JLabel class has a setBorder() method. This allows you to directly "add" the bounding box without having to subclass to 
draw it yourself. The system classes will calculate the appropriate location for you, as the following example shows. 

import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;

public class Bordered {

  public static void main(String args[]) {
    final String message;
    if (args.length == 0) {
      message = "Money";
    } else {
      message = args[0];
    }
    Runnable runner = new Runnable() {
      public void run() {
        JFrame frame = new JFrame("Border");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JComponent comp = new JLabel(message, JLabel.CENTER);
        comp.setFont(new Font("Serif", Font.PLAIN, 100));
        comp.setBorder(LineBorder.createBlackLineBorder());
        frame.getContentPane().setLayout(new GridBagLayout());
        frame.add(comp);
        frame.setSize(400, 250);
        frame.setVisible(true);
      }
    };
    EventQueue.invokeLater(runner);
  }
}
Happy days ahead...:)

Posted by Nibu babu thomas at 12:37 PM
Updated: Tuesday, 15 November 2005 12:51 PM

Newer | Latest | Older