Jun 08

In my opinion one of the most underappreciated (and perhaps underused) features of Swing is the ability to create custom UI delegates for existing controls. It seems to me that most of the time new delegates are only created as part of developing a new complete look and feel, however I think they could be better leveraged to help add polish to existing applications.

For example, if you look at an application like Adobe Photoshop, in order to save space in their palettes, they use a small tab control. The same goes for most of the Microsoft Office products. They each contain customized tab controls that better integrate into the confinements of their respective user interfaces. Functionally, these behave like a standard tab controls, however the look and feel of these are different.

Over the years, I’ve seen many custom Swing controls implemented where the developers have re-implemented all that logic that already exists in an existing control with the only purpose of creating a version of that control that looks different. In fact, I know that I am guilty of doing that as well. Doesn’t it seem stupid to have to reimplement the logic for a button, a tab control, a checkbox or whatever control you are trying to create if your only goal is to change the way it looks?

Luckily for us Swing developers, there is a way just to address this exact problem. :-)

I thought a good (and simple) example of how to implement a custom UI delegate would be to create an implementation for JTabbedPane that makes it look like the tabs used in the palletes of Adobe Photoshop:

An example of how the tabs look in an Adobe Photoshop palette

These tabs are simple enough, that we can implement this with little effort, and it will be (hopefully) a good example of how to create your own UI delegate.

Step 1: Create a new delegate class

If you’ve ever dug into the implementation of the different look and feels of Swing, you see that all the look and feels extend a basic look and feel implementation (in the javax.swing.plaf.basic package). These “basic” implementations (generally) break down the drawing of the respective controls into smaller units to make it easier to create a new delegates.

Since we want to create a new delegate for JTabbedPane, our new class needs to extend the BasicTabbedPaneUI class:

import javax.swing.plaf.basic.BasicTabbedPaneUI;

public class PSTabbedPaneUI extends BasicTabbedPaneUI
{

}

Believe it or not, this is actually enough to start using this “custom” delegate in a real application. In the next step, we will create a test application that we can use to see the transformation as we implement our look and feel.

Step 2: Create a small application to test our tabs

In order to test our new delegate, we need to create a small application that will use it. In order to get a good idea of how the delegate behaves, we will create a panel with three tabs in it. Tab 1 will contain a standard JPanel; Tab 2 will contain a JPanel with a black, 2 pixel border; and Tab 3 will contain a JButton. The reason we will do this, is that we can see the changes to the border of the content area of the tab, as well as the insets of the content area.

Here is the code that we will use for our sample application:

public class TestPSTabbedPaneUI
{
	public static void main(String[] args)
	{
		try
		{
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		}
		catch (Exception exc)
		{
			// Do nothing...
		}

		JFrame vFrame = new JFrame();
		vFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		vFrame.setSize(200, 200);
		JTabbedPane vTab = new JTabbedPane();
		vTab.setUI(new PSTabbedPaneUI());

		vTab.add("One", new JPanel());

		JPanel vPanel2 = new JPanel();
		vPanel2.setBorder(BorderFactory.createLineBorder(Color.BLACK,2));
		vTab.add("Two", vPanel2);

		vTab.add("Three", new JButton("three"));

		vFrame.getContentPane().add(vTab);
		vFrame.setTitle("Tabs Example");
		vFrame.show();
	}
}

If we would run this right now, we would see the following:

 

As you can see, the application runs and is usable, but the tabs just look like standard (older) Windows style tabs. We can now begin with the fun stuff, actually changing the way the tabs are presented.

Step 3: Customize the way the tabs are drawn

In order to change the way the tabs are drawn, we need to override the paintTabBorder method. If you look closely at the the tabs in Photoshop, you will see that in addition to having a border, there is a “beveled” look to the selected tab. It has a white line inside the left and top edge of the, and with a darker gray line inside of the right hand (angled) side. We will implement this beveled look in the paintTabBorder method as well:

	protected void paintTabBorder(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected)
	{
		g.setColor(Color.BLACK);
		g.drawLine(x, y, x, y + h);
		g.drawLine(x, y, x + w - (h / 2), y);
		g.drawLine(x + w - (h / 2), y, x + w + (h / 2), y + h);

		if (isSelected)
		{
			g.setColor(Color.WHITE);
			g.drawLine(x + 1, y + 1, x + 1, y + h);
			g.drawLine(x + 1, y + 1, x + w - (h / 2), y + 1);

			g.setColor(shadow);
			g.drawLine(x + w - (h / 2), y + 1, x + w + (h / 2)-1, y + h);
		}
	}

