Java: 既存の Look&Feel をカスタマイズ

最近、Java 1.4 で Swing アプリを作った。Windows 用の Look&Feel (LaF) が、微妙にネイティブのと違っていて気持ち悪い。せめてフォントだけでも変えられたら、だいぶまともなのに。
というわけで、変えてみた (と書きつつ、以下のコード例は気まぐれで Java 6)。

BasicLookAndFeel サブクラスを作る

LaF を自作するには、javax.swing.LookAndFeel のサブクラスを作ればいい。カスタマイズする元の LookAndFeel を継承してもいいんだけど、非公開のクラスだったりするし、カスタマイズ対象は実行時に決められた方がいい。ので、LookAndFeel を継承するかたちで、既存の LookAndFeel に対するラッパーを作ればよさそうだ。
……と思ってたんだけど、LookAndFeel を直接継承したら、Java 1.4 で例外が起きる箇所があった。ので、BasicLookAndFeel を継承することにした。

public class OverriderLookAndFeel extends BasicLookAndFeel {
    private BasicLookAndFeel base;

こんな感じ。コンストラクタはお好みで。

getDefaults() をオーバーライド

次に、getDefaults() メソッドをオーバーライドして、値をすりかえる。

    public UIDefaults getDefaults() {
        UIDefaults defaults = base.getDefaults();
        
        for (Map.Entry entry : defaults.entrySet()) {
            String key = entry.getKey().toString();
            
            if (key.toLowerCase().endsWith("font")) {
                if (entry.getValue() instanceof UIDefaults.ActiveValue) {
                    UIDefaults.ActiveValue av = new OverridingActiveValue(
                            (UIDefaults.ActiveValue)entry.getValue(), "MS UI Gothic", 12);
                    entry.setValue(av);
                }
            }
        }
        
        return defaults;
    }

javax.swing.UIDefaults は、Hashtable のサブクラス (この継承関係が、時代を感じさせるなぁ……)。キーが String で、値に UIResource やら UIDfaults.ActiveValue やら UIDefaults.LazyValue やらが入っている。
ここでは、フォントのファミリとサイズだけ変えたかったので、キーの末尾が "font" または "Font" のエントリだけを拾って、UIDefaults.ActiveValue のラッパクラス (後述) ですりかえている。
ちなみに、キャストじゃなくて toString() で String にしているのは、Java 6u3 の Windows LaF だと動かなかったから。よく見たら、キーの中に StringBuffer が混じっていたのだった。これってありなんかな。

その他のメソッド

他のメソッドは、base に対して委譲する。Eclipse でいえば、メニューの「ソース」→「委譲メソッドの生成」を使うとらくちん。getID() あたりは自分で値を決めないといけないけど、クラス名でも返しておけばとりあえず動く。

    public boolean isSupportedLookAndFeel() {
        return base.isSupportedLookAndFeel();
    }

値もラッピング

UIDefaults に入っている値が Font とか Color なら話は簡単なんだけど、UIResource、あるいは、UIDefaults.ActiveValue や UIDefaults.LazyValue になっている。そこで、これらのクラスについてもラッパーが必要になる。
今回はフォントのファミリとサイズだけ変えたかったので、自作 LookAndFeel の中に、UIDefaults.ActiveValue のラッパーを作った。

    private static final class OverridingActiveValue implements UIDefaults.ActiveValue {
        private UIDefaults.ActiveValue base;
        private String fontName;
        private int size;
        
        OverridingActiveValue(UIDefaults.ActiveValue base, String fontName, int size) {
            this.base = base;
            this.fontName = fontName;
            this.size = size;
        }

        public Object createValue(UIDefaults table) {
            Object o = base.createValue(table);
            if (o instanceof FontUIResource) {
                FontUIResource r = (FontUIResource)o;
                return new FontUIResource(fontName, r.getStyle(), size);
            } else {
                return o;
            }
        }
    }

だいたいこんな感じで、とりあえず動いた。これで、「フォントを設定し直した JButton を作るユーティリティメソッド」みたいなことをしなくてよくなった。めでたしめでたし。