Search code examples
javaswingjtabbedpanepreferredsize

Size issues with JTabbedPane and fixed height content


While writing this question I have been able to find a way to get it to behave the way I wanted. Based on this, I am still posting the question as other people might face similar issues.

I have the following issues with the sizing of a JTabbedPane used for content that stretches horizontally but has a fixed height. Both options for setTabLayoutPolicy() seem to alter the height of the content and won’t consistently display it at its preferred or minimum height.

With the default WRAP_TAB_LAYOUT, the preferred size of the tab pane does not take into account whether the tabs are actually stacked or displayed next to each other at the moment, as discussed here, here and in this bug report. If the tabbed pane is laid out based on stacked tabs, the height of the content is increased by about 20 pixels (height of one tab) for each tab added when there is enough space for the tabs to be displayed next to each other. If the tabbed pane is laid out based on tabs being displayed next to each other, the content height is reduced when the tabs have to be stacked.

When the policy is set to SCROLL_TAB_LAYOUT, the height of the tab bar is fixed and the layout is mostly correct. However, depending on the look and feel, the size of the tab content is reduced by a few pixels. I have been able to find out that this is due to the insets of the tab area as defined by the L&F, which are not factored into the preferred size calculation of the tab pane (see this bug report). Setting UIManager.getDefaults().put("TabbedPane.tabAreaInsets", new Insets(0,0,0,0)) works for some L&F (e.g. Metal) but not for others (e.g. Nimbus).

It seems that there are only the following options:

  • use stacked tabs and have extra height added to the content
  • use stacked tabs and have the content covered when there is not enough space
  • use scrolled tabs and add a few pixels to the minimum/preferred size of the tab content, making it look a bit different with each L&F (but at least the content should not be cut off)
  • use scrolled tabs and set the UI of the tabbed pane to a new BasicTabbedPaneUI which does not look that great

Is there a clean way to enforce that the content of a tabbed pane is always displayed at a fixed heigh?

The following code and screenshots illustrate the problem

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.UIManager;

public class TabbedPaneTest extends JFrame {

    TabbedPaneTest() {

        JPanel mainPanel = new JPanel();
        mainPanel.setBackground(Color.white);

        JTabbedPane tabs = new JTabbedPane();
        //tabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); // content cut off by a few pixels
        tabs.setTabPlacement(JTabbedPane.BOTTOM);

        Dimension min  = new Dimension(100,200);
        Dimension max = new Dimension(Short.MAX_VALUE,Short.MAX_VALUE);
        //Dimension pref = new Dimension(Short.MAX_VALUE,200); // content cut off when small
        Dimension pref = new Dimension(0,200); // content gets extra space when large

        int tabCount = 3;
        for (int i = 0; i < tabCount; i++) {
            JLabel content = new JLabel();
            content.setMinimumSize(min);
            content.setMaximumSize(max);
            content.setPreferredSize(pref);
            tabs.addTab("lorem ipsum dolor sit amet", content);
        }

        // set up and render window
        getContentPane().add(mainPanel, BorderLayout.CENTER);
        getContentPane().add(tabs, BorderLayout.PAGE_END);
        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("JScrollPane Test");
        pack();
        setSize(700,400);

        tabs.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                JTabbedPane tabbedPane = (JTabbedPane) e.getComponent();
                int tabCount = tabbedPane.getTabCount();
                for (int i = 0; i < tabCount; i++) {
                    Component c = tabbedPane.getComponentAt(i);
                    ((JLabel) c).setText("<html>"
                            + getSizes(c, "content")
                            + getSizes(tabbedPane, "tabs")
                            + "</html>");
                }
            }
        });
    }

    private static String getSizes(Component c, String name) {
        return "<p>" + name + " - "
                + "  minimum:" + Integer.toString(c.getMinimumSize().width)
                         + "x" + Integer.toString(c.getMinimumSize().height)
                + "  maximum:" + Integer.toString(c.getMaximumSize().width)
                         + "x" + Integer.toString(c.getMaximumSize().height)
                + "  preferred:" + Integer.toString(c.getPreferredSize().width)
                         + "x" + Integer.toString(c.getPreferredSize().height)
                + "  actual:" + Integer.toString(c.getSize().width)
                         + "x" + Integer.toString(c.getSize().height)
                + "</p>";
    }

    public static void main(String args[]) {

        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
         */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(TabbedPaneTest.class
                    .getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            @Override public void run() {
                new TabbedPaneTest().setVisible(true);
            }
        });
    }

}

With scrolled tabs, the content is cut off by a few pixels (193px instead of 200px here):

screenshot with scrolled tabs

With stacked tabs and wide content, the content is cut off when the window is small (160px instead of 200px here):

enter image description here

With stacked tabs and narrow content, the content is made larger when the window is large (240px instead of 200px here):

enter image description here


Solution

  • After reading more about the way the preferred size of the tabbed pane is calculated, I have been able to come up with the following solution for the WRAP_TAB_LAYOUT case.

    The problem is that for the tabbed pane, the preferred height and width are coupled. The preferred height is calculated for the preferred width and not for the actual, current width. This is problematic if the layout manager of the parent respects the preferred height but not the preferred width.

    The solution I came up with is to set up a listener that sets the preferred width of each tab content to its current width. This way, the tabbed pane calculates its preferred height correctly and can be laid out e.g. with a BorderLayout.

    tabs.addComponentListener(new ComponentAdapter() {
        @Override
        public void componentResized(ComponentEvent e) {
            JTabbedPane tabbedPane = (JTabbedPane) e.getComponent();
            int tabCount = tabbedPane.getTabCount();
            for (int i = 0; i < tabCount; i++) {
                Component c = tabbedPane.getComponentAt(i);
                c.setPreferredSize(new Dimension(c.getSize().width, c.getPreferredSize().height));
            }
        }
    });
    

    I haven’t been able to find a satisfactory solution to the SCROLL_TAB_LAYOUT case yet, so if anyone has an idea it would be appreciated.