日曜プログラミング

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

JavaFX 遊び TableView 編(4)

公式チュートリアルをなぞった TableView 編は最後。
Using JavaFX UI Controls: Table View | JavaFX 2 Tutorials and Documentation
Example 12-10 に相当。

最後はセルの編集を可能にする。
実は公式だとベースクラス継承してより便利に編集みたいな例も残ってるんだけど今の時点では興味ないのでパス。

project.clj

(defproject fx-table-edit "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"]
                 [local.oracle/javafxrt "2.2.25"]]
  :aot [fx-table-edit.core]
  :main fx-table-edit.core)

core.clj

(ns fx-table-edit.core
  (:import javafx.application.Application
           [javafx.collections FXCollections ObservableList]
           [javafx.event ActionEvent EventHandler]
           javafx.geometry.Insets
           [javafx.scene Group Scene]
           [javafx.scene.control Button Label TableColumn TableView TextField]
           [javafx.scene.control.cell MapValueFactory TextFieldTableCell]
           [javafx.scene.layout HBox VBox]
           javafx.scene.text.Font
           javafx.stage.Stage
           [javafx.util Callback StringConverter])
  (:gen-class
   :extends javafx.application.Application))

(def ^:private key-names ["fname" "lname" "email"])

(defn make-cell-edit-event-handler []
  (proxy [EventHandler] []
    (handle [t]
      (.. t getTableView getItems
          (get (.. t getTablePosition getRow))
          (put (.. t getTableColumn getCellValueFactory)
               (.getNewValue t))))))

(defn -start [this ^Stage stage]
  (let [table (TableView.)
        data (FXCollections/observableArrayList
              (map #(java.util.HashMap. (zipmap key-names %))
                   [["Jacob", "Smith", "jacob.smith@example.com"]
                      ["Isabella", "Johnson", "isabella.johnson@example.com"]
                      ["Ethan", "Williams", "ethan.williams@example.com"]
                      ["Emma", "Jones", "emma.jones@example.com"]
                      ["Michael", "Brown", "michael.brown@example.com"]]))
        hb (HBox.)
        scene (Scene. (Group.))
        label (Label. "Address Book")
        firstNameCol (TableColumn. "First Name")
        lastNameCol (TableColumn. "Last Name")
        emailCol (TableColumn. "email")
        table-columns [firstNameCol lastNameCol emailCol]
        cell-edit-event (make-cell-edit-event-handler)
        addFirstName (TextField.)
        addLastName (TextField.)
        addEmail (TextField.)
        addButton (Button. "Add")
        add-fields [addFirstName addLastName addEmail]
        button-action (proxy [EventHandler] []
                        (handle [e]
                          (.add data (->> (map #(.getText %) [addFirstName addLastName addEmail])
                                          (zipmap key-names)))
                          (map #(.clear %) [addFirstName addLastName addEmail])))
        vbox (VBox.)]

    (doto stage
      (.setTitle "Table View Sample")
      (.setWidth 450)
      (.setHeight 500))
    (.setFont label (Font. "Arial" 20))
    (.setEditable table true)
    (.setItems table data)
    (doall
     (map (fn [c width k]
            (.setMinWidth c width)
            (.setCellValueFactory c (MapValueFactory. (some #{k} key-names)))
            (.setCellFactory c (TextFieldTableCell/forTableColumn))
            (.setOnEditCommit c cell-edit-event))
          table-columns
          [100 100 200]
          key-names))
    (.. table getColumns (setAll (to-array table-columns)))

    (map #(.setPromptText %1 %2) add-fields ["First Name" "Last Name" "Email"])
    (map #(.setMaxWidth %1 (.getPrefWidth %2)) add-fields table-columns)

    (.setOnAction addButton button-action)

    (.. hb getChildren (addAll (to-array [addFirstName addLastName addEmail addButton])))
    (.setSpacing hb 3)

    (doto vbox
      (.setSpacing 5)
      (.setPadding (Insets. 10 0 0 10)))
    (.. vbox getChildren (addAll (to-array [label table hb])))

    (as-> (.getRoot scene) obj (cast Group obj) (.getChildren obj) (.addAll obj (to-array [vbox])))

    (doto stage
      (.setScene scene)
      (.show))))

(defn -main [& args]
  (Application/launch fx_table_edit.core args))
  • import は単体なら単体、複数まとめる時は囲むと言う風に変えてみた。どっちがいいのかね。
  • make-cell-edit-event-handler は元のも結構なメソッドチェーンっぷりなんだけど Clojure 流に直してもそこは勿論変わらないw
    • また、こっちの例では ObservableList は HashMap のリストで保持してるので、カラムのキーとセルの値で HashMap.put と言う形に変わってる。
  • 前回 map で TableColumn 同じ処理させようとして上手く行ってないのはどうも doto どうこうでなく map が lazy-seq を返すからっぽい。doall かますと map 使っても表示された。doto 外れてるが今の時点じゃ練習だし面倒なのでこのまま。
  • 今回 data を入れるところで java.util.HashMap に変換するように変えてるが、これはイベントハンドラでセルを入力されたものに変更する時、TableColumn.cellValueFactory をキーにして Map データを更新する(put メソッド)ってのを Clojure の PersistentMap で保持してるとできないため*1。immutable を保持しておきたい所だけど、良い方法も思いつかなかったのでこうした。

何にせよ、TableView は Map データを入れ込めるのが Clojure にとっても大きいと思う。

*1:unSupportedOperation だっけ、そんな感じの例外が出る