Module nimqml

Authors:

Filippo Cucchetto <filippocucchetto@gmail.com>

Will Szumski <will@cowboycoders.org>

Version: 0.4.5
Date: 2015/09/15

Introduction

The NimQml module adds Qt Qml bindings to the Nim programming language allowing you to create new modern UI by mixing the Qml declarative syntax and the Nim imperative language.

The NimQml is made by two components:

This first component implements the glue code necessary for communicating with the Qt C++ library, the latter module wraps the libDOtherSide exported symbols in Nim

Building

At the time of writing the DOtherSide C++ library must be compiled and installed manually from source.

First clone the DOtherSide git repo

git clone https://github.com/filcuc/DOtherSide

than you can proceed with the common CMake build steps

mkdir build
cd build
cmake ..
make

If everything goes correctly, you'll have built both the DOtherSide C++ library and the Nim examples

Installation

The installation is not mandatory, in fact you could try the built Nim example in the following way

cd path/to/build/dir
cd Nim/Examples/HelloWorld
export LD_LIBRARY_PATH=path/to/libDOtherSide.so
./HelloWorld

The DOtherSide project is made of two components

  1. The DOtherSide C++ lib
  2. The NimQml module

You can procede with the installation of the C++ library in the following way

cd to/build/dir
make install

or by manually copying the library in your system lib directory

sudo cp build/dir/path/DOtherSide/libDOtherSide.so /usr/lib

For the NimQml module you can use the nimble package manager

nimble install NimQml

or

cd to/build/dir/Nim/NimQml
nimble install

Example 1: HelloWorld

As usual lets start with an HelloWorld example. Most of the NimQml projects are made by one or more nim and qml files. Usually the .nim files contains your app logic and data layer. The qml files contain the presentation layer and expose the data in your nim files.

Examples/HelloWorld/main.nim

import NimQml
import macros
import typeinfo

proc mainProc() =
  var app = newQApplication()
  defer: app.delete()
   
  var engine = newQQmlApplicationEngine()
  defer: engine.delete()

  engine.load("main.qml")
  app.exec()

when isMainModule:
  mainProc()
  GC_fullcollect()

Examples/HelloWorld/main.qml

import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1

ApplicationWindow
{
    width: 400
    height: 300
    title: "Hello World"
    Component.onCompleted: visible = true
}

The following example shows the basic steps of each NimQml app

  1. Create the QApplication for initializing the Qt runtime
  2. Create the QQmlApplicationEngine and load your main .qml file
  3. Call the exec proc of the QApplication instance for starting

the Qt event loop

Example 2: exposing data to Qml

The previous example shown you how to create a simple application window and how to startup the Qt event loop.

It's time to explore how to pass data to Qml, but lets see the example code first:

Examples/SimpleData/main.nim

import NimQml
import macros
import typeinfo

proc mainProc() =
  var app = newQApplication()
  defer: app.delete()
   
  var engine = newQQmlApplicationEngine()
  defer: engine.delete()

  var qVar1 = newQVariant(10)
  defer: qVar1.delete()

  var qVar2 = newQVariant("Hello World")
  defer: qVar2.delete()

  var qVar3 = newQVariant(false)
  defer: qVar3.delete()

  var qVar4 = newQVariant(3.5.float)
  defer: qVar4.delete()
  
  engine.rootContext.setContextProperty("qVar1", qVar1) 
  engine.rootContext.setContextProperty("qVar2", qVar2)
  engine.rootContext.setContextProperty("qVar3", qVar3)
  engine.rootContext.setContextProperty("qVar4", qVar4)
  engine.load("main.qml")
  app.exec()

when isMainModule:
  mainProc()
  GC_fullcollect()

Examples/SimpleData/main.qml

import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1

ApplicationWindow
{
    width: 400
    height: 300
    title: "SimpleData"
    Component.onCompleted: visible = true

    ColumnLayout
    {
        anchors.fill: parent
        SpinBox { value: qVar1}
        TextField { text: qVar2}
        CheckBox { checked: qVar3}
        SpinBox { value: qVar4; decimals: 1 }
    }
}

The following example shows how to expose simple data types to Qml:

  1. Create a QVariant and set its internal value.
  2. Create a property in the Qml root context with a given name.

Once a property is set through the setContextProperty proc, it's available globally in all the Qml script loaded by the current engine (see the official Qt documentation for more details about the engine and context objects)

At the time of writing the QVariant class support the following types:

Example 3: exposing complex data and procedures to Qml

As seen by the second example, simple data is fine. However most applications need to expose complex data, functions and update the view when something changes in the data layer. This is achieved by creating an object that derives from QObject.

