How to ensure Popup is visible inside a QML Map - qt

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

Related

Qt QML MapItem Rotation issue

I have a QML OSM map and a MapQuickItem with Text source item:
MapQuickItem {
property alias rulerRotationAngle: rulerRotation.angle
id: rulerTextMapItem
visible: false
width: 2
height: 2
transform: Rotation {
id: rulerRotation
origin.x: rulerText.width/2;
origin.y: rulerText.height/2;
angle: 0
}
anchorPoint.x: rulerText.width/2
anchorPoint.y: rulerText.height/2
z:5
sourceItem: Text {
id: rulerText; horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Material.color(Material.Amber, Material.Shade100)
text: "0.0 km";
}
}
I also have two points (QtPositioning.coordinate) and I want the text to rotate depending on the angle of the straight line (MapPolyLine) drawn between those points:
function drawRuler()
{
rulerLine.path = [];
rulerLine.addCoordinate(r_firstpoint);
rulerLine.addCoordinate(r_secondpoint);
rulerTextMapItem.visible = true;
rulerTextMapItem.coordinate = QtPositioning.coordinate((r_firstpoint.latitude+r_secondpoint.latitude)/2, (r_firstpoint.longitude+r_secondpoint.longitude)/2);
var atan = Math.atan2(r_secondpoint.longitude-r_firstpoint.longitude, r_secondpoint.latitude-r_firstpoint.latitude);
var angle = ((atan*180)/Math.PI); //used by another MapItem
var textAngle = angle+270;
if(textAngle>90 & textAngle<270) { textAngle+=180 }
if(angle>90 & angle<270) { angle +=180 }
rulerTextMapItem.rulerRotationAngle = textAngle;
}
However, text rotates correctly only at angles that are multiples of 90 degrees. At an angle of 45 degrees, the text deviates from the mappolyline by about 10-20 degrees.
I have no clue why it happens and appreciate any help.
Tried to move transform.origin of MapQuickItem - angle difference only gets bigger.
Tried to use Math.Atan instead of Math.Atan2 - no difference.
The main issue is this line and the order of inputs:
var atan = Math.atan2(
r_secondpoint.longitude-r_firstpoint.longitude,
r_secondpoint.latitude-r_firstpoint.latitude);
latitude should come before longitude, i.e.
var atan = Math.atan2(
r_secondpoint.latitude-r_firstpoint.latitude,
r_secondpoint.longitude-r_firstpoint.longitude);
Generally speaking, to use Math.atan2() to convert to an angle, you need to use one of the following patterns:
let radians = Math.atan2(vectorY, vectorX)
let degrees = Math.atan2(vectorY, vectorX) * 180 / Math.PI
Also over large angles, you definitely should use project your angular coordinates to a flat projection, e.g. QtPositioning.coordToMercator. (This point was raised in one of the earlier comments).
For very small angles you can get away with it because the earth can be approximated to a flat earth directly from angular coordinates, but, as the area goes, this fact quickly disappears.
The following code demonstrates Math.atan2() and how it must work with (vectorY, vectorX) inputs. It has two draggable squares and you watch that the text will always follow the direction of the blue line no matter where the squares are:
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Shapes
Page {
id: page
width: 200; height: 200
property int startX: rect1.x + rect1.width / 2
property int startY: rect1.y + rect1.height / 2
property int finishX: rect2.x + rect2.width / 2
property int finishY: rect2.y + rect2.height / 2
Rectangle {
id: rect1
x: 40; y: 40
width: 40; height: 40
color: "red"
Drag.active: dragArea.drag.active
Drag.hotSpot.x: 20
Drag.hotSpot.y: 20
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
}
}
Rectangle {
id: rect2
x: 400; y: 250
width: 40; height: 40
color: "red"
Drag.active: dragArea2.drag.active
Drag.hotSpot.x: 20
Drag.hotSpot.y: 20
MouseArea {
id: dragArea2
anchors.fill: parent
drag.target: parent
}
}
Shape {
id: shape
ShapePath {
strokeWidth: 4
strokeColor: "blue"
startX: page.startX
startY: page.startY
PathLine {
x: page.finishX
y: page.finishY
}
}
}
Item {
x: (startX + finishX) / 2
y: (startY + finishY) / 2
rotation: Math.atan2(finishY - startY, finishX - startX) * 180 / Math.PI
Frame {
anchors.centerIn: parent
background: Rectangle {
border.color: "black"
}
Text {
text: "Hello World"
}
}
}
}
You can Try it Online!

