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.
Related
I have 3 nested ListViews.The first is the month, second is day and third is hour of day.All of them constructs a calendar.There is a loader that loads a window when I click in the hour ListView, and set text in a Label.This text can be displayed in the hour view and can be deleted or edited through the above window .Unfortunately the text can not be saved because when I scroll the ListViews, delegates changing and not keeping the context of the label(That is normal I suppose).The objective is to be able to save those texts in the label(store the data) and restore them when the application is closed and re-opened.
Below is a generic code sample for 3 ListViews:
ApplicationWindow{
id:appwindow
............
Item{
id:dayView
...........
ListView{
id:monthofdayCalendar
orientation:Qt.Horizontal
model:12
delegate: Item{
ListView{
id:dayCalendar
orientation: Qt.Horizontal
model:32
delegate: Item{
...............
ListView{
id:daylistView
orientation: Qt.Vertical
model:24
delegate:Item{
id:hourItem
property string hourTime:hourweeklistviewLabel
property string notetaking:notesLabe
.............
MouseArea{
anchors.fill:parent
onClicked:{
windowLoader.active =true
daylistView.currentIndex=index
}
}
Rectangle{}
Label{
id:hourweeklistviewLabel
}
Label{
id:notesLabel
anchors.left:hourweeklistviewLabel.right
anchors.leftMargin: 30
text:""
}//Label
}//delegate:Item
}//ListView
} //delegate:Item
}//ListView
}//delegate:Item
}//Listview
}//Item
Below is the code of loader:
Loader {
id:windowLoader
focus: true
active:false
sourceComponent: Window{
id:inputWin
title:"Enter Note"
width:500
height:300
visible:true
onClosing:{
windowLoader.active=false
monthofdayCalendar.currentItem.daycalendarAlias.currentItem.dayList.currentIndex = calendarMonth.selectedDate.getDate() === new Date().getDate()
&& calendarMonth.selectedDate.getDay() === new Date().getDay()
&& calendarMonth.selectedDate.getMonth() === new Date().getMonth()?getHour():12
}
TextField {
id:title
x:50
y:20
placeholderText :'Enter Note'
text:monthofdayCalendar.currentItem.daycalendarAlias.currentItem.dayList.currentItem.notetaking.text
}
TextField{
id:timeDate
anchors.horizontalCenter: title.horizontalCenter
anchors.top:title.bottom
anchors.topMargin:10
placeholderText : calendarMonth.selectedDate.getDate() +"-"
+ (calendarMonth.selectedDate.getMonth()+1)+"-"
+ calendarMonth.selectedDate.getFullYear() + " "
+ monthofdayCalendar.currentItem.daycalendarAlias.currentItem.dayList.currentItem.hourTime.text
}
Button {
id: button
text: qsTr("Add Note")
anchors.centerIn:parent
onClicked: {
if (title.text !==""){monthofdayCalendar.currentItem.daycalendarAlias.currentItem.dayList.currentItem.notetaking.text= title.text}
else{}
}
}
}
}
The big question is how to save (store) the data of notesLabel.text and be able to display it and restore it every time I close and re-open the application.
As you can see the model for each ListView is not a ListModel so I think I can not use those models to save the data if I am right.If I am wrong please advise.
Anyway your help will be appreciateed.
EDIT
I've changed the integer models with ListModel dynamically created.The code of the ListModels is below:
ListModel{
id:hourlistModel
Component.onCompleted:{
for (var i = 0; i <25; i++){
append(createListElement())
}
}
property int h:0
function createListElement(){
return {
hour : h++
}
}
}
ListModel{
id:daylistModel
Component.onCompleted:{
for (var j=0; j <= 31; j++){
append(createListElement())
}
}
property int dD:0
function createListElement(){
return {
day : dD++
}
}
}
ListModel{
id:monthlistModel
Component.onCompleted:{
for (var k=0; k <=11; k++){
append(createListElement())
}
}
property int mN:0
function createListElement(){
return {
monthName : mN++
}
}
}
Can I store the data from Label notesLabel, now I've changed the models of ListViews with ListModels?
Thanks in advance.
I would create an exposed C++ class.
Using the exposed C++ class you have a range of options to pass data from the front/backend
Q_Property/Member
Q_Invokable
Signal/Slots
Given that you are using strictly strings, I would use an exposed series of QStrings or a QStringList.
QString
QStringList
To tackle to file read/write use your now exposed C++ class. You can either stick to file I/O via standard c++ or QFile system.
Constructor - Read the .txt file and save the data to your property data.
Exchange data as needed updating either the QML or C++ property member
Deconstructor - save the property member data back to file.
Brief example code:
someQML.qml
Import MyExposedClass 1.0
Item {
MyExposedClass {
id: myExposedClassID
text: myExposedClassID
}
Text{
id: yearTextID
text: myExposedClassID.year
}
Text{
id: monthTextID
text: myExposedClassID.month
}
Text{
id: dayTextID
text: myExposedClassID.day
}
Button {
id: myButtonID
onButtonPressed {
var finalStr = yearTextID + monthTextID + dayTextID
// If you used a QMember of qstring lets say
myExposedClassID.saveFile = finalStr
// If you used a QInvokable
myExposedClassID.saveFile_INVOK(finalStr)
}
}
}
myClass.h
class myClass : public QObject {
Q_OBJECT
// Serial Dev
Q_PROPERTY(QString day READ getDay WRITE setDay NOTIFY dayChanged)
Q_PROPERTY(QString month READ ... WRITE ... NOTIFY ...)
Q_PROPERTY(QString year READ ... WRITE ... NOTIFY ...)
...
// On construction, read the text file and update the qproperty variables
//implement getters and setters for qproperties
// On deconstruction, write the properties to file
}
If you have issues with MVC and QStrings/QStringlist. You may have to look into QVariants or QAbstracts.
I find QML to be a risky hole to dive into. Its easy to add more and more functionality to a QML file. But if you try and redesign it or change up some logic, it can very quickly ruin the QML. Splitting the QML into a QML & C++ is a nice way to achieve modularity and control.
I cannot find a way to communicate from one qml file to the other one. I know there are many ways to send signals from qml to C++ slots and reverse, but all my research about signals between two different qml files failed.
So I would be glad if someone can tell me, how I have to solve this problem.
First of all a little abstracted example to understand the problem in a better way...
The first QML in basics looks like that:
//MyQML1.qml
Rectangle
{
id: idMyRec1
signal mySignalFromQML1()
Button
{
id: idMyButton1
onClicked:
{
idMyRec1.mySignalFromQML1(); //to send the signal
}
}
}
The 2nd one looks like this:
//MyQML2.qml
Rectangle
{
id: idMyRec2
Text{
id: idMyText2
text: "Hello World!"
onMySignalFromQML1: //to receive the signal from the other qml
{
idMyText2.text = "Good Bye World!";
}
}
}
So this button should change the text in my 2nd QML to "Good Bye World!" when clicked...but this doesn't work...are there any other ways to send signals from QML to another QML?! Or am I doing something wrong?
You don't communicate between qml files, the QML file is just a prototype, you communicate between the object instances.
// Rect1.qml
Rectangle {
id: rect1
signal mySignal
Button {
onClicked: rect1.mySignal()
}
}
// Rect2.qml
Rectangle { // Rect1.qml
property alias text: txt.text
Text {
id: txt
}
}
And then you create the objects:
Rect1 {
onMySignal: r2.text = "Goodbye world!"
}
Rect2 {
id: r2
}
There are other ways to make a connection, however, connections happen between object instances, not qml files. The objects don't have to be in the same qml file too, but initially for simple things they will rarely be in different files.
For me this works with Connections and signal in one qml file as follow:
import QtQuick 2.4
import QtQuick.Controls 1.2
Item {
id: item
width: 200
height: 200
signal sendMessage(string msg, int compId)
Button {
text: "SendMessage"
onClicked: sendMessage("hello",1)
}
Item {
id: item1
Connections {
target: item
onSendMessage: if(compId==1) { console.log("Hello by Comp 1") }
}
}
Item {
id: item2
Connections {
target: item
onSendMessage: if(compId==2) { console.log("Hello by Comp 2") }
}
}
}
Of course the items with the Connections can be also in different files.
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);
}
}
TL;DR: TextEdit paints highlighted text only when I click on it. Nothing helps
I have a ListView with a QAbstractListModel model with string properties.
Those string properties are being spellchecked and QSyntaxHighlighter is used to show spell errors. I create QSyntaxHighlighter descendant in Component.onCompleted of TextEdit. I double-checked highlighting get's executed with correct spell errors and setFormat() of Highlighter is executed with correct positions. The problem is that it draws text in red (invalidates) only when I click on the TextEdit itself.
TextEdit lives in a Flickable (to track cursor) and Flickable lives in a Rectangle (to have nice background and border). Binding to some signals and calling update() of TextEdit does not help.
After spellcheck finishes, I emit rehighlight() signal of created SyntaxHighlighter.
Rectangle {
id: descriptionRect
height: 30
border.width: descriptionTextInput.activeFocus ? 1 : 0
clip: true
Flickable {
id: descriptionFlick
contentWidth: descriptionTextInput.paintedWidth
contentHeight: descriptionTextInput.paintedHeight
anchors.fill: parent
interactive: false
flickableDirection: Flickable.HorizontalFlick
height: 30
clip: true
focus: false
function ensureVisible(r) {
if (contentX >= r.x)
contentX = r.x;
else if (contentX+width <= r.x+r.width)
contentX = r.x+r.width-width;
}
TextEdit {
id: descriptionTextInput
width: descriptionFlick.width
height: descriptionFlick.height
text: description
onTextChanged: model.editdescription = text
Component.onCompleted: {
globalModel.initDescriptionHighlighting(index, descriptionTextInput.textDocument)
}
onCursorRectangleChanged: descriptionFlick.ensureVisible(cursorRectangle)
}
}
}
Here is a small sample of project with demonstration of how it does not work until you click on a text https://bitbucket.org/ribtoks/qt-highlighting-issue
Any ideas how I can solve this?
Just encountered this issue on 5.11.2 and found the following fix which allows updating of individual blocks without having to highlight/deselect the whole text area
rehighlightBlock(newBlock);
Q_EMIT document()->documentLayout()->updateBlock(newBlock);
The issue was probably caused by QTBUG-44765, fixed in Qt 5.5.
Given the low level of the bug, I don't think it is practically to work around it.
You can work around that by appending an empty string to the TextEdit when you're done with the syntax highlighting
TextEdit {
id: captionTextEdit
width: wrapperFlick.width
height: wrapperFlick.height
text: display
readOnly: true
Component.onCompleted: {
itemsModel.initHighlighter(index, captionTextEdit.textDocument)
}
Connections {
target: itemsModel
onUpdateTextEdit: {
console.log("Update element at index: " + indexToUpdate)
if (indexToUpdate == index)
{
console.log("Update me!")
captionTextEdit.append("")
}
}
}
onCursorRectangleChanged: wrapperFlick.ensureVisible(cursorRectangle)
}
where updateTextEdit(indexToUpdate) is a new signal your itemsModel has to emit.
itemsmodel.h
signals:
void updateTextEdit(int indexToUpdate);
itemsmodel.cpp
void ItemsModel::initHighlighter(int index, QQuickTextDocument *document) {
// Signal mapper could be avoided if lamda slot are available (Qt5 and C++11)
QSignalMapper* signalMapper = new QSignalMapper(this);
if (0 <= index && index < m_ItemsList.length()) {
SingleItem *item = m_ItemsList.at(index);
SpellCheckHighlighter *highlighter = new SpellCheckHighlighter(document->textDocument(), item);
QObject::connect(item, SIGNAL(spellCheckResultsReady()),
highlighter, SLOT(rehighlight()));
// TODO: Don't connect this slot for Qt 5.5+ to avoid performance overhead
QObject::connect(item, SIGNAL(spellCheckResultsReady()),
signalMapper, SLOT(map()));
signalMapper->setMapping(item, index);
}
connect(signalMapper, SIGNAL(mapped(int)),
this, SIGNAL(updateTextEdit(int)));
}
Full code is available here: https://bitbucket.org/swarta/rehighlighdemo/branch/workaround#diff
In my application I'm displaying a list of audio files and the user can drag an external file to add it to the list. I want to be able to refuse the drag if no file in the list is supported by my application.
The issue is that when I call drag.accepted = false; in onEntered of my DropArea then it becomes completely unresponsive to any other event.
Here is some sample code showing the issue. If you drag an MP3 in the window you see that it works. Then if you drag any other file it won't work, as expected. But then dragging an MP3 file back will not work either.
import QtQuick 2.1
import QtQuick.Window 2.0
ApplicationWindow {
title: qsTr("Hello World")
width: 640
height: 480
DropArea {
anchors.fill: parent
onEntered: {
console.log("[Droparea] entered");
// Ensure at least one file is supported before accepted the drag
var validFile = false;
for(var i = 0; i < drag.urls.length; i++) {
if(validateFileExtension(drag.urls[i])) {
validFile = true;
break;
}
}
if(!validFile) {
console.log("No valid files, refusing drag event");
drag.accepted = false;
return false;
}
}
onExited: {
console.log("[Droparea] entered");
}
onDropped: {
console.log("[Droparea] dropped");
}
// Only MP3s
function validateFileExtension(filePath) {
var extension = filePath.split('.').pop();
var valid = false;
if(extension == "mp3") {
valid = true;
}
return valid;
}
}
Text {
id: textDrop
anchors.centerIn: parent
text: "Please drag element"
}
}
Is there a bug in the DropArea or did I misunderstood something? I know I can filter the files in the onDropped but then you loose the visual feedback you get on OSX when dragging file on an area that does not accept them.
It has been a known bug for a long time. A patch has been submitted and after been stalled for several months is now merged into 5.6 branch.
Anyone who wants to use this functionality MUST upgrade to Qt 5.6 or MANULLY integrate the available patch into his/her Qt version.
QQuickDropAreaPrivate, contained in DropArea, updates the containsDrag flag to true when a dragEnterEvent occurs, emitting the entered signal. It updates containsDrag to false when adragLeaveEvent occurs, emitting an exited signal. However, when the drag event is not accepted dragLeaveEvent is never called, leaving the private object in a incosistent state. Each subsequent dragEnterEvent is discarded since containsDrag is still true, i.e. the previous drag event is still considered active and the entered is no more emitted.
Since the issue is related to an interaction between private APIs and usage of the public APIs, the problem does not affect filtering using keys. Unfortunately, this approach does not seem to fit for the presented use case.
A quite partial workaround is to use a MouseArea along with the DropArea. The latter disables itself when a rejection occurs while the former enables back the DropArea for the next drop. This workaround covers the common case in which a wrong item is dropped inside the DropArea, which is the most common and intuitive for an end user. Releasing the wrong item outside the DropArea invalidate the mechanism (until the next drop).
Here's the code:
import QtQuick 2.1
import QtQuick.Controls 1.0
import QtQuick.Window 2.0
ApplicationWindow {
title: qsTr("Hello World")
width: 640
height: 480
visible: true
MouseArea {
anchors.fill: parent
hoverEnabled: true
enabled: !drop.enabled
onContainsMouseChanged: drop.enabled = true
}
DropArea {
id: drop
anchors.fill: parent
onEntered: {
console.log("[Droparea] entered");
// Ensure at least one file is supported before accepted the drag
for(var i = 0; i < drag.urls.length; i++)
if(validateFileExtension(drag.urls[i]))
return
console.log("No valid files, refusing drag event")
drag.accept()
drop.enabled = false
}
onExited: console.log("[Droparea] exited")
onDropped: console.log("[Droparea] dropped")
// Only MP3s
function validateFileExtension(filePath) {
return filePath.split('.').pop() == "mp3"
}
}
Text {
id: textDrop
anchors.centerIn: parent
text: "Please drag element"
}
}
you never put accepteed = true
just add drag.accepted = true after you set the valid as valid
for(var i = 0; i < drag.urls.length; i++) {
if(validateFileExtension(drag.urls[i])) {
validFile = true;
drag.accepted = true;
break;
}
}