Search code examples
qmlqtquick2qtquickcontrols

How to restrict the mouseArea of a QML Slider to the handle only?


I want to implement a Slider using QtQuick.Controls where only the handle is clickable and can be used to drag the handle. If you click the groove, nothing should happen, the handle should stay where it is. How can I restrict the mouseArea of the Slider to the handle only?

In the below example the Slider is clickable on the whole Slider width and height:

import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Controls 1.3
import QtQuick.Controls.Styles 1.3

Window {
    id: mainItem
    width: 800
    height: 400
    visible: true

    Slider{
        id: autoSlider
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.verticalCenter: parent.verticalCenter
        maximumValue: 1.0
        value: 0
        updateValueWhileDragging : false

        style: SliderStyle {
            groove: Rectangle {
                implicitWidth: 350
                implicitHeight: 8
                color: "gray"
                radius: 8
            }
            handle: Rectangle {
                anchors.centerIn: parent
                color: control.pressed ? "white" : "lightgray"
                border.color: "gray"
                border.width: 2
                implicitWidth: 45
                implicitHeight: 45
                radius: 12
            }
        }
    }
}

I thought about changing the Slider.qml template in the "..\qml\QtQuick\Controls" folder, but I couldn't really figure out what to do.

All my search efforts lead to nothing. Any help would be greatly appreciated.


Solution

  • I could be wrong, but I think that this goes against the behaviour of sliders on all Desktop platforms, which is something to consider for usability reasons. That aside, let's try:

    diff --git a/src/controls/Slider.qml b/src/controls/Slider.qml
    index 20d1102..da6f051 100644
    --- a/src/controls/Slider.qml
    +++ b/src/controls/Slider.qml
    @@ -258,7 +258,7 @@ Control {
             }
    
             onPositionChanged: {
    -            if (pressed)
    +            if (pressed && handleHovered)
                     updateHandlePosition(mouse, preventStealing)
    

    We don't want the handle position to change when the handle wasn't hovered to begin with, because that would mean the mouse is outside the handle.

                 var point = mouseArea.mapToItem(fakeHandle, mouse.x, mouse.y)
    @@ -272,18 +272,21 @@ Control {
                 if (handleHovered) {
                     var point = mouseArea.mapToItem(fakeHandle, mouse.x, mouse.y)
                     clickOffset = __horizontal ? fakeHandle.width/2 - point.x : fakeHandle.height/2 - point.y
    +
    +                pressX = mouse.x
    +                pressY = mouse.y
    +                updateHandlePosition(mouse, !Settings.hasTouchScreen)
                 }
    -            pressX = mouse.x
    -            pressY = mouse.y
    -            updateHandlePosition(mouse, !Settings.hasTouchScreen)
             }
    

    Next, move the handle position update within the if (handleHovered) condition of the onPressed handle.

             onReleased: {
    -            updateHandlePosition(mouse, Settings.hasTouchScreen)
    -            // If we don't update while dragging, this is the only
    -            // moment that the range is updated.
    -            if (!slider.updateValueWhileDragging)
    -                range.position = __horizontal ? fakeHandle.x : fakeHandle.y;
    +            if (handleHovered) {
    +                updateHandlePosition(mouse, Settings.hasTouchScreen)
    +                // If we don't update while dragging, this is the only
    +                // moment that the range is updated.
    +                if (!slider.updateValueWhileDragging)
    +                    range.position = __horizontal ? fakeHandle.x : fakeHandle.y;
    +            }
                 clickOffset = 0
                 preventStealing = false
             }
    

    Finally, make a similar change to the onReleased handler.

    This will get you what you want, albeit a little rough around the edges — literally. I think the interaction is not 100%, but try it out. With a little more experimenting, you should be able to get it working perfectly.

    Another hacky option is to modify Slider.qml to have a MouseArea either side of the handle, above the slider itself. Each of these would fill the available space on either side of the handle, effectively stealing all mouse events the groove would receive for those areas.