Thursday, March 29, 2012

Connecting QML to C++, More Practical Example

As a first practical application of connecting C++ to QML I had to integrate a File Open/Close dialog to be accessed from QML, because QML does not have this kind of control natively. So here are the requirements to the simple task:

  • Open button starts the Open File dialog, which allows the user to select a text file
  • When the user selects the file, the contents of this file are loaded into an editable field
  • The user can edit the file
  • Save button opens the Save File dialog, which allows the user to save the contents of the editable field into a new or existing text file

The C++ header file will define only two functions, getFileContents will be called when the Open button is clicked, and saveFileContents will be called when the Save button is clicked. Here is the full header file:

//qmlfile.h
#ifndef QMLFILE_H
#define QMLFILE_H

#include

class QMLFile : public QObject
{
Q_OBJECT

public:
explicit QMLFile(QObject *parent = 0);

Q_INVOKABLE QString getFileContents() const;

Q_INVOKABLE void saveFileContents(QString fileContents) const;
};

#endif // QFILE_H

The C++ code file will implement these functions. getFileContents opens the Open File dialog and waits for the user to select a file. When the file is selected, the function tries to open it and, if successful, returns the string that has file contents. saveFileContents is the opposite: it takes a string as a parameter, opens a Save File dialog, waits for the user to select a file or enter a name of the file, and tries to save the string as file contents. Quick and dirty - a lot of things could go wrong and cause exceptions, but that's not the point at this stage.

//qmlfile.cpp
#include
#include
#include
#include "qmlfile.h"

QMLFile::QMLFile(QObject *parent): QObject(parent)
{

}

QString QMLFile::getFileContents() const
{
QString fileName = QFileDialog::getOpenFileName(NULL, tr("Open File"), "/home", tr("Text Files (*.txt)"));
qDebug() << "fileName:" << fileName;
QFile file(fileName);
if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
return "";

QString content = file.readAll();
file.close();
return content;
}

void QMLFile::saveFileContents(QString fileContents) const
{
QString fileName = QFileDialog::getSaveFileName(NULL, tr("Save File"), "/home", tr("Text Files (*.txt)"));

QFile file(fileName);
if(file.open(QIODevice::WriteOnly | QIODevice::Text))
{
qDebug() << "created file:" << fileName;
QTextStream stream(&file);
stream << fileContents << endl;
file.close();
return;
}
else
{
qDebug() << "could not create file:" << fileName;
return;
}
}

Now the only two things that are left is to update the main files: main.cpp and main.qml. In main.cpp I add the two lines similar to the example in the previous post.

//in main.cpp after viewer
QMLFile qmlFile;
viewer.rootContext()->setContextProperty("QMLFile", &qmlFile);

In the main.qml I will implement the onClicked events for Open and Save buttons. Each will only take a single line.

//open file and load contents into TextEdit
txt.text = QMLFile.getFileContents();
//get contents of the TextEdit and save to a file
QMLFile.saveFileContents(txt.text);

Now I can build and run the application and open some file, and if everything goes smoothly you'll see the results similar to the screenshot below - contents of a text file loaded into the QML TextEdit component and displayed.

Text File Loaded into QML TextEdit

For reference, the full main.qml file.

//main.qml
import QtQuick 1.0

Rectangle {
width: 360; height: 360

Rectangle{
id:buttons
height: 50; width: parent.width; anchors.top: parent.top

Rectangle{
id: btnOpen
width: 50; height: parent.height; anchors.left: parent.left

Image{
anchors.fill: parent; source: "images/open.png"
}

MouseArea{
anchors.fill: parent
onClicked: {
txt.text = QMLFile.getFileContents();
}
}
}

Rectangle{
id: btnSave
width:50; height: parent.height; anchors.left: btnOpen.right

Image{
anchors.fill: parent; source: "images/save.png"
}

MouseArea{
anchors.fill: parent
onClicked: {
QMLFile.saveFileContents(txt.text);
}
}
}
}

Rectangle{
id:textHandle
width: parent.width; height: parent.height - buttons.height; anchors.bottom: parent.bottom

TextEdit{
id: txt; anchors.fill: parent
}
}
}

References

QFileDialog Class Reference
Opening a file from a Qt String by . Also posted on my website

Wednesday, March 28, 2012

Connecting QML to C++, First Attempt

It took me some time to figure it out all the way, so I'm recording steps to create a minimal application that will have QML UI talking to the C++ code and receiving some data. I was using Qt Creator 2.2.1, and some syntax appear to have changed in further versions, which gave me some additional headaches while trying to figure out the examples. There are some "gotchas" on the way, but eventually it will look quite simple.

In Qt Creator, select File -> New File or Project and choose Qt Quick Project -> Qt Quick Application. Give it a name, i.e. SimpleCPP. Accept the defaults. Let's first add the C++ code. Right-click the project and select Add New -> C++ -> C++ Class. Give it a name too, i.e. "test". This will add two files to the project: test.h under Headers and test.cpp under Sources. Here is the full header file:

#ifndef TEST_H
#define TEST_H

#include <QObject>

class test : public QObject
{
Q_OBJECT

public:
explicit test(QObject *parent = 0);

Q_INVOKABLE QString getString() const;
};

#endif // TEST_H

The class test inherits from QObject and exposes getString function, which is marked as Q_INVOKABLE. This registers the function with the meta-object system and, whatever that means, will allow the function to be called from QML.

Here is the test.cpp listing, and not much to say about it - it's only purpose is to return a string so we could actually verify that the C++ code gets executed.

#include "test.h"

test::test(QObject *parent): QObject(parent)
{

}

QString test::getString() const
{
QString str = "string from cpp code";
return str;
}

Next step is to register the C++ class with the main.cpp which was automatically created by the project. Add a couple of includes on the top

#include <QDeclarativeContext>
#include <QtGui/QGraphicsObject>
#include "test.h"

And these two lines after the definition of the QApplicationViewer:

test dummy;
viewer.rootContext()->setContextProperty("Dummy", &dummy);

The instance of the test class will now be known to the QML file as "Dummy". See later.

Now to modify the main.qml so it will be able to receive something from C++. In the main.qml which was automatically generated, I give the Text element the objectName property of "textObject". This is how it will be referenced by C++.

Text {
objectName: "textObject"
text: "Hello World"
anchors.centerIn: parent
}

So, here's one way to modify the text on the screen: add the following two lines to main.cpp

QObject* testText = viewer.rootObject()->findChild<QObject*>("textObject");
if(testText) testText->setProperty("text", dummy.getString());

This code finds a child object in the QML file which has an objectName of "textObject" and, if found, sets its text property to the string returned by the test C++ class. Build and run the application and verify that the "string from cpp code" is shown in the middle of the screen.

Another way to access the C++ code is to call the function right from the QML file. I can modify the main.qml this way:

Rectangle {
width: 360
height: 360
Text {
id: textQML
objectName: "textObject"
text: "Hello World"
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
onClicked: {
textQML.text = Dummy.getString();
//Qt.quit();
}
}
}

Now, when the Rectangle is clicked, the QML file calls Dummy, which is how the test class is known to the QML file. See above. Remove or comment out the last two added lines from the main.cpp and run the application again. At first nothing happens, but when you click anywhere in the window, the text changes to "string from cpp code".

The full main.cpp now looks like that:

#include <QtGui/QApplication>
#include "qmlapplicationviewer.h"
#include <QDeclarativeContext>
#include <QtGui/QGraphicsObject>
#include "test.h"

