Authors: | Filippo Cucchetto <filippocucchetto@gmail.com> Will Szumski <will@cowboycoders.org> |
---|---|
Version: | 0.7.7 |
Date: | 2019/10/01 |
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.
You will need:
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
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 make install
The installation is not mandatory, in fact you could try the built-in examples in the following way
cd path/to/repo/nimqml cd examples/helloworld export LD_LIBRARY_PATH=path/to/libDOtherSide.so nim c -r main
Alternatively you can use the nimble package manager
nimble install NimQml
or
cd to/build/dir/Nim/NimQml nimble install
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 example shows the mandatory steps of each NimQml app
The previous example shown how to startup the Qt event loop to create an application with a window.
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.setRootContextProperty("qVar1", qVar1) engine.setRootContextProperty("qVar2", qVar2) engine.setRootContextProperty("qVar3", qVar3) engine.setRootContextProperty("qVar4", qVar4) engine.load("main.qml") app.exec() when isMainModule: mainProc() GC_fullcollect()
examples/simpledata/main.qml
import QtQuick 2.8 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 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 } } }
The example shows how to expose simple values to Qml:
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:
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 :
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.setRootContextProperty("contact", variant) engine.load("main.qml") app.exec() when isMainModule: mainProc() GC_fullcollect()
We can see:
The Qml file is as follows:
examples/slotsandproperties/main.qml
import QtQuick 2.8 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 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
import NimQml QtObject: type Contact* = ref object of QObject m_name: string proc delete*(self: Contact) = self.QObject.delete proc setup(self: Contact) = self.QObject.setup proc newContact*(): Contact = new(result, delete) result.m_name = "InitialName" result.setup proc getName*(self: Contact): string {.slot.} = result = self.m_name proc nameChanged*(self: Contact, name: string) {.signal.} proc setName*(self: Contact, name: string) {.slot.} = if self.m_name == name: return self.m_name = name self.nameChanged(name) QtProperty[string] name: read = getName write = setName notify = nameChanged
A Contact is a subtype derived from QObject
Defining a QObject is done using the nim QtObject macro
QtObject: type Contact* = ref object of QObject m_name: string
Inside the QtObject just define your subclass as your would normally do in Nim.
Since Nim doesn't support automatic invocation of base class constructors and destructors you need to call manually the base class setup and delete functions.
proc delete*(self: Contact) = self.QObject.delete proc setup(self: Contact) = self.QObject.setup
Don't forget to call the setup function and delete in your exported constructor procedure
proc newContact*(): Contact = new(result, delete) result.m_name = "InitialName" result.setup
The creation of a property is done in the following way:
QtProperty[string] name: read = getName write = setName notify = nameChanged
A QtProperty is defined by a:
Looking at the getName`, `setName, nameChanged procs, show that slots and signals are nothing more than standard procedures annotated with {.slot.} and {.signal.}
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 in detail 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 import 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.setRootContextProperty("logic", logicVariant) engine.load("main.qml") app.exec() when isMainModule: mainProc() GC_fullcollect()
The qml file shows a simple app with a central tableview
examples/contactapp/main.qml
import QtQuick 2.3 import QtQuick.Controls 1.3 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 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:
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) = self.QObject.delete self.contactList.delete proc setup(self: ApplicationLogic) = self.QObject.setup proc newApplicationLogic*(app: QApplication): ApplicationLogic = new(result) result.contactList = newContactList() result.app = app result.setup() proc getContactList(self: ApplicationLogic): QVariant {.slot.} = return newQVariant(self.contactList) proc onLoadTriggered(self: ApplicationLogic) {.slot.} = echo "Load Triggered" self.contactList.add("John", "Doo") proc onSaveTriggered(self: ApplicationLogic) {.slot.} = echo "Save Triggered" proc onExitTriggered(self: ApplicationLogic) {.slot.} = self.app.quit QtProperty[QVariant] contactList: read = getContactList
The ApplicationLogic object,
The ContactList object is as follows:
examples/contactapp/contactlist.nim
import NimQml, contact, Tables type ContactRoles {.pure.} = enum FirstName = UserRole + 1 Surname = UserRole + 2 QtObject: type ContactList* = ref object of QAbstractListModel contacts*: seq[Contact] proc delete(self: ContactList) = self.QAbstractListModel.delete for contact in self.contacts: contact.delete self.contacts = @[] proc setup(self: ContactList) = self.QAbstractListModel.setup proc newContactList*(): ContactList = new(result, delete) result.contacts = @[] result.setup method rowCount(self: ContactList, index: QModelIndex = nil): int = return self.contacts.len method data(self: ContactList, index: QModelIndex, role: int): 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: result = newQVariant(contact.firstName) of ContactRoles.Surname: result = newQVariant(contact.surname) method roleNames(self: ContactList): Table[int, string] = { ContactRoles.FirstName.int:"firstName", ContactRoles.Surname.int:"surname"}.toTable proc 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() proc 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: