Creating a better JTable

May 18, 2009

JTable looks a little rough around the edges straight out of the box. With a couple of tweaks, though, we can make it fit in better on the Mac (and Windows too).

Here’s a screen shot of a JTable that uses no auto-resize (the default on most platforms) with no customization:
jtable_bad

So what’s the most glaring problem here? Well for one, the table header doesn’t extend to the scrollbar. Second, there isn’t a scroll pane corner set on the upper right corner (in a perfect world, the scroll pane corner would merge into the table header, but we’ll let that go for now!).

Below, I’ve set the empty table header space to the right of the right-most column (if there is any) to paint the table header background. I’ve also installed a scroll pane corner and added row striping for effect. Here’s what it looks like:

jtable_good

Not bad.

Below is the implementation. Note that I’ve done the striping in a custom JViewport because it’s much easier that way. Also, the selection intentionally doesn’t span the entire width of the viewport, as the native table selection doesn’t do this.

public class BetterJTable extends JTable {

    private static final Color EVEN_ROW_COLOR = new Color(241, 245, 250);
    private static final Color TABLE_GRID_COLOR = new Color(0xd9d9d9);

    private static final CellRendererPane CELL_RENDER_PANE = new CellRendererPane();

    public BetterJTable(TableModel dm) {
        super(dm);
        init();
    }

    private void init() {
        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        setTableHeader(createTableHeader());
        getTableHeader().setReorderingAllowed(false);
        setOpaque(false);
        setGridColor(TABLE_GRID_COLOR);
        setIntercellSpacing(new Dimension(0, 0));
        // turn off grid painting as we'll handle this manually in order to paint
        // grid lines over the entire viewport.
        setShowGrid(false);
    }

