Sunday, November 1, 2009

Using a JButton in the cell of a JTable

There are many reasons why you would want a component to be displayed in the cell of your tables. Purely displaying text isn't enough. This is quite easy for most components like a checkbox, which only requires you to have a column of type "Boolean". The table will handle the rest. But the JButton is a different story.

There is no default handling for buttons, which has led many people to conjure up some pretty nasty ways of getting it to work. I've seen many of them work very well, but ending up 100/200+ lines of code just for getting the button to display and accept input. The basic idea is to create your own TableCellRenderer and TableCellEditor, and then have these return the component. There after you need to capture all input events you wish the button to react to and dispatch them to the button's handler. This is where the code bulks come in.

There is a much easier way of doing it though, and that's what I'll describe here. Assume you have a table, where every row represents an instance of a class "Employee". It has 3 columns, nl. "Name", "Surname" and "Options". In the "Options" column you wish to display a button "Get Fullname", which prints the full name of the employee in a JLabel above the table.

So, you will create your own TableModel implementation, which returns the appropriate values for every column, the row/column counts, and so forth. We'll call ours EmployeeTableModel. For the JButton to work we have to add some code towards this purpose in EmployeeTableModel.
  1. This code will handle column index "2", which is the index for the "Options" column. Firstly we have to return a "true" value for this column in the isCellEditable() method. This is necessary for the cell editor to be returned, which is how we get the pressed/action performed handling of the button.
  2. In the getColumnClass() method we need to return the JButton class for column 2, and String class for the other columns. This way the cells are identified and the correct editor used.
  3. Finally, in the getValueAt() method, which returns the actual value for a cell, we need to return the button we wish to display in each cell. This button should be prepared with action listeners and unique for each row if you want it to be aware of the row it represents. Our button will display the employee's full name, so it needs to know for which employee it was selected. So we create a unique button for each row which is aware of the employee.
Part of our EmployeeTableModel class is a button handler. Since the model class creates the JButton instances, we provide it with an implementation of EmployeeButtonHandler, which has a method buttonClicked(Employee) call every time a button is clicked. This allows us to create the buttons on request inside the model so to cache their instances but still keep the actual logic outside of the model. This allows for higher cohesion.

The second class we need to create is the javax.swing.table.TableCellRenderer and javax.swing.table.TableCellEditor implementation. I combine it into a single class, as it's behaviour is very similar and far too small to justify separate classes. We will also extend the javax.swing.AbstractCellEditor class to avoid having to recreate existing code.

The TableCellEditor interface has 2 methods we still need to define, nl. getCellEditorValue() and getTableCellEditorComponent(). We just return null for the former, as we're not actually editing anything. For the latter we return whatever is passed into it, which would be a JButton. The table will call getValueAt() for the 3rd column, receive a JButton instance, lookup our editor which is associated with the JButton class and call our editor's getTableCellEditorComponent() passing in the value of the cell, which is the JButton we wish to display in the cell. So returning it is save. This is all the TableCellEditor has to do.

For the TableCellRenderer it's a bit more complex, though this is where the biggest trick of them all come in. It follows similar logic in lookup/invocation as the TableCellEditor described above, and the method invoked on it, which is getTableCellRendererComponent(), has the same arguments as the editor counterpart. For the rendering and button events to work properly over multiple rows, though, we can't return the same instance as the editor does. The fact that it's purely used for rendering means we don't have to return a unique instance. We just need to return an instance with the same text as the actual button, so what we do is create a Map of JButtons, which maps the button text to instances. In our case all the buttons have the same text, so we'll only have one instance, but if you decide to have different text one your buttons, the code I supply will still work.

Finally, we just create the EmployeeButtonHandler implementation, the atual table and configure it with the table model. Then the last step for the button to work is to configure the cell renderer and editor to be the defaults for JButton classes in the table. We do all this by extending the JFrame class, have it implement the EmployeeButtonHandler and adding logic for the button clicks and to create and configure the model JTable.

Here is the code. I put them all as one string of code in a package "demotablebutton". The JFrame with the table creation/configuration is in a class DemoTableButton, which also has a main() method for running the demonstration. So just divide the following public classes into their own .java files with the same name as each class, and run it to see the table in action.

/** DemoTableButton.java **/
package demotablebutton;

import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

