Adding tab close buttons to a JTabbedPane in Java Swing

Any Java developer who works with tabbed panes in their applications knows that the basic JTabbedPane doesn’t provide out-of-the-box functionality to close tabs. Ten years ago that was fine, but these days we expect to be able to click little buttons on our tabs and close them. Until Java 6, it just wasn’t possible to add buttons to tabs. These days, we have the ability to add our own customizations to the tab via the setTabComponentAt method.

There are several examples of how to do this on the web, but most of them show the basics of adding a button to a tab and leave it at that.

I wanted to have an easy way to add the  to any tab, while still allowing a custom tab icon and text, and I wanted the close-tab button to support rollover effects, so that it is only red when you hover over it.

Here a short sample program that demonstrates using a simple method to decorate tabs with little close buttons.

Getting the Necessary Pieces

I did an image search for a suitable icon and found a nice little 11×11 red close-tab icon at this site, from the open source Notepad++ program. I went to Icon Finder for the example Edit Page icon.

In order to easily render a grayscale version of the button icon, for rollover, I used the RGBGrayFilter utility class from JGoodies.

This example also uses the popular Plastic look and feel, so you will need to download both JGoodies Common and JGoodies Looks from their download page.

Note that the JGoodies stuff is optional—I used it for the look and for the grayscale utility, but you can easily just provide two different versions of the icon for your tab button.

The Code 

package tab.demo;

import com.jgoodies.looks.common.*;
import com.jgoodies.looks.plastic.*;
import com.jgoodies.looks.plastic.theme.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * A demo class for showing how to add close buttons to tabs.
 *
 * @author Tad Harrison
 */
public class TabDemoFrame extends javax.swing.JFrame {

  private static final Icon CLOSE_TAB_ICON = new ImageIcon(TabDemoFrame.class.getResource("closeTabButton.png"));
  private static final Icon PAGE_ICON = new ImageIcon(TabDemoFrame.class.getResource("1353549278_page-edit.png"));
  private int tabCount = 0;

  /**
   * Creates new TabDemoFrame
   */
  public TabDemoFrame() {
    initComponents();
  }

  /**
   * Adds a component to a JTabbedPane with a little "close tab" button on the
   * right side of the tab.
   *
   * @param tabbedPane the JTabbedPane
   * @param c any JComponent
   * @param title the title for the tab
   * @param icon the icon for the tab, if desired
   */
  public static void addClosableTab(final JTabbedPane tabbedPane, final JComponent c, final String title,
          final Icon icon) {
    // Add the tab to the pane without any label
    tabbedPane.addTab(null, c);
    int pos = tabbedPane.indexOfComponent(c);

    // Create a FlowLayout that will space things 5px apart
    FlowLayout f = new FlowLayout(FlowLayout.CENTER, 5, 0);

    // Make a small JPanel with the layout and make it non-opaque
    JPanel pnlTab = new JPanel(f);
    pnlTab.setOpaque(false);

    // Add a JLabel with title and the left-side tab icon
    JLabel lblTitle = new JLabel(title);
    lblTitle.setIcon(icon);

    // Create a JButton for the close tab button
    JButton btnClose = new JButton();
    btnClose.setOpaque(false);

    // Configure icon and rollover icon for button
    btnClose.setRolloverIcon(CLOSE_TAB_ICON);
    btnClose.setRolloverEnabled(true);
    btnClose.setIcon(RGBGrayFilter.getDisabledIcon(btnClose, CLOSE_TAB_ICON));

    // Set border null so the button doesn't make the tab too big
    btnClose.setBorder(null);

    // Make sure the button can't get focus, otherwise it looks funny
    btnClose.setFocusable(false);

    // Put the panel together
    pnlTab.add(lblTitle);
    pnlTab.add(btnClose);

    // Add a thin border to keep the image below the top edge of the tab
    // when the tab is selected
    pnlTab.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));

    // Now assign the component for the tab
    tabbedPane.setTabComponentAt(pos, pnlTab);

    // Add the listener that removes the tab
    ActionListener listener = new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        // The component parameter must be declared "final" so that it can be
        // referenced in the anonymous listener class like this.
        tabbedPane.remove(c);
      }
    };
    btnClose.addActionListener(listener);

    // Optionally bring the new tab to the front
    tabbedPane.setSelectedComponent(c);

    //-------------------------------------------------------------
    // Bonus: Adding a <Ctrl-W> keystroke binding to close the tab
    //-------------------------------------------------------------
    AbstractAction closeTabAction = new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        tabbedPane.remove(c);
      }
    };

    // Create a keystroke
    KeyStroke controlW = KeyStroke.getKeyStroke("control W");

    // Get the appropriate input map using the JComponent constants.
    // This one works well when the component is a container. 
    InputMap inputMap = c.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

    // Add the key binding for the keystroke to the action name
    inputMap.put(controlW, "closeTab");

    // Now add a single binding for the action name to the anonymous action
    c.getActionMap().put("closeTab", closeTabAction);
  }

  /**
   * This method is called from within the constructor to initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is always
   * regenerated by the Form Editor.
   */
  @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        tabbedPane = new javax.swing.JTabbedPane();
        jScrollPane1 = new javax.swing.JScrollPane();
        jTextArea1 = new javax.swing.JTextArea();
        buttonPanel = new javax.swing.JPanel();
        createTabButton = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setPreferredSize(new java.awt.Dimension(800, 400));

        jTextArea1.setEditable(false);
        jTextArea1.setColumns(20);
        jTextArea1.setLineWrap(true);
        jTextArea1.setRows(5);
        jTextArea1.setText("This tab cannot be closed.\n\nClick on the \"Add New Tab\" button a few times.\n\nTry closing the tabs with the small buttons.\nTry closing the tabs with <Ctrl-W>.");
        jScrollPane1.setViewportView(jTextArea1);

        tabbedPane.addTab("About", jScrollPane1);

        getContentPane().add(tabbedPane, java.awt.BorderLayout.CENTER);

        createTabButton.setText("Add New Tab");
        createTabButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                createTabButtonActionPerformed(evt);
            }
        });
        buttonPanel.add(createTabButton);

        getContentPane().add(buttonPanel, java.awt.BorderLayout.SOUTH);

        pack();
    }// </editor-fold>                        

  private void createTabButtonActionPerformed(java.awt.event.ActionEvent evt) {                                                
    System.out.println("Add new tab!");
    tabCount++;
    JScrollPane scrollPane = new JScrollPane(new JTextArea("New tab number " + tabCount));
    Icon icon = PAGE_ICON;
    addClosableTab(tabbedPane, scrollPane, "Tab " + tabCount, icon);
  }                                               

  /**
   * @param args the command line arguments
   */
  public static void main(String args[]) {
    try {
      PlasticLookAndFeel laf = new Plastic3DLookAndFeel();
      PlasticLookAndFeel.setCurrentTheme(new ExperienceBlue());
      UIManager.setLookAndFeel(laf);
    } catch (javax.swing.UnsupportedLookAndFeelException ex) {
      ex.printStackTrace();
    }

    java.awt.EventQueue.invokeLater(new Runnable() {
      @Override
      public void run() {
        new TabDemoFrame().setVisible(true);
      }
    });
  }
    // Variables declaration - do not modify                     
    private javax.swing.JPanel buttonPanel;
    private javax.swing.JButton createTabButton;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JTextArea jTextArea1;
    private javax.swing.JTabbedPane tabbedPane;
    // End of variables declaration                   
}

