I am working on customizing a Qt5 Quick 2 QML progress bar. When progress updates are very few and large, the progress bar jumps in a blocky way. To fix this, I thought I would add a simple Behavior to the value animation so it smoothly moves to the next value. This works great except when the animation duration is larger that the period between updates. Then the behavior is that the updates move very slowly, and when they are stopped they seem to speed up and try and finish.
The following code increments the progress bar so that it repeats once per second. When the Behavior duration is less than the timer interval, it works, but when the duration is longer, it fails.
I would like a value set to stop the prior executing behavior animation and move on to the next, not simultaneously overlap somehow.
Timer
{
interval: 200; running:true; repeat:true
onTriggered:
{
if(mybar.doUpdate)
mybar.value = (mybar.value + 0.2 ) % 1
}
}
ProgressBar
{
id: mybar
value: .5
property bool doUpdate: true
Behavior on value
{
NumberAnimation
{
duration: 1000
easing.type: Easing.InOutQuad
}
}
MouseArea{
anchors.fill:parent
onClicked:
{
parent.doUpdate = !parent.doUpdate
console.log((!parent.doUpdate ? "Stop" : "Go") + " Now!")
}
}
}
I'm not positive I understand your expected behavior, but I think there's two issues. First, you need to use a go-between value that doesn't animate so you can refer to it later. Then you need a way to turn off the animation so you can jump to a value immediately. Something like this should work:
// This holds the current progress value without animating
property real tempVal: 0.5
// This controls whether or not to animate
property bool doAnimate: true
Timer
{
interval: 200; running:true; repeat:true
onTriggered:
{
if(mybar.doUpdate)
{
// Turn off the animation
doAnimate = false;
// Reset the progress bar to the current expected value
mybar.value = Qt.binding(function() { return tempVal });
// Turn on the animation again
doAnimate = true;
// Animate up to this new value
tempVal = (tempVal + 0.2 ) % 1
}
}
}
ProgressBar
{
id: mybar
// Bind the progress bar to our secondary value
value: tempVal
property bool doUpdate: true
Behavior on value
{
// Control animation with this flag
enabled: doAnimate
NumberAnimation
{
duration: 1000
easing.type: Easing.InOutQuad
}
}
MouseArea{
anchors.fill:parent
onClicked:
{
parent.doUpdate = !parent.doUpdate
console.log((!parent.doUpdate ? "Stop" : "Go") + " Now!")
}
}
}
Related
I'm animating the value transition in a gauge
CircularGauge {
onValueChanged: {
console.info('val' , value)
}
Behavior on value {
RotationAnimation {
duration: 20000
}
}
}
But the animation only happens in steps of 1. I did not find a way to make them do smaller steps.
Is there a way?
In MouseArea.onEntered, can I detect if the cause for the event firing is only that the MouseArea moved and came to be under the cursor, rather than the other way round?
I thought of doing something like this: (pseudocode)
MouseArea {
// ...
property bool positionDirty = false
Connections {
target: window
onAfterRendering: {
positionDirty = false;
}
}
onMouseAreaPosChanged: {
positionDirty = true;
}
onEntered: {
if(positionDirty) {
positionDirty = false;
return;
}
// handle event here
}
}
But this makes the assumption that entered will be fired after mouseAreaPosChanged, and before window.afterRendering. And I'm not confident in that assumption.
Also, it doesn't work when an ancestor of the MouseArea moves, or when the MouseArea is positioned/sized via anchoring.
Assumptions:
This only affects the edge case, that both, the cursor and the MouseArea are moving.
My Assumption here is, that the movement of the cursor is handled before the movement of the MouseArea. I don't have any definite proof for this. Only my test with the solution below, suggests that.
Solution
The first challenge is to detect movement of the MouseArea. It might be that it moves, without its own x and y-values changing, e.g. if its parent is moving.
To solve this, I'll introduce two properties globalX and globalX. Then I use the trick from this answer on how to track a gobal position of an object.
Now I'll have two signals to handle: globalXChanged and globalYChanged.
According to my assumption, they are fired, after the mouseXChanged and mouseYChanged. I will use a flag isEntered to make sure, I only handle one of them, by setting it to true, if the first of them is fired.
I will use the cursor position on a globalMouseArea to determine, whether the cursor is within bounds of the MouseArea. This requires, the cursor is not in some other MouseArea at that time, or at least I know of it
-> With this I already succeeded in detecting the entrance.
The second challenge is to detect the exit. Here we have 4 cases to distinguish:
Cursor enters and leaves the MouseArea because of it's movement.
Cursor enters and leaves the MouseArea because of the movement of the MouseArea
Cursor enters because the MouseArea moves, and leaves, because the cursor moves
Cursor enters because it moves, and leaves as the MouseArea moves away.
The first would be easy to handle. After it enters we handle entered and when it leaves we handle exited. But after the fix, mentioned by Mitch we can't rely on this anymore.
So we will not set hoverEnabled: true and map the position of the cursor to the targetMouseArea whenever either the cursor moves, or the targetMouseArea moves, and act accordingly.
import QtQuick 2.7
import QtQuick.Controls 2.0
ApplicationWindow {
id: root
visible: true
width: 400; height: 450
MouseArea {
id: globalMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: ani.restart()
}
Rectangle {
x: 300
y: 300
width: 50
height: 50
color: 'green'
}
Rectangle {
id: rect
width: 50
height: 50
color: 'red'
Text {
text: targetMouseArea.isEntered.toString()
}
MouseArea {
id: targetMouseArea
anchors.fill: parent
signal enteredBySelfMovement
signal enteredByMouseMovement
onEnteredByMouseMovement: console.log('Cause: Mouse')
onEnteredBySelfMovement: console.log('Cause: Self')
property point globalPos: {
var c = Qt.point(0, 0)
var itm = this
for (; itm.parent !== null; itm = itm.parent) {
c.x += itm.x
c.y += itm.y
}
return c
}
property bool isEntered: false
function checkCollision(sig) {
if ((globalPos.y < globalMouseArea.mouseY)
&& (globalPos.y + height > globalMouseArea.mouseY)
&& (globalPos.x < globalMouseArea.mouseX)
&& (globalPos.x + width > globalMouseArea.mouseX)) {
if (!isEntered) {
isEntered = true
sig()
}
}
else if (isEntered && !containsMouse) {
console.log(isEntered = false)
}
}
onGlobalPosChanged: {
checkCollision(enteredBySelfMovement)
}
Connections {
target: globalMouseArea
onPositionChanged: {
targetMouseArea.checkCollision(targetMouseArea.enteredByMouseMovement)
}
}
}
}
NumberAnimation {
id: ani
target: rect
properties: 'x,y'
from: 0
to: 300
running: true
duration: 10000
}
}
Problems left: When we clicked within the targetMouseArea, as long as a button is pressed, we won't detect the leaving.
You can check whether mouseX, mouseY were changed since last event.
property int previousMouseX = mouseX; // or use other values to init
property int previousMouseY = mouseY; // e.g., 0, x, parent.x,
// or set it from extern
onEntered() {
if (mouseX != previousMouseX || mouseY != previousMouseY) {
// TODO do something
previousMouseX = mouseX;
previousMouseY = mouseY;
}
}
In case mouseX, mouseY are relative to the mouse area 0,0 you can use mapFromItem(null, 0, 0) to get the absolute values.
Say I've got the following code:
Rectangle {
id: rect
property int minVal : 0
property int maxVal : 10
property int duration: 1000
SequentialAnimation {
loops: Animation.infinite
NumberAnimation {
target: rect; property: "x"
from: minVal; to: maxVal
duration: duration
}
NumberAnimation {
target: rect; property: "x"
from: maxVal; to: minVal
duration: duration
}
}
}
Then later, I hook up this object's minVal, maxVal, and duration properties to sliders, so that I can play around with this effect.
Is there any way to force the animation to use the new values for min/maxVal?
Currently, it seems that the minVal/maxVal values present when the animations were initialised get "baked" into the animations, with no way to force these to get recalculated/rebaked later
Previously I've been able to get away with throwing away the entire QML canvas everytime one of these changes was needed. But in this case, it's not really practical, so I was wondering if there were other alternatives?
I could not find a way to 'bake' in those values. For me, at least, this works as I expected:
Button {
id: but
property int count: 0
property int minX: 0
property int maxX: 1000
text: 'start' + x
onCountChanged: {
minX += 10
maxX -= 10
}
onClicked: ani.start()
}
SequentialAnimation {
id: ani
NumberAnimation {
target: but
property: 'x'
from: but.minX
to: but.maxX
}
ScriptAction {
script: { but.count += 1 }
}
}
I tried as much as possible to avoid bindings that could be baked in somehow.
Have I missed your problem?
AKA: Canvas requestPaint() too slow; requestAnimationFrame() too fast
I'm trying to create a QML Canvas that repaints as fast as possible—once per update in the main UI render loop—in order to create an FPS timer.
I initially wrote this simple test:
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
visible:true; width:100; height:100
Canvas {
anchors.fill:parent
onPaint: console.log(+new Date)
}
}
I only get the callback once. So I added requestPaint():
onPaint: {
console.log(+new Date)
requestPaint()
}
No change: I still only get one callback. Same if I use markDirty(). Same if I actually paint something on the canvas each callback.
So I moved to requestAnimationFrame():
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
visible:true; width:100; height:100
Canvas {
anchors.fill:parent
Component.onCompleted: crank()
function crank(){
console.log(+new Date)
requestAnimationFrame(crank)
}
}
}
Now I get callbacks, but way too many. On average, I get 77 callbacks per millisecond, some times as many as 127 callbacks in a single millisecond. So many callbacks that nothing else in the application displays, not even initially. Even if I remove the console.log(), to prove that I'm not i/o bound).
How can I get my canvas to repaint once "per frame", so that I can measure the FPS semi-accurately? Any why does requestPaint() not actually work? And why is requestAnimationFrame() apparently useless?
The problem with your approach is that you are requesting paint from onPaint, this is not going to work,
because onPaint event is triggered from within
QQuickItem::polish()
void QQuickItem::polish()
{
Q_D(QQuickItem);
if (!d->polishScheduled) {
d->polishScheduled = true;
if (d->window) {
QQuickWindowPrivate *p = QQuickWindowPrivate::get(d->window);
bool maybeupdate = p->itemsToPolish.isEmpty();
p->itemsToPolish.append(this);
if (maybeupdate) d->window->maybeUpdate();
}
}
}
During this call d->polishScheduled is set to true and if you call requestPaint() again, nothing happens. You need to trigger it asynchronously. For example, use Timer with interval 0.
import QtQuick 2.0
Canvas {
id: canvas
width: 200
height: 200
property real angle
property int fps
Timer {
id: repaintTimer
running: false
interval: 0
onTriggered: {
angle += 0.01
canvas.requestPaint()
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
console.log(fps)
fps = 0
}
}
onPaint: {
var ctx = getContext("2d")
ctx.save()
ctx.clearRect(0, 0, width, height)
ctx.moveTo(100, 100)
ctx.translate(100,100)
ctx.rotate(angle)
ctx.beginPath()
ctx.lineTo(40, 10)
ctx.lineTo(40, 40)
ctx.lineTo(10, 40)
ctx.lineTo(10, 10)
ctx.closePath()
ctx.stroke()
ctx.restore()
fps += 1
repaintTimer.start()
}
}
Another Timer is here to record fps. When I run this code in qmlscene, I get 60 fps.
There was a bug with requestAnimationFrame() prior to Qt 5.9. This bug has been fixed.
The following code works as expected and desired to keep the canvas continuously redrawing:
Canvas {
width:100; height:100;
property var ctx
onAvailableChanged: if (available) ctx = getContext('2d');
onPaint: {
if (!ctx) return;
ctx.clearRect(0, 0, width, height);
// draw here
requestAnimationFrame(paint);
}
}
I am trying to use QML Canvas.requestAnimationFrame to draw some custom animation. I expected that the provided callback is called once for each frame, about 60 times per second. The code I have is:
Canvas {
id: canvas
width: 600
height: 600
function draw() {
}
Component.onCompleted: {
var i = 1;
function drawFrame() {
requestAnimationFrame(drawFrame)
console.log("Frame callback: " + i++)
draw()
}
drawFrame()
}
onPaint: {
draw()
}
}
What I see is that the callback is called way more often. The counter reaches 70000 in a few seconds, after which the application becomes entirely unresponsive.
What am I doing wrong?
Your drawFrame() function passes itself as callback function for rendering and you're caught in a loop. You either want to render on demand only like for example after user input to keep resources at a minimum, or you have some logic that changes every frame or you just need continuous rendering.
If time-based rendering is what you want, just use a Timer:
import QtQuick 2.4
Canvas {
id: cvs
width: 600; height: 600
contextType: "2d"
property real i : 0
onPaint: {
console.timeEnd("t")
if (context) {
context.clearRect (0, 0, width, height)
context.fillRect(i, 50, 50, 50 + i)
}
console.time("t")
}
Timer {
interval: 1
repeat: true
running: true
onTriggered: {
cvs.i = (cvs.i + 0.1) % cvs.width
cvs.requestPaint()
}
}
}
Edit:
Just updated the code:
onPaint calls are synced to the display frame rate even though the timer interval is set to 1ms as can be seen from the log when running the sample above.
In fact the whole block assigned to the onTriggered signal is executed every millisecond but requestPaint() makes sure to synchronize rendering calls for best performance just like requestAnimationFrame() does for the HTML canvas.
Apparently, requestAnimationFrame() inside the QML.Canvas doesn't work as expected and there's not much documentation...
Hope this helps!
Just a small update on this topic. I've encountered same problem with the Qt qml Canvas and requestAnimationFrame while I was working on my project. The solution I've found is to switch the render strategy to Threaded and use onPainted signal. The example of qCring's code with my updates looks like this:
import QtQuick 2.4
Canvas {
id: cvs
width: 600; height: 600
//renderStrategy: Canvas.Cooperative // Will work as well but animation chops on my computer from time to time
renderStrategy: Canvas.Threaded
contextType: "2d"
property real i : 0
function animate() {
cvs.i = (cvs.i + 0.1) % cvs.width;
}
onPaint: {
console.timeEnd( "t" )
if ( context ) {
context.clearRect( 0, 0, width, height )
context.fillRect( i, 50, 50, 50 + i )
}
console.time("t")
cvs.requestAnimationFrame(animate);
}
onPainted: {
cvs.requestPaint();
}
}
There was a bug with requestAnimationFrame() prior to Qt 5.9. This bug has been fixed.
This code works as expected and desired to keep the canvas continuously redrawing.
Canvas {
width:100; height:100;
property var ctx
onAvailableChanged: if (available) ctx = getContext('2d');
onPaint: {
if (!ctx) return;
ctx.clearRect(0, 0, width, height);
// draw here
requestAnimationFrame(paint);
}
}