CaretListenerを使う

広告

ここではCaretListenerを使って現在のキャレットの位置の情報を取得してみます。キャレットとは今どこの位置を編集しようとしているかを表わしている縦の棒で点滅しているもののことです。

CaretListenerを使うと、キャレットの位置が変わった時に通知してくれるので、変更があった時の位置を調べて、そこに現在設定されているスタイル情報を読み出し、その結果を使って、フォントの種類や大きさを選択するコンボボックスの値を変えてやればいいと思います。

CaretListenerインターフェースで定義すべきメソッドは下記の1つだけです。

キャレット位置が更新されたときに呼び出されます。 

パラメータ:
  e - キャレットイベント

このメソッドではCaretEventクラスの値がパラメータとして渡されてきます。CaretEventクラスについても見てみましょう。

public abstract class CaretEvent
extends EventObject
java.lang.Object
 L java.util.EventObject
     L javax.swing.event.CaretEvent

このクラスではgetDot()メソッドとgetMark()メソッドが用意されています。

getDot:

キャレットの位置を返します。 

戻り値:
  ドット >= 0

getMark:

論理選択範囲の反対側の位置を返します。選択範囲がない場合は、ドットと同じになります。 

戻り値:
  マーク >= 0

getDotメソッドは、現在のキャレット位置を返します。また、getMarkメソッドは文字を複数に渡って選択している場合にその先頭のキャレット位置を返します。この2つのメソッドを試す簡単なサンプルを下記に記載します。

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import java.awt.*;

public class CaretListenerTest extends JFrame implements CaretListener{

  protected JTextPane textPane;

  protected DefaultStyledDocument doc;
  protected StyleContext sc;

  protected JTextField disp;

  public static void main(String[] args){
    CaretListenerTest test = new CaretListenerTest();

    test.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    test.setVisible(true);
  }

  CaretListenerTest(){
    setTitle("TextPaneTest Test");
    setBounds( 10, 10, 400, 200);

    textPane = new JTextPane();
    getContentPane().add(textPane, BorderLayout.CENTER);

    sc = new StyleContext();
    doc = new DefaultStyledDocument(sc);

    textPane.setDocument(doc);

    /* CaretListenerをセット */
    textPane.addCaretListener(this);

    disp = new JTextField("Caret Position");
    getContentPane().add(disp, BorderLayout.SOUTH);

    /* 初期文書の読み込み */
    initDocument();
  }

  protected void initDocument(){
    StringBuffer sb = new StringBuffer();
    sb.append("スタイル付きのテキストサンプルです。¥n");
    sb.append("スタイルを変えて表示しています。");

    try{
      /* 文書を挿入する */
      doc.insertString(0, new String(sb), 
        sc.getStyle(StyleContext.DEFAULT_STYLE));
    }catch (BadLocationException ble){
      System.err.println("初期文書の読み込みに失敗しました。");
    }
  }

  public void caretUpdate(CaretEvent e){
    int p = e.getDot();
    int g = e.getMark();

    try{
      String text = doc.getText(p, 1);
      String selectionText = doc.getText(g, 1);

      StringBuffer sb = new StringBuffer();
      sb.append("Caret Position=");
      sb.append(p);
      sb.append(", Text=");
      sb.append(text);
      sb.append(", Selection Top=");
      sb.append(g);
      sb.append(", Text=");
      sb.append(selectionText);

      disp.setText(new String(sb));
    }catch (BadLocationException ble){
      System.err.println("文書の読み込みに失敗しました。");
    }
  }
}

上記のサンプルは、キャレット位置を動かした時の情報を読むだけの簡単なサンプルです。色々試してみると、

選択していない場合:

上記のように、getDotとgetMark共に、キャレットの位置を取得し、そのキャレットの位置から1文字分だけ文字を読むと、キャレットの次の文字が読まれることが分かります。

左から右へ文字を選択した場合:

上記のように、getDotは選択が終わった最後の位置を取得し、getMarkは選択を開始した位置を取得します。それぞえの位置から1文字分だけ文字を読むと、それぞれで取得した位置の次の文字が読まれることが分かります。

右から左へ文字を選択した場合:

上記のように、getDotは選択が終わった最後の位置を取得し、getMarkは選択を開始した位置を取得します。それぞえの位置から1文字分だけ文字を読むと、それぞれで取得した位置の次の文字が読まれることが分かります。

今回の場合、例えば選択をしていない場合は、キャレットの次の文字の情報を調べればいいと思いますが、文字を選択している場合は、選択されている文字の中のどれかの文字の情報が欲しいです。ただ、左から右へ選択した場合は、getMarkであれば欲しい文字となりますがgetDotで取得した位置で文字を取得すると、実際には選択した文字ではなくその次の文字が選択されてしまいます。

逆に右から左へ選択した場合には、getDotは欲しい情報を返しますが、getMarkは欲しい位置情報を返しません。

よって今回は前のページで使ったgetSelectionStartメソッドとgetSelectionEndメソッドを使ってみます。

この2つのメソッドで選択しているテキストの最初と最後の位置が分かります。下記の簡単なサンプルで試してみます。

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import java.awt.*;

