QML how to get the position of Loader - qt

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
}
}

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 ensure Popup is visible inside a QML Map

I'm building a Qt 5.11 application which embeds an openstreetmap QML component.
I just wrote a minimal reproduce case. It consists in displaying objects (five blue dots here) on the map. When hovering the object, a small popup is displayed with some text.
When objects are close to the edge, the popup is not displayed correctly.
I though I would use visibleArea check this, but the property was added in Qt 5.12.
I can't find a solution for the popup to be fully visible. Is there a workaround in Qt 5.11 that I can do?
Here the QML file. Just type qmlscene sample.qml and hover blue dots to view it.
import QtQuick 2.11
import QtQuick.Controls 2.4
import QtLocation 5.11
import QtPositioning 5.11
import QtQuick.Window 2.11
Window {
id: root; width: 800; height: 600;
Plugin { id: mapPlugin; name: "osm"; }
ListModel {
id: myModel
ListElement { latitude: 48.2351164; longitude: 6.8986936; name: "The point on the center"; }
ListElement { latitude: 48.235111272600186; longitude: 6.9007217756551995; name: "The point on the right"; }
ListElement { latitude: 48.23512783507458; longitude: 6.896574932520792; name: "The point on the left"; }
ListElement { latitude: 48.23614708436043; longitude: 6.898623901851295; name: "The point on the top"; }
ListElement { latitude: 48.23417574713512; longitude: 6.898641104398024; name: "The point on the bottom"; }
}
Map {
id: map
anchors.fill: parent
plugin: mapPlugin
center: QtPositioning.coordinate(48.2351164, 6.8986936)
zoomLevel: 19
MapItemView {
model: myModel
delegate: MapQuickItem {
anchorPoint.x: myRect.width / 2
anchorPoint.y: myRect.height / 2
width: myRect.width
height: myRect.height
coordinate: QtPositioning.coordinate(model.latitude, model.longitude)
sourceItem: Rectangle {
id: myRect
readonly property int radius: 10
width: radius * 2
height: radius * 2
color: "transparent"
Canvas {
id: myCanvas
anchors.fill: parent
property alias textVisible: myPopup.visible
onPaint: {
var width = myRect.width;
var height = myRect.height;
var centreX = width / 2;
var centreY = height / 2;
var ctx = getContext("2d");
ctx.reset();
ctx.fillStyle = "blue";
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.moveTo(centreX, centreY);
ctx.arc(centreX, centreY, myRect.radius, 0, Math.PI * 2, false);
ctx.closePath();
ctx.fill();
}
MouseArea {
x: 0; y: 0;
width: myRect.radius * 2
height: myRect.radius * 2
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onEntered: { myCanvas.textVisible = true }
onExited: { myCanvas.textVisible = false }
}
}
Popup {
id: myPopup
x: myRect.width / 2 - width / 2
y: myRect.height / 2 + 20
visible: false
Label { text: model.name; horizontalAlignment: Text.AlignHCenter; }
}
}
}
}
}
}
Any help is greatly appreciated.
You can check if (popup width+popup x) goes outside screen width then change x,y to adjust popup position. You can look into following modified code for Popup component. adjust X and Y as per your marker position.
Popup {
id: myPopup
x: {
if((mapItem.x+myPopup.width) >= root.width)
return -(myRect.width/2)-(width)
else if((mapItem.x-myPopup.width) < 0)
return (myRect.width)
else
return myRect.width / 2 - width / 2
}
y: {
if((mapItem.y+myPopup.height) >= root.height)
return -(myRect.height/2)-(height)
else if((mapItem.y-myPopup.height) < 0)
return (height)
else
return myRect.height / 2 - height / 2
}
visible: false
Label { text: model.name;anchors.centerIn: parent;horizontalAlignment: Text.AlignHCenter; }
}
After looking for a while, I finally came across these two functions: mapToItem and mapFromItem.
So, I first need to map the current item point to the map item coordinate system. Then, I must check the point is inside the map viewport.
If not, I have to adjust the coordinate, and after that, I map the point back to the current item coordinate system. Popup width and height seem to decrease when approaching bottom and right borders, so I had to use contentHeight, contentWidth and padding properties to get the real popup size.
And I had to initialize the popup x and y to a value different of zero to allow mouse event to pass on the blue dot.
Here is the working code, for those who may need it.
import QtQuick 2.11
import QtQuick.Controls 2.4
import QtLocation 5.11
import QtPositioning 5.11
import QtQuick.Window 2.11
Window {
id: root; width: 800; height: 600;
Plugin { id: mapPlugin; name: "osm"; }
ListModel {
id: myModel
ListElement { latitude: 48.2351164; longitude: 6.8986936; name: "The point on the center"; }
ListElement { latitude: 48.235111272600186; longitude: 6.9007217756551995; name: "The point on the right"; }
ListElement { latitude: 48.23512783507458; longitude: 6.896574932520792; name: "The point on the left"; }
ListElement { latitude: 48.23614708436043; longitude: 6.898623901851295; name: "The point on the top"; }
ListElement { latitude: 48.23417574713512; longitude: 6.898641104398024; name: "The point on the bottom"; }
}
Map {
id: map
anchors.fill: parent
plugin: mapPlugin
center: QtPositioning.coordinate(48.2351164, 6.8986936)
zoomLevel: 19
MapItemView {
model: myModel
delegate: MapQuickItem {
anchorPoint.x: myRect.width / 2
anchorPoint.y: myRect.height / 2
width: myRect.width
height: myRect.height
coordinate: QtPositioning.coordinate(model.latitude, model.longitude)
sourceItem: Rectangle {
id: myRect
readonly property int radius: 10
width: radius * 2
height: radius * 2
color: "transparent"
Canvas {
id: myCanvas
anchors.fill: parent
property alias textVisible: myPopup.visible
onPaint: {
var width = myRect.width;
var height = myRect.height;
var centreX = width / 2;
var centreY = height / 2;
var ctx = getContext("2d");
ctx.reset();
ctx.fillStyle = "blue";
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.moveTo(centreX, centreY);
ctx.arc(centreX, centreY, myRect.radius, 0, Math.PI * 2, false);
ctx.closePath();
ctx.fill();
}
MouseArea {
x: 0; y: 0;
width: myRect.radius * 2
height: myRect.radius * 2
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onPositionChanged: {
myCanvas.textVisible = true;
// absolute position in map coordinate system
var absPos = mapToItem(map, mouse.x, mouse.y);
// margin between mouse pointer and the popup
var cursorMargin = 10;
// extra margin for right and bottom side
var bottomRightSideExtraMargin = 10;
// add the cursor margin to the position
var absPopupX = absPos.x + cursorMargin;
var absPopupY = absPos.y + cursorMargin;
// adjust if the popup is out of view on the bottom or right sides
if (absPos.x + myPopup.contentWidth + myPopup.leftPadding + myRect.radius * 2 + bottomRightSideExtraMargin > root.width) {
absPopupX = root.width - (myPopup.contentWidth + myPopup.leftPadding + cursorMargin + bottomRightSideExtraMargin);
}
if (absPos.y + myPopup.contentHeight + myPopup.topPadding + myRect.radius * 2 + bottomRightSideExtraMargin > root.height) {
absPopupY = root.height - (myPopup.contentHeight + myPopup.topPadding + cursorMargin + bottomRightSideExtraMargin);
}
// convert back to the current item coordinate system
var popupPos = mapFromItem(map, absPopupX, absPopupY);
myPopup.x = popupPos.x;
myPopup.y = popupPos.y;
}
onExited: {
myCanvas.textVisible = false;
}
}
}
Popup {
id: myPopup
// original offset to allow mouse hover
x: 20; y: 20;
visible: false
Label { text: model.name; horizontalAlignment: Text.AlignHCenter; }
}
}
}
}
}
}

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 }
}
}

