Safely deleting QML component being used in StackView transition - c++

Overview
My question deals with the lifetime of a QObject created by QQmlComponent::create(). The object returned by create() is the instantiation of a QQmlComponent, and I am adding it to a QML StackView. I am creating the object in C++ and passing it to QML to display in the StackView. The problem is that I am getting errors when I pop an item from the stack. I wrote a demo app to illustrate what's happening.
Disclaimer: Yes, I know that reaching into QML from C++ is not "best practice." Yes, I know that you should do UI stuff in QML. However, in the production world, there is a ton of C++ code that needs to be shared with the UI, so there needs to be some interop between C++ and CML. The primary mechanism I'm using is Q_PROPERTY bindings by setting the context on the C++ side.
This screen is what the demo looks like when it starts:
The StackView is in the center with a gray background and has one item in it (with the text 'Default View'); this item is instantiated and managed by QML. Now if you press the Push button, then the C++ back-end creates an object from ViewA.qml and places it on the stack...here is a screen shot showing this:
At this point, I press Pop to remove "View A" (in red in the picture above) from the StackView. C++ calls into QML to pop the item from the stack and then deletes the object it created. The problem is that QML needs this object for the transition animation (I'm using the default animation for StackView), and it complains when I delete it from C++. So I think I understand why this is happening, but I'm not sure how to find out when QML is done with the object so I can delete it. How can I make sure QML is done with an object that I created in C++ so I can safely delete it?
Summarizing, here are the steps that reproduce the problem I am describing:
Start program
Click Push
Click Pop
The following output shows the TypeErrors that happen when the item is popped in step 3 above:
Output
In the output below, I press "Push" once, then I press "Pop". Note the two TypeErrors when ~ViewA() is called.
root object name = "appWindow"
[c++] pushView() called
qml: [qml] pushView called with QQuickRectangle(0xdf4c00, "my view")
[c++] popView() called
qml: [qml] popView called
[c++] deleting view
~ViewA() called
file:///opt/Qt5.8.0/5.8/gcc_64/qml/QtQuick/Controls/Private/StackViewSlideDelegate.qml:97: TypeError: Cannot read property 'width' of null
file:///opt/Qt5.8.0/5.8/gcc_64/qml/QtQuick/Controls/StackView.qml:899: TypeError: Type error
Context must be set from C++
Clearly, what is happening is that the object (item) that the StackView is using is being deleted by C++, but QML still needs this item for the transition animation. I suppose I could create the object in QML and let the QML engine manage the lifetime, but I need to set the QQmlContext of the object to bind the QML view to Q_PROPERTYs on the C++ side.
See my related question on Who owns object returned by QQmlIncubator.
Code Example
I've generated a minimally complete example to illustrate the problem. All files are listed below. In particular, look at the code comments in ~ViewA().
// main.qml
import QtQuick 2.3
import QtQuick.Controls 1.4
Item {
id: myItem
objectName: "appWindow"
signal signalPushView;
signal signalPopView;
visible: true
width: 400
height: 400
Button {
id: buttonPushView
text: "Push"
anchors.left: parent.left
anchors.top: parent.top
onClicked: signalPushView()
}
Button {
id: buttonPopView
text: "Pop"
anchors.left: buttonPushView.left
anchors.top: buttonPushView.bottom
onClicked: signalPopView()
}
Rectangle {
x: 100
y: 50
width: 250
height: width
border.width: 1
StackView {
id: stackView
initialItem: view
anchors.fill: parent
Component {
id: view
Rectangle {
color: "#DDDDDD"
Text {
anchors.centerIn: parent
text: "Default View"
}
}
}
}
}
function pushView(item) {
console.log("[qml] pushView called with " + item)
stackView.push(item)
}
function popView() {
console.log("[qml] popView called")
stackView.pop()
}
}
// ViewA.qml
import QtQuick 2.0
Rectangle {
id: myView
objectName: "my view"
color: "#FF4a4a"
Text {
text: "View A"
anchors.centerIn: parent
}
}
// viewa.h
#include <QObject>
class QQmlContext;
class QQmlEngine;
class QObject;
class ViewA : public QObject
{
Q_OBJECT
public:
explicit ViewA(QQmlEngine* engine, QQmlContext* context, QObject *parent = 0);
virtual ~ViewA();
// imagine that this view has property bindings used by 'context'
// Q_PROPERTY(type name READ name WRITE setName NOTIFY nameChanged)
QQmlContext* context = nullptr;
QObject* object = nullptr;
};
// viewa.cpp
#include "viewa.h"
#include <QQmlEngine>
#include <QQmlContext>
#include <QQmlComponent>
#include <QDebug>
ViewA::ViewA(QQmlEngine* engine, QQmlContext *context, QObject *parent) :
QObject(parent),
context(context)
{
// make property bindings visible to created component
this->context->setContextProperty("ViewAContext", this);
QQmlComponent component(engine, QUrl(QLatin1String("qrc:/ViewA.qml")));
object = component.create(context);
}
ViewA::~ViewA()
{
qDebug() << "~ViewA() called";
// Deleting 'object' in this destructor causes errors
// because it is an instance of a QML component that is
// being used in a transition. Deleting it here causes a
// TypeError in both StackViewSlideDelegate.qml and
// StackView.qml. If 'object' is not deleted here, then
// no TypeError happens, but then 'object' is leaked.
// How should 'object' be safely deleted?
delete object; // <--- this line causes errors
delete context;
}
// viewmanager.h
#include <QObject>
class ViewA;
class QQuickItem;
class QQmlEngine;
class ViewManager : public QObject
{
Q_OBJECT
public:
explicit ViewManager(QQmlEngine* engine, QObject* topLevelView, QObject *parent = 0);
QList<ViewA*> listOfViews;
QQmlEngine* engine;
QObject* topLevelView;
public slots:
void pushView();
void popView();
};
// viewmanager.cpp
#include "viewmanager.h"
#include "viewa.h"
#include <QQmlEngine>
#include <QQmlContext>
#include <QDebug>
#include <QMetaMethod>
ViewManager::ViewManager(QQmlEngine* engine, QObject* topLevelView, QObject *parent) :
QObject(parent),
engine(engine),
topLevelView(topLevelView)
{
QObject::connect(topLevelView, SIGNAL(signalPushView()), this, SLOT(pushView()));
QObject::connect(topLevelView, SIGNAL(signalPopView()), this, SLOT(popView()));
}
void ViewManager::pushView()
{
qDebug() << "[c++] pushView() called";
// create child context
QQmlContext* context = new QQmlContext(engine->rootContext());
auto view = new ViewA(engine, context);
listOfViews.append(view);
QMetaObject::invokeMethod(topLevelView, "pushView",
Q_ARG(QVariant, QVariant::fromValue(view->object)));
}
void ViewManager::popView()
{
qDebug() << "[c++] popView() called";
if (listOfViews.count() <= 0) {
qDebug() << "[c++] popView(): no views are on the stack.";
return;
}
QMetaObject::invokeMethod(topLevelView, "popView");
qDebug() << "[c++] deleting view";
auto view = listOfViews.takeLast();
delete view;
}
// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickView>
#include <QQuickItem>
#include "viewmanager.h"
#include <QDebug>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQuickView view;
view.setSource(QUrl(QLatin1String("qrc:/main.qml")));
QObject* item = view.rootObject();
qDebug() << "root object name = " << item->objectName();
ViewManager viewManager(view.engine(), item);
view.show();
return app.exec();
}