A QObject is made of :

  1. Slots: slots are functions that could be called from the qml engine and/or connected to Qt signals
  2. Signals: signals allow the sending of events and be connected to slots
  3. Properties: properties allow the passing of data to the Qml view and make it aware of changes in the data layer

A QObject property is made of three things:

We'll start by looking at the main.nim file

Examples/SlotsAndProperties/main.nim

import NimQml
import Contact

proc mainProc() =
  var app = newQApplication()
  defer: app.delete()
   
  var contact = newContact()
  defer: contact.delete() 
  
  var engine = newQQmlApplicationEngine()
  defer: engine.delete()

  var variant = newQVariant(contact)
  defer: variant.delete()

  engine.rootContext.setContextProperty("contact", variant)
  engine.load("main.qml")
  app.exec()

when isMainModule:
  mainProc()
  GC_fullcollect()

Here, nothing special happens except:

  1. The creation of Contact object
  2. The injection of the Contact object to the Qml root context using the setContextProperty as seen in the previous example

The Qml file is as follows:

Examples/SlotsAndProperties/main.qml

import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1

ApplicationWindow
{
	width: 400
	height: 300

	Component.onCompleted: visible = true

	ColumnLayout
	{
	    anchors.fill: parent

	    Label
	    {
	        text: "Current name is:" + contact.name
	    }

	    TextField
	    {
	        id: textField
	    }

	    Button
	    {
	        text: "Change Name"
	        onClicked: contact.name = textField.text 
	    }
	}
}

The qml is made up of: a Label, a TextInput widget, and a button. The label displays the contact name - this automatically updates when the contact name changes.

When clicked, the button updates the contact name with the text from the TextInput widget.

So where's the magic?

The magic is in the Contact.nim file

Examples/SlotsAndProperties/Contact.nim

## Please note we are using templates where ordinarily we would like to use procedures
## due to bug: https://github.com/Araq/Nim/issues/1821
import NimQml

type Contact = ref object of QObject 
  m_name: string

proc delete*(self: Contact) = 
  var qobject = self.QObject
  qobject.delete()

proc create*(self: Contact) =
  var qobject = self.QObject
  qobject.create()
  self.m_name = "InitialName"
  self.registerSlot("getName", [QMetaType.QString])
  self.registerSlot("setName", [QMetaType.Void, QMetaType.QString])
  self.registerSignal("nameChanged", [QMetaType.Void])
  self.registerProperty("name", QMetaType.QString, "getName", "setName", "nameChanged")
  
proc newContact*(): Contact = 
  new(result, delete)
  result.create()
  
method getName*(self: Contact): string =
  result = self.m_name

method setName*(self: Contact, name: string) =
  if self.m_name != name:
    self.m_name = name
    self.emit("nameChanged")

method onSlotCalled(self: Contact, slotName: string, args: openarray[QVariant]) = 
  case slotName:
    of "getName":
      args[0].stringVal = self.getName()
    of "setName":
      self.setName(args[1].stringVal)
    else:
      discard()

First we declare a QObject subclass and provide a simple new method where we:

1. invoke the create() procedure. This invoke the C++ bridge and allocate a QObject instance

  1. register a slot getName for reading the Contact name field
  2. register a slot setName for writing the Contact name
  3. register a signal nameChanged for notify the contact name changes
  4. register a property called name of type QString with the given read, write slots and notify signal

Looking at the getName and setName methods, you can see that slots, as defined in Nim, are nothing more than standard methods. The method corresponding to the setName slot demonstrates how to use the emit method to emit a signal.

The last thing to consider is the override of the onSlotCalled method. This method is called by the NimQml library when an invocation occurs from the Qml side for one of the slots belonging to the QObject. The usual implementation for the onSlotCalled method consists of a switch statement that forwards the arguments to the correct slot. If the invoked slot has a return value, this is always in the index position 0 of the args array.

Example 4: QtObject macro

The previous example shows how to create a simple QObject, however writing all those register procs and writing the onSlotCalled method becomes boring pretty soon.

Furthermore all this information can be automatically generated. For this purpose you can import the NimQmlMacros module that provides the QtObject macro.

Let's begin as usual with both the main.nim and main.qml files

Examples/QtObjectMacro/main.nim

import NimQml
import Contact

proc mainProc() =
  var app = newQApplication()
  defer: app.delete()
   
  var contact = newContact()
  defer: contact.delete() 

  var engine = newQQmlApplicationEngine()
  defer: engine.delete()

  var variant = newQVariant(contact)
  defer: variant.delete()

  engine.rootContext.setContextProperty("contact", variant)
  engine.load("main.qml")
  app.exec()

