Qt Create a QML Slider Sensitive to Touch Events

2.7k views Asked by At

I am creating a game for touch screens that requires 2-4 players to each have access to a pair of slider controls. The problem is that the QML Slider control responds to touch as a mouse event and seizes the focus. Then only one player can access a single control at a time. I need multiple sliders to respond to touch events simultaneously. My question is how to do that?

With the help of a variety of stack overflow posts, I have been able to create my own answer that so far seems to work. I detail the answer below in the answer section to save other newbies like me the trouble.

2

There are 2 answers

0
Peter Jackson On

There is a pure qml solution to this problem. The TouchSlider C++ object in my first answer (elsewhere in this thread) was unnecessary. Here I have modified the code to the TouchSlider qml code to eliminate references to touchslider (the TouchSlider C++ object).

TouchPoint.qml:

import QtQuick 2.0
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.2

Item {
    property string sliderTitle

    property real sliderMin
    property real sliderMax
    property real sliderVal


    ColumnLayout{
        id: column1
        Label {
            text: qsTr(sliderTitle)
            font.pointSize: 10

        }
        Slider {
            id: touchSlider1

            minimumValue: sliderMin
            maximumValue: sliderMax

            orientation: Qt.Vertical
            value: sliderVal
            onValueChanged: function(){
                sliderVal = Math.round(this.value);
                labelSliderValue.text = qsTr(JSON.stringify(sliderVal));
            }
        }

        Label {
             id: labelSliderValue
             text: qsTr(JSON.stringify(sliderVal))
             font.pointSize: 10
        }
        function sliderSetValueFromTouch(position){
            // Assume qs a vertical slider
            var minvalue = touchSlider1.minimumValue;

            var maxvalue = touchSlider1.maximumValue;
            // Since this is a vertical slider, by assumption, get the height
            var height = touchSlider1.height;

            // Compute the new value based on position coordinate
            var newvalue = (height-position)/height * (maxvalue-minvalue);
            if (newvalue<minvalue) newvalue = minvalue;
            if (newvalue>maxvalue) newvalue = maxvalue;
            //qDebug() << newvalue;

            // Set the value of the slider
            touchSlider1.value = newvalue;
        }

        MultiPointTouchArea{
            anchors.fill: touchSlider1

            touchPoints: [
                TouchPoint {
                    id: point1
                    onPressedChanged: function(){
                        if(pressed){
                            //console.log("pressed");
                            //console.log(touchslider.testStringReturn());
                            //touchslider.sliderSetValueFromTouch(touchSlider1,point1.y);
                            column1.sliderSetValueFromTouch(point1.y);
                        }
                    }

                }
            ]
            onTouchUpdated: function(){
                //touchslider.sliderSetValueFromTouch(touchSlider1,point1.y);
                column1.sliderSetValueFromTouch(point1.y);
            }

        }
    }
}

The touchslider.h and touchslider.cpp files add no value.

0
Peter Jackson On

I could not find a pure QML way to solve the problem but I wanted to minimize the use of C++. Using C++, I create an object TouchSlider and add it to my qml scene. The TouchSlider object has a simple function to update the value of a vertical slider according to a position argument. Then in the QML code, I add a MultiPointTouchArea on top of a regular slider and respond to the touch events by calling C++ function.

Here are all my files for a project called SliderPair.

SliderPair.pro:

QT += qml quick widgets
QT += quickcontrols2
QT += core
CONFIG += c++11
SOURCES += main.cpp \
    touchslider.cpp
RESOURCES += \
    qml.qrc

# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH += qml

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

HEADERS += \
    touchslider.h

DISTFILES +=

main.cpp:

#include <QApplication>
#include <QQmlApplicationEngine>
// add following includes for exposing new class TouchSlider to QML
#include <QQmlEngine>
#include <QQmlContext>
#include "touchslider.h"

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

    //Create an object of type TouchSlider
    //When a scoped pointer goes out of scope the object is deleted from memory. Good housekeeping:
    QScopedPointer<TouchSlider> touchslider (new TouchSlider);

    QQmlApplicationEngine engine;
    engine.addImportPath(QStringLiteral("qml"));
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    //QML can now refer to the TouchSlider object using the handle "touchslider":
    engine.rootContext()->setContextProperty("touchslider",touchslider.data());

    return app.exec();
}

touchslider.h:

#ifndef TOUCHSLIDER_H
#define TOUCHSLIDER_H

#include <QObject>
#include <QDebug>
#include <QtQuickControls2>

class TouchSlider : public QObject
{
    Q_OBJECT
public:
    explicit TouchSlider(QObject *parent = 0);