int main(int argc, char *argv[])
{
QApplication app(argc, argv);

QmlApplicationViewer viewer;

test dummy;
viewer.rootContext()->setContextProperty("Dummy", &dummy);

viewer.setOrientation(QmlApplicationViewer::ScreenOrientationAuto);
viewer.setMainQmlFile(QLatin1String("qml/SimpleCPP/main.qml"));
viewer.showExpanded();

/* comment these two lines if you don't want to display the string on start up */
QObject* testText = viewer.rootObject()->findChild<QObject*>("textObject");
if(testText) testText->setProperty("text", dummy.getString());

return app.exec();
}

References

QObject Class Reference
qt, access c++ function from qml
Communication between C++ and QML by . Also posted on my website

Monday, March 26, 2012

Improved QML Simple Splitter.

The splitter from the last post works. What happens, however, if I want to add content to it, such as text boxes and other things? Well, currently I'll have to mess with the code of the splitter component itself. Obviously, I will not be able to reuse it after that, I will have to copy and paste chunks of it next time I need another splitter. This is not something I can call a good solution. So the next iteration is to somehow make it possible to reuse the splitter without making changes to its QML file, and make it possible to add content "from outside" of the component. Here comes splitter 2.0. The code itself changed very little, but the two rectangles, which are left and right panels (or top and bottom in the case of a horizontal splitter) now are not part of the component itself. To add content to panels, two properties are defined in the VerticalSplitter component.

property QtObject firstRect;
property QtObject secondRect;

These components will be assigned in the "main.qml" as follows:

//main.qml
import QtQuick 1.0

Rectangle {
width: 600
height: 600

Rectangle{
id: leftRect
color: "blue"
}

VerticalSplitter{
id: someSplitter
firstRect : leftRect
secondRect: rightRect
}

Rectangle{
id:rightRect
color: "red"
}
}

The splitter component is now simply inserted between the two panels. The panels and splitter are anchored to each other in the VerticalSplitter.qml which is almost the same as the one from the last post - probably slightly more simple. Great. I think my chunky, lousy QML is improving little by little.

//Reusable VerticalSplitter.qml
import QtQuick 1.0

Rectangle{
id: splitterRect

anchors.left: firstRect.right
anchors.right: firstRect.right
anchors.rightMargin: -10

width: 10
height: parent.height
clip: true

property QtObject firstRect;
property QtObject secondRect;

property int maximizedRect : -1;

function moveSplitterTo(x)
{
if(x > 0 && x < parent.width - splitterRect.width)
{
firstRect.width = x;
secondRect.width = parent.width - firstRect.width - splitterRect.width;
}
}

function maximizeRect(x)
{
firstRect.width = x===0 ? parent.width - splitterRect.width : 0
secondRect.width = x===0 ? 0 : parent.width - splitterRect.width
}

Component.onCompleted: {
firstRect.height = height;
firstRect.width = (firstRect.parent.width - width)/2;
firstRect.anchors.left = firstRect.parent.left;

secondRect.anchors.left = splitterRect.right;
secondRect.anchors.right = secondRect.parent.right;
secondRect.height = height;
}

onXChanged: {
moveSplitterTo(splitterRect.x);
}

BorderImage {
id: splitterBorder
anchors.fill: parent
source: "images/splitterBorder_vertical.png"
}

Image{
id: arrows
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
source: "images/splitterArrows_vertical.png"
}

MouseArea {
anchors.fill: parent
drag.axis: Drag.XAxis
drag.target: splitterRect

onClicked: {
maximizedRect = maximizedRect == 1 ? 0 : 1;
maximizeRect(maximizedRect);
}
}
}
by . Also posted on my website

Sunday, March 25, 2012

The Most Basic Splitter Component Possible

The most basic splitter involves the top part of the area, the bottom part and the splitter in between. The splitter is a narrow rectangle with a MouseArea. drag.target:splitterRect makes the MouseArea to listen to drag events from splitter. drag.axis specifies along which axis the splitter can be dragged. When the splitter is dragged with the mouse along the Y axis, its Y position changes, triggering the onYChanged event, and the moveSplitterTo just recalculates the widths of top and bottom rectangles according to the current Y position of the splitter.

Additionally, if the splitter is not dragged, but clicked, the splitter collapses one of the frames. On start up, the first frame to be collapsed is chosen, and then, on each click, the other frame is collapsed, and the one that was collapsed is expanded.


Basic Splitter

One of the Frames is Collapsed

//Splitter component
import QtQuick 1.0

Item{
id: root
anchors.fill: parent
width: parent.width
height: parent.height
clip: true

property int splitterHeight: 10
property int maximizedRect : -1;

function moveSplitterTo(y)
{
if(y > 0 && y < parent.height - splitterHeight)
{
topRect.height = y;
bottomRect.height = parent.height - topRect.height - splitterHeight;
}
}

function maximizeRect(x)
{
topRect.height = x===0 ? parent.height - splitterHeight : 0
bottomRect.height = x===0 ? 0 : parent.height - splitterHeight
}

Rectangle{
id: topRect
width: parent.width
height: (parent.height-splitterHeight)/2
anchors.top: parent.top
color: "blue"
}

Rectangle {
id: splitterRect
width: parent.width
height: splitterHeight
color: "black"

anchors.top: topRect.bottom
anchors.bottom: bottomRect.top

property int tempY : splitterRect.y

onYChanged: {
moveSplitterTo(splitterRect.y);
}

BorderImage {
id: splitterBorder
anchors.fill: parent
source: "images/splitterBorder.png"
}

Image{
id: arrows
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
source: "images/splitterArrows.png"
}

MouseArea {
anchors.fill: parent
drag.axis: Drag.YAxis
drag.target: splitterRect

onClicked: {
maximizedRect = maximizedRect == 1 ? 0 : 1;
maximizeRect(maximizedRect);
}
}
}

Rectangle{
id:bottomRect
width: parent.width
height: (parent.height-splitterHeight)/2
anchors.bottom: parent.bottom
color: "red"
}
}

References

QML MouseArea Element
SplitterRow.qml by . Also posted on my website

Tuesday, March 20, 2012

QML Check Box

The check box can be in seven different states: enabled - checked and unchecked, disabled - checked and unchecked, pressed - in checked and unchecked states, and finally "mouseover" state when the cursor is over the checkbox, but not pressed.

The mouseover state is triggered by the cursor entering and exiting the check box area. The disabled and checked stated can also be set from "outside" by setting the isDisabled and isChecked variables. The onPressed code remembers the state the check box was in and changes the icon to pressed version, and onReleased reverts the icon change. The onClicked toggles the state from on to off and back.

Checked state

Checked and pressed state

Disabled state

import QtQuick 1.0

