3

I've used other excellent answers to design a method that returns the correct dimension of a JTextPane, given it's content and width, using LineBreakMeasurer.

The problem is that it is slightly of when I try different font sizes, and I can't figure out why. It works perfectly for say Arial 12 but not for Arial 8 or Arial 16 because then nextLayout calcuates the length wrong. Can anyone tell me why?

import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextLayout;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextPane;
import javax.swing.border.LineBorder;


public class Example1 {

static JFrame frame = new JFrame();
static JPanel bigPanel = new JPanel();
static JTextPane textPane[] = new JTextPane[10];



public static void main(String[] args) {

    frame.setLayout(new FlowLayout());
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

    for(int i=0 ; i<10 ; i++) {
        textPane[i] = new JTextPane();
        textPane[i].setFont(new Font("Arial", 0, 12+i ));
        textPane[i].setText("This is 10 characters of " + ((Integer)(10+i)).toString() + " points : a b c d e f g h i j");
        textPane[i].setPreferredSize(getTextDimension(textPane[i], 100));
        textPane[i].setBorder(new LineBorder(Color.BLACK));
        frame.add(textPane[i]);
    }

    frame.setSize(500, 500);
    frame.setVisible(true);

}

static private Dimension getTextDimension(JTextPane textPane, float width) {

    FontMetrics fm = textPane.getFontMetrics(textPane.getFont());
    int fontHeight = fm.getHeight()+ fm.getDescent();
    //int fontHeight = fm.getHeight();

    AttributedString text = new AttributedString(textPane.getText());
    FontRenderContext frc = textPane.getFontMetrics(textPane.getFont()).getFontRenderContext();
    AttributedCharacterIterator styledText = text.getIterator();
    LineBreakMeasurer measurer = new LineBreakMeasurer(styledText, frc);

    float wrappingWidth = width - textPane.getInsets().left - textPane.getInsets().right;

    int lines = 0;
     while (measurer.getPosition() < styledText.getEndIndex()) {
        TextLayout layout = measurer.nextLayout(wrappingWidth);
        lines++;
     }

     return new Dimension((int)Math.round(width), lines * fontHeight);
}

}

whetstone
  • 635
  • 6
  • 11
  • 3
    Please cite the relevant answers and edit your question to include an [sscce](http://sscce.org/) that exhibits the problem you describe. – trashgod Mar 30 '13 at 03:54
  • I've edited the example to sscce and this shows the calculation to be slightly off when font size increases (even if font ascent and descent is included) – whetstone Apr 12 '13 at 22:05
  • +1 for [sscce](http://sscce.org/). Also consider `WindowAdapter` or `setDefaultCloseOperation()`. – trashgod Apr 13 '13 at 00:45
  • 1
    Altered example to use `setDefaultCloseOperation()` (only nicer code, not relevant for the actual problem). Learning all the time, thanks @thrashgod – whetstone Apr 13 '13 at 14:45

2 Answers2

4

I fixed the font size issue by adding the following line after the instantiation of the AttributedString:

AttributedString text = new AttributedString(textPane.getText());
text.addAttribute(TextAttribute.FONT, textPane.getFont());

I guess the Attributed String needs the font defined, although I thought it was handled by the FontRenderContext but, at least for me, it was not.

Urchin
  • 395
  • 3
  • 9
2

I'm not sure what's wrong with your getTextDimension(), but a more reliable approach is to ask the text component to do the calculation. Several perspectives are offered in How can I measure/calculate the size a Document needs to render itself?

Addendum: Here's a variation on @camickr's TextPanePerfectSize.

image

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.border.LineBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

/**
 * @see https://stackoverflow.com/a/15983197/230513
 * @see https://stackoverflow.com/a/3318949/230513
 */
public class Test {

    private static final int SIZE = 16;
    private static int size = SIZE;
    private static JTextPane textPane[] = new JTextPane[SIZE];

    public static void main(String[] args) {
        final JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel buttonPanel = new JPanel();
        buttonPanel.add(new JButton(new AbstractAction("Update") {
            @Override
            public void actionPerformed(ActionEvent e) {
                for (JTextPane jtp : textPane) {
                    StyledDocument doc = (StyledDocument) jtp.getDocument();
                    SimpleAttributeSet style = new SimpleAttributeSet();
                    StyleConstants.setFontFamily(style, "Serif");
                    StyleConstants.setFontSize(style, size++);
                    try {
                        doc.insertString(doc.getLength(), " one two three", style);
                        Dimension d = jtp.getPreferredSize();
                        Rectangle r = jtp.modelToView(jtp.getDocument().getLength());
                        d.height = r.y + r.height;
                        jtp.setPreferredSize(d);
                    } catch (BadLocationException ex) {
                        ex.printStackTrace(System.err);
                    }
                    frame.pack();
                }
            }
        }));
        frame.add(buttonPanel, BorderLayout.NORTH);
        JPanel textPanel = new JPanel(new GridLayout(0, 4));
        for (int i = 0; i < textPane.length; i++) {
            textPane[i] = new JTextPane();
            textPane[i].setText("Text pane number " + String.valueOf(i + 1));
            textPane[i].setBorder(new LineBorder(Color.BLACK));
            textPanel.add(textPane[i]);
        }
        frame.add(new JScrollPane(textPanel));
        frame.pack();
        frame.setVisible(true);
    }
}
Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Note: [`setPreferredSize()`](http://stackoverflow.com/q/7229226/230513) used for demonstration only. – trashgod Apr 13 '13 at 10:06
  • Thanks but I dont think this solve my problem. What `getTextDimension()` tries to do is to calculate the height of the text with a given width, so basically my problem is that I want to use `setPrefferedSize()` but only set the width and not the height and let the textpane figure that out for itself – whetstone Apr 13 '13 at 14:38
  • The example relies on the initial width; the pane provides the new height. – trashgod Apr 13 '13 at 14:40
  • Yes it does, but relies on the layout manager to restrict it's width. I need to set the textpane to work without a layout manager (it's up to the user do drag it around to the correct place). If I alter your example (very clever by the way) to use `frame.setLayout(new FlowLayout());` instead of GridLayout, and then force the width of jtp before the calculation with `jtp.setPreferredSize(new Dimension(80,500));` just after `try` it nearly works but then I've messed up the wordwrap instead. – whetstone Apr 13 '13 at 15:09
  • 1
    `JInternalFrame` may be an easier way to drag a `JTextPane`. – trashgod Apr 13 '13 at 15:22