[Interest] Best practices on pinch-zoomable item container with non-zoomable item attached

Alexander Ivash elderorb at gmail.com
Thu Sep 13 16:13:34 CEST 2018


Hi Shawn,

I've realized it might be faster to provide some code than try to
explain better.

Original.txt - is the code where scaling happens relatively top/left
Approach_1.txt - adjusting x/y
Approach_2.txt - scaling top level item & scaling non-zoomable to compensate

>> anchors.centerIn: parent

What I meant is rather anchors.centerIn: child (so that child would
free-zoom relatively center, and parent would center itself relatively
center of the child)
чт, 13 сент. 2018 г. в 16:15, Shawn Rutledge <Shawn.Rutledge at qt.io>:
>
>
> > On 13 Sep 2018, at 14:41, Alexander Ivash <elderorb at gmail.com> wrote:
> >
> > I need pinch-zoomable item container with non-zoomable item container
> > attached to the bottom. Like this:
> >
> > Item {
> >   id: item
> >   Item {
> >     id: zoomable
> >    }
> >
> >   Item {
> >     id: nonzoomable
> >   }
> > }
> >
> > (anchoring, geometry omitted for simplicity)
> >
> > I see at least the following solutions:
> >
> > 1. Change 'scale' of the whole item (zoomable + non-zoomable), then
> > change 'scale' of non-zoomable to compensate parent scaling. It feels
> > a bit wrong so I'd like to avoid it. Also doing in this way leads to
> > some issues with anchoring non-zoomable item to zoomable one properly.
> >
> > 2. Change 'scale' of the zoomable part only. But in this case I need
> > to adjust x/y of 'item', otherwise scaling will always be happening
> > relatively top/left (and I need relatively to the center)
>
> PinchArea only sets the scale property of the zoomable item directly, and that is relative to the Item transformOrigin which defaults to center.
>
> PinchHandler zooms relative to the centroid between the touchpoints, which is more typical behavior like what you get in most non-Qt interfaces (you need to be able to zoom into a particular geographical area of a map, or an interesting part of some drawing, for example).  It normally does that by setting the scale and the x and y properties at the same time.  But it’s not overriding anchors.centerIn: parent, so you can still use that to keep your item centered.
>
> If yours is always zooming toward the top left, it must be because of your anchors or bindings, or because of setting  transformOrigin to TopLeft.
>
> You could use another wrapper item to hold the place in the layout, and then add your zoomable item to that.  If it has no bindings or anchors, it can be free to zoom and move around within that space.  If you want to disable movement, or allow movement only within certain confines, you can try the minimumX/Y and maximumX/Y properties.
>
> > Is there better way to do it?
> >
> > If there would be a way to center parent in child - I could anchor
> > 'item' to the center of 'zoomable' and everything would work
> > magically. But such kinds of anchors are not supported.
>
> anchors.centerIn: parent
>
> What I sometimes wish we had is _bindable_ centerX/Y properties.  But at least we have anchors.
>
-------------- next part --------------
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Shapes 1.0
import Qt.labs.handlers 1.0

Item {
    id: draggableObject

    property int index: -1
    default property alias content: content.children

    signal grabChanged(var index);
    signal tapped(var index);

    width: contentContainer.width
    onWidthChanged: {
        console.debug('draggable width: ', width);
    }

    height: contentContainer.height
    onHeightChanged: {
        console.debug('draggable height: ', height);
    }

    Item {
        id: contentContainer
        width: content.width * content.scale
        property int prevWidth: width
        onWidthChanged: {
            console.debug('content width: ', width);
            draggableObject.x -= (width - prevWidth) / 2
            prevWidth = width;
        }

        height: content.height * content.scale
        property int prevHeight: height
        onHeightChanged: {
            console.debug('content height: ', height);
            draggableObject.y -= (height - prevHeight) / 2
            prevHeight = height;
        }

        DragHandler {
            onGrabChanged: {
                draggableObject.grabChanged(index);
            }
        }

        PinchHandler {
            target: content
        }

        TapHandler {
            onTapped: {
                draggableObject.tapped(index);
            }
        }

        Item {
            id: content
            anchors.centerIn: parent
            clip: true

            width: childrenRect.width
            onWidthChanged: {
                console.debug('content width: ', width);
            }

            height: childrenRect.height
            onHeightChanged: {
                console.debug('content height: ', height);
            }

            onScaleChanged: {
            }
        }

        Shape {
            anchors.centerIn: parent
            width: contentContainer.width
            height: contentContainer.height

            ShapePath {
                fillColor: 'transparent'
                strokeColor: 'green'
                strokeWidth: 4
                strokeStyle: ShapePath.DashLine
                startX: 0
                startY: 0
                dashPattern: [ 1, 4 ]
                PathLine { x: contentContainer.width; y: 0 }
                PathLine { x: contentContainer.width; y: contentContainer.height }
                PathLine { x: 0; y: contentContainer.height }
                PathLine { x: 0; y: 0 }
            }
        }

        MouseArea {
            id: mouseZoomer
            anchors.fill: parent
            hoverEnabled: false
            onPressed: {
                mouse.accepted = false
            }
            onReleased: {
                mouse.accepted = false
            }

            onWheel: {
                if(wheel.modifiers & Qt.ControlModifier) {
                    console.debug('onWheel');
                    if(wheel.angleDelta.y > 1.0) {
                        content.scale *= 1.1
                    } else {
                        content.scale /= 1.1
                    }
                } else {
                    wheel.accepted = false
                }
            }
        }
    }

    Button {
        anchors.top: contentContainer.bottom
        anchors.right: contentContainer.right

        width: 50
        height: 50
        text: 'button: ' + index
    }
}
-------------- next part --------------
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Shapes 1.0
import Qt.labs.handlers 1.0

