What is the proper separation of responsibilities between TableCellRenderer and TableModel when using JTables?

StackOverflow https://stackoverflow.com/questions/17773894

Question

I am working on a part of an application that displays tables with statistical data about video files represented by the class FrameInfo. Now after I initially just had a table model that would do everything including some formatting, I refactored it to the other extreme and had the table model only return FrameInfo instance for each row and then let the CellRenderer decide what field to render and how for each column. This was great as I could do nice things like switching the display of e.g. timecode values between ticks, seconds or timecodes ("00:01:02:03") only by a repaint. I was happy until I copied and pasted table contents into a gdocs spreadsheet and noticed that I only got the output of toString() of my model objects in all cells (which was logical when I started thinking about it but obviously not what I want).

My options, as far as I can see them now:

1) Put everything back into the model

Pros: I would have everything in the clipboard as it is displayed, when I copy

Cons: - means triggering model events when switching the display mode of timecodes - writing highlighters (I am using JXTables btw.) would become messy again, as I would have to do string matching, where I now can use my model objects

2) Leave as it is now and build a custom copy action that uses the renderer and then extracts the text from the rendered label

Pros: - Table code stays clean

Cons: - Amount of work(?) - For stuff like rounded number I would lose accuracy

3) Put all but the dynamic stuff (timecodes) into the model and do the timecodes in the renderer and live with the fact that I do not get WYSIWYG for copy & paste for those columns

Pros & Cons: - More or less a half-assed compromise

Any advice or maybe even some exiting code that I could use, anyone?

Thanks for your time!

Was it helpful?

Solution

Extending on @trashgod's answer: option 1 is plain wrong :-) A TableModel must contain the data, nothing else. It's a renderer's exclusive job to present the data in a table (in fact, in any of Swing's collection views). And it's a TransferHandler's job to export the data in a reasonable form, preferably with the same string representation as the renderer.

JXTable makes it particularly easy to share the string representation between collaborators: the small coin to produce textual content is called StringValue, with which all internal renderers are configured. Once configured, that string is used across all string-related extended functionality, like searching, sorting, regex-based filtering, and table's api:

String text = table.getStringAt(row, column);

Which allows a custom TransferHandler to base its string-building on:

/**
 * A TableTransferable that uses JXTable string api to build
 * the exported data.
 * 
 * C&p from BasicTableUI, replaced toString with 
 * table.getStringAt(row, col)
 */
public static class XTableTransferHandler extends TransferHandler {

    /**
     * Create a Transferable to use as the source for a data transfer.
     * 
     * @param c The component holding the data to be transfered. This
     *        argument is provided to enable sharing of TransferHandlers by
     *        multiple components.
     * @return The representation of the data to be transfered.
     * 
     */
    @Override
    protected Transferable createTransferable(JComponent c) {
        if (!(c instanceof JXTable))
            return null;
        JXTable table = (JXTable) c;
        int[] rows;
        int[] cols;

        if (!table.getRowSelectionAllowed()
                && !table.getColumnSelectionAllowed()) {
            return null;
        }

        if (!table.getRowSelectionAllowed()) {
            int rowCount = table.getRowCount();

            rows = new int[rowCount];
            for (int counter = 0; counter < rowCount; counter++) {
                rows[counter] = counter;
            }
        } else {
            rows = table.getSelectedRows();
        }

        if (!table.getColumnSelectionAllowed()) {
            int colCount = table.getColumnCount();

            cols = new int[colCount];
            for (int counter = 0; counter < colCount; counter++) {
                cols[counter] = counter;
            }
        } else {
            cols = table.getSelectedColumns();
        }

        if (rows == null || cols == null || rows.length == 0
                || cols.length == 0) {
            return null;
        }

        StringBuffer plainBuf = new StringBuffer();
        StringBuffer htmlBuf = new StringBuffer();

        htmlBuf.append("<html>\n<body>\n<table>\n");

        for (int row = 0; row < rows.length; row++) {
            htmlBuf.append("<tr>\n");
            for (int col = 0; col < cols.length; col++) {
                // original:
                // Object obj = table.getValueAt(rows[row], cols[col]);
                // String val = ((obj == null) ? "" : obj.toString());
                // replaced by JXTable api:
                String val = table.getStringAt(row, col);
                plainBuf.append(val + "\t");
                htmlBuf.append("  <td>" + val + "</td>\n");
            }
            // we want a newline at the end of each line and not a tab
            plainBuf.deleteCharAt(plainBuf.length() - 1).append("\n");
            htmlBuf.append("</tr>\n");
        }

        // remove the last newline
        plainBuf.deleteCharAt(plainBuf.length() - 1);
        htmlBuf.append("</table>\n</body>\n</html>");

        return new BasicTransferable(plainBuf.toString(),
                htmlBuf.toString());
    }

    @Override
    public int getSourceActions(JComponent c) {
        return COPY;
    }

}

Example of usage:

DefaultTableModel model = new DefaultTableModel(
        new String[]{"Action"}, 0);
JXTable table = new JXTable(model);
Object[] keys = table.getActionMap().allKeys();
for (Object key : keys) {
    model.addRow(new Object[]{table.getActionMap().get(key)});
}
StringValue sv = new StringValue() {

    @Override
    public String getString(Object value) {
        if (value instanceof Action) {
            return (String) ((Action) value).getValue(Action.NAME);
        }
        return StringValues.TO_STRING.getString(value);
    }

};
table.getColumn(0).setCellRenderer(new DefaultTableRenderer(sv));
table.setDragEnabled(true);
table.setTransferHandler(new XTableTransferHandler());

OTHER TIPS

Your TableModel should contain data, and the chosen renderer should format a cell's content. As shown here, you can apply the desired domain-specific rendering in your custom Transferable. Use java.text.MessageFormat to let both your TableCellRenderer and Transferable apply the same formatting to data obtained from the model.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top