Home > Software

HsQML

HsQML Logo

HsQML Logo

Latest release 0.3.2.1 (Changelog) (Release Announcement)

Please use the Issue Tracker to report any problems you find with using the library.

Overview

HsQML is a Haskell binding to Qt Quick, a cross-platform framework for creating graphical user interfaces. Qt Quick utilises a declarative language called QML in combination with JavaScript to allow you to create rich animated UIs using a range of components. HsQML then allows you to bind together front-end designs written in QML with back-end logic written in Haskell to create complete applications using the strengths of both.

HsQML is a mid-level binding which abstracts users away from lower level details such as resource management, while still exposing the imperative nature of the underlying library. It allows you to define custom QML classes with methods, properties, and signals, via which the Haskell and QML worlds can interact. Hence, QML designs can invoke methods which translate into Haskell function calls and receive signals fired asynchronously from Haskell.

Requirements

As of the current release, HsQML requires Qt 5 and works with only version 2 of the Qt Quick platform. Releases prior to 0.3 required Qt 4.7 or 4.8 and used the original Qt Quick 1, but this is no longer supported. HsQML has been tested on the major desktop platforms supported by Qt: Linux, Windows, and Mac OS.

In order to build HsQML, you will require the following:-

Installation

HsQML is provided as a Cabal package containing the necessary scripts and source code so that it can be easily distributed, built, and installed on multiple platforms. Given a correctly setup Qt installation and Haskell development environment, simply typing "cabal install hsqml" should install the latest version available in the Hackage repository along with any dependent libraries.

If the Qt installer or package you're using has not already taken care of placing itself on the search path, you may need to set to your PATH environment variable so that Cabal can find the Qt libraries and tools.

Linux

If you are using Qt packages provided by your Linux distribution then everything should have been installed into standard locations already present on the appropriate search paths. Howver, if your distribution uses qtchooser to support both Qt 4 and 5 development environments, you may need to set the QT_SELECT environment variable or equivalent configuration file to select Qt 5.

On the other hand, if your Qt installation is located in a non-standard location then the LD_LIBRARY_PATH and PKG_CONFIG_PATH variables will need to be set to include Qt's lib and lib/pkgconfig directories respectively.

Windows

The Qt libraries you use must have been built with a version of the MinGW tool-chain which is compatible with the one used by the Haskell compiler.

64-bit

The Qt project doesn't currently release official 64-bit MinGW binaries. However, compatible binaries are available from the Qt-x64 project. Specifically, the qt-5.3.1-x64-mingw482r4-sjlj-opengl build has been tested to work.

32-bit

Unfortunately, the version of 32-bit MinGW used by the official Qt SDK is known to be incompatible with the version of MinGW which ships with current releases of GHC and the Haskell Platform. The problem typically manifests either as an error message concerning symbols missing from libstdc++6.dll, or by compiled executables crashing on start-up.

For this reason, I have produced a special build of Qt 5.3.0 for 32-bit Windows using GHC's MinGW which can be used out of the box in place of the Qt SDK binaries. Download and unpack this zip file into the root of your C: drive so that the qt530-hp directory is unpacked into C:\qt530-hp. You must use this location as the Qt installation is not relocatable. Finally, add C:\qt530-hp\bin to your PATH variable before trying to install HsQML with Cabal.

More details about the build are present in the announcement blog post. The original source code for the libraries is available from here.

Alternatively, it also appears to be possible to work around the problem by modifying the GHC settings file to point to the gcc executable from the copy of MinGW which ships with the Qt SDK. You may find this file located at, for example, C:\Program Files (x86)\Haskell Platform\2013.2.0.0\lib\settings. The file consists of an associative list in Haskell Read/Show format. Modify the entry with the key "C compiler command" to point to the gcc.exe from your Qt install's MinGW. You must rebuild HsQML after making this change.

MacOS

By default, HsQML expects that the Qt libraries are available as MacOS frameworks such as is provided by the Qt SDK. You can force the use of regular libraries and pkg-config by setting the UsePkgConfig Cabal flag if desired.

Usage

While at present the only complete source of documentation is the API reference, this section attempts to introduce basic usage through development of an example program. Our program will provide a graphical user interface for calculating factorials. The large numbers involved provide an opportunity to make use of Haskell's facility for arbitrary-precision arithmetic and shows how Haskell can bring additional functionality to the QML environment.

The complete buildable source code for each version of the example problem described below is included in the hsqml-demo-samples package on Hackage.

First version (hsqml-factorial1)

Calculating a factorial in Haskell is relatively simple:-

factorial :: Integer -> Integer
factorial n = product [1..n]

The first version of our program will straightforwardly expose this functionality to QML so that it can wrap an interface around it. To do this the program needs to create an object that exists in the QML world and give it a method which the QML front-end can call. The following code defines a class (in the OOP sense) for our object:-

import Graphics.QML
import Data.Text (Text)
import qualified Data.Text as T

main :: IO ()
main = do
    clazz <- newClass [
        defMethod' "factorial" (\_ txt ->
            let n = read $ T.unpack txt :: Integer
            in return . T.pack . show $ product [1..n] :: IO Text)]

The call to newClass defines a class with a single method called "factorial". The implementation of the method is supplied by a Haskell function in the IO monad. This function will be called whenever QML code invokes the corresponding method. Methods can take an arbitrary number of parameters from QML and also return a value back to QML.

