In a QML application I have an item that is moving around the screen (not rotating). I want to display an indicator that rotates around this item, pointing away from the center of the screen, a fixed distance away from the center of the item.
The following simplified QML application performs this goal, by making the indicator a child of the item, and translating it to the desired location. However, when I try to rotate the indicator (the commented-out code) I cannot find any values for origin.x and .y that work. It feels like the QML scene graph calculates X/Y positioning in a way unlike any I've experienced.
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
id: win
visible:true; width:600; height:300
property real padding: 50
property real angle: 0
property real _rads: angle * Math.PI/180
Timer {
interval:50; running:true; repeat:true
onTriggered:win.angle = (new Date/50) % 360
}
Rectangle {
id:object; color:'blue'
width:50; height:width
property real xOffset: Math.cos(_rads)
property real yOffset: Math.sin(_rads)
x: win.width/2 + xOffset * (win.width/2 - padding*2)
y: win.height/2 + yOffset * (win.height/2 - padding*2)
Rectangle {
id:indicator; color:'red'
property real centerOffset: 40
width:10; height:width*2
x: object.width/2 + object.xOffset * centerOffset - width/2
y: object.height/2 + object.yOffset * centerOffset - height/2
// transform: Rotation { origin.x:0; origin.y:0; angle:win.angle }
}
}
}
I've tried making the indicator not be a child of the item. I've tried using Translate in the transform stack instead of X/Y positions. All of them result in amusing-but-incorrect rotations.
How can I simply rotate the indicator around its own center, or otherwise achieve my goal?
You might think of it as a clock and build yourself a clockhand.
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
id: win
visible:true; width:600; height:300
property real padding: 50
property real angle: 0
property real _rads: angle * Math.PI/180
Timer {
interval:50; running:true; repeat:true
onTriggered:win.angle = (new Date/50) % 360
}
Rectangle {
id:object; color:'blue'
width:50; height:width
property real xOffset: Math.cos(_rads)
property real yOffset: Math.sin(_rads)
x: win.width/2 + xOffset * (win.width/2 - padding*2)
y: win.height/2 + yOffset * (win.height/2 - padding*2)
Text {
width: 250
height: 250
x: -100
y: -100
text: '▲'
color: 'red'
font.pixelSize: 20
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignTop
transform: Rotation {
angle: win.angle + 90
origin.x: 125
origin.y: 125
}
}
Text {
x: 15
y: -125
width: 20
height: 20
text: '▲'
color: 'red'
font.pixelSize: 20
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
transform: Rotation {
angle: win.angle + 90
origin.x: 10
origin.y: 150
}
}
Rectangle {
id: clockhand
width: 1
height: 100
color: 'black'
anchors {
centerIn: parent
}
rotation: win.angle + 90
Text {
text: '▲'
color: 'red'
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.top
bottomMargin: -5
}
font.pixelSize: 20
}
}
}
}
Just turn the Clockhand into an Item and remove the color, to make it invisible.
Similar to #derM I have a solution that makes use of Item.rotation. However, with regard to the rest, I have generalized it by avoiding cos and sin since they are not required.
To demonstrate I create 3 SVG images crosshairs.svg, marker.svg, and triangle.svg. I placed crosshairs.svg in the center of the screen. I animate marker.svg by making it bounce around the screen "pong" style. Now, the secret sauce is the placement and orientation of triangle.svg.
To place an offset a triangle relative to the marker. I put the triangle Image inside an Item component. The Item component has no area, it merely has x, y, and rotation set. The Image component is placed relative to the Item and we need to compute its relative placement.
Because the triangle.svg is 16x16, I placed it at (20, -8) relative to the marker. If I had chosen (-8, -8) the SVG would sit on top of the marker. Because I put it at (20, -8) it puts it beyond the marker. Lastly, I compute the rotation using Math.atan2() on the vector between the marker and the crosshairs:
Item {
x: marker.x
y: marker.y
rotation: Math.atan2(
marker.y - crosshairs.y,
marker.x - crosshairs.x
) * 180 / Math.PI
Image {
source: "triangle.svg"
x: 20
y: -8
//cache: false
}
}
Here's a full working demo:
import QtQuick 2.15
import QtQuick.Controls 2.15
Page {
Timer {
interval: 50
running: true
repeat: true
onTriggered: marker.animate()
}
Rectangle {
id: frame
anchors.centerIn: parent
width: parent.width / 2
height: parent.height / 2
color: "#ffe"
border.color: "grey"
clip: true
Image {
id: crosshairs
anchors.centerIn: parent
source: "crosshair.svg"
//cache: false
}
Item {
id: marker
x: parent.width/2
y: parent.height/2
property int dx: 2
property int dy: 2
property int size: 20
Image {
anchors.centerIn: parent
source: "marker.svg"
//cache: false
}
function animate() {
x += dx;
y += dy;
if (x + size / 2 >= parent.width || x - size / 2 <= 0) {
dx = -dx;
x += dx;
x += dx;
}
if (y + size / 2 >= parent.height || y - size / 2 <= 0) {
dy = -dy;
py += dy;
py += dy;
}
}
}
Item {
x: marker.x
y: marker.y
rotation: Math.atan2(
marker.y - crosshairs.y,
marker.x - crosshairs.x
) * 180 / Math.PI
Image {
source: "triangle.svg"
x: 20
y: -8
//cache: false
}
}
}
}
//crosshair.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><path stroke="grey" d="M6 0L6 12M 0 6L 12 6"/></svg>
//marker.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path stroke="grey" fill="#ffe" d="M0 0 L20 0 L20 20 L0 20 z"/></svg>
//triangle.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path stroke="grey" fill="red" d="M16 8 L0 0 L 0 16z"/></svg>
You can Try it Online!
Related
I need to draw something like a target, a bunch of concentric circles split into 12 or so sectors (pizza slices). So the number of "layers" inside the outer circle can change anywhere from 2 to 10, the size of the whole target should remain the same, and the distance between inner circles should be equal (bascially OuterCircleRadius / NumberOfLayers)
So far I came up with a code that basically draws a single segment of a circle, then, by putting a Repeater in the main file I get a full circle, split into 12 sectors, which is what I need. Now I need to find a way to add a number of concentric circles inside the outer one. The problem is that the number of concentric circles is not constant, and is set by user. So the "target" should change when a different number of layers is chosen (for example with a SpinBox). So far my idea is to draw a few additional arcs inside of a single sector so I can keep the Repeater. My code below.
// main.qml
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts
import QtQuick.Controls
Window {
width: 600
height: 600
visible: true
id: window
property int sectors: 12
Rectangle {
anchors.centerIn: parent
width: 600
height: 600
Repeater {
model: sectors
CircleSector {
anchors.fill: parent
anchors.margins: 10
sectors: window.sectors
sector: index
fillColor: index == spinBox.value ? "orange" : "aliceblue"
}
}
}
Frame {
SpinBox {
id: spinBox
Layout.fillWidth: true
from: 0
to: sectors - 1
value: 1
wrap: true
stepSize: 1
}
}
}
// CircleSector.qml
import QtQuick 2.12
import QtQuick.Shapes 1.12
Shape {
id: circleSector
antialiasing: true
property int sectors: 12
property int sector: 0
property real from: sector * (360 / sectors)
property real to: (sector + 1) * (360 / sectors)
property real centerX: width / 2
property real centerY: height / 2
property alias fillColor: shapePath.fillColor
property alias strokeColor: shapePath.strokeColor
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)
containsMode: Shape.FillContains
ShapePath{
id: shapePath
fillColor: "aliceblue"
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 }
}
}
Depending on what you want to do with your polar coordinate system you could use PolarChartView. With that you can also easily plot data on it.
import QtQuick
import QtQuick.Controls
import QtCharts
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
PolarChartView {
anchors.fill: parent
legend.visible: false
antialiasing: true
ValueAxis {
id: axisAngular
min: 0
max: 1
labelsVisible: false
lineVisible: false
tickCount: axisAngularSpinBox.value
}
ValueAxis {
id: axisRadial
min: 0
max: 1
labelsVisible: false
lineVisible: false
tickCount: axisRadialSpinBox.value
}
SplineSeries {
id: series
axisAngular: axisAngular
axisRadial: axisRadial
pointsVisible: true
}
}
Column {
SpinBox {
id: axisAngularSpinBox
value: 9
}
SpinBox {
id: axisRadialSpinBox
value: 10
}
}
}
You could use this Canvas solution. It isn't pretty, but it hopefully does what you want.
import QtQuick
import QtQuick.Controls
ApplicationWindow {
id: root
title: "Polar Coordinates Canvas"
width: 640
height: 480
visible: true
color: "white"
property int numCircles: circlesSpinBox.value
property int numSegments: segmentsSpinBox.value
Canvas {
id: canvas
anchors.centerIn: parent
width: 400
height: canvas.width
onPaint: {
var ctx = getContext("2d")
ctx.fillStyle = 'white'
ctx.fillRect(0, 0, canvas.width, canvas.height);
const centerX = canvas.width / 2
const centerY = canvas.height / 2
const radius = canvas.width / 2
const circleDistance = (canvas.width / 2) / root.numCircles
for (let i = 1; i <= root.numCircles; ++i) {
let r = i * circleDistance
ctx.beginPath()
ctx.arc(centerX, centerY, r, 0, 2 * Math.PI, false)
ctx.lineWidth = 1
ctx.strokeStyle = 'black'
ctx.stroke()
}
if (root.numSegments < 2)
return
const segmentAngle = 360 / root.numSegments
if (root.numSegments % 2 === 0) { // even
for (let s = 0; s < (root.numSegments / 2); ++s) {
let a = s * segmentAngle
ctx.beginPath()
let x = centerX + radius * Math.cos(a * (Math.PI / 180))
let y = centerY + radius * Math.sin(a * (Math.PI / 180))
ctx.moveTo(x, y)
x = centerX + radius * Math.cos((a + 180) * (Math.PI / 180))
y = centerY + radius * Math.sin((a + 180) * (Math.PI / 180))
ctx.lineTo(x, y)
ctx.lineWidth = 1
ctx.strokeStyle = 'black'
ctx.stroke()
}
} else { // odd
for (let s = 0; s < root.numSegments; ++s) {
let a = s * segmentAngle
ctx.beginPath()
let x = centerX + radius * Math.cos(a * (Math.PI / 180))
let y = centerY + radius * Math.sin(a * (Math.PI / 180))
ctx.moveTo(x, y)
ctx.lineTo(centerX, centerY)
ctx.lineWidth = 1
ctx.strokeStyle = 'black'
ctx.stroke()
}
}
}
}
Column {
SpinBox {
id: circlesSpinBox
value: 3
onValueChanged: canvas.requestPaint()
}
SpinBox {
id: segmentsSpinBox
value: 7
onValueChanged: canvas.requestPaint()
}
}
}
I need scale and rotate a Rectangle around mouse point. When the Rectangle is not rotated my solution works fine, but if I apply Rotation transform I face the problem - my Rectangle move to an unexpected point. In my solution I use a MouseArea for drag the Rectangle, Scale and Rotation transforms for scale and rotate.
import QtQuick 2.0
Rectangle {
id: root
color: "gray"
Rectangle {
color: "black"
width: 360
height: 200
opacity: 0.5
x: 500
y: 350
}
Rectangle {
id: sample
color: "green"
width: 360
height: 200
opacity: 0.5
x: 500
y: 350
property real currX: 0
property real currY: 0
property real currZoom: 1
property real maxZoom: 5
property real minZoom: 0.5
transform: [
Scale {
id: scaler
origin.x: sample.currX
origin.y: sample.currY
xScale: sample.currZoom
yScale: sample.currZoom
},
Rotation{
id: rotation
origin.x: 180
origin.y: 100
angle: 30
}
]
MouseArea{
id: mouseArea
anchors.fill: parent
drag.target: sample
onClicked: mouse => {
zoom(true, mouse.x, mouse.y)
}
onWheel: (wheel) => {
var isIn = wheel.angleDelta.y > 0
zoom(isIn, wheel.x, wheel.y)
}
function zoom(isIn, x, y) {
var prevZoom = sample.currZoom
var prevX = sample.currX
var prevY = sample.currY
sample.currX = x
sample.currY = y
sample.currZoom = calculateZoom(isIn, prevZoom)
sample.x = sample.x + (prevX - sample.currX) * (1 - prevZoom)
sample.y = sample.y + (prevY - sample.currY) * (1 - prevZoom)
printSamplePostion()
}
function calculateZoom(isIn, prevZoom) {
var newZoom = isIn ? prevZoom + 0.1 : prevZoom - 0.1
if (newZoom > mouseArea.maxZoom)
newZoom = mouseArea.maxZoom
if (newZoom < mouseArea.minZoom)
newZoom = mouseArea.minZoom
return newZoom
}
function printSamplePostion() {
console.log("== x: 500 y: 350 ======")
console.log("-- x: " + sample.x)
console.log("-- y: " + sample.y)
console.log("=======================")
}
}
}
}
Thanks in advance
I'm using Qt Design Studio to define a ArcItem for a Gauge. The ArcItem needs a big Stroke Width to fill correct some background images later. The ArcItem is animated with the Timeline-Module, but when begin and end is same then I have this behavior shown in the image.
I tried then to change the Stroke Width through the Timeline by changing the Stroke Width to -1, if Begin (-125) and End (-125) are equal, but if I'm selecting frame 1, then the ArcItem is out of position.
This is frame 2
Question
How I can solve this issue and keep the Stroke Width?
Code
import QtQuick 2.12
import maskpietest 1.0
import QtQuick.Studio.Components 1.0
import QtQuick.Studio.Effects 1.0
import QtQuick.Controls 2.15
import QtQuick.Shapes 1.0
import QtQuick.Timeline 1.0
Rectangle {
id: rectangle
width: Constants.width
height: Constants.height
color: "#00000000"
layer.enabled: true
Slider {
id: slider
x: 0
y: 0
width: 791
height: 40
from: 0
to: 280
stepSize: 1
}
Label{
id: label
text: slider.value
x:8
y: 46
font.pointSize: 24
}
Rectangle {
width: 560
height: 560
color: "#00000000"
ArcItem {
id: arc
x: 274
y: 73
width: 520
height: 520
anchors.centerIn: parent
outlineArc: false
begin: -125
fillColor: "#00000000"
capStyle: 0
end: -125
strokeWidth: 100
strokeColor: "#37c1ff"
antialiasing: true
}
}
Timeline {
id: timeline
currentFrame: slider.value
animations: [
TimelineAnimation {
id: timelineAnimation
running: false
duration: 280
loops: 1
to: 280
from: 0
}
]
endFrame: 280
enabled: true
startFrame: 0
KeyframeGroup {
target: arc
property: "end"
Keyframe {
frame: 280
value: 125
}
}
KeyframeGroup {
target: arc
property: "strokeWidth"
Keyframe {
frame: 0
value: -1
}
Keyframe {
frame: 1
value: 100
}
}
}
}
Instead of changing the ArcItem strokeWidth you could just make it invisible on frame 0 and visible at frame 1:
KeyframeGroup {
target: arc
property: "visible"
Keyframe {
frame: 0
value: false
}
Keyframe {
frame: 1
value: true
}
}
Another solution (and probably the better one) is to set a specific Keyframe for the end property for frame 0:
KeyframeGroup {
target: arc
property: "end"
Keyframe {
frame: 0
value: -125
}
Keyframe {
frame: 280
value: 125
}
}
I want to design a ruler as shown in the image below:
Which approach is the best possible way to design a ruler with these small and big lines(scale divisions) as shown in the image.
Also text and numbers will be added with the scale divisions.
There is one knob which i can slide from left to right and vice versa. Can this be achieved using Slider component?
Try using the QtQuick.Extras module it has a Gauge QML Type. For tick marks use the tickmark and minorTickmark properties from the GaugeStyle QML Type. Then add to this what you want.
import QtQuick 2.15
import QtQuick.Window 2.12
import QtQuick.Extras 1.4
Window {
visible: true
width: 640
height: 480
color: "gray"
x: (Screen.width - width) / 2
y: (Screen.height - height) / 2
Gauge {
minimumValue: 0
value: 50
maximumValue: 100
anchors.centerIn: parent
orientation: Qt.Horizontal
}
}
Item {
width: parent.width
height: 8
anchors.bottom: parent.bottom
property real spacing: 33.3
Repeater {
model: parent.width / (parent.spacing + 1) - 1
delegate: Rectangle {
x: index * (rowLayout.spacing + 4)
y: parent.height - height
implicitWidth: major ? 2 : 1
implicitHeight: major ? 18 : 9
color: "grey"
readonly property bool major: index % 6 == 0
}
}
}
You can greatly customize the Slider in QtQuick.Controls 2.x. Here is a start for you
Slider {
id: control
width: parent.width
height: 50
background: Canvas {
x: control.leftPadding + (control.horizontal ? handle.width * 0.5 : (control.availableWidth - width) / 2)
y: control.topPadding + (control.horizontal ? (control.availableHeight - height) / 2 : 0)
implicitWidth: control.horizontal ? 200 : control.width
implicitHeight: control.horizontal ? control.height : 200
width: control.horizontal ? control.availableWidth - handle.width : implicitWidth
height: control.horizontal ? implicitHeight : control.availableHeight - handle.height
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
ctx.strokeStyle = Qt.rgba(1, 0, 0, 0.4)
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(width, 0)
ctx.lineTo(width, height)
ctx.lineTo(0, height)
ctx.lineTo(0, 0)
for(var x=0;x < width;x += 40) //assuming 40 is unit of measurement
{
ctx.moveTo(x, height)
if(x % 120 == 0)
ctx.lineTo(x, height * 0.3)
else
ctx.lineTo(x, height * 0.6)
}
ctx.stroke();
}
}
handle: Canvas {
id: handle
x: control.leftPadding + (control.horizontal ? control.visualPosition * (control.availableWidth - width) : 0)
y: control.topPadding + (control.horizontal ? 0 : control.visualPosition * (control.availableHeight - height))
implicitWidth: control.height
implicitHeight: 28
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
ctx.strokeStyle = Qt.rgba(1, 0, 0, 0.4)
ctx.fillStyle = "white"
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(width, 0)
ctx.lineTo(width, height - width * 0.5)
ctx.lineTo(width * 0.5, height)
ctx.lineTo(0, height - width * 0.5)
ctx.lineTo(0, 0)
ctx.fill()
ctx.stroke()
}
}
}
Part of the code was taken from QtQuick.Controls.Material. I only tested the horizontal slider, vertical will fail (especially the handle)
I have simple scene with only 2 Rectangles. The difference is that first one uses absolute coordinates and second one uses anchors. In this case both of rectangles are placed on the same place. But I get different coordinates at all.
import QtQuick 2.4
import QtQuick.Window 2.2
Window {
visible: true
width: 600
height: 600
Rectangle {
id: rec1
x: 200
y: 200
width: 200
height: 200
color: "green"
opacity: 0.5
Component.onCompleted: console.log("rec1: " + rec1.x + "," + rec1.y);
}
Rectangle {
id: rec2
anchors.centerIn: parent
width: 200
height: 200
color: "blue"
opacity: 0.5
Component.onCompleted: console.log("rec2: " + rec2.x + "," + rec2.y);
}
}
The output:
qml: rec2: -100,-100
qml: rec1: 200,200
Yes, I know that it's not really "wrong" result, but how can I get real item coordinates relative to its parent for both of rectangles, i.e. (200,200)?
The documentation of Item proposes the mapToItem function:
Maps the point (x, y) or rect (x, y, width, height), which is in this
item's coordinate system, to item's coordinate system, and returns an
object with x and y (and optionally width and height) properties
matching the mapped coordinate.
If item is a null value, this maps the point or rect to the coordinate
system of the root QML view.
Since the coordinate must be in item' system, the correct way to call the function in your case would be:
<item_id>.mapToItem(<parent_id>, 0, 0)
where (0, 0) is the origin of <item_id> coordinates system.
Since in this case the parent is not an Item itself, we can exploit the null version of the method described by documentation and write:
<item_id>.mapToItem(null, 0, 0)
That's the theory. However, in this particular case (as noted by others), the layout management has not set the coordinate properties yet and thus the methods fail. That seems to be related to the non-consistent state in which items fall during initialisation. Indeed, if we use the function in the onDestruction handler, i.e. when we are sure that initialisation has finished, they give the expected results. See your modified code below:
import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Controls 1.3
Window {
visible: true
width: 600
height: 600
Rectangle {
id: rec1
x: 200
y: 200
width: 200
height: 200
color: "green"
opacity: 0.5
}
Rectangle {
id: rec2
width: 200
height: 200
anchors.centerIn: parent
color: "blue"
opacity: 0.5
}
Component.onCompleted: {
console.info("NOPE! :(")
var cords = rec1.mapToItem(null, 0, 0)
console.info("rec1: " + cords.x + " " + cords.y)
cords = rec2.mapToItem(null, 0, 0)
console.info("rec2: " + cords.x + " " + cords.y)
}
Component.onDestruction: {
console.info("YES! :)")
var cords = rec1.mapToItem(null, 0, 0)
console.info("rec1: " + cords.x + " " + cords.y)
cords = rec2.mapToItem(null, 0, 0)
console.info("rec2: " + cords.x + " " + cords.y)
cords = rec2.mapToItem(null, 100, 100) // (100, 100) of second rec is...
console.info("rec2: " + cords.x + " " + cords.y) // correctly (300, 300) !!
}
}
Output:
qml: NOPE! :(
qml: rec1: 200 200
qml: rec2: -100 -100
qml: YES! :)
qml: rec1: 200 200
qml: rec2: 200 200
qml: rec2: 300 300
Both rectangles have same coordinates but on different time:
import QtQuick 2.4
import QtQuick.Window 2.2
Window {
visible: true
width: 600
height: 600
Rectangle {
id: rec1
x: 200
y: 200
width: 200
height: 200
color: "green"
opacity: 0.5
Component.onCompleted: console.log("rec1: " + rec1.x + "," + rec1.y);
}
Rectangle {
id: rec2
anchors.centerIn: parent
width: 200
height: 200
color: "blue"
opacity: 0.5
Component.onCompleted: console.log("rec2: " + rec2.x + "," + rec2.y);
onXChanged: console.log("rec2.x: " + rec2.x);
onYChanged: console.log("rec2.y: " + rec2.y);
}
}