Java Swingで複数のJTextFieldに対してUndo、Redoを行う(その2)-解決編

 複数のJTextFieldでUndo、Redoを行った場合に、Undo、Redo対象のJTextFieldにカーソルを移動するサンプルです。今回も最後に付けます(UndoTest2.java、MyUndoManager2.java)。

 解決策を探して、色んな方法を調べましたが、今回はUndoableEditのラッパークラスを作成し、UndoManagerを一部オーバーライドしました。他の方法についてはアイデアのみですが次回書きたいと思います。

 前回と同じように上記サンプルプログラムをコンパイル・実行し、上のJTextFieldに「abc」、下のJTextFieldにマウスなどで移動して「def」、また上のJTextFieldに「xyz」を入力します。そこからCtrl-Zを何度か押すと、z、y、x、f、e、d、c、b、aの順に消えていきますが、f、e、dが消える際には、カーソルが下のJTextFieldの該当個所に移動するのが分かると思います。また、Ctrl-YでRedoすると、文字が復活する場所にカーソルが移動します。
画像

 ではサンプルプログラムを説明します。
 大枠として、今回の問題を解決するためには、UndoManagerがUndo、Redoする際に、対象のJTextFieldにカーソルを移動(requestFocus())したい訳ですが、UndoManagerは編集内容であるインターフェースUndoableEdit(を実装したクラスのオブジェクト)を管理しているだけで、それがどのJTextFieldで行われた編集かは分かりません。
 そこで、UndoableEditのラッパークラスを作成し、そこにJTextFieldの情報も入れておいて、UndoManagerに管理させます……と行きたいところですが、編集が行われるタイミング(UndoableEditEvent)では、どのJTextFieldが対象かは分かりません。分かるのはどのDocumentが対象かです(UndoableEditEvent#getSource())。従って、ラッパークラスにはDocumentを入れておいて、UndoManagerを継承したMyUndoManager2で、DocumentとJTextFieldの対応を管理します。具体的には、初期化時にDocumentとJTextFieldの対応を記憶し、Undo、Redo時にDocumentから対応するJTextFieldを見つけてrequestFocus()を実行します。
 ここで、ラッパークラスについて説明すると、こちらに「※2 ラッパー(wrapper)とは,ラップする(包む)ということで,ある変数やクラスを内包する新しいクラスをつくり,内包するものに対して新しいクラスが機能を付加することを意味する。」とあるように、UndoableEditを実装したクラスでありながら、Documentも持つクラスです。クラス定義の先頭部分は以下の通りで、UndoableEditを実装するクラスであり、メンバ変数にUndoableEditとDocument(のインスタンス)を持ちます。また、ラッパークラスはMyUndoManager2の内部クラスにして、扱いやすいようにしています。
class MyUndoableEditWrapper implements UndoableEdit {

    private UndoableEdit undoableEdit;
    private Document document;

 なお、UndoableEditを実装すると言っても、実際には以下の例(一部)のように、メソッドが呼び出された場合に、メンバ変数のUndoableEditのインスタンスのメソッドをそのまま呼び出しているので、私が特別なコーディングを行う訳ではありません。また、Eclipseなどの開発環境を使うと、実装すべきメソッド(の定義部分)を全てソース中に自動的に挿入できるので、実際の手間はほとんどかかってません。
    public boolean addEdit(UndoableEdit anEdit) {
        return undoableEdit.addEdit(anEdit);
    }
    public boolean canRedo() {
        return undoableEdit.canRedo();
    }


 では、このラッパークラスを使うための、前のサンプルからの変更点を説明したいと思います。
 MyUndoManager2.javaを使うUndoTest2.javaでは、以下の部分がUndoTest1.javaから変わっています。
        //jtf1.getDocument().addUndoableEditListener(ud);//←コメント化
        //jtf2.getDocument().addUndoableEditListener(ud);//←コメント化
        jtf1.addKeyListener(ud);  //←変更なし
        jtf2.addKeyListener(ud);  //←変更なし
        ud.setJTextField(jtf1);//←新規
        ud.setJTextField(jtf2);//←新規

 下2行で、MyUndoManager2(のインスタンス)に対して、Undo、Redoを実現したいJTextFieldを登録しています。上記で書いた、DocumentとJTextFieldの対応の管理を行うためです。また、上2行は、MyUndoManager2のsetJTextFieldの中で実行するよう変更しています。中2行は変更ありません。

 MyUndoManager2.javaは、MyUndoManager1.javaから大幅に変わっています。ラッパークラスを使うため、UndoManagerのメソッドをオーバーライドします。
 まず、編集イベントが発生した場合にundoableEditHappenedメソッドが呼ばれますが、ここで、元のUndoManagerではUndoableEditのインスタンスを管理している(以下のコードで//でコメント化している行)ところを、ラッパークラスMyUndoableEditWrapperに変更します。ラッパーのラップするという名前の通り、元のUndoableEditのインスタンスをラッパークラスで包み、Documentを追加します。
 編集が行われているDocumentはUndoableEditEventから取得できます(取得できるんだから、元のUndoManagerでDocumentの情報を保存してくれていれば、と思いますが)。
    public void undoableEditHappened(UndoableEditEvent e) {

        //Superクラスの動作
        //addEdit(e.getEdit());
        addEdit(new MyUndoableEditWrapper(e.getEdit(), (Document)(e.getSource())));
    }


 次に、keyPressedメソッド中でUndo、Redoを行う際に、対象のJTextFieldに対してrequestFocusします。これは、別のrequestFocus4Documentというメソッドにしています。引数にUndoまたはRedoを行うUndoableEditのインスタンスを渡しますが、上記のundoableEditHappenedメソッドの変更により、ラッパークラスMyUndoableEditWrapperのインスタンスが渡されることになります。
 requestFocus4Documentメソッド内では、ラッパークラスから対象のDocumentを取り出し、対応するJTextFieldに対してrequestFocus()を実行します。
 なお、DocumentとJTextFieldの対応を配列に入れていますが、これはサンプルなのであまりツッコまないでください。

 今回のカーソル移動のやり方を調べる前には、もう少し簡単にできるかと思ってましたが、結構手間取ってしまいました。試行錯誤の内容を、次回書いてみたいと思います。

・UndoTest2.java
import javax.swing.*;
import java.awt.FlowLayout;

public class UndoTest2 {

    JFrame jf;
    JTextField jtf1;
    JTextField jtf2;
    
    public UndoTest2() {

        // JFrame
        jf = new JFrame("Undo Test 2");
        jf.setSize(250, 100);
        jf.setLayout(new FlowLayout());
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        jtf1 = new JTextField(20);
        jtf2 = new JTextField(20);
        jf.add(jtf1);
        jf.add(jtf2);

        jf.setVisible(true);

        // UndoManager
        MyUndoManager2 ud = new MyUndoManager2();
        //jtf1.getDocument().addUndoableEditListener(ud);
        //jtf2.getDocument().addUndoableEditListener(ud);
        jtf1.addKeyListener(ud);
        jtf2.addKeyListener(ud);
        ud.setJTextField(jtf1);
        ud.setJTextField(jtf2);
    }

    public static void main(String[] args) {

        UndoTest2 ut = new UndoTest2();
    }
}

・MyUndoManager2.java
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.*;
import javax.swing.event.UndoableEditEvent;
import javax.swing.text.Document;
import javax.swing.undo.*;

public class MyUndoManager2 extends UndoManager implements KeyListener {

    private JTextField[] jtfs = new JTextField[2];
    private Document[] docs = new Document[2];
    private int idx = 0;

    public MyUndoManager2() {

        super();
    }

    public void setJTextField(JTextField jtf) {

        jtfs[idx] = jtf;
        docs[idx] = jtf.getDocument();
        docs[idx].addUndoableEditListener(this);
        idx++;
    }

    class MyUndoableEditWrapper implements UndoableEdit {

        private UndoableEdit undoableEdit;
        private Document document;
        
        public MyUndoableEditWrapper(UndoableEdit undoableEdit, Document document) {

            this.undoableEdit = undoableEdit;
            this.document = document;
        }

        // Documentを取り出す
        protected Document getDocument() {
            
            return document;
        }
        
        // 各クラスはラップしたUndoableEditのメソッドを呼ぶ
        public boolean addEdit(UndoableEdit anEdit) {
            return undoableEdit.addEdit(anEdit);
        }
        public boolean canRedo() {
            return undoableEdit.canRedo();
        }
        public boolean canUndo() {
            return undoableEdit.canUndo();
        }
        public void die() {
            undoableEdit.die();
        }
        public String getPresentationName() {
            return undoableEdit.getPresentationName();
        }
        public String getRedoPresentationName() {
            return undoableEdit.getRedoPresentationName();
        }
        public String getUndoPresentationName() {
            return undoableEdit.getUndoPresentationName();
        }
        public boolean isSignificant() {
            return undoableEdit.isSignificant();
        }
        public void redo() throws CannotRedoException {
            undoableEdit.redo();
        }
        public boolean replaceEdit(UndoableEdit anEdit) {
            return undoableEdit.replaceEdit(anEdit);
        }
        public void undo() throws CannotUndoException {
            undoableEdit.undo();
        }
    }

    public void undoableEditHappened(UndoableEditEvent e) {

        //Superクラスの動作
        //addEdit(e.getEdit());
        addEdit(new MyUndoableEditWrapper(e.getEdit(), (Document)(e.getSource())));
    }

    public void keyPressed(KeyEvent e) {

        switch (e.getKeyCode()) {
        case KeyEvent.VK_Z:    //CTRL+Zのとき、UNDO実行
            if (e.isControlDown() && this.canUndo()) {

                requestFocus4Document(this.editToBeUndone());

                this.undo();
                e.consume();
            }
            break;
        case KeyEvent.VK_Y:    //CTRL+Yのとき、REDO実行
            if (e.isControlDown() && this.canRedo()) {

                requestFocus4Document(this.editToBeRedone());

                this.redo();
                e.consume();
            }
            break;
        }
    }

    public void keyReleased(KeyEvent e) {
        // NOP
    }

    public void keyTyped(KeyEvent e) {
        // NOP
    }

    private void requestFocus4Document(UndoableEdit ue) {
        
        if (ue instanceof MyUndoableEditWrapper) {
        
            // UndoableEventと対応するDocumentを取り出す
            Document doc = ((MyUndoableEditWrapper)ue).getDocument();
        
            // 記憶しているDocumentと一致していれば対応するJTextComponentにフォーカスを移す
            for (int i = 0; i < 2; i++) {
            
                if (doc == docs[i]) {
                
                    jtfs[i].requestFocus();
                    break;
                }
            }
        }
    }
}

ブログ気持玉

クリックして気持ちを伝えよう!

ログインしてクリックすれば、自分のブログへのリンクが付きます。

→ログインへ

なるほど(納得、参考になった、ヘー)
驚いた
面白い
ナイス
ガッツ(がんばれ!)
かわいい

気持玉数 : 0

この記事へのコメント

この記事へのトラックバック