Invoke Canvas onPaint exactly once per update? - qt

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

Related

Qt Progress Bar Animation When Duration is Longer Than Update

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!")
}
}
}

Understanding `markdirty()` in QML

I am unsure what the markdirty() function does in QML. The QT documentation of that function does not seem clear to me.
My interpretation of this is that it allows us to track changes to a small subset of the canvas, so that any changes to that part redraws everything only in that part, using paint()
Doing requestPaint() on the other hand would be far more inefficient because it would redraw the whole canvas.
Is this correct? Some simple example codes would be quite helpful in understanding the usecase of markDirty()
That's a widely used term in programming especially GUI. Using that you can mark part of the canvas as in need of updating. So if this part is visible the render engine will fire paint(rect region) as soon as possible. In the onPaint handler you should repaint only items inside this region. requestPaint() does almost the same but for all the visible region.
Check the output in the example below:
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
visible: true
width: 800
height: 800
title: qsTr("QML Test")
Canvas {
property var shapes: [20,20,220,20,20,220,220,220]
id: canvas
width: 400
height: 400
anchors.centerIn: parent
onPaint: {
console.log(region);
var ctx = getContext("2d");
ctx.beginPath();
ctx.fillStyle = Qt.rgba(Math.random(),Math.random(),Math.random(),1);
// draw 4 circles
for(var i = 0;i < canvas.shapes.length;i +=2) {
var x = canvas.shapes[i];
var y = canvas.shapes[i + 1];
var width = 160;
var height = 160;
// check the circle is inside the region
if(!( (x + width) < region.x || x > (region.x + region.width) || (y + height) < region.y || y > (region.y + region.height) ) ) {
ctx.ellipse(x, y, width, height);
}
}
ctx.fill();
}
}
Timer {
id: timer
property int step: 0
interval: 2000
repeat: true
running: true
onTriggered: {
switch(step++) {
case 0: canvas.markDirty(Qt.rect(0, 0, 200, 200)); break;
case 1: canvas.requestPaint(); break;
case 2: timer.stop(); break;
}
}
}
}

propagateComposedEvents: mouse data not accurate?

Update: Isn't it often the way: you ask a question and then discover the answer on your own a short time later.
It seems I have some confusion between referencing mouseX and mouseY in a MouseArea and getting the mouse data from the MouseEvent
If I change my code below to:
var pt = Qt.point( mouse.x, mouse.y)
from what I had:
var pt = Qt.point(mouseX, mouseY)
Then the newly created sprites are located at the click point. That's great but I am still not understanding the difference, particularly in this case since the MouseArea fills the entire window (parent).
The mouse data is the same in either approach, unless the mouse event has been propagated – then the mouse approach gives the correct data while mouseX, mouseY does not. That is the part that is confusing me.
Can anyone explain the difference in what is going on here?
I have made a reusable QML component which can load .png images with an alpha channel and handle clicks on transparent pixels by propagating the mouse event. I've got it working in the code below (any suggestions for improvement much welcomed) but it seems the mouse data is wrong.
In the image below I have marked the order and location of 3 clicks. In the log statements even though the mouse click position has changed, the reported position stays the same. So what is occurring is that although 3 sprites have been created, they are stacking on top of each other
Am I missing something about how a MouseArea reports the mouse position or how propagateComposedEvents works?
main clicked. Creating sprite at: 598.01953125 492.953125
graphic alpha clicked: 5.69921875 103.41015625 <----- EVENT PASSED THROUGH
main clicked. Creating sprite at: 598.01953125 492.953125
graphic alpha clicked: 121.953125 103.01953125 <----- EVENT PASSED THROUGH
graphic alpha clicked: 5.69921875 103.41015625 <----- EVENT PASSED THROUGH
main clicked. Creating sprite at: 598.01953125 492.953125
// main.qml
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Dialogs 1.2
ApplicationWindow {
id: appWindow
visible: true
width: 1024
height: 768
title: qsTr("QtQuick")
Item {
id: container
anchors.fill: parent
property var events: new Array()
property int counter: 0;
MouseArea {
anchors.fill: parent
onClicked: {
console.log("---------main clicked. Creating sprite at:", mouseX, mouseY);
var pt = Qt.point(mouseX, mouseY)
var component = Qt.createComponent("graphicAsset.qml");
var imageName = "Earth-icon.png";
var sprite = component.createObject(container, {"x": pt.x, "y": pt.y, "imageSource": imageName});
}
}
}
}
//graphicAsset.qml
import QtQuick 2.5
Canvas {
id: graphicAsset
width: 50
height: 50
property string imageSource;
onImageSourceChanged:loadImage(imageSource)
onImageLoaded: {
var ctx = getContext("2d")
var imageData = ctx.createImageData(imageSource)
graphicAsset.width = imageData.width;
graphicAsset.height = imageData.height;
x = x - (imageData.width /2);
y = y - (imageData.height /2);
requestPaint();
}
onPaint: {
if (isImageLoaded(imageSource)){
var ctx = getContext("2d")
var imageData = ctx.createImageData(imageSource)
ctx.drawImage(imageData, 0, 0)
}
}
MouseArea {
anchors.fill: parent
drag.target: parent
propagateComposedEvents: true
onClicked: {
var ctx = parent.getContext("2d")
var imageData = ctx.getImageData(mouseX, mouseY, 1, 1)
if (imageData.data[3] == 0 ){
console.log("graphic alpha clicked:", mouseX, mouseY);
mouse.accepted = false;
} else {
mouse.accepted = true;
}
}
}
}
Mouse events' x and y positions are relative to the MouseArea that generated the event, as well as the coordinates of the mouse cursor within the same area (named mouseX and mouseY).

