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