How to pan a QML item on MouseArea drag - qt

I have a Qt app which runs on iOS and OSX using Qt 5.10 commercial version. I have a QML item which hosts an image. I am trying to pan the QML item when user's finger drags on it OR mouse is dragged.
Following is somewhat I am trying to make my QML item pannable:
Code:
MyQmlItem {
id: my_qml_item
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
onXChanged: {
if (my_qml_item_mouse_area.drag.active) {
console.log("x = " + x)
my_qml_item.x = // what to set x here to move my_qml_item wrt finger or mouse pressed movement
}
}
onYChanged: {
if (my_qml_item_mouse_area.drag.active) {
console.log("y = " + y)
my_qml_item.y = // what to set y here to move my_qml_item wrt finger or mouse pressed movement
}
}
MouseArea {
id: my_qml_item_mouse_area
anchors.fill: parent
drag {
id: drag_area
target: my_qml_item
axis: Drag.XandYAxis
}
}
}
I understand that I have to update the x and y position of MyQmlItem when onXChanged and onYChanged is active and x y are getting updated. But I am struggling to figure how I should re-calculate the new my_qml_item.x and my_qml_item.y
Question:
I am getting x and y updates on onXChanged and onYChanged as well. The basic question is, how to calculate plus continuously update my_qml_item.x and my_qml_item.y.
Are there any good examples of Qt/QML for panning or dragging a QML item?
Is there some way to replicate the following anchors by only setting default x and y? Because, it falls in direct conflict with dragging the QML component
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter

Anchors should not be used if you wish to drag as it links some part of the geometry of the items.
In your case, you only need to establish the specific positions at certain times, such as when the application starts, so instead of setting the anchors you could use the properties "x", "y", "width" and "height" .
import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Controls 2.2
ApplicationWindow {
id: app
visible: true
visibility: "FullScreen"
title: qsTr("Scroll")
function resetPosition(){
item.x = Screen.orientation === Qt.PortraitOrientation ? (Screen.width - item.width)/2 : (Screen.height - item.height)/2
item.y = 0
}
Image {
id: item
source: "http://doc.qt.io/qt-5/images/declarative-qtlogo.png"
onStatusChanged: {
if(status == Image.Ready)
resetPosition()
}
MouseArea{
anchors.fill: parent
drag.target: item
drag.axis: Drag.XAndYAxis
onClicked: resetPosition()
}
}
property bool isPortrait: Screen.primaryOrientation === Qt.PortraitOrientation || Screen.primaryOrientation === Qt.InvertedPortraitOrientation
property bool isLandscape: Screen.primaryOrientation === Qt.LandscapeOrientation || Screen.primaryOrientation === Qt.InvertedLandscapeOrientation
onIsPortraitChanged: resetPosition()
onIsLandscapeChanged: resetPosition()
}

Related

How to make an Item draggable without flicker when parent's position changes during drag

I'm trying to make an item that can be resized by its edges.
For showing a minimal testcase of the problem it is enough to have its left edge draggable, so here it is:
Rectangle {
id: root
border.width: 1
border.color: 'black'
color: 'red'
// save original position and size at drag start
property real origX: 0
property real origWidth: 0
// drag this item:
Item {
id: dragDummy
x: 0
onXChanged: {
root.x = root.origX + x
root.width = root.origWidth - x
}
}
MouseArea {
anchors.fill: root
drag.target: dragDummy
drag.axis: Drag.XAxis
drag.onActiveChanged: {
// onDragStarted -> Cannot assign to non-existent property "onDragStarted" ???
if(!active) return
root.origX = root.x
root.origWidth = root.width
}
}
}
the problem seems to be that if drag causes parent position to change, that triggers another drag event, causing this flicker:
I'm guessing MouseArea can't help here? Then low level mouse events should be used like in "old-school" apps (i.e. capturing events at root Item, manually compute offset with respect to initial mouse down position, etc...)?
(or I have to move the MouseArea to an ancestor that won't move during drag, which is almost the same...)
There is a nice QML Item type called DragHandler which people often overlook, but I find that it works very well.
This solution is a little more idiomatic than other suggestions in that it uses a declarative style rather than imperative:
import QtQuick 2.15
Item {
id: root
width: 500
height: 100
Item {
height: 100
width: handle.x + handle.width / 2
}
Rectangle {
x: handle.x + handle.width / 2
width: root.width - (handle.x - handle.width/2)
height: 100
border{
width: 1
color: 'black'
}
color: 'red'
}
Item {
id: handle
x: -width / 2
width: 50
height: 100
DragHandler {
yAxis.enabled: false
xAxis{
minimum: -handle.width
maximum: root.width
}
}
}
}
The solution I come up with consists of having two MouseAreas:
a MouseArea moves with the item to drag, that is used only for hit-testing, so its onPressed handler is something like this:
onPressed: (mouse) => {
mouse.accepted = false
root.container.myDragTarget = root
}
onReleased: root.container.myDragTarget = null
another MouseArea, stacked below the others and not moving, handles the mouse position change and the dragging:
onPressed: _start = Qt.point(mouseX, mouseY)
onPositionChanged: {
if(myDragTarget) {
var delta = Qt.point(mouseX - _start.x, mouseY - _start.y)
// do any rounding/snapping of delta here...
_start.x += delta.x
_start.y += delta.y
myDragTarget.x += delta.x
myDragTarget.y += delta.y
}
}
This is able to drag the item reliably.
This is also what I wanted to avoid, because it reinvents mouse drag, but in absence of a better solution it is what I am going to use.
I won't accept this answer as I'm curious to see other ways to approach this problem.
You can workaround the movement and new positioning of the dragged Item by mapping the coordinates with the mapToItem functions.
In my solution, I've not used the drag functionality of the MouseArea as it needs a drag.target. I've used the pressed and position changed signals to implement drag behavior. The only downside is the background Item which is needed for the mapToItem function as it doesn't accept the Window due to it not being an Item.
import QtQuick
import QtQuick.Window
import QtQuick.Shapes
Window {
id: root
visible: true
width: 400
height: 400
Item {
id: background
anchors.fill: parent
Rectangle {
id: rectangle
property int rightX
x: 50
y: 50
width: 200
height: 80
border.width: 1
border.color: "black"
color: "red"
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 10
color: mouseArea.containsMouse || mouseArea.pressed ? "#ff808080" : "#aa808080"
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPressed: rectangle.rightX = rectangle.x + rectangle.width
onPositionChanged: function(mouse) {
if (mouseArea.pressed) {
var tmp = mouseArea.mapToItem(background, mouse.x, 0)
if (tmp.x <= rectangle.rightX)
rectangle.x = tmp.x
else
rectangle.x = rectangle.rightX
rectangle.width = rectangle.rightX - rectangle.x
}
}
}
}
}
}
}

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);
}
}
}
}
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.

QML SplitView auto collapse on handlebar mouse release

I have a QML Controls 2 SplitView and a redefined handle, which works well, but I want detect mouse release event on the handler, so I could collapse the SplitView under a certain threshold of width. Adding a MouseArea on top of the existing handle will absorb drag events, so I'm unable to move the handlebar. Any idea how could I gather the mouse release event, or any other solution which solves this problem?
Alright, I have created an example application. As you can see in this example, my MouseArea is marked with yellow and collapses the right view programmatically when double clicked, which is nice, but I also want to drag the handlebar and upon mouse release under a certain width threshold I want to collapse the view as well. The black part of the handlebar where my MouseArea is not covering the handlebar, responds to drag, but since there is no signal I can gather from it, the width threshold already set shouldCollapse boolean property, so the view won't update. Probably I could solve this issue with a timer, but I need a more sophisticated solution.
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
Window {
width: 800
height: 400
visible: true
SplitView {
id: splitView
anchors.fill: parent
orientation: Qt.Horizontal
function toggleCollapse() { collapsibleRect.shouldCollapse = !collapsibleRect.shouldCollapse }
handle: Rectangle {
implicitWidth: 20
implicitHeight: 20
color: "black"
MouseArea {
anchors.centerIn: parent
width: parent.width
height: parent.height / 2
onDoubleClicked: splitView.toggleCollapse()
Rectangle {
anchors.fill: parent
color: "yellow"
Text {
anchors.centerIn: parent
text: "Double click to collapse"
rotation: 90
}
}
}
}
Rectangle {
id: mainRect
color: "green"
SplitView.fillWidth: true
Text {
anchors.centerIn: parent
font.pixelSize: 24
text: "Main scene"
}
}
Rectangle {
id: collapsibleRect
property bool shouldCollapse: false
SplitView.preferredWidth: shouldCollapse ? 0 : 300
color: "purple"
clip: true
onWidthChanged: {
if(width < 200) shouldCollapse = true
else shouldCollapse = false
}
Text {
anchors.centerIn: parent
rotation: parent.shouldCollapse ? 90 : 0
font.pixelSize: 24
text: parent.shouldCollapse ? "SHOULD BE COLLAPSED" : "NOT COLLAPSED"
Behavior on rotation { NumberAnimation { duration: 100 } }
}
}
}
}
I had a similar problem and was able to solve it thanks to the hint of #Ponzifex that the SplitView's resizing property will be set to true as soon as the handle is clicked. Using a Timer I managed to detect whether the handle was quickly pressed twice in a row.
SplitView {
id: view
...
handle: Rectangle {
...
}
//============================================================
// double click behavior
Timer {
id: doubleClickTimer
interval: 300 // number of ms between clicks that should be considered a double click
}
property bool doubleClicked: false
// `resizing` will be set to true even if the handle is just pressed
onResizingChanged: {
if (view.resizing) {
if (!doubleClickTimer.running) {
doubleClickTimer.start();
return;
}
view.doubleClicked = true;
} else {
if (view.doubleClicked) {
// do any manual resizing in here
view.doubleClicked = false;
}
}
}
}
It is important to note, however, that it is only possible to resize the contents of a SplitView when resizing is false. That's why I need to have the doubleClicked helper property.
Add this to MouseArea:
onPressed: {
mouse.accepted = (mouse.flags & Qt.MouseEventCreatedDoubleClick);
}
propagateComposedEvents: true
cursorShape: Qt.SplitHCursor

