デフォルトは同期的にロードされる
Image のインスタンスは、普通に生成すると画像のロードが同期的に行われる(ロードが終了するまで処理は返ってこない)。
画像が小さい場合は特に問題はないが、大きいサイズの画像を表示する場合は処理がしばらく停止してしまうので、操作感が損なわれる恐れがある。
バックグラウンドでロードする
Image インスタンスでの画像のロードをバックグラウンドで行うには、コンストラクタ引数の backgroundLoading に true を指定する。
backgroundLoading を指定できるコンストラクタは、次の2つがある。
backgroundLoading に true を指定してインスタンスを生成すると、画像は非同期でロードされる。
これにより、処理は止まることなく次に進むことができるようになる。
ロードの進捗を表示する
ただ単純にロードをバックグラウンドにしただけだと、読み込みが完了するまで画像が表示されない状態が続く。
サイズが大きくロードに時間がかかる場合、画像が何も表示されないというのはユーザを不安にさせてしまうかもしれない。
そこで、ロードがどれくらい進んでいるか、進捗をプログレスバーなどで表示すると良いかもしれない。
Image クラスは、ロードの進捗を知ることができる progress プロパティを提供している。
これを利用すれば、わりと簡単にロードの進捗を表示できる。
実装
<?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>
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を追加している
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プロパティの値にバインドすることで実現している- プログレスバーは、
progressが1より小さいときだけ表示 - 画像は、
progressが1のときだけ表示、としている
- プログレスバーは、
Image image = new Image(Paths.get("./image/shirakawago.jpg").toUri().toString(), true);
progressBar.progressProperty().bind(image.progressProperty());
-
Imageを生成するときのコンストラクタ引数で、backgroundLoadingにtrueを指定 -
Imageのprogressプロパティをプログレスバーのprogressプロパティにバインドしている
あらかじめロードしておく
メリット
バックグラウンドでのロードは、現在表示しようとしている画像だけでなく、次に表示する予定の画像をあらかじめロードしておくといった手段でも利用できる。
この場合、 Image インスタンスだけを裏で生成しておき、画像を切り替えるときに ImageView の setImage() で 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 が画像データをバイト配列で格納しているっぽいので、その中を見に行く。
ビンゴっぽい。
capacity が 36636672 となっている(約 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 は画像のサイズを指定してロードができるようになっている。
これを利用すれば、オリジナルよりも小さいサイズで画像をロードできるので、メモリ消費量を抑えることができる。
ただし、小さいサイズでロードすることになるので、元画像と同じ品質では表示できないことに注意。
まとめ
バックグラウンドでのロードは表示速度の向上につながるかもしれないが、事前ロードによるキャッシュ利用をする場合はメモリの消費量にも注意したほうが良さげ。