public class CaretListenerTest extends JFrame implements CaretListener{

  protected JTextPane textPane;

  protected DefaultStyledDocument doc;
  protected StyleContext sc;

  protected JTextField disp;

  public static void main(String[] args){
    CaretListenerTest test = new CaretListenerTest();

    test.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    test.setVisible(true);
  }

  CaretListenerTest(){
    setTitle("TextPaneTest Test");
    setBounds( 10, 10, 400, 200);

    textPane = new JTextPane();
    getContentPane().add(textPane, BorderLayout.CENTER);

    sc = new StyleContext();
    doc = new DefaultStyledDocument(sc);

    textPane.setDocument(doc);

    /* CaretListenerをセット */
    textPane.addCaretListener(this);

    disp = new JTextField("Caret Position");
    getContentPane().add(disp, BorderLayout.SOUTH);

    /* 初期文書の読み込み */
    initDocument();
  }

  protected void initDocument(){
    StringBuffer sb = new StringBuffer();
    sb.append("スタイル付きのテキストサンプルです。¥n");
    sb.append("スタイルを変えて表示しています。");

    try{
      /* 文書を挿入する */
      doc.insertString(0, new String(sb), 
        sc.getStyle(StyleContext.DEFAULT_STYLE));
    }catch (BadLocationException ble){
      System.err.println("初期文書の読み込みに失敗しました。");
    }
  }

  public void caretUpdate(CaretEvent e){
    int start = textPane.getSelectionStart();
    int end = textPane.getSelectionEnd();

    try{
      StringBuffer sb = new StringBuffer();

      if (start == end){
        String selectText = doc.getText(start, 1);

        sb.append("Text = ");
        sb.append(selectText);
      }else{
        String startText = doc.getText(start, 1);
        String endText = doc.getText(end - 1, 1);

        sb.append("Start = ");
        sb.append(startText);
        sb.append(",End = ");
        sb.append(endText);
      }

      disp.setText(new String(sb));
    }catch (BadLocationException ble){
      System.err.println("文書の読み込みに失敗しました。");
    }
  }
}

選択していない場合:

選択している場合:

今回は右から選択しても、左から選択しても同じ結果となります。これを使うことにします。

選択された文字のスタイルの取得

次は選択した文字のスタイルを調べてみましょう。選択している文字が全て同じスタイルであれば問題無いのですが、異なる場合もありますので、選択開始した最初の文字を使うことにします(選択していない場合はキャレットがある位置の次の文字を調べることにします)。

指定した位置にある文字の情報を調べるにはDefaultStyledDocumentクラスのgetCharacterElementメソッドを使います。

位置に基づいて文字要素を返します。 

パラメータ:
  pos - ドキュメント内の位置 >= 0 
戻り値:
  要素

このメソッドを使って指定した位置にある文字の情報を取得します。返り値としてはElementインターフェースの値として帰ってきますので、Elementインターフェースで定義されているgetAttributesメソッドを合わせて使うことで、最終的に指定した位置にある文字のAttributeSetオブジェクトを取得することができます。

この要素が保持する属性のコレクションを取り出します。 

戻り値:
  要素の属性

実際に使う場合は、下記のようになります。

public void caretUpdate(CaretEvent e){
  int p = textPane.getSelectionStart();

  /* 指定して位置の文字スタイルを読み出す */
  AttributeSet a = doc.getCharacterElement(p).getAttributes();

  /* フォント名とフォントサイズを取り出す */
  String name = StyleConstants.getFontFamily(a);
  int size = StyleConstants.getFontSize(a);

  StringBuffer sb = new StringBuffer();
  sb.append("Font Name = ");
  sb.append(name);
  sb.append(", Point = ");
  sb.append(size);

  disp.setText(new String(sb));
}

上記でStyleConstantsクラスで用意されているgetFontFamilyメソッドとgetFontSizeメソッドを使っています。前にStyleConstantsクラスのsetXXXXメソッドで様々なスタイル情報を設定していましたが、逆に読み出す場合にはgetXXXXメソッドが色々用意されています。

getFontFamily:

属性リストからフォントファミリの設定を返します。 

パラメータ:
  a - 属性セット 
戻り値:
  フォントファミリ。デフォルトは「Monospaced」

getFontSize:

属性リストからフォントサイズの設定を返します。 

パラメータ:
  a - 属性セット 
戻り値:
  フォントサイズ。デフォルトは 12

上記のようにAttributeSetの値から様々なメソッドを使ってスタイルに関する情報を読み出せます。

では上記を応用して、現在のキャレットの位置などから情報を読み取り、フォントやフォントサイズを選択するコンボボックスの値を動的に変更するようにしてみましょう。

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import java.awt.event.*;
import java.awt.*;

