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:
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:
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:
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:
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:
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:
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:
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 theinstallDefaults
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);
}
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;
}
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:
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:
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 < width =" selectedRect.width">
If we run our sample application now, we would see the following:
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:
Step 12: Removing the content area insets
Implementing this turns out to be quite simple as well. All we need to do is override thegetContentBorderInsets
method: protected Insets getContentBorderInsets(int tabPlacement)
{
return new Insets(2, 0, 0, 0);
}
If we run our sample application now, we would see the following:
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.