Rectangle {

id: checkBoxRect
width: 100
height: 30

property string chkUnchecked: "images/checkbox_enabled.png"
property string chkChecked : "images/checkbox_checked.png"

property string chkUncheckedPressed : "images/checkbox_pressed.png"
property string chkCheckedPressed : "images/checkbox_checked_pressed.png"

property string chkDisabled: "images/checkbox_disabled.png"
property string chkDisabledChecked: "images/checkbox-checked_disabled.png"
property string chkMouseOver: "images/checkbox_enabled_mouseover.png"

property bool pressed: false
property string src: chkUnchecked
property bool isDisabled : false
property bool isChecked: false

property string tempState : ""
property string tempStateHover : ""
property string status : ""

Image {
id: checkBoxImg
width: 30
height: parent.height
source: src
fillMode: Image.PreserveAspectFit;

MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
if(!isDisabled)
{
if(checkBoxRect.state == "mouseover")
checkBoxRect.state = tempStateHover == "on" ? "off" : "on";
else
checkBoxRect.state = checkBoxRect.state == "on" ? "off" : "on";
tempStateHover = checkBoxRect.state;
}
}
onPressed: {
if(!isDisabled)
{
tempState = checkBoxRect.state;
checkBoxRect.state = "pressed";
}
}
onReleased: {
if(!isDisabled)
checkBoxRect.state = tempState;
}
onEntered: {
if(!isDisabled)
{
tempStateHover = checkBoxRect.state;
checkBoxRect.state = "mouseover";
}
}
onExited: {
if(!isDisabled)
checkBoxRect.state = tempStateHover;
}
}
}

Component.onCompleted: {
if(isDisabled)
src = isChecked ? chkDisabled : chkCheckedDisabled
else
checkBoxRect.state = "off";
}

Text{
id: checkboxText
height: parent.height
width: parent.width - checkBoxImg.width
text: "click here"
anchors.left: checkBoxImg.right
verticalAlignment: Text.AlignVCenter
}

states: [
State {
name: "on"
PropertyChanges { target: checkBoxImg; source: chkChecked }
PropertyChanges { target: checkBoxRect; pressed: true }
},
State {
name: "off"
PropertyChanges { target: checkBoxImg; source: chkUnchecked }
PropertyChanges { target: checkBoxRect; pressed: false }
},
State {
name: "pressed"
PropertyChanges {target: checkBoxImg; source: (tempState == "on" || tempStateHover == "on") ? chkCheckedPressed : chkUncheckedPressed}
},
State {
name: "mouseover"
PropertyChanges {target: checkBoxImg; source: chkMouseOver}
}
]
}

Reference:

QT QML anchors probleme by . Also posted on my website

Monday, March 19, 2012

QML Drop Down Menu

A drop down menu example. There are two states of the drop down list - dropDown and, well, not dropDown. When the top rectangle (chosenItem) is clicked, the state is switched from dropDown to none and back, and the height of the dropdownList is adjusted accordingly, showing or hiding the list with the selection options. When the list itself is clicked, there is an additional action - the text of the chosenItemText area is updated to reflect the selection.

List is expanded

A selection was made, text is updated and list is hidden again

import QtQuick 1.0

Rectangle {
width:400;
height: 400;

property int dropDownHeight:40

Rectangle {
id:dropdown
property variant items: ["Test Item 1", "Test Item 2", "Test Item 3"]
width: 100;
height: dropDownHeight;
z: 100;

Rectangle {
id:chosenItem
width:parent.width;
height:dropdown.height;
color: "lightsteelblue"
Text {
anchors.top: parent.top;
anchors.left: parent.left;
anchors.margins: dropDownHeight/5;
id:chosenItemText
text:dropdown.items[0];
}

MouseArea {
anchors.fill: parent;
onClicked: {
dropdown.state = dropdown.state==="dropDown"?"":"dropDown"
}
}
}

Rectangle {
id:dropdownList
width:dropdown.width;
height:0;
clip:true;
anchors.top: chosenItem.bottom;
anchors.margins: 2;
color: "lightgray"

ListView {
id:listView
height:dropDownHeight*dropdown.items.length
model: dropdown.items
currentIndex: 0
delegate: Item{

width:dropdown.width;
height: dropdown.height;

Text {
text: modelData
anchors.top: parent.top;
anchors.left: parent.left;
anchors.margins: 5;
}

MouseArea {
anchors.fill: parent;
onClicked: {
dropdown.state = "";
chosenItemText.text = modelData;
listView.currentIndex = index;
}
}
}
}
}

states: State {
name: "dropDown";
PropertyChanges { target: dropdownList; height:dropDownHeight*dropdown.items.length }
}
}
}

References:

QML drop Down Menu or Menu bar
Qt QML dropdown list like in HTML
by . Also posted on my website

Sunday, March 18, 2012

QML Improved Tree View

I added some styling to the tree view exercise, so it looks better overall. A little change to the ListView layout - there is now the separation line between the items, which is just a rectangle with the height of 2. Additionally, I do a couple other things when the selected item changes, by hooking to the onFocusChanged event and checking in the delegate if the item is selected: I toggle the transparency first, so that the highlight color is visible, but also when the selection is lost, the item is returned to its background color. Also, I invert the color of the item text (to white when the item is selected).

// ListView item layout
Rectangle{
id: listItemRect
anchors.fill: parent
color: "#E2E3E4"

Rectangle{ //dividing line
height: 2
width: parent.width

anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right

color: "#AFADB3"
}

Rectangle{

height: parent.height - 2
width: parent.width
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
color: "transparent"
...
}
}
//set text color to white if the item is selected.    
onFocusChanged: {
if(ListView.view.currentIndex == index)
{
listItemRect.color = "transparent";
textDelegate.color = "white";
}
else
{
listItemRect.color = "#E2E3E4";
textDelegate.color = "black";
}
}

Another addition is the navigation bar on the top, so the user knows where in the document structure he is currently positioned. The arrows effect is achieved by simply adding some "help" images that are toggled and their visibility is changed as required by calling the setVisibility function from outside. Navigation bar itself responds to clicks on the items, bringing the user back to the position in the document structure according to the element being clicked.

Here's how the improved tree view looks like:

Just loaded

Last level in the tree

Navigation bar modifications in main.qml and navigation bar code:

//added a navigation bar in NavigationBar.qml
Rectangle {
id:main

//change visibility of the navigation bar elements
function setVisibility()
{
navigationRect.hideRect(2, false)

if(level == 0)
{
navigationRect.hideRect(1, false)
backButton.visible = false;
}
else
{
navigationRect.hideRect(1, true)

if(level == 1)
{
navigationRect.setText(0, level0Name);
}
if(level == 2)
{
navigationRect.hideRect(2, true)
}
}
}
...

NavigationBar{
id:navigationRect
}
}
//Navigation bar code

import QtQuick 1.0