Measuring elapsed time in QML

Let's consider the following example: we have a Qt Quick Controls Button. The user clicks it twice within 5 seconds. After pushing the Button for the first time, the QML Timer is running for these 5 seconds. We want to measure the time elapsed between two clicks, with a millisecond accuracy.
Unfortunately, the QML Timer can't show us the elapsed time.
As suggested on the BlackBerry forums, it would be possible to compare the dates. This isn't very handy, though, since the first click might occur on 31 Dec 2015, 23:59:55 and the second on 1 Jan 2016, 00:00:05 and the check would have to be complex.
Is there any better option?
As explained in the comments, QML Timer is not suitable for your specific needs since it is synchronized with the animation timer (further details here) and thus its resolution is dependent on animation timer as well.
#qCring solution is for sure satisfying and I would prefer such an approach, if an higher precision is needed or a better performance (see also this answer and the interesting link at the bottom about improving precision).
However, given your requirements, a pure QML/JS approach is perfectly feasible. In this case you can exploit JavaScript Date, both because it's easy to calculate elapsed time, using getTime(), but also because QML fully supports JS Date and also extends it with some useful functions.
Here is a simple example:
import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 1.3
ApplicationWindow {
width: 300
height: 300
visible: true
property double startTime: 0
ColumnLayout {
anchors.fill: parent
Text {
id: time
font.pixelSize: 30
text: "--"
Layout.alignment: Qt.AlignCenter
}
Button {
text: "Click me!"
Layout.alignment: Qt.AlignCenter
onClicked: {
if(startTime == 0){
time.text = "click again..."
startTime = new Date().getTime()
} else {
time.text = new Date().getTime() - startTime + " ms"
startTime = 0
}
}
}
}
}
Unfortunately, the QML Timer doesn't provide a property to check the elapsed time. But you could write your custom Timer in C++ and expose it to QML:
MyTimer.h
#include <QObject>
#include <QElapsedTimer>
class MyTimer : public QObject
{
Q_OBJECT
Q_PROPERTY(int elapsed MEMBER m_elapsed NOTIFY elapsedChanged)
Q_PROPERTY(bool running MEMBER m_running NOTIFY runningChanged)
private:
QElapsedTimer m_timer;
int m_elapsed;
bool m_running;
public slots:
void start() {
this->m_elapsed = 0;
this->m_running = true;
m_timer.start();
emit runningChanged();
}
void stop() {
this->m_elapsed = m_timer.elapsed();
this->m_running = false;
emit elapsedChanged();
emit runningChanged();
}
signals:
void runningChanged();
void elapsedChanged();
};
After registering via qmlRegisterType<MyTimer>("MyStuff", 1, 0, "MyTimer") it's available in QML:
Window.qml
import QtQuick 2.4
import QtQuick.Controls 1.3
import MyStuff 1.0
ApplicationWindow {
width: 800
height: 600
visible: true
Button {
id: button
anchors.centerIn: parent
text: timer.running ? "stop" : "start"
checkable: true
onClicked: {
if (timer.running) {
timer.stop()
label.text = timer.elapsed + "ms"
} else {
timer.start()
}
}
MyTimer {
id: timer
}
}
Text {
id: label
anchors.left: button.right
anchors.verticalCenter: button.verticalCenter
text: "0ms"
visible: !timer.running
}
}
Hope this helps!
You don't mention in your question if the measured time is only for debugging purposes or if it will be needed for other calculations. Because if not QML offers a very simple way to debug the time spent doing various operations using console.time("id string") and console.timeEnd("id string").
An example using a Button would look like this:
Button {
text: "click here"
property bool measuring: false
onClicked: {
if(!measuring){
console.time("button")
measuring=true
} else {
console.timeEnd("button")
measuring=false
}
}
}
This will print the time in ms to the console, and can be very useful to measure the time needed to execute some long operations in QML.

QML Canvas.requestAnimationFrame explodes

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

Resources