How can I propagate hover or mousearea update events to lower elements in QML? - qt

I have some sibling Rectangle elements with a radius property so they appear as circles. Each has a child Item that has a child MouseArea, the purpose of the Item being to implement a "round mouse area" effect (original SO answer). The Item and MouseArea are instrumented such that clicks and drags will only take effect within the visible circular shape of the Rectangle, not within the bounding box that is the real footprint of the Rectangle.
Unfortunately there is a glitch illustrated below. The desired outcome when dragging at the dot is for circle 1 to move, and this happens in most circumstances. However, it does not happen when you create create circle 1 then circle 2 then move your mouse cursor to the dot. If you do that and attempt to drag or click, your interaction will fall through to the background full-window MouseArea and create a new circle.
The cause of this problem is that when the mouse cursor moves to the dot from circle #2, the mouseX and mouseY for circle #1's MouseArea do not get updated. When circle #2 allows the click to propagate downward, it hits the Rectangle of circle #1 but then circle #1's Item claims containsMouse is false and it propagates downward again.
As soon as the mouse cursor leaves the footprint of circle #2's bounding rectangle, such as by moving a bit up or left from the dot, circle #1's MouseArea gets updated and its containsMouse becomes true and it starts capturing clicks and drags again.
I have tried a handful of potential solutions and not gotten much farther than the code below.
import QtQuick 2.12
import QtQuick.Controls 2.5
ApplicationWindow {
visible: true
width: 640
height: 480
property real spotlightRadius: 100
MouseArea {
visible: true
anchors.fill: parent
onClicked: {
spotlightComponent.createObject(parent, {
"x": x + mouseX - spotlightRadius,
"y": y + mouseY - spotlightRadius,
"width": spotlightRadius * 2,
"height": spotlightRadius * 2
})
}
}
Component {
id: spotlightComponent
Rectangle {
id: spotlightCircle
visible: true
x: parent.x
y: parent.y
width: parent.width
height: parent.height
radius: Math.max(parent.width, parent.height) / 2
color: Qt.rgba(Math.random()*0.5+0.5,Math.random()*0.5+0.5,Math.random()*0.5+0.5,0.5);
Item {
anchors.fill: parent
drag.target: parent
onDoubleclicked: parent.destroy()
onWheel: { parent.z += wheel.pixelDelta.y; currentSpotlight = parent }
property alias drag: mouseArea.drag
//FIXME when moving the mouse out of a higher element's containsMouse circle
// but still inside its mouseArea.containsMouse square, lower elements'
// mouseArea do not update, so their containsMouse doesn't update, so clicks
// fall through when they should not.
property bool containsMouse: {
var x1 = width / 2;
var y1 = height / 2;
var x2 = mouseArea.mouseX;
var y2 = mouseArea.mouseY;
var deltax = x1 - x2;
var deltay = y1 - y2;
var distance2 = deltax * deltax + deltay * deltay;
var radius2 = Math.pow(Math.min(width, height) / 2, 2);
return distance2 < radius2;
}
signal clicked(var mouse)
signal doubleclicked(var mouse)
signal wheel(var wheel)
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
//FIXME without acceptedButtons, propagated un-accepted clicks end up with the wrong coordinates
acceptedButtons: parent.containsMouse ? Qt.LeftButton : Qt.NoButton
propagateComposedEvents: true
onClicked: { if (parent.containsMouse) { parent.clicked(mouse) } else { mouse.accepted = false } }
onDoubleClicked: { if (parent.containsMouse) { parent.doubleclicked(mouse) } }
onWheel: { if (parent.containsMouse) { parent.wheel(wheel) } }
drag.filterChildren: true
}
}
}
}
}

This is not the exact solution for your problem, but this is how I overcame the root of the issue.
In my application there is a MouseArea that overlaps a large chunk of the scene which is a QQuickFrameBufferObject. This is where I draw the 3D scene. Since you cannot propagate a QHoverEvent in QML, you will have to catch the position changed signal using the onPositionChanged handler and invoke a method in C++ which will send a QHoverEvent to the required items.
QML:
MouseArea {
onPositionChanged: {
model.sendHoverEvent(Qt.point(mouse.x, mouse.y))
}
}
C++:
class TreeViewModel : public QAbstractListModel
{
// ...
void TreeViewModel::sendHoverEvent(QPointF p) {
QHoverEvent hoverEvent(QEvent::HoverMove, p, p);
QApplication::sendEvent(mApplication.graphicsLayer(), &hoverEvent);
}
};

I think that using HoverHandler instead of MouseArea can give the desired result in this case because they stack as you'd expect i.e. they are full "transparent" for mouse movement, nor are they blocked by any MouseArea that happens to be on top.