public class DemoTableButton extends JFrame
  implements EmployeeTableModel.EmployeeButtonHandler
{
  private JLabel messageLabel;

  public static void main(String[] args)
  {
    new DemoTableButton();
  }
  
  public DemoTableButton()
  {
    setLayout(new BorderLayout());

    messageLabel = new JLabel("Click a button.");
    add(messageLabel, BorderLayout.NORTH);

    // create the table and the model
    EmployeeTableModel model = new EmployeeTableModel(this);
    JTable table = new JTable(model);
    // now make a renderer the default for columns of type JButton
    TableButtonRenderer renderer = new TableButtonRenderer();
    table.setDefaultEditor(JButton.class, renderer);
    table.setDefaultRenderer(JButton.class, renderer);

    // add the table in a scrollpane
    JScrollPane scroll = new JScrollPane(table);
    add(scroll, BorderLayout.CENTER);

    setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    SwingUtilities.invokeLater(new Runnable() {
      public void run()
      {
        setVisible(true);
        setSize(400, 300);
      }
    });
  }

  public void buttonClicked(Employee e)
  {
    messageLabel.setText("Full Name: " + e.getName() + " " + e.getSurname());
  }
}

/** Employee.java **/
package demotablebutton;

public class Employee
{
  private String name;

  private String surname;

  public Employee(String name, String surname)
  {
    this.name = name;
    this.surname = surname;
  }

  public String getName()
  {
    return name;
  }

  public String getSurname()
  {
    return surname;
  }
}

/** EmployeeTableModel.java **/
package demotablebutton;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JButton;
import javax.swing.table.AbstractTableModel;

public class EmployeeTableModel extends AbstractTableModel
{
  private static final Employee[] employees = new Employee[] {
    new Employee("Quintin", "Beukes"),
    new Employee("Anita", "Geber"),
    new Employee("Gavin", "Classen"),
    new Employee("Arie", "Kruger")
  };

  private Map<Employee, JButton> buttons = new HashMap<Employee, JButton>();

  private EmployeeButtonHandler handler;
  
  public EmployeeTableModel(EmployeeButtonHandler handler)
  {
    this.handler = handler;
  }

  @Override
  public String getColumnName(int column)
  {
    switch (column)
    {
      case 0:
        return "Name";
      case 1:
        return "Surnam";
      case 2:
        return "Options";
    }
    return "#invalid";
  }

  @Override
  public boolean isCellEditable(int rowIndex, int columnIndex)
  {
    return true;
  }

  @Override
  public Class<?> getColumnClass(int columnIndex)
  {
    if (columnIndex == 2)
    {
      return JButton.class;
    }
    else
    {
      return String.class;
    }
  }

  public int getRowCount()
  {
    return employees.length;
  }

  public int getColumnCount()
  {
    return 3;
  }

  public Object getValueAt(int rowIndex, int columnIndex)
  {
    switch (columnIndex)
    {
      case 0:
        return employees[rowIndex].getName();
      case 1:
        return employees[rowIndex].getSurname();
      case 2:
        return getCellButton(employees[rowIndex]);
    }
    
    return "#invalid";
  }

  private JButton getCellButton(Employee employee)
  {
    JButton button = buttons.get(employee);
    if (button == null)
    {
      button = createButton(employee);
      buttons.put(employee, button);
    }
    return button;
  }

  private JButton createButton(final Employee employee)
  {
    final JButton button = new JButton("Get Fullname");

    button.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e)
      {
        handler.buttonClicked(employee);
      }
    });

    return button;
  }

  public static interface EmployeeButtonHandler
  {
    public void buttonClicked(Employee e);
  }
}

/** TableButtonRenderer.java **/
package demotablebutton;

import java.awt.Component;
import java.util.Map;
import java.util.WeakHashMap;
import javax.swing.AbstractCellEditor;
import javax.swing.JButton;
import javax.swing.JTable;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;

public class TableButtonRenderer extends AbstractCellEditor
  implements TableCellRenderer, TableCellEditor
{
  private Map<String, JButton> renderButtons = new WeakHashMap<String, JButton>();

  public Component getTableCellRendererComponent(JTable table, Object value,
    boolean isSelected, boolean hasFocus, int row, int column)
  {
    JButton button = (JButton)value;
    JButton renderButton = renderButtons.get(button.getText());

    if (renderButton == null)
    {
      renderButton = new JButton(button.getText());
      renderButtons.put(button.getText(), renderButton);
    }
    
    return renderButton;
  }

  public Object getCellEditorValue()
  {
    return null;
  }

  public Component getTableCellEditorComponent(JTable table, Object value,
    boolean isSelected, int row, int column)
  {
    return (JButton)value;
  }
}


No comments: