QML: How to scale rotated Item around mouse point - qt

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

Related

A way to draw a concentric circles' sector in QML?

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

how to add PathLine to ShapePath with a Repeater?

UPDATE 1 - i can get it to work using Javascript - but that seems to be a little bit unoptimized (from 2-3% to 30% load with qmlscene.exe when activated) - complete recreation when the model changes (its not ever growing), is that the only way or is there a more "declarative" style available?
How can i add a PathLine to a ShapePath based on a Model?
i know how to use a Repeater from outside of Items but not how to implode them inside of Items
do i need some sort of Repeater-Delegate on pathElements?
and i don't want to use the LineSeries component
import QtQuick.Shapes 1.15
import QtQuick 2.15
Shape {
ListModel {
id: myPositions
ListElement { x: 0; y:38 }
ListElement { x: 10; y: 28 }
ListElement { x: 20; y: 30 }
ListElement { x: 30; y: 14 }
}
ShapePath {
strokeColor: "black"
strokeWidth: 1
fillColor: "transparent"
startX: 0
startY: 0
PathLine { x: 0; y: 38 }
PathLine { x: 10; y: 28 }
PathLine { x: 20; y: 30 }
PathLine { x: 30; y: 14 }
// Repeater {
// model: myPositions
// PathLine { x: model.x; y: model.y }
// }
}
}
UPDATE 1
import QtQuick.Shapes 1.15
import QtQuick 2.15
Shape {
ListModel {
id: myPositions
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Timer
{
interval: 100
running: true
repeat: true
property real myX: 0
onTriggered: {
myPositions.append({"x":myX, "y":getRandomInt(0,30)})
myX = myX + 10
}
}
function createPathLineElements(positionsModel, shapePath)
{
var pathElements = []
for (var i = 0; i < positionsModel.count; i++)
{
var pos = myPositions.get(i)
var pathLine = Qt.createQmlObject('import QtQuick 2.15; PathLine {}',
shapePath);
pathLine.x = pos.x
pathLine.y = pos.y
pathElements.push(pathLine)
}
return pathElements
}
ShapePath {
id: myPath
strokeColor: "black"
strokeWidth: 1
fillColor: "transparent"
startX: 0
startY: 0
pathElements: createPathLineElements(myPositions, myPath)
}
}
Use an Instantiator:
Shape {
ListModel {
id: myPositions
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Timer {
interval: 100
running: true
repeat: true
property real myX: 0
onTriggered: {
myPositions.append({"x":myX, "y":getRandomInt(0,30)})
myX = myX + 10
}
}
ShapePath {
id: myPath
fillColor: "transparent"
strokeColor: "red"
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
strokeWidth: 3
strokeStyle: ShapePath.SolidLine
}
Instantiator {
model: myPositions
onObjectAdded: myPath.pathElements.push(object)
PathLine {
x: model.x
y: model.y
}
}
}
You can't use repeater in that element.
The most performant way is to use QQuickItem in order to create a custom Item which draws incremental path.
Yet another simpler ways are:
1- Use PathSvg element and set its path runtime like below:
ShapePath {
fillColor: "transparent"
strokeColor: "red"
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
strokeWidth: 3
strokeStyle: ShapePath.SolidLine
PathSvg { id: ps; path: parent.p } //<== fill path property using js
property string p: ""
Component.onCompleted: {
for ( var i = 0; i < myModel.count; i++) {
p += "L %1 %2".arg(myModel.get(i).x).arg(myModel.get(i).y);
}
ps.path = p;
}
}
2- If you know steps, so you can pre-declare all PathLines and then set their values runtime. Just like a heart rate line on a health care monitor.

How to design a Ruler in qml

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)

How to add text in plotting area of chartview in qml

How do I place text at specific (x,y) locations within the plotting area of a QML ChartView type?
For example, I would like to place text at the location XYPoint{x: -3; Y: 20}
I don't want to place at window's(x,y), i want to put at plotting area's (x,y)
I read documentation,but i don't find any property !!!!!!
//ChartView for plotting points
ChartView{
id:chrt
anchors.fill: parent
height: parent.height
width: parent.width
legend.visible: false
backgroundColor: "black"
//X- axis
ValueAxis{
id: x_axis
min: -5
max: 0
tickCount: 6
}
//Right Y axis
ValueAxis{
id:right_y_axis
min:0
max:40
tickCount: 5
}
//Left Y axis
ValueAxis{
id:left_y_axis
min:0
max:40
tickCount: 5
}
//Line series for wave 1
LineSeries{
id:l1
axisY: left_y_axis
axisX:x_axis
color: "yellow"
width: 1
}
//Line series for wave 2
LineSeries{
id:l2
axisYRight: right_y_axis
style: Qt.DashLine
color: "yellow"
width: 0.6
}
}
Ok, you can use ChartView.mapToPosition to calculate the global position of the specified point from a series (or some another point inside the series), for example:
ChartView{
id:chart
anchors.fill: parent
backgroundColor: "black"
LineSeries{
id: series
XYPoint { x: 10; y: 5 }
XYPoint { x: 10; y: 1 }
XYPoint { x: 15; y: 5 }
XYPoint { x: 20; y: 10 }
XYPoint { x: 25; y: 5 }
XYPoint { x: 30; y: 20 }
XYPoint { x: 40; y: 10 }
}
Text {
id: txt
text: "Hello"
color: "white"
}
onWidthChanged: updatePointPosition();
onHeightChanged: updatePointPosition();
function updatePointPosition()
{
var p = chart.mapToPosition(series.at(3), series);
txt.x = p.x;
txt.y = p.y;
}
}

Offset rotation for QML Item

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!

Resources