An optimal way to highlight a circle segment in QML? - qt

I'm very new to QML and I want to make a basic application that consists of a segmented circle with 20-30 segments (pizza slices) and a counter. The number on the counter is the number of segment being highlighted. I found a few ways to make segmented circles in other questions but unfortunately none of them seem to work for my assignment.
The only way I see making it right now is by redrwing all the segments every time the counter is changed, and changing the color of the needed segment. So is there an optimal way to implement this?

To reduce the complexity, let's work through a simplified version of the problem:
Assume there are 6 pieces
Assume we want to draw piece 2
Assume we want to fit it in a 300x300 rectangle
Here's the math:
Each piece will occupy 60 degrees (i.e. 360 / 6)
Piece 2 will occupy angles from 120 to 180
To render the piece the drawing will be:
From the center point (150, 150)
Then (150 + 150 * cos(120), 150 + 150 * sin(120))
Then (150 + 150 * cos(180), 150 + 150 * sin(180))
Then back to the center point (150, 150)
Instead of a straight line, we want to draw a curve line between points 2 and points 3.
To render this, we can use Shape, ShapePath, PathLine, and PathArc.
To generalize, we can replace 6 with 20 and generalize all formulas accordingly. To draw 20 piece slices, we can make use of a Repeater, e.g.
Repeater {
model: 20
PizzaPiece {
piece: index
}
}
To polish it off, I added a Slider so you can interactively change the number of pieces you want from 0-20 and set the color to "orange", otherwise it will be a light yellow "#ffe".
Repeater {
model: 20
PizzaPiece {
piece: index
fillColor: index < slider.value ? "orange" : "#ffe"
}
}
Slider {
id: slider
from: 0
to: 20
stepSize: 1
}
As an extra bonus, I added a TapHandler so that each piece is clickable. If you leave the mouse pressed down, the piece will appear "red" until you release the mouse.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
id: page
property int pieces: 20
Rectangle {
anchors.centerIn: parent
width: 300
height: 300
border.color: "grey"
Repeater {
model: pieces
PizzaPiece {
anchors.fill: parent
anchors.margins: 10
pieces: page.pieces
piece: index
fillColor: pressed ? "red" : index < slider.value ? "orange" : "#ffe"
onClicked: {
slider.value = index + 1;
}
}
}
}
footer: Frame {
RowLayout {
width: parent.width
Label {
text: slider.value
}
Slider {
id: slider
Layout.fillWidth: true
from: 0
to: pieces
value: 3
stepSize: 1
}
}
}
}
//PizzaPiece.qml
import QtQuick
import QtQuick.Shapes
Shape {
id: pizzaPiece
property int pieces: 20
property int piece: 0
property real from: piece * (360 / pieces)
property real to: (piece + 1) * (360 / pieces)
property real centerX: width / 2
property real centerY: height / 2
property alias fillColor: shapePath.fillColor
property alias strokeColor: shapePath.strokeColor
property alias pressed: tapHandler.pressed
property real fromX: centerX + centerX * Math.cos(from * Math.PI / 180)
property real fromY: centerY + centerY * Math.sin(from * Math.PI / 180)
property real toX: centerX + centerX * Math.cos(to * Math.PI / 180)
property real toY: centerY + centerY * Math.sin(to * Math.PI / 180)
signal clicked()
containsMode: Shape.FillContains
ShapePath {
id: shapePath
fillColor: "#ffe"
strokeColor: "grey"
startX: centerX; startY: centerY
PathLine { x: fromX; y: fromY }
PathArc {
radiusX: centerX; radiusY: centerY
x: toX; y: toY
}
PathLine { x: centerX; y: centerY }
}
TapHandler {
id: tapHandler
onTapped: pizzaPiece.clicked()
}
}
You can Try it Online!

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!

QML: Show object over non siblings