Item {
    id: draggableObject

    property int index: -1
    default property alias content: content.children

    signal grabChanged(var index);
    signal tapped(var index);

    width: contentContainer.width
    onWidthChanged: {
        console.debug('draggable width: ', width);
    }

    height: contentContainer.height
    onHeightChanged: {
        console.debug('draggable height: ', height);
    }

    Item {
        id: contentContainer
        width: content.width * content.scale
        property int prevWidth: width
        onWidthChanged: {
            console.debug('content width: ', width);
        }

        height: content.height * content.scale
        property int prevHeight: height
        onHeightChanged: {
            console.debug('content height: ', height);
        }

        DragHandler {
            onGrabChanged: {
                draggableObject.grabChanged(index);
            }
        }

        PinchHandler {
            target: draggableObject
        }

        TapHandler {
            onTapped: {
                draggableObject.tapped(index);
            }
        }

        Item {
            id: content
            anchors.centerIn: parent
            clip: true

            width: childrenRect.width
            onWidthChanged: {
                console.debug('content width: ', width);
            }

            height: childrenRect.height
            onHeightChanged: {
                console.debug('content height: ', height);
            }

            onScaleChanged: {
            }
        }

        Shape {
            anchors.centerIn: parent
            width: contentContainer.width
            height: contentContainer.height

            ShapePath {
                fillColor: 'transparent'
                strokeColor: 'green'
                strokeWidth: 4
                strokeStyle: ShapePath.DashLine
                startX: 0
                startY: 0
                dashPattern: [ 1, 4 ]
                PathLine { x: contentContainer.width; y: 0 }
                PathLine { x: contentContainer.width; y: contentContainer.height }
                PathLine { x: 0; y: contentContainer.height }
                PathLine { x: 0; y: 0 }
            }
        }

        MouseArea {
            id: mouseZoomer
            anchors.fill: parent
            hoverEnabled: false
            onPressed: {
                mouse.accepted = false
            }
            onReleased: {
                mouse.accepted = false
            }

            onWheel: {
                if(wheel.modifiers & Qt.ControlModifier) {
                    console.debug('onWheel');
                    if(wheel.angleDelta.y > 1.0) {
                        draggableObject.scale *= 1.1
                    } else {
                        draggableObject.scale /= 1.1
                    }
                } else {
                    wheel.accepted = false
                }
            }
        }
    }

    Button {
        anchors.top: contentContainer.bottom
        anchors.right: contentContainer.right
        scale: 1 / draggableObject.scale

        width: 50
        height: 50
        text: 'button: ' + index
    }
}
-------------- next part --------------
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Shapes 1.0
import Qt.labs.handlers 1.0

Item {
    id: draggableObject

    property int index: -1
    default property alias content: content.children

    signal grabChanged(var index);
    signal tapped(var index);

    width: contentContainer.width
    onWidthChanged: {
        console.debug('draggable width: ', width);
    }

    height: contentContainer.height
    onHeightChanged: {
        console.debug('draggable height: ', height);
    }

    Item {
        id: contentContainer
        width: content.width * content.scale

        height: content.height * content.scale

        DragHandler {
            onGrabChanged: {
                draggableObject.grabChanged(index);
            }
        }

        PinchHandler {
            target: content
        }

        TapHandler {
            onTapped: {
                draggableObject.tapped(index);
            }
        }

        Item {
            id: content
            anchors.centerIn: parent
            clip: true

            width: childrenRect.width
            onWidthChanged: {
                console.debug('content width: ', width);
            }

            height: childrenRect.height
            onHeightChanged: {
                console.debug('content height: ', height);
            }

            onScaleChanged: {
            }
        }

        Shape {
            anchors.centerIn: parent
            width: contentContainer.width
            height: contentContainer.height

            ShapePath {
                fillColor: 'transparent'
                strokeColor: 'green'
                strokeWidth: 4
                strokeStyle: ShapePath.DashLine
                startX: 0
                startY: 0
                dashPattern: [ 1, 4 ]
                PathLine { x: contentContainer.width; y: 0 }
                PathLine { x: contentContainer.width; y: contentContainer.height }
                PathLine { x: 0; y: contentContainer.height }
                PathLine { x: 0; y: 0 }
            }
        }

        MouseArea {
            id: mouseZoomer
            anchors.fill: parent
            hoverEnabled: false
            onPressed: {
                mouse.accepted = false
            }
            onReleased: {
                mouse.accepted = false
            }

            onWheel: {
                if(wheel.modifiers & Qt.ControlModifier) {
                    console.debug('onWheel');
                    if(wheel.angleDelta.y > 1.0) {
                        content.scale *= 1.1
                    } else {
                        content.scale /= 1.1
                    }
                } else {
                    wheel.accepted = false
                }
            }
        }
    }

    Button {
        anchors.top: contentContainer.bottom
        anchors.right: contentContainer.right

        width: 50
        height: 50
        text: 'button: ' + index
    }
}


More information about the Interest mailing list