how to make an updatable model in QML

I'm trying to make a QML widget that is interactive and will update its model.
Directly modifying the model does not work, so I create a updateModel(int index, real x, real y) signal in InteractiveGraph.
However the change done in onUpdateModel produces no effect on the UI.
I also tried with an onUpdateModel handler such as this: { var newModel = model.slice(); newModel[index].x = x; newModel[index].y = y; model = newModel } but the result is even worse as it breaks the self-dragging of the vertex item, and the model is not updated in the UI anyway.
Is the Array class not a good model for this use-case?
Is there some readymade model that is more appropriate?
InteractiveGraph.qml:
import QtQuick 2.0
/* An interactive undirected graph, whose vertices are draggable */
Item {
id: root
property alias model: repeater.model
property real size: 20
signal updateModel(int index, real x, real y)
Canvas { // render lines connecting the vertices
anchors.fill: parent
onPaint: {
var ctx = getContext("2d")
ctx.beginPath()
for(var i = 0; i < model.length; i++) {
if(i === 0) ctx.moveTo(model[i].x, model[i].y);
else ctx.lineTo(model[i].x, model[i].y);
}
ctx.closePath()
ctx.stroke()
}
}
Repeater { // instantiate items corresponding to vertices
id: repeater
delegate: Rectangle {
id: self
color: "red"
border.color: "#000"
width: size
height: size
x: modelData.x - size/2
y: modelData.y - size/2
Text { anchors.centerIn: parent; text: index }
MouseArea { // make items self-draggable
anchors.fill: parent
drag.target: self
}
// invert the x and y relation, and send back the change to model
onXChanged: root.updateModel(index, x + size/2, y + size/2)
onYChanged: root.updateModel(index, x + size/2, y + size/2)
}
}
}
main.qml:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
id: root
visible: true
width: 640
height: 480
title: qsTr("Hello World")
InteractiveGraph {
id: interactiveGraph
anchors.fill: parent
model: [
{x: 15, y: 20},
{x: 100, y: 220},
{x: 145, y: 230},
{x: 225, y: 130},
{x: 140, y: 88},
{x: 290, y: 60},
]
onUpdateModel: { model[index].x = x; model[index].y = y; model = model; }
}
}
I should have read Models and Views in Qt Quick, to find out the appropriate model to use is ListModel.
The only caveat is that we can't use x/y property names inside the ListElement child of ListModel, as when using ListElement, the variable modelData is not used anymore, but directly the property name is exposed, and x/y would collide with delegate's properties with the same name.
(In case of name clash, we can use model.fieldName instead of fieldName.)
InteractiveGraph.qml:
Item {
id: root
property alias model: repeater.model
property real size: 20
Canvas {
id: canvas
anchors.fill: parent
onPaint: {
var ctx = getContext("2d")
ctx.fillStyle = "#eee"; ctx.fillRect(0, 0, width, height)
ctx.beginPath()
for(var i = 0; i < model.count; i++) {
var item = model.get(i)
if(i === 0) ctx.moveTo(item.xPos, item.yPos)
else ctx.lineTo(item.xPos, item.yPos)
}
ctx.stroke()
}
}
Repeater {
id: repeater
delegate: Rectangle {
id: self
color: "red"; border.color: "#000"
x: xPos - size/2; y: yPos - size/2; width: size; height: size
onXChanged: canvas.requestPaint(); onYChanged: canvas.requestPaint()
Text { anchors.centerIn: parent; text: index }
MouseArea {
anchors.fill: parent
drag.target: self
onPositionChanged: model.set(index, {xPos: self.x + size/2, yPos: self.y + size/2})
}
}
}
}
main.qml:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
id: root
visible: true
width: 640
height: 480
title: qsTr("Hello World")
ListModel {
id: graphModel
ListElement {xPos: 15; yPos: 20}
ListElement {xPos: 100; yPos: 220}
ListElement {xPos: 145; yPos: 230}
ListElement {xPos: 225; yPos: 130}
ListElement {xPos: 140; yPos: 88}
ListElement {xPos: 290; yPos: 60}
}
InteractiveGraph {
id: interactiveGraph
anchors.fill: parent
model: graphModel
}
}

