JavaFX + Apache POIでSpreadsheet操作 - JavaFX Advent Calendar 2016
はじめに
この記事はJavaFX Advent Calendar 2016の24日目の記事です。
以前下記のような記事を書いており、JavaFXとApache POIを使ってSpreadsheet(Excel)を操作する、という意味ではその続編となります。
- 【Kotlin】【Windows】JavaFX + Apache POI + GsonでExcelからJsonファイルを作る
- 【Kotlin】【Windows】JavaFX + Apache POI + GsonでExcelからJsonファイルを作る2 (1の修正)
違いとしては以下のような操作を行うことにあります。
Spreadsheet
- 複数のファイル(Book)にアクセスして値の入出力を行う。
- 1つのファイル内にあるSheetの表示、非表示を切り替える。
JavaFX
- もう一度SceneBuilderを使う。
- JavaFXにTabPaneを追加し、Tab切り替えのEnable、Disableを行う。
なお今回の使用言語がJavaになっているのは、ただ単にJava8を使ってみたかったからです。
build.gradle
build.gradle
apply plugin: 'java' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' compile 'io.reactivex.rxjava2:rxjava:2.0.3' compile group: 'org.apache.poi', name : 'poi', version: '3.15' compile(group: 'org.apache.poi', name : 'poi-ooxml', version: '3.15') { exclude group: 'stax', module: 'stax-api' } }
- Rx系ではRxJavaFXも存在するのですが、とりあえず今回はRxJavaのみを使うことにします。
- Spreadsheetの操作は以前と同じくApachePOIを使います。
SceneBuilderでGUIを作る
まずはGUI部分の作成から。
IntellliJ IDEAではFile -> Settings -> Languages & Frameworks -> JavaFXにSceneBuilderのexeを設定しておくと、 Textとして編集ができる他、GUIでボタンなどの追加ができるようになります。
MainForm.fxml
< ?xml version="1.0" encoding="UTF-8"? > < ?import java.lang.String? > < ?import javafx.collections.FXCollections? > < ?import javafx.scene.control.Button? > < ?import javafx.scene.control.ComboBox? > < ?import javafx.scene.control.Tab? > < ?import javafx.scene.control.TabPane? > < ?import javafx.scene.control.TextArea? > < ?import javafx.scene.control.TextField? > < ?import javafx.scene.layout.AnchorPane? > < TabPane maxHeight="768" maxWidth="1024" minHeight="768" minWidth="1024" prefHeight="768" prefWidth="1024" tabClosingPolicy="UNAVAILABLE" xmlns="http://javafx.com/javafx/8.0.66" xmlns:fx="http://javafx.com/fxml/1" fx:controller="MainCtrl" > < Tab fx:id="mainTab" text="Main" > < AnchorPane fx:id="mainPane" maxHeight="708" maxWidth="1024" minHeight="708" minWidth="1024" prefHeight="708" prefWidth="1024" > < TextField fx:id="idSheetPathField" layoutX="57.0" layoutY="35.0" prefHeight="51.0" prefWidth="760.0" /> < Button fx:id="searchIdSheetButton" layoutX="884.0" layoutY="35.0" mnemonicParsing="false" onAction="#onSearchIdSheetButtonClicked" text="参照" /> < TextField fx:id="docTitleField" layoutX="57.0" layoutY="160.0" prefHeight="51.0" prefWidth="760.0" /> < ComboBox fx:id="docTypeCombobox" layoutX="57.0" layoutY="276.0" onAction="#onDocTypeComboboxSelected" prefWidth="150.0" / > < TextField fx:id="outputSheetPathField" layoutX="57.0" layoutY="389.0" prefHeight="51.0" prefWidth="760.0" / > < Button fx:id="searchOutputSheetButton" layoutX="878.0" layoutY="389.0" mnemonicParsing="false" onAction="#onSearchOutputSheetButtonClicked" text="参照" / > < Button fx:id="createDocButton" layoutX="765.0" layoutY="601.0" mnemonicParsing="false" onAction="#onCreateDocButtonClicked" text="生成" / > < /AnchorPane > < /Tab > < Tab fx:id="sheet1Tab" text="Sheet1" > < AnchorPane maxHeight="708" maxWidth="1024" minHeight="708" minWidth="1024" prefHeight="708" prefWidth="1024" > < TextArea fx:id="sheet1InputTextArea" layoutX="56.0" layoutY="59.0" prefHeight="200.0" prefWidth="900.0" / > < /AnchorPane > < /Tab > < Tab fx:id="sheet2Tab" text="Sheet2" > < AnchorPane maxHeight="708" maxWidth="1024" minHeight="708" minWidth="1024" prefHeight="708" prefWidth="1024" > < TextArea fx:id="sheet2InputTextArea" layoutX="56.0" layoutY="59.0" prefHeight="200.0" prefWidth="900.0" / > < /AnchorPane > < /Tab > < /TabPane >
- 「onAction」でイベント関数を登録するため、対象のGUI(ボタンとか)の親となるPaneに対して、「fx:controller」を指定しておく必要があります(今回はMainCtrlというクラスを指定しています)。
なおTabPaneの子どもとしてAnchorPaneを追加しているのですが、Androidでいう「match_parent」のようなものは無いのでしょうか。
それとも、その辺りの細かいUIの設定はCSSを使う、ということなのかしら。この辺りはまた次回にでも。
Main Class
まずApplicationを継承しているメインクラスからです。
処理の開始とfxmlのロードだけを行っています。
import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.stage.Stage; import javafx.fxml.FXMLLoader; import java.io.IOException; public class MainForm extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { FXMLLoader loader = new FXMLLoader(); Alert alert = new Alert(Alert.AlertType.NONE , "問題が発生しました。" , ButtonType.CLOSE); try { Parent root = loader.load(getClass().getResourceAsStream("MainForm.fxml")); primaryStage.setScene(new Scene(root)); primaryStage.show(); }catch (IOException ex){ System.out.println(ex.getMessage()); alert.showAndWait() .filter(response -> response == ButtonType.CLOSE) .ifPresent(response -> Platform.exit()); } } }
- エラーが起きたらAlertを出してアプリを閉じるようにしています。
Controller Class
次はコントロール用のクラスです。
ボタンやComboboxのイベントの検出などを行います。
MainCtrl.java
import io.reactivex.Observer; import io.reactivex.disposables.Disposable; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.ComboBox; import javafx.scene.control.Tab; import javafx.scene.control.TextField; import javafx.scene.layout.AnchorPane; import javafx.stage.FileChooser; import java.io.File; import java.net.URL; import java.util.ResourceBundle; public class MainCtrl implements Initializable { @FXML private AnchorPane mainPane; @FXML private TextField idSheetPathField; @FXML private ComboBoxdocTypeCombobox; @FXML private TextField outputSheetPathField; @FXML private TextField docTitleField; @FXML private Tab sheet1Tab; @FXML private Tab sheet2Tab; @FXML private TextField sheet1InputTextArea; @FXML private TextField sheet2InputTextArea; private FileChooser fileChooser; private final static String FileTypeA = "TypeA"; private final static String FileTypeB = "TypeB"; private IdManager idManager; private int newId; @FXML public void onSearchIdSheetButtonClicked(){ // ダイアログを表示して、指定されたファイルのパスをTextFieldにセット. File selectedFile = fileChooser.showOpenDialog(mainPane.getScene().getWindow()); idSheetPathField.setText(selectedFile.getPath()); } @FXML public void onSearchOutputSheetButtonClicked(){ // ダイアログを表示して、指定されたファイルのパスをTextFieldにセット. File selectedFile = fileChooser.showOpenDialog(mainPane.getScene().getWindow()); outputSheetPathField.setText(selectedFile.getPath()); } @FXML public void onCreateDocButtonClicked(){ // 必須項目が空ならそのまま. if(idSheetPathField.getText().isEmpty() || docTitleField.getText().isEmpty() || outputSheetPathField.getText().isEmpty() || docTypeCombobox.getSelectionModel().getSelectedIndex() < 0){ return; } // Book1からIDを取得して、Book2にGUIで入力された値とともに挿入する. idManager.addNewId(docTitleField.getText(), idSheetPathField.getText()) .subscribe(new Observer () { @Override public void onSubscribe(Disposable d) { } @Override public void onNext(Integer integer) { newId = integer; } @Override public void onError(Throwable e) { Alert alert = new Alert(Alert.AlertType.NONE, e.getMessage(), ButtonType.CLOSE); alert.show(); } @Override public void onComplete() { // TODO: Book2.xlsxに値を挿入. } }); } @FXML public void onDocTypeComboboxSelected(){ switch(docTypeCombobox.getSelectionModel().getSelectedItem()){ case FileTypeA: sheet1Tab.setDisable(false); sheet2Tab.setDisable(true); break; case FileTypeB: sheet1Tab.setDisable(true); sheet2Tab.setDisable(false); break; default: sheet1Tab.setDisable(true); sheet2Tab.setDisable(true); break; } } @Override public void initialize(URL url, ResourceBundle rb) { // ComboBoxにセットするListの生成. ObservableList fileTypeList = FXCollections.observableArrayList(); fileTypeList.add(FileTypeA); fileTypeList.add(FileTypeB); docTypeCombobox.setItems(fileTypeList); fileChooser = new FileChooser(); // デフォルトでカレントディレクトリが開くようにする. fileChooser.setInitialDirectory(new File(System.getProperty("user.dir"))); fileChooser.setTitle("ファイルを選択"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Spreadsheet", "*.xlsx", "*.ods")); idManager = new IdManager(); } }
ComboboxにItemをセットする
以前トライしたときはセットしたはずのGUIがNullになる、といった問題がありましたが、どうやらコードからComboboxに値をセットするには、 Initializableを継承して「initialize(URL url, ResourceBundle rb)」の中で実行する必要があるようです。
~省略~ @Override public void initialize(URL url, ResourceBundle rb) { // ComboBoxにセットするListの生成. ObservableListfileTypeList = FXCollections.observableArrayList(); fileTypeList.add(FileTypeA); fileTypeList.add(FileTypeB); docTypeCombobox.setItems(fileTypeList); ~省略~
Applicationを継承していないクラスでprimaryStageを取得する
ファイル選択用にFileChooserでダイアログを表示する(showOpenDialogを実行する)ためには、Stageを引数として渡す必要があります。
今回であればApplicationを継承しているメインクラスから受け取っても良いのですが、表示中のPane(AnchorPaneなど)のインスタンスを取得すれば、「mainPane.getScene().getWindow()」を引数として渡すことでshowOpenDialogが実行できるようになります。
FileChooserでデフォルトでカレントディレクトリを開く
FileChooserでダイアログを表示したときのデフォルトのディレクトリは、「setInitialDirectory」で指定することができます。
~省略~ fileChooser = new FileChooser(); // デフォルトでカレントディレクトリが開くようにする. fileChooser.setInitialDirectory(new File(System.getProperty("user.dir"))); ~省略~
TabのEnable/Disable切り替え
javafx.scene.control.Tabのインスタンスを取得しておき、「setDisable」にTrueを渡せばOKです。
個人的にはDisableにするのにTrueを渡す、というのにちょっと違和感が...。
まぁ慣れの問題だとは思いますが。
~省略~ public class MainCtrl implements Initializable { ~省略~ @FXML private Tab sheet1Tab; @FXML private Tab sheet2Tab; ~省略~ @FXML public void onDocTypeComboboxSelected(){ // Comboboxで選択された値に合わせてTabをEnable/Disable切り替え. switch(docTypeCombobox.getSelectionModel().getSelectedItem()){ case FileTypeA: sheet1Tab.setDisable(false); sheet2Tab.setDisable(true); break; case FileTypeB: sheet1Tab.setDisable(true); sheet2Tab.setDisable(false); break; default: sheet1Tab.setDisable(true); sheet2Tab.setDisable(true); break; } }
終わりに
まだSpreadsheetに触れてもいませんが、長くなってきたので一旦切ります。
JavaFXで使えるというCSSや先に触れたRxJavaFXなども触ってみたいところですね。
今回のGUIもこのままだとTextFieldとボタンの高さがずれていたりして結構残念な見栄えになってしまっているので...orz。
それはそれとして、JavaFX Advent Calendar 2016はいよいよ明日が最終日。
skrbさん、よろしくお願いいたしますm( )m
参考
JavaFX
- JavaFXでHello world(IntelliJ IDEA) - 人生、気合いと具合 - blog
- FXMLでアクションイベントを実装する(3/5):初心者のためのJavaFXプログラミング入門
- JavaFX: How to get stage from controller during initialization? - Stack Overflow