JTabbedPane: tab placement set to LEFT but icons are not aligned
Asked Answered
P

5

3

I have a JTabbedPane with tab placement set to LEFT. The problem is that icons in each tab are not vertically aligned with one another.

Consider this picture:

enter image description here

As you can see the icon of "Editar Protocolo" (second tab) is not perfectly aligned with the icon of "Distribuir Protocolo" (first tab) and this also happen with the other tabs. I want all icons be vertically aligned to the left.

This is the code I'm using to set tab components:

...
jtabbedPane.setTabComponentAt(1, configurarJtabbedPane("Editar Protocolo", iconEditarProtocolo));
...

public JLabel configurarJtabbedPane(String title, ImageIcon icon) {
    JLabel l = new JLabel(title);
    l.setIcon(icon);
    l.setIconTextGap(5);
    l.setHorizontalTextPosition(SwingConstants.RIGHT);
    return l;
}

The code is extracted from this Q&A:JTabbedPane: icon on left side of tabs.

Peritoneum answered 10/10, 2014 at 22:18 Comment(6)
1) For better help sooner, post an MCVE (Minimal Complete Verifiable Example). 2) One way to get images for an example, is to hot link to images seen in this Q&A.Strigose
how this code is not a MCVE? more than this you want to see the JFrame frame = new JFrame();Peritoneum
It's M but not C, V or E. Follow the link, read about the MCVE.Strigose
Not sure I understand the question. This would appear to be the default behaviour. See the section from the Swing tutorial on How to Use TabbedPanes for more information and examples.Ellynellynn
i mean full on the left, not depending in the JLabel text size like this, see how the "Editar Protocolo" Icon is not exact under the Distribuir protocolo?Peritoneum
I think other people misunderstood your question. While question's quality could be improved, I don't think it has to be closed because it's a really interesting topic.Nellanellda
N
13

What I want: the icons ALL in the LEFT, not based on the Text Size [...]

The tab's content is centered by typical implementations, and it makes sense because the area needed to fit this content is unpredictable until the tab is effectively rendered. Since the area depends on the content and different tabs will likely have different title lengths, then there has to be a policy about how to render those tabs. The criteria was to center tabs content and fit the tab area to this content. When we have a default tabbed pane with tabs placed at the top, we don't care much about icon/text alignment:

top_tabbed_pane

The only concern could be tabs having different length, but who cares? After all, icons and text are visible and tabbed pane looks good enough. However, when you set the tabs placement to LEFT or RIGHT things are different and it looks unappealing:

left_tabbed_pane

Apparently this default behavior is a long standing problem, and there's a really interesting discussion here. Some SO members are involved there: @camickr, @kleopatra, @splungebob. As discussed in that post, a simple solution is not possible, and several workarounds were proposed: basically a custom UI implementation or using panels as renderers and playing with preferred width/height based on text length. Both alternatives involve quite a lot of work.

In order to avoid dealing with UI delegates and taking advantage of setTabComponentAt(...) method, I've started some time ago a tabbed pane extension that I'd like to share here. The approach is based on Swing concept of renderer: a class that has to generate a component to render another component's part, and the goal is to provide a flexible mechanism to add custom tab components.

I have included an example below using my custom tabbed pane and here is an overview of all interfaces/classes needed to provide the aforementioned mechanism.

ITabRenderer interface

The first step is to define an iterface to offer a contract to render a tab component.

AbstractTabRenderer class

An abstract class to provide base methods to help in the getTabRendererComponent(...) method implementation. This abstract class has three main properties:

  • prototypeText: used to define a prototype text to generate a default renderer component.
  • prototypeIcon: used to define a prototype icon to generate a default renderer.
  • horizontalTextAlignment: tab's text horizontal alignment.

Note this class is abstract because it doesn't implement getTabRendererComponent(...) method.

DefaultTabRenderer class

A concrete implementation by extending AbstractTabRenderer class. Note that if you want to include a close button as shown in tutorial demo, then a little work in this class would be enough. As a matter of fact, I already did, but I won't include that part to not extend this (already large) post.

JXTabbedPane

Finally the tabbed pane's extension which includes tab renderer support and overrides addTab(...) methods.

Example

I have run this example with positive results using these PLAFs:

  • WindowsLookAndFeel
  • WindowsClassicLookAndFeel
  • NimbusLookAndFeel
  • MetalLookAndFeel
  • SeaglassLookAndFeel

Additionaly if you switch tab placement from LEFT to TOP (default) or BOTTOM then all tabs still having the same width, solving the concern described at the second paragraph of this answer.

import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

public class Demo {

