QML ShapePath clear path elements - qt

I want to remove all path elements from my ShapePath. Since pathElements is a Qml list, the only way to modify it is by setting it to a new Javascript array. Therefore, I expected to be able to clear it by just assigning an empty array to it.
I tried path.pathElements = [], which does not work for me.
Then I tried path.pathElements = null, which does work (PathLine is no longer drawn), but it prints this ugly error message: QObject::connect: Cannot connect (nullptr)::changed() to QQuickShapePath::processPath()
Any other ideas?
Code to reproduce:
Shape {
anchors.fill: parent
ShapePath {
id: path
strokeWidth: 2
strokeColor: "green"
fillColor: "green"
Component.onCompleted: path.pathElements = []
PathLine { x: 50; y: 50 }
}
}

I filed a bug report at Qt and they confirmed my bug.
The workaround until this gets fixed is to first assign null, followed by [].
Shape {
anchors.fill: parent
ShapePath {
id: path
strokeWidth: 2
strokeColor: "green"
fillColor: "green"
Component.onCompleted: {
path.pathElements = null
path.pathElements = []
}
PathLine { x: 50; y: 50 }
}
}

Related

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

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

QML append new PathCurve elements to List<PathElements> in ShapePath

To be specific: I want to connect several Points on a Map with a spline curve. New points can be added with a Mouseclick and should also be connected to the existing Path. The points are stored within a model, so I can access them also in C++.
Unfortunately I can't figure out, how I can append new PathCurve elements to the existing List in the Shape::ShapePath Object.
I expected that something like this should work:
...
MapQuickItem {
coordinate: QtPositioning.coordinate(0.0000, 0.0000)
sourceItem: Shape {
id: myShape
anchors.fill: parent
vendorExtensionsEnabled: false
ShapePath {
id: myPath
strokeColor: "black"
strokeWidth: 2
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: 0; startY: 0
}
}
zoomLevel: 15
}
MouseArea {
anchors.fill: parent
onClicked: {
var coord = parent.toCoordinate(Qt.point(mouse.x,mouse.y))
myPath.pathElements.push( new PathCurve(mouse.x, mouse.y) ) //does not work
}
}
I also tried to fill the PathElements from C++, but the PathCurve class seems to be private and is only usable from within QML. Hardcoding PathCurve Elements works just fine like in every online example, but I want to dynamically modify the list of pathelements.
Any help would be appreciated very much!
You must dynamically components using the function createQmlObject, but for this you must bear in mind that it depends on the zoomLevel that you apply to the MapQuickItem the relation of sizes since that depends on the painting, the PathCurve does not use coordinates of the window but the coordinates of the Shape, and the Shape is painted according to how it is configured. So for in this case the zoomLevel of the MapQuickItem must be 0 or the same as the map. Considering the above, the solution is:
import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Shapes 1.12
import QtPositioning 5.9
import QtLocation 5.3
Window {
id: root
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Plugin {
id: mapPlugin
name: "osm" // "mapboxgl" "osm" "esri"
}
Map {
id: map
anchors.fill: parent
plugin: mapPlugin
zoomLevel: 14
center: QtPositioning.coordinate(59.91, 10.75) // Oslo
MapQuickItem {
id: map_item
coordinate: QtPositioning.coordinate(59.91, 10.75)
anchorPoint.x : 0
anchorPoint.y : 0
zoomLevel: map.zoomLevel
sourceItem: Shape {
id: myShape
anchors.fill: parent
vendorExtensionsEnabled: false
ShapePath {
id: myPath
strokeColor: "black"
strokeWidth: 2
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: 0; startY: 0
}
}
}
}
MouseArea {
id: mousearea
anchors.fill: map
onClicked: {
var item_pos = map.fromCoordinate(map_item.coordinate, false)
var pathcurve = Qt.createQmlObject('import QtQuick 2.12; PathCurve {}',
myPath);
pathcurve.x = mouse.x - item_pos.x
pathcurve.y = mouse.y - item_pos.y
myPath.pathElements.push(pathcurve)
}
}
}

How to change textfield property from 'onEditingFinished' in Qt QML

