How do I permanently delete objects from a QML canvas? - qt

Consider the QML code below, which allows me to insert points onto a blank QML canvas, with mouse-clicks and then clear all the input points and the corresponding pictures on the canvas, using a button placed in the upper-left hand corner
import QtQuick 2.5
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
Window{
id: root
width: 640
height: 480
visible: true
Canvas {
id: mycanvas
width: 1000
height: 1000
property var arrpoints : []
onPaint: {
var context = getContext("2d");
// Delete everything drawn before?
context.clearRect(0, 0, mycanvas.width, mycanvas.height);
// Render all the points as small black-circles
context.strokeStyle = Qt.rgba(0, 1, 1, 0)
// Draw all the points
for(var i=0; i < arrpoints.length; i++){
var point = arrpoints[i]
context.ellipse(point["x"], point["y"], 10, 10)
context.fill()
context.stroke()
}
}
// For mousing in points.
MouseArea {
id: mymouse
anchors.fill: parent
onClicked: {
// Record mouse-position into all the input objects
mycanvas.arrpoints.push({"x": mouseX, "y": mouseY})
mycanvas.requestPaint()
console.log( mycanvas.arrpoints )
} // onClicked
}// MouseArea
} // Canvas
Button {
text: "clear input"
onClicked: {
mycanvas.arrpoints.length = 0
mycanvas.requestPaint()
console.log( mycanvas.arrpoints )
}
}
}//Window
This code behaves quite strangely. Suppose I input a few points onto the canvas, and then click the "clear input" button. then as expected all the pictures (ie little circles corresponding to points) vanish from the canvas
and the arrpoints array is set to empty.
But when I start clicking on the canvas again, the cleared pictures are redrawn, alongside the new points being entered!! Why should this be? After printing to the console, I can still see arrpoints=[] so the problem should be with the clearing of the canvas in the onPaint section.
How do I tell QML to erase its canvas memory completely?

If you want to clean the Canvas you must reset the context. In this case, implement a function that does it and force the canvas to update.
import QtQuick 2.5
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
Window{
id: root
width: 640
height: 480
visible: true
Canvas {
id: mycanvas
width: 1000
height: 1000
property var arrpoints : []
onPaint: {
var context = getContext("2d");
// Delete everything drawn before?
context.clearRect(0, 0, mycanvas.width, mycanvas.height);
// Render all the points as small black-circles
context.strokeStyle = Qt.rgba(0, 1, 1, 0)
// Draw all the points
for(var i=0; i < arrpoints.length; i++){
var point = arrpoints[i]
context.ellipse(point["x"], point["y"], 10, 10)
context.fill()
context.stroke()
}
}
function clear() {
var ctx = getContext("2d");
ctx.reset();
mycanvas.requestPaint();
}
// For mousing in points.
MouseArea {
id: mymouse
anchors.fill: parent
onClicked: {
// Record mouse-position into all the input objects
mycanvas.arrpoints.push({"x": mouseX, "y": mouseY})
mycanvas.requestPaint()
console.log( mycanvas.arrpoints )
} // onClicked
}// MouseArea
} // Canvas
Button {
text: "clear input"
onClicked: {
mycanvas.arrpoints.length = 0
mycanvas.clear()
console.log( mycanvas.arrpoints )
}
}
}//Window

Related

How to perform dynamic conversion between mouse coordinates and world coordinates with QtQuick3D?

