How to create a Look&Feel aware TableCellRenderer?

943 views Asked by At

All the examples I can find of custom TableCellRenderers all seem extend DefaultTableCellRenderer. Nothing wrong with that, I guess, except that you do not get to extend the TableCellRenderer functionality of whatever is the current Look&Feel. This is bad. Your TableCellRenderer is then not L&F aware.

That is in fact exactly what I want to do: I want to create a custom TableCellRenderer that is L&F aware, so rather than extending DefaultTableCellRenderer it should extend (or decorate) whatever is the TableCellRenderer which the L&F has installed by default for the given class type.

I can get to that by doing myJTable.getDefaultRenderer(...) but that will not give me a new instance of that class, it will simply return the L&F's 'master' table cell renderer to be used for that type of class. I started out with a solution that created a custom TableCellRenderer that wrapped the instance that I got from the mentioned method (effectively using a Decorator pattern) until I realized that I didn't have my own private instance of a renderer. If I mess with that instance of renderer I cannot do column-specific rendering as any change I do on that instance will affect more columns than intended.

So I've concluded that what I need to do is to create a completely new instance of the L&F's TableCellRenderer for that class-type. (in my case class type = Object). I believe I can get to the relevant class name from somewhere in UIManager, but I wouldn't know which key to use. Secondly then - unfortunately - I believe I would have to use Reflection to actually instantiate an object of that TableCellRenderer class.

Are my assumptions correct, i.e. there's unfortunately no factory for TableCellRenderers? and how would I actually instantiate a new instance of my current L&F's TableCellRenderer for class-type Object ? (I know how to use Reflection, if that's the only way)

UPDATE

As you may have guessed I'm testing with a proprietary L&F as well as the standard ones. This particulary proprietary L&F installs its own TableCellRenderers which is perfectly legal, IMO. The Java Synth L&F does the same. So I don't want to make this a question of a particular L&F.

Test: I create a table with two String columns. I install my own custom cell renderer on one of the columns, but not the other. My custom renderer looks like this:

public class CustomCellRenderer extends DefaultTableCellRenderer {
    @Override
    public Component getTableCellRendererComponent(
              JTable table, Object value, boolean isSelected, boolean hasFocus,
              int row, int column) {
        return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
    }
}

As you can see the custom renderer does nothing. Since it does nothing I should get the same rendering effect on both table columns. I don't! The reason is of course that by simply extending DefaultTableCellRenderer I'll not 'inherit' the L&F's own TableCellRenderers should it have one/some. On many, many L&F's the above test will in fact render the two columns the same way, but that's more by chance. I want to do things the right way. The fact that extending DefaultTableCellRenderer produces expected results with most L&Fs is not enough for me. :-)

2

There are 2 answers

2
keuleJ On BEST ANSWER

We solved this Problem in like this:

public class CustomCellRenderer  implements TableCellRenderer {
    //Use appropriate class here
    private final TableCellRenderer defaultTableCellRenderer= new JTable().getDefaultRenderer(Object.class);
    @Override
    public Component getTableCellRendererComponent(
              JTable table, Object value, boolean isSelected, boolean hasFocus,
              int row, int column) {
        Component c = defaultTableCellRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        // do custom stuff to c here ...
        return c;
    }
}

If you don't create a new Instance for the TableCellRenderer you get funky effects, because you change the tables default instance.

4
MadProgrammer On

Are my assumptions correct, i.e. there's unfortunately no factory for TableCellRenderers? and how would I actually instantiate a new instance of my current L&F's TableCellRenderer for class-type Object ? (I know how to use Reflection, if that's the only way)

I would say your assumptions are off, for example, the following screen shots use the exact same code for Cross platform/metal, System/MacOS and Nimbus (Synth).

Meta/CrossPlatformSystem/MacOSNimbus

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.Vector;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;

public class Test101 {

    public static void main(String[] args) {
        new Test101();
    }

    public Test101() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
//                  UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
//                  UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
                    UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                DefaultTableModel model = new DefaultTableModel();
                for (int index = 0; index < 10; index++) {
                    model.addColumn(Character.toString((char)('A' + index)));
                }

                for (int row = 0; row < 10; row++) {
                    Vector rowData = new Vector(10);
                    for (int col = 0; col < 10; col++) {
                        rowData.add(row + "x" + col);
                    }
                    model.addRow(rowData);
                }

                JTable table = new JTable(model);
                table.setDefaultRenderer(String.class, new DefaultTableCellRenderer());
                table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer());
                // Just so you don't tell me that the lookup doens't work
                table.getColumnModel().getColumn(0).setCellRenderer(new DefaultTableCellRenderer());

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new JScrollPane(table));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

}

If you extend from AbstractTableCellRenderer or DefaultTableCellRenderer, you should be using either the JTable and it's properties (font, color etc) or the UIManager properties if JTable doesn't provide you the information you need.

So I've concluded that what I need to do is to create a completely new instance of the L&F's TableCellRenderer for that class-type. (in my case class type = Object). I believe I can get to the relevant class name from somewhere in UIManager, but I wouldn't know which key to use. Secondly then - unfortunately - I believe I would have to use Reflection to actually instantiate an object of that TableCellRenderer class.

You would only need to do this if you were actually intending to create your own look and feel. On a everyday basis providing your own TableCellRenderer, which is configured from the properties of the supplied JTable and on a few rare occasions, modifying the properties present in the UIManager should suffice...