7

A JLabel containing HTML-text automatically wraps lines using the available space. If one adds that JLabel to a JSrollPane he has to set the preferredSize to a decent value otherwise it won`t wrap. All this should work fine along other Components inside a JPanel using a LayoutManager.

Cause I want a resizeable application window I extended JScrollPane to keep track of the resize events and dynamically change the size synced to the width of the viewport. Basically it works but sometimes the calculation of the preferred height by the layout manager is wrong (value too big or too small). For instance the visibility of the red border cutting through the first line indicates that the calculation of the height is wrong.

framegrabbing

I cannot reproduce the failure with a single wrapping JLabel.

import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;

public class WrappedLabel implements Runnable {

    public static void main( String[] args ){
        SwingUtilities.invokeLater( new WrappedLabel() );
    }

    @Override
    public void run(){
        final JPanel panel = new JPanel( new GridBagLayout() );
        final GridBagConstraints gc = new GridBagConstraints();
        gc.fill = GridBagConstraints.BOTH;
        gc.weightx = 1.0;
        gc.weighty = 1.0;
        {
            gc.gridx = 0;
            gc.gridy = 0;
            final JLabel label = new JLabel(
                "<html>" + "please add some more text here"
            );
            label.setBorder( BorderFactory.createLineBorder( Color.RED ) );
            panel.add( label, gc );
        }
        {
            gc.gridx = 0;
            gc.gridy = 1;
            final JLabel label = new JLabel(
                "<html>" + "please add some more text here"
            );
            label.setBorder( BorderFactory.createLineBorder( Color.RED ) );
            panel.add( label, gc );
        }
        final JFrame frame = new JFrame();
        frame.add( new ScrollPane( panel ) );
        frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
        frame.setSize( 256, 256 );
        frame.setVisible( true );
    }

    private class ScrollPane extends JScrollPane implements ComponentListener {

        ScrollPane( Container view ){
            super( view );
            this.viewport.addComponentListener( this );
        }

        @Override
        public void componentHidden( ComponentEvent ce ){
        }

        @Override
        public void componentMoved( ComponentEvent ce ){
        }

        /** calculating required height is a 3 step process
          * 1. sync width of client and viewport, set height of client to high value
          * 2. let GridbagManager calculate required minimum size
          * 3. set preferredSize and revalidate
         **/
        @Override
        public void componentResized( ComponentEvent ce ){
            assert( this.viewport == ce.getSource() );
            final Container view = (Container) this.viewport.getView();
            final int width = this.viewport.getExtentSize().width;
            view.setPreferredSize( new Dimension( width, Integer.MAX_VALUE ) );
            final int height = view.getLayout().preferredLayoutSize( view ).height;
            view.setPreferredSize( new Dimension( width, height ) );
            view.revalidate();
        }

        @Override
        public void componentShown( ComponentEvent ce ){
        }

    }

}
  • 5
    `// add some text here` Uh, no thanks, I'll leave that for *you* to do. In fact, for better help sooner, post an [SSCCE](http://sscce.org/). – Andrew Thompson Nov 12 '12 at 12:50

1 Answers1

0

Apparently it's either a bug in GridBagLayout, or you are using the layout engine in a way totally unexpected by the developers. Several multi-line Labels with HTML inside, setting preferred size and immediately asking preferred size by the back door? Ugh!

I noticed that sometimes the layout works incorrectly when decreasing the window size: the panel inside scrollpane doesn't decrease and the horizontal scrollbar appears. (I am using Windows by the way).

when decreasing

Also, sometimes, if the vertical scrollbar was visible and the panel height was large, and then I increase the window size, the panel height remains unreasonably large and gaps appear around the label:

enter image description here

For me, the layout is wrong every other time when I decrease the window; increasing works better but if it goes wrong, it's also incorrect every other time. I tried debugging and printing values to console; it seems that view.getLayout().preferredLayoutSize( view ) depends not only on view.setPreferredSize but also on the current size of the panel and scrollpane. The code of GridBagLayout is too complicated to dive into.

DIRTY HACK

Since every other resize yields the correct result, why not resize it twice? Duplicating things in the ScrollPane.componentResized handler was unsuccessful, probably because the ScrollPane's size remains the same. The ScrollPane itself needs to be resized twice, with different values. To test it in the simplest way, I subclassed JFrame: it listens to componentResized and resizes its child window twice. The second resize has to be deferred via SwingUtilities.invokeLater.

Replace the lines

final JFrame frame = new JFrame();
frame.add( scroll );

by

final MyFrame frame = new MyFrame(scroll);

and add the following class:

private class MyFrame extends JFrame implements ComponentListener {

    private Component child;

    public MyFrame(Component child){
        this.child=child;
        setLayout(null);
        getContentPane().add(child);
        addComponentListener(this);
    }

    public void componentResized(ComponentEvent e) {
        Dimension size=getContentPane().getSize();
        child.setSize(new Dimension(size.width-1,size.height));
        validate();
        SwingUtilities.invokeLater(new ResizeRunner(size));
    }

    public void componentMoved(ComponentEvent e) {}
    public void componentShown(ComponentEvent e) {}
    public void componentHidden(ComponentEvent e) {}

    private class ResizeRunner implements Runnable {
        private Dimension size;
        public ResizeRunner(Dimension size){
            this.size=size;
        }
        public void run() {
            child.setSize(size);
            validate();
        }
    }

}

The same can be achieved by subclassing a layout manager.

Obviously, this approach is inelegant and inefficient, but as a workaround for a JRE bug, and if nothing else helps... ;-)

Goblin Alchemist
  • 829
  • 5
  • 11