WisdomSoft - for your serial experiences.

8.2 ドキュメント

Swing のテキストコンポーネントはデータモデルとして書式を持つ Document インターフェイスを採用しています。これによって DOM と同じような文書構造を表現できます。

8.2.1 テキストのデータモデル

JTextComponent クラスを継承するすべてのテキストコンポーネントは、そのデータモデルに javax.swing.text.Document インタフェースを利用してます。テキストコンポーネントは、必ずしも単純な String 型の文字列だけで表現できるとは限りません。ビジネス用途のオフィス製品であれば、単純なテキストだけではなく、複雑な書式付のテキストやイメージの挿入を希望するでしょう。

Document インタフェースはプレーンテキストはもちろん、HTML や XML のような書式を持つ文書を表現できるように設計されています。プレーンテキストは文字列以外のデータを持ちませんが、書式付の文書は個々の文字にフォントや色などの情報を保有させる必要があります。Document インタフェースは、要素や属性という概念を使ってこうした書式情報を提供します。

表1 Document インタフェースのメソッド
メソッド 解説
public void addDocumentListener(DocumentListener listener) 変更が加えられたときに通知されるリスナを登録する。
public void removeDocumentListener(DocumentListener listener) 登録されている DocumentListener を解除する。
public void addUndoableEditListener(UndoableEditListener listener) 取り消し可能な編集が加えられたときに通知されるリスナを登録する。
public void removeUndoableEditListener(UndoableEditListener listener) 登録されている UndoableEditListener を解除する。
public void putProperty(Object key, Object value) プロパティとドキュメントを関連付づける。
public Object getProperty(Object key) ドキュメントに関連するプロパティを取得する。
public void remove(int offs, int len) throws BadLocationException ドキュメントのコンテンツの一部を削除する。
public void insertString(int offset, String str, AttributeSet a) throws BadLocationException ドキュメントのコンテンツに文字列を挿入する。
public int getLength() ドキュメント内の現在のコンテンツの文字数を返す。
public String getText(int offset, int length) throws BadLocationException ドキュメントの指定部分内にあるテキストを取り出す。
public void getText(int offset, int length, Segment txt) throws BadLocationException ドキュメントの指定部分内にあるテキストを取り出す。
public Position getStartPosition() ドキュメントの先頭を表す位置を返す。
public Position getEndPosition() ドキュメントの末尾を表す位置を返す。
public Position createPosition(int offs) throws BadLocationException 文字コンテンツのシーケンスの位置にマークを付ける。
public Element[] getRootElements() 定義されているすべてのルート要素を返す。
public Element getDefaultRootElement() ビューのベースとなるルート要素を返す。
public void render(Runnable r) モデルが非同期的な更新をサポートしている場合、並行性に直面してモデルを安全に描画できるようにする。

表1は Document インタフェースで宣言されているメソッドです。Document インタフェースは複雑な文書も扱えるように設計されているため、多くのインタフェースとの関係を持っています。しかし、このインタフェースのメソッドだけを見れば、ドキュメントの変更と、やり直しに関する処理を監視するリスナ、コンテンツの挿入や削除、ドキュメントのルート要素などが主な機能となります。

Document オブジェクトに addDocumentListener() メソッドからリスナを追加すれば、ドキュメントに対する変更を常に監視することができるようになります。このメソッドが受けるリスナは javax.swing.event.DocumentListener インタフェースのオブジェクトです。 このインタフェースには、データが挿入された時に呼び出される insertUpdate() メソッド、削除されたときに呼び出される removeUpdate() メソッド、属性が変更されたときに呼び出される changedUpdate() メソッドの 3 つが宣言されています。

DocumentListener インタフェース insertUpdate() メソッド
public void insertUpdate(DocumentEvent e)
DocumentListener インタフェース removeUpdate() メソッド
public void removeUpdate(DocumentEvent e)
DocumentListener インタフェース changedUpdate() メソッド
public void changedUpdate(DocumentEvent e)