You need to reject the pressed event of your underlying MouseArea. It should be enough to solve your problems. If the pressed event is rejected, the click will automatically be forwarded to the underlying sibling items. propagateComposedEvents and filterChildren are useless in your case.
Note that if the wheel event causes the z coordinate of your spotlightCircle to become less than 0, it will no longer accept mouse event since they will be caught by the "Creation" MouseArea
import QtQuick 2.10
import QtQuick.Controls 2.3
ApplicationWindow {
visible: true
width: 640
height: 480
property real spotlightRadius: 100
MouseArea {
visible: true
anchors.fill: parent
onClicked: {
spotlightComponent.createObject(parent, {
"x": x + mouseX - spotlightRadius,
"y": y + mouseY - spotlightRadius,
"width": spotlightRadius * 2,
"height": spotlightRadius * 2
})
}
}
Component {
id: spotlightComponent
Rectangle {
id: spotlightCircle
visible: true
x: parent.x
y: parent.y
width: parent.width
height: parent.height
radius: Math.max(parent.width, parent.height) / 2
color: Qt.rgba(Math.random()*0.5+0.5,Math.random()*0.5+0.5,Math.random()*0.5+0.5,0.5);
Item {
anchors.fill: parent
onDoubleClicked: parent.destroy()
onWheel: { parent.z += wheel.pixelDelta.y; currentSpotlight = parent }
signal clicked(var mouse)
signal pressed(var mouse)
signal doubleClicked(var mouse)
signal wheel(var wheel)
property alias drag: mouseArea.drag
property bool containsMouse: {
var x1 = width / 2;
var y1 = height / 2;
var x2 = mouseArea.mouseX;
var y2 = mouseArea.mouseY;
var deltax = x1 - x2;
var deltay = y1 - y2;
var distance2 = deltax * deltax + deltay * deltay;
var radius2 = Math.pow(Math.min(width, height) / 2, 2);
return distance2 < radius2;
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
drag.target: spotlightCircle
onPressed: { if (parent.containsMouse) { parent.pressed(mouse) } else { mouse.accepted = false } }
onClicked: { if (parent.containsMouse) { parent.clicked(mouse) } else { mouse.accepted = false } }
onDoubleClicked: { if (containsMouse2) { parent.doubleClicked(mouse) } }
onWheel: { if (parent.containsMouse) { parent.wheel(wheel) } }
}
}
}
}
}

Related

In QML, how do I calculate the step size for a scroll bar such that the increase()/decrease() will only scroll to the next out-of-view item?

Pretty much included in the title. I have a ListView of some graphic items (100 - 10000+ items depending on the list model) that have specific heights/widths.
As it is right now, pgUp and pgDn will skip too many items in the list. How can I calculate the stepSize so that it will scroll to the next unseen item?
So far I've tried setting the index manually by calculating the number of entries in the view, which works somewhat, but it's not exactly the functionality I want.
I'm expecting it to be resizable, but always be able to pgUp/Dn to the next unseen entry.
You can use contentY to implement pgDn and pgUp by incrementing or decrementing in pixel units. The stepSize is simply, the visible height of the ListView:
function pgDn() {
listView.contentY += listView.height;
}
function pgUp() {
listView.contentY -= listView.height;
}
If you add bounds checking and updating the highlighted item, the above functions change to:
function pgDn() {
listView.contentY = Math.min(listView.contentY + listView.height, listView.contentHeight - listView.height);
listView.currentIndex = listView.indexAt(0, listView.contentY);
}
function pgUp() {
listView.contentY = Math.max(listView.contentY - listView.height, 0);
listView.currentIndex = listView.indexAt(0, listView.contentY);
}
Here's a sample that creates 50 items of random height. PgUp and PgDn buttons are at the bottom that help navigates the ListView and change the selected item. The delegates are clickable which allows for manual selection as well as setting input focus so keyboard presses will work.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
ListView {
id: listView
anchors.fill: parent
model: listModel
delegate: ItemDelegate {
width: ListView.view.width - 20
height: h
text: txt
background: Rectangle {
color: index & 1 ? "#eee" : "#ddd"
border.color: "grey"
}
onClicked: {
listView.currentIndex = index;
listView.forceActiveFocus();
}
}
ScrollBar.vertical: ScrollBar {
width: 20
policy: ScrollBar.AlwaysOn
}
clip: true
highlight: Rectangle {
border.color: "red"
color: "transparent"
width: 2
z: 2
}
Keys.onPressed: (event) => {
if (event.key == Qt.Key_PageUp) {
pgUp();
event.accepted = true;
}
if (event.key == Qt.Key_PageDown) {
pgDn();
event.accepted = true;
}
}
}
ListModel {
id: listModel
}
footer: Frame {
RowLayout {
width: parent.width
Button {
text: "PgUp"
onClicked: pgUp()
}
Button {
text: "PgDn"
onClicked: pgDn()
}
}
}
function populate() {
for (let i = 0; i <= 50; i++) {
let txt = `Item ${i}`;
let h = Math.floor(Math.random() * 100 + 40);
listModel.append( {txt, h} );
}
}
function pgDn() {
listView.contentY = Math.min(listView.contentY + listView.height, listView.contentHeight - listView.height);
listView.currentIndex = listView.indexAt(0, listView.contentY);
}
function pgUp() {
listView.contentY = Math.max(listView.contentY - listView.height, 0);
listView.currentIndex = listView.indexAt(0, listView.contentY);
}
Component.onCompleted: Qt.callLater(populate)
}
You can Try It Online!

