【Kotlin】【Windows】JavaFX + Apache POI + GsonでExcelからJsonファイルを作る
- はじめに
- GUIを作る
- FXMLを使ってみる(失敗)
- コード上でGUIを追加する
- FileChooser
- ApachePOIでシート名を取得する
- ApachePOIでセルのデータを取得する
- Jsonファイルを作成する
- おわりに
- 参考
はじめに
このところずっと取り組んでいる和歌山トイレマップを使ったアプリですが、トイレ情報はCSV形式で保存したファイルから取得しています。
これは、元データである和歌山トイレマップがCSV形式であり、アプリを作り始めた当初はデータをそのまま使用する予定でいたためです。
ただ現在は別途取得した緯度・経度を加えたり、あれこれデータをいじっているため特にCSVである必要はなくなってきました。
加えてCSVに比べ、Jsonの方が解析などのためのツールが揃っている印象もあります。
で、Spreadsheet(Excel)のデータをJson形式で出力するツールを探してみたのですが、いまいちしっくりこず…。
というわけで、自作してみることにしました。
Kotlinを使って。
https://github.com/masanori840816/KtCreateJson
GUIを作る
今回は(今回も)チャラくいきたかったので、GUIで読み込むファイルを選択してシートを指定→ファイルを読み込んでJsonファイルを出力することにしました。
ということでGUIなのですが、JavaFXを使ってみることにしました。
Kotlin用のGUIフレームワークも探せばありそうな気はしたのですが、それほど豊富に情報がなさそうだったので(ヘタレ)。
あと、以前ある方がJavaFXを使ってプレゼンをしていたのを見て以来気になってはいた、ということもあります。
FXMLを使ってみる(失敗)
Gluon Scene Builderといったツールを使うことで、ドラッグ&ドロップでGUIパーツを配置できます。
出来上がったファイル(FXML)を読み込むためには以下のようにします。
MainApplication.fxml
< ?xml version="1.0" encoding="UTF-8"? > < ?import javafx.scene.control.Button? > < ?import javafx.scene.control.ComboBox? > < ?import javafx.scene.control.TextField? > < ?import javafx.scene.layout.StackPane? > < StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" > < Button fx:id="findFileButton" onAction="#onFindFileButtonClicked" mnemonicParsing="false" text="参照" translateX="250.0" translateY="-150.0" / > < Button fx:id="createButton" onAction="#onCreateButtonClicked" mnemonicParsing="false" scaleX="1.3" scaleY="1.3" text="作成" translateX="230.0" translateY="150.0" / > < ComboBox fx:id="sheetNameCombobox" prefWidth="150.0" translateX="-200.0" translateY="-70.0" / > < TextField fx:id="loadFilePathField" scaleX="0.8" translateX="-35.0" translateY="-150.0" / > < /StackPane >
MainApplication.kt
import javafx.application.Application import javafx.collections.FXCollections import javafx.collections.ObservableList import javafx.fxml.FXML import javafx.fxml.FXMLLoader import javafx.scene.Parent import javafx.scene.Scene import javafx.scene.control.Button import javafx.scene.control.ComboBox import javafx.scene.control.TextField import javafx.stage.Stage import java.io.IOException class MainApplication : Application(){ // 変数名をキーにFXMLのGUIと関連付ける. @FXML lateinit private var loadFilePathField: TextField @FXML lateinit private var sheetNameCombobox: ComboBox< String > @Throws(IOException::class) override fun start(primaryStage: Stage) { // FXMLの読み込み val root = FXMLLoader.load< Parent >(javaClass.getResource("MainApplication.fxml")) val primaryScene = Scene(root) primaryStage.setScene(primaryScene) // ウインドウを表示する. primaryStage.show() } fun onFindFileButtonClicked(){ // findFileButtonがクリックされた時に呼ばれる. } fun onCreateButtonClicked(){ // createButtonがクリックされた時に呼ばれる. } }
- 「@FXML」を付けることで、FXML側の「fx:id」と、コード側の変数名とを関連付けます。そのため、これら2つの名前が異なっているとうまく関連付けされず、Nullになってしまいます。
- 「@FXML」の関連付けが行われるのはstartの後のようで、それより前にGUIにアクセスしようとするとぬるぽになります。
- クリックなどのイベントは、FXMLで「onAction」で指定することも、コードで「setOnAction」とすることもできます。
問題
ここまでは特に問題なく表示ができるのですが、Comboboxに値をセットしようとした辺りでハマってしまいました。
下記のようにstartで値を入れようとすると、何故か関連付けたGUIまでNullになってしまう、という問題が発生します。
MainApplication.kt
~省略~ @FXML lateinit private var sheetNameCombobox: ComboBox< String > private var sheetNameList: ObservableList< String >? = null @Throws(IOException::class) override fun start(primaryStage: Stage) { // FXMLの読み込み val root = FXMLLoader.load< Parent >(javaClass.getResource("MainApplication.fxml")) val primaryScene = Scene(root) primaryStage.setScene(primaryScene) // ウインドウを表示する. primaryStage.show() sheetNameList = FXCollections.observableArrayList() sheetNameList.add("test") sheetNameCombobox.items = sheetNameList } ~省略~
結局解決方法がよくわからず、FXMLではなくコードでGUIを追加することにしました。
コード上でGUIを追加する
コード上でGUIを追加するため、以下のように記述しました。
MainApplication.kt
import javafx.application.Application import javafx.collections.FXCollections import javafx.collections.ObservableList import javafx.scene.Scene import javafx.scene.control.Button import javafx.scene.control.ComboBox import javafx.scene.control.TextField import javafx.scene.layout.StackPane import javafx.stage.FileChooser import javafx.stage.Stage import java.io.File import java.io.IOException class MainForm : Application(){ ~省略~ lateinit private var loadFilePathField: TextField lateinit private var sheetNameCombobox: ComboBox< String > lateinit private var createButton: Button private var sheetNameList: ObservableList< String >? = null private var selectedFile: File? = null @Throws(IOException::class) override fun start(primaryStage: Stage) { spreadsheetAccesser = SpreadsheetAccesser() jsonFileCreator = JsonFileCreater() // ファイル選択用ダイアログを表示するボタン. val findFileButton = Button() findFileButton.text = "参照" findFileButton.translateX = 250.0 findFileButton.translateY = -150.0 findFileButton.setOnAction { event -> run { // ボタンクリック時の処理. } } // 読み込むSpreadsheetのパスを入力、表示するテキストフィールド. loadFilePathField = TextField() loadFilePathField.translateX = -35.0 loadFilePathField.translateY = -150.0 loadFilePathField.scaleX = 0.8 loadFilePathField.setOnAction { event -> run{ // テキストフィールドでEnterを押された時の処理. } } sheetNameList = sheetNameList?: FXCollections.observableArrayList("") // 読み込んだSpreadsheetのシート名を選択するコンボボックス. sheetNameCombobox = ComboBox() sheetNameCombobox.translateX = -200.0 sheetNameCombobox.translateY = -70.0 sheetNameCombobox.prefWidth = 160.0 if(sheetNameList != null){ sheetNameCombobox.items = sheetNameList } // Jsonのファイルを作成するボタン. createButton = Button() createButton.text = "作成" createButton.translateX = 230.0 createButton.translateY = 150.0 createButton.setOnAction { event -> run{ // ボタンクリック時の処理. } } // StackPaneを作成してGUIを追加する. val stackPane = StackPane() stackPane.children.addAll(findFileButton , loadFilePathField , sheetNameCombobox , createButton) val primaryScene = Scene(stackPane, 600.0, 400.0) primaryStage.setScene(primaryScene) primaryStage.show() }
- 今回各GUIはStackPaneに追加しています。このときaddAllを使うことで、一括で追加ができます。
FileChooser
読み込むSpreadsheetを選択するためのダイアログを表示します。
JavaFXではFileChooserを使用します。
MainApplication.kt
~省略~ findFileButton.setOnAction { event -> run { // FileChooserでダイアログを表示する. val fileChooser = FileChooser() fileChooser.title = "ファイルを選択" // 選択対象のファイルの拡張子を指定する. fileChooser.extensionFilters.add(FileChooser.ExtensionFilter("Spreadsheet", "*.xlsx", "*.ods")) // ダイアログの表示. selectedFile = fileChooser.showOpenDialog(primaryStage) // 選択したファイルのパスをテキストフィールドに入力する. loadFilePathField.text = selectedFile.toString() // 選択したファイルからシート名を取得して、コンボボックスにセットする(※後述). setSheetNames() } } ~省略~
ApachePOIでシート名を取得する
Apache POIを使ってSpreadsheetからシート名を取得します。
インストール
まずApache POIを使うために、プロジェクト直下にあるbuild.gradleに以下を追加します。
build.gradle
~省略~ dependencies { ~省略~ compile group: 'org.apache.poi', name : 'poi', version: '3.14' compile(group: 'org.apache.poi', name : 'poi-ooxml', version: '3.14') { exclude group: 'stax', module: 'stax-api' } ~省略~ }
シート名の取得
SpreadsheetAccesser.kt
import javafx.collections.FXCollections import javafx.collections.ObservableList import org.apache.poi.ss.usermodel.Cell import org.apache.poi.ss.usermodel.Sheet import org.apache.poi.ss.usermodel.WorkbookFactory import java.io.FileInputStream import java.util.ArrayList class SpreadsheetAccesser { ~省略~ fun getSheetNames(targetFilePath: String): ObservableList< String >?{ // SpreadsheetのパスからStreamを作成する. val fileStream = FileInputStream(targetFilePath) // StreamからWorkbookを取得. val currentWorkbook = WorkbookFactory.create(fileStream) if(currentWorkbook == null){ return null } // ComboboxにセットするためのArrayListの生成. val sheetNameList: ObservableList= FXCollections.observableArrayList() // 取得したWorkbookからシート名を取得する. for (i in 0..currentWorkbook.numberOfSheets - 1){ sheetNameList.add(currentWorkbook.getSheetName(i)) } // Workbook、Streamを閉じる. currentWorkbook.close() fileStream.close() return sheetNameList } ~省略~ }
- 本当は非同期で実行した方が良さそうですが、今回は特に何もせずそのまま実行しています。
ApachePOIでセルの値を取得する
読み込む対象のSpreadsheet、シート名が決まったら、セルの値を取得します。
今回は1列目の各セルの値を取得して、それが何列分あるかによって実際の値を読み込む時の列数を指定します。
SpreadsheetAccesser.kt
~省略~ fun loadFile(targetFilePath: String, targetSheetName: String){ // SpreadsheetのパスからStreamを作成する. val fileStream = FileInputStream(targetFilePath) // StreamからWorkbookを取得. val currentWorkbook = WorkbookFactory.create(fileStream) if(currentWorkbook == null){ return } // シート名から対象のシートを取得する. val targetSheet: Sheet? = currentWorkbook.getSheet(targetSheetName) if(targetSheet == null){ return } // セルに何らかの値が含まれる行数の取得. val rowCount = targetSheet.physicalNumberOfRows - 1 if(rowCount < 0){ return } // 最初の行から列数を取得する. val columnCount = targetSheet.getRow(0).physicalNumberOfCells - 1 // 最初の行をタイトル行としてArrayListを作成する. ColumnTitleList = ArrayList() for(cell in targetSheet.getRow(0)){ ColumnTitleList.add(getCellValue(cell)) } // 実際の値が入ったセルの値をセットするArrayListの生成. LoadedSheetItemList = ArrayList< ArrayList < String > >() // タイトル行から取得した列数 ✕ (セルに値が含まれる行数 - 1)の値をセットする. for(i in 1..rowCount){ val loadedRowItemList = ArrayList< String >() loadedRowItemList.add(i.toString()) for(t in 1..columnCount){ loadedRowItemList.add(getCellValue(targetSheet.getRow(i).getCell(t))) } LoadedSheetItemList.add(loadedRowItemList) } // Workbook、Streamを閉じる. currentWorkbook.close() fileStream.close() } fun getCellValue(targetCell: Cell?): String{ var result = "" if(targetCell == null){ return result } // SpreadsheetにおけるCellの型によらず、一律でStringとしてCellの値を取得. when(targetCell.cellType){ Cell.CELL_TYPE_BOOLEAN -> return targetCell.booleanCellValue.toString() Cell.CELL_TYPE_NUMERIC -> return targetCell.numericCellValue.toString() Cell.CELL_TYPE_STRING -> return targetCell.stringCellValue.toString() Cell.CELL_TYPE_FORMULA -> return targetCell.cellFormula.toString() } return result } }
- Spreadsheetから値を取る際に、Cell.XXXXCellValueのデータ型とセルのデータ型とが合致しないと(例:targetCell.booleanCellValueで文字列型のセルから値を取得しようとすると)、エラーが発生します。
そのためwhenを使ってセルのデータ型に合わせて値を取得するようにしています(getCellValue内)。
Jsonファイルを作成する
Spreadsheetから値が取得できたら、それをJsonとしてファイルに書き出します。
今回はGsonを使うことにしました。
インストール
プロジェクト直下にあるbuild.gradleに以下を追加します。
build.gradle
~省略~ dependencies { ~省略~ compile "com.google.code.gson:gson:2.7" }
Jsonファイルに書き出す
※2016.07.22 20:11更新
Jsonオブジェクトを作るところで誤りがあり、全データが一つの塊になっていたため、バラバラに分割するよう修正しました。
JsonFileCreater.kt
import com.google.gson.stream.JsonWriter import java.io.BufferedWriter import java.io.FileWriter import java.io.IOException import java.io.StringWriter import java.util.ArrayList class JsonFileCreater { fun createFile(titleList: ArrayList< String >, valueListCollection: ArrayList< ArrayList< String > >, fileTitle: String){ val stringWriter = StringWriter() val jsonWriter = JsonWriter(BufferedWriter(stringWriter)) // Jsonオブジェクトの作成開始. jsonWriter.beginObject() var i = 0 valueListCollection.forEach { valueList -> run{ jsonWriter.name("toilet_list_" + i.toString()) jsonWriter.beginObject() for(t in 0..valueList.size - 1){ // Jsonの項目名(name)と値(value)をそれぞれ指定する. jsonWriter.name(titleList[t]).value(valueList[t]) } jsonWriter.endObject() i++ } } // Jsonオブジェクトの作成を終了して文字列にする. jsonWriter.endObject() jsonWriter.close() val createdJson = String(stringWriter.buffer) try{ // 読み込んだSpreadsheetのファイル名を使ってJsonファイルを書き出す. val splittedTitles = fileTitle.split('.') if(splittedTitles.size <= 0){ return } val fileWriter = FileWriter(splittedTitles[0] + ".json") fileWriter.write(createdJson) fileWriter.close() }catch(e: IOException){ // TODO: 適切なエラー処理 } } }
おわりに
とりあえずそれっぽいファイルを作成することはできました。
ただ、セルから取り出した値を全て文字列型にしてしまっているため、例えば緯度・経度の値も文字列として扱われてしまいます。
今回のように使いみちが決まっている場合は、値を格納するためのクラスを専用に作成して、それぞれのデータ型のままJsonに書き出す方が良いかもしれません。
またはフラグを立てるとか...?
あと途中でも書きましたが、今回別スレッドでの実行や非同期処理を行っていないため、本当はその部分についても考慮が必要になるかと思います。
あと今回はKotlinで挑戦してみましたが、Androidアプリでは無いのだし、どうせなのでJavaで書いてver.8系の仕様を堪能する、ということにしても良かったかもしれません。
次は今回作成したJsonファイルを、Androidアプリから読み込むお話になるはず。
参考
JavaFX
- [JavaFX] FXMLの使い方 - キャスレーコンサルティング 技術ブログ
- 4 FXMLを使用したカスタム・コントロールの作成(リリース8) - ORACLE Javaドキュメント
- 初心者のためのJavaFXプログラミング入門 - libro ~入門ドキュメント・サイト~
- 【JavaFX】コンボボックスでバインドと使用方法 - Qiita
- 16 コンボ・ボックス(リリース8) - ORACLE Javaドキュメント
Apache POI
- Javaでexcelを読み書きするサンプル(Apache POIの使い方まとめ) - ぱーくん plus idea
- POIメモ(Hishidama's Apache POI Memo)
- java - get number of columns of a particular row in given excel - Stack Overflow
File Chooser
- FileChooser (JavaFX 8) - ORACLE Javaドキュメント
- [Java8] JavaFXでファイル選択ダイアログ - プログラミング・メモ
- ファイル選択ダイアログ(showOpenDialog)の作成方法 - JavaFXによるGUIアプリ開発入門:IT TRICK