How it Works

The static method addClosableTab will add a panel (or other component) to a JTabbedPane with a “close button” tab containing an icon and a text label.

The parameters are final, so that they can be accessed from within the anonymous classes used for listeners.

Lines 40-42 add the payload to the tabbed pane, without any label.

Lines 44-53 create a small panel that will contain everything shown on the tab itself. This area is circled in red below.

Lines 55-68 create the close-tab button.

Lines 70-76 put all the bits together in the tab panel.

Line 79 puts the panel into the tab using the aforementioned setComponentAt method.

Lines 81-90 add a listener to the button that will close the tab when it is clicked.

Line 91 brings the new tab to the front.

Optional bonus…

Lines 95-116 add a keystroke handler so you can close the tab with Control-W.

Additional Notes

As I mentioned earlier, I didn’t want to have to make my own “unselected” version of the button, since I might be trying different variations of buttons. Fortunately, JGoodies provides a handy-dandy utility class that they use to make gray icons for their toolbars. In line 62 I use the RGBGrayFilter class to make a grayed-out version of the button icon.

The actual placement of the labels on the tab depends heavily on the look and feel you are using. I found that I had to tweak the settings of the tab panel’s layout and border in order for it to look nice when using the Plastic look and feel. If I were using a different look and feel, I would probably have to tweak the borders.

I noticed that when a tab is selected, its label is raised up a few pixels. Without tweaking borders and opacity, this resulted in the tab panel being painted over the top edge of the tab, resulting in behavior like this:

Similar strange behavior happens if you omit some of the other border tweaks.

The action listener I use is quite simplistic. In a real application, you probably want to ask the content of the tab if it can be closed, to make sure the user doesn’t lose any changes.

Some apps, like Safari, hide the button altogether until the mouse hovers over it. To achieve this, simply use a blank icon for the normal (non rollover) button icon.

Finally, if you use the Control-W tab close hot key, note that some components (I’m looking at you, JTable), swallow keystrokes in ways that make it difficult for higher components to see the keystroke. In addition, if you run your app on a Mac, you would probably want to map the close command to Command-W instead of Control-W.

I hope this helps somebody!

 

11 Responses to “Adding tab close buttons to a JTabbedPane in Java Swing”

  1. Rajakrishna Reddy writes:

    Really very nice post … !!! Good help!!

    But one thing … Why is it not properly working with JTabbedPane#SCROLL_TAB_LAYOUT option !!????

  2. Chp writes:

    Extremely useful..many thanks for posting!!!

  3. Renata writes:

    Thank you a lot. Really well explained! It has helped a lot!

  4. Maduka writes:

    Thank you…
    Try to add more examle for JavaSE Plz….

  5. Moosh writes:

    Great! Helped me a lot. thanx

  6. Orgenetnom writes:

    Hey! Can someone figure out a way to do it from within the tabbedpane class? I think that would be very interesting, don’t you?

  7. Orgenetnom writes:

    Very nice and useful example, by the way.

  8. Michel Tank writes:

    Thank! Very good!

  9. Neal Bamford writes:

    Hi Tad

    Awesome bit of code thanks for sharing :-)

  10. Thiago writes:

    This is pretty awesome.

  11. Richard writes:

    Tremendous post, thank you so much!

Leave a Reply