How to split a rectangle into 2 equal parts using QML animation?

Scenario: A rectangle moves horizontally with some speed and at certain position it should be split into 2 parts and then both should move separately with same speed.
I have tried adding new rectangle at that "certain" position and used opacity property and animated movement of 2 separate rectangles but not able to achieve what i wished for.
Is there any other way like when first rectangle reaches that certain position
while moving in x direction can that send a signal and 2nd rectangle start moving which should be invisible till that time.
Rectangle id: material (1st rectangle),
Rectangle id: material1(2nd rectangle)
code which i have written:
Rectangle {
id: material1
x:291
y:187
width: 71
height: 10
color: "#ff5930"
states: [
State{
name: "Visible"
PropertyChanges{target: material1; opacity: 1.0}
PropertyChanges{target: material1; visible: true}
},
State{
name:"Invisible"
PropertyChanges{target: material1; opacity: 0.0}
PropertyChanges{target: material1; visible: false}
}
]
transitions: [
Transition {
from: "Invisible"
to: "Visible"
PropertyAnimation {
target: material1
property: opacity
duration: 11000
}
PropertyAnimation {
target: material1
property: visible
duration: 0
}
}
]
SequentialAnimation on x {
loops: Animation.Infinite
PropertyAnimation{ from: 291 ; to: 1008
duration: 11000
}
}
}
Rectangle {
id: material
x: 159
y: 187
width: 71
height: 10
color: "#ff5930"
SequentialAnimation on x {
loops: Animation.Infinite
PropertyAnimation{ from: 159 ; to: 1008
duration: 11000
}
}
}
I see no code where you try to create 2 items from a given one. You have to copy an item before deletion keeping direction on whatever you want. I would do that in the following way:
MyItem.qml
import QtQuick 2.11
Rectangle {
signal itemBeforeDelete(Item item)
signal itemDestroyed(Item item)
id: item
width: 50
height: 50
radius: 25
color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
x: 50 + Math.round(Math.random() * 700)
y: 50 + Math.round(Math.random() * 500)
property int dx: Math.round(Math.random() * 10) - 5
property int dy: Math.round(Math.random() * 10) - 5
property int maxTicks: 100 + Math.round(Math.random() * 500)
function tick() {
item.maxTicks --;
x += item.dx;
y += item.dy;
if(x <= 0|| ((x + width)>= parent.width - dx))
item.dx *= (-1);
if(y <= 0|| ((y + height)>= parent.height - dy))
item.dy *= (-1);
if(item.maxTicks == 0) {
item.state = "target"
}
}
state: "initial"
states: [
State {
name: "initial"
PropertyChanges { target: item; opacity: 1 }
},
State {
name: "target"
PropertyChanges { target: item; opacity: 0 }
}
]
transitions: Transition {
from: "initial"
to: "target"
SequentialAnimation {
ScriptAction { script: item.itemBeforeDelete(item); }
PropertyAnimation { target: item; property:"opacity"; duration: 1000 }
ScriptAction { script: item.itemDestroyed(item); }
}
}
}
main.qml
import QtQuick 2.11
import QtQuick.Controls 2.4
ApplicationWindow {
id: mainWindow
width: 800
height: 600
visible: true
Timer {
id: timer
interval: 20
repeat: true
running: true
onTriggered: {
for(var i = 0;i < mainWindow.contentItem.children.length;i++)
{
mainWindow.contentItem.children[i].tick();
}
}
}
function createChildrenFromItem(item) {
var component = Qt.createComponent("MyItem.qml");
if (component.status === Component.Ready)
{
var offsetx = Math.round(Math.random() * 4) - 2;
var offsety = Math.round(Math.random() * 4) - 2;
var params = item ? {
"x": item.x,
"y": item.y,
"dx": item.dx + offsetx,
"dy": item.dy + offsety
} :{}
var obj = component.createObject(mainWindow.contentItem, params);
obj.itemBeforeDelete.connect(onBeforeDelete);
obj.itemDestroyed.connect(onDestroyed);
}
}
function onBeforeDelete(item)
{
createChildrenFromItem(item);
createChildrenFromItem(item);
}
function onDestroyed(item)
{
item.destroy();
}
Component.onCompleted: {
createChildrenFromItem();
}
}

QML how to get the position of Loader

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

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