Drag and drop multiple items in Qml - qt

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.

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!

QML Shape: How can I force data property to update in a clean way?

I am trying to make a window where I can draw triangles and delete any of them with a Shape{}. In my example below, I can draw 2 types of triangle:
Up triangle: green and filled
Down triangle: yellow and not filled
Basically, I choose the type of triangle (with a button on the right-bottom corner) then I click anywhere on the window to get a triangle.
Once I click a triangle is created dynamically and it is stored in the property triangleList. Then I call the function shape.update() to update data property of shape. This part works well.
Here the function update I use in the Shape (since data is a list, I have to reassign to a new list.):
function update()
{
data = [];
var d = [];
for (var i = 0; i < canvas.triangleList.length; i++)
{
d.push( canvas.triangleList[i] );
}
data = d;
}
My problem appears when I try to delete a triangle. In my example, I can delete the first, the last or all triangles. When I delete a triangle, first I delete the value in triangleList then I call again shape.update(). It works when I delete all triangles or the last one.
However, when I try to delete the first triangle, data doesn't update its objects even if I give it a new list. In fact, it always deletes the last triangle. Below an example:
data property understands there is one less triangle but it doesn't update the other triangles. The only solution I found is to change a property then come back to the original value. This way, it forces the data to update.
But I have to do that for every property that can be different (colors and positions). Hence, my update() function looks like that:
for (var i = 0; i < canvas.triangleList.length; i++)
{
d.push( canvas.triangleList[i] );
////// Change properties one by one to force the refresh
// Force path redraw. Otherwise only the last path can be deleted
d[i].startX++;d[i].startX--;
// Force line color update
d[i].strokeColor = "red"
d[i].strokeColor = d[i].isUp ? "green" : "yellow";
// Force fill color update
d[i].fillColor = "red";
d[i].fillColor = d[i].isUp ? "green" : "transparent";
data = d;
}
I invite you to comment in/out these lines to see the difference.
I could use this trick to force the update but my real code is really bigger than this example and I use bindings.
So my question is: Is there a way to force the update without having to change each property?
Here the full code if you want to test it:
import QtQuick 2.9;
import QtQuick.Controls 2.2;
import QtQuick.Shapes 1.0;
ApplicationWindow {
visible: true; width: 640; height: 480;
Rectangle {
id: canvas;
anchors.fill: parent;
color: "black";
property var triangleList: [];
property bool triangleUp: true;
MouseArea {
anchors.fill: parent;
onClicked: {
var triangle = componentTriangle.createObject(componentTriangle, {
"isUp" : canvas.triangleUp,
"startX" : mouse.x,
"startY" : mouse.y,
}, canvas);
canvas.triangleList.push(triangle);
shape.update();
}
} // MouseArea
Shape {
id: shape;
anchors.fill: parent;
function update()
{
data = [];
var d = [];
for (var i = 0; i < canvas.triangleList.length; i++)
{
d.push( canvas.triangleList[i] );
///////////// HOW TO AVOID THE PART BELOW? /////////////
////// Change properties one by one to force the refresh
// Force path redraw. Otherwise only the last path can be deleted
d[i].startX++;d[i].startX--;
// Force line color update
d[i].strokeColor = "red"
d[i].strokeColor = d[i].isUp ? "green" : "yellow";
// Force fill color update
d[i].fillColor = "red";
d[i].fillColor = d[i].isUp ? "green" : "transparent";
//////////////////////////////////////////////////////
}
data = d;
// I make sure data has at least one path to ensure the refresh
if (data.length == 0)
data.push(Qt.createQmlObject('import QtQuick 2.9; import QtQuick.Shapes 1.0; ShapePath {startX:0;startY:0;}', canvas,
"force_refresh"));
}
} // Shape
} // Rectangle
//////////// Buttons to handle the triangles
Column {
anchors.bottom: parent.bottom;
anchors.right: parent.right;
Button {
text: canvas.triangleUp? "Draw triangleUp" : "Draw triangleDown";
onClicked: { canvas.triangleUp = !canvas.triangleUp; }
} // Button
Button {
text: "Clear first";
onClicked: {
canvas.triangleList[0].destroy();
canvas.triangleList.splice(0,1);
shape.update();
}
} // Button
Button {
text: "Clear last";
onClicked: {
canvas.triangleList[canvas.triangleList.length -1].destroy();
canvas.triangleList.splice(canvas.triangleList.length -1,1);
shape.update();
}
} // Button
Button {
text: "Clear all";
onClicked: {
for (var i = 0; i < canvas.triangleList.length; i++)
canvas.triangleList[i].destroy();
canvas.triangleList = [];
shape.update();
}
} // Button
}
//////////// Component to draw the triangle
Component {
id: componentTriangle;
ShapePath {
property bool isUp;
property real offsetX: isUp? -20 : 20;
property real offsetY: isUp? -30 : 30;
strokeColor: isUp ? "green" : "yellow";
strokeWidth: 3;
fillColor: isUp ? "green" : "transparent";
PathLine { x: startX - offsetX; y: startY - offsetY }
PathLine { x: startX + offsetX; y: startY - offsetY }
PathLine { x: startX; y: startY }
} // ShapePath
}
}
Thank you very much for your help and feel free to ask me if I was not clear.
Have a nice day!
If you are going to handle many items (Shape) it is advisable to use a Repeater with a model. The repeater is responsible for displaying the items based on the information of the model, and to remove the items you just have to remove items from the model.
main.qml
import QtQuick 2.9;
import QtQuick.Controls 2.2;
import QtQuick.Shapes 1.0;
ApplicationWindow {
visible: true; width: 640; height: 480;
QtObject{
id: internals
property bool triangleUp: true;
}
ListModel{
id: datamodel
}
Rectangle {
id: canvas;
anchors.fill: parent;
color: "black";
Repeater{
model: datamodel
Triangle{
x: model.x
y: model.y
isUp: model.isUp
}
}
MouseArea{
anchors.fill: parent
onClicked: datamodel.append({"x": mouse.x, "y": mouse.y, "isUp": internals.triangleUp})
}
}
Column {
anchors.bottom: parent.bottom;
anchors.right: parent.right;
Button {
text: internals.triangleUp ? "Draw triangleUp" : "Draw triangleDown";
onClicked: internals.triangleUp = !internals.triangleUp;
} // Button
Button {
text: "Clear first";
onClicked: if(datamodel.count > 0) datamodel.remove(0)
} // Button
Button {
text: "Clear last";
onClicked: if(datamodel.count > 0) datamodel.remove(datamodel.count - 1)
} // Button
Button {
text: "Clear all";
onClicked: datamodel.clear()
} // Button
}
}
Triangle.qml
import QtQuick 2.9;
import QtQuick.Shapes 1.0
Shape {
id: shape
property bool isUp: false
QtObject{
id: internals
property real offsetX: isUp? -20 : 20;
property real offsetY: isUp? -30 : 30;
}
ShapePath {
strokeWidth: 3;
strokeColor: isUp ? "green" : "yellow";
fillColor: isUp ? "green" : "transparent";
PathLine { x: -internals.offsetX ; y: -internals.offsetY }
PathLine { x: internals.offsetX; y: -internals.offsetY }
PathLine { x: 0; y: 0 }
}
}