これらのメソッドに渡されるパラメータは javax.swing.event.DocumentEvent インタフェースを実装しているオブジェクトです。 このオブジェクトは、変更が加えられた Document などを通知するメソッドを提供しています。このインタフェースが宣言するメソッドは表2に表します。

表2 DocumentEvent インタフェースのメソッド
メソッド 解説
public DocumentEvent.ElementChange getChange(Element elem) 指定された要素の変更情報を返します。
public DocumentEvent.EventType getType() イベントのタイプを返します。
public Document getDocument() 変更イベントの基となったドキュメントを返します。
public int getLength() 変更の長さを返します。
public int getOffset() 変更の始点のドキュメント内でのオフセットを返します。

getType() メソッドは、DocumentEvent インタフェースの内部で定義されている javax.swing.event.DocumentEvent.EventType クラスのオブジェクトを返し、このオブジェクトから発生したイベントの種類を調べることができます。EventType クラスは Object クラスを継承し toString() メソッドをオーバーライドしているだけで、新しいサービスは提供していません。このクラスは、EventType 型のフィールド INSERT、REMOVE、CHANGE 定数をそれぞれ公開しているので、これらのフィールド定数と getType() メソッドの戻り値を比較して種類を判断することができます。

構造的に変更部分を抽出するには getChange() メソッドが有効です。Element インタフェースによる要素の概念はすぐ後に詳しく説明しますが、このメソッドは指定した要素に対する追加された子要素、または削除された子要素などを提供する ElementChange インタフェースを実装するオブジェクトを返します。

Document インタフェースの実装をテキストコンポーネントに設定するには JTextComponent クラスの setDocument() メソッドに渡します。getDocument() メソッドを呼び出せば、現在設定されている Document オブジェクトを取得することができます。

JTextComponent クラス setDocument() メソッド
public void setDocument(Document doc)
JTextComponent クラス getDocument() メソッド
public Document getDocument()

設定したデータモデルをどのように利用して表示するかはコンポーネントのビューを担当するコードに依存します。

コード1
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import java.awt.*;

public class Test extends JFrame implements DocumentListener {
	public static void main(String args[]) {
		JFrame win = new Test();
		win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		win.setBounds(10 , 10 , 400 , 300);
		win.show();
	}

	private JTextField text = new JTextField();
	private JLabel label = new JLabel();
	public Test() {
		text.getDocument().addDocumentListener(this);

		getContentPane().add(text , BorderLayout.NORTH);
		getContentPane().add(label , BorderLayout.SOUTH);
	}

	public void insertUpdate(DocumentEvent e) { documentEvent(e); }
	public void removeUpdate(DocumentEvent e) { documentEvent(e); }
	public void changedUpdate(DocumentEvent e) { documentEvent(e); }

	private void documentEvent(DocumentEvent e) {
		try {
			Document doc = e.getDocument();
			label.setText(
				e.getType() + ",開始点=" + e.getOffset() +
				",長さ=" + e.getLength() +
				",テキスト=" + doc.getText(0 , doc.getLength())
			);
		}
		catch(Exception err) { label.setText(err.toString()); }
	}
}

コード1は、JTextField に設定されている Document の変更を常に監視し、ユーザーからの入力などによってテキストに変更が加えられるとリスナが呼び出され、ウィンドウ下部のラベルにイベント情報が表示されるプログラムです。

8.2.2 元に戻す

何らかのデータを編集可能なアプリケーションは、実用レベルであれば必ずといってよいほど「元に戻す」コマンドと「再実行」コマンドを提供しなければなりません。ユーザは編集の過程で操作ミスをする可能性が高く、うっかりデータを削除してしまったとき、元のデータがどのような状態だったのかを思い出せないかもしれません。こんなとき、元に戻すコマンドで 1 つ前の操作の状態に戻すことができれば便利です。

Document インタフェースと Swing は、このような元に戻す処理と再実行する処理を統合管理できるような仕組みを提供しています。アプリケーションが非効率的な方法でログを管理する必要はなくなったのです。

