JavaFXでGUIアプリケーションを作ってみる

「Java+SwingでGUIアプリケーションを作ってみる」では、JDKEclipseをインストールし、Java+SwingによるGUIアプリケーションを作った。 SwingはJava 1.2から存在する標準のGUIライブラリであるが、Java 8からは新たな標準としてJavaFXへの置き換えが進められている。 ここでは、JavaFXを使い、同様のGUIアプリケーションを作ってみる。

環境

Windows 8.1 Pro 64 bit版、JDK 8、Eclipse Mars 2

>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

e(fx)clipseのインストール

EclipseJavaFXプロジェクトを扱えるようにするために、JavaFX用のプラグインであるe(fx)clipseをインストールする。

まず、メニューの「Help」→「Install New Software」を選択する。 ウィザードが表示されたら、リポジトリとしてhttp://download.eclipse.org/efxclipse/updates-released/2.3.0/siteを追加し、「e(fx)clipse -IDE」にチェックを入れてNextを押す。 ユーザライセンスへの同意を求められるので、同意にチェックを入れてFinishを押すとインストールが行われる。

JavaFXGUIアプリケーションを作ってみる

「Java+SwingでGUIアプリケーションを作ってみる」と同様に、ドラッグアンドドロップされたファイルのSHA-1ハッシュ値をテーブルに表示するアプリケーションを書いてみる。

まず、ツールバーから「New」→「Project」を開き、「JavaFX」→「JavaFX Project」を選択して「SHA1CalculatorFX」プロジェクトを作成する。 src/application/Main.javaにファイルの雛形が作成されるので、続けて「Run」を押し、空のウィンドウが表示されることを確認する。

JavaFXでは、メインとなるクラスはプロジェクト名に関係なくMainクラスとなる。 雛形を編集し、実際にコードを書いてみると次のようになる。

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.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;


public class Main extends Application {
    private final TableView<FileInfo> table = new TableView<>();

    @Override
    @SuppressWarnings("unchecked")
    public void start(Stage primaryStage) {
        try {
            BorderPane root = new BorderPane();
            Scene scene = new Scene(root,600,400);  // expanded default size
            scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.setTitle("SHA-1 Calculator FX");

            // TableView settings
            table.setEditable(true);
            table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

            TableColumn<FileInfo, String> filePathCol = new TableColumn<>("File Path");
            filePathCol.setCellValueFactory(new PropertyValueFactory<>("path"));
            filePathCol.setCellFactory(TextFieldTableCell.forTableColumn());

            TableColumn<FileInfo, String> sha1sumCol = new TableColumn<>("sha1sum");
            sha1sumCol.setCellValueFactory(new PropertyValueFactory<>("sha1sum"));
            sha1sumCol.setCellFactory(TextFieldTableCell.forTableColumn());

            table.getColumns().addAll(filePathCol, sha1sumCol);
            ObservableList<FileInfo> data = FXCollections.observableArrayList();
            table.setItems(data);

            root.setCenter(table);

            // enable file drop
            scene.setOnDragOver(new DragOverHandler());
            scene.setOnDragDropped(new DragDroppedHandler());

            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        launch(args);
    }

    private class DragOverHandler implements EventHandler<DragEvent> {
        @Override
        public void handle(DragEvent event) {
            Dragboard db = event.getDragboard();
            if (db.hasFiles()) {
                event.acceptTransferModes(TransferMode.COPY);
            } else {
                event.consume();
            }
        }
    }

    private class DragDroppedHandler implements EventHandler<DragEvent> {
        @Override
        public void handle(DragEvent event) {
            Dragboard db = event.getDragboard();
            boolean success = false;
            if (db.hasFiles()) {
                success = true;
                ObservableList<FileInfo> data = table.getItems();
                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;
        }
    }


    public static class FileInfo {
        private final SimpleStringProperty path = new SimpleStringProperty();
        private final SimpleStringProperty sha1sum = new SimpleStringProperty();

        private 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;
        }
    }
}

JavaFXではデータバインディングの概念が取り入れられており、次のような特徴がある。

  • TableViewにitemsとしてobservableArrayListを登録することで、リストへの変更が直接テーブルに反映される
  • Tableの各セルはクラスのPropertyメンバで表現し、これをTaskクラスのPropertyにbindすることにより非同期での画面更新を行う

コードを保存した後「Run」を押して実行し、適当にファイルをドラッグアンドドロップした後のスクリーンショットを次に示す。

f:id:inaz2:20160426084024p:plain

Swingと比較して、より現代的なUIデザインになっていることが見て取れる。

実行可能なJARファイルを作ってみる

JavaFXプロジェクトで実行可能なJARファイルを作成する際は、プロジェクトのExportではなくビルドツールのAntを用いる。

まず、build.fxbuildファイルを開き、表示されるフォームに対して次の例のように必須項目を入力する。

  • Vendor name: inaz2
  • Application title: SHA-1 Calculator FX
  • Application version: 1.0.0
  • Application class: application.Main

そして、右側にある「Generate ant build.xml and run」をクリックするとビルドが行われ、完了するとbuild/distにJARファイルが生成される。

生成されたJARファイルを実行すると、上と同様のウィンドウが表示されることが確認できる。

関連リンク