QML: Bind loop detected without double assignment

As far as I know the bind loop happens when I try to assign two properties each other. Example:
CheckBox {
checked: Settings.someSetting
onCheckedChanged: {
Settings.someSetting = checked;
}
}
but in my scenario I can't see such a "double assignment". I report here the full code:
import QtQuick 2.7
import QtQuick.Window 2.3
Window {
visible: true;
width: 500
height: 500
Rectangle {
id: main
anchors.fill: parent
color: "black"
property bool spinning: true
property bool stopping: false
Rectangle {
x: 0.5 * parent.width
y: 0.5 * parent.height
width: 10
height: 200
radius: 5
color: 'red'
transformOrigin: Item.Top
rotation: {
if (main.stopping)
{
main.spinning = false;
main.stopping = false;
}
return timer.angle
}
}
Timer {
id: timer
interval: 5
repeat: true
running: true
onTriggered: {
if (main.spinning) angle += 1;
}
property real angle
}
MouseArea {
id: control
anchors.fill: parent
onClicked: {
main.stopping = true;
}
}
}
}
When you click with the mouse you will get the warning:
qrc:/main.qml:17:9: QML Rectangle: Binding loop detected for property "rotation"
I don't see my mistake. I'm using flags (bool variables) to control the execution of my code. I know in this case I can just stopping the timer directly, but the actual program is more complex than this example.
The binding is in the following lines:
rotation: {
if (main.stopping)
{
main.spinning = false;
main.stopping = false;
}
return timer.angle
}
The change of rotation is triggered by the change of main.stopping: let's say that change main.stopping is given by the mouseArea, then it will be called a rotation, but inside this there is an if, and in this you are changing back to main.stopping , where he will call rotation back.
If a property in QML changes all the elements that depend on it will change