I'm posting an answer to my own question. If you post an answer, I'll consider accepting your answer instead of this one. But, this is a possible work-around.
The problem is that a QML object that is created in C++ needs to live long enough for the QML engine to complete all transitions. The trick I'm using is to mark the QML object instance for deletion, wait a few seconds for QML to finish the animation, and then delete the object. The "hacky" part here is that I have to guess how many seconds I should wait until I think that QML is completely finished with the object.
First, I make a list of objects that are scheduled to be destroyed. I also make a slot that will be called after a delay to actually delete the object:
class ViewManager : public QObject {
public:
...
QList<ViewA*> garbageBin;
public slots:
void deleteAfterDelay();
}
Then, when the stack item is popped, I add the item to garbageBin and do a single-shot signal in 2 seconds:
void ViewManager::popView()
{
if (listOfViews.count() <= 0) {
qDebug() << "[c++] popView(): no views are on the stack.";
return;
}
QMetaObject::invokeMethod(topLevelView, "popView");
// schedule the object for deletion in a few seconds
garbageBin.append(listOfViews.takeLast());
QTimer::singleShot(2000, this, SLOT(deleteAfterDelay()));
}
After a few seconds, the deleteAfterDelay() slot is called and "garbage collects" the item:
void ViewManager::deleteAfterDelay()
{
if (garbageBin.count() > 0) {
auto view = garbageBin.takeFirst();
qDebug() << "[c++] delayed delete activated for " << view->objectName();
delete view;
}
}
Aside from not being 100% confident that waiting 2 seconds will always be long enough, it seems to work extremely well in practice--no more TypeErrors and all objects created by C++ are properly cleaned up.