I am trying to alter the border color and width of my TextField depending on a vaildation check (javascript function that returns true if OK). If the user enters an invalid date of birth I wish to colour the Textfield box border red. Here is my qml definition:
TextField{
id: dob
placeholderText: qsTr("dd-mm-yyyy")
font.pixelSize: 14
Layout.fillWidth: true
style: TextFieldStyle {
textColor: "black"
background: Rectangle {
id:dobstyle
radius: 2
implicitWidth: 100
implicitHeight: 24
border.color: "black"
border.width: 1
}
}
inputMethodHints: Qt.ImhDate
onActiveFocusChanged: {
inputMask= "00-00-0000"
}
onEditingFinished: {
var datesplit = dob.text.split("-");
var jsDOB = new Date(datesplit[2],datesplit[1]-1,datesplit[0]);
var logichk = (isValidDateLogical(jsDOB));
var validchk = (isValidDate(dob.text))
if(!validchk || !logichk)
{
dobstyle.border.color="red"
dobstyle.border.width=3
}
}
selectByMouse: true
}
I am getting 'ReferenceError: dobstyle is not defined' Any idea how best to achieve this?
TextFieldStyle is a component, and what is inside does not exist for outside, what you must do is create a property in the TextField and make a binding, then only modify the property:
...
Layout.fillWidth: true
property color bordercolor: "black" // <---
property int borderwidth: 1 // <---
style: TextFieldStyle {
textColor: "black"
background: Rectangle {
id:dobstyle
radius: 2
implicitWidth: 100
implicitHeight: 24
border.color: dob.bordercolor // <---
border.width: dob.borderwidth // <---
}
}
...
onEditingFinished: {
var datesplit = dob.text.split("-");
var jsDOB = new Date(datesplit[2],datesplit[1]-1,datesplit[0]);
var logichk = isValidDateLogical(jsDOB));
var validchk = (isValidDate(dob.text))
if(!validchk || !logichk)
{
dob.bordercolor ="red" // <---
dob.borderwidth = 3 // <---
}
}

QML: How to move an object from a previous set postion using states

I have an object that I want to move from it's previously set position every time that particular state is set. I've tried making a separate property called xPos to get around the binding loop error which is set by the object's new position of x after the state is set, then entering a default state just to be able to switch back to that specific state again since calling the same state does nothing but it doesn't seem to work.
Here is a snippet of my code:
property int xPos: 0
states: [
State {
name: "nextStep"
PropertyChanges {
target: progressBar_Id
x: -1*(progressBar_Id.step.width) + xPos
}
},
State {
name: "previousStep"
PropertyChanges {
target: progressBar_Id
x: progressBar_Id.step.width + xPos
}
},
State {
name: "default"
PropertyChanges {
target: progressBar_Id
x: xPos
}
}
]
transitions: [
Transition {
from: "*"
to: "nextStep"
NumberAnimation {properties: "x"; easing.type: Easing.Linear; duration: 1000}
onRunningChanged: {
if(!running) {
xPos = progressBar_Id.x;
console.info("xPos = " + xPos);
state = "default";
}
}
},
Transition {
from: "*"
to: "previousStep"
NumberAnimation {properties: "x"; easing.type: Easing.Linear; duration: 1000}
onRunningChanged: {
if(!running) {
xPos = progressBar_Id.x;
console.info("xPos = " + xPos);
state = "default";
}
}
}
]
xPos seems to get set the first time from the console output but never applies the new xCoord to the object.
Explicitly specify the id of the item on which you wish to set the state, e.g:
idOfComponent.state = "default"
All QML Items and derivatives have a property called state, so the code needs to be more specific.
Actually came up with a much better alternative using a ListView.
ListView {
id: listView_Id
width: contentWidth
height: bubbleSize
anchors.centerIn: parent
layoutDirection: Qt.RightToLeft
orientation: ListView.Horizontal
spacing: 0
delegate: Item {
width: bubbleSize
height: width
ProgressBubble {
stateText: stateNumber
currentState: state
}
}
model: ListModel { id: progressModel_Id }
}
Another qml file:
progressModel.insert(0, {stateNumber: number++, state: "current"});
But I ran into another problem, is there anyway to change a state within a delegate after it's been added to the ListView?
I've already tried progressModel.set and progressModel.setProperty but doesn't seem to work.
state is a property for qml types, so when you are assigning "state" to "currentState" for "ProgressBubble", its taking the value of state in which " ProgressBubble" is currently present.
Rename "state" to something else like "presentState" and then try.
Moreover id of ListView model(progressModel_Id) and the one used to insert model elements(progressModel) in different file are different, both of them must refer to same id.
After these changes, you can try set property of model. Sample code snippet:
import QtQuick 2.0
Rectangle {
id: root
width: 360
height: 360
property int number: 1
ListView {
id: listView_Id
width: 100 //contentWidth // this creates property binding issue
height: 100 // bubbleSize // I was not sure about this value
anchors.centerIn: parent
layoutDirection: Qt.RightToLeft
orientation: ListView.Horizontal
spacing: 0
delegate: Item {
width: 100 // bubbleSize // I was not sure about this value
height: width
ProgressBubble {
state: "default"
stateText: stateNumber
currentState: presentState
MouseArea {
anchors.fill: parent
onClicked: {
progressModel_Id.set(index,{stateNumber: stateNumber, presentState: "not current"})
}
}
}
}
model: ListModel { id: progressModel_Id }
}
Component.onCompleted:
{
progressModel_Id.insert(0, {stateNumber: number++, presentState: "current"});
}
}

