JavaFX+FXMLでGUIアプリケーションを作ってみる
「JavaFXでGUIアプリケーションを作ってみる」では、JavaFXによるGUIアプリケーションを作った。 JavaFXではコードから直接GUI画面を構築するほかに、FXMLと呼ばれるXMLファイルを使って構築することもでき、より見通しのよいコードを書くことができる。 ここでは、JavaFX+FXMLを使い、同様のGUIアプリケーションを作ってみる。
環境
Windows 8.1 Pro 64 bit版、JDK 8、Eclipse Mars 2 + e(fx)clipse
>systeminfo
OS 名: Microsoft Windows 8.1 Pro
OS バージョン: 6.3.9600 N/A ビルド 9600
OS ビルドの種類: Multiprocessor Free
システムの種類: x64-based PC
プロセッサ: 1 プロセッサインストール済みです。
[01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz
Scene Builderのインストール
FXMLでGUI画面を構築する際、おおまかなレイアウトに関してはGUIツールのScene Builderを使って編集すると楽である。 ここでは、e(fx)clipseとも連携できるexe版のScene Builderをインストールする。
まず、プロジェクトページから「Windows Installer (x64)」をダウンロードする。
インストーラを実行すると、プログラム一式は自動的にC:\Users\%USERNAME%\AppData\Local\SceneBuilderにインストールされる。
インストールが完了したら、続けてEclipseと連携できるように設定を行う。
メニューの「Window」→「Preference」を開き、「JavaFX」を選択して「SceneBuilder executable」にSceneBuilder.exeのパスを指定する。
JavaFX+FXMLでGUIアプリケーションを作ってみる
「Java+SwingでGUIアプリケーションを作ってみる」と同様に、ドラッグアンドドロップされたファイルのSHA-1ハッシュ値をテーブルに表示するアプリケーションを書いてみる。
まず、ツールバーから「New」→「Project」を開き、「JavaFX」→「JavaFX Project」を選択する。 プロジェクト名を「SHA1CalculatorFXML」としてNextを2回を押すとDeclative UIを選択できる画面が表示されるので、Languageを「FXML」に変更し、ファイル名、コントローラ名をそれぞれ「MainWindow」「MainWindowController」に指定してFinishを押す。
src/applicaion/MainWindow.fxmlを右クリックして「Open with SceneBuilder」を開くとScene Builderが起動するので、BorderPaneの中央にTableViewを配置し、カラム名をそれぞれ編集する。
さらにTableViewのプロパティを開いてeditableにチェックを入れ、ドラッグアンドドロップを有効にするためにfx:idにTableViewの変数名、onDragOver、onDragDroppedに適当な関数名を入力しておく。
このときのスクリーンショットを次に示す。

編集が終わったら、保存して終了する。
これに他のコードの編集も交えながら手作業での修正を加えると、最終的には次のようなコードとなる。
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.collections.FXCollections?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.cell.PropertyValueFactory?>
<?import javafx.scene.layout.BorderPane?>
<BorderPane xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MainWindowController">
<center>
<TableView fx:id="tableView" editable="true" onDragDropped="#handleDragDropped" onDragOver="#handleDragOver">
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
<columns>
<TableColumn fx:id="filePathCol" text="File Path">
<cellValueFactory><PropertyValueFactory property="path" /></cellValueFactory>
</TableColumn>
<TableColumn fx:id="sha1sumCol" text="sha1sum">
<cellValueFactory><PropertyValueFactory property="sha1sum" /></cellValueFactory>
</TableColumn>
</columns>
<items>
<FXCollections fx:factory="observableArrayList" />
</items>
</TableView>
</center>
</BorderPane>
新しいクラスとしてFileInfoを作成し、src/applicaion/FileInfoにテーブルの各行を表すクラスを記述する。
package application;
import javafx.beans.property.SimpleStringProperty;
public class FileInfo {
private final SimpleStringProperty path = new SimpleStringProperty();
private final SimpleStringProperty sha1sum = new SimpleStringProperty();
public FileInfo(String path, String sha1sum) {
setPath(path);
setSha1sum(sha1sum);
}
public String getPath() {
return path.get();
}
public void setPath(String newPath) {
path.set(newPath);
}
public String getSha1sum() {
return sha1sum.get();
}
public void setSha1sum(String newSha1sum) {
sha1sum.set(newSha1sum);
}
public SimpleStringProperty sha1sumProperty() {
return sha1sum;
}
}
FXMLファイルに対応するコントローラとなるsrc/applicaion/MainWindowController.javaを書くと、次のようになる。
package application;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
public class MainWindowController {
@FXML private TableView<FileInfo> tableView;
@FXML private TableColumn<FileInfo, String> filePathCol;
@FXML private TableColumn<FileInfo, String> sha1sumCol;
@FXML private void initialize() {
filePathCol.setCellFactory(TextFieldTableCell.forTableColumn());
sha1sumCol.setCellFactory(TextFieldTableCell.forTableColumn());
}
public void handleDragOver(DragEvent event) {
Dragboard db = event.getDragboard();
if (db.hasFiles()) {
event.acceptTransferModes(TransferMode.COPY);
} else {
event.consume();
}
}
public void handleDragDropped(DragEvent event) {
ObservableList<FileInfo> data = tableView.getItems();
Dragboard db = event.getDragboard();
boolean success = false;
if (db.hasFiles()) {
success = true;
for (File file : db.getFiles()) {
FileInfo fileinfo = new FileInfo(file.getAbsolutePath(), "");
data.add(fileinfo);
SHA1CalculationTask task = new SHA1CalculationTask(fileinfo);
fileinfo.sha1sumProperty().bind(task.messageProperty());
new Thread(task).start();
}
}
event.setDropCompleted(success);
event.consume();
}
class SHA1CalculationTask extends Task<Void> {
private final FileInfo fileinfo;
public SHA1CalculationTask(FileInfo fileinfo) {
this.fileinfo = fileinfo;
}
@Override
protected Void call() throws Exception {
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// update digest with each 8MB chunk
String path = fileinfo.getPath();
long totalBytes = (new File(path)).length();
long currentBytes = 0;
try (FileInputStream istream = new FileInputStream(path)) {
byte[] buf = new byte[8*1024*1024];
int n;
while ((n = istream.read(buf, 0, buf.length)) != -1) {
md.update(Arrays.copyOf(buf, n));
currentBytes += n;
String message = String.format("calculating %d%%...", 100 * currentBytes / totalBytes);
updateMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] digest = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
updateMessage(sb.toString());
return null;
}
}
}
@FXMLのついたメンバ宣言は、FXMLでfx:idを指定した要素と対応する。
また、CellFactoryについてはTextFieldTableCell.forTableColumn()の結果を用いるため、initializeメソッドを定義してコードから指定を行っている。
src/applicaion/Main.javaを編集し、ウィンドウタイトルの指定を追加しておく。
package application;
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.fxml.FXMLLoader;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
try {
primaryStage.setTitle("SHA-1 Calculator FXML");
BorderPane root = (BorderPane)FXMLLoader.load(getClass().getResource("MainWindow.fxml"));
Scene scene = new Scene(root,600,400); // expanded default size
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
ここで、それぞれのファイルの役割を整理すると、次のようになる。
- Main.java: FXMLのロード
- MainWindow.fxml: GUI画面の定義
- MainWindowController.java: GUI画面でのイベントを処理
- FileInfo.java: データモデルの定義
コードを保存した後「Run」を押して実行し、適当にファイルをドラッグアンドドロップした後のスクリーンショットを次に示す。

コードから直接GUI画面を構築した場合と同じユーザインタフェースになっていることが確認できる。
実行可能なJARファイルを作ってみる
build.fxbuildファイルを開き、表示されるフォームに対して次の例のように必須項目を入力する。
- Vendor name: inaz2
- Application title: SHA-1 Calculator FXML
- Application version: 1.0.0
- Application class: application.Main
そして、右側にある「Generate ant build.xml and run」をクリックするとビルドが行われ、完了するとbuild/distにJARファイルが生成される。
生成されたJARファイルを実行すると、上と同様のウィンドウが表示されることが確認できる。