Accessing Attributes of a ListView

I am writing a QML application that loads and displays images, I have a central frame that show the image the user is currently looking at as well as a sidebar that lets the user select images. What I am having trouble with is getting the information about what the currently selected item is in the sidebar (ListView).
Additionally, I am having trouble accessing the sourceChanged signal in the delegate for the ListView and therefore can't update the images in the list without the user scrolling down and then back up to force them to reload. Is there any easy way to access these attributes even though they are nested within a ListView?
Here is the code for my ListView. The issue is that I want to be able to access the Image from outside of the ListView in order to send a sourceChanged signal but I'm not sure how you would access a specific item in the list.
//The list of frames that have been loaded
ListView {
id: frameList
anchors {top: parent.top; bottom: parent.bottom; left: frameViewer.right; leftMargin: 5; topMargin: 5}
spacing: 5
width:300
height: parent.height
Component {
id: frameDelegate
Rectangle {
id: wrapper
anchors {horizontalCenter: parent.horizontalCenter}
height: 300
width: 300
//If there is an image in that space and if it's selected, highlight it
color: frames[number] === undefined ? "white" : wrapper.ListView.view.currentIndex === index ? "yellow" : "white"
Image {
id: image
height: 280
width: 280
anchors.centerIn: parent
fillMode: Image.PreserveAspectFit
source: "image://images/" + frames[number]
}
MouseArea {
height: 280
width: 280
anchors.fill: image
hoverEnabled: true
onClicked: frames[index] === undefined ? console.log(""): wrapper.ListView.view.currentIndex = index
}
}
}
ListModel {
id: frameModel
ListElement {
number: 0
}
ListElement {
number: 1
}
ListElement {
number: 2
}
ListElement {
number: 3
}
ListElement {
number: 4
}
ListElement {
number: 5
}
}
model: frameModel
delegate: frameDelegate
focus: true
}
Ok so I fixed the issue, although I may not have done it in the best way possible.
My solution is to create a signal newFrame in the window which would be sent every time a new frame is loaded. Then I use a Connections in the Image to update the source, every time that signal is sent.
signal newFrame()
//Stores all the fileURLs
function loadImages()
{
for(var i = 0; i < fileDialog.fileUrls.length; i++)
{
var exists = false
for(var j = 0; j < frames.length; j++)
{
if(frames[j] === fileDialog.fileUrls[i])
{
exists = true
}
}
if(exists == false)
{
if(frames == [])
{
frames = fileDialog.fileUrls[i]
}
else
{
frames.push(fileDialog.fileUrls[i])
}
frame.sourceChanged(frame.source)
newFrame()
}
}
}
Component
{
id: frameDelegate
Rectangle
{
id: wrapper
anchors {horizontalCenter: parent.horizontalCenter}
height: 300
width: 300
//If there is an image in that space and if it's selected, highlight it
color: frames[number] === undefined ? "white" : wrapper.ListView.view.currentIndex === index ? "yellow" : "white"
Image
{
id: image
height: 280
width: 280
anchors.centerIn: parent
fillMode: Image.PreserveAspectFit
source: "image://images/" + frames[number]
Connections {
target: window
onNewFrame: {
image.source = "image://images/" + frames[number]
}
}
}
MouseArea
{
height: 280
width: 280
anchors.fill: image
hoverEnabled: true
onClicked: {frames[index] === undefined ? console.log(""): wrapper.ListView.view.currentIndex = index; frame.sourceChanged(frame.source)}
}
}
}