Clear QML anchor

I have a MouseArea that I want to start off centered and then have an absolute position once the up/down/left/right keys are pressed. My problem is that I don't know how to clear the anchor on the MouseArea so that I can specify an absolute position:
import QtQuick 2.0
import QtQuick.Window 2.0
Window {
id: screen
width: 360
height: 360
visible: true
Rectangle {
anchors.fill: parent
states: [
State {
name: "moved"
AnchorChanges {
target: mouseArea
anchors.bottom: undefined
anchors.left: undefined
anchors.right: undefined
anchors.top: undefined
}
}
]
MouseArea {
id: mouseArea
anchors.centerIn: parent
width: 250
height: 250
focus: true
onClicked: console.log("clicked!")
onPositionChanged: console.log("position changed!")
function moveMouseArea(x, y) {
mouseArea.x += x;
mouseArea.y += y;
mouseArea.state = "moved";
mouseAreaPosText.text = 'Mouse area was moved... new pos: '
+ mouseArea.x + ', ' + mouseArea.y;
}
Keys.onPressed: {
if (event.key === Qt.Key_Up)
moveMouseArea(0, -1);
if (event.key === Qt.Key_Down)
moveMouseArea(0, 1);
if (event.key === Qt.Key_Left)
moveMouseArea(-1, 0);
if (event.key === Qt.Key_Right)
moveMouseArea(1, 0);
}
Rectangle {
anchors.fill: parent
border.width: 2
border.color: "black"
color: "transparent"
}
Text {
id: mouseAreaPosText
anchors.centerIn: parent
}
}
}
}
At first I just tried setting mouseArea.anchors to undefined but got an error about anchors being a read-only property. I then discovered AnchorChanges, but I can't find a way to remove/clear the anchor; setting anchors.bottom etc. to undefined doesn't work.
According to docs, setting an anchor attribute to undefined should work. I don't quite get why AnchorChanges did not allow to set anchors.centerIn, but you can workaround it in your moveMouseArea function:
function moveMouseArea(x, y) {
mouseArea.anchors.centerIn = undefined; // <-- reset anchor before state change
mouseArea.pos.x += x;
mouseArea.pos.y += y;
mouseArea.state = "moved";
mouseAreaPosText.text = 'Mouse area was moved... new pos: '
+ mouseArea.pos.x + ', ' + mouseArea.pos.y;
}
Thanks for your help guys. I have found that setting undefined within a state works (if by works you mean only that it doesn't give errors), however once the element moves to yet another state, the anchors magically (and very frustratingly) return. This happens EVEN if you set all anchors undefined in the final state. As mentioned above however, setting undefined in the function before changing state works great. In my case, I set it in my mouseArea in onPressed.
onPressed: {
plotWindow04Frame.anchors.bottom = undefined
plotWindow04Frame.anchors.left = undefined
plotWindow04Frame.state = "inDrag"
}
I found it was not necessary to mention the anchor in the onReleased, just the next state.
onReleased: {
plotWindow04Frame.state = "dropped"
}
Also, I should mention, that the final "dropped" state does not mention anchors either, just opacity.
states: [
State {
name: "inDrag"
PropertyChanges {
target: plotWindow04Frame
opacity: .5
}
},
State {
name: "dropped"
PropertyChanges {
target: plotWindow04Frame
opacity: 1
}
}
]
transitions: Transition {
NumberAnimation { properties: "opacity"; duration:200 }
}
}
(The idea here was that these plot windows would become translucent (opacity: 0.5) while dragging, but return to opaque (opacity: 1) when the user drops them)
What is nice is that the plotwindow "rectangles" are initially anchored to the bottom of the GUI, but once the user picks them up, they can put them where ever they like.

Resources