    /**
     * Creates a JTableHeader that paints the table header background to the right
     * of the right-most column if neccesasry.
     */
    private JTableHeader createTableHeader() {
        return new JTableHeader(getColumnModel()) {
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                // if this JTableHEader is parented in a JViewport, then paint the
                // table header background to the right of the last column if
                // neccessary.
                JViewport viewport = (JViewport) table.getParent();
                if (viewport != null && table.getWidth() < viewport.getWidth()) {
                    int x = table.getWidth();
                    int width = viewport.getWidth() - table.getWidth();
                    paintHeader(g, getTable(), x, width);
                }
            }
        };
    }

    /**
     * Paints the given JTable's table default header background at given
     * x for the given width.
     */
    private static void paintHeader(Graphics g, JTable table, int x, int width) {
        TableCellRenderer renderer = table.getTableHeader().getDefaultRenderer();
        Component component = renderer.getTableCellRendererComponent(
                table, "", false, false, -1, 2);

        component.setBounds(0,0,width, table.getTableHeader().getHeight());

        ((JComponent)component).setOpaque(false);
        CELL_RENDER_PANE.paintComponent(g, component, null, x, 0,
                width, table.getTableHeader().getHeight(), true);
    }

    @Override
    public Component prepareRenderer(TableCellRenderer renderer, int row,
                                     int column) {
        Component component = super.prepareRenderer(renderer, row, column);
        // if the rendere is a JComponent and the given row isn't part of a
        // selection, make the renderer non-opaque so that striped rows show
        // through.
        if (component instanceof JComponent) {
            ((JComponent)component).setOpaque(getSelectionModel().isSelectedIndex(row));
        }
        return component;
    }

    // Stripe painting Viewport. //////////////////////////////////////////////

    /**
     * Creates a JViewport that draws a striped backgroud corresponding to the
     * row positions of the given JTable.
     */
    private static class StripedViewport extends JViewport {

        private final JTable fTable;

        public StripedViewport(JTable table) {
            fTable = table;
            setOpaque(false);
            initListeners();
        }

        private void initListeners() {
            // install a listener to cause the whole table to repaint when
            // a column is resized. we do this because the extended grid
            // lines may need to be repainted. this could be cleaned up,
            // but for now, it works fine.
            PropertyChangeListener listener = createTableColumnWidthListener();
            for (int i=0; i<fTable.getColumnModel().getColumnCount(); i++) {
                fTable.getColumnModel().getColumn(i).addPropertyChangeListener(listener);
            }
        }

        private PropertyChangeListener createTableColumnWidthListener() {
            return new PropertyChangeListener() {
                public void propertyChange(PropertyChangeEvent evt) {
                    repaint();
                }
            };
        }

        @Override
        protected void paintComponent(Graphics g) {
            paintStripedBackground(g);
            paintVerticalGridLines(g);
            super.paintComponent(g);
        }

        private void paintStripedBackground(Graphics g) {
            // get the row index at the top of the clip bounds (the first row
            // to paint).
            int rowAtPoint = fTable.rowAtPoint(g.getClipBounds().getLocation());
            // get the y coordinate of the first row to paint. if there are no
            // rows in the table, start painting at the top of the supplied
            // clipping bounds.
            int topY = rowAtPoint < 0
                    ? g.getClipBounds().y : fTable.getCellRect(rowAtPoint,0,true).y;

            // create a counter variable to hold the current row. if there are no
            // rows in the table, start the counter at 0.
            int currentRow = rowAtPoint < 0 ? 0 : rowAtPoint;
            while (topY < g.getClipBounds().y + g.getClipBounds().height) {
                int bottomY = topY + fTable.getRowHeight();
                g.setColor(getRowColor(currentRow));
                g.fillRect(g.getClipBounds().x, topY, g.getClipBounds().width, bottomY);
                topY = bottomY;
                currentRow ++;
            }
        }

        private Color getRowColor(int row) {
            return row % 2 == 0 ? EVEN_ROW_COLOR : getBackground();
        }

        private void paintVerticalGridLines(Graphics g) {
            // paint the column grid dividers for the non-existent rows.
            int x = 0;
            for (int i = 0; i < fTable.getColumnCount(); i++) {
                TableColumn column = fTable.getColumnModel().getColumn(i);
                // increase the x position by the width of the current column.
                x += column.getWidth();
                g.setColor(TABLE_GRID_COLOR);
                // draw the grid line (not sure what the -1 is for, but BasicTableUI
                // also does it.
                g.drawLine(x - 1, g.getClipBounds().y, x - 1, getHeight());
            }
        }
    }

    public static JScrollPane createStripedJScrollPane(JTable table) {
        JScrollPane scrollPane =  new JScrollPane(table);
        scrollPane.setViewport(new StripedViewport(table));
        scrollPane.getViewport().setView(table);
        scrollPane.setBorder(BorderFactory.createEmptyBorder());
        scrollPane.setCorner(JScrollPane.UPPER_RIGHT_CORNER,
                createCornerComponent(table));
        return scrollPane;
    }

    /**
     * Creates a component that paints the header background for use in a
     * JScrollPane corner.
     */
    private static JComponent createCornerComponent(final JTable table) {
        return new JComponent() {
            @Override
            protected void paintComponent(Graphics g) {
                paintHeader(g, table, 0, getWidth());
            }
        };
    }
}
Advertisements