when isMainModule:
  mainProc()
  GC_fullcollect()

Examples/QtObjectMacro/main.qml

import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1

ApplicationWindow
{
	width: 400
	height: 300

	Component.onCompleted: visible = true

	ColumnLayout
	{
	    anchors.fill: parent

	    Label
	    {
	        text: "Current name is:" + contact.name
	    }

	    TextField
	    {
	        id: textField
	    }

	    Button
	    {
	        text: "Change Name"
	        onClicked: contact.name = textField.text 
	    }
	}
}

Nothing is new in both the main.nim and main.qml with respect to the previous example. What changed is the Contact object:

Examples/QtObjectMacro/Contact.nim

import NimQml

QtObject:
  type Contact* = ref object of QObject
    m_name: string

  proc delete*(self: Contact) =
    var qobject = self.QObject
    qobject.delete()

  proc newContact*(): Contact =
    new(result, delete)
    result.m_name = "InitialName"
    result.create

  method getName*(self: Contact): string {.slot.} =
    result = self.m_name

  method nameChanged*(self: Contact) {.signal.}

  method setName*(self: Contact, name: string) {.slot.} =
    if self.m_name != name:
      self.m_name = name
      self.nameChanged()

  QtProperty[string] name:
    read = getName
    write = setName
    notify = nameChanged

In details:

  1. Each QObject is defined inside the QtObject macro
  2. Each slot is annotated with the {.slot.} pragma
  3. Each signal is annotated with the {.signal.} pragma
  4. Each property is created with the QtProperty macro

The QtProperty macro has the following syntax

QtProperty[typeOfProperty] nameOfProperty

Example 5: ContactApp

The last example tries to show you all the stuff presented in the previous chapters and gives you an introduction to how to expose lists to qml.

Qt models are a huge topic and explaining how they work is out of scope. For further information please read the official Qt documentation.

The main file follows the basic logic of creating a qml engine and exposing a QObject derived object "ApplicationLogic" through a global "logic" property

Examples/ContactApp/main.nim

import NimQml, ApplicationLogic

proc mainProc() =
  let app = newQApplication()
  defer: app.delete
  let logic = newApplicationLogic(app)
  defer: logic.delete
  let engine = newQQmlApplicationEngine()
  defer: engine.delete
  let logicVariant = newQVariant(logic)
  defer: logicVariant.delete
  engine.rootContext.setContextProperty("logic", logicVariant)
  engine.load("main.qml")
  app.exec()

when isMainModule:
  mainProc()

The qml file shows a simple app with a central tableview

Examples/ContactApp/main.qml

import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1

ApplicationWindow {

	width: 500
	height: 300
    title: "ContactApp"
	visible: true

    menuBar: MenuBar {
        Menu {
            title: "&File"
            MenuItem { text: "&Load"; onTriggered: logic.onLoadTriggered() }
            MenuItem { text: "&Save"; onTriggered: logic.onSaveTriggered() }
            MenuItem { text: "&Exit"; onTriggered: logic.onExitTriggered() }
        }
    }

	ColumnLayout {
	    anchors.fill: parent

        Component {
            id: tableTextDelegate
            Label {
                id: tableTextDelegateInstance
                property var styleData: undefined
                states: State {
                    when: styleData !== undefined
                    PropertyChanges {
                        target: tableTextDelegateInstance;
                        text: styleData.value;
                        color: styleData.textColor
                    }
                }
            }
        }

        Component {
            id: tableButtonDelegate
            Button {
                id: tableButtonDelegateInstance
                property var styleData: undefined
                text: "Delete"
                onClicked: logic.contactList.del(styleData.row)
            }
        }

        Component {
            id: tableItemDelegate
            Loader {
                id: tableItemDelegateInstance
                sourceComponent: {
                    if (styleData.column === 0 || styleData.column === 1)
                        return tableTextDelegate
                    else if (styleData.column === 2)
                        return tableButtonDelegate
                    else
                        return tableTextDelegate
                }
                Binding {
                    target: tableItemDelegateInstance.item
                    property: "styleData"
                    value: styleData
                }
            }
        }

        TableView {
            model: logic.contactList
            Layout.fillWidth: true
            Layout.fillHeight: true
            TableViewColumn { role: "firstName"; title: "FirstName"; width: 200 }
            TableViewColumn { role: "surname"; title: "LastName"; width: 200}
            TableViewColumn { width: 100; }
            itemDelegate: tableItemDelegate
        }

        RowLayout {
            Label { text: "FirstName" }
            TextField { id: nameTextField; Layout.fillWidth: true; text: "" }
            Label { text: "LastName" }
            TextField { id: surnameTextField; Layout.fillWidth: true; text: "" }
            Button {
                text: "Add"
                onClicked: logic.contactList.add(nameTextField.text, surnameTextField.text)
                enabled: nameTextField.text !== "" && surnameTextField.text !== ""
            }
        }

	}
}