How can I dynamically draw a polygon and make its points/markers movable in QML?

I have came across a problem where I have to dynamically draw a polygon on QML Map using mouse and make its points movable so that user can change those points location. There is a very nice answer to a similar question which helped me to at least add some points/markers dynamically and connect them through lines but it doesn't allow the markers to be movable.
Can somebody please help me in this regard?
In the following code a marker will be added with the right click and you can drag a marker with the right click.
The logic of adding is simple is to detect the right click of the mouse and obtain with that information the position by adding it to the model associated with the MapItemView that handles the markers and the MapPolygon points.
On the other hand, the logic of the drag is first to detect without a marker has been pressed so that a MouseArea attached to each marker is used obtaining the index of that element, disabling the "gesture" of the map. The MouseArea of the markers was configured so that they continue to propagate the mouse events to the other elements since the detection of the release must be done on the map, for this the positionChanged and Released signals are used with which the position of the marker is updated and restore the variables when necessary.
import QtQuick 2.14
import QtQuick.Window 2.14
import QtLocation 5.14
import QtPositioning 5.14
Window {
visible: true
width: 640
height: 480
property int currentIndex: -1
ListModel{
id: polygonmodel
}
Map {
id: map
anchors.fill: parent
plugin: Plugin {
name: "osm"
}
gesture.enabled: currentIndex == -1
center: QtPositioning.coordinate(59.91, 10.75) // Oslo
zoomLevel: 14
MapItemView{
z: polygon.z + 1
model: polygonmodel
delegate: MapQuickItem{
anchorPoint: Qt.point(sourceItem.width/2, sourceItem.height/2)
coordinate: QtPositioning.coordinate(model.coords.latitude, model.coords.longitude)
sourceItem: Image {
width: 40
height: 40
source: "http://maps.gstatic.com/mapfiles/ridefinder-images/mm_20_red.png"
MouseArea{
anchors.fill: parent
acceptedButtons: Qt.LeftButton
propagateComposedEvents: true
onPressed: {
currentIndex = index
mouse.accepted = false
}
}
}
}
}
MapPolygon{
id: polygon
border.color: "green"
border.width: 10
}
MouseArea{
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
var point = Qt.point(mouse.x, mouse.y)
var coord = map.toCoordinate(point);
if (mouse.button == Qt.RightButton)
addMarker(coord)
}
onPositionChanged: {
if (currentIndex != -1){
var point = Qt.point(mouse.x, mouse.y)
var coord = map.toCoordinate(point);
if(coord.isValid)
moveMarker(currentIndex, coord)
}
}
onReleased: {
if (mouse.button == Qt.LeftButton && currentIndex != -1){
var point = Qt.point(mouse.x, mouse.y)
var coord = map.toCoordinate(point);
if(coord.isValid)
moveMarker(currentIndex, coord)
currentIndex = -1;
}
}
}
}
function moveMarker(index, coordinate){
polygonmodel.set(index, {"coords": coordinate})
var path = polygon.path;
path[index] = coordinate
polygon.path = path
}
function addMarker(coordinate){
polygonmodel.append({"coords": coordinate})
polygon.addCoordinate(coordinate)
}
}

