JavaFX - ImageView の大きさを他のノードと連動させる

ウィンドウサイズを変更したときに、 `ImageView` の表示も大きくしたり小さくしたりしたい、そんな願望を叶えようと四苦八苦したときのメモ。
色々試行錯誤した結果なので、正しさは保障できない。

# Stage と連動させた場合
**実装**

```xml:main.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.BorderPane?>

<BorderPane fx:controller="sample.javafx.MainController" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1">
   <center>
      <ImageView fx:id="imageView" fitHeight="150.0" fitWidth="200.0" pickOnBounds="true" preserveRatio="true" BorderPane.alignment="CENTER" />
   </center>
</BorderPane>
```

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/e86cb1eb-4b3a-ad6b-474c-2b2ec5a3ffc5.jpeg)


```java:MainController.java
package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.Stage;

import java.net.URL;
import java.nio.file.Paths;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    @FXML
    private ImageView imageView;

    public void initStage(Stage stage) {
        stage.setWidth(300);
        stage.setHeight(200);
        
        imageView.fitWidthProperty().bind(stage.widthProperty());
        imageView.fitHeightProperty().bind(stage.heightProperty());
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Image image = new Image(Paths.get("./image/shirakawago.jpg").toUri().toString());
        imageView.setImage(image);
    }
}
```

```java:Main.java
package sample.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }
    
    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader(this.getClass().getResource("/main.fxml"));

        Parent root = loader.load();

        MainController controller = loader.getController();
        controller.initStage(primaryStage);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/a88fecb2-3c06-feef-9ec3-f28149c47ec4.gif)


**説明**

- `ImageView` の `fitWidth` プロパティと `fitHeight` プロパティを、 `Stage` の `width` プロパティと `height` プロパティにバインドさせる
- 注意なのが、 `Stage` の `width` と `height` にあらかじめ値を設定しておく必要があるという点
    - 何も設定していないと、 `Stage` の `width` と `height` は初期値が `NaN` になる
    - その `width`, `height` プロパティに `ImageView` の `fitWidth`, `fitHeight` プロパティをバインドすると、どうやら設定している `Image` のオリジナルサイズで初期化されてしまうっぽい
- ただ、よく見てみるとウィンドウサイズを小さくしたときに画像の下部分が隠れてしまっている（右下の女性の姿を見ると分かりやすい）

[height | Window (JavaFX 8)](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/stage/Window.html#heightProperty)

> この値には、オペレーティング・システムで追加できる、**タイトル・バーなどのすべての装飾が含まれます**。

- ということなので、おそらくタイトルバーの高さだけ画像が隠れてしまっているっぽい

# シーングラフのルートに合わせる
**実装**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/d65ded26-e491-b97c-f3da-ddc5e973dc81.jpeg)

```java:MainController.java
package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

import java.net.URL;
import java.nio.file.Paths;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    @FXML
    private BorderPane borderPane;
    @FXML
    private ImageView imageView;
    
    public void initStage(Stage stage) {
        stage.setWidth(300);
        stage.setHeight(200);
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Image image = new Image(Paths.get("./image/shirakawago.jpg").toUri().toString());
        imageView.setImage(image);
        
        imageView.fitWidthProperty().bind(borderPane.widthProperty());
        imageView.fitHeightProperty().bind(borderPane.heightProperty());
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/73048d42-2cf0-d813-083b-fb563244f4d9.gif)

**説明**

- シーングラフのルートである `BorderPane` の `width`, `height` プロパティと `ImageView` の `fitWidth`, `fitHeight` プロパティをバインドしておく
- こちらも `Stage` の `width`, `height` に初期値を設定しておかないと、 `Image` のオリジナルサイズになってしまうっぽい
- ウィンドウサイズを小さくしても画像の下が隠れることもなく、うまくいっているっぽい

# ルート以外のノードと連動させる
## 他のノードが入ってきた場合
上述の方法は、あくまでシーングラフ内に他のノードが一切ない状態なので、いわゆるイメージビューアみたいなものしか実現できない。
試しに、シーングラフ内に他のノードを追加してみる。

**実装**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/4a43ae2b-e075-27df-25a5-2696aa5887b1.jpeg)

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/bd6ac565-b866-088c-151e-4149b6a3df76.gif)

**説明**

- ラベルのサイズだけ領域が使用されるため、ルートノードのサイズと `ImageView` のサイズを連動させると、画像を表示する範囲が足りなくなる

## とりあえず画像表示領域用にノードを追加してみる
**実装**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/7f63f38f-57c7-848a-7440-1f6b94e2ebb0.jpeg)

- `ImageView` を囲むように新しくノード（`BorderPane`）を追加
    - `width`, `height` 関係はとりあえず全てデフォルトの `USE_COMPUTED_SIZE` にしている
- `ImageView` は、この新しく追加した `BorderPane` の `width`, `height` プロパティと連動するように実装しておく

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/1ea69181-2565-4211-1751-9c648db46d09.gif)

**説明**

- ちょっと分かりにくいが、画像がオリジナルサイズで表示されていて、左上の部分だけが見えている
- なぜ `BorderPane` がルートノードのときはOKで、子供のノードになるとこういう動きになるのか、正直なところ自分も原理は理解できていない

## 非ルートノードに連動させる
**実装**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/4d754f9b-590b-230e-2cdc-6cec2a98b34b.jpeg)

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/6c58f5b2-800b-e202-06d6-51b5fe5edd70.gif)

**説明**

- `Min Width` と `Min Height` を、それぞれ `0` に設定する
    - おそらく、デフォルトの `USE_COMPUTED_SIZE` にしていると、領域の最小サイズが子要素のサイズ（ここでは `ImageView` のサイズ）と一致してしまうので、最小サイズ = 画像のサイズになってしまい、１つ前のような動きになったのではないかと想像している（あくまで想像）
    - ただ、そうだとすると `BorderPane` がルートノードのときも条件は同じはずなので、そっちで同様の状態にならなかった理由はよくわからない
- とりあえず、非ルートノードとサイズを連動させる場合は、 `Min Width`, `Min Height` を `USE_COMPUTED_SIZE`, `USE_PREF_SIZE` 以外の何らかの値にしておけば、良い感じになるっぽい

# まとめ
- 共通
    - `Stage` の `width`, `height` を設定しておく
    - `ImageView` の `fitWidth`, `fitHeight` プロパティを、連動させたいノードの `width`, `height` プロパティにバインドする
- 連動させるノードがルートノードでない場合
    - ノードの `Min Width`, `Min Height` を `USE_COMPUTED_SIZE`, `USE_PREF_SIZSE` 以外の何らかの値にしておく