I have a somehow very hard to solve problem in my QML code. I will summarize it the best I can since it is very long code..
I write a color picker qml file that is called when the user wants to pick a color. This is done in a big rectangle with little rectangles in it evenly distributed that have flat colors to choose from.
I have a parent rectangle, 1 outer repeater and nested in this repeater is another inner repeater that creates little rectangle in a row. The outer repeater places the inner repeaters under another so it fills the rectangle with little rectangles fully, preferably with different colors.
Every little rectangle also has a function that highlights itself with an animation. This animation is a circle that gets bigger than the rectangle itself. This is done so when the user clicks a color from e.g. a color history on the right, it should highlight the corresponding colors rectangle if is there.
Now, the problem:
No matter what z values I use, this animation won't show above the other rectangles. It will get blocked by neighboring rectangles. I have researched and it seems that z values don't account for non siblings, just for all items in a single parent.
Here's some code that leaves out all the unnecessary junk.
To note is that every rectangle has its own animation and mousearea.
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
color: 'black'
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
id: parentRectangle
width: 400
height: 400
property int numberOfBoxesInARow: 5
property int numberOfBoxesInAColumn: 5
Repeater {
id: outerRepeater
model: parentRectangle.numberOfBoxesInARow
Repeater {
id: innerRepeater
model: parentRectangle.numberOfBoxesInAColumn
y: parentRectangle.height / parentRectangle.numberOfBoxesInAColumn * outerIndex
x: 0
height: parent.height / parentRectangle.numberOfBoxesInAColumn
width: parent.width
property int outerIndex: index
Rectangle {
id: individualRectangle
color: Qt.rgba(1 / (outerIndex + 1), 0, 1 / (index + 1), 1)
x: parentRectangle.width / parentRectangle.numberOfBoxesInARow * index
y: outerIndex * parentRectangle.height / parentRectangle.numberOfBoxesInAColumn
width: parentRectangle.width / parentRectangle.numberOfBoxesInARow
height: parent.height / parentRectangle.numberOfBoxesInAColumn
Component.onCompleted: {
console.log("Rectangle at " + outerIndex + "|" + index + " created, width / height: " + width.toFixed(2) + "| " + height.toFixed(2))
}
MouseArea {
anchors.fill: parent
onClicked: {
highlightAnimation.running = true
}
}
Rectangle {
id: highlightCircle
visible: highlightAnimation.running
color: 'white'
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
property real size: 0
width: size
height: size
radius: size/2
}
PropertyAnimation {
id: highlightAnimation
target: highlightCircle
property: 'size'
from: 200
to: 0
duration: 500
}
}
}
}
}
}
Ok, to paint an item over another one you have at least 2 ways:
z (for siblings items only)
creating order (the last created is the highest)
I guess that the second way is preferable for you. So you just need to create the circle item after all others. For example:
Repeater {
id: outerRepeater
Repeater {
id: innerRepeater
...
MouseArea {
anchors.fill: parent
onClicked: {
highlightCircle.item = individualRectangle;
highlightAnimation.running = true;
}
}
}
}
}
Rectangle {
id: highlightCircle
property var item
...
anchors.horizontalCenter: item ? item.horizontalCenter : undefined
anchors.verticalCenter: item ? item.verticalCenter : undefined
}
PropertyAnimation {
id: highlightAnimation
...
}

How to create a shape that moves along the outline of a circle in QML

I use QT 5.11.3 and have a big problem...
I want to implement a shape that moves along the outline (or a specific path) of a circle using mouse drag.
I have referenced the example in the link below.
How to make an item drag inside a circle in QML?
import QtQuick 2.5
import QtQuick.Window 2.2
Window {
id: root
visible: true
width: 640
height: 480
title: qsTr("Hello World")
property int radius: 100
Rectangle {
id: circle
width: 2 * radius
height: 2 * radius
radius: root.radius
color: 'blue'
}
Rectangle {
id: mark
width: 20
height: 20
x: (dragObj.dragRadius <= root.radius ? dragObj.x : root.radius + ((dragObj.x - root.radius) * (root.radius / dragObj.dragRadius))) - 10
y: (dragObj.dragRadius <= root.radius ? dragObj.y : root.radius + ((dragObj.y - root.radius) * (root.radius / dragObj.dragRadius))) - 10
color: 'red'
MouseArea {
id: markArea
anchors.fill: parent
drag.target: dragObj
onPressed: {
dragObj.x = mark.x + 10
dragObj.y = mark.y + 10
}
}
}
Item {
id: dragObj
readonly property real dragRadius: Math.sqrt(Math.pow(x - root.radius, 2) + Math.pow(y - root.radius, 2))
x: root.radius
y: root.radius
onDragRadiusChanged: console.log(dragRadius)
}
}
But MouseArea has been moved not only to the outline of the circle but also to the inside of the circle.
If you have an example of moving along the contour of a shape using MouseArea or Mouse drag, please advise.

How to add a rectangle to a QML ChartView?

