Playing with QML and DBus (Part 1)
How to integrate your DBus app and a QML UI
In this tutorials I am going to explain the necessary steps to integrate an existing codebase with DBus IPC and a QML (a.k.a. QtQuick) user interface. In this case, I am going to write a QML interface that will query a ModemManager instance running in the background. It will be possible to see the contacts in the SIM card and each contact will have a detail page where you will be able to see all the SMS sent to that contact.
Preparation
You need to download QT 4.7 -at the time of writing still an alpha version- and compile it by yourself. Bear in mind that this process might take several hours. Also I recommend you to start using the wonderful QT Creator 2.0 -an alpha version too at the time of writing- as IDE. It has a decent VIM integration and several goodies, like a visual QML editor. Do not even think about it start downloading them now! Note that the QT 4.7 API might have changed since I wrote this, and some parts might need minor fixes. Also make sure to download the most recent version of both QT and QT Creator instead of the alpha versions I had to use.
Start the project
First I created a new "Qt Gui" project. I've used the ~/devel/git/contacts path. This will create a bunch of files, some of them need some editing. But first... design is not one of my strong points, so I started by reusing one of the examples included in the documentation, the recipes example:
cp -R ~/software/qt-everywhere-opensource-src-4.7.0-tp/examples/declarative/listview/* ~/devel/git/contacts
Now remove the following files, as we won't need them:
cd ~/devel/git/contacts rm dynamic.qml highlight.qml itemlist.qml listview.qml sections.qml rm -rf dummydata content/ClickAutoRepeating.qml
Move MediaButton to the top level:
mv content/MediaButton.qml .
Edit the lines starting with "source:" and add content/ to the path to reflect that we just moved it to the top level.
Download the default image for our contacts:
wget http://minimoesfuerzo.org/media/photos/person.png -O content/pics/person.png
Rename recipes.qml to contacts.qml:
mv recipes.qml contacts.qml
Understand what is going on under the hood
Before we go on, I think it will be good to provide a minimal overview of how recipes.qml works, and what our goal is. Note that this tutorial assumes certain familiarity with QML/QT and related concepts, if you never heard of this technologies, it is a good moment to check out the docs.
recipes.qml imports "content" at the beginning of the script, and that folder contains a bunch of images and a MediaButton.qml script. A QML script can import other QML scripts and refer to other files. In addition, "dummydata" is also imported and that is where the hardcoded data for recipes resides, check out dummydata/Recipes.qml and look at its structure and attributes.
So when you run recipes.qml, it imports in turn a bunch of other scripts and one of them happens to be a ListModel that contains the initial data that you get to see. How is that data referenced in recipes.qml then? Very easy, in recipes.qml line 137 you can see that a ListView is defined and has a model named Recipes. A ListView will always have a delegate for painting and showing the model's data. The delegate for this ListView is recipeDelegate.
recipeDelegate is a Component declared in line 13, and inside the block if you want to show/paint the model's data, you just use the name of the attribute in the model. For example, if with the current model I want to show the title of the recipe, I need to use 'text: title'. Similarly, if I want to show the details of a recipe, I would use 'text: details'.
So our goal is to get rid of that hardcoded data model, and use a custom one populated by ModemManager via DBus.
Initial steps
Instead save the following lines in ~/devel/git/contacts/contacts.pro:TEMPLATE = app TARGET = contacts DEPENDPATH += . INCLUDEPATH += . CONFIG += qdbus QT += declarative # Input SOURCES += main.cpp contact.cpp HEADERS += contact.h RESOURCES += contacts.qrc sources.files = $$SOURCES $$HEADERS $$RESOURCES contacts.pro contacts.qml sources.path = . target.path = . INSTALLS += sources target
This is a minimal QT project template with all the parts that we, initially, need for the task. The target will be a binary named "contacts", for now the project will use main.cpp and contact.cpp as sources, contact.h as headers and will use the QtDBus and QDeclarative libraries for obvious reasons.
Initial main.cpp implementation:
#include <QApplication>
#include <qdeclarativeengine.h>
#include <qdeclarativecontext.h>
#include <qdeclarative.h>
#include <qdeclarativeitem.h>
#include <qdeclarativeview.h>
#include "contact.h"
int main(int argc, char ** argv)
{
QApplication app(argc, argv);
const QString picture = QString("qrc:/content/pics/person.png");
const QString messages = QString("<html><ul><li>No SMS!</li></ul></html>");
QList<QObject*> dataList;
dataList.append(new Contact("John", "+34243234232", messages, picture));
QDeclarativeView view;
QDeclarativeContext *ctxt = view.rootContext();
ctxt->setContextProperty("myModel", QVariant::fromValue(dataList));
view.setSource(QUrl("qrc:contacts.qml"));
view.show();
return app.exec();
}Initial contact.h:
#ifndef CONTACT_H
#define CONTACT_H
#include <QObject>
class Contact : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName)
Q_PROPERTY(QString number READ number WRITE setNumber)
Q_PROPERTY(QString picture READ picture WRITE setPicture)
Q_PROPERTY(QString messages READ messages)
public:
Contact(QObject *parent=0);
Contact(const QString &name, const QString &number,
const QString &messages, const QString &picture,
QObject *parent=0);
QString name() const;
void setName(const QString &name);
QString number() const;
void setNumber(const QString &number);
QString picture() const;
void setPicture(const QString &picture);
QString messages() const;
private:
QString m_name;
QString m_number;
QString m_messages;
QString m_picture;
};
#endif // CONTACT_HInitial contact.cpp implementation:
#include "contact.h"
Contact::Contact(QObject *parent)
: QObject(parent)
{
}
Contact::Contact(const QString &name, const QString &number,
const QString &messages, const QString &picture,
QObject *parent)
: QObject(parent), m_name(name), m_number(number),
m_messages(messages), m_picture(picture)
{
}
QString Contact::name() const
{
return m_name;
}
void Contact::setName(const QString &name)
{
m_name = name;
}
QString Contact::number() const
{
return m_number;
}
void Contact::setNumber(const QString &number)
{
m_number = number;
}
QString Contact::picture() const
{
return m_picture;
}
void Contact::setPicture(const QString &picture)
{
m_picture = picture;
}
QString Contact::messages() const
{
return m_messages;
}What did we just do? We have declared a QObject class, Person, with four properties (name, number, messages, picture) that will be accessed from QML space transparently.
Modify contacts.qml
At this point, contacts.qml still has the contents of recipes.qml, and needs to be changed. We are going to change the references to old attributes, to the new ones that we will use.
- In line 57: Change 'text: title' to 'text: model.name'
- In line 59: Change 'text: "Ingredients"' to 'text: "Details"'
- In line 63: Change 'text: ingredients' to 'text: model.name'
- In line 78: Change 'text: "Method"' to 'text: "List of Messages"'
- In line 85: Change 'text: method' to 'text: model.messages'
- In line 137: Change 'model: Recipes' to 'model: myModel'
To sum up, we have changed the old attributes in the model (title, picture, ingredients and method) to (name, number, messages and picture). And the model name (from Recipes to myModel as that's the name we've used in main.cpp). A saavy reader will have noticed by now that there's no mention of the number attribute in the current contacts.qml file and that's true, to include it we just need this last modification to contacts.qml, line 44 defines the topLayout Row and should look like this after the changes:
Row {
id: topLayout
x: 10; y: 10; height: recipePic.height; width: parent.width
spacing: 10
Image {
id: recipePic
source: model.picture; width: 48; height: 48
}
Column {
height: recipePic.height; width: background.width-recipePic.width-20
spacing: 5
Column {
Text { id: title; text: model.name; font.bold: true; font.pointSize: 15 }
Text { id: phone; text: model.number; font.bold: true; font.pointSize: 9 }
}
Text {
text: "Details"; font.pointSize: 12; font.bold: true
opacity: wrapper.detailsOpacity
}
Text {
text: model.name; wrap: true; width: parent.width
opacity: wrapper.detailsOpacity
}
}
}Now is a good time to remove all the outstanding references to the old attributes in the code, I've done the following substitutions in contacts.qml:
sed -i 's/recipeDelegate/contactDelegate/g' contacts.qml sed -i 's/recipePic/contactPic/g' contacts.qml sed -i 's/methodTitle/smsListTitle/g' contacts.qml
The last thing we need to do is provide an initial contacts.qrc:
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file>contacts.qml</file>
<file>MediaButton.qml</file>
<file>content/pics/button.png</file>
<file>content/pics/button-pressed.png</file>
<file>content/pics/person.png</file>
<file>content/pics/moreDown.png</file>
<file>content/pics/moreUp.png</file>
</qresource>
</RCC>This file references every QML component/asset that is going to be used in the project. If they are not referenced here, you will get an error message for each time you try to access from QML an asset not included in contacts.qrc.
At this point, we are ready to compile our project and run our project:
qmake contacts.pro make
Make sure that the qmake you have used is the 4.7 one, otherwise some of the required libraries, such as QtDeclarative* will not be found. Now launch the contacts binary and rejoy!
Make sure to read part 2 of this post where I will show how to retrieve the contacts and messages from the SIM via ModemManager . This is the current source for the project contacts-part1.tar.gz.
Updated: I have added a small video showing the current state of the project, so you can have a quick look of what we are building. Thanks Andrew!
blog comments powered by Disqus