If we would run this right now, we would see the following:

 

It’s still pretty ugly, but as you can see, by changing overriding one method, we already have made a drastic change to the way it looks.

Step 4: Customize the way the tabs are painted

If you look at the selected tab in the screenshots from step 3, you can see that you can see the edge of the border of the adjacent tabs. The next thing we will want to do is clean that up. In order to do that, we can extend the paintTabBackground method. In this method, we will simply create a polygon which is the shape of the tab and fill it with the background color of the tab pane:

	protected void paintTabBackground(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected)
	{
		Polygon shape = new Polygon();

		shape.addPoint(x, y + h);
		shape.addPoint(x, y);
		shape.addPoint(x + w - (h / 2), y);

		if (isSelected || (tabIndex == (rects.length - 1)))
		{
			shape.addPoint(x + w + (h / 2), y + h);
		}
		else
		{
			shape.addPoint(x + w, y + (h / 2));
			shape.addPoint(x + w, y + h);
		}

		g.setColor(tabPane.getBackground());
		g.fillPolygon(shape);
	}

If we would run this right now, we would see the following:

 

With that simple change, the tabs themselves look a lot cleaner and a lot more polished, however there is still alot we need to do finish things off.

Step 5: Supress the painting of the focus indicator

If you look at the Photoshop screenshot, there is no focus indicator for the tabs. Additionally, if you look at the screenshots from Step 4, you will notice that that the focus indicators are rectangular, while the buttons are not. In order to simulate the way the Photoshop tabs are implemented, we are going to supress the painting of the focus indicator. In order to do this, we just need to override the paintFocusIndicator method and have it do nothing:

	protected void paintFocusIndicator(Graphics g, int tabPlacement, Rectangle[] rects, int tabIndex, Rectangle iconRect, Rectangle textRect, boolean isSelected)
	{
		// Do nothing
	}

If we run our sample application now, we would see the following:

 

Without the focus indicator painted, it looks alot more like the original control we are copying.

Step 6: Change the insets of the tab bar

If look at the screenshot of the Photoshop tabs, you will notice that there is a 4 pixel gap between the left edge of the first tab (”Layers”) and the left hand side of the container. In our current version, the left hand side of the tab is directly agains the left edge of the container.

Additionally, if you look at our current version, you will see that the text for the tabs are inset a few pixels farther from the top edge of the tab than in the original Photoshop tab.

In order to fix this, we need to override the installDefaults method and change the default values for the insets. To fix problem number 1, we will need to modify the tabAreaInsets field, and to fix problem number 2, we will need to modify the selectedTabPadInsets field and the tabInsets field:

	protected void installDefaults()
	{
		super.installDefaults();
		tabAreaInsets.left = 4;
		selectedTabPadInsets = new Insets(0, 0, 0, 0);
		tabInsets = selectedTabPadInsets;
	}

If we run our sample application now, we would see the following:

 

While we fixed the problem with the insets, the changes make our tabs look alot worse than before. In order to make them look better, we need a way to specify their size.

Step 7: Specify the size of the tabs

In order to specify the size of the tabs we need to override the calculateTabHeight method and the calculateTabWidth method. We can also use the calculateTabHeight method to enforce the fact that a tab will always be a height that is divisible by two. This will make sure that the angled line on the right hand side of the tabs always looks good (unlike the screenshots from the previous step).

	protected int calculateTabHeight(int tabPlacement, int tabIndex, int fontHeight)
	{
		int vHeight = fontHeight;
		if (vHeight % 2 > 0)
		{
			vHeight += 1;
		}
		return vHeight;
	}

	protected int calculateTabWidth(int tabPlacement, int tabIndex, FontMetrics metrics)
	{
		return super.calculateTabWidth(tabPlacement, tabIndex, metrics) + metrics.getHeight();
	}