I want to put a number of rectangles as overlays to show regions of interest on a ChartView of ScatterSeries. However when I try to do this it is clearly using a different coordinate system to that of the ScatterSeries as it is drawn in a completely different place.
For example the following is intended to draw a rectangle that captures all of the ScatterSeries but it just draws a small green rectangle top left as shown in the screenshot.
ChartView {
id: view
Layout.fillWidth : true
Layout.fillHeight : true
Rectangle {
id: rec
x: 30
y: 50
width: 40
height: 10
color: "green"
}
ScatterSeries{
id: series
XYPoint { x: 30; y: 50 }
XYPoint { x: 50; y: 60 }
XYPoint { x: 60; y: 50 }
XYPoint { x: 70; y: 60 }
axisX: ValueAxis {
min: 0
max: 100
}
axisY: ValueAxis {
min: 0
max: 100
}
}
The documentation suggests that the rectangle should use the coordinate system of the parent ChartView. I assume I actually want it to use the coordinate system of the ChartView scene. How do I do this?
To translate from the ScatterSeries coordinate system to pixel coordinates to place a child in ChartView use mapToPosition(...):
function updateRectangle() {
var topLeftPoint = view.mapToPosition(Qt.point(30,60), series)
var bottomRightPoint = view.mapToPosition(Qt.point(70,50), series)
rec.x = topLeftPoint.x
rec.y = topLeftPoint.y
rec.width = bottomRightPoint.x - topLeftPoint.x
rec.height = bottomRightPoint.y - topLeftPoint.y
}
Where series is your ScatterSeries and rec is your Rectangle.
Invoke the update function whenever the chart points get recalculated (e.g. after creation and size changes).
See also related question How to map QChart coordinate to QChartView point?

Snap text to a point

In my test application I am drawing triangles. In the down left corner of this triangle (lest call it nr 2) I need to add text that is just under the line. If the triangle rotates, the text should rotate as well. Heres an "explanation" image:
And here is the code:
import QtQuick 2.9
import QtQuick.Window 2.2
import QtGraphicalEffects 1.0
Window
{
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Rectangle
{
id: rectMain
anchors.centerIn: parent
width: parent.width
height: parent.height
color: "white"
Item
{
anchors.fill: parent
rotation: 0
property real trheight: 0;
property real hfBase: 0;
Canvas
{
id: canvas
anchors.fill: parent
// set properties with default values
property real hFactor: 1 // height factor
property real trbase: 200
property color strokeColor: "black"
property color fillColor: "yellow"
property int lineWidth: 1
property real alpha: 1
property real parentWidth: parent.width; // try
property real parentHeight: parent.height;
onStrokeColorChanged: requestPaint();
onFillColorChanged: requestPaint();
onLineWidthChanged: requestPaint();
onPaint:
{
hFactor = Math.abs(hFactor)
var ctx = getContext("2d") // get context to draw with
ctx.clearRect(0, 0, width, height); // remove what is painted so far
ctx.lineWidth = lineWidth
ctx.strokeStyle = strokeColor
ctx.fillStyle = fillColor
ctx.globalAlpha = alpha
ctx.save();
ctx.beginPath();
ctx.translate(parentWidth / 2, parentHeight / 2);
ctx.moveTo(0, 0);
// drawing part, first calculate height using Pythagoras equation
parent.trheight = (Math.sqrt(Math.pow(trbase, 2) -
Math.pow(trbase / 2, 2))) * hFactor;
parent.hfBase = trbase * hFactor;
ctx.lineTo(parent.hfBase / -2, parent.trheight); // left arm
ctx.lineTo(parent.hfBase / 2, parent.trheight); // right arm
ctx.closePath(); // base drawn automatically
ctx.fill();
ctx.stroke();
ctx.restore();
}
}
DropShadow
{
anchors.fill: canvas
horizontalOffset: 0
verticalOffset: 3
radius: 3
samples: 7
color: "#80000000"
source: canvas
}
}
Text
{
//x: ?
//y: ?
font.family: "RobotoCondensed-Regular";
font.pointSize: 12;
text: qsTr("TEST");
opacity: 0.6;
}
}
}
So ideally the end effect would look like this (edited in paint):
Then on 30 degree rotation like this:
The easiest way would be to have the Text as a child item of the item that is rotated. Then you could just position it at the bottom of it and it would rotate with its parent.
However, your canvas covers the whole window, so your only option (given the current code) is to draw the text when you draw the shape. You can draw text with text(), fillText() or strokeText().
I would suggest rewriting your code to use the first approach I mentioned. Give each shape its own canvas, and make the text a child of the Canvas item (or its parent if you don't want the drop shadow effect to be applied to the text). Rotating an item is much cheaper than rotating by repainting a Canvas.

Resources