2

As shown in the following picture, an AttributedString is drawn on a JPanel (500X500).

The FontMetrics.getStringBounds() of that AttributedString gives a width of 164.0, as indicated by the trace output.

java.awt.geom.Rectangle2D$Float[x=0.0,y=-12.064453,w=164.0,h=15.09375]

However, the picture suggests the width should be 300-400 (because the width of the panel is 500).

Could you help to comment the reason and the workaround ?

enter image description here

MyJFrame.java

import javax.swing.*;
import java.awt.*;
import java.awt.font.TextAttribute;
import java.text.AttributedString;

class MyJPanel extends JPanel {
    MyJPanel() {
        setPreferredSize(new Dimension(500,500));
    }

    @Override
    public void paintComponent(Graphics gold) {
        super.paintComponent(gold);
        Graphics2D g = (Graphics2D)gold;
        //
        AttributedString text = new AttributedString("Bunny rabits and flying ponies");
        text.addAttribute(TextAttribute.FONT, new Font("Arial", Font.BOLD, 24), 0, "Bunny rabits".length());
        text.addAttribute(TextAttribute.FOREGROUND, Color.RED, 0, "Bunny rabits".length());

        text.addAttribute(TextAttribute.FONT, new Font("Arial", Font.BOLD & Font.ITALIC, 32), 17, 17 + "flying ponies".length());
        text.addAttribute(TextAttribute.FOREGROUND, Color.BLUE, 17, 17 + "flying ponies".length());

        FontMetrics fm = g.getFontMetrics();

        System.out.println(fm.getStringBounds(text.getIterator(), 0, text.getIterator().getEndIndex(), g));
        g.drawString(text.getIterator(), 50, 50);
        //
        g.dispose();
    }
}

public class MyJFrame extends JFrame {

    public static void main(String[] args) {
        MyJFrame frame = new MyJFrame();
        frame.setContentPane(new MyJPanel());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }    
}
Community
  • 1
  • 1
SOUser
  • 3,802
  • 5
  • 33
  • 63

2 Answers2

5

FontMetrics fontMetrics = graphics.getFontMetrics() returns a FontMetrics object based on the single font currently set on the graphics object. You are not changing the font used by graphics explicitly so it uses the default font designated for JPanel by the current L&F.

FontMetrics methods related to bounds calculation accept a "simple" CharacterIterator (which does not provide font information) instead of AttributedCharacterIterator (which does). Hence fontMetrics.getStringBounds() simply calculates the text bounds based on the single font of the same size.

You need to use java.awt.font.TextLayout to determine the proper bounds when using AttributedCharacterIterator with different fonts and font sizes:

TextLayout textLayout = new TextLayout( 
        text.getIterator(), 
        g.getFontRenderContext() 
);
Rectangle2D.Float textBounds = ( Rectangle2D.Float ) textLayout.getBounds();

g.drawString( text.getIterator(), 50, 50 );
// lets draw a bounding rect exactly around our text
// to be sure we calculated it properly
g.draw( new Rectangle2D.Float(
        50 + textBounds.x, 50 + textBounds.y,
        textBounds.width, textBounds.height
) );
Oleg Estekhin
  • 8,063
  • 5
  • 49
  • 52
  • When using `Rectangle2D` instead of `Rectangle2D.Float`, you can avoid the cast. However, `TextLayout` seems to be another option (besides `TextMeasurer`, as in my answer) - so +1 anyhow – Marco13 Jun 03 '14 at 15:50
  • @Marco13 plain `Rectangle2D` API is `double`-based. Either you cast to `Rectangle2D.Float` and use simple `float` fields or you do not cast the object but will have to use `getSomething():double` getters and cast from `double` to `float` everywhere else. – Oleg Estekhin Jun 03 '14 at 17:16
  • You don't know whether `getBounds` returns a `Rectangle` or a `Rectangle2D.Float` or a `Rectangle2D.Double`. Casting there may throw a `ClassCastException`, because the type of the `Rectangle` is simply not specified. You can rely on the `Rectangle2D` API, which is using `double` for all getters. In order to draw the rectangle, you can create a `new Rectangle2D.Double(50 + textBounds.getX(), ...)`. – Marco13 Jun 03 '14 at 17:24
  • I know because its source code is available. It was always R2D.Float and probably always will be. – Oleg Estekhin Jun 03 '14 at 17:26
  • I think this is just a matter of "good practice" and http://stackoverflow.com/questions/383947/what-does-it-mean-to-program-to-an-interface , but here it can be considered as a detail that is not directly related to the actual question. – Marco13 Jun 03 '14 at 17:59
1

The FontMetrics only receives a CharacterIterator, and does not take into account that it is actually an AttributedCharacterIterator. You can use a TextMeasurer to compute the actual bounds of your string. For comparison, add this after you called the drawString method:

    // Compensate for the 50,50 of the drawString position
    g.translate(50, 50);

    g.setColor(Color.RED);
    Rectangle2D wrongBounds = fm.getStringBounds(
            text.getIterator(), 0, text.getIterator().getEndIndex(), g);
    g.draw(wrongBounds);
    System.out.println("wrong: "+wrongBounds);

    g.setColor(Color.BLUE);
    AttributedCharacterIterator iterator = text.getIterator();
    TextMeasurer tm = new TextMeasurer(iterator, g.getFontRenderContext());
    Rectangle2D rightBounds = tm.getLayout(0, iterator.getEndIndex()).getBounds();
    g.draw(rightBounds);
    System.out.println("right: "+rightBounds);

(And BTW: Don't call g.dispose() on the Graphics that was handed to you in the paintComponent method)

Marco13
  • 53,703
  • 9
  • 80
  • 159