Drawing segments on QML canvas

Consider the code below, which allows you to insert successive points represented as black circles onto the canvas. The aim of the code, is to
join each successive point by a red line segment.
However, the code gives me a canvas which looks like this
Only the first and second points have been joined by a red segment. None of the other points have been joined by segments. How do I fix that?
Here is the QML code
import QtQuick 2.7
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
Window{
id: root
width: 640
height: 480
visible: true
Canvas {
id: mycanvas
width: 500
height: 500
function clear() {
var ctx = getContext("2d");
ctx.reset();
mycanvas.requestPaint();
}
Path {
id: myPath
startX: 0; startY: 100
PathLine { x: 400; y: 500 }
}
property var arrpoints : []
onPaint: {
var context = getContext("2d");
// Render all the points as small black-circles
context.strokeStyle = Qt.rgba(0, 1, 1, 0)
for(var i=0; i < arrpoints.length; i++){
var point= arrpoints[i]
context.ellipse(point["x"], point["y"], 10, 10)
}
context.fill()
context.stroke()
// Join successive points with red segments
for (var j=1 ; j < arrpoints.length ; j++){
var start = arrpoints[j-1]
var end = arrpoints[j]
context.beginPath();
context.lineWidth = 2;
context.moveTo(start["x"], start["y"]);
context.strokeStyle = "red"
context.lineTo(end["x"], end["y"]);
}
context.stroke();
}
MouseArea {
id: mymouse
anchors.fill: parent
onClicked: {
mycanvas.arrpoints.push({"x": mouseX, "y": mouseY})
mycanvas.requestPaint()
console.log( mycanvas.arrpoints )
}
}
}
Button {
text: "Clear Points"
anchors.top : mycanvas.bottom
onClicked: {
mycanvas.arrpoints.length = 0
mycanvas.clear()
console.log( mycanvas.arrpoints )
}
}
}//Window
According to the documentation:
object ellipse(real x, real y, real w, real h)
Creates an ellipse within the bounding rectangle defined by its
top-left corner at (x, y), width w and height h, and adds it to the
path as a closed subpath.
The ellipse is composed of a clockwise curve, starting and finishing
at zero degrees (the 3 o'clock position).
That is, an ellipse is drawn inscribed in a rectangle determined by the position (x, y) and the size w, h, so we must obtain the topLeft point using the mouse point and the radius.
For lines it is not necessary to make a path for each line, you just have to use a path, and then create the line and move to a next point as shown below:
import QtQuick 2.7
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
Window{
id: root
width: 640
height: 480
visible: true
Canvas {
id: mycanvas
width: 500
height: 500
function clear() {
var ctx = getContext("2d");
ctx.reset();
mycanvas.requestPaint();
}
Path {
id: myPath
startX: 0; startY: 100
PathLine { x: 400; y: 500 }
}
property real radius: 10
property var arrpoints : []
onPaint: {
var context = getContext("2d");
context.save()
if(arrpoints.length > 0){
for(var i=0; i < arrpoints.length; i++){
var point= arrpoints[i]
context.ellipse(point["x"]-radius, point["y"]-radius, 2*radius, 2*radius)
}
context.strokeStyle = Qt.rgba(0, 1, 1, 0)
context.fill()
context.stroke()
context.beginPath()
var start = arrpoints[0]
context.moveTo(start["x"], start["y"])
for(var j=1; j < arrpoints.length; j++){
var end= arrpoints[j]
context.lineTo(end["x"], end["y"])
context.moveTo(end["x"], end["y"])
}
context.closePath()
context.strokeStyle = "red"
context.lineWidth = 2;
context.stroke()
}
context.restore()
}
MouseArea {
id: mymouse
anchors.fill: parent
onClicked: {
mycanvas.arrpoints.push({"x": mouseX, "y": mouseY})
mycanvas.requestPaint()
}
}
}
Button {
text: "Clear Points"
anchors.top : mycanvas.bottom
onClicked: {
mycanvas.clear()
console.log( mycanvas.arrpoints )
}
}
}//Window

QML-Move item with arrow keys

I'm using Qt version 5.6.0 from windows 8.1. I draw a shape that has 4 sides and the ability to drag its apexes.I want to move apexes of shape with arrow keys.I use this code but it does not work.
Point.qml:
Item {
id: root
signal dragged()
property alias color :point.color
Rectangle {
id:point
anchors.centerIn: parent
width: 20
height: 20
opacity: 0.2
MouseArea {
anchors.fill: parent
drag.target: root
onPositionChanged: {
if(drag.active) {
dragged()
}
}
onClicked: point.focus=true;
}
// Keys.onPressed: {
// console.log("move");
// if (event.key === Qt.Key_Left) {
// console.log("move left");
// event.accepted = true;
// point.x-=1;
// }
// }
// Keys.onRightPressed: {
// console.log("move");
// point.x+=1;
// }
// Keys.onLeftPressed: {
// console.log("move");
// point.x-=1;
// }
Keys.onPressed: {
switch(event.key) {
case Qt.Key_Left: point.x-=1;
break;
case Qt.Key_Right: point.x+=1;
break;
case Qt.Key_Up: point.y-=1;
break;
case Qt.Key_Down: point.y+=1;
break;
}
}
focus: true;
}}
main.qml:
Point {
id: pointA
x: 50
y: 50
}
Point {
id: pointB
x: 250
y: 50
}
Point {
id: pointC
x: 250
y: 250
}
Point {
id: pointD
x: 50
y: 250
}
Item {
anchors.fill: parent
Canvas {
id: canvas
anchors.fill: parent
onPaint: {
var ctx = canvas.getContext('2d');
ctx.moveTo(pointA.x, pointA.y);
ctx.lineTo(pointB.x, pointB.y);
ctx.lineTo(pointC.x, pointC.y);
ctx.lineTo(pointD.x, pointD.y);
ctx.lineTo(pointA.x, pointA.y);
ctx.stroke();
}
Component.onCompleted: {
pointA.dragged.connect(repaint)
pointB.dragged.connect(repaint)
pointC.dragged.connect(repaint)
pointD.dragged.connect(repaint)
}
function repaint() {
var ctx = getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
requestPaint()
}
}
}
Update:
main.qml:
PhotoPreview {
id : photoPreview
anchors.fill : parent
//focus:true //visible
visible: capture
}
photopreview.qml:
Item {
id:mywin
property real defaultSize:mywin.width
property var currentFrame: undefined
property real surfaceViewportRatio: 1.5
focus: true
ScrollView {
anchors.fill: parent
flickableItem.interactive: true
frameVisible: true
highlightOnFocus: true
Flickable {
id: flick
anchors.fill: parent
contentWidth: parent.width
contentHeight: parent.height
property alias source :image.source
signal closed
Rectangle {
id: photoFrame
width: parent.width
height: parent.height
color:"transparent"
scale:defaultSize / parent.width
Behavior on scale { NumberAnimation { duration: 200 } }
Behavior on x { NumberAnimation { duration: 200 } }
Behavior on y { NumberAnimation { duration: 200 } }
smooth: true
antialiasing: true
Image {
id:image
anchors.fill: parent
fillMode: Image.PreserveAspectFit
smooth: true
}
PinchArea {
anchors.fill: parent
pinch.target: photoFrame
pinch.minimumRotation: -360
pinch.maximumRotation: 360
pinch.minimumScale: 0.1
pinch.maximumScale: 10
pinch.dragAxis: Pinch.XAndYAxis
property real zRestore: 0
onSmartZoom: {
if (pinch.scale > 0) {
photoFrame.rotation = 0;
photoFrame.scale = Math.min(mywin.width, mywin.height) / Math.max(image.sourceSize.width, image.sourceSize.height) * 0.85
photoFrame.x = flick.contentX + (flick.width - photoFrame.width) / 2
photoFrame.y = flick.contentY + (flick.height - photoFrame.height) / 2
zRestore = photoFrame.z
photoFrame.z = ++mywin.highestZ;
} else {
photoFrame.rotation = pinch.previousAngle
photoFrame.scale = pinch.previousScale
photoFrame.x = pinch.previousCenter.x - photoFrame.width / 2
photoFrame.y = pinch.previousCenter.y - photoFrame.height / 2
photoFrame.z = zRestore
--mywin.highestZ
}
}
MouseArea {
id: dragArea
hoverEnabled: true
anchors.fill: parent
drag.target: photoFrame
scrollGestureEnabled: false // 2-finger-flick gesture should pass through to the Flickable
onPressed: {
photoFrame.z = ++mywin.highestZ;
}
onWheel: {
if (wheel.modifiers & Qt.ControlModifier) {
photoFrame.rotation += wheel.angleDelta.y / 120 * 5;
if (Math.abs(photoFrame.rotation) < 4)
photoFrame.rotation = 0;
} else {
photoFrame.rotation += wheel.angleDelta.x / 120;
if (Math.abs(photoFrame.rotation) < 0.6)
photoFrame.rotation = 0;
var scaleBefore = photoFrame.scale;
photoFrame.scale += photoFrame.scale * wheel.angleDelta.y / 120 / 10;
}
}
}
}
Point {
id: pointA
x: image.width/4
y: image.height/4
color: "blue"
}
Point {
id: pointB
x: image.width/2
y: image.height/2
color: "blue"
}
Point {
id: pointD
x: image.width/4
y: image.height/2
color: "red"
}
Point {
id: pointC
x: image.width/2
y: image.height/4
color: "red"
}
Item {
anchors.fill: parent
Canvas {
id: canvas
anchors.fill: parent
onPaint: {
//...
}
Component.onCompleted: {
//...
}
function repaint() {
//..
}
}
}
}
}
}}
You are using ScrollView that inherit FocusScope, which needs to have focus:true to forward it.
You instead are setting focus to PhotoPreview, which is plain Item that are not supposed to be focused.
So you need to simple remove focus:visible and set it to ScrollView.
The better fix is to remove that redundant Item, and leave ScrollView as PhotoPreview root item, with all its properties and signals.
In Point.qml the key event handler tries to change the x, y of point, however point is a rectangle that anchors.centerIn: parent. The key event handler should change root x, y instead:
//in Point.qml
case Qt.Key_Left:
root.x-=1;
break;
Point now changes it's position when keyboard event is triggered. Next, Point needs to notify the Canvas in main.qml to repaint. Point can emit dragged signal after changing it's x, y:
//in Point.qml
Keys.onPressed: {
switch(event.key) {
case Qt.Key_Left:
root.x-=1;
dragged();
break;
case Qt.Key_Right:
root.x+=1;
dragged();
break;
case Qt.Key_Up:
root.y-=1;
dragged();
break;
case Qt.Key_Down:
root.y+=1;
dragged();
break;
}
}

Resources