日曜プログラミング

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

JavaFX + Clojure でのイベントハンドリング

GUIライブラリで気になる事と言えばやっぱりイベントハンドリングなので次はそいつを試してみる。

以下の tnoda さんの所の記事を見れば大体の事は書いてあるんだけど実際に自分で試したのでメモ。
tnoda-clojure • Clojure で Java FX (5): FXML
情報として独自的な部分があるとすれば Clojure をスクリプトエンジンとして fx:script の source 指定でファイル外出ししても動きましたよ、と言うくらいでこの言葉の説明だけで分かる人は後は読む必要なし。

tnoda さんの記事ではイベントハンドラを FXML 内の Javascript で書く方法を試している。これは JSR-223 に対応してる言語であれば何でも良いとの事らしい。ひなたねこさんの所の記事によるとClojure も JSR-223 に対応する外部ライブラリを組み込めば同じように書けるとの事。

実は Javascript はほぼ全く知らず、何となく作ろうと思ってるアプリとして想定しているのもデスクトップアプリなので Clojure でイベント処理書く方法を試してみる。*1

確認環境

Java 1.7.0_25*2

簡単な FXML を用意

公式チュートリアルなぞらえるのもいいんだけどもっとお手軽に試してみたいので
Scene Builder で簡単なボタン一個の画面を用意。

こんなの。

f:id:shinmuro:20131002151238p:plain

動作としてはボタン押したら下のラベルのテキストを変えるだけと言うもの。
上の画面は Scene Builder のプレビューで出るものをキャプチャ。

Scene Builder の使い方はこちらを参考にした。他で GUI ツール作った事ある人にはこれくらいの画面なら Scene Builder 立ち上げただけでやり方は想像付くと思う。

こいつを保存すると .fxml と言う XML 形式のファイルで保存される。今回は fx-ui.fxml と言う名前で保存。

中身は単なるテキストで今の段階だとこんな感じ。

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>

<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="157.0" prefWidth="261.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2">
  <children>
    <Button layoutX="75.0" layoutY="67.0" mnemonicParsing="false" text="はろー" />
    <Label layoutX="75.0" layoutY="102.0" prefHeight="17.0" prefWidth="280.0" text="ボタン押すとテキストが変わります" />
  </children>
</AnchorPane>

leiningen プロジェクト用意

さて、ここまでだとガワしかない上に Scene Builder 上のプレビューでしかなく単品で起動すらできないので、こいつを単品で動かす為の leiningen プロジェクトを用意。

>lein new app fxevent
Generating a project called fxevent based on the 'app' template.
>

project.clj の設定

さっき保存した fx-ui.fxml をこのプロジェクト直下の resources フォルダに放り込む。
ほんで 依存関係、AOT、リソースパスの設定を追加する。

(defproject fxevent "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"]]
  :resource-paths ["resources"]
  :aot [fxevent.core]
  :main fxevent.core)

core.clj 編集

ここでやってるのは FXML を読み込んでアプリ起動するだけ。

(ns fxevent.core
  (:import [javafx.application Application]
           [javafx.fxml FXMLLoader]
           [javafx.scene Scene]
           [javafx.stage Stage])
  (:require [clojure.java.io :as io])
  (:gen-class
   :extends javafx.application.Application))

(defn -start [this ^Stage stage]
  (let [scene (-> "fx-ui.fxml" io/resource FXMLLoader/load)]
    (doto stage
      (.setScene (Scene. scene))
      (.show))))

(defn -main [& args]
  (javafx.application.Application/launch fxevent.core args))

:import の所を見ると、
前記事で指定していた Button や Label などの import は FXML の方に追い出されている事が分かる。

ガワだけでも独立して動かしてみる

今のところイベントハンドリングの前段階でしかないんだが FXML を読み込めてるか確認する為一旦lein clean, deps, compile, run して動かしてみると動いた。*3

本題、イベントハンドリング

fx-ui.fxml をちょいいじる。詳しい説明は参考にした記事を見てもらうと言う事であまり書かないけどここでやった事は 4 つ。

  1. イベント処理する言語指定
  2. イベントハンドラに処理する関数呼び出しを指定
  3. イベントで影響を与えるコントロールがある場合は名前を指定
  4. イベントハンドラで処理させる関数の定義
<?xml version="1.0" encoding="UTF-8"?>

<?language Clojure?>        ; ------------ (1)

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>

<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="157.0" prefWidth="261.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2">
  <children>
    <Button layoutX="75.0" layoutY="67.0" mnemonicParsing="false" text="はろー" 
            onAction="(f)"/> ; ------------ (2)
    <Label fx:id="label"     ; ------------ (3)
           layoutX="75.0" layoutY="102.0" prefHeight="17.0" prefWidth="280.0" text="ボタン押すとテキストが変わります" />

    <fx:script>              ; ------------ (4)
      (defn f [] (.setText label "変えました"))
    </fx:script>
  </children>
</AnchorPane>

対応箇所を ; 以降に番号を振っているが、説明の便宜上付けているだけでビルド時は ; 以降は削除しないと多分エラーになると思う。

さて、公式チュートリアルに既に Clojure と言うワードが出てたのでえいやで試したけどやっぱりダメだったので project.clj に Clojure で JSR-223 をサポートする依存関係追加。

(defproject fxevent "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"]]
  :resource-paths ["resources"]
  :aot [fxevent.core]
  :main fxevent.core)

Clojure の JSR-223 サポートライブラリの公式はここだけど今回の例だと本当に dependencies に追加するだけなので API リファレンスなどを見る必要もない。

これで uberjar して出来た jar を実行すると無事動いた。出てきた画面のボタン押すとこんな感じでちゃんと変わる。
f:id:shinmuro:20131002150725p:plain

fx:script の source 指定によるイベント処理外出し

FXML に直接処理を埋め込んだが、Javascript などと同じく fx:script の source の部分でファイルを外出しして指定できる模様。と言うか tnoda さんの所では最初からそうしておりバックエンドで動く言語が変わっただけで Clojure だって多分同じようにできるはずとこれも試す。

まずは fx-ui.fxml のイベント処理本体を外出しするよう変更。

<?xml version="1.0" encoding="UTF-8"?>

<?language Clojure?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>

<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="157.0" prefWidth="261.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2">
  <children>
    <Button layoutX="75.0" layoutY="67.0" mnemonicParsing="false" text="はろー" 
            onAction="(f)"/>
    <Label fx:id="label"
           layoutX="75.0" layoutY="102.0" prefHeight="17.0" prefWidth="280.0" text="ボタン押すとテキストが変わります" />

    <fx:script source="action.clj" />
  </children>
</AnchorPane>
>

source の所で指定した action.clj はまあ見せるまでもないかもしれんけどこれだけ。

(defn f [] (.setText label "変えました"))

こちらも動いたー。出てくる画面は同じなので載せない。
これでやりたかった事の最低限の下地はできたって所かなー。

*1:できれば FXML も S式で書けたらとも思ったがそれは情報が見つからなかったので断念

*2:同梱される JavaFX は 2.2.25

*3:実はここ、最初プロジェクト名を fx-event にしてかつ :gen-class の :name を未指定にしたことで Java で見える class 名が fx_event.core に自動変換され、結果 Application/launch の引数で指定してる fx-event.core が見えないと怒られていた。前似たような事でプチハマりしてたのに忘れてた。今回はサンプルにあるように fxevent に名前を変えて解決したものの、こういう Java ライブラリを使う時のコーディングルールは考えておかないといけないな。