S-JIS[2007-02-25/2009-04-26] 変更履歴

UndoManager(Swing)

UndoManagerは、UNDO/REDO(元に戻す/やり直し)を管理するクラス。

テキスト入力を行うJTextComponentJEditorPaneJTextField等)は デフォルトではUNDO/REDOを行うことは出来ないが、UndoManagerを組み合わせることで簡単に実現できる。


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

上記のUNDO/REDOは、1文字ずつ戻ったりやり直したりする。
これを ある程度の文字列単位でUNDO/REDOできるようなクラスを作ってみた。

TextUndoManager


UndoManagerインスタンス

UndoManagerに登録されるUndoableEditには、「どのDocumentに対する変更か」が保持されている。

したがって、複数のJTextComponent(Document)に対して同一のUndoManagerインスタンスを使用することができ、全体共通でUNDO/REDOが実現できる。

逆にJTextComponentひとつに対して1つのUndoManagerインスタンスを使用すると、各JTextComponentそれぞれで完結したUNDO/REDOが実現できる。

UndoManagerインスタンスを作り直さずに内部のUNDO情報を消去したい場合は、discardAllEdits()を呼び出せばよい。[2009-03-15]


テキスト以外のUNDO/REDO

テキスト以外のコンポーネントでもUndoManagerを使うことは出来る。[2009-04-25]
ただしテキスト以外では、変更内容をUndoManagerに登録(通知)したり実際のUNDO/REDO動作を行ったりする機能は用意されていないので、自分で作る必要がある。

UndoManagerのコンポーネントへの登録

テキストの場合は、テキストコンポーネントから取得した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();
		}

ExTableModel


Swing目次へ戻る / Java目次へ戻る
メールの送信先:ひしだま