HsQML

HsQML Logo
HsQML Logo

Latest release 0.3.5.1 (Changelog) (Release Announcement)

Please e-mail me 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 version 2 of the Qt Quick platform. HsQML has been tested on the major desktop platforms supported by Qt: Linux, Windows, and MacOS.

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

† When using cabal-install, version 1.24 or later is required in order to select the correct version of the Cabal library required for the setup script.

Licence

HsQML is Open Source and available under the 3-clause BSD licence.

Note that the Qt library itself is not BSD licensed, but is available under several open source licences including the LGPL and there are also commerical offerings. See Qt’s licensing page for more information.

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 your Haskell compiler. The version of MinGW included with GHC was significantly updated with GHC 7.10.2, making it much easier to obtain compatible Qt libraries. If you are using a version of GHC older than this then see the old issues page for more instructions.

32-bit

At the time of writing, GHC 7.10.2 or later is compatible with the 32-bit MinGW libraries provided by the official Qt SDK.

64-bit

The Qt project does not provide official binaries for 64-bit MinGW, but compatible libraries can be built from source using GHC’s MinGW or obtained from a third-party. For example, the qt-5.5.0-x64-mingw510r0-seh-rev0 build from the now the inactive Qt64 project is known to work with GHC 7.10.2.

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. If you using

The HsQML Setup script should automatically set the correct framework path based on the location of the Qt development tools in your PATH.

Depending on the Qt libraries you’re using, you may also need to provide a way for your executables to find the required frameworks at runtime, either by bundling them or by setting the DYLD_FRAMEWORK_PATH environment variable to point to Qt’s lib directory. This is required for newer version of the Qt SDK, but not for 5.4 and earlier or for libraries installed via homebrew.

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>