Rectangle{
id: navigationRect
height: 50
width: parent.width
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: marginValue
anchors.leftMargin: marginValue
anchors.rightMargin: marginValue

function setText(rect, text)
{
var texts = [level0Text, level1Text, level2Text];
texts[rect].text = text;
}

function hideRect(rect, hide)
{
var rects = [level0Rect, level1Rect, level2Rect];
var helps = [level0Help, level1Help, level2Help];
rects[rect].visible = hide;
helps[rect].visible = hide;

if(rect > 0)
{
hide? helps[rect-1].children[0].source = "icons/selected_black.png" : helps[rect-1].children[0].source = "icons/selected_black_half.png"
}
if(rect == 2)
{
hide? helps[rect].children[0].source = "icons/selected_black_half.png" : helps[rect].children[0].source = ""
}
}

Component.onCompleted: {
level1Rect.visible = false;
level2Rect.visible = false;
level2Rect.visible = false;
level2Help.visible = false;
}

Rectangle{
id: level0Rect
width: parent.width*4/15
height: parent.height
anchors.left: parent.left
anchors.top: parent.top
color: "black"

Text{
id: level0Text
anchors.centerIn: parent
color: "white"
text: level0Label
font.pixelSize: 10
}

MouseArea{
id: level0Mouse
anchors.fill: parent

onClicked: {
level = 0;
setQueries();
}
}
}

Rectangle{
id: level0Help
width: parent.width/15
height: parent.height
anchors.left: level0Rect.right
anchors.top: parent.top

Image{
anchors.fill: parent
}
}

Rectangle{
id: level1Rect
width: parent.width*4/15
height: parent.height
anchors.left: level0Help.right
anchors.top: parent.top
color: "black"

Text{
id: level1Text
color: "white"
anchors.centerIn: parent
text: level1Label
font.pixelSize: 10
}

MouseArea{
id: level1Mouse
anchors.fill: parent

onClicked: {
level = 1;
setQueries();
}
}
}

Rectangle{
id: level1Help
width: parent.width/15
height: parent.height
anchors.left: level1Rect.right
anchors.top: parent.top

Image{
anchors.fill: parent
}
}

Rectangle{
id: level2Rect
width: parent.width*4/15
height: parent.height
anchors.left: level1Help.right
anchors.top: parent.top
color: "black"

Text{
id: level2Text
color: "white"
anchors.centerIn: parent
text: level2Label
font.pixelSize: 10
}

MouseArea{
id: level2Mouse
anchors.fill: parent
}
}

Rectangle{
id: level2Help
width: parent.width/15
height: parent.height
anchors.left: level2Rect.right
anchors.top: parent.top

Image{
anchors.fill: parent
}
}
}
by . Also posted on my website

Wednesday, March 14, 2012

QML TreeView Exersice

Here's a bit of exersice I had to go through to implement a treeview-like structure in QML. The control has to process the XML file, read data and display it in the form that will allow the user to navigate the tree structure. As an example, the XML file is shown below.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<level1item name="level1item 1">
<level2item name="module 1.1">
<level3item name="1.1.1" attr1="0" attr2="1">
</level3item>
<level3item name="1.1.2" attr1="0" attr2="1">
</level3item>
</level2item>
<level2item name="module 1.2">
</level2item>
<level2item name="module 1.3">
</level2item>
<level2item name="module 1.4">
</level2item>
<level2item name="module 1.5">
</level2item>
</level1item>
<level1item name="level1item 2">
<level2item name="module 2.1">
</level2item>
<level2item name="module 2.3">
</level2item>
<level2item name="module 2.4">
</level2item>
<level2item name="module 2.5">
</level2item>
</level1item>
<level1item name="level1item 3">
</level1item>
</configuration>

The QML only has very basic means to parse XML files. In fact, the only suitable way I have found so far is to use XmlListModel. This means that I have to assume that the schema of the XML document is known beforehand. A more generic way would be to use C++ to parse the XML, but that was out of scope for me. I based my control on some existing solutions that are referenced at the end. Essentially, when the control is first loaded, a first level of the XML tree is read and the data is loaded into the ListView XmlListModel, which is defined in the main QML file.

The next idea is to check if each element of the ListView has any children at all. For that purpose, a separate XmlListModel is defined in the ListView delegate. Whenever the ListView item is created, the model is constructed and the query uses the delegate's data to retrieve the children of the item. I want a certain image button (with an arrow indicating that the user can navigate to the children of the item) to become visible only in case there are more than 0 children. First, the query is assigned to the model, and the onQueryChanged event fires. The button is made invisible on this event. Next, the query returns the results and the onCountChanged fires. If the result returned more than 0 items, the button is made visible.


XmlListModel{
id: delegateXmlModel
source: "tree1.xml"
query: buildQuery(false, name);

XmlRole{name: "name"; query: buildRoleQuery();}

onQueryChanged: {
button.visible = false;
}

onCountChanged: {
if(count > 0)
button.visible = true;
}
}

The "back" button is displayed at the bottom of the ListView if the user has moved past the first level of the tree and has an option to move back. The functionality revolves around using the global variable level, which indicates which level of the XML tree is current at the moment. Most of the JavaScript deals with constructing the correct queries for the XmlListModels.

First level of the tree

Last level of the tree

Full listing:

// XMLTree

import QtQuick 1.0

Rectangle {
id:main
width: 360
height: 480

property int level: 0 //0: level1item; 1: level2item; 2: level3item
property string topElement : "configuration"
property string level0: "level1item"
property string level1: "level2item"
property string level2: "level3item"
property string level0Name: ""
property string level1Name: ""
property string level2Name: ""

function buildRoleQuery()
{
return "@name/string()";
}

function buildQuery(isMainTree, name)
{
var level0path = "/" + topElement + "/" + level0;
var level1path = level0path + "[@name=\""
+ level0Name + "\"]/" + level1;
var level2path = level1path + "[@name=\""
+ level1Name + "\"]/" + level2;

var level0query = level0path + "[@name=\""
+ name + "\"]/" + level1;
var level1query = level0path + "[@name=\""
+ level0Name + "\"]/" + level1 + "[@name=\""
+ name + "\"]/" + level2;

if(level == 0)
{
if(isMainTree)
return level0path;
else
return level0query;
}
if(level == 1)
{
if(isMainTree)
return level1path;
else
return level1query;
}
if(level == 2)
{
if(isMainTree)
return level2path;
else
return level0query; //unused
}
return "/";
}

function setQueries()
{
listModel.query = buildQuery(true, "");
listModel.roles[0].query = buildRoleQuery();
listModel.reload();
}

Component.onCompleted: {
backButton.visible = false;
}

ListView {
id: listView
height: parent.height-50
width: parent.width
anchors.top: parent.top
model: listModel

delegate: ListViewDelegate{}
focus: true
}

Rectangle{
id: backRect
height: 50
width: parent.width
anchors.bottom: parent.bottom

Rectangle{
id:backButton
anchors.right: parent.right
width: 40
height: 40

Image{
id:backIcon
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: "icons/arrowBack.png"
}

MouseArea{
id: backMouseArea
anchors.fill: parent

onClicked: {
level--;
if(level == 0)
backButton.visible = false;
setQueries();
}
}
}
}

XmlListModel{
id: listModel
source: "tree1.xml"
query: "/" + topElement + "/" + level0

XmlRole {name: "name"; query: "@name/string()"}
XmlRole {name: "type"; query: "@type/string()"}
XmlRole {name: "attr1"; query: "@attr1/string()"}
XmlRole {name: "attr2"; query: "@attr2/string()"}
}
}
//ListView delegate

import QtQuick 1.0

Rectangle{
id: delegate
width: parent.width
height: textDelegate.height

property string fullText : ""

function addComma()
{
if(fullText)
fullText = fullText + ", ";
}

function getText()
{
fullText = "";
if(level == 0)
{
if(type)
fullText = "
" + "type=" + type;
if(attr1)
{
addComma();
fullText = fullText + "
" + "attr1=" + attr1;
}
return name + fullText;
}
else if(level == 1)
return name;
else if(level == 2)
{
if(attr1)
fullText = "
" + "attr1: " + attr1;
if(attr2)
{
addComma();
fullText = fullText + "attr2: " + attr2;
}
return name + fullText;
}
else
return name;
return "";
}

Component.onCompleted: {
textDelegate.text = getText();
}

XmlListModel{
id: delegateXmlModel
source: "tree1.xml"
query: buildQuery(false, name);

XmlRole{name: "name"; query: buildRoleQuery();}

onQueryChanged: {
button.visible = false;
}

onCountChanged: {
if(count > 0)
button.visible = true;
}
}

Rectangle{
id: contentDelegate
anchors.left: parent.left
anchors.right: nextButton.left
height: textDelegate.height
border.color: "red"
border.width: 1
z:1

Image{
id: elementIcon
height: textDelegate.height
fillMode: Image.PreserveAspectFit
}

Text{
id: textDelegate
anchors.left: elementIcon.right
anchors.leftMargin: 10
anchors.right: parent.right
height: 70
text: ""
font.pixelSize: 15
horizontalAlignment: Text.AlignHLeft
wrapMode: Text.WordWrap
}
}

Rectangle{
id: nextButton
anchors.right: parent.right
anchors.rightMargin: 4
width: 40
height: contentDelegate.height

Rectangle{
id: button
anchors.centerIn: parent
width: 40
height: 40

radius: 5
border{ color: "gray"; width: 3}
visible: (level < 2) //hide 'next' button when lowest level reached

Image{
id: nextIcon
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: "icons/arrow.png"
}

MouseArea{
id: nextMouseArea
anchors.fill: parent

onClicked: {
if(level == 0)
level0Name = name;
else if(level == 1)
level1Name = name;
level++;
if(level > 0)
backButton.visible = true;
setQueries();
listView.model = listModel;
}
}
}
}
}

