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に適当な関数名を入力しておく。 このときのスクリーンショットを次に示す。

f:id:inaz2:20160427024712p:plain

編集が終わったら、保存して終了する。

これに他のコードの編集も交えながら手作業での修正を加えると、最終的には次のようなコードとなる。

<?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」を押して実行し、適当にファイルをドラッグアンドドロップした後のスクリーンショットを次に示す。

f:id:inaz2:20160426084128p:plain

コードから直接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ファイルを実行すると、上と同様のウィンドウが表示されることが確認できる。

関連リンク