qtqmlflickablemousearea

Get mouse x & y in nested MouseArea with hoverEnabled


I made a simplified MapImage component which allows to zoom and pan an image with the mouse. This component uses Flickable and MouseArea components. The MapImage component just handles image display, zooming and panning. I want to use another MouseArea in the MapImage instance in main.qml (to be able to place objects using a Canvas but this is not important here). This is not the job of MapImage, so I really need this second MouseArea component.

I need to set the hoverEnabled property to true because I need onPositionChanged and others events... But this property seems to cause problems with mouseX and mouseY values taken from my updateFlickable function. When I'm zooming with the mouse wheel, zoom does not occur at the mouse position...

I've made a minimal example available here or in a gist.

Any hint to solve this?

main.qml

import QtQml.Models 2.11
import QtQuick 2.11
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.11

MapImage {
    id: map
    height: 600
    width: 800

    imageSource: "https://images.unsplash.com/photo-1651634099253-720df02a0d50"

    MouseArea {
        anchors.fill: parent
        acceptedButtons: Qt.LeftButton
        hoverEnabled: true // this is required to use onPositionChanged
        preventStealing: false

        onPressed: {
            // needed for flickable
            mouse.accepted = false;
        }

        onPositionChanged: {
            // do something.
        }

    }
}

MapImage.qml

import QtQuick 2.11

Item {
    id: root
    property alias imageSource: image.source

    Flickable {
        id: flickable
        anchors.fill: parent
        contentWidth: props.originalImageWidth
        contentHeight: props.originalImageHeight

        Image {
            id: image
            fillMode: Image.PreserveAspectFit

            onStatusChanged: {
                if (status === Image.Ready) {
                    props.originalImageWidth = sourceSize.width;
                    props.originalImageHeight = sourceSize.height;
                    props.changeCurrentScale(1);
                }
            }

            // define the image display size
            width: flickable.contentWidth;
            height: flickable.contentHeight;

            MouseArea {
                id: mouseArea
                anchors.fill: parent
                acceptedButtons: Qt.LeftButton
                hoverEnabled: true

                onWheel: {
                    wheel.accepted = false;
                    props.changeCurrentScale(wheel.angleDelta.y);
                }
            }
        }

        QtObject {
            id: props

            // original image size
            property int originalImageWidth
            property int originalImageHeight

            property real scaleStep: 0.2
            property real currentScale: 0.1

            onCurrentScaleChanged: updateFlickable(currentScale);

            function updateFlickable(scale) {
                console.log(mouseArea.mouseX, mouseArea.mouseY); // <------ I am no longer able to get mouse x and y coordinates
                flickable.resizeContent(originalImageWidth * scale, originalImageHeight * scale, Qt.point(mouseArea.mouseX, mouseArea.mouseY));
                flickable.returnToBounds();
            }

            function changeCurrentScale(wheelDelta) {
                if (wheelDelta > 0) currentScale = currentScale * (1 + scaleStep);
                else currentScale = currentScale / (1 + scaleStep);
            }
        }
    }
}

Solution

  • Finally found a solution. I had to add a new property in my MapImage component. This property role is to store the updated position of the mouse in the parent mouse area in the parent coordinate system. After that, I have to use mapToItem to convert in the flickable.contentItem coordinate system.

    main.qml

    import QtQml.Models 2.11
    import QtQuick 2.11
    import QtQuick.Controls 2.4
    import QtQuick.Layouts 1.11
    
    MapImage {
        id: map
        height: 600
        width: 800
    
        imageSource: "https://images.unsplash.com/photo-1651634099253-720df02a0d50"
    
        MouseArea {
            anchors.fill: parent
            acceptedButtons: Qt.LeftButton
            hoverEnabled: true // this is required to use onPositionChanged
            preventStealing: false
    
            onPressed: {
                // needed for flickable
                mouse.accepted = false;
            }
    
            onPositionChanged: {
                // CHANGE HERE
                // the position must be updated on every position change
                map.parentMouseAreaPosition = Qt.point(mouse.x, mouse.y);
                // TO HERE
            }
        }
    }
    

    MapImage.qml

    import QtQuick 2.11
    
    Item {
        id: root
        property alias imageSource: image.source
    
        // CHANGE HERE
        // the current mouse position in the parent mouse area in parent coordinate system
        property var parentMouseAreaPosition: Qt.point(0, 0)
    
        // this function maps the parent coordinate system to that of contentItem
        function __mapToContentItem(x, y) {
            return mapToItem(flickable.contentItem, x, y);
        }
        // TO HERE
    
        Flickable {
            id: flickable
            anchors.fill: parent
            contentWidth: props.originalImageWidth
            contentHeight: props.originalImageHeight
    
            Image {
                id: image
                fillMode: Image.PreserveAspectFit
    
                onStatusChanged: {
                    if (status === Image.Ready) {
                        props.originalImageWidth = sourceSize.width;
                        props.originalImageHeight = sourceSize.height;
                        props.changeCurrentScale(1);
                    }
                }
    
                // define the image display size
                width: flickable.contentWidth;
                height: flickable.contentHeight;
    
                MouseArea {
                    id: mouseArea
                    anchors.fill: parent
                    acceptedButtons: Qt.LeftButton
                    hoverEnabled: true
    
                    onWheel: {
                        wheel.accepted = false;
                        props.changeCurrentScale(wheel.angleDelta.y);
                    }
                }
            }
    
            QtObject {
                id: props
    
                // original image size
                property int originalImageWidth
                property int originalImageHeight
    
                property real scaleStep: 0.2
                property real currentScale: 0.1
    
                onCurrentScaleChanged: updateFlickable(currentScale);
    
                function updateFlickable(scale) {
                    // CHANGE HERE
                    // get the mapped point
                    let point = __mapToContentItem(root.parentMouseAreaPosition.x, root.parentMouseAreaPosition.y);
                    console.log(point.x, point.y);
                    flickable.resizeContent(originalImageWidth * scale, originalImageHeight * scale, point);
                    // TO HERE
                    flickable.returnToBounds();
                }
    
                function changeCurrentScale(wheelDelta) {
                    if (wheelDelta > 0) currentScale = currentScale * (1 + scaleStep);
                    else currentScale = currentScale / (1 + scaleStep);
                }
            }
        }
    }
    

    I don't know if there is a better solution.