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!
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
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.
I want generate a binary tree like flow to show organization relationship info with QML.enter image description here
My way:
Create tree node like flow example.
Line the tree nodes with Canvas like above picture. I want get the
point and draw line after Loader.onLoaded.
My question:
After the first step has been done. In the second step, I can't get the right position point of the tree node.
Details:
According to Qt document, if I don't set the width and height of Loader explicitly, it will automatically set by its item size. However, I can't get the node position point in Loader.onloaded()
import QtQuick 2.3
import QtQuick.Window 2.2
Window {
id: appWindow
visible: true
width: 1066
height: 600
property var jsonData: [{"id":1,"pid":0},{"id":2,"pid":1},{"id":3,"pid":1},
{"id":4,"pid":2},{"id":5,"pid":2},{"id":6,"pid":3},
{"id":7,"pid":3},{"id":8,"pid":4},{"id":9,"pid":4},{"id":10,"pid":6}]
property int maxRectangles: 5
Component.onCompleted: {
var color = Qt.lighter("red", maxRectangles / 7)
pLoader.sourceComponent = rectangleComponent;
var currItem = pLoader.item;
currItem.color = "blue"
currItem.text = 0
var tmp = {};
tmp[jsonData[0]["id"]] = currItem ;
for(var index = 1; index < jsonData.length; index++) {
color = Qt.lighter("red", index / 7)
var sItem = jsonData[index]["pid"];
if(tmp[sItem].rComponent.sourceComponent === null){
tmp[sItem].rComponent.sourceComponent = rectangleComponent
attachData(tmp[sItem].rComponent.item,index, color);
tmp[jsonData[index]["id"]] = tmp[sItem].rComponent.item ;
}
else {
tmp[sItem].lComponent.sourceComponent = rectangleComponent
attachData(tmp[sItem].lComponent.item,index, color);
tmp[jsonData[index]["id"]] = tmp[sItem].lComponent.item ;
}
}
}
function attachData(item, text, color){
item.text = text;
item.color = color
}
Component {
id: rectangleComponent
Column{
property alias rComponent: rightLoader
property alias lComponent: leftLoader
property alias color: rect.color
property alias text: myText.text
spacing: 40
//anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
id:rect
width: 100
height: 100
Text{
id:myText
font.pixelSize: 18
anchors.centerIn: parent
color:"white"
}
Component.onCompleted: {
console.log("Component.onCompleted", mapToItem(canvas, 0, 0))
}
MouseArea {
anchors.fill: parent
onClicked: {
updateDotPosition(parent)
//here, printed coordinate is my expected
//but i want line them after tree node loaded antomate
console.log("updateDotPosition", mapToItem(canvas, 0, 0))
}
}
}
Row{
spacing: 40
anchors.horizontalCenter: parent.horizontalCenter
Loader {
id:leftLoader
onLoaded: {
console.log("leftLoader", mapToItem(canvas, 0, 0))
}
}
Loader {
id:rightLoader
onLoaded: {
console.log("rightLoader", mapToItem(canvas, 0, 0))
}
}
}
}
}
Canvas{
anchors.fill: parent
id:canvas
Loader {
id:pLoader
anchors.horizontalCenter: parent.horizontalCenter
//sourceComponent: rectangleComponent
}
}
Rectangle {
id: testMapToItemDot
width: 20
height: width
radius: width / 2
z: 1
color: "darkblue"
}
function updateDotPosition(itemToMap) {
var pos = testMapToItemDot.mapFromItem(
itemToMap,
(itemToMap.width - testMapToItemDot.width) / 2, // these parameters are optional - I want to move the dot to
(itemToMap.height - testMapToItemDot.height) / 2) // the center of the object, not upper left corner
testMapToItemDot.x += pos.x
testMapToItemDot.y += pos.y
}
}
Position and size of the loaded item can be accessed like this: Loader.item.x, Loader.item.y, Loader.item.width, Loader.item.height.
If this is not what your are looking for please describe the question more precisely.
Please have a look at this example application. I think it is what you are looking for - have a look at function updateDotPosition(itemToMap). Please note that if you will be "mapping" to an object that is not moving (dot in the example is moving) and this object will be a sibling of the Canvas with the same x, y values then the result of the function will give you exact points where from and where to draw a line. You will not need to use += as I used.
import QtQuick 2.3
import QtQuick.Window 2.2
Window {
id: appWindow
visible: true
width: 1066
height: 600
property int maxRectangles: 5
Component {
id: rectangleComponent
Rectangle {
x: 110
y: 30
width: 100
height: 100
property var loaderSource
Component.onCompleted: {
color = Qt.lighter("red", maxRectangles / 7)
if (maxRectangles-- > 0)
loaderSource = rectangleComponent
else
loaderSource = undefined
}
Loader {
sourceComponent: loaderSource
}
MouseArea {
anchors.fill: parent
onClicked: {
updateDotPosition(parent)
}
}
}
}
Loader {
sourceComponent: rectangleComponent
}
Rectangle {
id: testMapToItemDot
width: 20
height: width
radius: width / 2
z: 1
color: "darkblue"
}
function updateDotPosition(itemToMap) {
var pos = testMapToItemDot.mapFromItem(
itemToMap,
(itemToMap.width - testMapToItemDot.width) / 2, // these parameters are optional - I want to move the dot to
(itemToMap.height - testMapToItemDot.height) / 2) // the center of the object, not upper left corner
testMapToItemDot.x += pos.x
testMapToItemDot.y += pos.y
}
}
Here is working version of your example. This is just an example. Dirty one unfortunately. This is because it was a little hard for me to understand what is going on in the code - I am sure you will not have this problem. Also I have used Timer because the Rectangle objects on creation have x,y equal to 0. I needed to wait a bit. Please do not use this in release code - it is bad design :)
import QtQuick 2.3
import QtQuick.Window 2.2
Window {
id: appWindow
visible: true
width: 1066
height: 600
property var jsonData: [{"id":1,"pid":0},{"id":2,"pid":1},{"id":3,"pid":1},
{"id":4,"pid":2},{"id":5,"pid":2},{"id":6,"pid":3},
{"id":7,"pid":3},{"id":8,"pid":4},{"id":9,"pid":4},{"id":10,"pid":6}]
property int maxRectangles: 5
Component.onCompleted: {
var color = Qt.lighter("red", maxRectangles / 7)
pLoader.sourceComponent = rectangleComponent;
var currItem = pLoader.item;
currItem.color = "blue"
currItem.text = 0
connectedItems.push([currItem, null])
var tmp = {};
tmp[jsonData[0]["id"]] = currItem ;
for(var index = 1; index < jsonData.length; index++) {
color = Qt.lighter("red", index / 7)
var sItem = jsonData[index]["pid"];
if(tmp[sItem].rComponent.sourceComponent === null){
tmp[sItem].rComponent.sourceComponent = rectangleComponent
attachData(tmp[sItem].rComponent.item,index, color);
tmp[jsonData[index]["id"]] = tmp[sItem].rComponent.item ;
connectedItems.push([tmp[sItem].rComponent.item, null])
}
else {
tmp[sItem].lComponent.sourceComponent = rectangleComponent
attachData(tmp[sItem].lComponent.item,index, color);
tmp[jsonData[index]["id"]] = tmp[sItem].lComponent.item ;
connectedItems.push([tmp[sItem].lComponent.item, null])
}
if ( jsonData[index]["pid"] !== 0) {
connectedItems[connectedItems.length - 1][1] = connectedItems[jsonData[index]["pid"] - 1][0]
}
}
}
function attachData(item, text, color){
item.text = text;
item.color = color
}
property var connectedItems: [];
function paintConnection(objectFrom, objectTo) {
var vectorStart = canvas.mapFromItem(
objectFrom, objectFrom.width / 2, objectFrom.height)
var vectorEnd = canvas.mapFromItem(
objectTo, objectTo.width / 2,0)
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(vectorStart.x, vectorStart.y);
ctx.lineTo(vectorEnd.x,vectorEnd.y);
ctx.stroke();
canvas.requestPaint()
}
Timer {
id: paintLines
interval: 100
running: true
onTriggered: {
for (var i = 1 ; i < connectedItems.length ; i++) {
paintConnection(connectedItems[i][1].rect, connectedItems[i][0].rect)
}
}
}
Component {
id: rectangleComponent
Column{
property alias rComponent: rightLoader
property alias lComponent: leftLoader
property alias color: rect.color
property alias text: myText.text
property alias rect: rect
spacing: 40
//anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
id:rect
width: 100
height: 100
Text{
id:myText
font.pixelSize: 18
anchors.centerIn: parent
color:"white"
}
Component.onCompleted: {
console.log("Component.onCompleted", mapToItem(canvas, 0, 0))
}
MouseArea {
anchors.fill: parent
onClicked: {
updateDotPosition(parent)
//here, printed coordinate is my expected
//but i want line them after tree node loaded antomate
console.log("updateDotPosition", mapToItem(canvas, 0, 0))
}
}
}
Row{
spacing: 40
anchors.horizontalCenter: parent.horizontalCenter
Loader {
id:leftLoader
onLoaded: {
console.log("leftLoader", mapToItem(canvas, 0, 0))
}
}
Loader {
id:rightLoader
onLoaded: {
console.log("rightLoader", mapToItem(canvas, 0, 0))
}
}
}
}
}
Canvas{
anchors.fill: parent
id:canvas
Loader {
id:pLoader
anchors.horizontalCenter: parent.horizontalCenter
//sourceComponent: rectangleComponent
}
}
Rectangle {
id: testMapToItemDot
width: 20
height: width
radius: width / 2
z: 1
color: "darkblue"
}
function updateDotPosition(itemToMap) {
var pos = testMapToItemDot.mapFromItem(
itemToMap,
(itemToMap.width - testMapToItemDot.width) / 2, // these parameters are optional - I want to move the dot to
(itemToMap.height - testMapToItemDot.height) / 2) // the center of the object, not upper left corner
testMapToItemDot.x += pos.x
testMapToItemDot.y += pos.y
}
}
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"
}
}
}
}