Draw rectangle using mouse QML

I'm creating a wave form using QML 2.0.
I would like to know how I can draw rectangle starting when the user clicks on the waveform and ends when user releases mouse button.
I need something like the yellow rectangle.
I tried with Canvas but isn't working properly.
Can you help me?
Canvas {
property int prevX
property int prevY
property int hh:wave.height
property int lineWidth: 2
property color drawColor: "red"
id:mycanvas
height: 200
width: 2000
MouseArea {
id:mousearea
anchors.fill: parent
cursorShape:Qt.PointingHandCursor
onPositionChanged: mycanvas.requestPaint();
onPressed: {
prevX = mouse.x;
prevY = mouse.y
var mousePosition = mouse.x / mousearea.width;
wave.zoomOut(mousePosition);
console.log("QML: ZoomStart mousePosition " + mousePosition)
}
onReleased: {
var mousePosition = mouse.x / mousearea.width;
console.log("QML: ZoomFinish mousePosition " + mousePosition)
wave.zoomOut2(mousePosition);
}
}
onPaint: {
var ctx = getContext('2d');
ctx.beginPath();
ctx.fillStyle = "#000000"
ctx.globalAlpha = 0.05
ctx.fillRect(prevX, 0, mousearea.mouseX-prevX,mainRectangle.height/4.5);
ctx.stroke();
ctx.restore();
}
}
What I would do is instanciate a QtQuick Rectangle dynamically and set its visual properties in the same time :
Just put this a a children of your 'wave graph' component :
MouseArea {
id: selectArea;
anchors.fill: parent;
onPressed: {
if (highlightItem !== null) {
// if there is already a selection, delete it
highlightItem.destroy ();
}
// create a new rectangle at the wanted position
highlightItem = highlightComponent.createObject (selectArea, {
"x" : mouse.x
});
// here you can add you zooming stuff if you want
}
onPositionChanged: {
// on move, update the width of rectangle
highlightItem.width = (Math.abs (mouse.x - highlightItem.x));
}
onReleased: {
// here you can add you zooming stuff if you want
}
property Rectangle highlightItem : null;
Component {
id: highlightComponent;
Rectangle {
color: "yellow";
opacity; 0.35;
anchors {
top: parent.top;
bottom: parent.bottom;
}
}
}
}
That should do the trick !
I fixed the rectangle to be drawn even when the mouse drag position is not from left to right down. Hope it helps others.
Rectangle {
color: "transparent"
MouseArea {
id: selectArea
anchors.fill: parent
property Rectangle highlightItem : null;
property real startX
property real startY
onPressed: (mouse) => {
if (highlightItem !== null) {
// if there is already a selection, delete it
highlightItem.destroy ();
}
// create a new rectangle at the wanted position
highlightItem = highlightComponent.createObject (selectArea, {
"x" : mouseX,
"y" : mouseY,
});
startX = mouseX;
startY = mouseY;
}
onPositionChanged: (mouse) => {
// on move, update the width of rectangle
if (mouseX - startX < 0 ) {
highlightItem.x = mouseX;
}
if (mouseY - startY < 0) {
highlightItem.y = mouseY;
}
highlightItem.width = (Math.abs (mouseX - startX));
highlightItem.height = (Math.abs (mouseY - startY));
}
onReleased: {
}
Component {
id: highlightComponent;
Rectangle {
color: "#0D0080FF"
border.width: 1
border.color: "#fff"
}
}
}
}

Resources