FX - Image をバックグラウンドでロードする


デフォルトは同期的にロードされる

Image のインスタンスは、普通に生成すると画像のロードが同期的に行われる(ロードが終了するまで処理は返ってこない)。

画像が小さい場合は特に問題はないが、大きいサイズの画像を表示する場合は処理がしばらく停止してしまうので、操作感が損なわれる恐れがある。

バックグラウンドでロードする

Image インスタンスでの画像のロードをバックグラウンドで行うには、コンストラクタ引数の backgroundLoadingtrue を指定する。
backgroundLoading を指定できるコンストラクタは、次の2つがある。

backgroundLoadingtrue を指定してインスタンスを生成すると、画像は非同期でロードされる。
これにより、処理は止まることなく次に進むことができるようになる。

ロードの進捗を表示する

ただ単純にロードをバックグラウンドにしただけだと、読み込みが完了するまで画像が表示されない状態が続く。

サイズが大きくロードに時間がかかる場合、画像が何も表示されないというのはユーザを不安にさせてしまうかもしれない。
そこで、ロードがどれくらい進んでいるか、進捗をプログレスバーなどで表示すると良いかもしれない。

Image クラスは、ロードの進捗を知ることができる progress プロパティを提供している。
これを利用すれば、わりと簡単にロードの進捗を表示できる。

実装

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

<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>

<BorderPane style="-fx-padding: 10px;" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.javafx.MainController">
   <center>
      <VBox alignment="CENTER" minHeight="0.0" minWidth="0.0" BorderPane.alignment="CENTER">
         <children>
            <ProgressBar fx:id="progressBar" maxWidth="1.7976931348623157E308" progress="0.0" />
            <ImageView fx:id="imageView" fitHeight="200.0" fitWidth="300.0" pickOnBounds="true" preserveRatio="true" />
         </children>
      </VBox>
   </center>
</BorderPane>
MainController.java
package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ProgressBar;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

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

    public void initStage(Stage stage) {
        stage.setWidth(500);
        stage.setHeight(400);
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        progressBar.managedProperty().bind(progressBar.visibleProperty());
        progressBar.visibleProperty().bind(progressBar.progressProperty().lessThan(1));

        imageView.managedProperty().bind(imageView.visibleProperty());
        imageView.visibleProperty().bind(progressBar.progressProperty().isEqualTo(1));

        Image image = new Image(Paths.get("./image/shirakawago.jpg").toUri().toString(), true);
        progressBar.progressProperty().bind(image.progressProperty());
        imageView.setImage(image);
    }
}

実行結果

ちょっと早くてわかりづらいが、画像をロードしている間はプログレスバーが表示され、ロードが完了すると画像が表示されている。

説明

  • 画像表示のための ImageView のとなりに ProgressBar を追加している
MainController.java
        progressBar.managedProperty().bind(progressBar.visibleProperty());
        progressBar.visibleProperty().bind(progressBar.progressProperty().lessThan(1));

        imageView.managedProperty().bind(imageView.visibleProperty());
        imageView.visibleProperty().bind(progressBar.progressProperty().isEqualTo(1));
  • ロード中はプログレスバーのみを表示しロードが完了したら画像だけを表示するため、それぞれの visible プロパティを制御している
  • visible プロパティの制御は、プログレスバーの progress プロパティの値にバインドすることで実現している
    • プログレスバーは、 progress1 より小さいときだけ表示
    • 画像は、 progress1 のときだけ表示、としている
MainController.java
        Image image = new Image(Paths.get("./image/shirakawago.jpg").toUri().toString(), true);
        progressBar.progressProperty().bind(image.progressProperty());
  • Image を生成するときのコンストラクタ引数で、 backgroundLoadingtrue を指定
  • Imageprogress プロパティをプログレスバーの progress プロパティにバインドしている

あらかじめロードしておく

メリット

バックグラウンドでのロードは、現在表示しようとしている画像だけでなく、次に表示する予定の画像をあらかじめロードしておくといった手段でも利用できる。

この場合、 Image インスタンスだけを裏で生成しておき、画像を切り替えるときに ImageViewsetImage()Image インスタンスを差し替えるように実装する。

イメージ
@FXML
private ImageView imageView;
private Image nextImage;

public void initialize(URL location, ResourceBundle resources) {
    // バックグラウンドで次の画像をロードしておく
    nextImage = new Image("next-image.jpg", true);

    Image initialImage = new Image("initial-image.jpg", true);
    imageView.setImage(initialImage);
}

@FXML
public void nextPage() {
    // ロードしておいた次の画像をセットする
    imageView.setImage(nextImage);
}

重い画像でも、あらかじめロードしておくことで素早く表示が実現できるようになり、操作感の向上につながるかもしれない。

メモリ使用量

事前にロードをしておくことで表示速度は向上するかもしれないが、その代わりメモリの消費は増えることに注意しなければならない。

特に、画像はディスク上でのサイズとメモリ上にロードしたときのサイズには大きな差があることに気を付ける必要がある。
ディスク上では、例えば JPEG 画像の場合は大幅に圧縮されたサイズになっているのに対して、 Image でメモリ上にロードした場合は非圧縮状態になっている。

実際にどれくらい違うが調べてみる。

こちらがディスク上でのサイズで、およそ 6.5 MB ある。

この画像を Image でロードしてから、ヒープダンプを取って Image がロードした画像がどれくらいメモリを使用しているか調べてみる。
ヒープダンプの調査には Eclipse Memory Analyzer を使用した。


Image の中の platformImage がそれっぽいので、その中を見に行く。


com.sum.prism.Image がそれっぽいので、その中を見に行く。


width, height に画像のピクセルサイズが格納されている。
さらに、 pixelBuffer が画像データをバイト配列で格納しているっぽいので、その中を見に行く。


ビンゴっぽい。
capacity36636672 となっている(約 35 MB)。
調べてみると hb というのが byte の配列になっていて、画像データが全て格納されているっぽい。

この 36,636,672 というサイズは、ちょうど画像の「縦×横×3」のサイズに一致している。
4,288 * 2,848 = 12,212,224, 12,212,224 * 3 = 36,636,672

つまり、全ピクセルの RGB (3byte) 分のデータが全てロードされているということなのだろう。

ディスク上でのサイズが約 6.5 MB なのに対して、メモリ上にロードしたときは 35 MB とかなり大きくなっている。

元画像の圧縮形式や、アルファチャンネル(透明度)の有無によってはディスク上とメモリ上でのサイズの差は色々変わると思う。

サイズを小さくしてロードする

Image は画像のサイズを指定してロードができるようになっている。

これを利用すれば、オリジナルよりも小さいサイズで画像をロードできるので、メモリ消費量を抑えることができる。
ただし、小さいサイズでロードすることになるので、元画像と同じ品質では表示できないことに注意。

まとめ

バックグラウンドでのロードは表示速度の向上につながるかもしれないが、事前ロードによるキャッシュ利用をする場合はメモリの消費量にも注意したほうが良さげ。