    //call Q_INVOKABLE macro to set up functions for QML
    Q_INVOKABLE void testDebug();          //hello world from C++
    Q_INVOKABLE QString testStringReturn(); //hello world to javascript
    Q_INVOKABLE void sliderSetValueFromTouch(QQuickItem *qs,int position );//use touch event to set slider value
signals:

public slots:
};


#endif // TOUCHSLIDER_H

touchslider.cpp:

#include "touchslider.h"

TouchSlider::TouchSlider(QObject *parent) : QObject(parent)
{

}

void TouchSlider::testDebug()
{
   qDebug() << "Hello from C++";
}

QString TouchSlider::testStringReturn()
{
    QString message = "Hi from C++";
    return message;
}

void TouchSlider::sliderSetValueFromTouch(QQuickItem *qs, int position)
{
    // Assume qs a vertical slider
    // Get its properties (its slider properties are accessible even though it is declared as QQuickItem)
    // minimumValue and maximumValue are of type QVariant so we need to cast them as double
    double minvalue = qs->property("minimumValue").value<double>();
    double maxvalue = qs->property("maximumValue").value<double>();
    // Since this is a vertical slider, by assumption, get the height
    double height = qs->property("height").value<double>();

    // Compute the new value based on position coordinate
    double newvalue = (height-position)/height * (maxvalue-minvalue);
    if (newvalue<minvalue) newvalue = minvalue;
    if (newvalue>maxvalue) newvalue = maxvalue;
    //qDebug() << newvalue;

    // Set the value of the slider
    qs->setProperty("value",newvalue);
}

TouchSlider.qml:

import QtQuick 2.0
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.2

Item {
    property string sliderTitle

    property real sliderMin
    property real sliderMax
    property real sliderVal


    ColumnLayout{
        Label {
            text: qsTr(sliderTitle)
            font.pointSize: 10

        }
        Slider {
            id: touchSlider1


            minimumValue: sliderMin
            maximumValue: sliderMax

            orientation: Qt.Vertical
            value: sliderVal
            onValueChanged: function(){
                sliderVal = Math.round(this.value);
                labelSliderValue.text = qsTr(JSON.stringify(sliderVal));
            }
        }

        Label {
            id: labelSliderValue
            text: qsTr(JSON.stringify(sliderVal))
            font.pointSize: 10
        }
        MultiPointTouchArea{
            anchors.fill: touchSlider1
            touchPoints: [
                TouchPoint {
                    id: point1
                    onPressedChanged: function(){
                        if(pressed){
                            //console.log("pressed");
                            //console.log(touchslider.testStringReturn());
                            touchslider.sliderSetValueFromTouch(touchSlider1,point1.y);

                        }
                    }

                }
            ]
            onTouchUpdated: function(){
                touchslider.sliderSetValueFromTouch(touchSlider1,point1.y);
            }
        }
    }
}

PlayerControls.qml:

import QtQuick 2.0
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.2


Item {
    // These properties act as constants, useable outside this QML file
    property string playerName
    property real priceMin
    property real priceMax
    property real qualityMin
    property real qualityMax
    property real priceValue
    property real qualityValue
    property int sliderWidth


    ColumnLayout{
        id: columnLayout1
        width: 640
        height: 480
        Layout.minimumWidth: 640
        Layout.fillWidth: true
        anchors.fill: parent
        spacing: 10.2

        Label {
            Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
            id: labelPlayer1
            text: qsTr(playerName)
            font.pointSize: 10
        }
        RowLayout{
            ColumnLayout{
                Label {
                    text: qsTr("")
                    font.pointSize: 10
                    width: 50
                }
            }
            TouchSlider {
                width: sliderWidth

                sliderTitle: "Price"
                sliderMin: priceMin
                sliderMax: priceMax
                sliderVal: priceValue

            }
            TouchSlider {
                width: sliderWidth

                sliderTitle: "Quality"
                sliderMin: qualityMin
                sliderMax: qualityMax
                sliderVal: qualityValue
            }
        }

    }
 }

main.qml:

import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: qsTr("SliderPair Test")
    Item {
        PlayerControls{
            playerName: "Player 1"
            priceMin: 0
            priceMax: 200
            priceValue: 100
            qualityMin: 0
            qualityMax: 50
            qualityValue: 25
            sliderWidth: 200
        }
    }

}

The result should look like this: Pair of Sliders Responding to Separate Touch Points

On a touch screen like my Surface Pro, I can control each slider simultaneously with two fingers. Since Windows supports up to 10 simultaneous touches that should mean I can have 2-4 players without a problem. We shall see.