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.
Related
How do you pass a pointer to string property from QML to C++?
When I attempted to do it the obvious way, I got this error:
qrc:/NewAccount.qml:236: Error: Unknown method parameter type: QString*
Which means, QML engine can't convert new_account.error_string property to C++ when calling save_account (Q_INVOKABLE) method
This is my code in QML:
import myproject.aewm.ethkey 1.0
import myproject.aewm.walletaccount 1.0
...
id: new_account
property EthKey key: EthKey{}
property WalletAccount account: WalletAccount{}
property string error_string: ""
....
if (aewm_obj.save_account(key,account,new_account.error_string)) {
wallet_accounts_tabbar.currentIndex=0
} else {
console.log("error occurred:"+new_account.error_string)
}
Where aewm_obj is a C++ type (registered in QML) and save_account is declared in C++ as :
Q_INVOKABLE bool save_account(EthKey *key, WalletAccount *account, QString *err_str);
The docs say that a (QML) string property is a QString object, and that these types are automatically converted from from C++ and QML . I am passing pointers to my custom QObject-derived classes without any problems after qmlRegisterType() call, so why I can't do the same with strings?
I thought that maybe string pointers are not supported and I tried to add this line:
qRegisterMetaType<QString*>("QString*");
but after this change, the pointer I received at C++ side was 0x0 and I got a segfault.
So, how do you pass pointers to QML string properties from QML to C++ ?
Or, do you think I should register QString class with qmlRegisterType() too? I tried it but I have encountered some compilation issues, so I think it won't compile.
The last solution would be to create a custom object with a QString inside, and send a pointer to it from QML to C++. But, this would be an overkill, if QString exists why not find a way to use it?
Will appreciate very much your comments. I definitely want to use pointers , it is safer than dealing with object ownership when exchanging data between C++ and QML.
As I said in the comments, QML only passes pointers from the QObjects, and QString is not a QObject.
I think you are giving an incorrect approach to the problem, you could create a property in the object that performs the calculations that have the error message as shown below.
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
class Foo: public QObject{
Q_OBJECT
Q_PROPERTY(QString errorMessage READ errorMessage)
public:
using QObject::QObject;
Q_INVOKABLE bool process(int a, int b, int res){
bool status;
// some operation
status = (a+b) == res;
mErrorMessage = status? "": QString("error message: %1 + %2 is different to %3").arg(a).arg(b).arg(res);
return status;
}
QString errorMessage() const{
return mErrorMessage;
}
private:
QString mErrorMessage;
};
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
qmlRegisterType<Foo>("com.eyllanesc.org", 1, 0, "Foo");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
#include "main.moc"
main.qml
import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import com.eyllanesc.org 1.0
Window {
visible: true
width: 640
height: 480
Foo{ id: foo }
Row{
SpinBox{ id: a }
SpinBox{ id: b }
SpinBox{ id: c }
Button{
text: "process"
onClicked: {
var message;
if(foo.process(a.value, b.value, c.value)){
message = "correct"
console.log("successful")
}
else{
message = foo.errorMessage
console.log("error is "+ message)
}
txt.text = message
}
}
Label{ id: txt }
}
}
With Qt, the C++ API and the QML API are completely different, and practically incompatible layers. There is a lot of conversion of data back and forth in order to make the whole thing work. And you don't really have control over it when it comes to primitives like strings. So just get that idea out of your head.
If you want to access a particular property of a particular object, you need to pass the object that will be received as a pointer, and the property name, which then you can access via its name string via QObjects property() and setProperty().
But in your case that is entirely redundant, simply pass the string itself.
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));
I want to use my C++ type registered in QML as parameter in QML slot. So I already have C++ signal which has QString type parameter:
emit newMessage(myString);
and QML slot:
onNewMessage: {
console.log(myString);
}
Now I want to pass C++ type registered in QML as parameter of this slot.
I found similar question, but I don't understand how I can use this vice versa. Any help will be useful.
So Lets say you wanted to pass a QObject derived file back and forth from C++ to QML via a signal/slot setup.
class MyClass : public QObject
{
Q_OBJECT
public:
explicit MyClass(QObject *parent = 0);
signals:
void sendSignal(MyClass* anotherMyClass)
public slots:
void send_signals() { emit sendSignal(this); }
};
If you have a C++ type called MyClass which is a QObject-based type, and you successfully did
qmlRegisterType<MyClass>("com.myapp", 1, 0, "MyClass");
Then In QML all you have to do is this:
import QtQuick 2.0
import com.myapp
Item {
MyClass {
id: myInstance
Component.OnCompleted: {
// connect signal to javascript function
myInstance.sendSignal.connect( myInstance.receiveSignal);
}
function receiveSignal(mySentObject) {
// here is your QObject sent via Signal
var receivedObject = mySentObject;
// Now to send it back
// (will cause an endless loop)
myInstance.sendSignal(receivedObject);
// or you could do this to perform
// the same endless loop
receivedObject.send_signals();
// or you could do this to perform
// the same endless loop
receivedObject.sendSignal(myInstance)
}
}
}
Summary:
You can just send any QObject-derived object as long as its registered with the QML engine as the actual object, you just treat it like any other type in QML
Hope this helps
I've fully edit my question since I've made some progress, and the first one was unclear.
I use Qt 4.8, with QtQuick 1.0.
I have a page where I need to keep the top and the bottom margin. So I've defined a Main.qml like that :
Item {
id: salesWindow
width: 800
height: 600
[...] //Properties def
TopBar {[...]}
CloseButton{[...]}
Rectangle {[...]}
//I want to load qml file in this loader. The QML file loaded use some of the Main.qml properties
Loader {
id: appPlaceHolder
objectName: "loader"
anchors.centerIn: parent
}
Rectangle {[...]}
BotBar {[...]}
}
If I put a qml file into the loader sourceComponent, it works.
Now I want to do it with C++, and well designed. I've subclass QDeclarativeComponent in SalesAppDisplay.h
class SalesAppDisplay : public IDisplayScreen
{
Q_OBJECT
static const std::string QML_FILENAME;
static const std::string QML_DIR_PATH;
public:
SalesAppDisplay(DisplayContext& context, QDeclarativeEngine& engine, QObject* parent = 0);
~SalesAppDisplay();
void doScreenInit();
const std::string getQmlFilename() const;
};
class IDisplayScreen : public QDeclarativeComponent
{
Q_OBJECT
[...]
}
and the Ctor in charge of component instanciation :
IDisplayScreen::IDisplayScreen(DisplayContext& context, QDeclarativeEngine& engine, std::string qmlFilepath, QObject* parent)
: QDeclarativeComponent(&engine, QString(qmlFilepath.c_str()), parent)
Instead of loading a qml file in the loader by changing the source, I want to insert my component into the QML from main.cpp :
m_view.setSource(QUrl::fromLocalFile("../displaymanager/rsrc/qml/Main.qml"));
QObject* mainObj = m_view.rootObject();
[ .. Set file property ]
//this is the component subclass instantiation (made by factory)
m_currentScreen = displaymanager::createDisplayScreen(IDisplayScreen::SALESAPP, *(m_context), *(m_view.engine()), mainObj);
QDeclarativeItem* saleAppObj = qobject_cast<QDeclarativeItem*>(m_currentScreen->create(m_view->rootContext()));
saleAppObj->setParentItem(qobject_cast<QDeclarativeItem*>(mainObj));
[ .. Set file property ]
//I can find my loader without any problems
QDeclarativeItem *loader = mainObj->findChild<QDeclarativeItem*>("loader");
/* I don't know what to do here for making it works */
m_view.show();
m_qApp.exec();
I've tried loader->setProperty("sourceComponent", qobject_cast<QVariant>(saleAppObj));, and some other tricks like that without any results.
I have errors from my saleApp.qml saying that he don't know Main.qml properties that i used in it (he is clearly load at the Components instanciation). And despite that main.qml is perfectly loaded, nothing from SaleApp.qml appears.
I have made it work.
There is the screen handling function :
void QtDisplayManager::switchScreen(int screenID)
{
if(m_currentScreen)
{
m_currentScreen->destroyItem();
}
//App screen creation
m_currentScreen = displaymanager::createDisplayScreen(screenID, m_context, *m_engine, m_mainObj);
//Get App placehoder
QDeclarativeItem* loaderItem = m_mainObj->findChild<QDeclarativeItem*>("loader");
//Put app in placeholder
if(loaderItem)
{
m_currentScreen->getItem()->setParentItem(loaderItem);
m_engine->clearComponentCache();
m_context.setcurrentDisplayID(screenID);
}
}
destroyItem() is a function I've add to delete item pointer without deleting the whole component, just because I had a problem where the component was not removed from the view when a new was added.
SalesApp.qml have no reference on the MainWindow.qml, so I have add two wrapper :
m_view.rootContext()->setContextProperty("managerWrapper", this);
m_view.rootContext()->setContextProperty("appWrapper", m_currentScreen);
Works perfectly, design is good, code is sweet.
I try to write some application using FileDialog and I have to get file urls in my C++ class. I try to do this in this way:
FileDialog {
id: fileDialog
objectName: "fileDialog"
selectMultiple: true
signal getFiles(var urls)
title: qsTr("Open file")
nameFilters: [qsTr("MP3 files (*.mp3)"), qsTr("All files (*.*)")]
onAccepted: getFiles(fileDialog.fileUrls)
}
........................................
class MyClass : public QObject
{
Q_OBJECT
public slots:
void addToPlaylist (const QList<QUrl> & urls){
for(int i = 0; i < urls.length(); ++i)
qDebug() << "Get\n";
}
};
............................................
QObject *fileDialog = root->findChild<QObject *>("column")->findChild<QObject *>("row")->findChild<QObject *>("openButton")->findChild<QObject*>("fileDialog");
MyClass myClass;
QObject::connect(fileDialog, SIGNAL(getFiles(QVariant)), &myClass, SLOT(addToPlaylist(QList<QUrl>)));
I found solution but I do not understand. Can someone explain me in my example?
just a question (and maybe solution), why do you need to define a
signal in QML for this? can’t you simply call the c++ slot and pass
the URL list directly?
Well,you are trying to pass a list of urls from qml to c++. These are the ways.
1)Make sure that you make an object of MyClass available to QML by calling QQmlContext::setContextProperty() and registering that object.
Example:
QQuickView view;
MyClass myObj;
view.rootContext()->setContextProperty("myObj", &myObj);
Then you can call your MyClass::addToPlaylist() from QML like this.
FileDialog{onAccepted: myObj.addToPlaylist(fileDialog.fileUrls);}
Reference: Embedding C++ Objects into QML with Context Properties
2)Make the FileDialog instance emit a signal whenever user selects some files from QML. Now this signal needs to be connected to the slot of an object of MyClass. In order to do this, you need to have pointers to both FileDialog instance and object of MyClass object.
You are getting pointer to an instance of FileDialog by doing this.
QObject *fileDialog = root->findChild<QObject *>("column")->findChild<QObject *>("row")->findChild<QObject *>("openButton")->findChild<QObject*>("fileDialog");
Now you are connecting the signal and slot. Thats it.