The important things to notice are:

  1. The menubar load, save and exit items handlers call the logic load, save and exit slots
  2. The TableView model is retrieved by the logic.contactList property
  3. The delete and add buttons call the del and add slots of the logic.contactList model

The ApplicationLogic object is as follows:

Examples/ContactApp/ApplicationLogic.nim

import NimQml, ContactList

QtObject:
  type ApplicationLogic* = ref object of QObject
    contactList: ContactList
    app: QApplication

  proc delete*(self: ApplicationLogic) =
    let qobject = self.QObject
    qobject.delete
    self.contactList.delete

  proc newApplicationLogic*(app: QApplication): ApplicationLogic =
    new(result)
    result.contactList = newContactList()
    result.app = app
    result.create()

  method getContactList(self: ApplicationLogic): QVariant {.slot.} =
    return newQVariant(self.contactList)

  method onLoadTriggered(self: ApplicationLogic) {.slot.} =
    echo "Load Triggered"
    self.contactList.add("John", "Doo")

  method onSaveTriggered(self: ApplicationLogic) {.slot.} =
    echo "Save Triggered"

  method onExitTriggered(self: ApplicationLogic) {.slot.} =
    self.app.quit

  QtProperty[QVariant] contactList:
    read = getContactList

The ApplicationLogic object,

  1. expose some slots for handling the qml menubar triggered signals
  2. expose a contactList property that return a QAbstractListModel derived object that manage the list of contacts

The ContactList object is as follows:

Examples/ContactApp/ContactList.nim

import NimQml, Contact, Tables

QtObject:
  type
    ContactList* = ref object of QAbstractListModel
      contacts*: seq[Contact]
    ContactRoles {.pure.} = enum
      FirstName = 0
      Surname = 1

  converter toCInt(value: ContactRoles): cint = return value.cint
  converter toCInt(value: int): cint = return value.cint
  converter toInt(value: ContactRoles): int = return value.int
  converter toInt(value: cint): int = return value.int
  converter toQVariant(value: string): QVariant = return value.newQVariant

  proc delete(self: ContactList) =
    let model = self.QAbstractListModel
    model.delete
    for contact in self.contacts:
      contact.delete
    self.contacts = @[]

  proc newContactList*(): ContactList =
    new(result, delete)
    result.contacts = @[]
    result.create

  method rowCount(self: ContactList, index: QModelIndex = nil): cint =
    return self.contacts.len

  method data(self: ContactList, index: QModelIndex, role: cint): QVariant =
    if not index.isValid:
      return
    if index.row < 0 or index.row >= self.contacts.len:
      return
    let contact = self.contacts[index.row]
    let contactRole = role.ContactRoles
    case contactRole:
      of ContactRoles.FirstName: return contact.firstName
      of ContactRoles.Surname: return contact.surname
      else: return

  method roleNames(self: ContactList): Table[cint, cstring] =
    result = initTable[cint, cstring]()
    result[ContactRoles.FirstName] = "firstName"
    result[ContactRoles.Surname] = "surname"

  method add*(self: ContactList, name: string, surname: string) {.slot.} =
    let contact = newContact()
    contact.firstName = name
    contact.surname = surname
    self.beginInsertRows(newQModelIndex(), self.contacts.len, self.contacts.len)
    self.contacts.add(contact)
    self.endInsertRows()

  method del*(self: ContactList, pos: int) {.slot.} =
    if pos < 0 or pos >= self.contacts.len:
      return
    self.beginRemoveRows(newQModelIndex(), pos, pos)
    self.contacts.del(pos)
    self.endRemoveRows

The ContactList object:

  1. overrides the rowCount method for returning the number of rows stored in the model
  2. overrides the data method for returning the value for the exported roles
  3. overrides the roleNames method for returning the names of the roles of the model. This name are then available in the qml item delegates
  4. defines two slots add and del that add or delete a Contact. During this operations the model execute the beginInsertRows and beginRemoveRows for notifing the view of an upcoming change. Once the add or delete operations are done the model execute the endInsertRows and endRemoveRows.