Sending a signal to a QML item from C++ (Qt5) - c++

I have a QML file containing this:
Text {
id: testData
onTaskClicked:{
testData.text = task.name
}
}
The catch is this taskClicked signal. It is emitted by another widget (C++) and needs to be relayed to QML.
This is similar to this SO question, except that the solution posted there doesn't work (why is written below).
The C++ code:
ctxt->setContextProperty(QLatin1Literal("holiday"), m_model);
ctxt->setContextProperty(QLatin1Literal("bgcolor"), color);
view->setResizeMode(QQuickView::SizeRootObjectToView);
auto mainPath = QStandardPaths::locate(QStandardPaths::DataLocation,
QLatin1Literal("taskview.qml"));
view->setSource(QUrl::fromLocalFile(mainPath));
ctxt->setContextProperty(QLatin1Literal("viewer"), m_view);
m_view is a QListView subclass that emits the taskClicked(HolidayTask* task) signal (from the .h file):
Q_SIGNALS:
void taskClicked(HolidayTask* task);
color and m_model are registered in QML and are used elsewhere. The object from the signal is already registered in QML. view is my QQuickView.
First I tried the solution presented in the question above:
auto root = view->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData");
connect(m_view, SIGNAL(taskClicked(HolidayTask* task), myElement,
SLOT(taskClicked(HolidayTask* task);
However, myElement is always null (and I get a runtime warning about a non existent slot).
If I try to set the view (the QListView) pointer as a context property of the QML view, it still doesn't work.
In all cases, I get also:
QML Connections: Cannot assign to non-existent property "onTaskClicked"
What could I possibly be doing wrong here?
EDIT to clarify some details: HolidayTask is a custom QObject subclass, and the signal taskClicked is defined in C++ (in a QListView subclass)
EDIT2: We're getting close, but no cigar:
auto root = quickView->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData"));
connect(m_view, SIGNAL(taskClicked(HolidayTask*)),
myElement, SIGNAL(taskClicked(HolidayTask* task)));
and
Text {
id: testData
objectName: "testData"
signal taskClicked(HolidayTask task)
onTaskClicked: {
testData.text = task.name
console.log("CLICk!")
}
}
yields
QObject::connect: No such signal QQuickText_QML_0::taskClicked(HolidayTask* task) in /home/lb/Coding/cpp/holiday-planner/src/mainwindow.cpp:178
QObject::connect: (receiver name: 'testData')
More details: HolidayTask, my custom QObject subclass, is registered in the code as
qmlRegisterType<HolidayTask>("HolidayPlanner", 1, 0, "HolidayTask");
Minimal QML with the data:
import QtQuick 2.0
import QtQml 2.2
import HolidayPlanner 1.0
Rectangle {
id: container
objectName: "container"
color: bgcolor
Text {
id: testData
objectName: "testData"
signal taskClicked(HolidayTask task)
onTaskClicked: {
testData.text = task.name
console.log("CLICK")
}
}
}
EDIT3: The final, working code is (see answers on why)
connect(m_view, SIGNAL(taskClicked(HolidayPlanner::HolidayTask*)),
myElement, SIGNAL(taskClicked(HolidayPlanner::HolidayTask*)));
This worked only through the use of objects with full namespaces. Otherwise the signature will not match in QML.

However, myElement is always null (and I get a runtime warning about
a non existent slot).
You are trying to find the child based on id, whereas it is based on the objectName property. You would need to set the objectName property to the desired to actually find it.
Also, you do not seem to declare the signal in your QML Text item. I am not sure if it is a custom C++ item or a built-in one. You have not shared enough code unfortunately to understand that bit. Either way, declare your signal as per documentation.
Therefore, try this code out:
Text {
id: testData
objectName: "testData"
// ^^^^^^^^^^^^^^^^^^^
signal taskClicked (HolidayTask task)
// ^^^^^^^^^^^^^^^^^^^
onTaskClicked:{
testData.text = task.name
}
}
Once that is done, you are almost ready. You need to have your HolidayTask registered to QML for sure, and you also need to change the connect syntax in your main.cpp as follows:
connect(m_view, SIGNAL(taskClicked(HolidayTask* task), myElement, SIGNAL(taskClicked(HolidayTask* task)));
In short, you need to trigger your QML signal handler this way, and not via SLOT.
Also, note that your connect syntax is broken as it is missing the closing brackets at the end. That needs to be fixed.
I would even consider removing the pointer assignment and use value or reference based passing for this.

You can connect a signal from C++ to QML by:
view->rootContext()->setContextProperty("testData",this);
QObject::connect(this,SIGNAL(taskClicked(HolidayTask* task)),(QObject *)view->rootObject(),SLOT(onTaskClicked(HolidayTask* task)));
When your signal is named taskClicked, The slot in QML should be onTaskClicked.
Also in QML you should name the object testData by:
objectName: "testData"

QML Connections: Cannot assign to non-existent property "onTaskClicked"
The error tells you that your Text item do not have signal taskClicked or property onTaskClicked!
You need to declare a slot inside your text item. To do that, you simply declare a function:
Text {
id: testData
objectName: "testData" // as Laszlo said
function onTaskClicked( task ) {
testData.text = task.name;
}
}
But that also won't work because you create a SLOT( onTaskClicked(QVariant) ) instead of SLOT(taskClicked(HolidayTask*)). In order to exchange data with QML you need to change your signal to SIGNAL(taskClicked(QVariant)):
Q_SIGNALS:
void taskClicked(QVariant task);
And emit it with:
emit taskClicked( QVariant::fromValue( task ) );
Remember that in order to be able to use HolidayTask it must be a QObject that is registered with qmlRegisterType.
You can also simply call that qml function.
If you are not able to use QVariant you can declare a signal inside you Text object:
Text {
id: testData
objectName: "testData" // as Laszlo said
signal taskClicked ( HolidayTask task )
onTaskClicked: {
testData.text = task.name;
}
}
And then connect from a C++ SIGNAL to qml SIGNAL:
auto root = view->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData");
connect(m_view, SIGNAL(taskClicked(HolidayTask*), myElement,
SIGNAL(taskClicked(HolidayTask*));

Related

How to pass a QML callback to c++ invokable function and execute it in the GUI thread?

I have a c++ Q_INVOKABLE function that starts an asynchronous operation (image downloading). I want to pass a QML callback to the function for when the operation finishes.
I know that it could be done with QJSValue, but this way the callback is not called in the GUI thread so when I try to update some QML element, it prints an error and crashes the application: Updates can only be scheduled from GUI thread or from QQuickItem::updatePaintNode(). Also, that question is 5 years old and maybe a better way has been introduced since then.
This is my QML code:
Rectangle {
// ...
Loader {
id: loader
source: "[loading animation]";
}
Component {
id: imageView
Image {
id: image
// ...
}
}
Component.onCompleted: {
ImageLoader.start_loading(post_id, function () {
loader.sourceComponent = imageView;
});
}
And the c++ code:
void ImageLoader::start_loading(const QString & id, QJSValue on_finished)
{
ImageLoaderRunnable * runnable_raw = new ImageLoaderRunnable(id, this->m_cache);
connect(runnable_raw, &ImageLoaderRunnable::finished, [on_finished]() mutable {
if (on_finished.isCallable()) {
on_finished.call();
}
});
runnable_pool.start(runnable_raw);
}
Also, this method is unsafe since the QML object can be destroyed before the callback is called.
You need to emit a signal from your C++ model, and handle the signal in the QML.
You really need to give more detail why that doesn't work for your use case.
If you have a ListView then you might need to define a role for whatever you need to pass to your view and emit dataChanged for the index of image that was loaded to update the view.
I figured it out. I just needed a way to call the slot in the GUI thread. When I pass the receiver object to the connect() method, it runs the slot in the receiver's thread. Thus, I can pass either the qml engine object or the qml component that calls the function to the connect() method and it will work.
A:
void ImageLoader::start_loading(const QString & id, QJSValue on_finished)
{
ImageLoaderRunnable * runnable_raw = new ImageLoaderRunnable(id, this->m_cache);
// m_engine -- pointer to QQmlApplicationEngine.
connect(runnable_raw, &ImageLoaderRunnable::finished, m_engine, [on_finished]() mutable {
if (on_finished.isCallable()) {
on_finished.call();
}
});
runnable_pool.start(runnable_raw);
}
B:
// c++
void ImageLoader::start_loading(const QString & id, QObject * receiver, QJSValue on_finished)
{
ImageLoaderRunnable * runnable_raw = new ImageLoaderRunnable(id, this->m_cache);
connect(runnable_raw, &ImageLoaderRunnable::finished, receiver, [on_finished]() mutable {
if (on_finished.isCallable()) {
on_finished.call();
}
});
runnable_pool.start(runnable_raw);
}
// qml
Component.onCompleted: {
ImageLoader.start_loading(post_id, this, function () {
loader.sourceComponent = imageView;
});
}

How to hook up to the onClick event of a QML item from C++ side

Scenario:
I have a Qt app which runs on Qt 5.9.4 commercial edition. Its a QtQuick and QML based application which is running on iOS and Android.
I have a QML item on the UI like this:
SomeItem {
text: qsTr("Some Item")
objectName: "someitem"
visible: false
onClicked: {
console.log("Some item was clicked")
}
}
I have a C++ function which can easily control the properties of SomeItem.
void MyCppClass::Func() {
QQuickItem *someItem = qml_engine->rootObjects()[0]->findChild<QQuickItem*>("someitem");
someItem->setVisible(true); // this works
// How to listen to someItem's onClick event here
}
Question:
I want to listen to the onClick event of someItem in a C++ method or a lambda without changing anything in the QML. Basically hook up to the onClick signal signal of someItem from C++ side itself. How can I do it?
The method to use to interact can be dangerous in the general case because the life cycle of the items depends on QML, so make sure it does not happen. Going to your request and I assume that MyCppClass inherits from QObject or a child class you must create a slot and use the old connection syntax:
*.h
class MyCppClass: public QObject
{
...
private slots:
void on_clicked();
...
};
*.cpp
void MyCppClass::Func() {
QQuickItem *someItem = qml_engine->rootObjects()[0]->findChild<QQuickItem*>("someitem");
if(!someItem)
return;
someItem->setVisible(true); // this works
connect(someItem, SIGNAL(clicked()), this, SLOT(on_clicked()));
}
void MyCppClass::on_clicked(){
qDebug()<<"on clicked";
}

QML not registering property change from C++ property

I'm displaying data in a GridView from a custom QAbstractListItem subclass (implementation here). Adding and removing items works fine, QML is notified of the changes and transitions work fine.
Now I'm trying to set a property of an Item inside the model and have QML react on it. The problem is, the onPropertyChanged inside QML is not called.
Here is the property in C++:
// item.h
Q_PROPERTY(bool pToBeDeleted READ toBeDeleted NOTIFY toBeDeletedChanged)
// item.cpp
void Item::requestDelete()
{
toBeDeleted_m = true;
qDebug() << "emitting";
emit toBeDeletedChanged();
}
This is what the GridView looks like:
// main.qml
GridView {
id: grid
// ...
model: empty
delegate: customComponent {
toBeDeleted: pToBeDeleted
}
ListModel {
id: empty
}
}
When the program starts, grid's model is set to my itemmodel.
And this is the QML type that does not see the changes:
// customComponentForm.ui.qml
Item {
property bool toBeDeleted: false
}
// customComponent.qml
CustomComponentForm {
onToBeDeletedChanged: {
console.debug("change")
}
}
Now when I call the method from inside the model like this:
this->items.at(i++)->requestDelete();
The output shows emitting but not change.
I have tried to include
emit dataChanged(createIndex(i, 0), createIndex(i, 0));
which did result in onToBeDeletedChanged to be called sometimes, but that also resulted in some wonky behaviour with the error
DelegateModel::item: index out range 3 3
Two things went wrong here. First, because of the ++ at
this->items.at(i++)->requestDelete();
the dataChanged emit had the wrong index which resulted in wrong items being updated. Second of all,
emit dataChanged(createIndex(i, 0), createIndex(i, 0));
was missing the third argument, and since in another attempt I had tried inline defining of a Vector the wrong way, I didn't find this to be the problem right away. The right call here would have been
QVector<int> v;
v.append(Qt::UserRole + 7 + 1);
// pToBeDeleted being the 7th property, always check this with
// roleNames()[Qt::UserRole + i + 1]. It should say your property.
emit dataChanged(createIndex(i, 0), createIndex(i, 0), v);
My mistake.
But on another note, since the rolename index seems to be platform dependent and signaling the change from the model is somewhat of a dirty approach, a better solution (as suggested by Kevin Krammer) would be to rewrite the itemmodel to only contain a single property, which is the QObject item. That way QML is notified of the changes item's properties have.

Cannot connect c++ signal to QML [duplicate]

I have a QML file containing this:
Text {
id: testData
onTaskClicked:{
testData.text = task.name
}
}
The catch is this taskClicked signal. It is emitted by another widget (C++) and needs to be relayed to QML.
This is similar to this SO question, except that the solution posted there doesn't work (why is written below).
The C++ code:
ctxt->setContextProperty(QLatin1Literal("holiday"), m_model);
ctxt->setContextProperty(QLatin1Literal("bgcolor"), color);
view->setResizeMode(QQuickView::SizeRootObjectToView);
auto mainPath = QStandardPaths::locate(QStandardPaths::DataLocation,
QLatin1Literal("taskview.qml"));
view->setSource(QUrl::fromLocalFile(mainPath));
ctxt->setContextProperty(QLatin1Literal("viewer"), m_view);
m_view is a QListView subclass that emits the taskClicked(HolidayTask* task) signal (from the .h file):
Q_SIGNALS:
void taskClicked(HolidayTask* task);
color and m_model are registered in QML and are used elsewhere. The object from the signal is already registered in QML. view is my QQuickView.
First I tried the solution presented in the question above:
auto root = view->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData");
connect(m_view, SIGNAL(taskClicked(HolidayTask* task), myElement,
SLOT(taskClicked(HolidayTask* task);
However, myElement is always null (and I get a runtime warning about a non existent slot).
If I try to set the view (the QListView) pointer as a context property of the QML view, it still doesn't work.
In all cases, I get also:
QML Connections: Cannot assign to non-existent property "onTaskClicked"
What could I possibly be doing wrong here?
EDIT to clarify some details: HolidayTask is a custom QObject subclass, and the signal taskClicked is defined in C++ (in a QListView subclass)
EDIT2: We're getting close, but no cigar:
auto root = quickView->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData"));
connect(m_view, SIGNAL(taskClicked(HolidayTask*)),
myElement, SIGNAL(taskClicked(HolidayTask* task)));
and
Text {
id: testData
objectName: "testData"
signal taskClicked(HolidayTask task)
onTaskClicked: {
testData.text = task.name
console.log("CLICk!")
}
}
yields
QObject::connect: No such signal QQuickText_QML_0::taskClicked(HolidayTask* task) in /home/lb/Coding/cpp/holiday-planner/src/mainwindow.cpp:178
QObject::connect: (receiver name: 'testData')
More details: HolidayTask, my custom QObject subclass, is registered in the code as
qmlRegisterType<HolidayTask>("HolidayPlanner", 1, 0, "HolidayTask");
Minimal QML with the data:
import QtQuick 2.0
import QtQml 2.2
import HolidayPlanner 1.0
Rectangle {
id: container
objectName: "container"
color: bgcolor
Text {
id: testData
objectName: "testData"
signal taskClicked(HolidayTask task)
onTaskClicked: {
testData.text = task.name
console.log("CLICK")
}
}
}
EDIT3: The final, working code is (see answers on why)
connect(m_view, SIGNAL(taskClicked(HolidayPlanner::HolidayTask*)),
myElement, SIGNAL(taskClicked(HolidayPlanner::HolidayTask*)));
This worked only through the use of objects with full namespaces. Otherwise the signature will not match in QML.
However, myElement is always null (and I get a runtime warning about
a non existent slot).
You are trying to find the child based on id, whereas it is based on the objectName property. You would need to set the objectName property to the desired to actually find it.
Also, you do not seem to declare the signal in your QML Text item. I am not sure if it is a custom C++ item or a built-in one. You have not shared enough code unfortunately to understand that bit. Either way, declare your signal as per documentation.
Therefore, try this code out:
Text {
id: testData
objectName: "testData"
// ^^^^^^^^^^^^^^^^^^^
signal taskClicked (HolidayTask task)
// ^^^^^^^^^^^^^^^^^^^
onTaskClicked:{
testData.text = task.name
}
}
Once that is done, you are almost ready. You need to have your HolidayTask registered to QML for sure, and you also need to change the connect syntax in your main.cpp as follows:
connect(m_view, SIGNAL(taskClicked(HolidayTask* task), myElement, SIGNAL(taskClicked(HolidayTask* task)));
In short, you need to trigger your QML signal handler this way, and not via SLOT.
Also, note that your connect syntax is broken as it is missing the closing brackets at the end. That needs to be fixed.
I would even consider removing the pointer assignment and use value or reference based passing for this.
You can connect a signal from C++ to QML by:
view->rootContext()->setContextProperty("testData",this);
QObject::connect(this,SIGNAL(taskClicked(HolidayTask* task)),(QObject *)view->rootObject(),SLOT(onTaskClicked(HolidayTask* task)));
When your signal is named taskClicked, The slot in QML should be onTaskClicked.
Also in QML you should name the object testData by:
objectName: "testData"
QML Connections: Cannot assign to non-existent property "onTaskClicked"
The error tells you that your Text item do not have signal taskClicked or property onTaskClicked!
You need to declare a slot inside your text item. To do that, you simply declare a function:
Text {
id: testData
objectName: "testData" // as Laszlo said
function onTaskClicked( task ) {
testData.text = task.name;
}
}
But that also won't work because you create a SLOT( onTaskClicked(QVariant) ) instead of SLOT(taskClicked(HolidayTask*)). In order to exchange data with QML you need to change your signal to SIGNAL(taskClicked(QVariant)):
Q_SIGNALS:
void taskClicked(QVariant task);
And emit it with:
emit taskClicked( QVariant::fromValue( task ) );
Remember that in order to be able to use HolidayTask it must be a QObject that is registered with qmlRegisterType.
You can also simply call that qml function.
If you are not able to use QVariant you can declare a signal inside you Text object:
Text {
id: testData
objectName: "testData" // as Laszlo said
signal taskClicked ( HolidayTask task )
onTaskClicked: {
testData.text = task.name;
}
}
And then connect from a C++ SIGNAL to qml SIGNAL:
auto root = view->rootObject();
auto myElement = root->findChild<QObject*>(QLatin1Literal("testData");
connect(m_view, SIGNAL(taskClicked(HolidayTask*), myElement,
SIGNAL(taskClicked(HolidayTask*));

cascades and signals / slots

I'm running around in circles about this. Just can't wrap my head around signals and slots.
Just looking for some mechanism that can automatically update my UI when a signal in my C++ occurs.
Example:
I have two labels in Qml that have text: _app.method that returns a value.
I have a button that onClicked runs a Q_INVOKABLE method. That method emits a signal when it's done, eg, fetches geocordinates and updates the values that the above text: assignments rely on.
What I want is SOMETHING to update the text: assignments once those values change.
I just need these signals / slots explained plainly. The only examples in documentation seem to assume ONLY QML or C++ but not a mix of both. The sample code have examples, but not explained specifically in documentation.
If you had plain description, im sure I could adapt to it. Eg, 1: define this in QML, 2: define this in hpp file, 3: define these in cpp file.
I've tried using QObject's setPropery("text","value") but my app crashes when attempting this.
Tell me if i'm wrong...
1) in QML:
Button {
id: aButton
text: _app.value
onClicked: {
_app.valueChanged.connect(aButton.onValueChanged);
_app.value = _app.value + 1;
}
function onValueChanged (val) {
aButton.text = "New value: " + val;
}
}
2) in HPP:
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
public:
int value();
void setValue(int i);
signals:
void valueChanged(int);
private:
int m_iValue;
3) in CPP:
int class::value()
{
return m_iValue;
}
void class::setValue(int i)
{
// name is same as HPP WRITE Q_PROPERTY statement
m_iValue = i;
emit valueChanged(m_iValue);
}
So, what happens is that, in QML, the onClick method CONNECTS the signal with a QML Function; which means, now we're listening for a value change, and when it does, that function will be called. THEN, we change the value... since the Q_PROPERTY set the write value to a function called setValue, setValue is called with the new value; internally, m_iValue is changed, and an emit occurs, which tells whoever is listening to valueChanged that there's a new value.
Hey, my QML is listening to that! (via the _app.valueChanged.connect script). So, the QML object (the Button) that was listening to that, has it's onValueChanged function called, with the new value (because of the emit valueChanged(m_iValue).
Please tell me i've figured this out??!?!
If you are using Q_PROPERTY macro, there's no need to bind onValueChanged signal with a function explicitly to change button's text. And also you need not emit valueChanged signal with m_iValue. Make below mentioned changes in corresponding files
QML:
Button {
horizontalAlignment: HorizontalAlignment.Center
verticalAlignment: VerticalAlignment.Center
id: aButton
text: _app.value
onClicked: {
_app.value = _app.value + 1
}
}
HPP:
signals:
void valueChanged();
CPP:
emit valueChanged();