Wednesday, April 4, 2012

QML ScrollBar

I had to implement a ScrollBar component but the examples I could find did not do what I needed. I needed the ScrollBar with arrows on the sides, which would move the contents of the field a certain number of points to the required direction. After that, I added the sliding feature, that moved the contents of the field when the slider is dragged.

The ScrollBar consists of four rectangles. Two of them are arrows at the ends. One is the whole length of the scroll bar, and the last one is the slider that can be dragged along the body of the scroll bar. I used the Flickable component because I need to know the size of the field contents. Otherwise, I disabled the functionality of the flickable. When any of the arrows is clicked, first action is to identify if the slider can move in the required direction. This is calculated based on the height/width of the field (returned by flickable.height or width), height/width of the contents of the field (returned by flickable.contentHeight or contentWidth) and the current position of the contents within the flickable control (returned by flickable.contentX or contentY). If there is space to move, the flickable.contentX or contentY is changed, which moves the contents inside the field, and the slider position is adjusted.

The slide operates on the similar principle. When it is released, its position is read and then basing on the position and the length of the slider body the percentage of length the slider has traveled is calculated. From that percentage, the flickable.contentX or contentY is calculated so that the field contents move to reflect that percentage. It is not yet completely presice, but it does what is expected to do.

To use the ScrollBar, it needs to know several things: the id of the Flickable component associated with the field, the desired width of the scroll bar, the amount of pixels the contents need to move each click and the orientation - vertical or horisontal. In the example below, the Flickable component called view is associated with both scroll bars, the width of the scroll bar is 10 and the image moves 20 pixels each arrow click.

import QtQuick 1.1

Rectangle {
width: 360
height: 360

Flickable{

id:view
anchors.fill: parent
contentWidth: picture.width
contentHeight: picture.height
interactive: false

Image {
id: picture
source: "images/Desert.jpg"
asynchronous: true
}
}

ScrollBar{
id: verticalScroll
flickable: view
step: 20
size: 10
orientation: Qt.Vertical
}

ScrollBar{
id: horisontalScroll
flickable: view
step: 20
size: 10
orientation: Qt.Horizontal
}
}

QML ScrollBar

The listing of the ScrollBar component is as follows:

import QtQuick 1.1

Rectangle {

id: scrollBar
property variant orientation
property variant flickable
property int step
property int size
height: orientation === Qt.Vertical ? flickable.height : size
width: orientation === Qt.Vertical ? size : flickable.width

function canMove(first)
{
if(first)
return orientation === Qt.Vertical ? flickable.contentY > 0 : flickable.contentX > 0;
else
return orientation === Qt.Vertical ? flickable.height + flickable.contentY < flickable.contentHeight :
flickable.width + flickable.contentX < flickable.contentWidth
}

function arrowClicked(first)
{
if(canMove(first))
{
if(first)
{
if(orientation === Qt.Vertical)
flickable.contentY -= step;
else
flickable.contentX -= step;
}
else
{
if(orientation === Qt.Vertical)
flickable.contentY += step;
else
flickable.contentX += step;
}
positionSlider();
}
}

function moveContents()
{
if(orientation === Qt.Vertical)
flickable.contentY = (slider.y - size)*(flickable.contentHeight - flickable.height)/(body.height - slider.height);
else
flickable.contentX = (flickable.contentWidth - flickable.width)*slider.x/flickable.width;
}

function positionSlider()
{
var percentage = orientation === Qt.Vertical ? (flickable.contentY)/(flickable.contentHeight - flickable.height)
: (flickable.contentX + size)/(flickable.contentWidth - flickable.width);
if(orientation === Qt.Vertical)
slider.y = percentage*(body.height - slider.height) + size;
else
slider.x = percentage*(body.width - slider.width) + size;
}

onHeightChanged: {
if(canMove(true) || canMove(false))
positionSlider();
}

onWidthChanged: {
if(canMove(true) || canMove(false))
positionSlider();
}

Component.onCompleted: {
sliderArea.drag.axis = orientation === Qt.Vertical ? Drag.YAxis : Drag.XAxis;

if(orientation === Qt.Vertical)
{
scrollBar.anchors.right = flickable.right
firstArrow.anchors.top = scrollBar.top;
body.anchors.top = firstArrow.bottom;
secondArrow.anchors.bottom = scrollBar.bottom;
slider.y = size;
}
else
{
scrollBar.anchors.bottom = flickable.bottom;
firstArrow.anchors.left = scrollBar.left;
body.anchors.left = firstArrow.right;
secondArrow.anchors.right = scrollBar.right;
slider.x = size;
}
}

Rectangle{
id: firstArrow
width: size
height: size

Image{
id: imgFirstArrow
anchors.fill: parent
source: orientation === Qt.Vertical ? "images/vertUpArrow.png" : "images/horisLeftArrow.png"
}

MouseArea{
id: firstArrowArea
anchors.fill: parent
onClicked: {
arrowClicked(true);
}
}
}

Rectangle{
id: body
width: orientation === Qt.Vertical ? size : scrollBar.width - 2*size
height: orientation === Qt.Vertical ? scrollBar.height - 2*size : size
color: "#575B5E"
}

Rectangle{
id: slider
width: orientation === Qt.Vertical ? size : 3*size;
height: orientation === Qt.Vertical ? 3*size : size;

Image{
id: imgSlider
anchors.fill: parent
source: orientation === Qt.Vertical ? "images/SliderVert-Enabled.png" : "images/SliderHoris-Enabled.png"
}

MouseArea{
id:sliderArea
anchors.fill: parent
drag.target: slider
drag.minimumX: 10
drag.minimumY: 10
drag.maximumX: orientation === Qt.Vertical ? 0 : body.width - slider.width + size
drag.maximumY: orientation === Qt.Vertical ? body.height - slider.height + size : 0

onReleased: {
moveContents();
}
}
}

Rectangle{
id: secondArrow
width: size
height: size

Image{
id: imgSecondArrow
anchors.fill: parent
source: orientation === Qt.Vertical ? "images/vertDownArrow.png" : "images/horisRightArrow.png"
}

MouseArea{
id: secondArrowArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
arrowClicked(false);
}
}
}
}

