FXML で相対サイズ指定

## 初めに

これは [JavaFX Advent Calendar 2016](http://qiita.com/advent-calendar/2016/javafx) の 7 日目の記事です。

## JavaFX における長さについて

JavaFX では、DPI 値が違うモニターで表示しても (だいたい) 同じ大きさでモニターに映るように、デフォルトのコントロールのサイズや余白の設定は、UI のフォントサイズに対する相対サイズで指定しています。JavaFX の標準テーマである Modena の CSS を見ると、様々な長さが px ではなく em (フォントサイズ基準) で設定されていることが分かります。

例えば、一般的な Windows 環境ではデフォルトのフォントサイズは 12px になっています。一方、一般的な Mac OS ではフォントサイズは 13px です。したがって、Windows 上で 1px で表示される部分は、Mac OS 上では 13/12＝1.0833px で表示されることになります。高 DPI のモニターを使っていてデフォルトフォントサイズが 15px になっているなら、通常の 1px はこのモニターでは 15/12＝1.25px で表示されます。

さて、JavaFX はこのような仕様になっているので、自作のアプリケーションでもこの仕様に則り、コントロールの余白などをフォントサイズ基準で相対的に設定したいものです。CSS で長さを指定するときは単位に em を使えば良いだけなので問題はないのですが、コード中で長さを指定するときは単位に px しか使えないので、そのままでは相対指定になりません。例えば、`new VBox(5)` などと書いたときの `5` は 5px の意味です。

さらに、JavaFX で UI をデザインするときは普通 FXML を使うと思いますが、FXML 中でも長さを相対的に指定したくなります。しかし、FXML 中には Java コードが書けないので、たとえ単位の変換メソッドを作ったとしても FXML でそれを使うには少し工夫が必要です。

## こんなものを作ってみた

ということで、こんなものを作ってみました。

```java:Measurement.java
package ziphil.custom;

import javafx.scene.text.Font;
import javafx.util.Builder;

public class Measurement implements Builder<Double> {

  private static final Double UNIT_REM = Font.getDefault().getSize();
  private static final Double UNIT_RPX = Font.getDefault().getSize() / 12;

  private double value = 0;
  private String unit;

  public Double build() {
    if (unit.equals("rem")) {
      return value * UNIT_REM;
    } else if (unit.equals("rpx")) {
      return value * UNIT_RPX;
    } else {
      return value;
    }
  }

  public String getRem() {
    return null;
  }
  public void setRem(String valueString) {
    value = Double.parseDouble(valueString);
    unit = "rem";
  }
  public static double rem(double inputValue) {
    return inputValue * UNIT_REM;
  }

  public String getRpx() {
    return null;
  }
  public void setRpx(String valueString) {
    value = Double.parseDouble(valueString);
    unit = "rpx";
  }
  public static double rpx(double inputValue) {
    return inputValue * UNIT_RPX;
  }
  
}
```

こうすることで、`Measurement.rem(5)` と書けば CSS で `5em` と指定したのと同じ長さになり、フォントサイズに対する相対指定ができます。ただ、em は正直分かりにくいので、`rpx` というメソッドも用意しました。これは、例えば `Measurement.rpx(5)` とすると、通常の Windows 環境では 5px で表示され、フォントサイズに応じて拡大縮小される長さになります。つまり、Mac OS 環境では 5×13/12＝5.4167px で表示されます[^1]。

このクラスは `Builder` インターフェースを実装しているので、FXML のビルダーとしても使えます。まず、以下のようなクラスを作成します。

```java:CustomBuilderFactory.java
package ziphil.custom;

import javafx.fxml.JavaFXBuilderFactory;
import javafx.util.Builder;
import javafx.util.BuilderFactory;

public class CustomBuilderFactory implements BuilderFactory {

  private BuilderFactory baseFactory = new JavaFXBuilderFactory();

  public Builder getBuilder(Class clazz) {
    if (clazz == Double.class) {
      return new Measurement();
    } else {
      return baseFactory.getBuilder(clazz);
    }
  }

}
```

そして、`FXMLLoader` クラスのコンストラクタに、ビルダーファクトリとしてこのクラスのインスタンスを渡します。

```java
FXMLLoader loader = new FXMLLoader(resource, null, new CustomBuilderFactory());
```

すると、これを使って読み込む FXML の中では、以下のような記述が可能になります。

```xml
<VBox>
  <spacing><Double rpx="5"/></spacing>
  <padding>
    <Insets>
      <top><Double rpx="10"/></top>
      <right><Double rpx="20"/></right>
      <bottom><Double rpx="10"/></bottom>
      <left><Double rpx="20"/></left>
    </Insets>
  </padding>
</VBox>
```

`<Double rpx="5"/>` の部分が `Double.rpx(5)` と同じ値になります。

ということで、コード中でも FXML 中でもサイズの相対指定がわりと簡単にできるようになりました。

## 問題点

CSS 中で単位を em にして長さを指定すると、その要素におけるフォントサイズが基準になります。したがって、あるコントロールだけフォントサイズを 30px にして、そのコントロールにおいて 1em を指定すると、当然その 1em は 30×1＝30px になります。しかし、フォントサイズを 30px にしたコントロールに対して、FXML で `<Double rem="1"/>` と指定しても、`Measurement` クラスはそのコントロールのフォントサイズではなくデフォルトフォントサイズを基準に長さを計算するので、30px になりません。

したがって、コントロールのフォントサイズを変えている場合は、`Measurement` クラスを使った相対指定はうまくいきません[^2]。

[^1]: 「rpx」というのは relative px のつもりです。同様に「rem」も relative em の略です。
[^2]: コントロールの大きさや余白の設定はコード中ではなくて全部 CSS でやるべきだと言われたらおしまいですが…。