January 30, 2010

JButtons in a JTable

Tags: ,

Recently I wanted to have a clickable button in a table. Searching on Google for JButton in JTable I found a couple of suggestions, most notably this DevX article and this Esus article. There was also a StackOverflow question that just referenced other solutions. None really satisfied me. So borrowing their ideas I created my own solution.

Fundamentally, there are two problems with having a JButton in a JTable. Firstly, by default a JTable will display cell values as a String, so JButtons appear as “javax.swing.JButton”. Secondly, a JTable does not pass clicks through to the cells.

To display the buttons correctly a custom cell renderer needs to be applied to any columns displaying JButtons. This can be done with the code table.getColumn(“Button1”).setCellRenderer(new JTableButtonRenderer()) (this assumes that there is a column in the table labelled “Button1”. The button renderer is below. The default renderer always returns a JLabel, but this one returns a JButton after colouring it in an appropriate manner. Note, this renderer assumes the contents of column will always be a JButton and will throw an exception if this is not the case. If the table column may not contain JButtons, just extend DefaultTableCellRenderer, detect if the value is a JButton (or even a Swing component) with instanceof, otherwise call super. This Sun article may help with this.

public class JTableButtonRenderer implements TableCellRenderer {		
  @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
    JButton button = (JButton)value;
    if (isSelected) {
      button.setForeground(table.getSelectionForeground());
      button.setBackground(table.getSelectionBackground());
    } else {
      button.setForeground(table.getForeground());
      button.setBackground(UIManager.getColor("Button.background"));
    }
    return button;	
  }
}

To get the table to pass clicks through to the button, add a mouse listener to the table with table.addMouseListener(new JTableButtonMouseListener(table)). This mouse listener (example below) will have to take the click, work out in which cell it occurred and if that cell contains a JButton, click it.

public class JTableButtonMouseListener extends MouseAdapter {
  private final JTable table;
		
  public JTableButtonMouseListener(JTable table) {
    this.table = table;
  }

  @Override public void mouseClicked(MouseEvent e) {
    int column = table.getColumnModel().getColumnIndexAtX(e.getX());
    int row    = e.getY()/table.getRowHeight(); 

    if (row < table.getRowCount() && row >= 0 && column < table.getColumnCount() && column >= 0) {
      Object value = table.getValueAt(row, column);
      if (value instanceof JButton) {
        ((JButton)value).doClick();
      }
    }
  }
}

And with that, the buttons in your table should start to work. All you need to do is return the buttons (which perform the required actions) when requested from your model, using getValueAt(). A complete working example is below.

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;

public class ButtonExample {

	public static void main(String[] args) {
		final ButtonExample example = new ButtonExample();
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
            	example.createAndShowGUI();
            }
        });
	}
	
	private void createAndShowGUI() {
        JFrame frame = new JFrame("Button Example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        JTable table = new JTable(new JTableModel()); 
        JScrollPane scrollPane = new JScrollPane(table);
		table.setFillsViewportHeight(true);	
		
		TableCellRenderer buttonRenderer = new JTableButtonRenderer();
		table.getColumn("Button1").setCellRenderer(buttonRenderer);
		table.getColumn("Button2").setCellRenderer(buttonRenderer);
		table.addMouseListener(new JTableButtonMouseListener(table));
        
        frame.getContentPane().add(scrollPane, BorderLayout.CENTER);
        frame.getContentPane().setPreferredSize(new Dimension(500, 200));
        frame.pack();
        frame.setVisible(true);
	}
	
	public static class JTableModel extends AbstractTableModel {
		private static final long serialVersionUID = 1L;
		private static final String[] COLUMN_NAMES = new String[] {"Id", "Stuff", "Button1", "Button2"};
		private static final Class<?>[] COLUMN_TYPES = new Class<?>[] {Integer.class, String.class, JButton.class,  JButton.class};
		
		@Override public int getColumnCount() {
			return COLUMN_NAMES.length;
		}

		@Override public int getRowCount() {
			return 4;
		}
		
		@Override public String getColumnName(int columnIndex) {
	        return COLUMN_NAMES[columnIndex];
	    }
		
		@Override public Class<?> getColumnClass(int columnIndex) {
			return COLUMN_TYPES[columnIndex];
		}

		@Override public Object getValueAt(final int rowIndex, final int columnIndex) {
			switch (columnIndex) {
				case 0: return rowIndex;
				case 1: return "Text for "+rowIndex;
				case 2: // fall through
				case 3: final JButton button = new JButton(COLUMN_NAMES[columnIndex]);
						button.addActionListener(new ActionListener() {
							public void actionPerformed(ActionEvent arg0) {
								JOptionPane.showMessageDialog(JOptionPane.getFrameForComponent(button), 
										"Button clicked for row "+rowIndex);
							}
						});
						return button;
				default: return "Error";
			}
		}	
	}

	private static class JTableButtonRenderer implements TableCellRenderer {		
		@Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
			JButton button = (JButton)value;
			if (isSelected) {
				button.setForeground(table.getSelectionForeground());
				button.setBackground(table.getSelectionBackground());
		    } else {
		    	button.setForeground(table.getForeground());
		    	button.setBackground(UIManager.getColor("Button.background"));
		    }
			return button;	
		}
	}
	
	private static class JTableButtonMouseListener extends MouseAdapter {
		private final JTable table;
		
		public JTableButtonMouseListener(JTable table) {
			this.table = table;
		}

		public void mouseClicked(MouseEvent e) {
			int column = table.getColumnModel().getColumnIndexAtX(e.getX());
			int row    = e.getY()/table.getRowHeight(); 

			if (row < table.getRowCount() && row >= 0 && column < table.getColumnCount() && column >= 0) {
			    Object value = table.getValueAt(row, column);
			    if (value instanceof JButton) {
			    	((JButton)value).doClick();
			    }
			}
		}
	}
}

comments powered by Disqus