Pain is inevitable. Suffering is optional.
― Haruki Murakami, What I Talk About When I Talk About Running
Software Development and Data Consultant
Pain is inevitable. Suffering is optional.
― Haruki Murakami, What I Talk About When I Talk About Running
As stated in a previous post I am in the process of building a GUI application and I would like to use Clojure for that. In this post I would like to write about my experiences building a small app for evaluation of the halgari/fn-fx Clojure library that aims at building GUIs with JavaFX in a declarative way, similar to what React does for the browser DOM.
I also considered using Electron but I would prefer a solution that runs on the JVM so that I can make full use of that. However, if you can live with the JS runtime I would definitely recommend checking out Electron too. Probably I will have a follow-up post about Electron. 🙂
I did implement a simple app concept with various technologies (e.g. fn-fx or Electron) to get some impressions how they differ. The app is quite simple but is supposed to cover some important basics like local file access, native dialogs, state updates and rich widgets:
If you prefer to get the complete code immediately, just check it out on GitHub (there is also an Electron version which is not yet finished though).
I am using Leiningen, here is what the project.clj
looks like:
(defproject fn-fx-ui "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.8.0"]
[halgari/fn-fx "0.3.0-SNAPSHOT"]
[org.clojure/data.csv "0.1.3"]]
:main fn-fx-ui.javafx-init
:aot [fn-fx-ui.javafx-init]
:target-path "target/%s"
:profiles {:uberjar {:aot :all}})
Of course fn-fx
was added as a dependency and also org.clojure/data.csv
to handle the csv data. The only other important details are the declaration of the :main
namespace and explicitly enabling :aot
for that. This is a JavaFX initialisation namespace that we need for lein run
as well as running the application from an Uberjar.
Here is the whole fn-fx-ui.javafx-init
namespace:
(ns fn-fx-ui.javafx-init
(:require [fn-fx-ui.core :as core])
(:gen-class
:extends javafx.application.Application))
(defn -start [app stage]
(core/start {:root-stage? false}))
(defn -main [& args]
(javafx.application.Application/launch fn_fx_ui.javafx_init
(into-array String args)))
The :gen-class
statement in the namespace declaration is important! We need to generate a class for being able to use the -main
function as an entry point. But it is as important to extend the JavaFX Application
class: This is needed for JavaFX to start properly. If you don’t do that you might run into compilation issues and hanging applications on exit.
Extending Application
requires us to implement -main
as well as -start
. The main
function just launches the app via direct Java interop passing along the application class (fn_fx_ui.javafx_init
is the name of the Java class generated due to our namespace declaration) and the main arguments.
Calling launch
ultimately invokes the -start
function. Here we simply call another start function from our core namespace core/start
. I am going to explain the argument {:root-stage? false}
in the next section.
Let us have a look at the core namespace now.
The application state is handled in a single atom on the namespace top-level:
(def initial-state
{:options {:csv {:first-row-headers false}}
:root-stage? true
:data [[]]})
(defonce data-state (atom initial-state))
initial-state
just exists to being able to reset the data-state
atom easily which can be very useful when working from the REPL.
Here is the start
function mentioned above, the entry point in our core application:
(defn start
([] (start {:root-stage? true}))
([{:keys [root-stage?]}]
(swap! data-state assoc :root-stage? root-stage?)
(let [handler-fn (fn [event]
(println event)
(try
(swap! data-state handle-event event)
(catch Throwable exception
(println exception))))
ui-state (agent (fx-dom/app (stage @data-state)
handler-fn))]
(add-watch
data-state :ui
(fn [_ _ _ _]
(send ui-state
(fn [old-ui]
(println "-- State Updated --")
(println @data-state)
(fx-dom/update-app old-ui
(stage @data-state)))))))))
The root-stage?
argument gets written to the application state atom. The handler-fn
receives events triggered from UI elements like clicking a button. In addition to printing some logs and handling exceptions it only updates the data-state
atom with whatever a function handle-event
returns when called with the UI event
. handle-event
is a multimethod handling the various events, we will see it later.
The next let binding is very important ui-state
is an agent holding the app created by the fn-fx
library via fx-dom/app
. The first argument is a “stage” returned by (stage @data-state)
. This is the rendered root element of the declaratively defined UI tree. We will see the UI tree in the next section. The app
function also takes the handler-fn
as an argument so it can invoke that with any UI events.
The last thing we need to do is to register a watcher on the data-state
atom. Whenever the state changes this watcher will be triggered and send an update function to the ui-state
agent. This will call fx-dom/update-app
with the old UI state and a new rendered “stage” based on the updated data-state
. This function will do a diff between the old state and the new state and update only the UI elements that need an update. You basically get the same update mechanism like you would in React and similar frameworks. Every UI update is driven by a change in the application state instead of fiddling with UI updates yourself.
The stage
function we called above is being created by the following usage of the defui
macro provided by fn-fx
:
(defui Stage
(render [this {:keys [root-stage? data options] :as state}]
(controls/stage
:title "fn-fx-ui"
:on-close-request (force-exit root-stage?)
:shown true
:scene (controls/scene
:root (controls/border-pane
:top (controls/h-box
:padding (javafx.geometry.Insets. 15 12 15 12)
:spacing 10
:alignment (javafx.geometry.Pos/CENTER)
:children [(controls/button
:text "Import CSV"
:on-action {:event :import-csv
:fn-fx/include {:fn-fx/event #{:target}}})
(controls/check-box
:text "Import first row as headers"
:selected (get-in options [:csv :first-row-headers])
:on-action {:event :toggle-option
:path [:csv :first-row-headers]})
(controls/button
:text "Reset"
:on-action {:event :reset})])
:center (controls/v-box
:children [(table {:data data})
(plot {:data data})]))))))
This is a declarative description of our app root UI element as a simple Clojure map. Well, there is the initial definition of render
as an implementation of a protocol function (see IUserComponent
in fn-fx
) this is how you get the state
passed to be used in the UI tree.
The function calls like controls/stage
, controls/border-pane
and controls/check-box
are very thin wrappers around the JavaFX component APIs. The names arguments passed to those functions, like :text "Import CSV", are turned into corresponding setter methods of the Java API (e.g.
.setText(“Import CSV”)`. This is pretty cool, luckily the JavaFX API is very consistent across components and thus you can pretty much use any setter like that to define the component. I hope most of the settings make sense without having to explain them but when in doubt, please just check the corresponding doc for the JavaFX component and you will see it match easily.
I think noteworthy are three things here:
:on-close-request
(will trigger the Java setter [setOnCloseRequest] to force an application exit:on-action
(maps to setter setOnAction
)table
and plot
are being used in the very endI am going to explain these three things in the next sections.
When closing the window of our stage, the one described in the declarative tree above, we still have the stage from the javafx-init
namespace if we used that (like we would with lein run
or from an Uberjar). This means the application process would not stop because we still have a running stage. But that remaining stage does not have an actual window which could be closed. So by setting root-stage?
to false
, I enforce closing the whole application when we close our main stage:
(defn force-exit [root-stage?]
(reify javafx.event.EventHandler
(handle [this event]
(when-not root-stage?
(println "Closing application")
(javafx.application.Platform/exit)))))
On a macOS machine you might not want to do that to mimic the OS behaviour of not closing applications automatically when closing the last window. You might want to tweak this to fit to your needs and maybe have an OS dependent solution.
Via :on-action
the UI actions are being set. The value can be a map of whatever you want to pass along to the registered event handler. Here is an example from the code above:
(controls/button
:text "Reset"
:on-action {:event :reset})
I am using the :event
key-value pair for dispatching in a multimethod handling all events. So this is a very simple example but you could have any additional data in the :on-action
map that you would like to use. fn-fx
has a cool mechanism to include contextual data in the map automatically. Here is an example that includes the target of an event (the component on which the event was triggered):
(controls/button
:text "Import CSV"
:on-action {:event :import-csv
:fn-fx/include {:fn-fx/event #{:target}}})
Here is how to use that contextual data from the event handler for the :import-csv
event:
(defmethod handle-event :import-csv
[{:keys [options] :as state} {:keys [fn-fx/includes]}]
(let [window (.getWindow (.getScene (:target (:fn-fx/event includes))))
dialog (doto (FileChooser.) (.setTitle "Import CSV"))
file (util/run-and-wait (.showOpenDialog dialog window))
data (with-open [reader (io/reader file)]
(doall (csv/read-csv reader)))]
(assoc state :file file :data
(if (get-in options [:csv :first-row-headers])
data
(cons (map #(str "x" (inc %)) (range (count (first data)))) data)))))
You can see that the event handler gets the state and the value of the :on-action
declaration (check back at start
: handle-event
is used as a function to update the data-state
via swap!
). The contextual data can easily be retrieved from the event. Above, destructuring is being used in the function signature. The first let
binding then uses the includes
map to get a JavaFX Window
object.
A dialog is being built on the next binding. To show the dialog for opening a file, we use the fn-fx
function util/run-and-wait
to open the dialog on the JavaFX Application Thread (this is the UI thread!) and wait until it was closed. The return value is the selected file which then gets read, parsed and associated in the state map. The return value of handle-event
will be swapped as the new value into data-state
(see handler-fn
in start
function).
Remember those calls to plot
and table
in the UI tree? Those are components defined in dedicated functions. Rendering a table requires an additional component for each column and also a factory to create table cell values. Everything we need for the table looks like this:
(defn cell-value-factory [f]
(reify javafx.util.Callback
(call [this entity]
(ReadOnlyObjectWrapper. (f (.getValue entity))))))
(defui TableColumn
(render [this {:keys [index name]}]
(controls/table-column
:text name
:cell-value-factory (cell-value-factory #(nth % index)))))
(defui Table
(render [this {:keys [data]}]
(controls/table-view
:columns (map-indexed
(fn [index name]
(table-column {:index index
:name name}))
(first data))
:items (rest data)
:placeholder (controls/label
:text "Import some data first"))))
I think this should not be too hard to understand. Just nested components with columns being built via map-indexed
. The stuff happening in cell-value-factory
is a very basic implementation to make JavaFX happily accept it and render the values.
Ok, does that sound a bit scary? At first I though I hit a wall with fn-fx
when I wanted to use the ScatterChart
component of JavaFX because it has some mandatory arguments for its constructor. With everything we saw so far, fn-fx
would first construct an object and then use setters to further specify attributes. This does not work with ScatterChart
but here is a solution, pay attention to the call of diff/component
:
(defui Plot
(render [this {:keys [data]}]
(diff/component [:javafx.scene.chart.ScatterChart
[]
[(javafx.scene.chart.NumberAxis.)
(javafx.scene.chart.NumberAxis.)]]
{:data (data->series data)})))
We call diff/component
with two arguments: A “type” of component which in our case is a vector with the first element being a key for the JavaFX class, the second argument is a vector with the names of named arguments (in our case []
) and the last element is a vector with the argument values (two JavaFX NumberAxis
here which are the mandatory arguments). The second argument are the specifications for the component the same way as we saw them before, so :data 123
would call the setter .setData(123)
.
So this way we can handle the situations where we have to invoke the constructor in a custom way instead of being able to just use whatever fn-fx
has already wrapped nicely.
To conclude the plotting component I want to show you the data->series
function:
(defn data->series [data]
(if (some? data)
(let [transpose #(apply mapv vector %)
transposed-data (transpose data)
xs (rest (first transposed-data))
build-series (fn [[name & ys]]
(diff/component :javafx.scene.chart.XYChart$Series
{:name name
:data (map
#(javafx.scene.chart.XYChart$Data.
(bigdec (first %))
(bigdec (second %)))
(transpose [xs ys]))}))]
(map build-series transposed-data))
[]))
I hope there are no big questions marks about that anymore now. We need to add multiple series to the plot each with a set of data points to be plotted.
Hopefully you got some insights about how to work with fn-fx
and if this is something you would like to do. I actually like it a lot after having had to figure out a couple of things on my own. Also I had to patch fn-fx
a bit as some things were not yet implemented or not working as I needed them to. So at the time writing this, there is an open PR for those changes. Kudos to Timothy for putting fn-fx
out there!!
The full code is available on GitHub. In my opinion this could be a very viable way of building cross-platform GUIs with Clojure, expecting to run into some issues seems fair considering how fresh the library is. So please be aware of that risk should you plan to rely on it.
If you have any questions about the topic, would like to sync or chat, please get in touch with me or just leave a comment. I am looking forward to continue my work with fn-fx
! Nevertheless I also plan to write about my experience with other approaches including Electron, so stay tuned.
Happy hacking!
[…] Blum-Oeste has a long post about functional GUI programming with Clojure and JavaFX with […]
Sean says
February 4, 2017 at 06:52It seems to me that the “root-stage?” part of the example isn’t necessary. Setting javafx.application.Platform.setImplicitExit() to true seems sufficient to cause the application to exit when that aspect of the example is removed.
Nils Blum-Oeste says
February 4, 2017 at 16:50Great thanks, your help is appreciated I am going to test what you suggest!
Bo says
July 19, 2017 at 05:49Thank you for your effort and upload fn-fx-ui.
I’m newbie for clojure and javaFX. Your code draw my curiosity and interest a lot. I downloaded your code and try to run on mac osx.
It looked working, but there was an error after importing example1.csv.
I think the plot is not working. Please advise me.
Here is error message.
—————
{:event :import-csv, :fn-fx/include {:fn-fx/event #{:target}}, :fn-fx/includes {:fn-fx/event {:target #object[javafx.scene.control.Button 0x5638cdca Button@5638cdca[styleClass=button]’Import CSV’]}}}
— State Updated —
{:options {:csv {:first-row-headers false}}, :root-stage? false, :data ((x1 x2 x3 x4 x5) [10 11 12 13 14] [21 21 12 51 15] [12 51 75 12 51]), :file #object[java.io.File 0xb08b53a /Users/Bo/Works/clojure/clj-ui-spikes/data/example2.csv]}
“Elapsed time: 28.1026 msecs”
#error {
:cause Assert failed: TODO: Implement this
(= idx (count lst))
:via
[{:type java.lang.AssertionError
:message Assert failed: TODO: Implement this
(= idx (count lst))
:at [fn_fx.fx_dom.FXDom$fn__843 invoke fx_dom.clj 33]}]
:trace
[[fn_fx.fx_dom.FXDom$fn__843 invoke fx_dom.clj 33]
[clojure.lang.AFn run AFn.java 22]
[com.sun.javafx.application.PlatformImpl lambda$null$173 PlatformImpl.java 295]
[java.security.AccessController doPrivileged AccessController.java -2]
[com.sun.javafx.application.PlatformImpl lambda$runLater$174 PlatformImpl.java 294]
[com.sun.glass.ui.InvokeLaterDispatcher$Future run InvokeLaterDispatcher.java 95]]}
————————————-
Nils Blum-Oeste says
July 24, 2017 at 09:37Hi Bo,
you are on an old version of fn-fx. Here is the relevant change that you seem to be missing:
https://github.com/halgari/fn-fx/commit/7ee92f4900cbb90b2a0728ef2a746ccdd2b15db1#diff-c59ddf359d8cb0337c368ffafaf83c0aL33
Could you please make sure that you actually have that? The latest halgari/fn-fx master from github should be fine (e.g. using Leiningen checkouts or installing it locally via lein install).
Takuya says
October 24, 2017 at 03:26Hello, I want to use control/writable-image with getPixelWriter (so I’ll draw image pixel-by-pixel). But I cannot find the solution.
I think …
(defui View-data
(render [this {:keys data}]
(let [writer (.getPixelWriter (controls/writable-image :width width :height height))]
(for [i (range width) j (range height)
(.setColor writer i j (nth data (+ ((dec i) * width) (dec j)))))
)))
* data is a seq of Color Data (javafx.scene.paint.Color(r,g,b,alpha))
But This function cannot run…
Nils Blum-Oeste says
November 6, 2017 at 08:51Hey, would you mind sharing some more details, like an error message, stacktrace, etc.?
mist says
January 3, 2018 at 09:02Hello Nils,
I was thinking of using fn-fx, but it seems the fn-fx project on github is abandoned, issues go unanswered. Do you still recommend it?
Nils Blum-Oeste says
January 6, 2018 at 15:48Hi, that’s a pity. Did you already try reaching out to Timothy (the author) about it? It’s hard to tell if I would recommend it for your use case. I am still fine with it but I am also building a very simplistic GUI with it. Also I have not been working on the GUI so much recently but rather on the algorithms in the background. So I might not be the best authority to judge the maturity and feasibility actually.
George Kontsevich says
May 17, 2018 at 06:13This is a fantastic guide. Thank you so much for writing it. I saw your fixes were merged, but it seems it no longer works with fn-fx 0.4.0. I posted an issue on your repo https://github.com/nblumoe/clj-ui-spikes/issues/1
I’m really hoping to getting it running! This is really cool stuff 🙂
Nils Blum-Oeste says
May 26, 2018 at 07:58Thanks!
I need to look into the issue, I haven’t been using this for quite a while.
Paul says
October 19, 2018 at 15:40I submitted a pull request to fn-fx/fn-fx (https://github.com/fn-fx/fn-fx/pull/39) which fixes this issue. It seems that the way that the constructor method gets chosen did not take into account the number of arguments supplied and so the instead of calling the 2 argument constructor, the 3 argument version was being selected and called.