If we run our sample application now, we would see the following:

 

These changes now give our tabs the right proportions as compared to the real tabs in Photoshop. We can now turn our attention to the text of the tabs.

Step 8: Change the way the text is drawn

In Photoshop, the selected tab always has it’s text drawn in bold. Additionally, if you look at the text of all of the tabs in Photoshop, you will see they are all drawn at the same y location. In our tabs, the text for the selected tab is always drawn 2 pixels higher than the rest. In order to fix these problems, we will need to override a few methods.

First of all, we need to create the bold font to use to draw the selected text. We can create that font in the installDefaults method where we specified the custom insets:

	protected void installDefaults()
	{
		super.installDefaults();
		tabAreaInsets.left = 4;
		selectedTabPadInsets = new Insets(0, 0, 0, 0);
		tabInsets = selectedTabPadInsets;

		boldFont = tabPane.getFont().deriveFont(Font.BOLD);
		boldFontMetrics = tabPane.getFontMetrics(boldFont);
	}

Note: In a “real” delegate, you would additionally want to listen for the font property of the tabPane to change so that you could update the cached bold font. For our simple example, we’ll overlook that small detail to simplify things.

Next, in order to prevent the text of the selected tab to be drawn at a different y location than the unselected tabs, we need to override the getTabLabelShiftY method:

	protected int getTabLabelShiftY(int tabPlacement, int tabIndex, boolean isSelected)
	{
		return 0;
	}

Finally, in order to paint the text in a bold font for the selected text, we need to override the paintText method:

	protected void paintText(Graphics g, int tabPlacement, Font font, FontMetrics metrics, int tabIndex, String title, Rectangle textRect, boolean isSelected)
	{
		if (isSelected)
		{
			int vDifference = (int)(boldFontMetrics.getStringBounds(title,g).getWidth()) - textRect.width;
			textRect.x -= (vDifference / 2);
			super.paintText(g, tabPlacement, boldFont, boldFontMetrics, tabIndex, title, textRect, isSelected);
		}
		else
		{
			super.paintText(g, tabPlacement, font, metrics, tabIndex, title, textRect, isSelected);
		}
	}

If we run our sample application now, we would see the following:

 

Step 9: Paint the background behind the tabs

If you look at the background behind (and to the right) of the tabs in Photoshop, it is painted a little bit darker than the color of the tabs. In order to implement this, we need to override the paintTabArea method.

Before we do that however, we need to decide what color to paint the background. In order to make this delegate work fairly well with any look and feel, we will simply create a darker version of the background color by calling the darker()<> method on the background color. We can add the creation of this color to the installDefaults method like we’ve done before:

	protected void installDefaults()
	{
		super.installDefaults();
		tabAreaInsets.left = 4;
		selectedTabPadInsets = new Insets(0, 0, 0, 0);
		tabInsets = selectedTabPadInsets;

		Color background = tabPane.getBackground();
		fillColor = background.darker();

		boldFont = tabPane.getFont().deriveFont(Font.BOLD);
		boldFontMetrics = tabPane.getFontMetrics(boldFont);
	}

Now that we have that out of the way, we can implement the paintTabArea method:


	protected void paintTabArea(Graphics g, int tabPlacement, int selectedIndex)
	{
		int tw = tabPane.getBounds().width;

		g.setColor(fillColor);
		g.fillRect(0, 0, tw, rects[0].height + 3);

		super.paintTabArea(g, tabPlacement, selectedIndex);
	}

If we run our sample application now, we would see the following:

 

The color is a little darker than in the original, however by implementing it this way, the delegate should be reusable between any look and feel (and any background color). You’ll also notice that we have the same issue with the font. The font does match up exactly, but by using the default font for the look and feel, we make the delegate more portable.

Step 10: Change the way the top border of the content area is drawn

If you at the screenshot from the previous step where tab “One” is selected, you will notice that there are a few problems with how the top line of the content area is painted. First of all, the Photoshop tabs have a black line there as opposed to a while line. Secondly, there is a bevel effect in the real version, and finally in our (current) version, the lines don’t even match up to the border of the tabs (on the right hand side of the tab where the angle comes down).

