日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

JavaFX 遊び TableView 編(1)

以下のチュートリアルに基本従いつつ Clojure を織り交ぜるとどうなるか試してみた。
Using JavaFX UI Controls: Table View | JavaFX 2 Tutorials and Documentation

まずは Example 12-6 でやってる、固定データをテーブルに追加する所まで。
結論から言うと ClojureJava を織り交ぜたコードゴリゴリ書く方法ならできなくはない。

すべき手順

結構手間かかるので何をしないといけないのかを簡単に箇条書き

  1. TableView は列を表す TableColumn を複数持つのでそのインスタンス作って入れる。ここまでは当然入れ物を用意するだけで中身のデータは一切入ってない。
  2. 1 行データを表すクラス定義をする*1
  3. ↑のクラス定義のインスタンスを ObservableList と言う TableColumn と紐付ける為の箱のようなものに行単位で入れ込んでいく*2
  4. 各 TableColumn の列と 1 行データを表すクラス内の変数を紐付ける
  5. ここまで仕込み完了したら TableView にデータとして追加可能

あまり正確ではないがイメージとしては掴めるんじゃないかと。

諦めた他の方法

Clojure JavaFX ラッパライブラリ upshot

seesaw の作者が作ってるものがあるが、EXPERIMENTAL と書いてあったので試さず。

FXML に fx:control でコントローラクラスを指定する

FXML で定義した fx:id を Clojure(Java)コード内で見られるようにするには、fx:control で指定したコントローラクラス内で @FXML を付けた、fx:id と同名のフィールドを定義してやる必要があるっぽい。

これは Clojureアノテーションを使えばできそうな気がするが、Clojure でのアノテーションの使い方を知らないのと、実際に試した情報が出てないので一旦ペンディング。

TableView の初期化イベントみたいなのを探しそこで無理やりデータを入れ込むようにする

fx:script で指定してるコード内では fx:id で定義した名前は見えるのでこれで良いのではと言う気もしたが、適当なイベントが見つからず。

ついでに言うと onAction もない。API ドキュメント見ると onAction があるのは ButtonBase から継承したもののみらしい。当たり前っちゃ当たり前か。

これか 1 つ前が使えたら GUI デザインは Scene Builder にお任せに出来るだけに惜しい。

FXML でゴリゴリ

実は FXML を使っての公式のチュートリアルもあるにはある。
Mastering FXML: Creating an Address Book with FXML | JavaFX 2 Tutorials and Documentation

ただ、これは読んでみると Java でゴリゴリ書いてるのをほぼ FXML に置き換えただけに見え、
TableView の雰囲気を掴むには適してない気がして試してない。

Clojure コードでゴリゴリ

↑で試した手法がダメだったので仕方なくこれならさすがに行けるだろうと思ったのだが、
Person クラスを defrecord で作ってセットしても何故か値が出ず。例外が出るわけではない。
値を追っかける方法も分からない為諦めた。

Clojure + Java 混在で何とかする

leiningen は ClojureJava で混在プロジェクトとして作成する事が可能で、
結果としての関連ソースを示す事にする。

フォルダ構成

プロジェクト名を fxtable とすると最終的にはこうなる。

fxtable
 ├ /java
  │ └ Person.java
  ├ /src/fxtable
 │ └ core.clj
  └ project.clj

project.clj

(defproject fxtable "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [clojure-jsr223/clojure-jsr223 "1.0"]
                 [local.oracle/javafxrt "2.2.25"]]
  :java-source-paths ["java"]
  :javac-options ["-cp" "C:/Java/jre7/lib/jfxrt.jar"]
  :aot [fxtable.core]
  :main fxtable.core)

JavaFX の依存関係が ClojureJava で同じものを参照するのにそれぞれ指定する必要があるのが
ちょっと気に入らない。

Person.java

一行データを入れるためのクラス。defrecord でまかなえるんじゃねと思って諦めた部分でもある。
チュートリアルとほぼ同じなのだが、SimpleStringProperty を import してるのとコンストラクタに public 付けてるのが異なる。チュートリアルでのクラス内クラスから外出しにしてるので public にしておかないと Clojure からは見えずんなコンストラクタねーよと怒られる。