Description/ Code
I have a Qt Quick 3D View and corresponding scene that was designed to be compiled on Qt 6.3.0
import QtQuick
import QtQml
import QtQuick3D
import QtQuick3D.Helpers
Window {
width: 800
height: 600
visible: true
property var selectedItem
property bool mousePressed: false
function multiply_vectors(vec1, vec2) {
return Qt.vector3d(vec1.x * vec2.x, vec1.y * vec2.y, vec1.z * vec2.z);
}
View3D {
renderMode: View3D.Inline
camera: camera
anchors.fill: parent
width: 800
height: 600
x: 0
y: 0
id: view
environment: SceneEnvironment {
clearColor: "black"
backgroundMode: SceneEnvironment.Color
depthTestEnabled: false
depthPrePassEnabled: true
}
Model {
id: rootEntity
pickable: true
source: "#Cube"
materials: PrincipledMaterial {
baseColor: "red"
roughness: 0.1
}
position: Qt.vector3d(25.0, 15.0, -60.0)
scale: Qt.vector3d(1.0, 1.0, 1.0)
}
PerspectiveCamera {
id: camera
position.z: 330.0
position.y: 0.75
eulerRotation.x: -12
clipNear: 0.0
clipFar: 1600.0
}
MouseArea {
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
id: mouseArea
onPressed: function (mouse) {
var result = view.pick(mouse.x, mouse.y);
if (result.objectHit) {
selectedItem = result.objectHit;
mousePressed = true;
} else {
mousePressed = false;
}
}
onMouseXChanged: function(mouse) {
if (mousePressed) {
var viewCoords = view.mapFromGlobal(mouseArea.mapToGlobal(mouse.x, mouse.y));
var sceneCoords = Qt.vector3d(viewCoords.x, viewCoords.y, 0);
var worldCoords = view.mapTo3DScene(sceneCoords);
worldCoords.z = selectedItem.z
selectedItem.position = multiply_vectors(worldCoords, Qt.vector3d(Math.abs(camera.z - selectedItem.z), Math.abs(camera.z - selectedItem.z), 1.0))
}
}
onReleased: function (mouse) {
mousePressed = false
}
}
Component.onCompleted: {
camera.lookAt(rootEntity)
}
}
}
Overview
The use case is that whenever the mouse is pressed while pointing at the cube, whenever the mouse moves it will cause the cube to move along with it to the corresponding point in the 3d Scene.
This works great when looking from a point that is on the same z-axis. However when looking at the object from a point say along the x-axis, the model will move along the x-axis instead of following the mouse position.
Question
How can I modify the business logic in onMouseXChanged: function(mouse) { to correctly transform the matrix (or equivalent transform) to consistently match the mouse position irregardless of the camera's position relative to the Model?
If I understood you correctly, you need to move the object with the mouse parallel to the camera regardless of the camera position and model scaling? I admit that I don't have a solution, but still it's better than the original code. First of all, do not set the clipNear to 0, it would make the frustum degenerate and break the projection math.
Secondly, I would suppose that the code which sets the object position should look like
selectedItem.position = view.mapTo3DScene(
Qt.vector3d(mouse.x, mouse.y,
view.mapFrom3DScene(selectedItem.position).z))
The docs say that mapFrom3DScene/mapTo3DScene should interpret the z coordinate as the distance from the near clip plane of the frustum to the mapped position. However when I move it towards the sides of the window the object gets larger, whereas it should get smaller.
Here's the complete code with a few corrections of mine:
import QtQuick
import QtQml
import QtQuick3D
import QtQuick3D.Helpers
Window {
width: 800
height: 600
visible: true
property var selectedItem
property bool mousePressed: false
View3D {
renderMode: View3D.Inline
camera: camera
anchors.fill: parent
width: 800
height: 600
x: 0
y: 0
id: view
environment: SceneEnvironment {
clearColor: "black"
backgroundMode: SceneEnvironment.Color
depthTestEnabled: false
depthPrePassEnabled: true
}
Model {
id: rootEntity
pickable: true
source: "#Cube"
materials: PrincipledMaterial {
baseColor: "red"
roughness: 0.1
}
position: Qt.vector3d(25.0, 15.0, -60.0)
scale: Qt.vector3d(2.0, 1.0, 0.5)
}
PerspectiveCamera {
id: camera
position.z: 330.0
position.y: 100
position.x: 700
eulerRotation.x: -12
// Note 1: clipNear shouldn't be 0, otherwise
// it would break the math inside the projection matrix
clipNear: 1.0
clipFar: 1600.0
}
MouseArea {
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
id: mouseArea
onPressed: function (mouse) {
var result = view.pick(mouse.x, mouse.y);
if (result.objectHit) {
selectedItem = result.objectHit;
mousePressed = true;
} else {
mousePressed = false;
}
}
onPositionChanged: function(mouse) {
if (mousePressed) {
// Note 2: recalculate the position, since MouseArea has
// the same geometry as View3D we can use coords directly
selectedItem.position = view.mapTo3DScene(
Qt.vector3d(mouse.x, mouse.y,
view.mapFrom3DScene(selectedItem.position).z))
}
}
onReleased: function (mouse) {
mousePressed = false
}
}
Component.onCompleted: {
camera.lookAt(rootEntity)
}
}
}
After spending a while experimenting with different approaches, I found that mapping the mouse coordinates to the 3d space wasn't fully supported by the Qt API in terms of when the mouse is not fixed over an active object.
So, instead, the way that I made a workout was by casting a new RayCast each time the mouse moves and storing the offset when the mouse is pressed originally and then translating the item based on the result of the raycast and lining up the offset by translating by the normalized matrix with a small scalar.
onMouseXChanged: function (mouse) {
if (mousePressed) {
if (selectedItem != null) {
var result = view.pick(mouse.x, mouse.y)
if (result.objectHit) {
if (result.objectHit == selectedItem) {
var mouseGlobalPos = mouseArea.mapToGlobal(
mouse.x, mouse.y)
var mouseViewPos = view.mapFromGlobal(
mouseGlobalPos)
var mouseScenePos = result.scenePosition
var resultPos = result.position
/* here we subtract the result of the new raycast by the starting offset and then normalize
* the result and multiply it by a scalar 3 to determine the amount of offset the Model
* under the mouse is from where the mouse was originally pressed, so we can translate it */
var differencePos = resultPos.minus(
startMousePressSelectedItemLocalDragOffset).normalized(
).times(3)
selectedItem.position = selectedItem.position.plus(
differencePos)

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

Line colors overlap when changed

I am working on an QML program in which I change the color and the border color of a polygon painted on a Canvas. I have 2 buttons that change the colors, ie. 1 button making the border color red and the other one blue.
My problem is that each time I change the color set, the border of the polygon seems "corrupted", such as the used border colors mixed with each other. The drawn polygon is being resized each time I resize the window. So when I resize, its being repainted I believe. The colors are getting fixed at that point.
My question is: is there a way to disable the overlapping or is there a way to manually force the redrawing of all Canvases in the project?
Ucolors.qml:
import QtQuick 2.9
/**
* #brief Holds the color parameters of the whole UI
*
*/
Item
{
property var canvas
property var text
property var spiderBckgrnd
property var spiderLines
}
main.qml
Ucolors
{
id: colDay
canvas: "#eaedf1"
text: "#0e0e12"
spiderBckgrnd: "#f7fbff"
spiderLines: "#C8CBD0"
}
Ucolors
{
id: colNight
canvas: "#22212c"
text: "#ffffff"
spiderBckgrnd: "#0e0e12"
spiderLines: "#3C3C3F"
}
property var colGlob: colDay
Button
{
id: btn1
anchors.left: parent.left
text: "button1"
onClicked:
{
colGlob = colNight;
}
}
Button
{
id: btn2
anchors.left: btn1.right
text: "button2"
onClicked:
{
colGlob = colDay;
}
}
Then in the code colors are set like this: some_property: colGlob.spiderLines
If you do not explicitely call clearRect() on the context object of your Canvas, any drawing is painted on the existing content.
Since you're drawing a polygon, some antialiasing pixels are added on the edges to get smooth lines. Those pixels are semi-transparent, so when you draw the same polygon over the existing one with another color, color blending occurs on the edges; hence the "corrupted" appearance.
When you change the height or width of the canvas, the context is implicitely cleared, so the "corrupted" edge disappears.
An easy fix is to call clearRect in the onPaint handler of your Canvas.
Canvas
{
id: canvas
onPaint: {
var ctx = getContext("2d")
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height)
// Draw anything you want
...
}
}
Here is a small example that reproduces your problem, and show how it is fixed by calling clearRect
import QtQuick 2.10
import QtQuick.Window 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
ApplicationWindow {
id: root
visible: true
width: 400
height: 200
property string color: "red"
onColorChanged: {
canvas.requestPaint()
}
ColumnLayout
{
anchors.fill: parent
anchors.margins: 20
spacing: 20
RowLayout
{
Layout.alignment: Qt.AlignHCenter
Repeater
{
model: ["red", "blue", "yellow"]
Button
{
text: modelData
highlighted: root.color == modelData
onClicked: {
root.color = modelData
}
}
}
CheckBox
{
id: clearBeforeRepaintCb
text: "Clear before paint"
}
}
Canvas
{
id: canvas
Layout.fillHeight: true
Layout.fillWidth: true
onPaint: {
var ctx = getContext("2d")
if(clearBeforeRepaintCb.checked)
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.strokeStyle = root.color
ctx.lineWidth = 10
ctx.beginPath()
ctx.moveTo(10, height/2)
ctx.lineTo(width-10, height/2+3)
ctx.stroke()
}
}
}
}

Updating a QML canvas

I am a QML / Javascript noob, and would like some help with that.
I want to insert some points (represented as small black circles) onto a white QML canvas element and then run an algorithm on them (such as finding convex hulls via an external geometric library)
Here is my QML code.
import QtQuick 2.5
import QtQuick.Window 2.2
Window{
id: root
width: 640
height: 480
visible: true
Canvas {
width: 1000
height: 1000
onPaint: {
var context = getContext("2d");
}
MouseArea {
id: mymouse
anchors.fill: parent
property var arrpoints : []
onClicked: {
// Record mouse-position
arrpoints = arrpoints.concat([mouseX, mouseY])
console.log(arrpoints)
}
}
}
}
So far the above code, opens up a window, with a QML canvas on it, and can keep track of the positions on the canvas (via the array arrpoints) where I single-clicked with my mouse, and outputs the array of clicked-points to the console.
But now, everytime the arrpoints changes, how do I 'tell' QML to draw a small black circle at that point immediately?
I would have thought the onPaint part of QML would trigger the rendering of the new state immediately, but it seems that part is only for the initial drawing on the canvas, before the user starts interacting with it.
You have to call the canvas requestPaint() function to force the painting. It is also advisable to save the data of the positions appropriately: {"x": x_value, "y": y_value}
import QtQuick 2.5
import QtQuick.Window 2.2
Window{
id: root
width: 640
height: 480
visible: true
Canvas {
id: canvas
width: 1000
height: 1000
onPaint: {
var context = getContext("2d")
context.strokeStyle = Qt.rgba(0, 0, 0, 1)
context.lineWidth = 1
for(var i=0; i < mymouse.arrpoints.length; i++){
var point = mymouse.arrpoints[i]
context.ellipse(point["x"]-5, point["y"]-5, 10, 10)
}
context.stroke()
}
MouseArea {
id: mymouse
anchors.fill: parent
property var arrpoints : []
onClicked: {
arrpoints.push({"x": mouseX, "y": mouseY})
canvas.requestPaint()
}
}
}
}

MultiPointTouchArea and Canvas

I am playing with MultiPointTouchArea in a Canvas component to make a little drawing exercise. The code below works but the onReleased event is getting called twice and I don't understand why.
From the log statements below, I see it gets called first with one TouchPoint, then again with two TouchPoints – the x and y positions are the same for all. Also the id of these touchPoints are undefined.
I don't get it. Since I define a maximumTouchPoints and am testing with just one touch (I am testing this on my laptop using a trackpad, with just one "finger".) :
why am I getting multiple touchpoints
why is onReleased getting called twice?
why are the touchPoints id undefined, since I have defined my touchPoints?
qml: released 1
qml: undefined 386.66015625 207.6640625
qml: is this touch1? true
qml: released 2
qml: undefined 386.66015625 207.6640625
qml: is this touch1? true
qml: undefined 386.66015625 207.6640625
qml: is this touch1? true
import QtQuick 2.5
import QtQuick.Controls 1.4
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Canvas")
Canvas {
id: canvas
anchors.fill: parent
property real lastX: 0
property real lastY: 0
onPaint: {
var ctx = getContext("2d")
ctx.lineWidth = 1
ctx.strokeStyle = "blue"
ctx.beginPath()
ctx.moveTo(lastX,lastY)
ctx.lineTo(touch1.x,touch1.y)
ctx.stroke()
canvas.lastX = touch1.x;
canvas.lastY = touch1.y;
}
function clearCanvas() {
var ctx = canvas.getContext("2d")
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
MultiPointTouchArea {
anchors.fill: parent
minimumTouchPoints: 1
maximumTouchPoints: 1
touchPoints: [TouchPoint { id: touch1 }]
onPressed: {
canvas.lastX = touch1.x;
canvas.lastY = touch1.y;
canvas.clearCanvas();
}
onReleased: {
console.log("released", touchPoints.length); // CALLED TWICE?
var tp;
for (var i = 0; i < touchPoints.length; i++) {
tp = touchPoints[i];
console.log("\t",tp.id, tp.x, tp.y);
console.log("is this touch1?", tp === touch1);
}
}
onUpdated: canvas.requestPaint();
}
}
}
So it seems that there are open bugs for these issues
The "two release events" issue has been reported and is open: https://bugreports.qt.io/browse/QTBUG-44781
The "no previousX, previousY" TouchPoint issue is there too: https://bugreports.qt.io/browse/QTBUG-41692

Resources