vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

JavaFX + Apache POIでSpreadsheet操作 - JavaFX Advent Calendar 2016

はじめに

この記事はJavaFX Advent Calendar 2016の24日目の記事です。

以前下記のような記事を書いており、JavaFXApache POIを使ってSpreadsheet(Excel)を操作する、という意味ではその続編となります。

違いとしては以下のような操作を行うことにあります。

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 ComboBox docTypeCombobox;
    @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の生成.
        ObservableList fileTypeList = 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

Alert

FileChooser