public class TextPaneTest extends JFrame 
  implements ActionListener, CaretListener{

  protected JTextPane textPane;

  protected DefaultStyledDocument doc;
  protected StyleContext sc;

  protected JToolBar toolBar;

  protected JComboBox comboFonts;
  protected JComboBox comboSizes;

  protected String currentFontName = "";
  protected int currentFontSize = 0;
  protected boolean flag = false;

  public static void main(String[] args){
    TextPaneTest test = new TextPaneTest();

    test.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    test.setVisible(true);
  }

  TextPaneTest(){
    setTitle("TextPaneTest Test");
    setBounds( 10, 10, 500, 200);

    initToolbar();
    getContentPane().add(toolBar, BorderLayout.NORTH);

    textPane = new JTextPane();
    JScrollPane scroll = new JScrollPane(textPane, 
      JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, 
      JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

    getContentPane().add(scroll, BorderLayout.CENTER);

    sc = new StyleContext();
    doc = new DefaultStyledDocument(sc);

    textPane.setDocument(doc);

    /* CaretListenerをセット */
    textPane.addCaretListener(this);

    /* 初期文書の読み込み */
    initDocument();
  }

  protected void initDocument(){
    StringBuffer sb = new StringBuffer();
    sb.append("スタイル付きのテキストサンプルです。¥n");
    sb.append("スタイルを変えて表示しています。");

    try{
      /* 文書を挿入する */
      doc.insertString(0, new String(sb), 
        sc.getStyle(StyleContext.DEFAULT_STYLE));
    }catch (BadLocationException ble){
      System.err.println("初期文書の読み込みに失敗しました。");
    }
  }

  protected void initToolbar(){
    toolBar = new JToolBar();
    toolBar.setLayout(new FlowLayout(FlowLayout.LEFT));

    GraphicsEnvironment ge = 
      GraphicsEnvironment.getLocalGraphicsEnvironment();
    String familyName[] = ge.getAvailableFontFamilyNames();

    comboFonts = new JComboBox(familyName);
    comboFonts.setMaximumSize(comboFonts.getPreferredSize());
    comboFonts.addActionListener(this);
    comboFonts.setActionCommand("comboFonts");
    toolBar.add(comboFonts);

    toolBar.addSeparator();
    comboSizes = new JComboBox(new String[] {"8", "9", "10", 
      "11", "12", "14", "16", "18", "20", "22", "24", "26", 
      "28", "36", "48", "72"});
    comboSizes.setMaximumSize(comboSizes.getPreferredSize());
    comboSizes.addActionListener(this);
    comboSizes.setActionCommand("comboSizes");
    toolBar.add(comboSizes);
  }

  public void actionPerformed(ActionEvent e){
    if (flag){
      /* キャレット変更に伴うActionEventの場合はパスする */
      return;
    }

    String actionCommand = e.getActionCommand();
    MutableAttributeSet attr = new SimpleAttributeSet();

    if (actionCommand.equals("comboFonts")){
      String fontName = comboFonts.getSelectedItem().toString();
      StyleConstants.setFontFamily(attr, fontName);
    }else if (actionCommand.equals("comboSizes")){
      int fontSize = 0;
      try{
        fontSize = Integer.parseInt(comboSizes.
          getSelectedItem().toString());
      }catch (NumberFormatException ex){
        return;
      }

      StyleConstants.setFontSize(attr, fontSize);
    }else{
      return;
    }

    setAttributeSet(attr);
    textPane.requestFocusInWindow();
  }

  protected void setAttributeSet(AttributeSet attr) {
    int start = textPane.getSelectionStart();
    int end = textPane.getSelectionEnd();

    doc.setCharacterAttributes(start, end - start, attr, false);
  }

  public void caretUpdate(CaretEvent e){
    flag = true;

    int p = textPane.getSelectionStart();
    AttributeSet a = doc.getCharacterElement(p).getAttributes();

    String name = StyleConstants.getFontFamily(a);

    /* 変更前と同じ場合は無視する */
    if (!currentFontName.equals(name)){
      currentFontName = name;
      comboFonts.setSelectedItem(name);
    }

    int size = StyleConstants.getFontSize(a);

    /* 変更前と同じ場合は無視する */
    if (currentFontSize != size){
      currentFontSize = size;
      comboSizes.setSelectedItem(Integer.toString(size));
    }

    flag = false;
  }
}

実行結果は下記のようになります。

起動直後に適当な位置をクリックした場合:

デフォルトで設定されているフォント名とフォントサイズが表示されます。

文書の一部のフォント名とサイズを変更します:

適当な位置をクリックすると:

もとのフォント名とフォントサイズが表示されますが

変更したテキストを選ぶと:

コンボボックスの内容も合わせて変更されます。

上記のサンプルではflagという変数を1つ追加しています。これは、キャレットの位置を変更した場合に、コンボボックスの選択されている項目を変更する場合がありますが、コンボボックスの内容が変わるとActionEventが発生してしまい実際にフォント名やフォントサイズを変更してしまうのを防ぐために追加してあります。(選択した文書に複数のスタイルが含まれている場合にActionEventが発生してしまうと、先頭の文字のスタイルに全部変更されてしまうのを防ぎます)。

これで当初の目的は達せられましたので、フォントの変更以外にBOLDやITALICなどの他の属性についても変更できるようにしていきます。

( Written by Tatsuo Ikura )

関連記事 (一部広告含む)