アノテーションを使って、GUIコンポーネントのTabOrderを設定する

今年最後のJavaネタ。

最初にSwingでGUIアプリケーションを書く場合に一番面食らったのが「タブオーダを設定するための方法が無い」ということだった。
その後のGUIビルダの殆どが影響を受けたVisual Basicファミリの伝統であり、GUIコンポーネント(コントロール)のフォーカス順を設定する手法として最もプリミティブな方法がいわゆる「タブ・オーダ」「タブ・インデクス」による直接的な順番の設定であり、それは.NET WindowFormsでも基本的には変わらない。その方法がSwingには無いのだ。
具体的にはSwingはjava.awt.FocusTraversalPolicy抽象クラスによってフォーカス遷移を制御できる。このクラスのデフォルトの実装は既にJFrameに組み込まれており、基本的にはフレーム上に追加された順にタブオーダが振られるし、その順が気にいらないのであればこのクラスをオーバライドして自分の好きな順に遷移させることもできる。
FocusTraversalPolicyを使う方法はOO的であり、スマートな解決方法ではあるものの、しかし直接タブ順を設定する方法は直感的であり、何より慣れた手法であることは否定できない。そこでこのFocusTraversalPolicyを使いつつも伝統的な手法を採りいれてみよう。
ということで(表題でネタをばらしているが)JFrame中にフィールドとして記述されたGUIコンポーネントに対して、アノテーションでタブ順を記述し、FocusTraversalPolicyの継承クラスでそのアノテーションに沿ってタブ順を振るわけだ。(同じことを考える人はたくさんいると思うので既に他に実装はあると思うが気にしないことにする)

@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TabOrder {
    int Index() default 0;
}

プロパティにIndexを持たせただけの非常に単純なアノテーションであり、用途は明白だろう。というか、アノテーションは書いてみてそれが何の用途なのかパッと想像できない場合は失敗だと思う。

このアノテーションを使い、好きなようにJFrameクラスに書かれたフィールドに記述していく。

@TabOrder(Index=5)
private javax.swing.JButton jButton1;
@TabOrder(Index=3)
private javax.swing.JCheckBox jCheckBox1;
@TabOrder(Index=4)
private javax.swing.JComboBox jComboBox1;
private javax.swing.JTable jTable1;
@TabOrder(Index=0)
private javax.swing.JTextField jText1
@TabOrder(Index=1)
private javax.swing.JTextField jText2
@TabOrder(Index=2)
private javax.swing.JTextField jText3


jTable1にアノテーションが記述されていないのは忘れた訳ではない。フォーカスを遷移させたく無いコンポーネントにはこのように記述を省くことで対応することにする。

  • TabIndexFocusTraversalPolicy

さて、アノテーションの定義と記述が終わったので、実際にフォーカスの遷移を制御するFocusTraversalPolicyクラスの実装に入ろう。
処理としては1.アノテーションを収集 2.タブ順とコンポーネントのリスト(タブ順で格納する)を作成。これらを初期処理として、後はFocusTraversalPolicyのメソッドを埋めていくだけだ。リフレクションによりフィールドよりフィールド値自体を取得しているので、この手の実装でよくある「決まり事としてコンポーネント名を必ず設定しなくてはならない」という縛りも必要が無い。

public class TabIndexFocusTraversalPolicy extends FocusTraversalPolicy {
    private ArrayList tabOrderList = new ArrayList();
    public TabIndexFocusTraversalPolicy(Container container) {
        Class clazz = container.getClass();
        FinderUtil.findAll(clazz.getDeclaredFields()
            , new IPredicate() {
                @Override
                public boolean evaluate(final Field input) {
                    input.setAccessible(true);
                    if ( input.isAnnotationPresent(TabOrder.class)) {
                        TabOrder taborder = input.getAnnotation(TabOrder.class);
                        if ( taborder.Index() >= tabOrderList.size()) {
                            for(int i = 0; i <= taborder.Index(); i++) {
                                tabOrderList.add(null);
                            }
                        }
                        try {
                            Object fieldVal = input.get(container);
                            if ( fieldVal instanceof Component ) {
                                Component cmp = (Component)fieldVal;
                                if ( cmp.isFocusable() ) {
                                    tabOrderList.set(taborder.Index(), cmp);
                                }
                            }
                        } catch (IllegalArgumentException e) {
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                        return true;
                    }
                    return false;
                }
         });
    }
    @Override
    public Component getDefaultComponent(Container container) {
        return this.tabOrderList.get(0);
    }
    @Override
    public Component getFirstComponent(Container container) {
        return this.tabOrderList.get(0);
    }
    @Override
    public Component getComponentAfter(Container container, Component component) {
        int index = this.tabOrderList.indexOf(component);
        return (index+1) < this.tabOrderList.size() 
	           ? this.tabOrderList.get(index+1)
	           : null;
    }
    @Override
    public Component getComponentBefore(Container container, Component component) {
        int index = this.tabOrderList.indexOf(component);
        return (index-1) >= 0 
                    ? this.tabOrderList.get(index-1)
                    : null;
    }
    @Override
    public Component getLastComponent(Container container) {
        return this.tabOrderList.get(this.tabOrderList.size()-1));
    }
}

FinderUtil.findAllメソッドは標準ライブラリィのメソッドではない。「Attach API (アプリケーションのインスタンス数を制御するには-その4)」で紹介した、最近お気に入りの述語論理を用いた絞込み用のユーティリティだ。

また、コンポーネントがJFrame上ではprivateスコープなフィールドとして定義される場合を考慮してgetDeclaredFieldでフィールドを収集しているが、その際に必要なField#setAccessible(true)も気にせず裸で使っている。とりあえずの実装としてはこんな所だろう。(タブ順をインデクスとしてそのままの位置でリストに格納するために、予めリストにダミーのエントリを追加しているが、これは何か他の方法に切り替えた方が良いだろう。)

では実際に使ってみよう。といってもJFrameクラスを生成した後に(コンポーネントの参照を得るためには必ず生成後である必要がある)TabIndexFocusTraversalクラスを実体化し、JFrameクラスが元々保持しているFocusTraversalPolicyを切り替えてやればよいだけだ。

JFrame1 fr = new JFrame1();
fr.setFocusTraversalPolicy(new TabIndexFocusTraversalPolicy(fr));
fr.setVisible(true);

実際に業務で使用するには、アノテーションが取得できずリストが作れなかった場合や、コンポーネントの取得に失敗した場合、対象のコンポーネントにフォーカスを設定できる状態に無いケース等を考慮する必要があるだろう。

追記: 対象のコンポーネントがフォーカスを得ることができるか否かを検査するためにという意味では、FocusTraversalPolicyのデフォルト実装クラスであるContainerOrderFocusTraversalPolicyクラスのaccept(Component aComponent)メソッドが用意されている。これと同様のメソッドを用意して適宜挿入すれば良さそうだ(てか、先にJSDKのソースコード見ようよ俺)