I use the Substance Look & Feel in most of my Java Swing GUIs these days. It’s a very well engineered, mature, configurable implementation of a Swing look & feel which can transform the appearance of any Swing GUI. The author Kirill Grouchnikov has done a great job with it over the years and I’d like to thank him for his work on this project (and his other projects such as the Trident animation library and Flamingo component suite).
Although Substance is extremely configurable, one area that I’ve had problems with in the past is that of custom table cell renderers. Substance provides default renderers for common cell value classes such as strings, numbers, booleans etc. which covers most use cases. However, if you want to customise the cell renderer behaviour and still benefit from all of the Substance effects such as row striping, selection highlighting animation etc. you’re quite limited because Substance enforces that your custom renderer is sub-classed from the SubstanceDefaultTableCellRenderer, which itself sub-classes DefaultTableCellRenderer. This is fine as long as your custom renderer is happy to be, effectively, a sub-class of JLabel, but for anything involving custom painting it’s no good.
This is one area that I think Substance could possibly be improved to make it a bit more extensible and flexible.
One specific situation I encountered this problem with recently was where I wanted to display a mini bar-chart in a table cell. The actual custom bar component was a trivial amount of custom painting but I couldn’t see an easy way of getting this into a Substance table cell renderer. I tried creating a simple cell renderer which extended the bar component and implemented the obligatory TableCellRenderer interface, and although this worked to a degree, I lost all of the nice Substance effects mentioned above by virtue of the fact that the renderer wasn’t derived from SubstanceDefaultTableCellRenderer. I managed to get row background striping back quite easily by calling the SubstanceStripingUtils.applyStripedBackground() method but that wasn’t good enough.
Then I had a mini flash of inspiration triggered when I noticed that Substance provided a default renderer for icons…
Because the Substance default renderer is based on a label, I could set the icon property to an implementation of the javax.swing.Icon interface to get my custom painting “injected” into the renderer. The steps involved were:
- Make my custom bar component implement the Icon interface. This was simply a case of moving my custom painting code from the paintComponent method to paintIcon and also providing implementations of the getIconWidth and getIconHeight methods.
- Create a simple SubstanceDefaultTableCellRenderer sub-class which wraps an instance of my bar component. In the renderer constuctor I call setIcon to attach the bar component to the renderer. The renderer also needs to override the two setBounds methods so that it can set the size of the wrapped bar component, otherwise it wouldn’t resize with the table cell! Oh, and of course an overridden setValue method needs to update the wrapped bar component value as appropriate.
As a result, my two classes looked something like this:
BarIconComponent
package com.purplegem.substanceplay; import javax.swing.*; import java.awt.*; /** * Simple custom component which renders a mini bar chart. * Implements the Icon interface so it can be used where an Icon can. */ public class BarIconComponent extends JComponent implements Icon { private double value = 0.5d; public BarIconComponent() { setSize(40, 20); } public double getValue() { return value; } public void setValue(double value) { this.value = value; } @Override public void paintIcon(Component c, Graphics g, int x, int y) { Graphics2D g2 = (Graphics2D) g; g2.setColor(Color.GREEN); g2.fillRect(0, 0, (int) (getIconWidth() * value), getIconHeight()); } @Override public int getIconWidth() { return getWidth(); } @Override public int getIconHeight() { return getHeight(); } @Override protected void paintComponent(Graphics g) { paintIcon(this, g, 0, 0); } }
BarCellRenderer
package com.purplegem.substanceplay; import org.pushingpixels.substance.api.renderers.SubstanceDefaultTableCellRenderer; import java.awt.*; /** * Custom Substance table cell renderer which renders a mini bar * chart using the wrapped BarIconComponent instance */ public class BarCellRenderer extends SubstanceDefaultTableCellRenderer { private BarIconComponent barComponent = new BarIconComponent(); public BarCellRenderer() { // attach our bar component as an icon setIcon(barComponent); } @Override public void setBounds(int x, int y, int width, int height) { super.setBounds(x, y, width, height); // ensure we update the size of the wrapped bar component barComponent.setSize(width, height); } @Override public void setBounds(Rectangle r) { super.setBounds(r); // ensure we update the size of the wrapped bar component barComponent.setSize((int) r.getWidth(), (int) r.getHeight()); } @Override protected void setValue(Object value) { if (value != null) { // update the value of the bar component for this particular table cell barComponent.setValue((Double) value); } } }
Once I’d implemented all of this I then had exactly the result I was expecting – a mini bar chart in a table cell with all the expected Substance table cell effects š