tags:

views:

74

answers:

2

Good evening,

I'd like to know how to get the size of a JComponent after it's been laid out with a LayoutManager. I've read that this is possible, if you just call dolayout(), validate(), or setVisible() beforehand. However, I can't get it to work in my code.

The reason I'd like to know this is to only add as many components as will fit in the frame's set size, while not knowing the size of the components beforehand. Calling validate() doesn't set the size of the components in this code sample. Any ideas on how I can get the right size?

public class TestFrame extends JFrame {

    public static void main(String args[]) {
        TestFrame frame = new TestFrame();
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public TestFrame() {
        super("Test Frame");
        super.setSize(300, 600);
        this.addComponents();
    }

    private void addComponents() {

        int heightLeft = this.getHeight();
        JPanel panel = new JPanel();
        panel.setSize(300, 600);
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        String text = "This is a long String that represents "
            + "the length of strings in my application. "
            + "This will vary in the real application."
            + "<ul><li>It uses html</li><li>not just</li>"
            + "<li>plain text</li></ul>"
            + "I'd like as many as will fit in the fame to be added.\n";

        while (true) {
            JTextPane textPane = createTextPane(text);

            this.invalidate();

            if (heightLeft > 0) {
                panel.add(textPane);
            } else {
                break;
            }
            System.out.println("Before validation:" + textPane.getSize());
            // returns width and height = 0
            this.validate();
            System.out.println("After validation:" + textPane.getSize());
            // Still returns width and height = 0
            heightLeft -= textPane.getPreferredSize().getHeight();
        }

        super.add(panel);
    }

    public JTextPane createTextPane(String text) {
        JTextPane textPane = new JTextPane();

        textPane = new JTextPane();
        textPane.setEditorKit(new StyledEditorKit());
        textPane.setEditable(false);
        textPane.setOpaque(false);

        textPane.setContentType("text/html");
        textPane.setText(text);

        return textPane;
    }
}

Thanks for your time!

+1  A: 

The frame on which the components are displayed must be visible in order to get the size (because if the frame is not visible, layout manager sets the size to (0, 0)). You must call frame.setVisible(true) before adding the components. I also recommend that you make your own panel class (which would extend JPanel) and to create components and put them on panel from there, not from the main class.

protector
Doing this in my code made the size get set as expected. Thanks. However, as Tim pointed out, the BoxLayout stretches the components, so only 2 textPanels are added, not as many as fit the frame.
Kate
Yeah, I became aware of that after reading Tim's comment. You should definately use another layout manager. I always use GridBagLayout, it may seem a bit complicated at first, but it never gave me much trouble, so I recommend that you use it.
protector
+2  A: 

What you're trying to accomplish seems to be surprisingly difficult, because you have to rely on Swing's method of calculating the size of your various components, and then manage to get the correct values out to perform the calculations on them. While it looks fairly trivial based on the available API methods, a glance at the source code reveals a different story. This is additionally complicated by the fact that some values are only correctly calculated when the component is actually displayed, meaning that the values you try to get before that aren't representative of what you see on the screen (as you found out).

That aside, it seems like it should be possible to do what you're aiming for. Keep in mind though that when adding elements to a BoxLayout, the expected behaviour is that the entire space of the container to which you've assigned the layout manager is utilized in positioning the child components. This means that as you add JTextPane objects to your JPanel, they will stretch so that their summed heights takes up all 600 pixels you've specified. As a result, you need to go about determining when you've exceeded the available height a little differently, since the rendered height of each component might change in-process.

After playing around with it a little and cursing Swing repeatedly, I had though I had come up with a solution by using getBounds() (which BoxLayout uses in delegation to its LayoutParameters to determine the component height), but the method I had turned out to not be very reliable. I'm going to keep playing around with it, so hopefully I'll come up with some usable code, or someone else will come along with a solution that I've overlooked.

Edit: For completeness, here's a code sample that calls doLayout() to calculate the necessary sizes before showing the form (still with the drawbacks of BoxLayout). It won't run on my JRE 1.5 due to some ArrayIndexOutOfBoundsException in SizeRequirements, but that bug seems to have been fixed in 1.6. In 1.5, if your list of components wasn't too large, you could add them all, call doLayout() after the loop (as this seems to work in 1.5 for whatever reason), then just traverse the array returned by getComponents(), removing everything after the maximum height has been exceeded.

As a final note, I'm personally a fan of using MigLayout for more complex layouts, since I find its positioning and styling mechanism a bit easier to work with than the current built-in layout managers. Not overly relevant here, but worth a mention anyway.

import java.awt.Dimension;
import java.awt.Rectangle;

import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextPane;
import javax.swing.text.StyledEditorKit;

public class TestFrame extends JFrame {
    private static final int CONTENT_HEIGHT = 600;
    private static final int CONTENT_WIDTH = 300;

    public static void main(String[] args) {
        TestFrame frame = new TestFrame();

        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public TestFrame() {
        super("Test Frame");
        this.addComponents();
        this.pack();
    }

    private void addComponents() {
        JPanel panel = new JPanel();

        panel.setPreferredSize(new Dimension(CONTENT_WIDTH, CONTENT_HEIGHT));
        panel.setBounds(new Rectangle(CONTENT_WIDTH, CONTENT_HEIGHT));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));

        String text = "This is a long String that represents the length of" +
            " stings in my application. This will vary in the real" + 
            " application.<ul><li>It uses html</li><li>not just</li>" +
            "<li>plain text</li></ul>I'd like only as many as will fit" +
            " in the fame to be added.\n";

        this.setContentPane(panel);

        int height = 0;

        while(height < CONTENT_HEIGHT) {
            JTextPane textPane = createTextPane(text);

            panel.add(textPane);
            panel.doLayout();

            // The height on the preferred size has been set to reflect
            // the rendered size after calling doLayout()
            height += textPane.getPreferredSize().getHeight();

            // If we've exceeded the height, backtrack and remove the
            // last text pane that we added
            if (height > CONTENT_HEIGHT) {
                panel.remove(textPane);
            }
        }
    }

    private JTextPane createTextPane(String text) {
        JTextPane textPane = new JTextPane();

        textPane.setEditorKit(new StyledEditorKit());
        textPane.setEditable(false);
        textPane.setOpaque(false);
        textPane.setContentType("text/html");
        textPane.setText(text);

        return textPane;
    }
}
Tim Stone
Thanks for taking such a detailed look into this. I didn't know that BoxLayout worked quite that way. I'll look into other LayoutManagers that may avoid this problem. Maybe FlowLayout after setting a fixed width for all the components? (It's important to have the components flow like paragraphs on a page.) Playing around with it and cursing Swing has been most of what I've been doing with this; glad I'm not alone.
Kate
No problem, it was an interesting question. `FlowLayout` might work like you want, as long as you give the container a fixed-width as well. This probably needs to be done with `setPreferredSize()`, even though it seems a little counter-intuitive given the other options (`setSize()`, `setMaximumSize()`). Swing has a lot of good intentions, but the resulting implementation sometimes misses the mark I feel. It's not so bad once you get used to it, though.
Tim Stone
It works! Perfectly! Thank you! This is a good enough solution to solve my problem, though I'll play around with different LayoutManagers, as for my project best design would be for all the text to bunch together at the top of the page, as much like text on a sheet of paper as possible.
Kate