I believe I have identified a way to ditch the garbage list that #Matthew Kraus recommended. I let QML handle destroying the view while popping out of the StackView.
warning: Snippets are incomplete and only meant to illustrate extension to OP's post
function pushView(item, id) {
// Attach option to automate the destruction on pop (called by C++)
rootStackView.push(item, {}, {"destroyOnPop": true})
}
function popView(id) {
// Pop immediately (removes transition effects) and verify that the view
// was deleted (null). Else, delete immediately.
var old = rootStackView.pop({"item": null, "immediate": true})
if (old !== null) {
old.destroy() // Requires C++ assigns QML ownership
}
// Tracking views in m_activeList by id. Notify C++ ViewManager that QML has
// done his job
viewManager.onViewClosed(id)
}
You will quickly find that the interpreter yells at you on delete if the object was created, and still owned, by C++.
m_pEngine->setObjectOwnership(view, QQmlEngine::JavaScriptOwnership);
QVariant arg = QVariant::fromValue(view);
bool ret = QMetaObject::invokeMethod(
m_pRootPageObj,
"pushView",
Q_ARG(QVariant, arg),
Q_ARG(QVariant, m_idCnt));

Related

Qt: onChildrenChanged not launched when QML components are created at runtime from C++ code

For a project, I need to create QML components at runtime from C++.
My general architecture is the following:
Project1:
Engine.h
Engine.cpp
CustObject.h
CustObject.cpp
Plugin.h
Plugin.cpp
Dummy.qml
Project2:
main.cpp
main.qml
What I want to do is instantiate Engine.cpp as a QML object (possible since I registered it in the Plugin class and made it available to Project2) and then create dynamically CustObject instances (which are also registered to be used by Project2) from Engine. In the end I want that if I write:
ApplicationWindow{
id: window
visible: true
Engine{
id: eng1
CustObject{
id: custObj1
}
}
}
This will be the same as writing something like
ApplicationWindow {
id: window
visible: true
Button {
text: "add new child"
onClicked: {
console.log("QML: Number children before", eng1.children.length);
eng1.addNewChildren();
console.log("QML: Number children after", eng1.children.length);
}
}
Engine{
id: eng1
onChildrenChanged: console.log("Changed")
}
}
And I should see that the number of children is incremented and onChildrenChanged from eng1 should be launched.
The problem is that neither the number of children is incremented, nor the signal onChildrenChanged is launched.
I had also the other problem that in order to add a children to the parent, eng1 in my case, I used the function QQmlComponent(QQmlEngine *engine, const QUrl &url, QObject *parent = 0) of QQMLComponent class. But I cannot find a way to transform my CustObject class into a QUrl since it is not a .qml file.
Therefore I first tried to add a dummy qml object called: Dummy.qml instead of CustObject object. Dummy.qml looks like this:
import QtQuick 2.0
Item {
property int nb: 1
}
The code of my Engine class looks like this:
Engine.h:
#ifndef ENGINE_H
#define ENGINE_H
#include <QQuickItem>
#include <QQmlComponent>
#include <QQmlEngine>
#include <QQmlContext>
class Engine : public QQuickItem{
Q_OBJECT
public:
explicit Engine(QQuickItem* parent = 0);
virtual ~Engine();
Q_INVOKABLE QObject* addNewChildren();
};
#endif // ENGINE_H
Engine.cpp:
#include "Engine.h"
Engine::Engine(QQuickItem* parent) :
QQuickItem(parent)
{
}
Engine::~Engine(){ }
QObject* Engine::addNewChildren(){
qDebug() << "CPP: Number children before " << this->children().size();
QObject* parentEntity = this;
QQmlComponent* childrenEntity;
QQmlComponent component(qmlEngine(this), QUrl("qrc:///src/Dummy.qml"));
QQuickItem *childrenItem = qobject_cast<QQuickItem*>(component.create());
QQmlEngine::setObjectOwnership(childrenItem, QQmlEngine::CppOwnership);
childrenItem->setParent(parentEntity);
childrenItem->setProperty("nb", 2);
qDebug() << "CPP: Number children after" << this->children().size();
//qDebug() << "Property value:" << QQmlProperty::read(childrenItem, "nb").toInt();
return childrenItem;
}
But my output when I run main.qml is the following:
qml: QML: Number children before 0
CPP: Number children before 0
CPP: Number children after 1
qml: QML: Number children after 0
And I commented the line corresponding to QQmlProperty::read due to the following error: "incomplete type 'QQmlProperty' used in nested name specifier
qDebug() << "Property value:" << QQmlProperty::read(childrenItem, "nb").toInt();"
^
I have therefore the following questions:
Why is the number of children incrementation not seen from qml (but visible from cpp)?
Why is onChildrenChanged not launched from qml?
How can I add dynamically CustObject class (which is visible as a qml object from Project2 point of view since it is registered)
instead of Dummy.qml?
How to read a property of a dynamically added object in C++ just after its creation (i.e. how to use QQMlProperty::read)?
Thank you a lot in advance for any help you could give me!
Why is the number of children incrementation not seen from qml (but visible from cpp)?
QML doesn't use QObject::children(), instead it uses QQuickItem::childItems(). Yes, that's right, there are two different list of children, one from QObject, and one from QQuickItem. Both serve different purposes: The one from QObject is mainly for memory management (children get deleted when parent gets deleted), while the one from QQuickItem is for the 'visual hierachy', e.g. children get drawn on top of their parent. More details are available in the docs.
Why is onChildrenChanged not launched from qml?
Because the onChildrenChanged is only emitted when QQuickItem::childItems() changes, which it doesn't. Call setParentItem() in addition to setParent() to fix that.
How can I add dynamically CustObject class (which is visible as a qml object from Project2 point of view since it is registered) instead of Dummy.qml?
By simply creating the object yourself and setting the parent and parentItem. There is no need to use QQmlComponent here.
QObject childrenItem = new CustObject();
childrenItem->setParent(parentEntity);
childrenItem->setParentItem(parentEntity);
How to read a property of a dynamically added object in C++ just after its creation (i.e. how to use QQMlProperty::read)?
Calling QQuickItem::childItems() should do the trick, no need to read a property. FWIW, there probably was an #include <QQmlProperty> missing in the code that didn't work.

