Custom JPanel cell with JButtons in JTable

If you ever wanted to add a JPanel with various interactive components (e.g. JButtons, JCheckBoxes etc.) in a JTable cell and could not figure out how to make them work, then this post is for you. Otherwise, continue googling ;)

The Intro

It's really difficult to find something in the Internet when you are not really sure how to phrase it. That's why you need to practice your google-fu as much as possible. See, I was looking for a way to have a JPanel with buttons in a JTable cell, but had no idea how to look for it. JPanel in JTable? JPanel with buttons in JTable? JTable JPanel JButton?

OK, to be fair, I'm pretty sure my answer is in there somewhere deep (as in, after the first 3 results). But, as any impatient person that respects himself, I went the easy way: StackOverflow: JTable: Buttons in Custom Panel in Cell.

You see, most examples I found with Google talked about Cell Editors, but for simple column components, such as using a JTextField to edit an integer, or a JComboBox to edit a String etc. I wanted a custom JPanel that did not change, but simply had interactive controls.

In retrospect, I could have simply used a panel with MigLayout, but NO! I wanted the challenge. I wanted to learn how to do it. Plus, now I can sort my table (which is absolutely useless in my context).

Anyway, on with the code!

The Code

First thing's first, we need to have a basic ADT that will contain some data. This is what we ultimately want to display in our table rows. Let's say we want to have some RSS Feeds.

public class RssFeed {
  public String name;
  public String url;
  public Article[] articles;

  public RssFeed(String name, String url, Article[] articles) {
    this.name = name;
    this.url = url;
    this.articles = articles;
  }
}

public class Article {
  public String title;
  public String url;
  public String content;

  public Article(String title, String url, String content) {
    this.title = title;
    this.url = url;
    this.content = content;
  }
}

Yeah yeah, make the fields private, add getters/setters blah blah blah. Here is a simple JFrame that displays a JTable with some example data:

public class JInteractiveTableExample extends JFrame {
  public JInteractiveTableExample() {
    super("Interactive Table Cell Example");
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setSize(500, 300);

    List feeds = new ArrayList();
    feeds.add(new RssFeed("pekalicious", "http://feeds2.feedburner.com/pekalicious",
      new Article[] {
        new Article("Title1", "http://title1.com", "Content 1"),
        new Article("Title2", "http://title2.com", "Content 2"),
        new Article("Title3", "http://title3.com", "Content 3"),
        new Article("Title4", "http://title4.com", "Content 4"),
    }));
    feeds.add(new RssFeed("Various Thoughts on Photography", "http://various-photography-thoughts.blogspot.com/feeds/posts/default",
      new Article[] {
        new Article("Title1", "http://title1.com", "Content 1"),
        new Article("Title2", "http://title2.com", "Content 2"),
        new Article("Title3", "http://title3.com", "Content 3"),
        new Article("Title4", "http://title4.com", "Content 4"),
    }));

    JTable table = new JTable(
      new Object[][] {
        new RssFeed[] { feeds.get(0) },
        new RssFeed[] { feeds.get(1) }
      },
      new String[] { "Feeds" }
    );
    add(new JScrollPane(table));
  }
}

Here we create two RssFeeds with articles on the fly and add them to a JTable. If we run this example, we will see that each row displays the toString() value of our RssFeed object.

JTable without a Cell Renderer

This is the default behavior of JTable when the data is not a known class (Strings, ints, etc.) We can change this behavior by creating a custom TableModel. Here is a basic table model for our example:

public class RssFeedTableModel extends AbstractTableModel {
  List feeds;

  public RssFeedTableModel(List feeds) {
    this.feeds = feeds;
  }

  public Class getColumnClass(int columnIndex) { return RssFeed.class; }
    public int getColumnCount() { return 1; }
    public String getColumnName(int columnIndex) { return "Feed"; }
    public int getRowCount() { return (feeds == null) ? 0 : feeds.size(); }
    public Object getValueAt(int rowIndex, int columnIndex) { return (feeds == null) ? null : feeds.get(rowIndex); }
    public boolean isCellEditable(int columnIndex, int rowIndex) { return true; }
}

We then simply change our table declaration to:

JTable table = new JTable(new RssFeedTableModel(feeds));

Voila! We now see the exact same thing in our table!

JTable with Table Model (but still no Cell Renderer)

Errr.. OK, so it turns out that it's not the Model that converts our objects into Strings, but the Renderer. Each JTable has a TableCellRenderer that, given an object, it returns a Swing component that can be used to display the underlying data.

So now we need to tell the JTable how to convert an RssFeed into a Component. We can do this by implementing the TableCellRenderer like so:

public class RssFeedCellRenderer implements TableCellRenderer{
  public Component getTableCellRendererComponent(JTable table, Object value,
        boolean isSelected, boolean hasFocus, int row, int column) {
    RssFeed feed = (RssFeed)value;

    JButton showButton = new JButton("View Articles");
    showButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent arg0) {
        JOptionPane.showMessageDialog(null, "HA-HA!");
      }
    });

    JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
    panel.add(new JLabel("" + feed.name + ""
            + feed.url + "Articles " + feed.articles.length + ""));
    panel.add(showButton);

    if (isSelected) {
      panel.setBackground(table.getSelectionBackground());
    }else{
      panel.setBackground(table.getBackground());
    }
    return panel;
  }
}

We also need to register this renderer in our table:

