So, as promised, here's a few notes on how I am using Qt Designer with Clojure.
Some important things to remember:
- You must use Qt Designer with the QtJambi plugins installed. The easiest way to do this is to run designer.sh (Linux / UNIX / Mac OS X) or designer.bat (Windows) from the distribution folder that QtJambi came in. If you've got QtJambi installed as a package under Linux then you may need to check that the QtJambi plugins are correctly installed , they weren't in my case.
- If you have the QtJambi plugins installed designer should be able to save .jui files instead of .ui files.
I don't intend to tell you how to use Qt Designer, there's a good user manual available.
Once you've put together a UI in Designer plugging it into a clojure application is relatively simple, though there are some subtleties that took me a while to work out (along with a large chunk of time trawling #clojure logs, the wiki and various blog post - thanks to all who share their work!)
What follows is the full source code for an incredibly simple app that uses a single QMainWindow with a Designer derived layout, I'll explain some more after the code:
(gen-and-load-class "foo.gen.Slot"
:methods [["slot" [] Void]])
(ns foo
(:import (foo Ui_MainWindow))
(:import (com.trolltech.qt.gui QApplication QMainWindow))
(:import (com.trolltech.qt.core QCoreApplication))
(:import (foo.gen Slot)))
(def slots
(proxy [Slot] []
(slot [] (prn "Slot Called"))))
(defn init []
(QApplication/initialize (make-array String 0)))
(defn exec []
(QApplication/exec))
(defmacro qt4 [& rest]
`(do
(try (init) (catch RuntimeException e# (println e#)))
~@rest
(exec)))
(def Foo-main
(fn [& args]
(qt4
(let [ui_main (new Ui_MainWindow)
mainWindow (new QMainWindow)]
(. ui_main (setupUi mainWindow))
(let [pushButton (. ui_main pushButton)]
(.. pushButton clicked (connect slots "slot()")))
(. mainWindow (show))))))
This code lives in the package "foo", in the file "Foo.clj". If you're familiar
R.P. Dillon's guide to with making Jar's from clojure you'll know that Foo-main is the Main function of the resulting JAR. Notice that we also import Ui_MainWindow from the same package. This is the generated Java code from the Designer file Ui_MainWindow.jui. In order to generate the java code you simply run:
juic Ui_MainWindow.jui
As I want the resulting code to live in a package, "foo", I need to manually add the following line to the top of the resulting file, Ui_MainWindow.java:
package foo;
We also need to arrange for this .java file to be compiled before we can use it. How you use this depends on your build system, but you can choose to just compile it by hand when it changes (as this should be comparitevly rare). I guess you know how, but just for completeness:
javac foo/Ui_MainWindow.java
Now, the qt4 macro I've defined there is directly lifted from
Brian Carper's blog posting on using Qt4 in Clojure, and again I won't go into that here. Instead lets look at Foo-main. Note that we create instances of both the Ui_MainWindow class from the generated java code and the QMainWindow class. We call the setupUI method of the Ui_MainWindow class with the QMainWindow class as it's arguement (this essentially fills out the QMainWindow with the layout you put together in designer). Note also that I reference the pushButton widget inside the Ui_MainWindow class.
The remaining interesting thing about this file is that it defines a custom slot. Signals and Slots are fundemental to the way that Qt hangs together. Wiring together the prexisting signals and slots is very easy and can be done either in Designer or in code (as R.P. Dillon's example shows). Making a custom slot turns out to be rather more difficult. I naively assumed that I could just use a proxy around the QMainWindow class and add a slot function there, but this simply doesn't work. Instead I need to use (gen-class) to make a real class and bind a clojure function to it. For convenience I use (gen-and-load-class) - this call generates the bytecode and loads it. You have to pass it a fully qualifed class name - I've found it better to make it a subpackage, so I place it in foo.gen, and therefore the classname is foo.gen.Slot. I also have to give a place holder for the slot method. Confusingly enough I've chosen to call it "slot" here. We pass a vector containing a vector for each method, which in turn contains a method name, a vector of arguements and a return type. The method name will get bound to a function in the current namespace (not foo.gen, but foo) with the name
-. In my example code this is contained in the proxy "slots" as the "slot" method. It's possible to just use a normal (defn) for this mapped method, in which case the function name would be "Slot-slot", I just find the proxy cleaner. Lastly you'll see that, back in Foo-main, I connect the pushButton's clicked signal to the custom slot, "slot" in the proxy, "slots". I hope that's clear!
Now all things being equal you should be able to make your JAR and execute the code. I hope that helps someone avoid spending a lot of time trying to work this out.
When I need to implement a custom signal I'll try and write that up too. Till then, ta ta!