How to catch C++ signal in QML signal handler after type registration?

I am developing a basic qml-cpp application to understand how one interacts with another. I have a MessageSetter C++ class and one main.qml. Since I wish to understand two-way communication, I exposed MessageSetter properties to qml using setContextProperty and also registered MessageSetter class with qml (instantiable registration). Exposed properties work fine. Now when a qml button is clicked, then the signal (qmlBtnClicked) is successfully caught in a MessageSetter slot(onQmlButtonClicked). This slot further emits another MessageSetter signal (colorChanged). This new (C++) signal should be caught in qml registered MessageSetter's signal handler (onColorChanged) but it does not arrive here in any case. Below is main.cpp code:
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
qmlRegisterType<MessageSetter>("com.SkillLotto.MessageSetter", 1, 0, "SetMessage");
MessageSetter data;
engine.rootContext()->setContextProperty("msgSetter", &data);
QQmlComponent component(&engine, QUrl::fromLocalFile("main.qml"));
QObject *object = component.create()->findChild<QObject*>("setTextBtn");
QObject::connect(object, SIGNAL(qmlBtnClicked()), &data, SLOT(onQmlButtonClicked()));
return app.exec();
}
This is MessageSetter slot that emits another signal:
void MessageSetter::onQmlButtonClicked()
{
emit colorChanged("red");
}
This is qml code, this signal handler never gets called:
SetMessage{
onColorChanged: {
rect.color = color //rect is some rectangle in this file.
}
}
As I stated, qml signal is successfully caught in C++ slot but I am unable to catch this C++ signal in qml signal handler. Any help please.
This question, as I see, is focussed on qmlRegisterType() and should not be duplicate of this question? I also want to know whether qmlRegisterType() and setContextProperty() cant be used simultaneously or not ?
I think your code should work well.
I don't have the whole code so I don't know if you have the right methods implemented.
In order to get the signal using qmlRegisterType you need some requirements. Check if you have the Q_PROPERTY macro call implemented. Any property that is writable should have an associated NOTIFY signal that is emitted whenever the property value has changed.
If so, when you change the color property in the SetMessage component, the signal onColorChanged should be triggered.
Here you have an example where two signals are emitted: the first one when the size property is updated and the second one if the C++ mouseClick method is called using a MouseArea.
By the way, you don't need setContextProperty to integrate your C++ class with QML. qmlRegisterType should be enough. Or vice versa, depending on your needs. You can use both, but then you will have two different objects to work with. It really depends on what you want to achieve.
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include "customitem.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<CustomItem>("CustomItem", 1,0, "CustomItem");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
customitem.h
#ifndef CUSTOMITEM_H
#define CUSTOMITEM_H
#include <QObject>
class CustomItem: public QObject
{
Q_OBJECT
/*
* Any property that is writable should have an associated NOTIFY signal.
* Ref: http://doc.qt.io/qt-5/qtqml-cppintegration-exposecppattributes.html#exposing-properties
*/
Q_PROPERTY(int size READ size WRITE setSize NOTIFY sizeChanged)
public:
CustomItem(QObject *parent = 0);
int size() const;
void setSize(int);
Q_INVOKABLE void mouseClick();
private:
int m_size;
signals:
void sizeChanged(int size);
void clicked();
public slots:
};
#endif // CUSTOMITEM_H
customitem.cpp
#include "customitem.h"
#include <QDebug>
CustomItem::CustomItem(QObject *parent)
: QObject(parent), m_size(0)
{
}
int CustomItem::size() const
{
return m_size;
}
void CustomItem::setSize(int size)
{
m_size = size;
emit sizeChanged(m_size);
}
void CustomItem::mouseClick()
{
qDebug() << "CustomItem::mouseClick()";
emit clicked();
}
main.qml
import QtQuick 2.5
import QtQuick.Window 2.2
import CustomItem 1.0
Window {
visible: true
TextInput {
id: mySize
x: 0
y: 0
text: "100"
}
CustomItem {
id: customItem
size: mySize.text
onSizeChanged: console.log("size changed:", size)
onClicked: console.log("onClicked!")
}
Rectangle {
id: rect
x: 50
y: 50
width: customItem.size
height: customItem.size
color: "red"
MouseArea {
anchors.fill: parent
onClicked: { customItem.mouseClick() }
}
}
}
Because you are using two different instances of your MessageSetter, one is data in main.cpp and other is new instance SetMessage. Use only one to connect both signals/slots.
You are expecting onColorChanged signal from SetMessage but the signal is coming from data (in main.cpp).
Why do you need instantiable if you want to create a context property?
Add this in your main.qml file
Connections {
target: msgSetter
onColorChanged: {
console.log("received color changed signal");
}
}

