Search code examples
javaswingjtableheader

Custom JTableHeader causes NullPointerException


I've been trying to use the code from Oracle's How To Use Tables to create a JTable with tool tips for each column header. The demo seems to work but whether I paste the code in directly or abstract my own class, I get a NullPointerException on a call to getTableCellRendererComponent() in SynthTableHeaderUI.java line 233. This is due to a call to header.getTable() which returns null on any table I try to setTableHeader() on, even if I setTableHeader(new JTableHeader(tblWhatever.getColumnModel()));

The function I pasted from the demo is inside a custom TableModel that otherwise works very well and looks like this:

public class TestTableModel extends AbstractTableModel {
private final String[] columnNames = {"Name", "Height", "Weight", "Age"};
private final String[] columnToolTips = {"Person's Name",
                                     "Height in centimetres.",
                                     "Weight in kilograms.",
                                     "Age in years as of 2015-Jan-01."};
private ToolTipTableHeader ClientTableHeader; // = new ToolTipTableHeader((new JTable()).getColumnModel(), columnToolTips);

    private final Client[] List = {
        new Client("Abigale", 150, 108, 22),
        new Client("Bob", 180, 175, 36),
        new Client("Charles", 150, 210, 52)
    };

    /*
     * Constructors
     */
    public TestTableModel() {
        super();
    }

    public void setTableHeader(JTable tblClients) {
        tblClients.setTableHeader(createDefaultTableHeader(tblClients.getColumnModel()));
    }

    /*
     * AbstractCellEditor Implementations
     */
    @Override
    public Class getColumnClass(int col) throws java.lang.IndexOutOfBoundsException {
        switch(col) {
            case 0: return String.class;  //.ClientName;
            case 1: return Integer.class; //.Height;
            case 2: return Integer.class; //.Weight;
            case 3: return Integer.class; //.Age;
            default: throw new IndexOutOfBoundsException("Column " + col + ": class not accounted for in " + this.getClass().getName() + ".getColumnClass");
        }
    }

    @Override
    public int getColumnCount() { return columnNames.length; }

    @Override
    public String getColumnName(int col) { return columnNames[col]; }

    @Override
    public int getRowCount() { return List.length; }

    @Override
    public Object getValueAt(int row, int col) throws java.lang.IndexOutOfBoundsException {
        switch(col) {
            case 0: return List[row].ClientName;
            case 1: return List[row].Height;
            case 2: return List[row].Weight;
            case 3: return List[row].Age;
            default: throw new IndexOutOfBoundsException("Column " + col + ": value not accounted for in " + this.getClass().getName() + ".getValueAt");
        }
    }

    @Override
    public boolean isCellEditable(int row, int col) { return true; }

    @Override
    public void setValueAt(Object value, int row, int col) {
        switch(col) {
            case 0: List[row].ClientName = (String) value; break;
            case 1: List[row].Height = (Integer) value; break;
            case 2: List[row].Weight = (Integer) value; break;
            case 3: List[row].Age = (Integer) value; break;
            default: throw new IndexOutOfBoundsException("Column " + col + ": value not accounted for in " + this.getClass().getName() + ".setValueAt");
        }
        fireTableCellUpdated(row, col);
    }

    /*
     * Extensions
     */

    //Implement table header tool tips.
    protected JTableHeader createDefaultTableHeader(TableColumnModel tcmThis) {
        return new JTableHeader(tcmThis) {
            @Override
            public String getToolTipText(MouseEvent e) {
                String tip = null;
                java.awt.Point p = e.getPoint();
                int index = columnModel.getColumnIndexAtX(p.x);
                int realIndex = 
                        columnModel.getColumn(index).getModelIndex();
                return columnToolTips[realIndex];
            }
        };
    }
}

The custom class looks like this:

public class ToolTipTableHeader extends JTableHeader {
    private final String ColumnToolTips[];

    ToolTipTableHeader(TableColumnModel cm, String iniToolTips[]) {
        super(cm);

        if(iniToolTips.length != cm.getColumnCount()) 
            throw new InvalidParameterException("The size of iniToolTips must be precisely equal to the columnModel column count.");
        ColumnToolTips = iniToolTips;
    }

    @Override
    public String getToolTipText(MouseEvent meToolTipEvent) {
        String tip = null;
        if(columnModel == null) return "columnModel == null";
        if(meToolTipEvent == null) return "meMouseEvent == null";
        Point p = meToolTipEvent.getPoint();
        int index = columnModel.getColumnIndexAtX(p.x);
        int realIndex = columnModel.getColumn(index).getModelIndex();
        return ColumnToolTips[realIndex];
    }
}

The initialization is done in a JDialog constructor (the JTable tblTest is created in the designer):

public TestForm(java.awt.Frame parent, boolean modal) {
    super(parent, modal);
    initComponents();

    TestTableModel htmTest = new TestTableModel();
    tblTest.setModel(new TestTableModel());
    htmTest.setTableHeader(tblTest);
}

I notice the constructor for a default JTableHeader doesn't require a JTable be passed to it, and I've implemented a constructor and an overridden getTable() which doesn't seem to be called. As I write this I realize that the demo that works puts the function inside a custom JTable, which I don't want to do because I'm using NetBeans IDE and I don't know a simple way to add a custom table to the designer.

What am I missing? How do I implement this without creating a custom JTable? Thanks for any pointers.


Solution

  • The JTableHeader extension must be added as part of the construction of the JTable. Any subsequent setting of the table header results in the above error. If using NetBeans, go to Design Mode for the dialog/form/panel, select the table in question and click the Code Tab of the Properties window. Add (for example)

    new javax.swing.JTable() {
        //Implement table header tool tips.
        protected JTableHeader createDefaultTableHeader() {
            return new JTableHeader(columnModel) {
                public String getToolTipText(MouseEvent e) {
                    String tip = null;
                    java.awt.Point p = e.getPoint();
                    int index = columnModel.getColumnIndexAtX(p.x);
                    int realIndex =  columnModel.getColumn(index).getModelIndex();
                    return myToolTips[realIndex];
                }
            };
        }
    };
    

    to the Custom Creation Code field and be sure to define

    private final String[] myToolTips = {
        "Column 0 tool tip",
        "Column 1 tool tip",
        // ...
        "Final column tool tip"
    };
    

    somewhere in the dialog/form/panel's class. This works but is still a bit inelegant in that it needs to be done for every form that uses that table model, rather than being able to meld that in and have a single class that sets up the table.