The first parameter to the Haskell function is special because it carries the "this" value, a reference to the instance on which the method was called. It's discarded in this example because the method is essentially static and doesn't depend on data from the instance.

The function's second parameter is the method's first and only real argument. The value is received as a string to avoid it being truncated by the more limited numerical types available in QML. The function then calculates the factorial and then converts it back to a string prior to returning for the same reason.

The next step is to create an instance of the class for us to use:-

    ctx <- newObject clazz ()

The first parameter to newObject is the class we just created. The second is a piece of data to be associated with the instance. As above, the instance isn't used for anything other than carrying around the method and so a dummy value suffices. Supplying () does have the effect of fixing the type of the class value to Class () and the type of the method's "this" parameter to ObjRef (). If we had some data to associate with the instance then the method could extract its value from the ObjRef using fromObjRef.

The final step in the program is to load the QML file which describes the graphical interface and connect to the Haskell logic via our object:-

    runEngineLoop defaultEngineConfig {
        initialDocument = fileDocument "factorial1.qml",
        contextObject = Just $ anyObjRef ctx}

The above code specifies two important things. Firstly, that the QML code is stored in a file called factorial1.qml in the current working directory and, secondly, that the object we just created should be set as the QML environment's context object. This means that its members will be present in the global namespace and accessible to any QML expressions without qualification. With this set, the runEngineLoop function will block until the graphical interface is terminated, typically by closing the window. All that remains is to show the actual QML code in factorial1.qml:-

import QtQuick 2.0

Column {
    height: 300;
    TextInput {
        id: input; width: 300; height: 30; font.pixelSize: 30; focus: true;
    }
    Rectangle {
        color: "red"; width: childrenRect.width; height: childrenRect.height;
        Text {
            width: 300; height: 30; font.pixelSize: 30;
            text: "Calculate Factorial"; color: "white";
        }
        MouseArea {
            anchors.fill: parent;
            onClicked: output.text = factorial(input.text);
        }
    }
    Text {
        width: 300; wrapMode: Text.WrapAnywhere; font.pixelSize: 30;
        id: output;
    }
}

This code defines a simple interface consisting of a field for entering the input number, a button to trigger the factorial calculation, and an area for displaying the result. Have a look at the complete code in the hsqml-demo-samples package and give it a try yourself.

Screenshot of hsqml-factorial1

Screenshot of hsqml-factorial1

Second version (hsqml-factorial2)

However, the program given above does have one important defect. The function which implements the "factorial" method is called on the main UI event thread. If the calculation takes a noticeable amount of time then it will hang the interface while it completes. This is easy to demonstrate if you type a large number into the input field, the text field stops responding between pressing the start button until the calculation completing, and the window may become visually corrupt if other things overlap it (depending on platform). Ideally, the interface should remain responsive while the calculation is taking place.

The is possible to achieve by having the factorial method run the calculation on a different thread and then update the interface when it's complete using a signalling property. To begin with, a few additional things are needed before defining the revised class:-

    state <- newIORef $ T.pack ""
    skey <- newSignalKey

The state variable is mutable reference which will hold the current result value to be displayed. The newSignalKey function generates a unique identifier which can then be used to mark a QML signal when defining it and hence to refer to it when triggering that signal from Haskell code. This mechanism supports asynchronously notifying QML code of events.

The revised class begins by defining a read-only property called "result". The property simply accesses the state variable mentioned above and returns its current value to QML expression which requested it. The crucial thing is that it the property definition also sets up an association with a signal key. Hence, when that signal is triggered using the fireSignal function it will cause any QML expressions which used the value of this property to recompute themselves and hence update the interface.

    clazz <- newClass [
        defPropertySigRO' "result" skey (\_ ->
            readIORef state),

The "factorial" method now starts by setting the state variable to a temporary value and then firing the signal to update the interface:-

        defMethod' "factorial" (\obj txt -> do
            let n = read $ T.unpack txt :: Integer
            writeIORef state $ T.pack "Working..."
            fireSignal skey obj

Next, the actual factorial calculation is forked onto a separate thread. Note the use of evaluate to force the calculation to occur on the thread. If it weren't forced here then it wouldn't occur until the value was demanded by QML reading the "result" property on the UI thread due to Haskell's lazy semantics. Also, the result string is truncated to 1000 characters otherwise the string can become long enough to slow down the string marshalling on the UI thread, and that would spoil this example.

            forkIO $ do
                let out = T.take 1000 . T.pack . show $ product [1..n]
                evaluate out
                writeIORef state out
                fireSignal skey obj

The method doesn't return anything anymore, because the QML interface reads the result from the "result" property.

            return ())]

Finally, we need a slightly revised version of the QML code to make use of our signalling property. Instead of assigning the text property of the output element with the result of the factorial method, we data bind it to the result property. When the signal associated with the property is fired, the interface will update automatically.

        MouseArea {
            anchors.fill: parent;
            onClicked: factorial(input.text);
        }
    }
    Text {
        width: 300; wrapMode: Text.WrapAnywhere; font.pixelSize: 30;
        text: result;
    }
Screenshot of hsqml-factorial2

Screenshot of hsqml-factorial2

Further information

Downloads & Source Code


Robin KAY <komadori@gekkou.co.uk>