Let's say I have a slider control and my user is sliding it back and forth really fast.
Is it possible to limit the rate at which QML calls the "new value available" C++ callback?
If you want to completely avoid the value being updated while the slider is dragged, you can use the updateValueWhileDragging property in Qt Quick Controls 1, and the live property in Qt Quick Controls 2.
In Qt Quick Controls 2, the slider controls have a valueAt() function which can be called to check the value at any time.
If you're writing your own slider in QML, you could limit the change signal emission using a Timer, for example:
property int value
readonly property int actualValue: // some calculation...
Timer {
running: slider.pressed
interval: 200
repeat: true
onTriggered: slider.value = slider.actualValue
}
Here's a generic C++-side solution that works with any QObject:
// https://github.com/KubaO/stackoverflown/tree/master/questions/qml-rate-limter-42284163
#include <QtCore>
class PropertyRateLimiter : public QObject {
Q_OBJECT
qint64 msecsPeriod{500};
const QByteArray property;
bool dirty{};
QVariant value;
QElapsedTimer time;
QBasicTimer timer;
QMetaMethod slot = metaObject()->method(metaObject()->indexOfSlot("onChange()"));
void signal() {
if (time.isValid()) time.restart(); else time.start();
if (dirty)
emit valueChanged(value, parent(), property);
else
timer.stop();
dirty = false;
}
Q_SLOT void onChange() {
dirty = true;
value = parent()->property(property);
auto elapsed = time.isValid() ? time.elapsed() : 0;
if (!time.isValid() || elapsed >= msecsPeriod)
signal();
else
if (!timer.isActive())
timer.start(msecsPeriod - elapsed, this);
}
void timerEvent(QTimerEvent *event) override {
if (timer.timerId() == event->timerId())
signal();
}
public:
PropertyRateLimiter(const char * propertyName, QObject * parent) :
QObject{parent}, property{propertyName}
{
auto mo = parent->metaObject();
auto property = mo->property(mo->indexOfProperty(this->property));
if (!property.hasNotifySignal())
return;
connect(parent, property.notifySignal(), this, slot);
}
void setPeriod(int period) { msecsPeriod = period; }
Q_SIGNAL void valueChanged(const QVariant &, QObject *, const QByteArray & name);
};
#include "main.moc"
And a test harness for it:
#include <QtQuick>
const char qmlData[] =
R"__end(
import QtQuick 2.6
import QtQuick.Controls 2.0
ApplicationWindow {
minimumWidth: 300
minimumHeight: 250
visible: true
Column {
anchors.fill: parent
Slider { objectName: "slider" }
Label { objectName: "label" }
}
}
)__end";
int main(int argc, char ** argv) {
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app{argc, argv};
QQmlApplicationEngine engine;
engine.loadData(QByteArray::fromRawData(qmlData, sizeof(qmlData)-1));
auto window = engine.rootObjects().first();
auto slider = window->findChild<QObject*>("slider");
auto label = window->findChild<QObject*>("label");
PropertyRateLimiter limiter("position", slider);
QObject::connect(&limiter, &PropertyRateLimiter::valueChanged, [&](const QVariant & val){
label->setProperty("text", val);
});
return app.exec();
}
Related
I have horizontal slider element which is disabled. I want to enable it when I click on it. As the slider is disabled I am unable to capture mouse events on it.
Thanks in advance
I must admit that my first suggestion was wrong:
However, if the slider is disabled the mouse event will be captured probably by something else - I would expect the parent widget.
That expectation was wrong.
I found out about this by trying myself in an MCVE.
For my luck, the other option – using an event filter – does work.
Sample testQClickDisabled.cc:
// Qt header:
#include <QtWidgets>
void populate(QListWidget &qLst)
{
for (int i = 1; i <= 20; ++i) {
qLst.addItem(QString("item %0").arg(i));
}
}
class EventFilter: public QObject {
private:
QListWidget &qLst;
public:
EventFilter(QListWidget &qLst): QObject(), qLst(qLst)
{
qApp->installEventFilter(this);
}
~EventFilter() { qApp->removeEventFilter(this); }
EventFilter(const EventFilter&) = delete;
EventFilter& operator=(const EventFilter&) = delete;
protected:
virtual bool eventFilter(QObject *pQObj, QEvent *pQEvent) override;
};
bool EventFilter::eventFilter(QObject *pQObj, QEvent *pQEvent)
{
if (QScrollBar *const pQScrBar = dynamic_cast<QScrollBar*>(pQObj)) {
if (pQScrBar == qLst.verticalScrollBar()
&& pQEvent->type() == QEvent::MouseButtonPress) {
qDebug() << "Vertical scrollbar hit.";
pQScrBar->setEnabled(true);
}
}
return QObject::eventFilter(pQObj, pQEvent);
}
// main application
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// setup GUI
QListWidget qLst;
qLst.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
qLst.verticalScrollBar()->setEnabled(false);
qLst.resize(200, 200);
qLst.show();
populate(qLst);
EventFilter qEventFilter(qLst);
// runtime loop
return app.exec();
}
Output:
I would just use a sibling MouseArea, like this:
Slider {
id: slider
enabled: false
}
MouseArea {
anchors.fill: slider
visible: !slider.enabled
onClicked: slider.enabled = true
}
So this may seem like a strange setup. I have a C++ object that inherits from QObject called "MasterGuiLogic" for simplicity. It is created with a pointer to another object called "MainEventBroker" which as you might guess handles all of my applications events. The MasterGuiLogic object is registered with qml as a context property so that it's properties can be used anywhere in my qml. So main.cpp looks like this:
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
MasterEventBroker *MainEventBroker = new MasterEventBroker();
MasterGuiLogic *MainGuiLogic = new MasterGuiLogic(*MainEventBroker);
qmlRegisterUncreatableType<MasterGuiLogic>("GrblCom", 1, 0, "MasterGuiLogic", "");
qmlRegisterUncreatableType<GuiLogic_SerialCom>("GrblCom", 1, 0, "GuiLogic_SerialCom", "");
QQmlApplicationEngine engine;
QQmlContext* context = engine.rootContext();
context->setContextProperty("MasterGuiLogic", &(*MainGuiLogic));
engine.load(QUrl(QLatin1String("qrc:/QmlGui/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
MasterGuiLogic creates an instance of another class called SerialCom, which is set as a Q_PROPERTY so that it's properties and public slots can be reached in qml through the MasterGuiLogic property.
MasterGuiLogic.h:
class MasterGuiLogic : public QObject
{
Q_OBJECT
Q_PROPERTY(GuiLogic_SerialCom* serialCom READ serialCom CONSTANT)
public:
MasterEventBroker *eventBroker;
explicit MasterGuiLogic(MasterEventBroker &ev, QObject *parent = nullptr);
GuiLogic_SerialCom* serialCom() const {
return Gui_SerialCom;
}
private:
GuiLogic_SerialCom *Gui_SerialCom;
MasterGuiLogic.cpp:
MasterGuiLogic::MasterGuiLogic(MasterEventBroker &ev, QObject *parent) : QObject(parent)
{
this->eventBroker = &ev;
this->Gui_SerialCom = new GuiLogic_SerialCom(this);
}
SerialCom.h:
//Forward Declare our parent
class MasterGuiLogic;
class GuiLogic_SerialCom : public QObject
{
Q_OBJECT
Q_PROPERTY(QStringList portNames READ portNames NOTIFY portNamesChanged)
Q_PROPERTY(bool connectedToPort READ connectedToPort NOTIFY connectedToPortChanged)
public:
MasterGuiLogic *parent;
explicit GuiLogic_SerialCom(MasterGuiLogic *parent = nullptr);
std::map<QString, QSerialPortInfo> portsMap;
QStringList portNames() {
return _portNames;
}
bool connectedToPort() {
return _connectedToPort;
}
private:
QStringList _portNames;
bool _connectedToPort = false;
signals:
void portNamesChanged(const QStringList &);
void connectedToPortChanged(const bool &);
public slots:
void connectToPort(const QString portName);
void disconnectFromPort(const QString portName);
};
SerialCom.cpp:
GuiLogic_SerialCom::GuiLogic_SerialCom(MasterGuiLogic *parent) : QObject(qobject_cast<QObject *>(parent))
{
this->parent = parent;
QList<QSerialPortInfo> allPorts = QSerialPortInfo::availablePorts();
for (int i = 0; i < allPorts.size(); ++i) {
this->_portNames.append(allPorts.at(i).portName());
this->portsMap[allPorts.at(i).portName()] = allPorts.at(i);
}
emit portNamesChanged(_portNames);
}
void GuiLogic_SerialCom::connectToPort(const QString portName) {
//TODO: Connect To Port Logic Here;
//Set Connected
this->_connectedToPort = true;
emit connectedToPortChanged(this->_connectedToPort);
qDebug() << portName;
}
void GuiLogic_SerialCom::disconnectFromPort(const QString portName) {
//TODO: DisConnect To Port Logic Here;
//Set DisConnected
this->_connectedToPort = false;
emit connectedToPortChanged(this->_connectedToPort);
qDebug() << portName;
}
So from qml it's pretty easy to read any of these properties and even send signals from qml to c++
For example, this works just fine:
connectCom.onClicked: {
if (MasterGuiLogic.serialCom.connectedToPort === false) {
MasterGuiLogic.serialCom.connectToPort(comPort.currentText);
} else {
MasterGuiLogic.serialCom.disconnectFromPort(comPort.currentText);
}
}
The problem is, I can't seem to find a way to connect to signals that are emitted from SerialCom. I thought I would be able to do something like this:
Connections: {
target: MasterGuiLogic.serialCom;
onConnectedToPortChanged: {
if (MasterGuiLogic.serialCom.connectedToPort === false) {
connectCom.text = "Disconnect";
comPort.enabled = false;
} else {
connectCom.text = "Connect";
comPort.enabled = true;
}
}
}
This should listen to the boolean property on SerialCom to change, but I get the following error:
QQmlApplicationEngine failed to load component
qrc:/QmlGui/main.qml:21 Type Page1 unavailable
qrc:/QmlGui/Page1.qml:49 Invalid attached object assignment
This just means that I can't "connect" using the target line above. Is there any other way I can connect to signals from a Q_PROPERTY of type QObject inside a ContextProperty?
First of all what should &(*MainGuiLogic) mean?
You are dereferencing and referencing again the MainGuiLogic? Why?
context->setContextProperty("MasterGuiLogic", MainGuiLogic); will be enought.
But registering MasterGuiLogic as Type and adding the Object named MasterGuiLogic can overide themself in QML world.
Set it like context->setContextProperty("MyGuiLogic", MainGuiLogic); to eleminate this behavior.
Also don't pass references between C++ and QML worlds like:
void connectedToPortChanged(**const bool &**);.
Just use atomic type and values (const bool);
and give it a name, to be able to use it as named value in QML:
void connectedToPortChanged(bool connected)
Here is an example with the structure like yours, which works. Just click in window and look in output console.
test.h:
#ifndef TEST_H
#define TEST_H
#include <QObject>
class GuiLogic_SerialCom : public QObject
{
Q_OBJECT
public:
GuiLogic_SerialCom(){}
signals:
void connectedToPortChanged(bool connected);
public slots:
void connectToPort(const QString & portName);
};
class MasterGuiLogic : public QObject
{
Q_OBJECT
public:
MasterGuiLogic();
Q_PROPERTY(GuiLogic_SerialCom * serialCom READ serialCom CONSTANT)
GuiLogic_SerialCom* serialCom() const {return test;}
Q_INVOKABLE void generate_signal();
private:
GuiLogic_SerialCom * test;
};
#endif // TEST_H
test.cpp:
#include "test.h"
#include <QDebug>
MasterGuiLogic::MasterGuiLogic()
{
this->test = new GuiLogic_SerialCom();
}
void MasterGuiLogic::generate_signal()
{
qDebug() << __FUNCTION__ << "Calling serialcom to gen signal";
this->test->connectToPort("88");
}
void GuiLogic_SerialCom::connectToPort(const QString &portName)
{
qDebug() << __FUNCTION__ << "got signal" << portName;
emit this->connectedToPortChanged(true);
}
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "test.h"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
qmlRegisterUncreatableType<MasterGuiLogic>("GrblCom", 1, 0, "MasterGuiLogic", "");
qmlRegisterUncreatableType<GuiLogic_SerialCom>("GrblCom", 1, 0, "GuiLogic_SerialCom", "");
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty(QStringLiteral("Test"), new MasterGuiLogic());
engine.load(QUrl(QLatin1String("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
main.qml:
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import GrblCom 1.0
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
MouseArea
{
anchors.fill: parent
onClicked:
{
Test.generate_signal();
}
}
Connections
{
target: Test.serialCom
onConnectedToPortChanged:
{
console.log("Got signal from SerialCom in QML. passed bool value is: " + connected);
}
}
}
Ok, I found the problem... The provided answers, while indeed helpful for other reasons, were not the correct solution. After going over the code by #Xplatforms, I couldn't figure out what the difference was between what I was doing and what he did.... until I saw this in my own code:
Connections: {
target: MasterGuiLogic.serialCom;
onConnectedToPortChanged: {
...
}
}
There isn't supposed to be a colon(:) there...
Connections {
target: MasterGuiLogic.serialCom;
onConnectedToPortChanged: {
...
}
}
Never try programming while sleepy...lol
I inherited QQuickWindow and created a frame-less window that can be moved by drag. Inside my window I put a Slider element. The problem is that the Slider forwards the events to the parent window and when I try to change the value on the slider, the window moves along. Here's how it behaves:
Is there a possibility to make the slider accept the mouse events and not forward them to the window?
Here's my code:
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QUrl>
#include "mywindow.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<MyWindow>("mycustomlib", 1, 0, "MyWindow");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
main.qml
import QtQuick 2.7
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import mycustomlib 1.0
MyWindow {
width: 300
height: 180
visible: true
x: 250
y: 250
color: "beige"
Slider {
anchors.fill: parent
value: 0.5
}
}
mywindow.h
#ifndef MYWINDOW_H
#define MYWINDOW_H
#include <QQuickWindow>
class MyWindow : public QQuickWindow
{
Q_OBJECT
public:
MyWindow(QWindow *pParent = Q_NULLPTR);
protected:
virtual void mouseMoveEvent(QMouseEvent *e) Q_DECL_OVERRIDE;
virtual void mousePressEvent(QMouseEvent *e) Q_DECL_OVERRIDE;
virtual void mouseReleaseEvent(QMouseEvent* e) Q_DECL_OVERRIDE;
private:
bool m_move;
QPoint m_initialMouseClickPos;
};
#endif // MYWINDOW_H
mywindow.cpp
#include "mywindow.h"
#include <QDebug>
#include <QCursor>
MyWindow::MyWindow(QWindow *pParent) :
QQuickWindow(pParent),
m_move(false)
{
setFlags(Qt::FramelessWindowHint);
}
void MyWindow::mouseMoveEvent(QMouseEvent *e)
{
if (m_move) {
const QPoint newMousePosition = e->pos() - m_initialMouseClickPos + position();
setPosition(newMousePosition);
}
QQuickWindow::mouseMoveEvent(e);
}
void MyWindow::mousePressEvent(QMouseEvent *e)
{
if (e->button() == Qt::LeftButton)
{
m_initialMouseClickPos = e->pos();
m_move = true;
}
QQuickWindow::mousePressEvent(e);
}
void MyWindow::mouseReleaseEvent(QMouseEvent *e)
{
if (e->button() == Qt::LeftButton)
{
m_move = false;
}
QQuickWindow::mouseReleaseEvent(e);
}
The problem is that QQuickWindow::mouseXxxEvent() delivers the event to the item it belongs to. You have overridden the event handlers, do your handling first, and then pass on the event to QQuickWindow. Therefore the Slider receives the events right after you have done your custom event handling. Either don't call the base class implementation when you don`t want it to deliver the event to items, or call the base class implementation first and do your custom handling afterwards only if the event was not accepted or so.
void MyWindow::mousePressEvent(QMouseEvent *e)
{
QQuickWindow::mousePressEvent(e);
if (!e->isAccepted() && e->button() == Qt::LeftButton)
{
m_initialMouseClickPos = e->pos();
m_move = true;
}
}
The slider isn't a widget, and it doesn't process events like widgets do :(
To implement drag on a QQuickWindow, you could have a mouse area in Qt Quick, behind the controls, and have it forward drags to a helper object that then drags the window around.
I created "fruit_swap" project in Qt Creator bt choosing-- Application -> Qt Quick Application -> Qt 5.5.
In fruit_swap, 'main()' function calls forkSwapper() which forks a process that swaps fruit image (apple <--> pear) once every second.
The swapping is done inside setImageURL() method which also emits "imageURLChanged" signal for the qml. From the output(shown below at the bottom), the signal is proven to be delivered to the qml side.
I was expecting the qml Image swapping. However, it didn't. The first image shown('pear') stayed motionlessly. What could have I done wrong? Any suggestion is welcome. I uploading every single source code below.
fruit_swap.pro
/* generated by Qt Creator */
TEMPLATE = app
QT += qml quick
CONFIG += c++11
SOURCES += main.cpp \
imageitem.cpp
RESOURCES += qml.qrc
# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH =
# Default rules for deployment.
include(deployment.pri)
HEADERS += \
imageitem.h
DISTFILES += \
FruitFrame.qml
imageitem.h
/* https://quickgit.kde.org/?p=scratch%2Fsune%2Fimageitem.git */
#include <QQuickPaintedItem>
/**
* #brief QQuickItem to show images/pixmaps/colors in a QML item
*
* As opposed to the Image from plain qml, this works on
* QImages, QPixmaps and QColors
*/
class ImageItem : public QQuickPaintedItem
{
// Q_PROPERTY(QVariant imageData READ imageData() WRITE setImageData
// NOTIFY imageDataChanged)
Q_OBJECT
Q_PROPERTY(QVariant imageURL READ imageURL() WRITE setImageURL
NOTIFY imageURLChanged)
public:
explicit ImageItem(QQuickItem *parent = 0);
/**
* \reimpl
*/
Q_INVOKABLE void paint(QPainter* painter) Q_DECL_OVERRIDE;
/**
* #brief image data u-ed by this item
* #return a QVariant wrapping the data
*/
QVariant imageData() const;
/**
* #brief Sets the image data
* #param newData
*/
void setImageData(const QVariant& newData);
QVariant imageURL() const;
/**
* #brief Sets the image data
* #param newData
*/
void setImageURL(const QVariant& fileName);
Q_SIGNALS:
/**
* #brief imageChanged
*/
void imageDataChanged();
void imageURLChanged();
private:
enum Type {
Unknown,
Pixmap,
Image,
Color
};
Type m_type;
QVariant m_imageData;
QVariant m_imageURL;
QRectF scaledRect(const QRect& sourceRect) const;
};
#endif // IMAGEITEM_H
imageitem.cpp
/* https://quickgit.kde.org/?p=scratch%2Fsune%2Fimageitem.git */
#include "imageitem.h"
#include <QPainter>
#include <qobjectdefs.h>
ImageItem::ImageItem(QQuickItem *parent) :
QQuickPaintedItem(parent)
{
m_imageURL = QUrl::fromLocalFile(QString("apple.jpg"));
}
void ImageItem::paint(QPainter* painter)
{
switch(m_type) {
case Unknown: {
return;
}
case Image: {
QImage image = m_imageData.value<QImage>();
painter->drawImage(scaledRect(image.rect()), image);
return;
}
case Pixmap: {
QPixmap pixmap = m_imageData.value<QPixmap>();
painter->drawPixmap(scaledRect(pixmap.rect()).toRect(), pixmap);
return;
}
case Color: {
QColor color = m_imageData.value<QColor>();
painter->fillRect(contentsBoundingRect(),color);
return;
}
}
}
QVariant ImageItem::imageData() const
{
return m_imageData;
}
QVariant ImageItem::imageURL() const
{
return m_imageURL;
}
void ImageItem::setImageURL(const QVariant &fileName)
{
m_imageURL = QUrl::fromLocalFile(fileName.value<QString>());
if (m_imageURL.canConvert<QUrl>()) {
QUrl url = m_imageURL.value<QUrl>();
if (!url.isEmpty() && url.isValid()
&& url.isLocalFile())
{
qDebug() << "URL is valid";
} else {
qDebug() << "URL is INvalid";
}
} else {
qDebug() << "URL is INvalid";
}
emit imageURLChanged();
}
void ImageItem::setImageData(const QVariant& newData)
{
switch(newData.userType()) {
case QMetaType::QPixmap: {
m_type = Pixmap;
break;
}
case QMetaType::QImage: {
m_type = Image;
break;
}
case QMetaType::QColor: {
m_type = Color;
break;
}
default: {
m_type = Unknown;
break;
}
}
m_imageData = newData;
emit imageDataChanged();
}
QRectF ImageItem::scaledRect(const QRect& sourceRect) const
{
QRectF targetRect = contentsBoundingRect();
QSizeF scaledSize;
double widthScale = targetRect.width() / sourceRect.width();
double heightScale = targetRect.height() / sourceRect.height();
if(widthScale < heightScale) {
scaledSize.setHeight(sourceRect.height() * widthScale);
scaledSize.setWidth(sourceRect.width() *widthScale);
} else {
scaledSize.setHeight(sourceRect.height() * heightScale);
scaledSize.setWidth(sourceRect.width() *heightScale);
}
QRectF result = QRectF(targetRect.left() + targetRect.width() /2 - scaledSize.width() /2,
targetRect.top() + targetRect.height()/2 - scaledSize.height()/2,
scaledSize.width(),scaledSize.height());
// QRectF result(QPointF(0,0),scaledSize);
qDebug() << result << targetRect << sourceRect << widthScale << heightScale ;
return result;
}
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QImage>
#include <unistd.h>
#include "imageitem.h"
void forkSwapper(ImageItem * fImage);
int main(int argc, char *argv[])
{
qmlRegisterType<ImageItem>("ImageItem",1,0,"ImageItem");
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
ImageItem *fImage = new ImageItem();
fImage->setImageURL("fruit.jpg");
engine.rootContext()->setContextProperty("cpp_imageURL", fImage);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
forkSwapper(fImage);
return app.exec();
}
void forkSwapper(ImageItem * fImage) {
int pid = fork();
if (pid == 0) {
int i = 0;
while (true) {
if (i++ % 2 == 0)
fImage->setImageURL("apple.jpg");
//std::system("cp apple.jpg fruit.jpg");
else
fImage->setImageURL("pear.jpg");
//std::system("cp pear.jpg fruit.jpg");
qDebug() << "fruit image changed";
sleep(1);
}
_exit (EXIT_FAILURE);
} else {
qDebug() << "swapper forked, PID:" << pid;
}
}
FruitFrame.qml
import QtQuick 2.5
import ImageItem 1.0
Rectangle {
property alias mouseArea: mouseArea
width: 360
height: 360
MouseArea {
id: mouseArea
anchors.fill: parent
Connections {
target: cpp_imageURL
onImageURLChanged: {
fruit_image.update();
// fruit_image.source = cpp_imageURL.imageURL;
console.log("image UURL-" + cpp_imageURL.imageURL);
}
}
Image {
id: fruit_image
x: 39
y: 94
width: 274
height: 204
source: cpp_imageURL.imageURL
cache: false
}
}
Text {
anchors.centerIn: parent
text: "Hello World"
anchors.verticalCenterOffset: -137
anchors.horizontalCenterOffset: -104
}
}
main.qml
import QtQuick 2.5
import QtQuick.Window 2.2
import ImageItem 1.0
Window {
visible: true
width: 360
height: 460
FruitFrame {
anchors.fill: parent
mouseArea.onClicked: {
Qt.quit();
}
}
}
Application Output
Starting /home/jbpark03/wem/fruit_swap/build/fruit_swap...
QML debugging is enabled. Only use this in a safe environment.
URL is valid
URL is valid
qml: image UURL-file:apple.jpg
fruit image changed
swapper forked, PID: 3078
URL is valid
qml: image UURL-file:pear.jpg
fruit image changed
URL is valid
qml: image UURL-file:apple.jpg
fruit image changed
URL is valid
qml: image UURL-file:pear.jpg
fruit image changed
URL is valid
qml: image UURL-file:apple.jpg
END.
The code works perfectly well. I think the problem is you're blocking the Qt application because you're using sleep.
In fact, you see the image pear.jpg because is the first image shown after fruit.jpg and before any sleep.
If, for example, you use a QTimer or a QThread, you will check the image is properly updated.
Example:
mytimer.h
#ifndef MYTIMER_H
#define MYTIMER_H
#include <QObject>
#include <QTimer>
#include "imageitem.h"
class MyTimer : public QObject
{
Q_OBJECT
public:
explicit MyTimer(ImageItem * fImage, QObject *parent = 0);
private:
QTimer *timer;
ImageItem *myImage;
signals:
public slots:
void update();
};
#endif // MYTIMER_H
mytimer.cpp
#include <QDebug>
#include <QTest>
#include "mytimer.h"
MyTimer::MyTimer(ImageItem * fImage, QObject *parent) : QObject(parent)
{
myImage = fImage;
timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(update()));
timer->start(1000);
}
void MyTimer::update()
{
myImage->setImageURL(":pear.jpg");
QTest::qWait(250);
myImage->setImageURL(":apple.jpg");
}
With this timer, we're going to change the image every 1 second. Now, in our main.cpp:
#include "mytimer.h"
int main(int argc, char *argv[])
{
qmlRegisterType<ImageItem>("ImageItem",1,0,"ImageItem");
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
ImageItem *fImage = new ImageItem();
MyTimer *timer = new MyTimer(fImage);
fImage->setImageURL(":fruit.jpg");
engine.rootContext()->setContextProperty("cpp_imageURL", fImage);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
I want to perform a long task when a button is clicked. I want this task to block the UI, because the application cannot function until the task is done. However, I want to indicate to the user that something is happening, so I have a BusyIndicator (that runs on the render thread) and is set to display before the operation begins. However, it never renders. Why?
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QDateTime>
#include <QDebug>
class Task : public QObject
{
Q_OBJECT
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
public:
Task() : mRunning(false) {}
Q_INVOKABLE void run() {
qDebug() << "setting running property to true";
mRunning = true;
emit runningChanged();
// Try to ensure that the scene graph has time to begin the busy indicator
// animation on the render thread.
Q_ASSERT(QMetaObject::invokeMethod(this, "doRun", Qt::QueuedConnection));
}
bool running() const {
return mRunning;
}
signals:
void runningChanged();
private:
Q_INVOKABLE void doRun() {
qDebug() << "beginning long, blocking operation";
QDateTime start = QDateTime::currentDateTime();
while (start.secsTo(QDateTime::currentDateTime()) < 2) {
// Wait...
}
qDebug() << "finished long, blocking operation";
qDebug() << "setting running property to false";
mRunning = false;
emit runningChanged();
}
bool mRunning;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Task task;
engine.rootContext()->setContextProperty("task", &task);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
main.qml:
import QtQuick 2.6
import QtQuick.Window 2.2
import Qt.labs.controls 1.0
Window {
width: 600
height: 400
visible: true
Shortcut {
sequence: "Ctrl+Q"
onActivated: Qt.quit()
}
Column {
anchors.centerIn: parent
spacing: 20
Button {
text: task.running ? "Running task" : "Run task"
onClicked: task.run()
}
BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: task.running
onRunningChanged: print("BusyIndicator running =", running)
}
}
}
The debugging output looks correct in terms of the order of events:
setting running property to true
qml: BusyIndicator running = true
beginning long, blocking operation
finished long, blocking operation
setting running property to false
qml: BusyIndicator running = false
Most animations in QML depends on properties managed in the main thread and are thus blocked when the main UI thread is blocked. Look into http://doc.qt.io/qt-5/qml-qtquick-animator.html for animations that can run when the main thread is blocked. If possible, I'd move the operation into another thread though, that's much simpler and also allows for e.g. cancellation of the operation from the UI.
Invoking a function with Qt::QueuedConnection doesn't guarantee that the BusyIndicator has had the chance to begin animating. It merely guarantees that:
The slot is invoked when control returns to the event loop of the receiver's thread.
Another solution that might sound promising is QTimer::singleShot():
QTimer::singleShot(0, this, SLOT(doRun()));
As a special case, a QTimer with a timeout of 0 will time out as soon as all the events in the window system's event queue have been processed. This can be used to do heavy work while providing a snappy user interface [...]
However, this also won't work. I'm not sure why. It could be that, internally, the rendering/animation is not done via a queued invocation, and so the timeout happens too early.
You could specify an arbitrary amount of time to wait:
QTimer::singleShot(10, this, SLOT(doRun()));
This will work, but it's not that nice; it's just guessing.
What you need is a reliable way of knowing when the scene graph has begun the animation.
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QDateTime>
#include <QDebug>
#include <QTimer>
#include <QQuickWindow>
class Task : public QObject
{
Q_OBJECT
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
public:
Task(QObject *parent = 0) :
QObject(parent),
mRunning(false) {
}
signals:
void runningChanged();
public slots:
void run() {
qDebug() << "setting running property to true";
mRunning = true;
emit runningChanged();
QQmlApplicationEngine *engine = qobject_cast<QQmlApplicationEngine*>(parent());
QQuickWindow *window = qobject_cast<QQuickWindow*>(engine->rootObjects().first());
connect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
}
bool running() const {
return mRunning;
}
private slots:
void doRun() {
qDebug() << "beginning long, blocking operation";
QDateTime start = QDateTime::currentDateTime();
while (start.secsTo(QDateTime::currentDateTime()) < 2) {
// Wait...
}
qDebug() << "finished long, blocking operation";
QQmlApplicationEngine *engine = qobject_cast<QQmlApplicationEngine*>(parent());
QQuickWindow *window = qobject_cast<QQuickWindow*>(engine->rootObjects().first());
disconnect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
qDebug() << "setting running property to false";
mRunning = false;
emit runningChanged();
}
private:
bool mRunning;
};
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Task task(&engine);
engine.rootContext()->setContextProperty("task", &task);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
main.qml:
import QtQuick 2.6
import QtQuick.Window 2.2
import Qt.labs.controls 1.0
Window {
width: 600
height: 400
visible: true
Shortcut {
sequence: "Ctrl+Q"
onActivated: Qt.quit()
}
Column {
anchors.centerIn: parent
spacing: 20
Button {
text: task.running ? "Running task" : "Run task"
onClicked: task.run()
}
BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: task.running
onRunningChanged: print("BusyIndicator running =", running)
}
}
}
This solution relies on having access to the application window, which isn't that nice, but it eliminates any guesswork. Note that if we don't disconnect from the signal afterwards, it will continue to be called each time the scene graph has finished synchronising, so it's important to do this.
If you have several operations that will need this type of solution, consider creating a reusable class:
class BlockingTask : public QObject
{
Q_OBJECT
Q_PROPERTY(bool running READ running NOTIFY runningChanged)
public:
BlockingTask(QQmlApplicationEngine *engine) :
mEngine(engine),
mRunning(false) {
}
bool running() const {
return mRunning;
}
signals:
void runningChanged();
public slots:
void run() {
qDebug() << "setting running property to true";
mRunning = true;
emit runningChanged();
QQuickWindow *window = qobject_cast<QQuickWindow*>(mEngine->rootObjects().first());
connect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
}
protected:
virtual void execute() = 0;
private slots:
void doRun() {
QQuickWindow *window = qobject_cast<QQuickWindow*>(mEngine->rootObjects().first());
disconnect(window, SIGNAL(afterSynchronizing()), this, SLOT(doRun()));
execute();
qDebug() << "setting running property to false";
mRunning = false;
emit runningChanged();
}
private:
QQmlApplicationEngine *mEngine;
bool mRunning;
};
Then, subclasses only have to worry about their logic:
class Task : public BlockingTask
{
Q_OBJECT
public:
Task(QQmlApplicationEngine *engine) :
BlockingTask(engine) {
}
protected:
void execute() Q_DECL_OVERRIDE {
qDebug() << "beginning long, blocking operation";
QDateTime start = QDateTime::currentDateTime();
while (start.secsTo(QDateTime::currentDateTime()) < 2) {
// Wait...
}
qDebug() << "finished long, blocking operation";
}
};