In order to fix this, we will need to override the paintContentBorderTopEdge method:

	protected void paintContentBorderTopEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h)
	{
		Rectangle selectedRect = selectedIndex < 0 ? null : getTabBounds(selectedIndex, calcRect);

		selectedRect.width = selectedRect.width + (selectedRect.height / 2) - 1;

		g.setColor(Color.BLACK);

		g.drawLine(x, y, selectedRect.x, y);
		g.drawLine(selectedRect.x + selectedRect.width + 1, y, x + w, y);

		g.setColor(Color.WHITE);

		g.drawLine(x, y + 1, selectedRect.x, y + 1);
		g.drawLine(selectedRect.x + 1, y + 1, selectedRect.x + 1, y);
		g.drawLine(selectedRect.x + selectedRect.width + 2, y + 1, x + w, y + 1);

		g.setColor(shadow);
		g.drawLine(selectedRect.x + selectedRect.width, y, selectedRect.x + selectedRect.width + 1, y + 1);
	}

If we run our sample application now, we would see the following:

 

With those changes, we are now done with the tab area itself. Now, we just need to clean up the borders (actually remove them) of the content area.

Step 11: Removing the borders in the content area

In order to remove the borders of the content area, we just need to override the methods paintContentBorderRightEdge, paintContentBorderLeftEdge and paintContentBorderBottomEdge and have them do nothing:

	protected void paintContentBorderRightEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h)
	{
		// Do nothing
	}

	protected void paintContentBorderLeftEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h)
	{
		// Do nothing
	}

	protected void paintContentBorderBottomEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h)
	{
		// Do nothing
	}

If we run our sample application now, we would see the following:

 

If you look at the screenshot where “One” is selected, you might think we are done with our delegate. However, if you look at the screenshot where tab “Two” is selected, you will notice that the content is still inset a little bit from the edges. In the orignal Photoshop tab, there is no inset, so we still need to remove that before we can be finished.

Step 12: Removing the content area insets

Implementing this turns out to be quite simple as well. All we need to do is override the getContentBorderInsets method:

	protected Insets getContentBorderInsets(int tabPlacement)
	{
		return new Insets(2, 0, 0, 0);
	}

The reason the insets have a pixel height of 2 for the top is that we need to inset for the black line and the white “bevel” line below it.

If we run our sample application now, we would see the following:

 

If you look at the screenshot where the tab “Two” is selected, you can now see that content area extends to the borders exactly like the Photoshop example.

If you want to play with the example yourself, you can download the source code for the delegate here, and you can download the source code for the test application here.

Note: I’m releasing this (and probably all future examples) under an the Apache license, so that there are no questions about whether or not you can use this stuff in real applications if you want.