QAbstractVideoSurface example

I'm trying to make myself a QML Camera item which has more functions, and also provide a source to the VideoOutput element. Such as this:
VideoOutput{
source:mycamera
}
MyCustomCamera{
id:mycamera
}
in the document it says
If you are extending your own C++ classes to interoperate with
VideoOutput, you can either provide a QObject based class with a
mediaObject property that exposes a QMediaObject derived class that
has a QVideoRendererControl available, or you can provide a QObject
based class with a writable videoSurface property that can accept a
QAbstractVideoSurface based class and can follow the correct protocol
to deliver QVideoFrames to it.
I have tried giving my object a private property mediaObject, which is of type QCamera, but looks like QCamera does not have a QVideoRenderControl (or its my fault not knowing how to do it correctly).
I need to achieve the effect I've shown in the beginning, anyway is welcomed.
Or otherwise can anyone give me a short example on what is meant by "a writable videoSurace property that accept blablabla and follow the correct protocol"?
I can't help you with your main concern but i can give you an example usage of the videoSurface.You can use the "writable videoSurface" like this:
My example consists of three main steps:
You write a class that has a QAbstactVideoSurface property. This class will be your video provider which can display frames on the VideoOutput via calling its present() function.
videoadapter.h
#ifndef VIDEOADAPTER_H
#define VIDEOADAPTER_H
#include <QObject>
#include <QAbstractVideoSurface>
#include <QVideoSurfaceFormat>
#include <QTimer>
class VideoAdapter : public QObject
{
Q_OBJECT
Q_PROPERTY(QAbstractVideoSurface* videoSurface READ videoSurface WRITE setVideoSurface NOTIFY signalVideoSurfaceChanged)
public:
explicit VideoAdapter(QObject *parent = nullptr);
QAbstractVideoSurface *videoSurface() const;
void setVideoSurface(QAbstractVideoSurface *videoSurface);
signals:
void signalVideoSurfaceChanged();
private slots:
void slotTick();
private:
void startSurface();
private:
QAbstractVideoSurface *mVideoSurface;
QVideoSurfaceFormat *mSurfaceFormat;
QImage *mImage;
QTimer mTimer;
};
#endif // VIDEOADAPTER_H
videoadapter.cpp
#include "videoadapter.h"
#include <QDebug>
VideoAdapter::VideoAdapter(QObject *parent)
: QObject(parent), mVideoSurface(nullptr), mSurfaceFormat(nullptr)
{
mTimer.setInterval(1000);
connect(&mTimer, &QTimer::timeout, this, &VideoAdapter::slotTick);
}
QAbstractVideoSurface *VideoAdapter::videoSurface() const
{
return mVideoSurface;
}
void VideoAdapter::setVideoSurface(QAbstractVideoSurface *videoSurface)
{
if(videoSurface != mVideoSurface)
{
mVideoSurface = videoSurface;
emit signalVideoSurfaceChanged();
startSurface();
// This is the test timer that will tick for us to present the image
// on the video surface
mTimer.start();
}
}
void VideoAdapter::slotTick()
{
QVideoFrame frame(*mImage);
mVideoSurface->present(frame);
}
void VideoAdapter::startSurface()
{
mImage = new QImage("../resources/images/test.jpg");
auto pixelFormat = QVideoFrame::pixelFormatFromImageFormat(mImage->format());
mSurfaceFormat = new QVideoSurfaceFormat(mImage->size(), pixelFormat);
if(!mVideoSurface->start(*mSurfaceFormat))
{
qDebug() << "Surface couldn't be started!";
}
}
This class only loads an image file and displays it with the usage of a timer but in your case you will be having a frame source so you can change this to suit your needs. If you can convert your frame to QImage of QVideoFrame you can display it like this.
You have to make this class usable in QML. In my case i created an object and made it visible to QML via setting it as a property.
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
QQmlDebuggingEnabler enabler;
VideoAdapter adapter;
// When you do this this object is made visible to QML context with the
// given name
engine.rootContext()->setContextProperty("videoAdapter", &adapter);
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
You give this object to the VideoOutput as source in QML.
Window {
visible: true
width: 640
height: 480
color: "black"
title: qsTr("Video Player")
VideoOutput {
id: videoPlayer
anchors.fill: parent
source: videoAdapter
}
}
This example as i said is a simple one that only loads an image and only displays that one image periodically.
This question is an old one and you probably moved on but hope this can at least help other people.
The code provided by #U.Tuken works fine, except if I change the name of property name in Q_PROPERTY from "videoSurface" to any other word, it doesn't work. That is very strange behaviour cause from Qt's point of view "videoSurface" is just a name.
Additionally I got error
"qt.gui.icc: fromIccProfile: failed minimal tag size sanity".
This error pops up if the imported "JPG" is not of correct format
as per this link.
Changing the "JPG" file helped me get rid of the above warning.

