JTableは、スプレッドシート(Excelのワークシートのような表)を扱う為のコンポーネント。
表データの列や行を操作(増減)する為にはTableModelを使用する。
表示されている形式(列の幅など)を変更するにはTableColumnModelを使用する。
|
|
|
|
|
JTableは、TableModelとTableColumnModelといったデータを内部に保持している。[2009-03-08]
JTableに対する操作(メソッド呼び出し)は、内部ではそれらのモデルクラスに委譲されている。
TableModelは、各行・各列の具体的なデータを保持・管理する。「テーブル」モデルと言うよりは、「データ」モデルという名前の方がしっくりくるような気がする。
「モデル」とだけ省略する場合は、基本的にはTableModelを指す。
実際、JDK内の変数名はdataModel(dm)だったりmodelだったりする。
TableColumnModelは、表示に関する情報(幅や表示形式など)を管理する。「表示」モデルという感じ?
JTableをインスタンス化すると、指定されていないモデルはデフォルトのモデルクラスでインスタンス化される。
その際、TableModelとTableColumnModelの列数は同じになる。
しかし、TableColumnModelの方だけ列を減らしたり、順序を入れ替えて表示したりすることが出来る。
| メソッド | 概要 | 更新日 | |
|---|---|---|---|
| JTable | setAutoResizeMode(モード) | 自動サイズ変更の有無を設定する。 | 2009-03-08 |
| getValueAt(int vr, int vc) | データを取得する。指定するのは表示行・列。 内部でconvertColumnIndexToModel()・convertRowIndexToModel()が使われる。 最終的にTableModel#getValueAt()が呼ばれる。 |
2009-03-11 | |
| setValueAt(Object v, int vr, int vc) | データをセットする。指定するのは表示行・列。 内部でconvertColumnIndexToModel()・convertRowIndexToModel()が使われる。 最終的にTableModel#setValueAt()が呼ばれる。 |
2009-03-11 | |
| convertColumnIndexToModel(int vc) | 表示用の列番号をデータモデル用の列番号に変換する。 TableColumnModelによっては表示列とモデル列が異なる場合がある。 内部ではTableColumnModel#getColumn(vc).getModelIndex()を呼び出している。 |
2009-03-11 | |
| convertRowIndexToModel(int vr) | 表示用の行番号をデータモデル用の行番号に変換する。JDK1.6以降。 ソートやフィルターによって、表示行とモデル行が異なる場合がある。 |
2009-03-11 | |
| convertColumnIndexToView(int mc) | データモデル用の列番号を表示用列番号に変換する。 | 2009-03-11 | |
| convertRowIndexToView(int mr) | データモデル用の行番号を表示用行番号に変換する。JDK1.6以降。 | 2009-03-11 | |
| setRowSorter(ソーター) | ソータやフィルターを設定する。 | 2009-03-13 | |
| JTable | getModel() | TableModelを取得する。 | 2009-03-08 |
| TableModel | addColumn(Object name) | 新項目を追加する。 | 2009-03-01 |
| getColumnCount() | データモデル内の項目(列)数を取得する。 | 2009-03-08 | |
| getColumnName(int mc) | データモデル内の項目名を取得する。 | 2009-03-08 | |
| addRow(Object[] mr) addRow(Vector mr) |
行(レコード)を追加する。 | 2009-03-01 | |
| getValueAt(int mr, int mc) | データを取得する。指定するのはモデル行・列。 | 2009-03-08 | |
| setValueAt(Object v, int mr, int mc) | データをセットする。指定するのはモデル行・列。 | 2009-03-08 | |
| DefaultTableModel | setRowCount(int rows) | 行(レコード)数をセットする。 たぶん主に、0をセットすることによってデータを全て削除するのに使う。 |
2009-04-08 |
| setColumnCount(int cols) | 列(項目)数をセットする。 | 2009-04-08 | |
| JTable | getColumnModel() | TableColumnModelを取得する。 | 2009-03-08 |
| TableColumnModel | getColumnCount() | 表示している列数を取得する。 | 2009-03-08 |
| getColumn(int vc) | 該当表示列のTableColumnを取得する。 | 2009-03-08 | |
| TableColumn | getModelIndex() | 紐付いているデータ(TableModel)の列番号を返す。 | 2009-03-08 |
| setHeaderValue(Object name) | 表示する項目名を設定する。 | 2009-03-08 | |
| setPreferredWidth(int w) | 項目(列)の幅を(後から)設定する。 | 2009-03-08 | |
| JTable | getTableHeader() | JTableHeaderを取得する。 | 2009-03-08 |
| JTableHeader | setReorderingAllowed(false) | ドラッグ&ドロップによる列の入れ替えを禁止する。 | 2009-03-08 |
import javax.swing.JTable; import javax.swing.table.DefaultTableModel;
/**
* スクロール機能付きテーブルの初期化
*/
private void initPane(Container c) {
Object[] cnames = { "項目1", "項目2", "項目3", };
DefaultTableModel model = new DefaultTableModel(cnames, 0);
JTable table = new JTable(model);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
JScrollPane scroll = new JScrollPane(table);
c.add(scroll);
}
setAutoResizeMode()によって自動サイズ変更をOFFにすると、表全体が左寄せになる。
デフォルトでは表全体が表示領域全体にぴったり収まるように列幅が均等に調整されて表示される。
DefaultTableModel#addColumn(Object name)で
データの項目(列)を追加する。
引数に指定するのは項目名だが、何故かObject型であり、デフォルトでは、toString()で文字列に変換されて列ヘッダーに表示される。
列の幅とかはTableColumnクラスで管理されているが、これはJTableから取得できるTableColumnModelで管理される。
TableColumnModel#addColumn(TableColumn
column)による追加は、データとは無関係に、表示用の列を追加する。[2009-03-08]
JTable#addColumn(TableColumn
column)は、TableColumnModel(表示のみ)の列追加となる。
(データモデルTableModelに指定されている項目名が列ヘッダーとなる)
DefaultTableModel#addRow()やinsertRow()でデータを追加する。
model.addRow(new Object[]{ "abc", 123, new Date(), Boolean.TRUE });
これも個々のデータはObject型で、基本的にはtoString()によって文字列に変換されて表示される。
しかし列毎にデータの型(クラス)あるいはレンダラーを明示することで、文字列以外のデータを表現(表示)することが出来る。
TableModel#getColumnClass()によって、列毎のデータの型を明示することが出来る。
デフォルトはObject.classを返すようになっているので、自分でオーバーライドして必要な型を返すようにする。
Object[] cnames = { "文字列", "数値", "日付", "真偽" };
DefaultTableModel model = new ClassModel(cname, 0);
model.addRow(new Object[]{ "abc", 123, new Date(), Boolean.TRUE });
public class ClassModel extends DefaultTableModel {
public ClassModel(Object[] columnNames, int rowCount) {
super(columnNames, rowCount);
}
@Override
public Class<?> getColumnClass(int columnIndex) {
switch (columnIndex) {
case 0: //文字列
return String.class;
case 1: //数値
return Integer.class;
case 2: //日付
return Date.class;
case 3: //真偽値
return Boolean.class;
default:
return Object.class;
}
}
}
ここで返されたクラスを元にレンダラーを決定(JTable#getDefaultRenderer(Class))し、そのレンダラーを使ってデータを描画しているようだ。
| 種類 | クラス | レンダラー | 描画 | |
|---|---|---|---|---|
| デフォルト | Object.class | DefaultTableCellRenderer$UIResource | JLabel | 左揃えの文字列 |
| 数値 | Number.class | JTable$NumberRenderer | JLabel | 右揃えの文字列 |
| Float.class Double.class |
JTable$DoubleRenderer | JLabel | 右揃えの文字列(NumberFormat.getInstance()) | |
| 日付 | Date.class | JTable$DateRenderer | JLabel | 左揃えの文字列(DateFormat.getDateInstance()) |
| アイコン | Icon.class ImageIcon.class |
JTable$IconRenderer | JLabel | 中央揃えのアイコン |
| 真偽値 | Boolean.class | JTable$BooleanRenderer | JCheckBox | 中央揃えのチェックボックス |
レンダラーを追加したい場合はJTable#setDefaultRenderer()を使用する。
なお、ぴったり一致するクラスでない場合は、スーパークラス(親クラス)に遡ってその型が使われる。
つまり、Integer.classを返しておけば、Number.classのレンダーが使われる。
最悪でもObject.classのレンダーが使われる(Stringはこのレンダーとなる)。
ただしプリミティブ型(int.class等)は使えない(NullPointerExceptionが発生する)。なぜなら、プリミティブ型はオブジェクト型ではないので、親クラスは無い(Objectを継承していない)から。
→クラスを指定せず(getColumnClass()をオーバーライドせず)にレンダラーやエディターを指定する方法
→ソートでもgetColumnClass()は使われる
表の表示に関する属性(列名、横幅のサイズ、レンダラー、編集可否、エディター)は、TableColumnというクラスで管理できる。[2009-03-08]
(レンダラーも、こちらを使う方がちょっと高速なのではないかと思う)
import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel;
/**
* テーブルの各列の表示方法を指定する
*/
private void initPane(Container c) {
Object[] cnames = { "文字列", "数値", "日付", "真偽" };
int[] width = { 128, 64, 256, 32 };
Class<?>[] cls = { String.class, Integer.class, Date.class, Boolean.class };
DefaultTableModel model = new DefaultTableModel(cnames, 0);
JTable table = new JTable(model);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
// 実験用:trueにすると、日付列の表示が消える
if (false) {
TableColumnModel columnModel = table.getColumnModel();
TableColumn tc = columnModel.getColumn(2);
columnModel.removeColumn(tc);
}
TableColumnModel columnModel = table.getColumnModel();
for (int i = 0; i < columnModel.getColumnCount(); i++) {
TableColumn tc = columnModel.getColumn(i);
int j = tc.getModelIndex();
int w = width[j];
System.out.printf("%d→%d: %3d%n", i, j, w);
//× tc.setWidth(w); ←これを使っても変更されない
tc.setPreferredWidth(w); //幅の設定
//レンダラー
tc.setCellRenderer(table.getDefaultRenderer(cls[j]));
//エディター
tc.setCellEditor(table.getDefaultEditor(cls[j]));
}
JScrollPane scroll = new JScrollPane(table);
c.add(scroll);
model.addRow(new Object[] { "abc", 123, new Date(), Boolean.TRUE });
}
※TableColumnに独自のレンダラーとエディターを設定した場合は、TableModel#getColumnClass()をオーバーライドする必要は無い。
が、そこで指定するのがデフォルトのエディター(GenericEditor)である場合は、結局オーバーライドしておく必要がある。[2009-03-11]
TableColumnModel自体は、JTableのインスタンス化の前に作成して、JTableのコンストラクターに渡すことが出来る。[2009-03-08]
(ただしJTable#getDefaultRenderer()やgetDefaultEditor()はJTableのインスタンス生成後にしか使えないので、レンダラーやエディターは後から改めてセットする必要がある)
private void initPane(Container c) {
Object[] cnames = { "文字列", "数値", "日付", "真偽" };
int[] width = { 128, 64, 256, 32 };
// データモデル初期化
DefaultTableModel model = new DefaultTableModel(cnames, 0);
// 項目モデル初期化
TableColumnModel columnModel = new DefaultTableColumnModel();
for (int i = model.getColumnCount() - 1; i >= 0; i--) { // データモデルから項目を逆順で取得
TableColumn tc = new TableColumn(i, width[i]);
tc.setHeaderValue(model.getColumnName(i)); // データモデルの項目名を取得してヘッダーにセット
columnModel.addColumn(tc);
}
JTable table = new JTable(model, columnModel);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
JScrollPane scroll = new JScrollPane(table);
c.add(scroll);
// 実際のデータを追加
model.addRow(new Object[] { "abc", 123, new Date(), Boolean.TRUE });
}
デフォルトでは、表示されている列ヘッダーをドラッグ&ドロップすることによって、表示列を入れ替えることが出来る。[2009-03-08]
(JTableHeaderのsetReorderingAllowed()でfalseを設定すると、入れ替えを禁止できる)
列を入れ替えると、TableColumnModel内のTableColumnの順序が入れ替わる。が、データモデル(TableModel)の方の順序は変わらない。
つまり、TableColumnModelに与えられる列インデックス(列番号)は、表示されている列そのものに対応している(一番左の列が0)。
そのインデックスを指定してTableColumnModel#getColumn(index)を呼び出してTableColumnを取得し、TableColumn#getModelIndex()で、その表示列に対応しているデータモデルのインデックスが取得できる。
(デフォルトであれば列の個数も並び順もデータモデルとカラムモデルに違いは無いのだが、実際は上述のような関係になっている)
import java.awt.BorderLayout; import java.awt.event.ActionEvent; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JPanel;
private JTable table;
private void initPane(Container c) {
〜
table = new JTable(〜);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
table.getTableHeader().setReorderingAllowed(true); // ドラッグ&ドロップによる列の入れ替えを許可する
JPanel panel = new JPanel(new BorderLayout());
{
JPanel p = new JPanel();
JButton btn = new JButton(new DebugAction("デバッグ表示"));
p.add(btn);
panel.add(p, BorderLayout.PAGE_START);
}
{
JScrollPane scroll = new JScrollPane(table);
panel.add(scroll);
}
c.add(panel);
}
/** ボタンの動作(アクション) */
class DebugAction extends AbstractAction {
private static final long serialVersionUID = -6779745415007382217L;
public DebugAction(String name) {
super(name);
}
@Override
public void actionPerformed(ActionEvent e) {
TableModel dm = table.getModel();
TableColumnModel cm = table.getColumnModel();
for (int i = 0; i < cm.getColumnCount(); i++) {
TableColumn tc = cm.getColumn(i);
int j = tc.getModelIndex();
Object name = tc.getHeaderValue(); // 項目名
Object data = dm.getValueAt(0, j); // 1行目のデータ
System.out.printf("%d→%d %s:%s%n", i, j, name, data);
}
}
}
データを表示形式に変換するのがレンダラーで、値を手入力で変更する際のGUIがエディター。[2009-03-08]
通常の状態ではレンダラーを使って表示されるが、セルをダブルクリックしたりF2キーを押したりして編集する際にはエディターに切り替わる。
(エディター用のコンポーネントがセルの位置に上書きで描画されて、編集できるように見えるらしい)
TableModel#getColumnClass()で列毎のクラスを返すようになっている場合、以下のエディターが使われる。
(ぴったり合致するクラスがない場合は親クラスに遡って判断される。レンダラーと同様)
(デフォルトではObject.classのエディターしか使われない)
| 種類 | クラス | エディター | 描画 | |
|---|---|---|---|---|
| デフォルト | Object.class | JTable$GenericEditor | JTextField | 左揃えの文字列 |
| 数値 | Number.class | JTable$NumberEditor | JTextField | 右揃えの文字列 |
| 真偽値 | Boolean.class | JTable$BooleanEditor | JCheckBox | 中央揃えのチェックボックス |
真偽値はチェックボックスでチェックを入れたり外したりして値を入力できる。[2009-03-11]
数値型のNumberEditorはGenericEditorを継承しており、ただ単に右揃えになっているだけ。
GenericEditorは汎用的なエディターで、与えられたデータ(初期値)をtoString()で文字列に変換してJTextFieldで
ユーザーに編集させる。
その結果を、JTable#getColumnClass()(実体はTableModel#getColumnClass())で取得したクラスの、引数がStringであるコンストラクターを用いて元の(期待されている)データ型に変換する。
Stringを引数に持つコンストラクターが無いクラスではGenericEditorは使えない。
また、そのコンストラクターを用いて変換する際に、変換できなかった場合(何らかの例外が発生した場合)は変換エラーとなり、編集を完了できない。赤枠が表示され、編集続行となる。
例えばInteger型(TableModel#getColumnClass()がInteger.classを返す)の場合、Integer#toString()を用いて編集用の文字列を作り、new
Integer(String)を用いて編集(入力)された文字列をIntegerに変換する。
その際にパースできない文字が含まれていた場合は変換エラーとなり、編集が完了しない。
TableModel#getColumnClass()をオーバーライドしていない場合はObject.classとなるが、その場合はStringでそのまま返る。この場合は変換が行われないので、数字以外を入れていてもエラーにならない。
NumberRedererは実は文字列を右揃えにしているだけなので、入力された文字列が(数字でなくても)そのまま表示される。
Date型の場合、toString()だと「EEE MMM dd HH:mm:ss zzz yyyy」形式となり、new
Date(String)はその形式を解釈できない(苦笑)
→日付エディターの作成
日付(Date型)は、日付のデフォルトレンダラー(JTable$DateRenderer)は「yyyy/MM/dd」形式で表示してくれるのだが、
日付専用のデフォルトエディターは無い。(JTable$GenericEditor(すなわちJTableの内部クラスGenericEditor)が使われる)[2009-03-08]
なのでDate#toString()のデフォルト形式「EEE MMM dd HH:mm:ss zzz yyyy」で表示されてしまう。
また、TableModel#getColumnClass()をオーバーライドしてDate.classを返すようになっていない場合、編集結果そのものであるStringがレンダラーに渡る為、DateRendererを使うようになっている場合は期待しているDate型でないので例外が発生し、レンダリングできない。
Date.classを返すようになっている場合でも、「EEE MMM dd HH:mm:ss zzz
yyyy」形式は解釈できず、変換エラーになる(赤枠が表示されて編集続行になる)。この場合は「yyyy/MM/dd」形式で入力し直してやればエラーにはならないが。[2009-03-11]
という訳で、「yyyy/MM/dd」形式で表示・登録できる日付エディターを作ってみた。
import java.text.DateFormat; import java.text.ParsePosition; import java.util.Date; import javax.swing.DefaultCellEditor; import java.awt.Color; import javax.swing.border.LineBorder;
private void initPane(Container c) {
Class<?>[] cls = { String.class, Integer.class, Date.class, Boolean.class };
〜
JTable table = new JTable(〜);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
// Date.classのエディターとして自作エディターを指定
table.setDefaultEditor(Date.class, new DateEditor());
TableColumnModel cm = table.getColumnModel();
for (int i = 0; i < cm.getColumnCount(); i++) {
TableColumn tc = cm.getColumn(i);
int j = tc.getModelIndex();
tc.setCellRenderer(table.getDefaultRenderer(cls[j]));
tc.setCellEditor(table.getDefaultEditor(cls[j]));
}
JScrollPane scroll = new JScrollPane(table);
c.add(scroll);
}
/** 日付エディター */
class DateEditor extends DefaultCellEditor {
private static final long serialVersionUID = 7018363642358649067L;
private DateFormat formatter;
protected Date dvalue;
/** コンストラクター. */
public DateEditor() {
super(new JTextField());
}
protected DateFormat getFormatter() {
if (formatter == null) {
formatter = DateFormat.getDateInstance();
formatter.setLenient(false);
}
return formatter;
}
/**
* 編集開始時に呼ばれる.
*
* @param table 対象テーブル
* @param value 編集対象データ(初期値)
* @param isSelected
* @param row データのある行番号
* @param column データのある列番号
* @return 描画用コンポーネント
*/
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
this.dvalue = null; //初期化
((JComponent) getComponent()).setBorder(new LineBorder(Color.black)); //最初は黒枠
// row,columnは、TableColumnModelの体系(つまり表示されていたセルの位置)
// System.out.printf("getComponent(%b,%d,%d)+++%s %s%n", isSelected, row, column, value.getClass(), value);
DateFormat f = getFormatter();
String str = (value == null) ? "" : f.format(value);
return super.getTableCellEditorComponent(table, str, isSelected, row, column);
}
/**
* 編集完了時に呼ばれる.
* <p>super.stopCellEditing()を呼び出すと正常終了扱いとなる模様。</p>
*
* @return trueの場合、正常終了(と思われるが、実際には何を返しても無関係っぽい)
*/
@Override
public boolean stopCellEditing() {
Date d = null;
String str = (String) super.getCellEditorValue(); //編集された値の取得
if (str != null && !str.isEmpty()) {
DateFormat f = getFormatter();
ParsePosition pos = new ParsePosition(0);
d = f.parse(str, pos);
if (pos.getErrorIndex() >= 0) {
// エラー時は赤枠を表示する
((JComponent) getComponent()).setBorder(new LineBorder(Color.red));
return false;
}
}
this.dvalue = d; //返す値(編集された値)を保持しておく
return super.stopCellEditing();
// このsuper.stopCellEditing()の中でgetCellEditorValue()が呼ばれ、
// JTable#setValueAt()によって値がセットされ、
// 編集用コンポーネントが消され、
// 編集が完了する
}
/**
* 入力された値(加工済み)を返す.
*
* @return 編集された値
*/
@Override
public Object getCellEditorValue() {
return dvalue;
}
}
エディターを自作するには、JTable内でデフォルトとして使われているJTable$GenericEditorを継承したいところだが、このクラスはパッケージプライベートなので、(不可視だから)継承できない。
仕方が無いのでDefaultCellEditorからextendsする。
コンストラクターでは、編集(描画)に使用されるコンポーネントの具象クラスを作成する。
上記の例では日付の文字列を入力するので、JTextFieldを指定している。
他にJCheckBox(チェックボックス:boolean用)とJComboBox(コンボボックス/ドロップダウンリスト)が使える。
セルをダブルクリックしたりF2キーを押したりすると、編集が開始される。
編集開始時点で、編集対象となったセルの位置と現在値(データモデルで保持されていた値)がgetTableCellEditorComponent()に渡される。
上記の例ではDate型(のはず)なので、DateFormatを使って文字列(JTextField用)に変換している。
Enterキーを押して編集が完了されるときにstopCellEditing()が呼ばれる。
ここでスーパークラスのstopCellEditing()を呼ぶと、編集が本当に完了となる。
値の精査を行ってエラー(不正な値)だった場合は、スーパークラスのメソッドを呼ばないで戻れば完了にならない(編集続行となる)。
その際、コンポーネントの周囲(枠)を赤色に変えている。(GenericEditorがそういう実装になっているので、真似してみた)
(Editorインスタンスは使い回されるので、編集開始時点で枠を黒に戻している)
最後にgetCellEditorValue()が呼ばれるので、入力された値を返す。
この値がデータモデルやレンダラーに渡されるので、レンダラーが解釈できる型にする必要がある。
(デフォルトのGenericEditorでは、JTextFieldに入力されていた値をそのまま返す。つまりStringを返す。が、Date型のレンダラーはDateしか受け付けないので、クラスがアンマッチで例外が発生することになる)
(Enterキーで完了させずに)Escキーでキャンセルした場合は、上記のメソッドは呼ばれず、自動的に入力前の値に戻る。
(というか、たぶんエディターがそのまま終了してデータモデル内のデータに変化が無いだけだと思うけど)
デフォルトでは、文字キーを押して文字列の編集を開始した場合、セルに入っている既存文字列の末尾への追加となる。[2009-03-15]
でも感覚的には、いきなりキーを押した場合は以前の文字列をクリアして新規入力、F2キーを押してセル編集を開始した場合は追加入力となるようにしたい。
エディターで使われているJTextFieldの初期状態で文字列全体を選択状態にしておけば、初回のキー入力の際に以前の文字列が消えてくれる。(てゆーか、素直にクリアしてもいいかも?)
エディターの初期処理ではどんなキーが押されたかの判断は出来ない(キー情報は来ていない)ので、その呼出元で判断してやれば良い。
@Override
public boolean editCellAt(int row, int column, EventObject e) {
boolean r = super.editCellAt(row, column, e); // ここでエディターに初期値が設定される
if (r) {
if (e instanceof KeyEvent) {
KeyEvent ke = (KeyEvent) e;
char ch = ke.getKeyChar();
if (!Character.isISOControl(ch)) { //普通の文字の場合
if (super.editorComp instanceof JTextField) {
JTextField tf = (JTextField) super.editorComp;
tf.selectAll(); //既存文字列を全選択
}
}
}
}
return r;
}
※エディター(TableCellEditor)にはshouldSelectCell(イベント)という、イベントに対して編集できるかどうか事前に判定する為っぽいメソッドがあるのだが、JTableではそのメソッドは呼ばれないらしい。
デフォルトのエディターにはUNDO/REDO機能は無い。UNDO/REDOを行うには、UndoManagerを登録する。[2009-03-15]
デフォルトエディターの場合、JTable内のメソッドでエディター内のJTextFieldインスタンスを取得することが出来るので、それに対してUndoManagerを登録することが出来る。
ただし、エディターのインスタンスは使い回されるので、UndoManagerも共通となる。すなわち、別セルの編集中にUNDOすると、以前のセルの内容まで戻ってしまう。
そこで、編集開始時にUndoManagerの中身をクリアしてやる必要がある。
(編集開始時を捕らえるイベントがどれなのかよく分からないが…AncestorListener#ancestorAdded()はコンポーネントが可視になる時に呼ばれるようなので、これでいいかな?)
@Override
protected void createDefaultEditors() {
super.createDefaultEditors();
DefaultCellEditor editor;
editor = (DefaultCellEditor) getDefaultEditor(Object.class);
installUndoManager(editor);
editor = (DefaultCellEditor) getDefaultEditor(Number.class);
installUndoManager(editor);
}
protected UndoManager um = new UndoManager();
private void installUndoManager(DefaultCellEditor editor) {
JTextField field = (JTextField) editor.getComponent();
〜UndoManagerを登録〜
field.addAncestorListener(new AncestorListener() {
@Override
public void ancestorAdded(AncestorEvent event) {
// 編集開始時にUndoManagerの中身をクリアする
um.discardAllEdits();
}
@Override
public void ancestorMoved(AncestorEvent event) {
}
@Override
public void ancestorRemoved(AncestorEvent event) {
}
});
}
自作エディターでUndoManagerを登録したい場合は、コンストラクターでsuper(new JTextField())を呼び出しているだろうから、これをUNDO/REDO機能付きJTextFieldに置き換えてやれば良い。[2009-03-15]
編集の開始はgetTableCellEditorComponent()が呼ばれた時だと分かっているので、その中でUndoManagerをクリアしてやればよい。
public class 自作エディター extends DefaultCellEditor {
/** コンストラクター */
public 自作エディター() {
super(new UndoTextField());
getComponent().setHorizontalAlignment(JTextField.LEFT);
}
@Override
public UndoTextField getComponent() { //共変戻り値
return (UndoTextField) super.getComponent();
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
getComponent().setBorder(new LineBorder(Color.black));
UndoTextField utf = (UndoTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column);
utf.clearUndoEdit(); //UndoManagerの中身をクリアする
return utf;
}
〜
}
ところで、F2キーやダブルクリックによって編集を開始した場合は上記の方法でUNDO/REDO(Ctrl+Z、Ctrl+Y)が行えるが、文字を直接入力して開始した場合は、Ctrl+Z・Ctrl+Yが効かない。[2009-03-15]
何故かというと、文字キーを直接押した場合は、テキストフィールド(編集用コンポーネント)にフォーカスが設定されない為。
したがって、コンポーネントにKeyListenerを登録しておいても、そのメソッド(Ctrl+ZやYの処理)は呼ばれない。
文字キーを押して編集を開始した場合にコンポーネントにフォーカスを与えるかどうかの設定がJTableにあるので、それを設定しておく。(デフォルトはfalse)
table.setSurrendersFocusOnKeystroke(true);
セルの内容の編集時に右側にボタンを表示し、そのボタンを押した時にはダイアログを出して別途値を入力する、という操作を考えてみる。[2009-04-01]
エディターは結局コンポーネントを返すので、JPanelでテキストエリアとボタンを保持し、そのJPanelを返せばよい。
(この方法の場合、ボタンの代わりにチェックボックス等の別のコンポーネントを使っても大丈夫っぽい)
親クラスとしてDefaultCellEditorを使う場合、コンストラクターはJTextField・JComboBox・JCheckBoxしか用意されていない。
そこで、(最終的に呼び出し元に返したいのはJPanelだが、)コンストラクターにはJTextFieldを渡しておく。
getComponent()でそのJTextFieldを取得することが出来るので、改めてJPanelに追加してやればいい。
親クラスとしてAbstractCellEditorを使う(TableCellEditorもimplementsする)という方法も当然考えられる(というか そっちの方が素直?)が、編集開始のクリック数を設定したりしたい場合はDefaultCellEditorを使う方が便利。
class ボタン付きエディター extends DefaultCellEditor {
JPanel panel;
JTextField text;
/** コンストラクター */
public ボタン付きエディター() {
super(new JTextField()); //親クラスにはJTextFieldを渡しておく
text = getComponent(); //親クラスに渡したJTextField
JButton button = new JButton(new InAction());
//デフォルトのボタンは横幅が広すぎるので、最小にしておく
Insets iset = button.getMargin();
iset.left = iset.right = 0;
button.setMargin(iset);
panel = new JPanel() {
@Override
public void requestFocus() { //パネルにフォーカスが移る際は
text.requestFocus(); //テキストへフォーカスを移す
}
};
panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
panel.add(text);
panel.add(button);
}
@Override
public JTextField getComponent() { //共変戻り値
return (JTextField) super.getComponent();
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
text.setBorder(new LineBorder(Color.black)); //テキストエリアだけ枠を付ける
super.getTableCellEditorComponent(table, value, isSelected, row, column);
//↑親クラスを呼び出して準備作業だけは行うが
// 返すのはパネル↓
return panel;
}
class InAction extends AbstractAction {
public InAction() {
super("…"); //ボタンに表示する文言
}
@Override
public void actionPerformed(ActionEvent e) {
// 入力ダイアログを表示
String str = JOptionPane.showInputDialog("入れてちょ", text.getText());
if (str != null) {
text.setText(str); //テキストエリアに値を設定
}
text.requestFocusInWindow(); //テキストエリアにフォーカスを戻しておく
}
}
JPanelでは、requestFocus()をオーバーライドしておく。
マウスをダブルクリックして編集を開始した場合はテキストエリアにフォーカスが来るので問題ないが、キーを押して編集を始めた場合は、テキストエリアにフォーカスを渡しておかないと入力されない。
JDK1.6かららしいが、TableRowSorterというクラスにより、ヘッダーをクリックした際にデータ(各行)をソートできる。[2009-03-13]
import javax.swing.RowSorter; import javax.swing.table.TableRowSorter;
private void initPane(Container c) {
JTable table = new JTable(〜);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
// ソートの設定
RowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
table.setRowSorter(sorter);
〜
}
なお、自分でsetRowSorter()によってソーターをセットしなくても、JTable#setAutoCreateRowSorter(true)を呼び出しておけば、自動的にTableRowSorterが使われる。[2009-04-02]
table.setAutoCreateRowSorter(true);
TableRowSorterのデフォルトでは、toString()を用いてデータを変換して比較するらしい。
データモデル(TableModel)のgetColumnClass()をオーバーライドしておけば、そこで返されるクラスがComparableを実装していればそれが使われる。
もしくは、TableRowSorter#setComparator()によって列ごとにComparatorを登録しておけば、それが使われる。[2009-04-02]
また、(ソーターという名前のクラスではあるけれども)フィルター機能も有しているらしい。
表示されている行の行番号とデータモデル内の行番号との変換は、JTableのコンバートメソッド(convertRowIndexToModel()やconvertRowIndexToView())によって行う。
(実体はDefaultRowSorter#convertRowIndexToModel()とconvertRowIndexToView())
JTableでは、デフォルトで行が選択できる。[2009-04-04]
つまり、セルをクリックすると、行全体が選択される。(getRowSelectionAllowed()がtrueでgetColumnSelectionAllowed()はfalse)
列も選択できるようにするには、JTable#setColumnSelectionAllowed(true)を呼び出す。
これで、行内のセルが個別に選択できるようになる。
デフォルトではCtrlキーを押しながら選択することで複数行を選択することが出来る。
これはJTable#setSelectionMode()でモードを指定することによって変更できる。
| 定数 | 内容 | |
|---|---|---|
| ListSelectionModel | SINGLE_SELECTION | 1行だけ選択可能。 |
| SINGLE_INTERVAL_SELECTION | 間をとばさずに範囲選択可能。(Shiftキー) | |
| MULTIPLE_INTERVAL_SELECTION | 複数行を選択可能。(Shiftキー+Ctrlキー) | |
選択されている行・列の保持はリストセレクションモデルで行われている。
行と列は別々のモデル(クラスは同じだが別インスタンス)で保持されており、行の分はJTable#getSelectionModel()で、列の分はTableColumnModel#getSelectionModel()で取得できる。
これらの選択方法は行と列を指定する方式なので、個々のセルを自由に操作すること(ある範囲内の1セルだけを選択しないといったような操作)は出来ない。
細かい制御を行いたい場合は、以下のようなメソッドをオーバーライドして独自実装する。
| メソッド | 概要 | |
|---|---|---|
| JTable |
changeSelection(int row, int column, boolean toggle, boolean extend) |
セルがクリックされて選択される時に呼ばれる。 toggleはCtrlキーが押されている時trueになる。 extendはShiftキーが押されている時trueになる。 |
| clearSelection() | 選択を全て解除する。 | |
| isCellSelected(int row, int column) | セルが選択されているかどうかを返す。 | |
| isRowSelected(int row) isColumnSelected(int column) |
行や列が選択されているかどうかを返す。 デフォルトではisCellSelected()から呼ばれる。 |
|
| getSelectedRowCount() getSelectedColumnCount() |
||
|
getSelectedRows() getSelectedColumns() |
選択されている行・列番号を全て返す。 | |
| getSelectedRow() getSelectedColumn() |
||
セルが選択されていると、背景色(や前景色)が変わる。
これはレンダラーが処理している。
TableCellRenderer#getTableCellRendererComponent(JTable
table, Object value, boolean isSelected, boolean hasFocus, int row, int
column)において、
引数isSelectedがtrueなら選択されているので、その場合に背景色を変えている(setBackground(table.getSelectionBackground()))。
列ヘッダー(各列の最上部)はJTableHeaderで管理される。[2009-03-13]
例えば列ヘッダーの列と列の間をマウスでドラッグすると列の幅を変えることが出来るが、これはJTableHeaderに登録されたイベントリスナーで処理されている。
| クラス | メソッド | 内容 |
|---|---|---|
| BasicTableHeaderUI$MouseInputHandler | mouseClicked() | ソーターが登録されている場合、ヘッダーをクリックするとソート順が変わる。 そのクリック時動作が当メソッド。 内部でRowSorter#toggleSortOrder()を呼び出し、ソート順が変更される。 |
| mouseMoved() | ヘッダー上の列と列の間にマウスカーソルを移動させると、列幅変更のカーソルに変わる。 その変更を行っているのが当メソッド。 |
|
| mouseDragged() | ヘッダーをドラッグ&ドロップすると、列を入れ替えることが出来る。 それを行っているのが当メソッド。 |
ソート機能を有効にしている場合、ヘッダーを左クリックすると行の表示順序が変わる。
一方、ヘッダーの列と列の間にマウスが移動すると、矢印マーク(列幅変更)のマウスカーソルに変わる。
この矢印マークに変わっている間も、左クリックすると行のソートが行われる。
…これはちょっと違和感のある動作なので、この場合はソートしないようにしたい。[2009-03-13]
イベントリスナーを登録することでマウス操作時のイベントを捕捉することは出来るのだが、自分で登録するリスナーはデフォルトリスナーの処理後に呼ばれる。
したがって、デフォルトリスナーでの処理を停止させることは出来ない。
そこで、JTableHeaderそのもののマウスイベント処理メソッドをオーバーライドする(このメソッドがリスナーを呼び出している為)。
JTableに対して後から独自JTableHeaderをセットすればよいが、その場合はデフォルトのJTableHeaderインスタンスが作られる分が無駄になってしまうので、デフォルトJTableHeaderインスタンス生成自体を独自のJTableHeaderに置き換えてやる。
import javax.swing.table.JTableHeader;
private void initPane(Container c) {
JTable table = new JTable(〜) {
@Override
protected JTableHeader createDefaultTableHeader() {
return new SampleHeader(super.columnModel);
}
};
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
// ソートの設定
RowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
table.setRowSorter(sorter);
〜
}
class SampleHeader extends JTableHeader {
/** コンストラクター */
public SampleHeader(TableColumnModel columnModel) {
super(columnModel);
}
@Override
protected void processMouseEvent(MouseEvent e) {
if (e.getID() == MouseEvent.MOUSE_CLICKED // クリックイベント
&& SwingUtilities.isLeftMouseButton(e)) { // 左クリック
Cursor cur = super.getCursor();
if (cur.getType() == Cursor.E_RESIZE_CURSOR) { // 矢印カーソル
// ここでリターンしない場合、ソート機能が働いてしまう
return;
}
}
super.processMouseEvent(e);
}
}
ヘッダーの列と列の間にマウスカーソルがある(列幅変更の矢印マークになっている)ときにダブルクリックして、データの最大幅に自動的に合わせる方法。[2009-03-13]
これはイベントリスナーをJTableHeaderに登録してやれば実現できるが、そのままではソートも行われてしまう。
(ダブルクリックの1回目のクリックでソート機能が働くから)
そこで前述の事前キャンセル機能も組み込む必要があり、そうであればイベントリスナーをわざわざ作らなくても、そこでイベント処理してしまってもよいかと。
class SampleHeader extends JTableHeader {
/** コンストラクター */
public SampleHeader(TableColumnModel columnModel) {
super(columnModel);
}
@Override
protected void processMouseEvent(MouseEvent e) {
if (e.getID() == MouseEvent.MOUSE_CLICKED // クリックイベント
&& SwingUtilities.isLeftMouseButton(e)) { // 左クリック
Cursor cur = super.getCursor();
if (cur.getType() == Cursor.E_RESIZE_CURSOR) { // 矢印カーソル
int cc = e.getClickCount();
if (cc % 2 == 1) {
// シングルクリック
// ここでリターンしない場合、ソート機能が働いてしまう
return;
} else {
// ダブルクリック
Point pt = new Point(e.getX() - 3, e.getY()); // 列幅変更の場合、3ピクセルずらされて考慮されている
int vc = super.columnAtPoint(pt);
if (vc >= 0) {
sizeWidthToFitData(vc);
e.consume();
return;
}
}
}
}
super.processMouseEvent(e);
}
/**
* データの幅に合わせる.
*
* @param vc 表示列番号
*/
public void sizeWidthToFitData(int vc) {
JTable table = super.getTable();
TableColumn tc = table.getColumnModel().getColumn(vc);
int max = 0;
int vrows = table.getRowCount(); // 表示行数
for (int i = 0; i < vrows; i++) {
TableCellRenderer r = table.getCellRenderer(i, vc); // レンダラー
Object value = table.getValueAt(i, vc); // データ
Component c = r.getTableCellRendererComponent(table, value, false, false, i, vc);
int w = c.getPreferredSize().width; // データ毎の幅
if (max < w) {
max = w;
}
}
tc.setPreferredWidth(max + 1); // +1してやらないと、省略表示になってしまう
}
}
ちなみにTableColumn#sizeWidthToFit()というメソッドもあるのだが、デフォルトではヘッダーレンダラーはnullなので、何も処理されない。
ヘッダーレンダラーが定義されている場合は、そのレンダラーを用いて、ヘッダーデータの幅に合わせて列幅が自動調節されるらしい。
行ヘッダー(各行の左側の見出し)をJTableで直接実現する機能は用意されていないらしい。[2009-03-14]
行ヘッダー用のJTableをもう一つ作成し、JScrollPaneの行ヘッダー機能を使って描画すれば良いそうだ。
行ヘッダーの各行は、デフォルトでは背景が白くなってしまうので、レンダラーを別途作成して背景を列ヘッダーと同じ色(灰色)にしてやる。
private void initPane(Container c) {
// データ用テーブル
JTable table = new JTable(〜);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
RowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
table.setRowSorter(sorter);
// 行ヘッダー用テーブル
JTable rhTable = new RowHeaderTable(table, "HEAD", 48);
// スクロールペイン
JScrollPane scroll = new JScrollPane(table);
// スクロールペインの行ヘッダーに登録
scroll.setRowHeaderView(rhTable);
scroll.setCorner(JScrollPane.UPPER_LEFT_CORNER, rhTable.getTableHeader());
Dimension sz = new Dimension(rhTable.getPreferredSize().width, table.getPreferredSize().height);
scroll.getRowHeader().setPreferredSize(sz); // 行ヘッダーのサイズ
}
import javax.swing.table.DefaultTableCellRenderer;
class RowHeaderTable extends JTable {
private static final long serialVersionUID = 1L;
/**
* 行ヘッダーテーブル コンストラクター
*
* @param dataTable データ用JTable
* @param name 行ヘッダーの列見出し
* @param width 行ヘッダーの幅
*/
public RowHeaderTable(JTable dataTable, String name, int width) {
super(new RowHeaderDataModel(dataTable), null, new RowHeaderSelectionModel(dataTable));
// 唯一の列
{
TableColumn tc = new TableColumn(0, width);
// 中央揃え・背景灰色のレンダラーを登録する
DefaultTableCellRenderer r = new DefaultTableCellRenderer();
r.setHorizontalAlignment(SwingConstants.CENTER);
r.setBackground(super.getTableHeader().getBackground());
tc.setCellRenderer(r);
tc.setHeaderValue(name); // 列見出し(項目名)
tc.setResizable(false); // サイズ変更禁止
super.addColumn(tc);
}
// 列ヘッダー(列見出し)
{
JTableHeader h = super.getTableHeader();
h.setReorderingAllowed(false); // 列の入れ替え(ドラッグ)を禁止
h.addMouseListener(new RowHeaderCHMouseListener(dataTable)); // ソート解除処理を登録
}
// データ用テーブルにソーターがある場合、リスナーを登録する
RowSorter<? extends TableModel> sort = dataTable.getRowSorter();
if (sort != null) {
sort.addRowSorterListener(new RowHeaderSortListener(this));
}
super.setEnabled(false);
}
}
import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.DefaultTableModel;
class RowHeaderDataModel extends DefaultTableModel implements TableModelListener {
private static final long serialVersionUID = 1L;
protected JTable dataTable;
public RowHeaderDataModel(JTable dataTable) {
this.dataTable = dataTable; // データ用テーブル
TableModel dataModel = dataTable.getModel();
dataModel.removeTableModelListener(this);
dataModel.addTableModelListener(this);
}
@Override
public Object getValueAt(int row, int column) {
// データ用テーブルの行番号を返す
// RowHeaderTable自身はソートやフィルタリングは行われない為、引数rowは表示用行番号と一致している
return dataTable.convertRowIndexToModel(row) + 1;
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return Integer.class;
}
@Override
public void setValueAt(Object aValue, int row, int column) {
// 値の登録は行わない
}
@Override
public boolean isCellEditable(int row, int column) {
return false; // 編集不可
}
@Override
public int getRowCount() {
if (dataTable != null) {
return dataTable.getRowCount(); // データテーブルの表示行数を返す
}
return 0;
}
/*
* TableModelListenerのメソッドであり、データ用TableModelに変更があったときに呼ばれる。
*/
@Override
public void tableChanged(TableModelEvent e) {
switch (e.getType()) {
case TableModelEvent.INSERT: // 行追加
case TableModelEvent.DELETE: // 行削除
super.fireTableChanged(e);
break;
default:
//System.out.println("tableChanged:" + e.getType());
break;
}
}
}
}
import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent;
class RowHeaderCHMouseListener extends MouseAdapter {
protected JTable dataTable;
public RowHeaderCHMouseListener(JTable dataTable) {
this.dataTable = dataTable;
}
// 行ヘッダーの列見出しがクリックされた時に呼ばれる
@Override
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
int cc = e.getClickCount();
if (cc == 1) {
RowSorter<? extends TableModel> sort = dataTable.getRowSorter();
if (sort != null) {
sort.setSortKeys(null); // ソートを解除する
e.consume();
}
}
}
}
}
import javax.swing.event.RowSorterEvent; import javax.swing.event.RowSorterListener;
class RowHeaderSortListener implements RowSorterListener {
private JTable table;
public RowHeaderSortListener(RowHeaderTable table) {
this.table = table;
}
// データ用テーブルでソートが行われた時に呼ばれる
// 行ヘッダーの再描画を行う
@Override
public void sorterChanged(RowSorterEvent e) {
table.revalidate();
table.repaint();
}
}
import javax.swing.DefaultListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener;
class RowHeaderSelectionModel extends DefaultListSelectionModel implements ListSelectionListener {
private static final long serialVersionUID = 5416695784129610563L;
protected ListSelectionModel selectModel;
public RowHeaderSelectionModel(JTable dataTable) {
this.selectModel = dataTable.getSelectionModel();
selectModel.removeListSelectionListener(this);
selectModel.addListSelectionListener(this);
}
//ListSelectionModelインターフェースで宣言されている全メソッドに対して、selectModelのメソッドを呼ぶようにする(委譲する) @Override public void setSelectionInterval(int index0, int index1) { selectModel.setSelectionInterval(index0, index1); } @Override public void addSelectionInterval(int index0, int index1) { selectModel.addSelectionInterval(index0, index1); } @Override public void removeSelectionInterval(int index0, int index1) { selectModel.removeSelectionInterval(index0, index1); } 〜 @Override public void addListSelectionListener(ListSelectionListener x) { selectModel.addListSelectionListener(x); } @Override public void removeListSelectionListener(ListSelectionListener x) { selectModel.removeListSelectionListener(x); }
/* * ListSelectionListenerのメソッドであり、データ用テーブルで行選択が変更されたときに呼ばれる */ @Override public void valueChanged(ListSelectionEvent e) { int fi = e.getFirstIndex(), li = e.getLastIndex(); super.removeSelectionInterval(fi, li); for (int i = fi; i <= li; i++) { if (selectModel.isSelectedIndex(i)) { super.addSelectionInterval(i, i); } } } }
RowHeaderTableが、行ヘッダー専用のJTable。
コンストラクターにデータ用JTableを渡して内部で保持し、後続の各処理でデータ用JTableの各モデルを呼び出す。
RowHeaderDataModelが、行ヘッダー専用のTableModel。
データ用JTableに合わせて行番号を返したりしている。
また、TableModelListenerを実装することにより、データ用JTableで行の追加や削除が行われたときに行ヘッダーJTableも変更を行っている。
RowHeaderCHMouseListenerは、行ヘッダーの列見出しのイベントリスナー。
データ用JTableでソート機能を有効にした場合、デフォルトのソート機能では元のソート状態に戻すことが出来ない。
そこで、行ヘッダーの列見出しをクリックした場合に元のソート順に戻すようにしてみた。
なお、行ヘッダーそのものはJTableをJScrollPaneの行ヘッダービューに登録(setRowHeaderView())すれば表示されるが、それだけでは行ヘッダーの見出しは表示されない。
別途JTableHeaderを取り出し、JScrollPaneの左上コーナー(隅)に登録(setCorner())してやる必要がある。(逆に言えば、列見出しを表示する必要が無いなら、JTableHeaderを登録する必要は無い)
RowHeaderSortListenerは、データ用JTableのソートが行われた時に呼ばれるイベントリスナー。
データの並び順が変わったら行ヘッダーを再描画してやらないと、番号が変わらない。
RowHeaderSelectionModelは、行の選択・解除を処理する。
行ヘッダーで選択された場合はデータ用セレクションモデルに処理を委譲する。
イベントリスナーも実装している為、データ用セレクションモデルで行選択・解除の処理が行われた場合にvalueChanged()が呼ばれる。
ここだけ、行ヘッダーのセレクションモデル(superクラス)の処理を呼び出している。
(行を選択しながらドラッグすると複数行を選択できる。その際、通常はマウスが列をまたがっても問題ないのだが、上記クラスでは、行ヘッダーとデータテーブル間をまたがる事は出来ない…まぁそれくらいは目をつぶるということで^^;)
セルの値の変更に関してUNDO/REDO機能を追加するのも、UndoManagerを使う。[2009-04-25]
UNDO/REDO情報の登録方法はUndoManagerのJTableの例を参照。
実際にUNDO/REDOを行う為には、メニューバーやポップアップメニュー、あるいはアクセラレーターキーでUNDO/REDO処理を呼び出すようコーディングする必要がある。
(UndoAction・RedoActionを自作する)
Action undo = new UndoAction(um); Action redo = new RedoAction(um); InputMap im = table.getInputMap(); im.put((KeyStroke) undo.getValue(Action.ACCELERATOR_KEY), undo.getValue(Action.NAME)); im.put((KeyStroke) redo.getValue(Action.ACCELERATOR_KEY), redo.getValue(Action.NAME)); ActionMap am = table.getActionMap(); am.put(undo.getValue(Action.NAME), undo); am.put(redo.getValue(Action.NAME), redo);
これでCtrl+ZやCtrl+Yを押した時にUNDO/REDOが出来る。
しかし、UNDO/REDOが出来ない場合、Ctrl+ZやCtrl+Yを押すとセルの編集開始になってしまう。
(編集可能なJTableの場合、何らかのキーを押すと編集開始になる)
(UNDO/REDOが出来ない場合、Ctrl+ZやCtrl+Yのキー入力は処理されず、デフォルト処理が行われてしまう)
そこで、コントロールキーが押されている場合はセルの編集が開始されないようにしておくと、いい感じ。
class UndoTable extends JTable {
〜
@Override
public boolean editCellAt(int row, int column, EventObject e) {
if (e instanceof KeyEvent) {
KeyEvent ke = (KeyEvent) e;
if (ke.isControlDown()) {
return false;
}
}
return super.editCellAt(row, column, e);
}
}
editCellAt()は、セルの編集を行う時に呼ばれるメソッド。
JTableには、isCellEditable()という、セルが編集可能かどうかを判定するメソッドもあるが、それはイベントで判定できない。
行が追加・削除された場合も、セルのUNDO/REDOと同様に処理を作れる。
行が変更された場合は、データモデル内でイベントが発生する。
ただしこれは、あくまで変更があった後に呼ばれる。
行の追加の場合、REDO時に戻すデータはこのイベント内で(既にテーブルに追加されているので)取得できる。
行の削除の場合、UNDO時にデータを復帰させたいが、削除後に呼ばれるメソッドでは削除前のデータが取得できない。
(変更前に呼ばれるメソッドがあればいいんだけどなぁ)
そこで、上記のfire系メソッドを呼び出している(イベント発生をさせる)メソッドでUNDOデータの保持を行うことになる。
| メソッド | 呼び出しているfire | 処理内容 |
|---|---|---|
| insertRow() | fireTableRowsInserted | 行の挿入。 addRow()等も最終的にはinsertRow()を呼び出している。 |
| moveRow() | fireTableRowsUpdated | 行の入れ替え。 |
| removeRow() | fireTableRowsDeleted | 行の削除。 |
| setNumRows() | fireTableRowsInserted fireTableRowsDeleted |
行数の変更(末尾への追加または削除)。 setRowCount()の方が推奨されたメソッドだが、内部的にはこちらが呼ばれている。 |
挿入(や削除)辺りは比較的易しいので、そこだけ抜粋。
class UndoModel extends DefaultTableModel {
〜
@SuppressWarnings("unchecked")
@Override
public void insertRow(int row, Vector rowData) {
// 行の挿入
UndoableEdit ue = new InsertRowUndo(row, rowData);
undoSupport.postEdit(ue);
super.insertRow(row, rowData);
}
class InsertRowUndo extends AbstractUndoableEdit {
protected Vector<?> newData;
protected int row;
/** コンストラクター */
protected InsertRowUndo(int row, Vector<?> rowData) {
if (rowData != null) {
newData = (Vector<?>) rowData.clone(); //複製しておく
}
this.row = row;
}
@Override
public void undo() throws CannotUndoException {
super.undo();
UndoModel.super.removeRow(row);
}
@Override
public void redo() throws CannotRedoException {
super.redo();
Vector<?> rowData = (newData != null) ? (Vector<?>) newData.clone() : null; //複製する
UndoModel.super.insertRow(row, rowData);
}
@Override
public void die() {
super.die();
newData = null;
}
}
}
保持する行データは、clone()を使って複製しておく。また、REDO時にinsertRow()に渡す際もclone()した複製を渡す。
なぜなら、行データのVectorの中は、後でセルの値が編集された時に書き換わるから。
もっと厳密に言えば、行データの中で保持されている各オブジェクトの中身も書き替えられる可能性があるので、本当はディープコピーをしたいところ。