SwingのJTable中にJButtonを入れる
今更Swingでも無いだろうが、という意見もあるだろうが、単純なインターフェースを作成するにはJavaFXに比較すると、かなり手軽に使えることは否定できない。
また、Swingが出現した頃に比較すると、マシンのスピードが上がっていることもあり、当初見られたもっさり感も解消されていると思う。
ただし、Swingの設計がかなりおかしなもので、動作を拡張しようとすると、かなり苦労することになる。特にテーブルまわりは訳のわからない仕様になっている。
あるとき、JTableの中にJButtonを入れたいと思ったのだが、適当なサンプルがどこを探しても得られない。あっても、なぜそうするのかの理由が無い。最終的には、以下の書き込みを見つけた。2010年のものである。
この人も、あらゆるサンプルを試してみたが、満足の行くものは得られなかったそうだ。以下、この書き込みに沿って解説してみる。
問題点
JTable中のJButtonが持つ、基本的な二つの問題点としてはこうだ。
デフォルトでは、JTableはセル値を文字列として表示する。したがって、JButtonは、“javax.swing.JButton”などという文字列として現れる。第二に、JTableはクリックをセルに渡さないのだ。
表示
ボタンを正しく表示するためには、カスタムセルレンダラが必要だ。これは以下のコードで行われる。
table.getColumn(“Button1”).setCellRenderer(new JTableButtonRenderer())
※ここでは、”Button1″という列があると仮定している。
ボタンレンダラは以下のコードだ。デフォルトのレンダラは常にJLabelを返すのだが、しかし、ここでは、JButtonを適切にカラーリングした後で返す。
注意して欲しいことは、このレンダラは、列のコンテンツがいつでもJButtonであることを仮定しており、そうでない場合は例外が発生する。テーブル列がJButtonを含まないことがある場合には、単純にDefaultTableCellRendererを拡張し、instanceof等で、JButtonかどうかを判断すること。
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;
}
}
クリックを渡す
テーブルへのクリックをボタンを渡すためには、テーブルにマウスリスナーをつける。
table.addMouseListener(new JTableButtonMouseListener(table))
このマウスリスナー(以下に例がある)は、クリックを得て、それが起こったボタンセルを特定し、それをクリックする。
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();
}
}
}
}
まとめる(ただし、ボタンクリック時の描画がされない)
これで、テーブル中のボタンが機能するようになるだろう。テーブルモデルにgetValueAtで中身が要求されたらボタンを返せばよい。完全なサンプルとしては以下だ。
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();
}
}
}
}
}
この例では、ボタンをクリックしたときに「ボタンが沈み込んだ状態」の描画がされない。
ボタンクリック時の描画のされるサンプル
Adding Jbutton to JTableに、きちんとボタンクリック時の「沈み込み」を描画するサンプルがあった。
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultCellEditor;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
public class JButtonTableExample {
public JButtonTableExample() {
JFrame frame = new JFrame("JButtonTable Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
DefaultTableModel dm = new DefaultTableModel();
dm.setDataVector(new Object[][]{{"button 1", "foo"},
{"button 2", "bar"}}, new Object[]{"Button", "String"});
JTable table = new JTable(dm);
table.getColumn("Button").setCellRenderer(new ButtonRenderer());
table.getColumn("Button").setCellEditor(new ButtonEditor(new JCheckBox()));
JScrollPane scroll = new JScrollPane(table);
table.setPreferredScrollableViewportSize(table.getPreferredSize());//thanks mKorbel +1 http://stackoverflow.com/questions/10551995/how-to-set-jscrollpane-layout-to-be-the-same-as-jtable
table.getColumnModel().getColumn(0).setPreferredWidth(100);//so buttons will fit and not be shown butto..
frame.add(scroll);
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new JButtonTableExample();
}
});
}
}
class ButtonRenderer extends JButton implements TableCellRenderer {
public ButtonRenderer() {
setOpaque(true);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
if (isSelected) {
setForeground(table.getSelectionForeground());
setBackground(table.getSelectionBackground());
} else {
setForeground(table.getForeground());
setBackground(UIManager.getColor("Button.background"));
}
setText((value == null) ? "" : value.toString());
return this;
}
}
class ButtonEditor extends DefaultCellEditor {
protected JButton button;
private String label;
private boolean isPushed;
public ButtonEditor(JCheckBox checkBox) {
super(checkBox);
button = new JButton();
button.setOpaque(true);
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
fireEditingStopped();
}
});
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value,
boolean isSelected, int row, int column) {
if (isSelected) {
button.setForeground(table.getSelectionForeground());
button.setBackground(table.getSelectionBackground());
} else {
button.setForeground(table.getForeground());
button.setBackground(table.getBackground());
}
label = (value == null) ? "" : value.toString();
button.setText(label);
isPushed = true;
return button;
}
@Override
public Object getCellEditorValue() {
if (isPushed) {
JOptionPane.showMessageDialog(button, label + ": Ouch!");
}
isPushed = false;
return label;
}
@Override
public boolean stopCellEditing() {
isPushed = false;
return super.stopCellEditing();
}
}