how to access events of a specific QML control from c++

Is there a way of accessing signals(such as clicked()) of a QML control such as a button, from c++. Assume that I have the memory address of that specific control. I just want to simulate a click event from c++ code.
Easy. You just create a slot in a C++ object that has a QObject base to it, make sure its registered as a QML type, then you "instantiate" it in the QML document, and connect the desired signal to the C++ object through QML using connect() and handle the logic from the C++ side.
Example:
I have a Rectangle I want to get the onWidthChanged signal from and use it in my class ShapeTracker which tracks when shapes change or whatever
in main.cpp:
#include "shapetracker.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
/* this is where you register ShapeTracker as a QML type that can be
accessed through the QML engine even though its a C++ QObject */
qmlRegisterType<ShapeTracker>("my_cpp_classes", 1, 0, "ShapeTracker");
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
then in
main.qml
import QtQuick 2.6
/* import the C++ class you registered into this QML document */
import my_cpp_classes 1.0
Window {
visible: true
Item {
/* set up a parent item that has a signal which is exposed to
the ShapeTracker object and the Rectangle you want to track */
id: myRootItem
signal rectangleChanged(var newWidth)
/* Heres our special Rectangle from QML that will send a signal when
its width changes */
Rectangle {
id: specialRectangle
width: 250
/* send the signal rectangleChanged(width) from our parent item
root item whenever width changes
*/
onWidthChanged: function() { rectangleChanged(width); }
}
/* Special Button that when clicked enlarges the Rectangle but
10px */
Button {
id: mySpecialButton
onClicked: { click_handler(mouse); }
function click_handler(mouse): {
if (specialRectangle.width < 500)
specialRectangle.width += 10;
}
}
/* Heres the actual ShapeTracker instance, which exists
as a QObject inside of the QML context, but has its methods
which are declared in C++, and exposed to the QML engine */
ShapeTracker {
id: myShapeTracker
/* similar to a constructor, but more like a callback */
Component.onCompleted: {
/* connect our signal from the parent root Item called
"rectangleChanged" to the ShapeTracker's Slot called
"myRectangleChangeHandler" */
myRootItem.rectangleChanged.connect(myShapeTracker.myRectangleChangeHandler);
/* connect send_mouse_click to the click_handler of
the button */
myShapeTracker.send_mouse_click.connect(mySpecialButton.click_handler)
}
}
}
}
in shapetracker.h you simply add a new slot with the name myRectangleChangeHandler and it will receive that signal whenever it is send via QML to be processed via C++
class ShapeTracker : public QObject {
Q_OBJECT
public:
ShapeTracker(QObject *parent = 0 );
signal:
void send_mouse_click(QMouseEvent *event);
public slots:
void myRectangleChangeHandler(QVariant newWidth) {
/* Perform a mouse click on our QML Object mySpecialButton
using QQuickItem::mousePressEvent and sending it via
signal back to QML */
QMouseEvent myEvent(QEvent::MouseButtonPress, QPointF(1,1), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
QMouseEvent* pressEvent = QQuickItem::mousePressEvent(&myEvent);
emit send_mouse_click(pressEvent);
}
};
In Summary, you expose a C++ QObject to QML, then you use
object.signal.connect(cppObject.desired_slot)
To connect them -- all the extra stuff was for a functional example in case anyone needs it later
In reality, you don't even need this functionality because anything happening in an onClick event could just as easily be put into any other property such on
Rectangle {
id: rect
signal customClick(var var1)
onCustomClick : { console.log(var1); }
}
Item {
rect.customClick(1);
}
The easy way would be to call all the receiving SLOTS manually. But that would be tedious and error prone.
You might try implementing a sub-class of QObject that has one slot onClicked() which emits the signal clicked() and use it as a shim between the button and elements controlled by the button. Connect the button clicked() to the new object onClicked() and then connect the new object to the original receivers. Then calling onClicked() would trigger the behavior.
This is a very simple example, and I haven't run it through the compiler.
ButtonShim.hpp
#include <QObject>
class ButtonShim : public QObject {
Q_OBJECT
public:
ButtonShim(QObject *parent = 0);
virtual ~ButtonShim();
public slots:
void onClicked();
signals:
void clicked();
};
ButtonShim.cpp
#include "ButtonShim.hpp"
ButtonShim::ButtonShim(QObject *parent) : QObject(parent) {
}
ButtonShim::~ButtonShim() {
}
void ButtonShim::onClicked() {
// All we do here is emit the clicked signal.
emit clicked();
}
SomeFile.cpp
#include <bb/cascades/Button>
#include "ButtonShim.hpp"
...
ButtonShim * pButtonShim = new ButtonShim(pButton); // pButtonShim will live as long as pButton
bool c = connect(pButton, SIGNAL(clicked()), pButtonShim, SLOT(onClicked()));
c = connect(pButtonShim, SIGNAL(clicked()), pSomeObject, SLOT(onButtonClicked()));
...
// to simulate a click of pButton
pButtonShim->onClicked();
SomeFile.qml
// assuming ButtonShim has been exposed to QML from your application
...
attachedObjects: [
ButtonShim {
id: buttonShim
onClicked: {
clickedLabel.text = "I've been clicked";
}
}
]
...
Label {
id: clickedLabel
text: "I haven't been clicked"
}
Button {
text: "Click Me"
onClicked: {
buttonShim.onClicked();
}
}
I think that you can look at code for tests. There they get object from QML file loaded into engine.
If you have an QObject you can just call signal because, AFAIR
public signals:
void clicked();
is expanded by moc into
public:
void clicked();

Communication between C++ and QML

This page shows how to call C++ functions from within QML.
What I want to do is change the image on a Button via a C++ function (trigger a state-change or however it is done).
How can I achieve this?
UPDATE
I tried the approach by Radon, but immediately when I insert this line:
QObject *test = dynamic_cast<QObject *>(viewer.rootObject());
Compiler complains like this:
error: cannot dynamic_cast '((QMLCppBinder*)this)->QMLCppBinder::viewer.QDeclarativeView::rootObject()' (of type 'struct QGraphicsObject*') to type 'class QObject*' (source is a pointer to incomplete type)
In case it is relevant, QMLCppBinder is a class that I try to build to encapsulate the connections from several QML pages to C++ code. Which seems to be trickier than one might expect.
Here is a skeleton class to give some context for this:
class QMLCppBinder : public QObject
{
Q_OBJECT
public:
QDeclarativeView viewer;
QMLCppBinder() {
viewer.setSource(QUrl("qml/Connect/main.qml"));
viewer.showFullScreen();
// ERROR
QObject *test = dynamic_cast<QObject *>(viewer.rootObject());
}
}
If you set an objectName for the image, you can access it from C++ quite easy:
main.qml
import QtQuick 1.0
Rectangle {
height: 100; width: 100
Image {
objectName: "theImage"
}
}
in C++:
// [...]
QDeclarativeView view(QUrl("main.qml"));
view.show();
// get root object
QObject *rootObject = dynamic_cast<QObject *>(view.rootObject());
// find element by name
QObject *image = rootObject->findChild<QObject *>(QString("theImage"));
if (image) { // element found
image->setProperty("source", QString("path/to/image"));
} else {
qDebug() << "'theImage' not found";
}
// [...]
→ QObject.findChild(), QObject.setProperty()
So, you could set your C++ object as a context property on the QDeclarativeView in C++, like so:
QDeclarativeView canvas;
ImageChanger i; // this is the class containing the function which should change the image
canvas.rootContext()->setContextProperty("imgChanger", &i);
In your ImageChanger class, declare a signal like:
void updateImage(QVariant imgSrc);
Then when you want to change the image, call emit updateImage(imgSrc);.
Now in your QML, listen for this signal as follows:
Image {
id: imgToUpdate;
}
Connections {
target: imgChanger;
onUpdateImage: {
imgToUpdate.source = imgSrc;
}
}
Hope this helps.