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();
}