Custom JPanel cell with JButtons in JTable

or How I have no idea how to call this...

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<RssFeed> feeds = new ArrayList<RssFeed>();
    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

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)

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("<strong>" + feed.name + "</strong>"
            + feed.url + "Articles " + feed.articles.length + ""));
    panel.add(showButton);
 
    if (isSelected) {
      panel.setBackground(table.getSelectionBackground());
    }else{
      panel.setBackground(table.getSelectionForeground());
    }
    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

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("<strong>" + feed.name + "</strong>" + feed.url + "Articles " + feed.articles.length + "");
    if (isSelected) {
      setBackground(table.getSelectionBackground());
    }else{
      setBackground(table.getSelectionForeground());
    }
  }
}

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 it’s 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("<strong>" + feed.name + "</strong>" + feed.url + "Articles " + feed.articles.length + "");
 
    if (isSelected) {
      panel.setBackground(table.getSelectionBackground());
    }else{
      panel.setBackground(table.getSelectionForeground());
    }
  }
 
  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

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!

Posted by Panagiotis Peikidis on 31 Aug 2010

Comments (28 )

  1. Michel Albert wrote:

    Nice read. But, do you know how to make the button clickable with a renderer only? I currently have a different cell editor and render. The renderer shows a panel with the button and a label, the editor is a JFormattedTextField.

    p.s.: Are those my Void Rays you are countering? I see your Stalkers and raise you some Colossi! :]

    March 10th, 2011 at 19:40
  2. Panagiotis Peikidis wrote:

    @Michel If I understand correctly, you want to change the cell from a panel to a JFormattedTextField whenever the button is pressed. If this is true, I would make the editor have two states: initial and editing. So you have the same panel both in the renderer and the editor (as explained in my post) and when the button is pressed, return a JFormattedTextField instead of the panel in the Editor.

    You’ll have to keep track of the row you are currently editing to know when to return a panel and when the text field. To allow editing of multiple cells, just track an arrays of row indexes instead.

    If this is not the case, then things get a little more complicated. This has been asked many many times, for example: http://stackoverflow.com/questions/3771158/interaction-with-a-cell-renderer-in-jtable

    I personally try to avoid this. I believe you are better off using a celleditor that has two states. You’ll avoid having to deal with delegating all the events Swing does automatically (mouseover, mouseclick, etc).

    Your question sounds like a good future post :P

    p.s.: Good move. I see your Colossi and raise you a couple of Phoenix.

    March 12th, 2011 at 11:06
  3. Raf wrote:

    Great tutorial man!

    April 12th, 2011 at 19:37
  4. Raf wrote:

    Just to say that it’s very important to put line “fireEditingStopped();” at the end of actionPerformed event, otherwise you will face some problems when removing rows.

    April 18th, 2011 at 11:32
  5. Panagiotis Peikidis wrote:

    @Raf yeah, there are many cases where forgetting to stop editing (either by fireEditingStopped() or by getCellEditor().stopCellEditing()) you’ll get weird behavior.

    April 18th, 2011 at 13:49
  6. Bin.Bai wrote:

    THANK YOU!
    IT’S SO GOOD!

    May 11th, 2011 at 14:38
  7. Sylwek wrote:

    THANKS! U saved my day!

    August 25th, 2011 at 10:18
  8. Sossij wrote:

    Spot on! Well done that man!

    November 24th, 2011 at 20:24
  9. RichA wrote:

    Nice tutorial, worked well for me…one thing I might add is I found that the button doesn’t work unless the cell is set to editable. Pretty obvious I guess, but took a little tracking down.

    December 29th, 2011 at 15:16
  10. Panagiotis Peikidis wrote:

    @RichA yeah, figuring out why things in a table don’t work is tricky. It took me some time to find out that fireEditingStop() needed to be called in some cases. I’m writing a follow up for more advanced topics. Feedback would be appreciated.

    January 2nd, 2012 at 03:15
  11. Piotr wrote:

    Hi,

    Very usefull tutorial. I have, however, a question in regards to the RssFeed class itself. Generally I would like to ask You why You didn’t choose this class to become a JPanel?

    I’m asking because I’m bulding a PlugIn based application where plugins will be providing their own configuration panel. I wanted to put this particular panel as the held values and, while performing the renderer acitivities, I wanted to <> insted of Your panel.

    Generally I suceeded, with small issue that I have to click twice before the selected component appears active…

    I wounder if this is not related to design issue…

    Best regards!

    January 19th, 2012 at 23:09
  12. Piotr wrote:

    In above comment the statement between brackets is missing… it should be:
    return (Component) value;

    January 19th, 2012 at 23:10
  13. gugum web wrote:

    so this is the case!! Thanks man!
    I should try to use your tips over here at least to use my own JButton inside JPanel placed on JTable’s Cells….

    January 21st, 2012 at 03:41
  14. Panagiotis Peikidis wrote:

    @Piotr JTable follows the MVC architecture. So, RssFeed, in this case, is our Model, while RssFeedCell is our View and Controller.

    I can’t think of a good and elegant solution for your problem right now, but it did give me some ideas for my next post.

    @gugum Thanks!

    January 30th, 2012 at 05:26
  15. Alberto wrote:

    Tidy and clean, thank you!

    March 14th, 2012 at 19:16
  16. Alexandre Ogrodovski wrote:

    One of best posts I ever read

    May 12th, 2012 at 15:15
  17. Jörg wrote:

    Excellent tutorial, thanks!

    May 16th, 2012 at 20:48
  18. Fan Pat wrote:

    Hello,
    Thank you for the detailed example.
    I followed your example and created something similar: a JTable with one column; each column cell is a JPanel (renderer); the JPanel contains 3 JLabel and 1 JToggleButton.

    Problem:
    The getTableCellRendererComponent() is called and working. The getTableCellEditorComponent() is never called and JToggleButton’s actionPerformed() is never called. Therefore, nothing happens when I press the toggle button.

    I can send you the example code I wrote. Please let me know.
    Much appreciated,
    Fan

    July 31st, 2012 at 15:35
  19. Panagiotis Peikidis wrote:

    @Fan,

    I looked at your code and found the problem. There are two locations where you can override isCellEditable: you can override TableModel’s or CellEditor’s. In your code, you are overriding the CellEditor’s, which is the wrong one. You have to override the TableModel’s because, in our structure, JTable asks the TableModel which cell is editable, not the CellEditor.

    So, simply remove the isCellEditable from PersonCell and put it in MyTableModel and you should be fine.

    August 2nd, 2012 at 06:56
  20. JeanPierre wrote:

    Hello,

    Great tutorial.
    The step by step explanation made it all very clear for me.

    I thought it would take me one day to implement a similar problem, but I did it in 2 hours.

    thanks!

    January 24th, 2013 at 23:12
  21. Panagiotis Peikidis wrote:

    Happy to have helped ;)

    January 25th, 2013 at 05:29
  22. Sotero Ordones wrote:

    Hello, your tutorial is complete.

    I have a small doubt. How do you operate the button? I have completed your tutorial, but my button does not work.

    Best regards.

    October 22nd, 2013 at 22:13
  23. Rudi Kershaw wrote:

    This is fantastic. I’ve been trying to do this myself and I have got sooo close, but no biscuit. This helped me iron out all the faults.

    Cheers

    November 8th, 2013 at 19:14
  24. Warren wrote:

    Hi man, thats very nice tutorial, good job man, its very usefull!!
    Thanks!

    November 21st, 2013 at 16:24
  25. Nestor Octavio wrote:

    nice tutorial
    Greetings from Mexico Wey!! :3

    December 6th, 2013 at 03:19
  26. Batbold Sukhbaatar wrote:

    Nice tutorial. was starting to develop a brain tumor looking for this :(….Can’t believe how hard it is to find something when you really need it…..:)…Again thanks

    February 6th, 2014 at 03:39
  27. Panagiotis Peikidis wrote:

    Glad you found it useful. I completely agree with how hard it is to find such a tutorial, which is why I wrote this post in the first place. I think part of the problem is how difficult it is to describe what you are trying to do. As far as I know, there is no terminology in the official Java tutorials for this kind of things.

    February 6th, 2014 at 04:01
  28. zohir wrote:

    good tutorial
    thanx a lot

    February 21st, 2014 at 05:21

Leave a Reply