41 Responses to “Creating a better JTable”


  1. Very nice! Smart way to do the StripedViewport

  2. Brendon Says:

    Ken,

    Great attention to detail as always. One other way you can accomplish the striping is by doing it within the JTable. Something like this:

        public Component prepareRenderer(TableCellRenderer renderer, int row,
                                         int column) {
      
            alternateBackground = row % 2 == 0;
            //... the rest
            alternateBackground = false;
        }
    
        public Color getBackground() {
            return alternateBackground ? Color.lightGray : super.getBackground();
        }
    

    At first glance, it appears like a dirty hack, but the single threaded nature of Swing means the getBackground hack only comes into play when called by the renderers (and only when needed). It’s primary advantage though is that it’s not using any transparency to accomplish the striping. I can’t say for sure precisely what impact having a transparent renderer will have, but I’d watch out for individual cell repaints in odd rows triggering a whole JTable repaint.

    Performance in JTable isn’t everyone’s problem, but if it is then these type of enhancements can make all the difference.

    • Ken Says:

      Hi Brendon,

      Good suggestion. There is one wrinkle though — that technique won’t work if you want the row striping to extend to the edge of the viewport. You could override getScrollableTracksViewportWidth() and return true when the columns don’t fill the viewport, but then the columns would be strectched rather than given their preferred size. Also, even if you overrode getScrollableTracksViewportHeight() to return true when there weren’t enough rows to fill the viewport, the renderer wouldn’t be called for the non-existent rows, and thus the striping wouldn’t extend to the bottom of the table.

      At any rate, your suggestion in many cases will work just fine!

      Thanks for the feedback Brendon.
      -Ken

  3. jakob Says:

    Creating a better JTable? How about reusing a better JTable: JXTable from swinglabs.

    • Ken Says:

      Hi jakob,

      As far as I can tell, JXTable does not address the issue I was talking about here, namely filling the empty JTableHeader space — maybe your comment is keyed off the title rather than the content of this post. Even if JXTable *did* support what I talked about, not everyone can pull in SwingX as a dependency, that is, some production environments are pretty stick. Also, any developers with an extension of JTable may not be able to easily cut over to JXTable.

      -Ken

      • jakob Says:

        Ken,

        you are right JXTable does not address issue with filling the empty space in a table with autoresize off. I am not sure what makes you think that auto-resize off is the default on “most” platforms? Which combinations of JRE + OS did you verify that with?

        It does address the issue of the empty corner, by providing a useful column control that allows selecting which columns should be visible (among other things) as well as the alternating row colors.

        Lauch the demo and choose “extended tables and trees”:

        http://swinglabs.java.sun.com/hudson/job/Swinglabs%20Demos%20Continuous%20Build/ws/swingx.jnlp

        I realize your blog does not target reusable components necessarily but “digging through the dirt” of UIs and I understand your point about the problem of integrating 3rd-party libraries in some environments. My point is just to encourage people to reuse powerful components (if possible) instead of coding things from scratch, so I am suggesting an alternative (which still extends JTable, so part of your solution still applies).

      • Ken Says:

        Hi jakob,

        Most *native* tables on both Mac and Windows don’t auto-resize columns by default (e.g. Finder, Windows Explorer, iTunes etc.).

        I fully sympathize with reusing existing components. However, the aim of this blog (much of the time) is to provide insight into fixing targeted problems. That said, I think it is valuable for you and others to highlight fuller more vetted reusable solutions via the comments — I do appreciate it!

        Thanks for the thoughtful response.
        -Ken

      • jakob Says:

        Ah, native tables. Misunderstanding on my part, thanks for clearing it up.

        Cheers,

        Jakob

  4. Jambo Says:

    Nice one, but one thing your code doesn’t seem to handle is variable row heights. I would suggest to replace fTable.getRowHeight() by fTable.getRowHeight(currentRow) for none empty rows only. However this approach might be suboptimal on the performance point of view compared to yours. It should be checked.

  5. Ken Says:

    Good point Jambo — I’ll play with that idea and see how it affects performance.

  6. Roberto Says:

    Great work! It’s awesome how you pay attention to details.


  7. […] Orr posts about making a better JTable , particularly when on the Mac, but also on most other platforms. If you ever use JTables in your […]

  8. martinm1000 Says:

    Hi,

    I use the code from Santhosh in my tables, and I saw that under some L&F, like Windows Vista / 7 the header that fills the space is not rendered like a normal header…

    Will this code be any different ?

  9. Ken Says:

    Hi martinm1000,

    My code uses the default header renderer to paint the empty header area, so if the default header render on Vista paints that type of header background, you’ll be all set.

    I’m curious, what does the empty header space look like with your other solution? You can email me at kenneth.orr@gmail.com

    -Ken


  10. Hi Ken,

    this is a great blog. I happen to be working pretty heavily with the JTable component and have developed an application (entirely in Swing) which you may find of interest. You can undo/redo, copy/paste and quite a lot more…

    it can be downloaded here at http://isatab.sourceforge.net/isacreator.html, although you can get screenshots from the facebook group/page (http://www.facebook.com/pages/ISAcreator/54764244582). it’s a pretty new tool for the bioinformatics domain…i hope you find it interesting. there is a lot of custom swing things going on :o)

    once again, great work. i’m waiting on being able to create nice HUDs cross-platform now to make my application even nicer :o)

    thanks,

    eamonn

    • Ken Says:

      Hi Eamonn,

      Thanks for the links. I always love seeing what other developers are doing with Swing!

      -Ken

  11. Richard Says:

    Nice work. Although I do not care about look so much ;)

    What I am interested in is functionality. A japanese OSX user told me that cell editing doesn’t work as expected.

    He tells me that moving to a new cell (e.g. by tab or arrow keys) doesn’t allow him to directly edit cells.

    At least I can easily do that. I move to a new cell and directly start typing. He has to select a cell with the mousepointer do do that.

    Any idea what is going on?

    • Ken Says:

      Hi Richard,

      I’m not sure what your referring to — in a Java table on my Mac, if I move to a new cell I can start typing to enter edit mode.

      -Ken

  12. ranjith Says:

    Hey, is this working fine under Windows? I get a transparent table with no headers with Java6 u13
    With the following:
    JFrame frame = new JFrame(“Test”);
    TableModel tm = new DefaultTableModel(new String[][]{{“Honda”,”Civic”},
    {“Honda”,”CRV”}},
    new String[] {“Make”,”Model”});
    BetterJTable table = new BetterJTable(tm);
    frame.getContentPane().add(table);
    frame.pack();
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setVisible(true);

    • Ken Says:

      Hi ranjith,

      I haven’t tried it on Windows, but I’ll give it a shot and see what happens.

      -Ken


      • You will have no column headers since you’ll need to add the JTableHeader manually if you are not using a JScrollPane to wrap around the JTable.

        so…

        BetterJTable table = new BetterJTable(tm);
        JScrollPane tableScroller = new JScrollPane(table, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED)

        frame.getContentPane().add(tableScroller);

        hope this helps…

        eamonn

  13. Ken Says:

    @Eamonn,

    Your absolutely right! Nice catch.

    -Ken

  14. jago Says:

    I would prefer that the stripes are only painted for the rows not the full viewport.

    How do I do that?

    Thanks!

    • Ken Says:

      Hi jago,

      Note that if you do this, when there are no rows, you’ll get no row striping. In any event, you could do following. In the paintStripedBackground method, do the following:

      g.fillRect(g.getClipBounds().x, topY, stripeWidth, bottomY);

      where stripWidth is:

      stripeWidth = rowAtPoint < 0
      ? 0 : fTable.getCellRect(rowAtPoint,0,true).width

      -Ken

  15. jago Says:

    Thanks!

    but your suggested modifications fill the first column till bottom and nothing else.

    I want the stripes painted for all columns. What I do not want is the viewport to be filled with stripes where there are no rows.

    • Ken Says:

      Hi jago,

      I’d suggest that you just override JTable.prepareTableCellRenderer if you don’t want to fill the viewport. That would be the most straight forward solution. My solution is intended to address the problem of painting the entire viewport.

      -Ken

  16. jago Says:

    Obviously the table grid should also not be rendered to the bottom. Just to the bottom of the last cell.

  17. fny Says:

    Hi there! I wanna ask about jtable. What if the table consists of hundreds of record and I want to divide the content of table in pages. So, how to do pagination in JTable? Thx for ur reply. :)


  18. […] a JIDE customer, you’ll be happy to know that they’ve incorporated my blog article on Creating a better JTable back into their tables (see their news release […]

  19. jrico Says:

    Great tip, thanks.

    I had problems at scrolling the table, the rows seemed static. Also when a popup was shown, the clip area was painted wrong. I fixed it with the next changes:

    private void paintStripedBackground(Graphics g) {
    Rectangle clipBounds = g.getClipBounds();
    int topY = clipBounds.y;
    int rowHeight = table.getRowHeight();
    int clipYRelativeToTable = getViewPosition().y + topY;
    int currentRow = clipYRelativeToTable / rowHeight;
    int bottomY = topY + rowHeight;

    // calculate the first value of the bottom ‘y’ taking in count that
    // first row may be partially displayed
    bottomY -= clipYRelativeToTable % rowHeight;

    while (topY < clipBounds.y + clipBounds.height) {
    g.setColor(getRowColor(currentRow++));
    g.fillRect(clipBounds.x, topY, clipBounds.width, bottomY);
    topY = bottomY;
    bottomY = topY + rowHeight;
    }
    }

  20. Martin Says:

    Thanks for the tip. I fixed the scroll problems by modifying the code in paintStripedBackground() with a translation on the graphics object. The key is to align the painting of the StripedViewport with the view position, since it is no longer just a simple background color. Don’t forget to correct the translation after paint the stripes or the table will be misaligned because it doesn’t need the graphics translation.

    Point viewPosition = getViewPosition();
    g.translate(0, -viewPosition.y);

    // create a counter variable to hold the current row. if there are no
    // rows in the table, start the counter at 0.
    int currentRow = rowAtPoint < 0 ? 0 : rowAtPoint;
    while (topY < g.getClipBounds().y + g.getClipBounds().height) {
    int bottomY = topY + table.getRowHeight();
    g.setColor(currentRow % 2 == 0 ? one : two);
    g.fillRect(g.getClipBounds().x, topY, g.getClipBounds().width, bottomY);
    topY = bottomY;
    currentRow ++;
    }

    g.translate(0, viewPosition.y);

    Doing this allowed resizing horizontally to keep the stripes aligned with the table stripes, but vertical scrolling didn't repaint the portions of the viewport that were being scrolled in. So, I also overrode the setViewPosition() method as such:

    @Override
    public void setViewPosition(Point p){
    super.setViewPosition(p);
    repaint();
    }

    Repaint is probably a little blunt, so there could be some refinement there, but the main idea is that when the JScrollPane (and really it is the UI class calling it) does a scroll it calls the setViewPosition() in the JViewport. JViewport then creates the graphics object to repaint the view, but it doesn't repaint itself, which with a static background is okay, but StripedViewport's background is not static.

    Thanks again for the great work and I hope these fixes will be useful to someone else.

  21. Dieter Says:

    It’s quite an old thread, but still useful :-)

    But I had a problem with horizontal scrolling. The vertical gridlines did not scroll properly. To fix this, I’ve made the following changes:

    1) added new overwritten method setViewPosition in the view port class:

    public void setViewPosition(Point p) {
    super.setViewPosition(p);
    repaint();
    }

    2) changed the method paintVerticalGridLines:

    private void paintVerticalGridLines(Graphics g) {
    // paint the column grid dividers for the non-existent rows.

    int offset = getViewPosition().x;
    int x = -offset;

    for (int i = 0; i = 0) {
    g.setColor(Colors.TABLE_GRID_COLOR);
    // draw the grid line (not sure what the -1 is for, but BasicTableUI
    // also does it.
    g.drawLine(x – 1, g.getClipBounds().y, x – 1, getHeight());
    }
    }
    }

    hope, that helps…
    Dieter

  22. Dieter Says:

    sorry, paintVerticalGridLines(..) was not completely pasted. It looks like this:

    private void paintVerticalGridLines(Graphics g) {
    // paint the column grid dividers for the non-existent rows.

    int offset = getViewPosition().x;
    int x = -offset;
    for (int i = 0; i = 0) {
    g.setColor(Colors.TABLE_GRID_COLOR);
    // draw the grid line (not sure what the -1 is for, but BasicTableUI
    // also does it.
    g.drawLine(x – 1, g.getClipBounds().y, x – 1, getHeight());
    }
    }
    }

  23. Dieter Says:

    once again (pasting seems to fail, if code contains a greater than). Hope this time it works:

    private void paintVerticalGridLines(Graphics g) {
    // paint the column grid dividers for the non-existent rows.

    int offset = getViewPosition().x;
    int x = -offset;
    for (int i = 0; i < table.getColumnCount(); i++) {
    TableColumn column = table.getColumnModel().getColumn(i);
    // increase the x position by the width of the current column.
    x += column.getWidth();

    if (x >= 0) {
    g.setColor(Colors.TABLE_GRID_COLOR);
    // draw the grid line (not sure what the -1 is for, but BasicTableUI
    // also does it.
    g.drawLine(x – 1, g.getClipBounds().y, x – 1, getHeight());
    }
    }
    }


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: