先週に引き続き,JTableクラスの機能拡張を取り上げます。今週はフィルタリングです。

フィルタリングといっても,使うのは先週のソーティングと同じjavax.swing.table.TableRowSorterクラスです。

実際にフィルタリングを行うのはjavax.swing.RowFilterクラスです。TableRowSorterオブジェクトにsetRowFilterメソッドで指定します。

RowFilterクラスはabstractクラスで,それ自身をインスタンス化することはできません。その代わり,用途に応じたstaticなメソッドが定義されているので,それを使用します。

表1 Dialog.ModalityType
メソッド名 説明
regexFilter(String regx, int... indices) 正規表現を使用したフィルタリングを行うRowFilterオブジェクトを返す
numberFilter(RowFilter.ComparisonType type, Number number, int... indices) 数値によるフィルタリングを行うRowFilterオブジェクトを返す
dateFilter(RowFilter.ComparisonType type, Date date, int... indices) 日付によるフィルタリングを行うRowFilterオブジェクトを返す
andFilter(Iterable filters) 複数のRowFilterオブジェクトのANDを取るRowFilterオブジェクトを返す
orFilter(Iterable filters) 複数のRowFilterオブジェクトのORを取るRowFilterオブジェクトを返す
norFilter(RowFilter filter) 指定されたRowFilterオブジェクトのNOTを取るRowFilterオブジェクトを返す

regexFilterメソッド,numberFilterメソッド,dateFilterメソッドの最後の引数であるindicesには,フィルタリングを適用するカラムのインデックスを指定します。複数のカラムを指定することもできますし,何も指定しないことも可能です(可変長引数なので省略できます)。何も指定しないと,すべてのカラムがフィルタリングの対象となります。

まずは数字でフィルタリングしてみましょう。

サンプルのソースコード TableFilteringSample1.java

TableFilterlingSample1クラスはフィルタリング以外の部分は先週使用したサンプルと同一です。フィルタリングの設定をする部分を以下に示します。

TableRowSorter sorter
    = new TableRowSorter(model);

// 2003年以降を表示
sorter.setRowFilter(
    RowFilter.numberFilter(RowFilter.ComparisonType.AFTER, 
                           2003, 1));

第2引数が閾値となる数値,第1引数で閾値以上であることを指定します。RowFilter.ComparisonTypeは列挙型で,AFTER以外にBEFOREとEQUALE,NOT_EQUALが定義されています。

第3引数がカラム数を示しています。

さて,これで実行してみましょう。ソートはヘッダをクリックしないとソートされませんでしたが,フィルタリングはデフォルトの表示でも行われます。

数値によるフィルタリング
図1 数値によるフィルタリング

正しくフィルタリングできたことがおわかりでしょうか。2003年以降で指定しましたが,これだと2003年は含まれないようです。

もう少し複雑なフィルタリングの条件をつけてみましょう。2000年から2004年までに公開したものを表示するようにしてみます。

// 2000年から2004年までを表示
List<RowFilter<Object, Object>> filters
  = new ArrayList<RowFilter<Object, Object>>();
filters.add(RowFilter.numberFilter(RowFilter.ComparisonType.AFTER, 2000, 1));
filters.add(RowFilter.numberFilter(RowFilter.ComparisonType.BEFORE, 2004, 1));

sorter.setRowFilter(RowFilter.andFilter(filters));

このような複合条件を扱うためにandFilterメソッドやorFilterメソッドがあります。ここでは,andFilterメソッドを使用しました。

andFilterメソッドの引数はIterableインタフェースなので,ArrayListクラスを使用しました。

さて,これで実行してみましょう。

数値によるフィルタリング
図2 数値によるフィルタリング

2000<Year<2004なので,2003年の映画が表示されました。

次は,正規表現でフィルタリングするサンプルです。

サンプルのソースコード TableFilteringSample2.java

正規表現を使用する場合はRowFilter.regexFilterメソッドを使用します。

// 題名の頭文字がAからMに入るもの
sorter.setRowFilter(
    RowFilter.regexFilter("[a-mA-M].*", 0));

はじめの文字がaからm,もしくはAからMの題名を持つ映画を抽出します。ところが,このままだとうまくいきません。

正規表現によるフィルタリング(誤用)
図3 正規表現によるフィルタリング(誤用)

調べてみると,java.util.regex.Matcherクラスのfindメソッドを使用してマッチングしているようです。findクラスは文字列の先頭からマッチする部分を検索するので,"[a-mA-M].*"という正規表現は,文字列中にaからm,もしくはAからMの文字が使われていればマッチしてしまいます。

そこで,先頭を示すために次のように表記します。

// 題名の頭文字がAからMに入るもの
sorter.setRowFilter(
    RowFilter.regexFilter("^[a-mA-M]", 0));

正規表現で"^"は先頭を表します。これならばうまくいくはずです。

正規表現によるフィルタリング
図4 正規表現によるフィルタリング

このサンプルではフィルタははじめから定義してありますが,フィルタを動的に差し替えることもできます。

サンプルのソースコード TableFilteringSample3.java

TableFilteringSample3クラスでは,テキスト・フィールドに正規表現を入力して,フィルタリングを実行できます。

TableRowFilter#setRowFilterメソッドは,コールされるとすぐに適用されるので,動的にフィルタの入れ替えが可能です。フィルタリングを行わないようにするにはsetRowFilterメソッドの引数をnullにします。

TableFilteringSample3では,ボタンのイベント処理でこれを行っています。

JButton filterButton = new JButton("Filter");
filterButton.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        // フィルタの変更
        sorter.setRowFilter(RowFilter.regexFilter(regex));
    }
});

JButton resetButton = new JButton("Reset");
resetButton.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        // nullを代入することでフィルタリングを
        // 行わないようにする
        sorter.setRowFilter(null);
    }
});

RowFilter.regexFilterメソッドの引数でカラム番号を指定していないので,すべてのカラムに対してフィルタリングを行います。

実行結果を図5,図6に示しました。図5が初期画面,図6がフィルタリング後の画面です。

動的なフィルタリング(フィルタリング前)
図5 動的なフィルタリング(フィルタリング前)
動的なフィルタリング(フィルタリング後)
図6 動的なフィルタリング(フィルタリング後)

今までは,標準で定義されていたフィルタを使用してきましたが,フィルタを自作することも可能です。試しに正規表現でフィルタリングするフィルタを作ってみましょう。

サンプルのソースコード TableFilteringSample4.java

フィルタを自作する場合,RowFilterクラスのincludeメソッドをオーバーロードします。

sorter.setRowFilter(new RowFilter<Object, Object>() {
    private Pattern pattern = Pattern.compile(regex);

    @Override
    public boolean include(
        RowFilter.Entry<? extends Object,
                        ? extends Object> entry) {
        for (int i = 0; i < entry.getValueCount(); i++) {
            Object value = entry.getValue(i);
            String valueText;
            if (value instanceof String) {
                valueText = (String)value;
            } else {
                valueText = value.toString();
            }
            
            Matcher matcher = pattern.matcher(valueText);
            if (matcher.find()) {
                return true;
            }
        }

        return false;
    }
});

includeメソッドの引数はRowFilter.Entryオブジェクトです。RowFilter.Entryはフィルタリングの対象となる複数のカラムの値を保持しています。

保持している値の個数はRowFilter.Entry#getValueCountメソッドで取得でき,値はgetValueメソッドで取得できます。

値が取得できれば,後は正規表現でマッチさせるだけです。includeメソッドの戻り値は,booleanでマッチする場合はtrue,しない場合はfalseになります。

いかがでしょうか。フィルタリングも簡単に適用できることがおわかりになったと思います。フィルタを自作するのも,それほど難しいことではありません。

先週のソーティングと同様にフィルタリングもぜひ活用してください。

著者紹介 櫻庭祐一

横河電機 ネットワーク開発センタ所属。Java in the Box 主筆

今月の櫻庭

自家製桜もち

櫻庭は洋菓子しか興味がないと思ったあなた,それは間違いです(笑)。

実は和菓子も大好きなんです。

特に冬から春にかけての和菓子。花びら餅,福豆,桜もちに柏餅。どれもおいしいですよね。

ところで,最近,江戸菓子が京菓子化しているような気がしてしかたありません。例えば,桜もち。江戸菓子の白玉粉を使った桜もちが減って,道明寺が増えているような気がします。

櫻庭は白玉粉を使った桜もちが大好きなので(もちろん道明寺も好きですけど),去年は自分で作ってしまいました。意外と簡単ですよ。

これから桜の季節なので,自家製桜もちでお花見などいかがですか。