    private void createAndShowGUI() {

        JXTabbedPane tabbedPane = new JXTabbedPane(JTabbedPane.LEFT);
        AbstractTabRenderer renderer = (AbstractTabRenderer)tabbedPane.getTabRenderer();
        renderer.setPrototypeText("This text is a prototype");
        renderer.setHorizontalTextAlignment(SwingConstants.LEADING);

        tabbedPane.addTab("Short", UIManager.getIcon("OptionPane.informationIcon"), createEmptyPanel(), "Information tool tip");
        tabbedPane.addTab("Long text", UIManager.getIcon("OptionPane.warningIcon"), createEmptyPanel(), "Warning tool tip");
        tabbedPane.addTab("This is a really long text", UIManager.getIcon("OptionPane.errorIcon"), createEmptyPanel(), "Error tool tip");

        JFrame frame = new JFrame("Demo");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.add(tabbedPane);
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);

    }

    private JPanel createEmptyPanel() {
        JPanel dummyPanel = new JPanel() {

            @Override
            public Dimension getPreferredSize() {
                return isPreferredSizeSet() ?
                            super.getPreferredSize() : new Dimension(400, 300);
            }

        };
        return dummyPanel;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Demo().createAndShowGUI();
            }
        });
    }

    class JXTabbedPane extends JTabbedPane {

        private ITabRenderer tabRenderer = new DefaultTabRenderer();

        public JXTabbedPane() {
            super();
        }

        public JXTabbedPane(int tabPlacement) {
            super(tabPlacement);
        }

        public JXTabbedPane(int tabPlacement, int tabLayoutPolicy) {
            super(tabPlacement, tabLayoutPolicy);
        }

        public ITabRenderer getTabRenderer() {
            return tabRenderer;
        }

        public void setTabRenderer(ITabRenderer tabRenderer) {
            this.tabRenderer = tabRenderer;
        }

        @Override
        public void addTab(String title, Component component) {
            this.addTab(title, null, component, null);
        }

        @Override
        public void addTab(String title, Icon icon, Component component) {
            this.addTab(title, icon, component, null);
        }

        @Override
        public void addTab(String title, Icon icon, Component component, String tip) {
            super.addTab(title, icon, component, tip);
            int tabIndex = getTabCount() - 1;
            Component tab = tabRenderer.getTabRendererComponent(this, title, icon, tabIndex);
            super.setTabComponentAt(tabIndex, tab);
        }
    }

    interface ITabRenderer {

        public Component getTabRendererComponent(JTabbedPane tabbedPane, String text, Icon icon, int tabIndex);

    }

    abstract class AbstractTabRenderer implements ITabRenderer {

        private String prototypeText = "";
        private Icon prototypeIcon = UIManager.getIcon("OptionPane.informationIcon");
        private int horizontalTextAlignment = SwingConstants.CENTER;
        private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);

        public AbstractTabRenderer() {
            super();
        }

        public void setPrototypeText(String text) {
            String oldText = this.prototypeText;
            this.prototypeText = text;
            firePropertyChange("prototypeText", oldText, text);
        }

        public String getPrototypeText() {
            return prototypeText;
        }

        public Icon getPrototypeIcon() {
            return prototypeIcon;
        }

        public void setPrototypeIcon(Icon icon) {
            Icon oldIcon = this.prototypeIcon;
            this.prototypeIcon = icon;
            firePropertyChange("prototypeIcon", oldIcon, icon);
        }

        public int getHorizontalTextAlignment() {
            return horizontalTextAlignment;
        }

        public void setHorizontalTextAlignment(int horizontalTextAlignment) {
            this.horizontalTextAlignment = horizontalTextAlignment;
        }

        public PropertyChangeListener[] getPropertyChangeListeners() {
            return propertyChangeSupport.getPropertyChangeListeners();
        }

        public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
            return propertyChangeSupport.getPropertyChangeListeners(propertyName);
        }

        public void addPropertyChangeListener(PropertyChangeListener listener) {
            propertyChangeSupport.addPropertyChangeListener(listener);
        }

        public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
            propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
        }

        protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
            PropertyChangeListener[] listeners = getPropertyChangeListeners();
            for (int i = listeners.length - 1; i >= 0; i--) {
                listeners[i].propertyChange(new PropertyChangeEvent(this, propertyName, oldValue, newValue));
            }
        }
    }

    class DefaultTabRenderer extends AbstractTabRenderer implements PropertyChangeListener {

        private Component prototypeComponent;

        public DefaultTabRenderer() {
            super();
            prototypeComponent = generateRendererComponent(getPrototypeText(), getPrototypeIcon(), getHorizontalTextAlignment());
            addPropertyChangeListener(this);
        }

        private Component generateRendererComponent(String text, Icon icon, int horizontalTabTextAlignmen) {
            JPanel rendererComponent = new JPanel(new GridBagLayout());
            rendererComponent.setOpaque(false);

            GridBagConstraints c = new GridBagConstraints();
            c.insets = new Insets(2, 4, 2, 4);
            c.fill = GridBagConstraints.HORIZONTAL;
            rendererComponent.add(new JLabel(icon), c);

            c.gridx = 1;
            c.weightx = 1;
            rendererComponent.add(new JLabel(text, horizontalTabTextAlignmen), c);

            return rendererComponent;
        }

        @Override
        public Component getTabRendererComponent(JTabbedPane tabbedPane, String text, Icon icon, int tabIndex) {
            Component rendererComponent = generateRendererComponent(text, icon, getHorizontalTextAlignment());
            int prototypeWidth = prototypeComponent.getPreferredSize().width;
            int prototypeHeight = prototypeComponent.getPreferredSize().height;
            rendererComponent.setPreferredSize(new Dimension(prototypeWidth, prototypeHeight));
            return rendererComponent;
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            String propertyName = evt.getPropertyName();
            if ("prototypeText".equals(propertyName) || "prototypeIcon".equals(propertyName)) {
                this.prototypeComponent = generateRendererComponent(getPrototypeText(), getPrototypeIcon(), getHorizontalTextAlignment());
            }
        }
    }
}

