JTable - セルレンダラに依存せずセルの色設定を行う

JTableはセルの描画の諸元をテーブルセルレンダラ(以降セルレンダラ)に任せており、通常はJTableに設定されたテーブルモデル(AbstractTableModel)が扱う型によって内部でデフォルトのセルレンダラが使用される。よって、セルの描画をカスタマイズする場合の常套は仕様に合わせたセルレンダラを用意して、カラム又はカラムが扱う型のセルレンダラを置き換えることで実現できる。

セルレンダラはインタフェースjavax.swing.table.TableCellRendererを実装するクラスとして書くことができる。

public interface TableCellRenderer {
    Component getTableCellRendererComponent(JTable table, Object value,
					    boolean isSelected, boolean hasFocus, 
					    int row, int column);
}

同インタフェースは唯一つのメソッドgetTableCellRendererComponentで構成されており、JTable#paintComponentからJTable#prepareRendererを経由してセル単位に呼ばれる。面白いのは戻り値にはコンポーネント(java.awt.Component)を返すことだ。セルレンダラは描画に必要な情報を持つ、仮のコンポーネントの実体を返すことで間接的に描画内容を決定するのである。セルの内容は通常のGUIに似たものになるのが当たり前なので、この方法は理に適っているし、セル単位の描画を一から行うよりはずっと簡単にカスタマイズを行える。

さて、ここからが本題。
拙作のフレームワークではテーブルに対してもバリデーションを、必要であれば個々のセル(制約はカラム毎)に対して実施するのだが、その際にバリデーションに失敗した場合には対象のセルの背景色を一時的に変えなくてはならない。この処理は上記のように用意されたセルレンダラによって既存のセルレンダラを置き換えることによって可能だが、そもそもアプリケーションはJTableを様々な形で既にカスタマイズしている可能性があり、こちらが用意したセルレンダラを使わせることを強制するのは困難だ。

このような場合に有効なのはこの日記でも何度となく採りあげてきたAOPを適用して、こちらの用意するセルレンダラをこっそりと処理を挿入したものに置き換えることだが、これまた何度も書いているように今回のJava - Swing向けのフレームワークに関してはできるだけ添付ファイルを減らしたい、という要件があり、仮にAOPの実装を考えた場合に、現在最も適しているであろうjavassistなどのバイトコード生成ラィブラリィは、できれば使わないで済ませたい。

ということでJava SDKだけで必要最小限のAOPを実現するとなると、現状ではダイナミックプロキシクラスを使うしかない。
ダイナミックプロキシクラス
元々J2SE1.3で追加されたこのクラス群はインタフェースを偽装する完全なプロキシを生成するためのラィブラリィであり、JSDKの中でも各所で使われている。(例えば、アノテーションはインタフェースとして書かれているが、ランタイムでは全てこのプロキシとして生成されていることが解る)
なお、解説を見れば解るがJSDKのダイナミックプロキシはインタフェースに対して生成されるものであり、Seasar2等、バイトコード生成を利用したAOPのように任意のPOJOの型を拡張することはできない。といっても今回のような用途であればインタフェースを対象するだけで十分だ。(この段階で既にAOPではなく、単なるデコレータパターンと言うのかもしれない)

サンプルとしては、4×4の行/列を持つJTableを配置した簡単なJFrameクラス(CellRendererTestFlame)を用意した。何も手を加えず表示するとこんな感じだ。

肝心のセルレンダラはテーブルのセルに対して一定の条件で赤と緑の背景色を設定するものとするが、TableCellRendererを実装して置き換えるのではなく、既存のセルレンダラの戻り値のコンポーネントの背景色を変更するアスペクトを元々のセルレンダラのメソッドに挿入する処理を記述してあるダイナミックプロキシ(java.lang.reflect.Proxy)に置き換えることで実現している。

public class CellRendererTest {
    final CellRendererTestFlame flame;
    public CellRendererTest() {
        super();
        this.flame = new CellRendererTestFlame();
    }
    public void run() {
        for ( int i = 0; i < this.flame.jTable.getColumnCount(); i++) {
            TableColumn column = this.flame.jTable.getColumnModel().getColumn(i);
            Class columnClass = this.flame.jTable.getModel().getColumnClass(i);
            TableCellRenderer columnRenderer = column.getCellRenderer();
            final TableCellRenderer origRenderer = columnRenderer != null
                        ? columnRenderer
                        : this.flame.jTable.getDefaultRenderer(columnClass);
            TableCellRenderer rendererProxy = (TableCellRenderer)
                Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader()
                    , new Class{TableCellRenderer.class}
                    , new InvocationHandler(){
                        @Override
                        public Object invoke(Object proxy, Method method,
                                Object args) throws Throwable {
                            Component c = (Component)
                                method.invoke(origRenderer, args);

                            //getTableCellRendererComponent(
                            // JTable table
                            // , Object value
                            // , boolean isSelected
                            // , boolean hasFocus
                            // , int row
                            // , int column);
                            boolean isSelected = (Boolean)args[2];
                            boolean hasFocus = (Boolean)args[3];
                            int row = (Integer)args[4];
                            int col = (Integer)args[5];
                            
                            
                            if (isSelected) {
                                c.setForeground(UIManager.getColor("Table.selectionForeground"));
                                c.setBackground(UIManager.getColor("Table.selectionBackground"));
                            } else {
                                c.setForeground(UIManager.getColor("Table.foreground"));
                                if ( (col % 2) == 0 ) {
                                    c.setBackground(Color.GREEN);
                                } else {
                                    c.setBackground(Color.RED);
                                }
                            } 
                            if (hasFocus) {
                              c.setForeground(UIManager.getColor("Table.focusCellForeground"));
                              c.setBackground(UIManager.getColor("Table.focusCellBackground"));
                            }
                            
                            return c;
                        }});
            if ( origRenderer == columnRenderer ) {
                column.setCellRenderer(rendererProxy);
            } else {
                if (! Proxy.isProxyClass(origRenderer.getClass())) {
                    this.flame.jTable.setDefaultRenderer(columnClass, rendererProxy);
                }
            }
            flame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            flame.setVisible(true);
        }
        
    }
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable(){
            @Override
            public void run() {
                new CellRendererTest().run();
            }});
    }

肝は、"Component c = (Component)method.invoke(origRenderer, args);"により、元々のセルレンダラの同メソッドを実行していること。これにより実装されているJTableが実際の描画に使うコンポーネントの参照を得ることができるので、その後任意の色をコンポーネントに対して設定できる。また、カラムに対してセルレンダラが必ずしも設定されていないことがあるので、(サンプルでもそうだ)その場合はJTable#getDefaultRendererメソッドにより、カラムの型に適合したレンダラを使うようにしているが、この場合プロキシの多重登録が発生しないようにレンダラがプロキシかどうか判定している。

実行結果は予想通りでしかないが、こんな感じになる。

できるだけ短いコードということで無名クラスを使っているために見難いが、実際の拙作のフレームワークではSeasar2.NETのAOPと同様にアスペクトを処理できるよう、AOPProxyクラスにProxyクラスを組み込んで使っている。当然DIコンテナを用いたアスペクトの自動設定もできるので非常に便利だ。

Javaダイナミックプロキシは始めて使ってみたが、思ったよりもかなり軽いもので気に入っている。


追記:
本エントリはartonさんCodeZineに寄稿された記事を参考にさせて頂いています。
Oracle JDBCドライバにオブジェクトの自動クローズ処理を追加する [初級〜中級] Proxyによる既存クラスへの機能追加とスタブを利用したユニットテスト/CodeZine - arton