How to perform dynamic conversion between mouse coordinates and world coordinates with QtQuick3D?

Description/ Code
I have a Qt Quick 3D View and corresponding scene that was designed to be compiled on Qt 6.3.0
import QtQuick
import QtQml
import QtQuick3D
import QtQuick3D.Helpers
Window {
width: 800
height: 600
visible: true
property var selectedItem
property bool mousePressed: false
function multiply_vectors(vec1, vec2) {
return Qt.vector3d(vec1.x * vec2.x, vec1.y * vec2.y, vec1.z * vec2.z);
}
View3D {
renderMode: View3D.Inline
camera: camera
anchors.fill: parent
width: 800
height: 600
x: 0
y: 0
id: view
environment: SceneEnvironment {
clearColor: "black"
backgroundMode: SceneEnvironment.Color
depthTestEnabled: false
depthPrePassEnabled: true
}
Model {
id: rootEntity
pickable: true
source: "#Cube"
materials: PrincipledMaterial {
baseColor: "red"
roughness: 0.1
}
position: Qt.vector3d(25.0, 15.0, -60.0)
scale: Qt.vector3d(1.0, 1.0, 1.0)
}
PerspectiveCamera {
id: camera
position.z: 330.0
position.y: 0.75
eulerRotation.x: -12
clipNear: 0.0
clipFar: 1600.0
}
MouseArea {
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
id: mouseArea
onPressed: function (mouse) {
var result = view.pick(mouse.x, mouse.y);
if (result.objectHit) {
selectedItem = result.objectHit;
mousePressed = true;
} else {
mousePressed = false;
}
}
onMouseXChanged: function(mouse) {
if (mousePressed) {
var viewCoords = view.mapFromGlobal(mouseArea.mapToGlobal(mouse.x, mouse.y));
var sceneCoords = Qt.vector3d(viewCoords.x, viewCoords.y, 0);
var worldCoords = view.mapTo3DScene(sceneCoords);
worldCoords.z = selectedItem.z
selectedItem.position = multiply_vectors(worldCoords, Qt.vector3d(Math.abs(camera.z - selectedItem.z), Math.abs(camera.z - selectedItem.z), 1.0))
}
}
onReleased: function (mouse) {
mousePressed = false
}
}
Component.onCompleted: {
camera.lookAt(rootEntity)
}
}
}
Overview
The use case is that whenever the mouse is pressed while pointing at the cube, whenever the mouse moves it will cause the cube to move along with it to the corresponding point in the 3d Scene.
This works great when looking from a point that is on the same z-axis. However when looking at the object from a point say along the x-axis, the model will move along the x-axis instead of following the mouse position.
Question
How can I modify the business logic in onMouseXChanged: function(mouse) { to correctly transform the matrix (or equivalent transform) to consistently match the mouse position irregardless of the camera's position relative to the Model?
If I understood you correctly, you need to move the object with the mouse parallel to the camera regardless of the camera position and model scaling? I admit that I don't have a solution, but still it's better than the original code. First of all, do not set the clipNear to 0, it would make the frustum degenerate and break the projection math.
Secondly, I would suppose that the code which sets the object position should look like
selectedItem.position = view.mapTo3DScene(
Qt.vector3d(mouse.x, mouse.y,
view.mapFrom3DScene(selectedItem.position).z))
The docs say that mapFrom3DScene/mapTo3DScene should interpret the z coordinate as the distance from the near clip plane of the frustum to the mapped position. However when I move it towards the sides of the window the object gets larger, whereas it should get smaller.
Here's the complete code with a few corrections of mine:
import QtQuick
import QtQml
import QtQuick3D
import QtQuick3D.Helpers
Window {
width: 800
height: 600
visible: true
property var selectedItem
property bool mousePressed: false
View3D {
renderMode: View3D.Inline
camera: camera
anchors.fill: parent
width: 800
height: 600
x: 0
y: 0
id: view
environment: SceneEnvironment {
clearColor: "black"
backgroundMode: SceneEnvironment.Color
depthTestEnabled: false
depthPrePassEnabled: true
}
Model {
id: rootEntity
pickable: true
source: "#Cube"
materials: PrincipledMaterial {
baseColor: "red"
roughness: 0.1
}
position: Qt.vector3d(25.0, 15.0, -60.0)
scale: Qt.vector3d(2.0, 1.0, 0.5)
}
PerspectiveCamera {
id: camera
position.z: 330.0
position.y: 100
position.x: 700
eulerRotation.x: -12
// Note 1: clipNear shouldn't be 0, otherwise
// it would break the math inside the projection matrix
clipNear: 1.0
clipFar: 1600.0
}
MouseArea {
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
id: mouseArea
onPressed: function (mouse) {
var result = view.pick(mouse.x, mouse.y);
if (result.objectHit) {
selectedItem = result.objectHit;
mousePressed = true;
} else {
mousePressed = false;
}
}
onPositionChanged: function(mouse) {
if (mousePressed) {
// Note 2: recalculate the position, since MouseArea has
// the same geometry as View3D we can use coords directly
selectedItem.position = view.mapTo3DScene(
Qt.vector3d(mouse.x, mouse.y,
view.mapFrom3DScene(selectedItem.position).z))
}
}
onReleased: function (mouse) {
mousePressed = false
}
}
Component.onCompleted: {
camera.lookAt(rootEntity)
}
}
}
After spending a while experimenting with different approaches, I found that mapping the mouse coordinates to the 3d space wasn't fully supported by the Qt API in terms of when the mouse is not fixed over an active object.
So, instead, the way that I made a workout was by casting a new RayCast each time the mouse moves and storing the offset when the mouse is pressed originally and then translating the item based on the result of the raycast and lining up the offset by translating by the normalized matrix with a small scalar.
onMouseXChanged: function (mouse) {
if (mousePressed) {
if (selectedItem != null) {
var result = view.pick(mouse.x, mouse.y)
if (result.objectHit) {
if (result.objectHit == selectedItem) {
var mouseGlobalPos = mouseArea.mapToGlobal(
mouse.x, mouse.y)
var mouseViewPos = view.mapFromGlobal(
mouseGlobalPos)
var mouseScenePos = result.scenePosition
var resultPos = result.position
/* here we subtract the result of the new raycast by the starting offset and then normalize
* the result and multiply it by a scalar 3 to determine the amount of offset the Model
* under the mouse is from where the mouse was originally pressed, so we can translate it */
var differencePos = resultPos.minus(
startMousePressSelectedItemLocalDragOffset).normalized(
).times(3)
selectedItem.position = selectedItem.position.plus(
differencePos)

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.

Drag and drop multiple items in Qml

I need to draw bunch of Rectangles in my application. The user should be able to select each of them individually and move them freely and change their position. The user also should be able to select multiple Rectangles and move all the selected one simultaneously and release them somewhere else.
I could already implement something based on Gridview that can handle selection and movement of one Rectangle, but I cannot make it work for multiple selection/movement. Here is a snippet of code I have currently:
GridView {
id: mainGrid
cellWidth: 7;
cellHeight: 7;
ListModel {
id: myModel
function createModel() {
for(var i = 0; i < totalZoneList.length; i++)
{
for (var j = 0; j < moduleZoneList.length; j++)
{
myModel.append({"item1": ITEM1, "item2": ITEM2})
}
}
}
Component.onCompleted: {createModel()}
}
Component {
id: myblocks
Item {
id: item
width: mainGrid.cellWidth;
height: mainGrid.cellHeight;
Rectangle {
id: box
parent: mainGrid
x: //CALCULATED BASED ON MODEL
y: //CALCULATED BASED ON MODEL
width: //CALCULATED BASED ON MODEL
height: //CALCULATED BASED ON MODEL
MouseArea {
id: gridArea
anchors.fill: parent
hoverEnabled: true
drag.axis: Drag.XandYAxis
drag.minimumX: 0
drag.minimumY: 0
property int mX: (mouseX < 0) ? 0 : ((mouseX < mainGrid.width - mainGrid.cellWidth) ? mouseX : mainGrid.width - mainGrid.cellWidth)
property int mY: (mouseY < 0) ? 0 : ((mouseY < mainGrid.height - mainGrid.cellHeight) ? mouseY : mainGrid.height - mainGrid.cellHeight)
property int index: parseInt(mX/mainGrid.cellWidth) + 5*parseInt(mY/mainGrid.cellHeight) //item underneath cursor
property int activeIndex
property var xWhenPressed
property var yWhenPressed
propagateComposedEvents: true
onPressed: {
activeIndex = index
drag.target = box
xWhenPressed = box.x
yWhenPressed = box.y
gridArea.drag.maximumX = mainGrid.width - box.width
gridArea.drag.maximumY = mainGrid.height - box.height
}
onReleased: {
if(xWhenPressed !== box.x || yWhenPressed !== box.y)
{
//RECALCULATE THE POSITION
}
}
onPositionChanged: {
if (drag.active && index !== -1 && index !== activeIndex) {
mainGrid.model.move(activeIndex, activeIndex = index, 1)
}
}
} // Mousearea
} // Rectangle
} // Item
} // Component
} //mainGrid
I didn't manage to have your code working. First, I see mistakes on it:
Rectangle {
id: box
parent: mainGrid
...
}
you just need to remove the parent Item which is of no use, and set the Rectangle as root of the delegate.
Then, you forgot to mention that the target of drag is the Rectangle
drag.target: parent
Here is the code after correcting:
Component {
id: myblocks
Rectangle {
id: box
color: "red"
width: 20
height: 20
MouseArea {
id: gridArea
anchors.fill: parent
drag.target: parent
hoverEnabled: true
drag.axis: Drag.XandYAxis
} // Mousearea
} // Rectangle
} // Component
Then, you shouldn't use a GridView because you want elements to be moved. If you use a Repeater it works, and you just have to set x and y in the Rectangle to place the elements at the beginning.
Now this is a solution for your problem: you click on an element to select it, and you can move all selected items at once.
Window {
visible: true
width: 640
height: 480
property var totalZoneList: ["total1", "total2"]
property var moduleZoneList: ["module1", "module2"]
Repeater{
id: iRepeater
model: ListModel {
id: myModel
function createModel() {
for(var i = 0; i < totalZoneList.length; i++)
{
for (var j = 0; j < moduleZoneList.length; j++)
{
myModel.append({"item1": totalZoneList[i], "item2": moduleZoneList[j]})
}
}
}
Component.onCompleted: {createModel()}
}
delegate: myblocks
}
Component {
id: myblocks
Rectangle {
id: box
color: {
switch(index){
case 0: selected ? "red" : "#FF9999";break;
case 1: selected ? "blue" : "lightblue";break;
case 2: selected ? "green" : "lightgreen";break;
case 3: selected ? "grey" : "lightgrey";break;
}
}
x: (width + 5)*index
width: 20
height: 20
property int offsetX:0
property int offsetY:0
property bool selected: false
function setRelative(pressedRect){
disableDrag();
x = Qt.binding(function (){ return pressedRect.x + offsetX; })
y = Qt.binding(function (){ return pressedRect.y + offsetY; })
}
function enableDrag(){
gridArea.drag.target = box
}
function disableDrag(){
gridArea.drag.target = null
}
MouseArea {
id: gridArea
anchors.fill: parent
hoverEnabled: true
drag.axis: Drag.XandYAxis
onClicked: parent.selected=!parent.selected
onPressed: {
var pressedRect = iRepeater.itemAt(index);
if (pressedRect.selected == true){
for (var i=0; i<iRepeater.count; i++ ){
var rect = iRepeater.itemAt(i);
if (i != index){
//init for breaking existing binding
rect.x = rect.x
rect.y = rect.y
rect.disableDrag()
if (rect.selected == true){
rect.offsetX = rect.x - pressedRect.x
rect.offsetY = rect.y - pressedRect.y
rect.setRelative(pressedRect)
}
}
}
pressedRect.enableDrag()
}
}
} // Mousearea
} // Rectangle
} // Component
}
Recipe One:
Use a Repeater, so the positioning is not determined by a view but by yourself.
Use an invisible helper Item. This is your drag.target.
Implement your prefered way of selecting your objects - weather by clicking, or drawing boxes and performing a check which objects are included in this box. Make the positions of all selected objects relative to your invisible helper object.
Drag the helper object, and move all other objects accordingly.
When finished, unselect the objects again and make their positions absolute (within the parents coordinate system)
Recipe Two:
Reparent all selected Objects to a new Item, while mapping their coordinates accordingly
Move the new Item
Reparen all objects back to the original canvas, mapping their coordinates back.
I hope this is sufficient to solve your problem (as far as I understood it)
If it solves another problem, than you need to be more specific about the expected behavior of your draged objects.

Resources