import javafx.beans.property.SimpleStringProperty;

public class Person {
    private final SimpleStringProperty firstName;
    private final SimpleStringProperty lastName;
    private final SimpleStringProperty email;
 
    public Person(String fName, String lName, String email) {
        this.firstName = new SimpleStringProperty(fName);
        this.lastName = new SimpleStringProperty(lName);
        this.email = new SimpleStringProperty(email);
    }
 
    public String getFirstName() {
        return firstName.get();
    }
    public void setFirstName(String fName) {
        firstName.set(fName);
    }
        
    public String getLastName() {
        return lastName.get();
    }
    public void setLastName(String fName) {
        lastName.set(fName);
    }
    
    public String getEmail() {
        return email.get();
    }
    public void setEmail(String fName) {
        email.set(fName);
    }
}

core.clj

(ns fxtable.core
  (:import [javafx.application Application Platform]
           [javafx.scene Group Scene]
           [javafx.stage Stage]
           [javafx.collections FXCollections ObservableList]
           [javafx.geometry Insets]
           [javafx.scene.control Label TableColumn TableView TextField]
           [javafx.scene.control.cell PropertyValueFactory]
           [javafx.scene.layout VBox]
           [javafx.scene.text Font]
           Person)
  (:gen-class
   :extends javafx.application.Application))

(defn -start [this ^Stage stage]
  (let [scene (Scene. (Group.))
        table (TableView.)
        vbox (VBox.)
        firstNameCol (TableColumn. "First Name")
        lastNameCol (TableColumn. "Last Name")
        emailCol (TableColumn. "Email")
        label (Label. "Address Book")
        data (FXCollections/observableArrayList
              (to-array [(Person. "Jacob" "Smith" "jacob.smith@example.com")
                         (Person. "Isabella" "Johnson" "isabella.johnson@example.com")]))]
    (doto stage
      (.setTitle "Table View Sample")
      (.setWidth 450)
      (.setHeight 500))
    (.setFont label (Font. "Arial" 20))
    (.setEditable table true)
    (doto firstNameCol
      (.setMinWidth 100)
      (.setCellValueFactory (PropertyValueFactory. "firstName")))
    (doto lastNameCol
      (.setMinWidth 100)
      (.setCellValueFactory (PropertyValueFactory. "lastName")))
    (doto emailCol
      (.setMinWidth 100)
      (.setCellValueFactory (PropertyValueFactory. "email")))
    (.setItems table data)
    (.. table getColumns (addAll (to-array [firstNameCol lastNameCol emailCol])))
    (doto vbox
      (.setSpacing 5)
      (.setPadding (Insets. 10 0 0 10)))
    (.. vbox getChildren (addAll (to-array [label table])))
    (as-> (.getRoot scene) obj (cast Group obj) (.getChildren obj) (.addAll obj (to-array [vbox])))
    (doto stage
      (.setScene scene)
      (.show))))

(defn -main [& args]
  (Application/launch fxtable.core args))

これら構成ファイルで leiningen プロジェクト作って lein run すればチュートリアルの Example 12-6 と同じ画面が出る。

まとめ

そのまま書き下すにはメリットほぼない気がする。protocol 駆使して Java のクラスに従属してるメソッドを関数として同じ動詞にまとめちゃうと言う場面が増えたら割と良いかもしれない。しかし単にデータ入れ込むだけのクラスだったら defrecord でいいじゃんと思いつつ動かないのがイマイチ納得できん。とは言え TableColumn と紐付ける時に具体的にどうやってヒモ付けてるかが見えないから取り敢えず諦める。

まずはこの手法でチュートリアルで残ってるデータの追加・セルの編集・別形式でのデータモデルとのヒモ付けのトピックを試してみる事にする。

*1:チュートリアルで言う Defining the Data Model の項目に当たる

*2:ここでは紐付け完了してない為注意