Document インタフェースは addUndoableEditListener() メソッドを提供しているため javax.swing.event.UndoableEditListener インタフェースを登録することができます。 UndoableEditListener は、取り消し可能な操作が行われたときにオブジェクトから呼び出されるリスナです。Document に対して取り消し可能な操作が行われると、このリスナが呼び出されるという仕掛けです。UndoableEditListener は undoableEditHappened() メソッドだけを宣言しています。

UndoableEditListener インタフェース undoableEditHappened() メソッド
public void undoableEditHappened(UndoableEditEvent e)

このメソッドが受け取るイベント引数は javax.swing.event.UndoableEditEvent クラスのオブジェクトです。

javax.swing.event.UndoableEditEvent クラス
java.lang.Object
  |
  +--java.util.EventObject
        |
        +--javax.swing.event.UndoableEditEvent
public class UndoableEditEvent extends EventObject

このクラスは、リスナを登録しているイベントを発生させた源泉オブジェクトで行われた操作と編集内容を保存しているオブジェクトを提供する getEdit() メソッドを公開してます。このメソッド以外に、新しいメソッドはありません。

UndoableEditEvent クラス getEdit() メソッド
public UndoableEdit getEdit()

このメソッドが返すオブジェクトは javax.swing.undo.UndoableEdit インタフェースのオブジェクトです。このクラスは元に戻すと再実行を行うメソッドを提供しています。

表3 UndoableEdit インタフェースのメソッド
メソッド 解説
public boolean addEdit(UndoableEdit anEdit) 可能な場合は UndoableEdit を取り込むことができる。
public boolean replaceEdit(UndoableEdit anEdit) anEdit が置換される場合は true を返す。
public void undo() throws CannotUndoException 実行された編集結果を元に戻す。
public boolean canUndo() この操作をまだ元に戻せる場合に true を返す。
public void redo() throws CannotRedoException 元に戻されている場合に、編集結果を再び適用する。
public boolean canRedo() この操作をまだ再実行できる場合に true を返す。
public void die() 編集結果に、これ以上使用できないことを通知する。
public boolean isSignificant() この編集結果に意味がない場合は false を返す。
public String getPresentationName() この編集結果の、判読可能な地域に対応した記述を返す。
public String getUndoPresentationName() この編集結果の、元に戻せる形式の判読可能な記述を返す
public String getRedoPresentationName() この編集結果の、再実行できる形式の判読可能な記述を返す。
 

UndoableEdit インタフェースは、編集された何らかのオブジェクトを 1 つ前の操作の状態に復元する undo() メソッドと、元に戻した上体から再び最終的な状態に復元する再実行 redo() メソッドを提供しています。オブジェクトの編集状態によっては使えないこともあるため、canUndo() メソッドcanRedo() メソッドを使って undo() や redo() メソッドが使えるかどうかを調べると良いでしょう。

コード2
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.undo.*;
import java.awt.*;
import java.awt.event.*;

public class Test extends JFrame implements UndoableEditListener , ActionListener {
	public static void main(String args[]) {
		JFrame win = new Test();
		win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		win.setBounds(10 , 10 , 400 , 300);
		win.show();
	}

	private JTextField text = new JTextField();
	private JButton undoButton = new JButton("元に戻す");
	private JButton redoButton = new JButton("再実行");
	private UndoableEdit undo = null;

	public Test() {
		undoButton.setEnabled(false);
		undoButton.addActionListener(this);
		redoButton.setEnabled(false);
		redoButton.addActionListener(this);
		text.setPreferredSize(new Dimension(400 , 30));
		text.getDocument().addUndoableEditListener(this);

		getContentPane().setLayout(new FlowLayout());
		getContentPane().add(text);
		getContentPane().add(undoButton);
		getContentPane().add(redoButton);
	}

	public void undoableEditHappened(UndoableEditEvent e) {
		undo = e.getEdit();

		undoButton.setEnabled(undo.canUndo());
		redoButton.setEnabled(undo.canRedo());
	}

	public void actionPerformed(ActionEvent e) {
		if (e.getSource() == undoButton) undo.undo();
		else if(e.getSource() == redoButton) undo.redo();

		undoButton.setEnabled(undo.canUndo());
		redoButton.setEnabled(undo.canRedo());
	}
}
実行結果
コード2 実行結果