Screenshots

MetalLookAndFeel

enter image description here

NimbusLookAndFeel

enter image description here

SeaglassLookAndFeel

enter image description here

WindowsLookAndFeel

enter image description here

Nellanellda answered 11/10, 2014 at 19:50 Comment(3)
Very Nice i will give a try, very nice man, thanks for all :)!Peritoneum
+1 Very thorough and well-documented; apologies in advance for my leaden editorial hand.Seabolt
On the contrary, thank you very much for taking the time to read and improve my answer :) @SeaboltNellanellda
S
2

It's not clear from your fragment where the alignment is going awry. TabComponentsDemo is a complete example that illustrates how to create Tabs With Custom Components. In ButtonTabComponent, note how the component is given a FlowLayout having FlowLayout.LEFT alignment. You might compare this with your current approach.

Seabolt answered 11/10, 2014 at 11:56 Comment(1)
+1 because referencig tutorials is always useful. However IMHO the problem is deeper although it's not well described in the question. Of course in tutorials all things go well and we are happy but there's a design problem about tabbed pane and custom tab components. Could you please take a look to my answer? I really appreciate that.Nellanellda
P
2

There is a simpler solution using HTML formatting:

final String PRE_HTML = "<html><p style=\"text-align: left; width: 230px\">"; 
final String POST_HTML = "</p></html>"; 

tabbedpane.setTitleAt(0, PRE_HTML + "your title" + POST_HTML);
tabbedpane.setTitleAt(2, PRE_HTML + "your title 2" + POST_HTML);
Pinprick answered 18/11, 2015 at 13:15 Comment(2)
this is the best solutionClaritaclarity
The difficult part is determining the width. Too small and it won't achieve the desired result. Too large, and the tabs will be partially hidden behind the selected component. I found that using the largest width of JLabel created with the title text, icon, etc, works well (at least in the metal theme, and several others).Unthinkable
R
1

what I did was to add a component (a JPanel in this case) for the tab. I needed to add a checkbox to the tabs so instead of a checkbox you could add the Icon manually.

here's my code:

private JPanel makeTabPanel(JCheckBox checkBox) {
    String title = checkBox.getText();
    checkBox.setText("");
    JPanel pane = new JPanel();
    pane.setLayout(new BoxLayout(pane, BoxLayout.LINE_AXIS));
    pane.add(checkBox);
    pane.add(new JLabel(title, SwingUtilities.LEFT));
    pane.setMinimumSize(new Dimension(150, 25));
    pane.setPreferredSize(new Dimension(180, 25));
    pane.setMaximumSize(new Dimension(220, 25));
    return pane;
    }
     //then I call it with
    tabbed.setTabComponentAt(0, makeTabPanel(checkIncluderesume));

I know it's not good practice to add size to the panels, but this is the easiest method I could find. The results are:

sample

Rashad answered 14/9, 2016 at 19:13 Comment(0)
A
0
tabbedPane.setTabComponentAt(idx, new JLabel(title, JLabel.LEFT));

This does not work because the JLabel's size fits to the string it displays. See image below. I drew line borders around the JLabels to make their size more obvious:

Swing window with tabs titles center aligned

I got around this by setting the preferredSize of all JLabels to be the same as the longest JLabel.

You will have to do your own code to find the longest title, but once you get it, you can do this:

JLabel prototypeLabel = new JLabel("The longest tab title");
        
for(int i = 0; i < tabbedPane.getTabCount(); ++i) {
    String title = tabbedPane.getTitleAt(i);
    JLabel label = new JLabel(title, JLabel.LEFT);
    label.setPreferredSize(prototypeLabel.getPreferredSize());

    tabbedPane.setTabComponentAt(i, label);
}

Swing window with tabs titles left aligned

Alow answered 14/9, 2022 at 20:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.