JTable table = new JTable(new RssFeedTableModel(feeds));
table.setDefaultRenderer(RssFeed.class, new RssFeedCellRenderer());
table.setRowHeight(60);

In the above lines, we effectively say "Whenever a column is an RssFeed class, use this renderer". We already said that our one and only column is of type RssFeed in our table model:

public Class getColumnClass(int columnIndex) { return RssFeed.class; }

Now if we run our application, we can finally see something useful (although, I still prefer reading "com.pekalicious.interactiveJPanelTableCell.data.RssFeed@106caf16". Yes, I'm THAT good):

JTable with Renderer

There are two things that are wrong here: first, every time the JTable needs to render a cell, it will instantiate a new JPanel. This can harm the performance if there are many rows. Second, the button does not work! Cell renderers are used only for what they say, to render cells. They do not provide any mechanism for the underlying components. However, JTable also has Cell Editors. Cell Editors work pretty much the same way as Renderers, they take an Object and return a Component, but the component returned can be interactive.

As you have probably guessed already, you can have different components returned by renderers and editors. An int, for example, can be rendered using a JLabel and edited using a JTextField (which is the default behavior of JTable). However, in our example, we want the same panel both for editing and rendering. This is why we will create a common Component that will be used by both:

public class RssFeedCellComponent extends JPanel {
  RssFeed feed;

  JButton showButton;
  JLabel text;

  public RssFeedCellComponent() {
    showButton = new JButton("View Articles");
    showButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent arg0) {
        JOptionPane.showMessageDialog(null, "Reading " + feed.name);
      }
    });

    text = new JLabel();
    add(text);
    add(showButton);
  }

  public void updateData(RssFeed feed, boolean isSelected, JTable table) {
    this.feed = feed;

    text.setText("" + feed.name + "" + feed.url + "Articles " + feed.articles.length + "");
    if (isSelected) {
      setBackground(table.getSelectionBackground());
    }else{
      setBackground(table.getBackground());
    }
  }
}

Now, our cell renderer becomes:

public class RssFeedCellRenderer implements TableCellRenderer{
  RssFeedCellComponent feedComponent;

  public RssFeedCellRenderer() {
    feedComponent = new RssFeedCellComponent();
  }

  public Component getTableCellRendererComponent(JTable table, Object value,
      boolean isSelected, boolean hasFocus, int row, int column) {
    RssFeed feed = (RssFeed)value;

    feedComponent.updateData(feed, isSelected, table);
    return feedComponent;
  }
}

And once again, nothing changed! But we did optimize (and that's always good, right?). Now, there is only one instance of RssFeedCellComponent which changes its components depending on the RssFeed passed. In addition, creating a Cell Editor that returns the same component is now pretty simple:

public class RssFeedCellEditor extends AbstractCellEditor implements TableCellEditor {
  RssFeedCellComponent feedComponent;

  public RssFeedCellEditor() {
    feedComponent = new RssFeedCellComponent();
  }

  public Component getTableCellEditorComponent(JTable table, Object value,
      boolean isSelected, int row, int column) {
    RssFeed feed = (RssFeed)value;
    feedComponent.updateData(feed, true, table);
    return feedComponent;
  }

  public Object getCellEditorValue() {
    return null;
  }
}

And now we register our editor:

JTable table = new JTable(new RssFeedTableModel(feeds));
table.setDefaultRenderer(RssFeed.class, new RssFeedCellRenderer());
table.setDefaultEditor(RssFeed.class, new RssFeedCellEditor());
table.setRowHeight(60);

We can do even better. We can combine all three classes, RssFeedCellComponent, RssFeedCellRenderer and RssFeedCellEditor into a single RssFeedCell:

public class RssFeedCell extends AbstractCellEditor implements TableCellEditor, TableCellRenderer{
  JPanel panel;
  JLabel text;
  JButton showButton;

  RssFeed feed;

  public RssFeedCell() {
    text = new JLabel();
    showButton = new JButton("View Articles");
    showButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent arg0) {
        JOptionPane.showMessageDialog(null, "Reading " + feed.name);
      }
    });

    panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
    panel.add(text);
    panel.add(showButton);
  }

  private void updateData(RssFeed feed, boolean isSelected, JTable table) {
    this.feed = feed;

    text.setText("" + feed.name + "" + feed.url + "Articles " + feed.articles.length + "");

    if (isSelected) {
      panel.setBackground(table.getSelectionBackground());
    }else{
      panel.setBackground(table.getBackground());
    }
  }

  public Component getTableCellEditorComponent(JTable table, Object value,
      boolean isSelected, int row, int column) {
    RssFeed feed = (RssFeed)value;
    updateData(feed, true, table);
    return panel;
  }

  public Object getCellEditorValue() {
    return null;
  }

  public Component getTableCellRendererComponent(JTable table, Object value,
      boolean isSelected, boolean hasFocus, int row, int column) {
    RssFeed feed = (RssFeed)value;
    updateData(feed, isSelected, table);
    return panel;
  }
}

Now we use only one Panel for all rendering and editing!

JTable with Renderer and Editor

The Conclusion

Every time I try to use a JTable I discover how difficult it is to just create a f*cking simple table, but I also see how powerful it is for more complex things. I'm pretty sure there is a way to display the button only on mouse over. You know, web2.0-y.

Anyway, that's that. As usual, you can find the source and the executable in my Java Corner.

Now excuse me, but I got some Void Rays to counter attack. En Taro Adun!