References

QML Treeview
New Version of the QML TreeView by . Also posted on my website

Thursday, March 8, 2012

A Disabled Tab and Adding Tabs on the Fly

A custom tab control received a slight update: An isDisabled property was added to the CustomTab. The disabled tab does absolutely nothing - does not respond to mouse clicks or mouse enter and exit events. Its name is displayed in gray. So a couple changes were done to the CustomTab which are summarized below.

import QtQuick 1.0

Rectangle {
...
property bool isDisabled;
property int tabIndex;

function applyState()
{
...
if(isDisabled)
{
textOnTab.color = "darkgrey";
}
}

MouseArea{
hoverEnabled: true
anchors.fill: parent
onClicked: {
if(!isDisabled)
{
...
}
}
...
}

The JavaScript for the main control was modified to become somewhat more generic. There is certainly space for some more improvement! Tabs are currently added with the minimum number of properties. When all the tabs are added, the recalculateTabProperties() is called to calculate properties of each tab such as width, margins or anchors using the total number of tabs and the index of the current tab as starting points. The applyXMLModel() is totally optional, it just sits there so that the code that reads the XML is not lost.

Another bit of functionality is adding a tab "on the fly", to the right of the existing tabs. Using the changes above, the addOneMoreTab() function adds a CustomTab object to the children of the tabsRow element. Currently this function modifies the iPosition of the previous tab to make sure it's no longer marked as rightmost. As an improvement, this bit can be moved into the recalculateTabProperties(), where it most likely belongs. The way the tab is added is shown on the screenshots.

Tab Control Loaded

New Tab Added Dynamically

Full code of the main application.

import QtQuick 1.0

Rectangle {
id: screen
width: 490; height: 400
property int numTabs: 5
property int margin: 2
property string transparentColor : "transparent"
property string redColor: "red"
property string grayColor: "#B7B9BC" // "lightgray"

function addOneMoreTab()
{
//get the current tab count
var numTabs = tabsRow.children.length;
//get the last tab and change its position state
var lastTab = tabsRow.children[numTabs-1];
lastTab.iPosition = 1;
//add the new tab
var tab = Qt.createComponent("CustomTab.qml");
var obj = tab.createObject(tabsRow);

obj.tabIndex = tabsRow.children.length;
obj.tabtext = "Just Added";

recalculateTabProperties();

tabsRow.clearState();
}

function recalculateTabProperties()
{
var numTabs = tabsRow.children.length;
var i=0;
for(i=0;i<=numTabs-1;i++)
{
console.log("recalculating tab " + i);

var obj = tabsRow.children[i];
if(i==0)
{
obj.iPosition = 0;
}
else if(i==numTabs-1)
{
obj.iPosition = 2;
}
else
{
obj.iPosition = 1;
}

obj.ctlHeight = tabsRow.height - margin;
obj.isSelected = false;
obj.anchors.bottom = tabsRow.bottom;
obj.anchors.bottomMargin = margin;

if(obj.iPosition == 0)
{
obj.ctlWidth = tabsRow.width/numTabs;
obj.anchors.left = tabsRow.left;
}
else
{
obj.ctlWidth = tabsRow.width/tabsRow.children.length - margin;
obj.anchors.left = tabsRow.children[i-1].right;
obj.anchors.leftMargin = margin;
}
}
}

function createTab()
{
var tab = Qt.createComponent("CustomTab.qml");

if(tab.status == Component.Ready)
{
var obj = tab.createObject(tabsRow);
if(obj == null)addOneMoreTab();
{
return false;
}

var numTabs = tabsRow.children.length
console.log("tabsRow children:" + numTabs)

obj.tabIndex = numTabs;
}
return true;
}

function applyXMLModel()
{
var i=0;
for(i=0;i<=xmlTabModel.count-1;i++)
{
var obj = tabsRow.children[i];
if(obj != null)
{
console.log("obj is not null")
if(xmlTabModel.get(i).size == "Disabled")
{
obj.tabtext = "Disabled";
obj.isDisabled = true;
}
else
{
obj.tabtext = xmlTabModel.get(i).name;
}
}
}
}

function createTabs(num)
{
var i=0;

for(i=0;i<=num-1;i++)
{
var success = createTab();
if(!success)
{
console.log("Failed to create tab #" + i);
}
}

recalculateTabProperties();
applyXMLModel();

tabsRow.clearState();
}

XmlListModel{
id: xmlTabModel
source: "tabs.xml"
query: "/tabList/tab"
XmlRole{name: "name"; query: "name/string()" }
XmlRole{name: "size"; query: "size/string()"}

onCountChanged: {
createTabs(count);
}
}

Rectangle {
id: backRect
radius: 10
width: parent.width
height: parent.height - 50 // need to expand to free space
color: grayColor
anchors.top: parent.top
anchors { leftMargin: 10; bottomMargin: 10; topMargin: 10; rightMargin:10 }

Rectangle{
id: tabsRect
radius: 10
width: parent.width
height: 80
anchors.top: parent.top

Row{
id:tabsRow
width: parent.width
height: parent.height

function clearState()
{
var j=0;
for(j=0;j<= tabsRow.children.length - 1;j++)
{
children[j].isSelected = false;
children[j].state = "unselected";
children[j].applyState();
}
}
}
}
}

Rectangle{
id: buttonRect
height: 40
width: 75
border.width: 1
border.color: "black"
anchors.bottom: parent.bottom

Text{
text: "Click Here"
x:5
y:10
}

MouseArea{
anchors.fill: parent
onClicked: {
addOneMoreTab();
}
}
}
}
by . Also posted on my website

Wednesday, March 7, 2012

Loading Tabs Dynamically

The next task is to remove all hard-coded CustomTab elements from the custom tab control and load them all through JavaScript. Furthermore, verify that any settings can be loaded from the XML file. For the start, let's use the simple XML file, suspiciously similar to the one I've recently used to playing with a ListView element.

<?xml version="1.0" encoding="utf-8"?>
<tabList>
<tab>
<name>Tab 1</name>
<size>Medium</size>
</tab>
<tab>
<name>Tab 2</name>
<size>Medium</size>
</tab>
<tab>
<name>Tab 3</name>
<size>Medium</size>
</tab>
<tab>
<name>Tab 4</name>
<size>Medium</size>
</tab>
<tab>
<name>Tab 5</name>
<size>Medium</size>
</tab>
</tabList>

Just like in a ListView example, the XmlListModel element will be used to read data from the XML file. The usage of the XmlListModel is not limited to lists - data can be accessed directly. When the XmlListModel element is just created, it has no data. Then, at some point, data is loaded and the onCountChanged event fires. This fact is used to trigger the creation of tabs.

XmlListModel{
id: xmlTabModel
source: "tabs.xml"
query: "/tabList/tab"
XmlRole{name: "name"; query: "name/string()" }
XmlRole{name: "size"; query: "size/string()"}

onCountChanged: {
createTabs(count);
}
}

This line

var tab = Qt.createComponent("CustomTab.qml");

creates a Component object created using the QML file that is located at the address specified - can be a local file, or a URL, for example. The next point of interest,

var obj = tab.createObject(tabsRow);

creates an object instance of that component. The parameter is the parent object. So, I want the tab to be a child of the tabsRow. Next, some simple JavaScript follows - I'm dynamically assigning properties to the object which were previously hard-coded and make sure the initial position of the tabs is correct. The line

obj.tabtext = xmlTabModel.get(i).name;

confirms that my XML file was read correctly: I'm displaying the name property from the XML file on the tab. That's about all that's interesting about this example - the behavior or the visual layout did not change, just the way the tabs are created inside the tab control.

The CustomTab.qml did not change much. I only added a tabtext property that is used by a Text element to display some text that is retrieved from the XML file and confirms that the tab control works as expected. So all the code added to the CustomTab.qml is summarized below:

import QtQuick 1.0

Rectangle {
id: customTab
...

property string tabtext;

Text{
id: textOnTab;
text: tabtext;
y:30;
z:1;
}
...
}

The screenshot is about the same.

The Tab Control - Looks the Same, but Dynamic

The full main QML file for reference, there's much more JavaScript and less QML layout:

import QtQuick 1.0

Rectangle {
id: screen
width: 490; height: 400
property int numTabs: 5
property int margin: 2
property string transparentColor : "transparent"
property string redColor: "red"
property string grayColor: "#B7B9BC" // "lightgray"

function createTab(i, num)
{
var tab = Qt.createComponent("CustomTab.qml");

if(tab.status == Component.Ready)
{
var obj = tab.createObject(tabsRow);

if(obj == null)
return false;
if(i == 0)
obj.iPosition = 0;
else if(i == num-1)
obj.iPosition = 2;
else
obj.iPosition = 1;

obj.ctlHeight = tabsRow.height - margin;
obj.isSelected = false;
obj.anchors.bottom = tabsRow.bottom;
obj.anchors.bottomMargin = margin;
obj.tabtext = xmlTabModel.get(i).name;
if(i == 0)
{
obj.ctlWidth = tabsRow.width/num;
obj.anchors.left = tabsRow.left;
}
else
{
obj.ctlWidth = tabsRow.width/num - margin;
obj.anchors.left = tabsRow.children[i-1].right;
obj.anchors.leftMargin = margin;
}
}
else
{
return false;
}
return true;
}

function createTabs(num)
{
var i=0;

for(i=0;i<=num-1;i++)
{
var success = createTab(i, num);
if(!success)
{
console.log("Failed to create tab #" + i);
}
}

tabsRow.clearState();
}

XmlListModel{
id: xmlTabModel
source: "tabs.xml"
query: "/tabList/tab"
XmlRole{name: "name"; query: "name/string()" }
XmlRole{name: "size"; query: "size/string()"}

onCountChanged: {
createTabs(count);
}
}

Rectangle {
id: backRect
radius: 10
width: parent.width
height: parent.height // need to expand to free space
color: grayColor
anchors.top: parent.top
anchors { leftMargin: 10; bottomMargin: 10; topMargin: 10; rightMargin:10 }

Rectangle{
id: tabsRect
radius: 10
width: parent.width
height: 80
anchors.top: parent.top

Row{
id:tabsRow
width: parent.width
height: parent.height
anchors.fill: parent

function clearState()
{
var j=1;
for(j=0;j<= numTabs-1;j++)
{
children[j].isSelected = false;
children[j].state = "unselected";
children[j].applyState();
}
}
}
}
}
}

References

Dynamic Object Management in QML
QML Advanced Tutorial 2 - Populating the Game Canvas by . Also posted on my website

Tuesday, March 6, 2012

Customizing a Tab Control with QML

As my next task, I'm developing a tab control that has to follow certain design standards. The end result is shown at the end of this post. The easiest way to do this would be just to use images as backgrounds for UI elements. However, the control also has to scale and in most cases the uneven stretching of images would lead to ugliness. Therefore, the whole control had to be developed by using QML design elements, such as Rectangles, Rows or Grids. There were two major issues to be considered: firstly, there is no way to round only certain corners of the rectangle, its either all or none. Secondly, when the tab is selected or moused over, a red line has to be displayed across the top of the tab.

Generally, the tab control is a rectangle which has a row at the top. A tab is a component which is specified in a separate QML file, CustomTab.qml. For now, tabs are hard-coded as child elements of the said row. Other than layout properties, that anchor the tab to a proper position, it also holds some properties that are important for visual display. isSelected defines if the tab is currently selected. If it is, it becomes slightly taller and displays a red line across the top. isHighlighted defines if the mouse is currently over the tab. If it is, a more narrow red line is displayed across the top, and the tab height does not change. The iPosition has three states and defines if the tab is rightmost, leftmost or is in between the tabs. Rightmost tab has the top right corner rounded, leftmost - the left, and the middle tabs are not rounded. When the tab is selected, both corners are rounded regardless of position. All of this logic was implemented in somewhat messy JavaScript functions.

From the design point of view, each tab is a 2x2 Grid element. A rectangular grid covers a rounded rectangle. By making top right or top left elements or both of the grid transparent, I make rounded corners visible, giving the impression of a tab with one or two rounded corners. Top elements of the grid are also used to change the height of the red line by modifying their height.

The most important function is the applyState(). After tab properties such as isSelected or isHighlighted were assigned, the function makes sure that the tab is visualised correctly.

function applyState()
{
if(isSelected)
{
applyHeights(ctlHeight + redLineHeight/2, redLineHeight, ctlHeight + redLineHeight/2)
customTab.color=redColor
}
else if(isHighlighted)
{
applyHeights(ctlHeight, redLineHeight/2, ctlHeight)
customTab.color = redColor
}
else
{
applyHeights(ctlHeight, redLineHeight, ctlHeight)
customTab.color=grayColor
}

if(iPosition == "0")
{
elements.children[0].color = transparentColor;
elements.children[1].color = getCorrectColor();
}
else if(iPosition == "1")
{
elements.children[0].color = getCorrectColor();
elements.children[1].color = getCorrectColor();
}
else if(iPosition == "2")
{
elements.children[0].color = getCorrectColor();
elements.children[1].color = transparentColor;
}
}

If the tab is selected, the height of the grid rows is increased as required, if it is highlighted the height of the top row is adjusted and if none of that, it is returned to the initial state. Next, the grid elements are colored appropriately or set to transparent according to the tab position in the tab control.

function clearState()
{
var j=1;
for(j=0;j<= numTabs-1;j++)
{
children[j].isSelected = false;
children[j].state = "unselected";
children[j].applyState();
}
}

The clearState() function just loops through the children of tabRow, which are individual tabs. It clears the isSelected tag and applies the proper state (see below). Then it applies the tab state, essentially resetting it to initial state.

MouseArea{
hoverEnabled: true
anchors.fill: parent
onClicked: {
parent.parent.clearState();
parent.isSelected = true;
parent.state = "selected"
parent.applyState();
}
onEntered: {
parent.isHighlighted = true;
if(!parent.isSelected)
{
parent.applyState();
}
}
onExited: {
parent.isHighlighted = false;
if(!parent.isSelected)
{
parent.applyState();
}
}
}

Each tab has a MouseArea, which listens to three events: onClicked, onEntered and onExited. Note the usage of hoverEnabled: true, without which the MouseArea would just ignore the last two events mentioned. When the tab is clicked, all tabs are reset first, and then the clicked tab is set to be selected. When the tab is entered or exited, the isHighlighted attribute is toggled and the changes are applied to the tab - but only is it is not already selected. If it is, the event is ignored since the tab should not change if it is already selected, and the mouse is over it.

states: [
State {
name: "selected"
PropertyChanges { target: customTab; anchors.bottomMargin: 0}
},

State {
name: "unselected"
PropertyChanges {target: customTab; anchors.bottomMargin: margin}
}
]

And lastly, the anchor.bottomMargin can not be changed from JavaScript, therefore two states had to be created. Removing the margin when the tab is selected gives the "melding" effect, when the white line between the selected tab and the rest of the control disappears, and appears again when a different tab is selected. The state of the tab gets toggled by the onClicked event of the MouseArea, as shown above.

Finally, the full listing of the code - it is not yet overly huge.

// The main qml file

import QtQuick 1.0

Rectangle {
id: screen
width: 490; height: 400
property int numTabs: 5
property int margin: 2
property string transparentColor : "transparent"
property string redColor: "red"
property string grayColor: "#B7B9BC" // "lightgray"

Rectangle {
id: backRect
radius: 10
width: parent.width
height: parent.height
color: grayColor
anchors.top: parent.top
anchors { leftMargin: 10; bottomMargin: 10; topMargin: 10; rightMargin:10 }

Rectangle{
id: tabsRect
radius: 10
width: parent.width
height: 80
anchors.top: parent.top

Row{
id:tabsRow
width: parent.width
height: parent.height
anchors.fill: parent

function clearState()
{
var j=1;
for(j=0;j<= numTabs-1;j++)
{
children[j].isSelected = false;
children[j].state = "unselected";
children[j].applyState();
}
}

CustomTab{
id: customTab0
ctlWidth: parent.width/numTabs
ctlHeight: parent.height - margin
tabIndex: 0
isSelected: false;
iPosition: 0
anchors { left: parent.left; bottom: parent.bottom; bottomMargin: margin }
}

CustomTab{
id: customTab1
ctlWidth: parent.width/numTabs-margin
ctlHeight: parent.height - margin
tabIndex: 1
isSelected: false;
iPosition: 1
anchors { left: customTab0.right; bottom: parent.bottom; leftMargin: margin; bottomMargin: margin; }
}

CustomTab{
id: customTab2
ctlWidth: parent.width/numTabs-margin
ctlHeight: parent.height - margin
tabIndex: 2
isSelected: false;
iPosition: 1
anchors { left: customTab1.right; bottom: parent.bottom; leftMargin: margin; bottomMargin: margin; }
}

CustomTab{
id: customTab3
ctlWidth: parent.width/numTabs-margin
ctlHeight: parent.height - margin
tabIndex: 3
isSelected: false;
iPosition: 1
anchors { left: customTab2.right; bottom: parent.bottom; leftMargin: margin; bottomMargin: margin; }
}

CustomTab{
id: customTab4
ctlWidth: parent.width/numTabs-margin
ctlHeight: parent.height - margin
tabIndex: 4
isSelected: false;
iPosition: 2
anchors { left: customTab3.right; bottom: parent.bottom; leftMargin: margin; bottomMargin: margin; }
}
}
}
}
}
// The CustomTab.qml file

import QtQuick 1.0

Rectangle {

id: customTab
clip: true

property int ctlWidth;
property int ctlHeight;
property int redLineHeight: 20;
property bool isSelected;
property bool isHighlighted;
property int tabIndex;
property int iPosition; // 0 = left; 1 = middle; 2 = right

width: ctlWidth
height: ctlHeight
color: "#B7B9BC"
radius: 10

function applyHeights(customTabHeight, topHeight, bottomHeight)
{
customTab.height = customTabHeight

elements.children[0].height = topHeight
elements.children[1].height = topHeight
elements.children[2].height = bottomHeight
elements.children[3].height = bottomHeight
}

function clearChildState()
{
applyHeights(ctlHeight, redLineHeight, ctlHeight - redLineHeight)
}

function getCorrectColor()
{
if(isSelected)
{
return transparentColor;
}
else if(isHighlighted)
{
return redColor;
}
else
{
return grayColor;
}
}

function applyState()
{
if(isSelected)
{
applyHeights(ctlHeight + redLineHeight/2, redLineHeight, ctlHeight + redLineHeight/2)
customTab.color=redColor
}
else if(isHighlighted)
{
applyHeights(ctlHeight, redLineHeight/2, ctlHeight)
customTab.color = redColor
}
else
{
applyHeights(ctlHeight, redLineHeight, ctlHeight)
customTab.color=grayColor
}

if(iPosition == "0")
{
elements.children[0].color = transparentColor;
elements.children[1].color = getCorrectColor();
}
else if(iPosition == "1")
{
elements.children[0].color = getCorrectColor();
elements.children[1].color = getCorrectColor();
}
else if(iPosition == "2")
{
elements.children[0].color = getCorrectColor();
elements.children[1].color = transparentColor;
}
}

Component.onCompleted: {
applyState();
}

Grid{
id:elements
width: parent.width
height: parent.height
columns: 2
rows: 2

Rectangle{id: topLeft; color: grayColor; width: parent.width/2; height: redLineHeight;}
Rectangle{id: topRight; color: grayColor; width: parent.width/2; height: redLineHeight;}
Rectangle{id: bottomLeft; color: grayColor; width: parent.width/2; height: parent.height - redLineHeight;}
Rectangle{id: bottomRight; color: grayColor; width: parent.width/2; height: parent.height - redLineHeight;}
}

MouseArea{
hoverEnabled: true
anchors.fill: parent
onClicked: {
parent.parent.clearState();
parent.isSelected = true;
parent.state = "selected"
parent.applyState();
}
onEntered: {
parent.isHighlighted = true;
if(!parent.isSelected)
{
parent.applyState();
}
}
onExited: {
parent.isHighlighted = false;
if(!parent.isSelected)
{
parent.applyState();
}
}
}

states: [
State {
name: "selected"
PropertyChanges { target: customTab; anchors.bottomMargin: 0}
},

State {
name: "unselected"
PropertyChanges {target:customTab; anchors.bottomMargin: margin}
}
]
}

Tabs Just Loaded, None Selected

Tab Selected, Another Tab Highlighted

References

QML States
QML PropertyChanges Element
Anchor-based Layout in QML
JavaScript Functionsby . Also posted on my website

Monday, March 5, 2012

ListView in QML

The ListView is similar to ListViews in other programming languages - it displays a list of items! There is some difference, of course, in the way it is implemented. The data to be displayed is called a model, and if it is some fixed data then it can be defined right in the code by adding a ListModel element like this:


ListModel {
ListElement {
name: "Joe Bloggs"
number: "123 1234"
}
}

That will be a list with just one element. Next, there is the delegate. The delegate defines the way data is displayed. Again, it can be very simple, for example

ListView{
id: listView

...

delegate: Text{text: name}
}

Such delegate will just display some text which is taken from the model.

To make this post slightly less boring, I will first make the ListView read its model from the xml file. For that purpose, the XmlListModel element is used. I will use this sample XML structure:

<?xml version="1.0" encoding="utf-8"?>
<listModel>
<item>
<name>Item One</name>
<size>Medium</size>
<desc>Item One Detailed Description</desc>
</item>
<item>
<name>Item Two</name>
<size>Large</size>
<desc>Item Three Detailed Description</desc>
</item>
<item>
<name>Item Three</name>
<size>Small</size>
<desc>Item Three Detailed Description</desc>
</item>
<item>
<name>Item Four</name>
<size>Small</size>
<desc>Item Four Detailed Description</desc>
</item>
<item>
<name>Item Five</name>
<size>Small</size>
<desc>Item Five Detailed Description</desc>
</item>
</listModel>

The XMLListModel that reads this file will be defined as follows:

XmlListModel{
id: xmlTestListModel
source: "listModel.xml"
query: "/listModel/item"
XmlRole{name: "name"; query: "name/string()" }
XmlRole{name: "size"; query: "size/string()" }
XmlRole{name: "desc"; query: "desc/string()" }
}

I give the element the location where to look for the file, and the XPath to look for individual items. Next, I define all the tags I want to read into the model - in this case, name, size and description, but there can be more. The model will parse the XML file and read all this information, and if I want to use it or not - it's completely up to me. Next, I'll do something with the delegate to show that its activity is not limited to displaying lines of text, it is capable of more complex behaviour. Before that, a quick note about the section property of the ListView: it allows the list to be separated into different parts, and the sections can have a delegate specified. Additionally, QML allows breaking the code into multiple files. I will add the file called SectionHeading.qml and specify the delegate for the section in the following manner:

import QtQuick 1.0

Component {
id: sectionHeading
Rectangle {
width: listViewRect.width
height: childrenRect.height
color: "lightsteelblue"

Text {
text: section
font.bold: true
}
}
}

This delegate displays the section as the rectangle with a background colour, and displays the text that is defined in the variable section, in bold. To reference this delegate I can use the QML file name: section.delegate: SectionHeading{}. At this point the "desc" property from the XML file is not used anywhere - but the XmlListModel already knows it and I can use it later. Here is the full code after making all these modifications:

import QtQuick 1.0

Rectangle{

id: main
color: "lightGrey"
property int rectWidth: 480
property int rectHeight: 480

width: rectWidth
height: rectHeight

Rectangle{
id: listViewPanel
width: rectWidth/3
height: rectHeight
x: 0
y: 0
border.color: "black"
color: "green"
border.width: 1

Text{
text: "Left panel"
}

Rectangle{
id: listViewRect
state: "list"
anchors.fill: parent
width: parent.width
height: parent.height
anchors.margins: 15

XmlListModel{
id: xmlTestListModel
source: "listModel.xml"
query: "/listModel/item"
XmlRole{name: "name"; query: "name/string()" }
XmlRole{name: "size"; query: "size/string()" }
XmlRole{name: "desc"; query: "desc/string()" }
}

ListView{
id: listView

width: parent.width
height:parent.height
anchors.top: parent.top
anchors.bottom: parent.bottom

model: xmlTestListModel
delegate: Text{text: name}

section.property: "size"
section.criteria: ViewSection.FullString
section.delegate: SectionHeading{}
}
}
}

Rectangle{
id: detailsPanel
width: parent.width*2/3
height: parent.height*2/3
x: parent.width/3
y: 0
border.color: "black"
color: "yellow"
border.width: 1

Text{
text: "Details panel"
}
}

Rectangle{
id: loggingPanel
width: parent.width*2/3
height: parent.height/3
border.color: "black"
border.width: 1
x: parent.width/3
y: parent.height*2/3
color: "blue"

Text{
text: "Logging panel"
}
}
}

The ListView is drawn inside a Rectangle. The XmlListModel then parses the XML file specified. The delegate for the ListView only displays the "name" property from the XML file for each item. The "size" property is assigned to the section. It is passed to the section.delegate as a parameter, which then displays it in bold, inside a Rectangle which has lightsteelblue background colour. The end result of the application is displayed below.

The Example Application

References:

QML ListView Element
QML XmlListModel Element
Models and Views: Sections ListView Example
by . Also posted on my website

Sunday, March 4, 2012

Starting With QML

Qt is a framework for developing software with a GUI (and other software, too). It uses C++. QML (Qt Meta Language) is a declarative language for developing UI, and is based on JavaScript. I'll be using QML to build a prototype of a UI for a certain device. For that purpose, I'll be using Qt Creator that is installed on the Ubuntu 11.10. There are some new experiences here. I'll move on to connecting QML applications with C++ plugins later, but for now just some plain QML.

To create a simple QML project, I select File -> New File or Project from Qt Creator menu. From Projects I select Qt Quick Project and Qt Quick UI on the right.

New Qt Creator Project

Next I select the location for the project files and when I'm done, the project contains only one qml file and a project file with the extension "qmlproject" which I do not need to know much at that early stage. The qml file contains a simple "Hello World" application.

Qt Creator Environment

Compared to my experience, QML appears to be somewhat similar to WPF, considering that I define the layout with the declarative part and then can apply scripts additionally to provide extra functionality. In this first post, I'll only look at some of the most basic layout elements. I'm starting with the UI that has three panels: the left panel takes up one third of the space, and the right side is divided into the top and bottom parts, the bottom being one third of the vertical space. The screen size is hard-coded at this point.

The most basic element is a Rectangle. It is just that - a UI element of a rectangular shape. It can be filled with a color and can hold other UI elements. For example, it can hold a Text element which is, essentially, a label.

Rectangle{
id: listViewPanel
width: 100
height: 100
x: 0
y: 0
border.color: "black"
color: "green"
border.width: 1

Text{
text: "Left panel"
}
}

The id is optional, but will be used later to reference the element. Other parameters are self-explanatory.

Here is the first QML application - just a few rectangles that take up the screen.

import QtQuick 1.0

Rectangle{

id: main
color: "lightGrey"
property int rectWidth: 480
property int rectHeight: 480

width: rectWidth
height: rectHeight

Rectangle{
id: listViewPanel
width: rectWidth/3
height: rectHeight
x: 0
y: 0
border.color: "black"
color: "green"
border.width: 1

Text{
text: "Left panel"
}
}

Rectangle{
id: detailsPanel
width: parent.width*2/3
height: parent.height*2/3
x: parent.width/3
y: 0
border.color: "black"
color: "yellow"
border.width: 1

Text{
text: "Details panel"
}
}

Rectangle{
id: loggingPanel
width: parent.width*2/3
height: parent.height/3
border.color: "black"
border.width: 1
x: parent.width/3
y: parent.height*2/3
color: "blue"

Text{
text: "Logging panel"
}
}
}

Rectangle main is the parent element, and defines the size of the screen. It defines two properties, which are screen height and width and are used by child elements to calculate their own sizes. A child element can access the parent's properties by using parent.property syntax (i.e. parent.height). Each child rectangle has a Text element inside, just to provide some visual feedback.

Here's the result of executing the application:

First QML Application

References:

Qt - Cross-platform application and UI framework
QML Rectangle Element by . Also posted on my website