References:

ScrollBar.qml Example File
Scrollable and Scroll indicators with QML by . Also posted on my website

Sunday, April 1, 2012

Return Multiple Values to QML from C++

In some cases it is required to return several values from a C++ function to the QML code. In my case, I call the function that checks the syntax of the Qt script that is passed as a string. The QScriptSyntaxCheckResult class does that check for me. If an error is found inside the script, I want to get back the error message and the line and column where the error was detected. In this case a QVariantMap class is the elegant and effective solution. In my C++ code, I pack the values I want to return into an instance of a QVariantMap class:

QVariantMap QMLFile::checkScriptSyntax(QString input) const
{
QScriptSyntaxCheckResult result = QScriptEngine::checkSyntax(input);

QVariantMap value;
value.insert("errorMessage", result.errorMessage());
value.insert("errorLineNumber", result.errorLineNumber());
value.insert("errorColumnNumber", result.errorColumnNumber());
return value;
}

In my QML code, I can now access the values in the following manner, after opening a file that contains a Qt script and passing its contents to the C++ function:

onClicked: {
var fileContent = QMLFile.getFileContents();
var result = QMLFile.checkScriptSyntax(fileContent);

if(result.errorMessage !== "")
{
console.log("Error on line " + result.errorLineNumber + ", column " + result.errorColumnNumber + " : " + result.errorMessage);
}
else
{
console.log("Script syntax is correct!");
}
}

The result is further used in the ToolTipRectangle element, which is a Text element that appears when the mouse is hovered over the line which contains an error.

import QtQuick 1.1

Rectangle {

property string toolTip : ""
property bool showToolTip: false

Rectangle{
id: toolTipRectangle
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.bottom
width: parent.toolTip.length * toolTipText.font.pixelSize / 2
height: toolTipText.lineCount * (toolTipText.font.pixelSize + 5)
z:100

visible: parent.toolTip !== "" && parent.showToolTip
color: "#ffffaa"
border.color: "#0a0a0a"

Text{
id:toolTipText
width: parent.parent.toolTip.length * toolTipText.font.pixelSize / 2
height: toolTipText.lineCount * (toolTipText.font.pixelSize + 5)
text: parent.parent.toolTip
color:"black"
wrapMode: Text.WordWrap
}
}

MouseArea {
id: toolTipArea
anchors.fill: parent
onEntered: {
parent.showToolTip = true
}
onExited: {
parent.showToolTip = false
}
hoverEnabled: true
}
}

The end result looks like this:

Syntax check result

References:

Q_INVOKABLE member function: Valid argument types
Implementing tool tips in QML by . Also posted on my website