26 Responses to “Creating a custom UI delegate for JTabbedPane”

  1. Ryan Sonnek Says:

    Great example! Swing needs more helpful tutorials like this to show how to customize an application without making it a total mess of code.

    Although I don’t plan on using this in the near future, THANK YOU for releasing this with an apache license. I fully believe this is the easiest and most effective way of “sharing” this type of work.

  2. James Head Says:

    Top tutorial, - very well documented and excellent you went through each step individually and slowly. More like this please :)

  3. djohnson Says:

    Very nice, keep up the good work.

  4. jawa Says:

    amazing. keep it up.

  5. raj_n Says:

    Very nice, but what happens if you need to wrap or scroll the tabs?

  6. Jon Lipsky Says:

    I thought wrapping and/or scrolling the tabs would 1) make the tutorial a little more complex and 2) I didn’t think it would be needed for this type of tab.

  7. raj_n Says:

    I have the wrapping problem at the moment. When my application resizes my custom JTabbedPane UI delegate doesn’t redraw. Well, it redraws but it is no longer visible. Scrolling gives me null pointer exceptions. I subclassed MetalTabbedPaneUI and overrode the painting code.

  8. Sir Mad Says:

    nice tutorial

    i have done some similiar things myself, but always i think there must be a better way of doing this. I see the following problems:

    1) when u have a possibility in ur application to change l&f, the special UI disappears

    2) when u already use a special l&f like Kunststoff l&f and u want only to change the appereance a little bit, than u have to derivate from KunststoffUI or u have to simulate parts of the behaviour of the Kunststoff l&f. Both things i dont like very well.

    3) If u change the l&f of ur application in the future u have work (according to 2)

    For a special look of a Button if done something different:
    Instead of setting an special UI, i wrote a Sub-Class of JButton and overloaded paintComponent. In this function i called super.paintComponent(g) and after that i did some special painting.
    I though by myself this could be easy extended to something like a “ButtonRenderer” (similiar to a table renderer).
    If the l&f changes in the future, i hope my special button will be unchanged. ;)

  9. Jon Lipsky Says:

    If I want to prevent someone from changing the look and feel of the component, then I create a specific subclass of that component (for example JTabbedPane) which uses the new UI delegate by default. I also override the setUI() method so that it won’t change to another look and feel if someone tries to change it.

  10. Bob van den Hoek Says:

    really great !

    searched a couple of hours or so on the internet to find something really useful …

    thanks a lot, appreciate your tutorial very much!

  11. José Júnior Says:

    Excelent! It really works!! :D

    (not that I doubted of it but… you know).

    Great job. Congratullations ;]

  12. Ferenc Says:

    Great!
    It works. Great job. I appreciate this tutorial very much!

  13. Milind Rao Says:

    As others have said - a really good tutorial. Especially the step by step approach to solving the problems that cropped up. Thanks.

  14. Mandar Says:

    Very nice tutorial.

  15. Kees de Kooter Says:

    Great stuff, very very helpful.

    One question still: how can I change the font color of the selected tab?

  16. Kees de Kooter Says:

    Found it:

    in paintText

    g.setColor(Color.WHITE);
    g.drawString(title, textRect.x, textRect.y + metrics.getAscent());

  17. Kristensson Says:

    Want to thank you for a really good tutorial. The graphical snapshots of the progress with the UI are really great. I have searched so long for some concrete information on coding UI delegates. And although I am not likely to need your special tab pane, I appreciate the fact you took the time to choose a license to wrap up with the code. When you are working within a company it’s a pain to see some small good code that you just can’t use, because the authors released it as “freeware” but without an explicit license.

  18. David Stair Says:

    Absolutely phenomenal tutortial, Jon. Thanks for this! Your clear-cut approach helped ween me into the world of UI manager extensions.

    Have you considered authoring a book to further elucidate on Java’s occult UI variables and functionality?

  19. Asharam Says:

    Great work Sir,
    I am doing similar work and this tutorial gives a straight path to do it.

  20. Sanjay S, Says:

    Great!!!

    This example is very nice and easy to understand.
    Thanks a lot.

  21. indushekhar Says:

    gr8 work .u really came out with what i needed.i have done the same 4 JButton.

  22. Brian Cole Says:

    Nice tutorial–keep up the good work.

    I think there’s still a step to do, though.
    When One is selected Two appears to be
    behind Three. I kept waiting for the step
    in which you fixed that, but it never came.

    I agree that that custom delegates are
    underappreciated/underused. Probably
    because javax.swing.plaf is documented
    poorly. Maybe also because it is daunting
    if you want to keep UI-specific behavior.

  23. Brian Cole Says:

    Oops, that should have been “if you want
    to keep LnF-specific behavior.” Sorry.

  24. Gary Says:

    Thanks, You have really helped me with understanding how to skin swing components. I also have a question, how do can you add a component/s, eg: a button to the left of the first tab or a button along side the scroll buttons.

  25. Serkan ULUÇ Says:

    Nice tutorial, good job! Thank you for sharing…

  26. Elvis Says:

    This is possibly the best tutorial on Swing I’ve found…I especially liked that you took it step by step and pictorially showed what the affects of every change was…Hats off.

    I have a request, I actually landed at this link when I was trying to find a solution of adding close button gifs to tabs…So, it would be awesome if you could extend the tutorial to add such features too.

    Thanks, brilliant work again :)

Leave a Reply