コード2は、JTextField クラスの Document に対して addUndoableEditListener() メソッドから UndoableEditListener オブジェクトを追加しています。ユーザーがテキスト入力コンポーネントに対して入力や削除処理を行ったり、プログラムからコンポーネントや Document に対して制御を行うと、その処理が復元可能であれば設定したりスナが呼び出されます。

リスナのメソッドでは、UndoableEdit オブジェクトから undo() または redo() メソッドを押されたボタンに従って実行します。その後、canUndo() メソッドと canRedo() メソッドでそれぞれの処理を行うボタンの有効状態を設定しています。

8.2.3 要素と属性

Document は、単純な String 型テキストを提供するだけのデータモデルではありません。書式データを持つ複雑な文書に対応できるように、データに対する要素と属性を保有しています。要素は他の要素を子要素として含むことができ、単一の親要素を保有することもできます。つまり、要素は木構造であると考えることができます。

要素の役割は、文書内の単一の部品を提供することにあります。文書の中に存在するオブジェクトのようなものだと考えてください。オブジェクト指向におけるオブジェクトにプロパティが存在するように、文書内の要素は属性を保有することができます。属性は、その要素の具体的な特性を決定する値を提供します。

要素は javax.swing.text.Element インタフェースで表現されます。要素は、要素名、属性、親要素、子要素などを提供しなければなりません。Element インタフェースが宣言するメソッドは表4を見てください。

表4 Element インタフェース
メソッド 解説
public Document getDocument() この要素に関連したドキュメントを取り出す。
public Element getParentElement() 親要素を返す。ルートレベルの要素である場合は、null を返す。
public String getName() 要素名を返す。
public AttributeSet getAttributes() この要素が保持する属性のコレクションを返す。
public int getStartOffset() この要素が始まる、ドキュメントの先頭からのオフセットを返す。
public int getEndOffset() この要素が終わる、ドキュメントの先頭からのオフセットを返す。
public int getElementIndex(int offset) 指定オフセットに最も近い子要素のインデックスを返す。
public int getElementCount() この要素が含む子要素の数を返す。
public Element getElement(int index) 指定されたインデックスの子要素を返す。
public boolean isLeaf() この要素が葉要素かどうかを判別する。

純粋な String 型の JTextField コンポーネントであれば、標準テキストを表す要素がルート要素として設定されているだけで良いと考えられますが、書式付の文書であれば、色やフォントが異なる個々の要素を識別できるように構成する必要があります。

Element インタフェースの getName() メソッドを使えば、この要素の名前を得ることができます。要素が適用されているドキュメントの位置は getStartOffset() メソッドgetEndOffset() メソッドから判断することができるでしょう。この要素が保有する属性群は getAtributes() メソッドから得ることができます。属性群は javax.swing.text.AttributeSet インタフェースを実装しています。

表5 AttributeSet インタフェースのメソッド
メソッド 解説
public int getAttributeCount() このセットに含まれている属性の数を返す。
public boolean isDefined(Object attrName) 指定した属性名に対する値を持つ場合は true を返す。
public boolean isEqual(AttributeSet attr) 2 つの属性セットが等しければ true を返す。
public AttributeSet copyAttributes() 時間が経っても変化しないことが保証された属性セットを返す。
public Object getAttribute(Object key) 指定された属性の値を取り出す。
public Enumeration getAttributeNames() セット内の属性の名前を列挙の形で返す。
public boolean containsAttribute(Object name, Object value) 等しい値の属性が含まれていれば true を返す。
public boolean containsAttributes(AttributeSet attributes) 等しい値の属性がすべて含まれていれば true を返す。
public AttributeSet getResolveParent() 解釈処理側の親を返す。

属性は、結局のところ Object 型のキーと、キーに関連付けられた値のセットを提供するオブジェクトで、その性質は Map インタフェースに似ています。属性がどのような名前を持つのか、どのような値を提供するのかは、属性を保有する要素で決定されます。