How to recreate Android's Pull to Refresh icon in QML?

This is the pull to refresh icon used to refresh views in Android.
I've been trying to bring that to qml but it is not so easy.
There are so many transitions that it quickly becomes very complex.
How difficult this should be to recreated in QML?
Is using canvas the better solution?
As i have first seen, the swipe brings down the arrow in a different pace of the swipe, while the arrow rotates. If this arrow comes from a canvas how can it relate to outside events, that is the swipe?
I used something like this:
//
// Slot called when the flick has started
//
onFlickStarted: {
refreshFlik = atYBeginning
}
//
// Slot called when the flick has finished
//
onFlickEnded: {
if ( atYBeginning && refreshFlik )
{
refresh()
}
}
It seems to work as expected and it is easy to implement
The problem is that Flickable and the derived ListView don't really provide any over-drag or over-shoot information in the cases where the visual behavior is disabled.
If dragging the visual over the beginning is not a problem for you, you can simply use the negated value of contentY which goes into the negative if the view is dragged before its beginning.
The only solution I can think of to not have any visual over-dragging but still get the over-drag information in order to drive your refresher is to set the view interactive property to false, and put another mouse area on top of that, and redirect drags and flicks manually to the now non-interactive view.
That last part might sound complex, but it isn't that complex, and I happen to know for a fact that it works well, because I have already used this approach and the source code is already here on SO.
So once you have access to the mouse area that controls the view, you can track how much you are in the negative, and use that information to drive the logic and animation of the refresher.
The notable difference between the implementation in the linked answer and what you need is that the linked answer has the mouse area in each delegate, due to the requirements of the specific problem I wanted to solve. You don't need that, you only need one single mouse area that covers the view.
I did like this recently.
Basically I use the position of a ScrollBar and if it goes negative I show a spinner and refresh. So I don't need to mess with the flick stuff.
import QtQuick.Controls 6.0
import QtQuick 6.0
ListView {
ScrollBar.vertical: ScrollBar {
id: scrollbar
}
property bool negativescroll: scrollbar.position < 0
onNegativescrollChanged: {
if (spinner.visible) {
refresh()
}
spinner.visible = !spinner.visible
}
BusyIndicator {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
visible: false
running: visible
id: spinner
}
width: 180; height: 200
model: model
delegate: Text {
text: name + ": " + number
}
ListModel {
id: model
ListElement {
name: "Bill Smith"
number: "555 3264"
}
ListElement {
name: "John Brown"
number: "555 8426"
}
ListElement {
name: "Sam Wise"
number: "555 0473"
}
}
}
I came to a simpler solution based on dtech's experience involving multiple Flickable elements, which basically consists on filling the Flickable with a MouseArea, setting its boundsBehavior property to Flickable.StopAtBounds, and from there, if it's at the top, do things based on mouseY values.
The better approximation i could get is in the following code. A possible drawback is that diagonal swiping also counts as a refresh intention. It could be improved with GestureArea, but i'm too lazy to get my hands on this at the moment.
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Window 2.2
ApplicationWindow {
property real mm: Screen.pixelDensity
property real margins: 2 * mm
id: mainWindow
visible: true
width: 60 * mm
height: 120 * mm
title: qsTr("Hello World")
ListModel {
id: myModel
Component.onCompleted: {
for(var i = 0; i <= 100; ++i) {
myModel.append({num: i})
}
}
}
ListView {
id: view
boundsBehavior: Flickable.StopAtBounds
interactive: true
anchors.fill: parent
model: myModel
spacing: 4
delegate: Rectangle {
width: parent.width
height: 25 * mm
border.color: 'red'
Text {
id: name
text: num
anchors.centerIn: parent
}
}
Rectangle {
signal follow
id: swatch
width: 15 * mm
height: width
radius: width / 2
color: 'lightgray'
anchors.horizontalCenter: parent.horizontalCenter
y: - height
}
MouseArea {
property int mouseYSart
property int biggerMouseY
anchors.fill: view
onPressed: {
mouseYSart = mouseY
biggerMouseY = 0
}
onMouseYChanged: {
if(view.contentY == 0) {
var currentMouseY = mouseY
if(currentMouseY > biggerMouseY) {
biggerMouseY = currentMouseY
swatch.y += 1
}
if(currentMouseY < biggerMouseY) {
biggerMouseY = currentMouseY
swatch.y -= 1
}
}
}
onReleased: swatch.y = - swatch.height
}
}
}

Resources