UndoManagerは、UNDO/REDO(元に戻す/やり直し)を管理するクラス。
テキスト入力を行うJTextComponent(JEditorPaneやJTextField等)は デフォルトではUNDO/REDOを行うことは出来ないが、UndoManagerを組み合わせることで簡単に実現できる。
JTextComponent#getDocument()で取得できるDocumentクラスには、「UNDO/REDOが行える編集(文字が入力されたとか削除されたとか)」を登録する為のリスナーがある。このリスナーが呼ばれたときに、UndoManagerにその編集内容を登録してやる。
UNDO/REDOを実際に行うには、UNDOメニューやREDOメニューの実装として、UndoManager#undo()やredo()を呼んでやるだけでいい。
Documentクラス自身がUNDO/REDOを行えるような作りになっているので、実際に何が行われるかについては気にしなくても動作する。
import javax.swing.undo.UndoManager;
protected UndoManager um = new UndoManager();
JTextField field = new JTextField();
// 編集リスナーの登録
Document doc = field.getDocument();
doc.addUndoableEditListener(new UndoableEditListener() {
public void undoableEditHappened(UndoableEditEvent e) {
//行われた編集(文字の追加や削除)をUndoManagerに登録
um.addEdit(e.getEdit());
}
});
// UNDO/REDOを実行するキーの登録例 field.addKeyListener(new KeyListener() { public void keyTyped(KeyEvent e) { } public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_Z: //CTRL+Zのとき、UNDO実行 if (e.isControlDown() && um.canUndo()) { um.undo(); e.consume(); } break; case KeyEvent.VK_Y: //CTRL+Yのとき、REDO実行 if (e.isControlDown() && um.canRedo()) { um.redo(); e.consume(); } break; } } public void keyReleased(KeyEvent e) { } });
→UNDO/REDO機能を組み込んだJTextField継承クラス
UndoManagerを直接リスナーに登録する方が素直かも。[2009-04-25]
// 編集リスナーの登録 Document doc = field.getDocument(); doc.addUndoableEditListener(um);
(UndoManager#undoableEditHappened()の中は、上記と同じく自分自身(UndoManager)に対してaddEdit()している)
キーの登録に関しては、InputMapやActionMapを使ってキー登録する方がいいのかも知れない。[2009-04-25]
UNDO用とREDO用のActionを用意し、アクセラレーターキーを設定しておく。
(Actionの形にしておけば、メニューバーやポップアップメニューに使い回せるので便利)
Action undo = new UndoAction(um); Action redo = new RedoAction(um); InputMap im = field.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 = field.getActionMap(); am.put(undo.getValue(Action.NAME), undo); am.put(redo.getValue(Action.NAME), redo);
public class UndoAction extends AbstractAction { protected UndoManager um; /** * コンストラクター. * * @param um UNDOを行う際に使われるUndoManager */ public UndoAction(UndoManager um) { super("元に戻す(U)"); putValue(MNEMONIC_KEY, 'U'); putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK)); this.um = um; } @Override public boolean isEnabled() { return um.canUndo(); } @Override public void actionPerformed(ActionEvent e) { if (um.canUndo()) { um.undo(); } } }
public class RedoAction extends AbstractAction { protected UndoManager um; /** * コンストラクター. * * @param um REDOを行う際に使われるUndoManager */ public RedoAction(UndoManager um) { super("やり直す(R)"); putValue(MNEMONIC_KEY, 'R'); putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Y, KeyEvent.CTRL_DOWN_MASK)); this.um = um; } @Override public boolean isEnabled() { return um.canRedo(); } @Override public void actionPerformed(ActionEvent e) { if (um.canRedo()) { um.redo(); } } }
上記のUNDO/REDOは、1文字ずつ戻ったりやり直したりする。
これを ある程度の文字列単位でUNDO/REDOできるようなクラスを作ってみた。
UndoManagerに登録されるUndoableEditには、「どのDocumentに対する変更か」が保持されている。
したがって、複数のJTextComponent(Document)に対して同一のUndoManagerインスタンスを使用することができ、全体共通でUNDO/REDOが実現できる。
逆にJTextComponentひとつに対して1つのUndoManagerインスタンスを使用すると、各JTextComponentそれぞれで完結したUNDO/REDOが実現できる。
UndoManagerインスタンスを作り直さずに内部のUNDO情報を消去したい場合は、discardAllEdits()を呼び出せばよい。[2009-03-15]
テキスト以外のコンポーネントでもUndoManagerを使うことは出来る。[2009-04-25]
ただしテキスト以外では、変更内容をUndoManagerに登録(通知)したり実際のUNDO/REDO動作を行ったりする機能は用意されていないので、自分で作る必要がある。
テキストの場合は、テキストコンポーネントから取得したDocumentに対してaddUndoableEditListener()を
呼んでUndoManagerを登録する。
テキスト以外のコンポーネントにはaddUndoableEditListener()メソッドは無いので、自分で用意する必要がある。
が、UndoableEditSupportでリスナーの管理をする事が出来るので、それを利用するのが簡単。
import javax.swing.undo.UndoableEditSupport;
JComponent component = 〜; //テキスト以外のコンポーネント UndoableEditSupport us = new UndoableEditSupport(component); us.addUndoableEditListener(um);
コンポーネントの値に変更が発生した(新しい値が登録された)ら、それを記憶する必要がある。(その記憶を元にUNDO/REDOを行う)
その記憶の為のクラス(インターフェース)がUndoableEdit。
このインターフェースを実装した具象クラスを自作し、値の保持と、UNDO/REDO動作を記述する。
この具象クラスのインスタンス生成を行うのは、コンポーネントの変更(値の設定)系のメソッドが呼ばれた時。
コンポーネントの値設定系メソッドをオーバーライドして、その中でUndoableEditのインスタンスを作成し、UndoManagerに変更を通知する。
(UndoManagerへの通知=UndoManager#undoableEditHappened()呼び出しは、変更が発生した際にそれを記録しておく為に行う必要がある)
undoableEditHappened()を呼び出すことによってUndoManagerに通知しておけば、UndoManager#undo()やredo()が実行された際にUndoableEditのundo()やredo()が呼ばれるので、そこで実際のUNDO/REDO処理を行う。
例として、JTableの値の変更のUNDO/REDOを作ってみる。[2009-04-25]
import javax.swing.undo.AbstractUndoableEdit; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoManager; import javax.swing.undo.UndoableEdit; import javax.swing.undo.UndoableEditSupport;
class UndoTable extends JTable { protected UndoableEditSupport undoSupport; /** コンストラクター */ public UndoTable() { initUndoManager(); } protected void initUndoManager() { UndoManager um = new UndoManager(); undoSupport = new UndoableEditSupport(this); undoSupport.addUndoableEditListener(um); // UndoManagerを登録する。 }
@Override public void setValueAt(Object value, int row, int column) { UndoableEdit ue = new SetValueUndo(value, row, column); // UNDOできるようにデータを保持し、 undoSupport.postEdit(ue); // UndoManagerに通知する。 super.setValueAt(value, row, column); }
setValueAt()は、JTableのセルに値をセットするときに呼ばれるメソッド。
この中でSetValueUndoという自作クラスをインスタンス化し、それをUndoManagerに通知している。
(UndoableEditSupport#postEdit()を呼び出すと、undoableEditHappened()用のイベントが作られて、登録されている全てのUndoManagerに通知される)
protected class SetValueUndo extends AbstractUndoableEdit { protected Object oldValue; protected Object newValue; protected int row, column; /** コンストラクター */ protected SetValueUndo(Object value, int row, int column) { oldValue = getValueAt(row, column); newValue = value; this.row = row; this.column = column; } @Override public void undo() throws CannotUndoException { super.undo(); UndoTable.super.setValueAt(oldValue, row, column); } @Override public void redo() throws CannotRedoException { super.redo(); UndoTable.super.setValueAt(newValue, row, column); } @Override public void die() { super.die(); oldValue = null; newValue = null; } } }
コンストラクターの中で、JTable内の変更位置(row, column)と新旧両方の値を保存している。
undo()・redo()の中では、スーパークラス(AbstractUndoableEdit)の同名メソッドを呼び出している。
この中でフラグが更新される。(canUndo()やcanRedo()が呼ばれた際に正しい状態を返すようにしてくれる)
UNDO/REDOの際の値の登録は、UndoTable#setValueAt()を呼ばずに、スーパークラス(JTable)のsetValueAt()を直接呼ぶようにする。
UndoTableのsetValueAt()はUNDO用のインスタンスを作ってUndoManagerに登録するようになっているので、UNDOやREDOの際にそれを呼ぶと、登録が妙な事になってしまう。
注意:
JTable#setValueAt()の際のrow,columnは表示用の行番号・列番号なので、後からテーブルをソートしたり列を入れ替えたりすると本来の場所から変わってしまう。
番号体系をモデルのものに変換して管理するか、UNDO/REDO処理自体をJTableでなくTableModelで行う必要がある。
それでも行や列を追加したり削除したりすると変わるので、場合によってはそれも考慮しないといけない。
クリップボードからの貼り付け等の、複数セルの変更になるが一括で管理したい(UNDOでまとめて戻したい)場合も、UndoableEditSupportには便利な機能がある。[2009-04-26]
undoSupport.beginUpdate(); try { 〜複数セルへのsetValueAt()呼び出し(UndoableEditをUndoManagerへ通知)〜 } finally { undoSupport.endUpdate(); }