QML not registering property change from C++ property - c++

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.

Related

How to know when the VerticalScrollBar is showing?

I need to know when the verticalScrollBar of my QTableWidget is being shown.
I am currently using the following instruction:
Header:
#ifndef MYCLASS_H
#define MYCLASS_H
#include <QDebug>
#include <QWidget>
#include <QScrollBar>
namespace Ui {
class MyClass;
}
class MyClass: public QWidget
{
Q_OBJECT
public:
explicit MyClass(QWidget *parent = 0);
~MyClass();
private:
void populateTable(QVector<QString> content);
private:
Ui::MyClass *ui;
};
#endif // MYCLASS_H
Populate table function:
void MyClass::populateTable(QVector<QString> content)
{
while( ui->myTableWidget->rowCount() > 0 )
{
ui->myTableWidget->removeRow(0);
}
QTableWidgetItem* item;
for (int row = 0; row < content.length(); ++row)
{
ui->myTableWidget->insertRow(row);
item = new QTableWidgetItem( QString::number(row) );
item->setTextAlignment(Qt::AlignCenter);
ui->myTableWidget->setItem(row, 0, item);
item = new QTableWidgetItem( content.at(row) );
item->setTextAlignment(Qt::AlignCenter);
ui->myTableWidget->setItem(row, 1, item);
}
qDebug() << "This : " << this->isVisible();
qDebug() << "QTableWidget : " << ui->myTableWidget->isVisible();
qDebug() << "VerticalScrollBar : " << ui->myTableWidget->verticalScrollBar()->isVisible(); // <-HERE
}
Output:
// Called from the constructor
This : false
QTableWidget : false
VerticalScrollBar : false
// Called by a button pressed
This : true
QTableWidget : true
VerticalScrollBar : true
This : true
QTableWidget : true
VerticalScrollBar : false
But it returns a wrong value. When the ScrollBar is visible it returns false and when it is not visible it returns true. Note: myTableWidget (QTableWidget) is always visible.
Is there any other way that I can do this?
I'm using Qt version 5.3.2
In general the code you are using is supposed to work - checked on Qt 5.3.0.
However, you must be sure that when you are making the call the QTableWidget itself is visible.
For example if you make the call inside the MainWindow constructor you will certainly get a false answer. Only after the form is shown the call to isVisible() on particular scrollbar would return the correct value.
EDIT:
With your code pasted I was able to reproduce the issue. I needed to go through the Qt code a bit to see whats going on. Basically it turns out that for QTableView which is parent class of QTableWidget scroll bar values are updated via updateGeometries (do not confuse it with the regular updateGeometry the one I'm mentioning is protected). Internally this method is called either directly or the event is processed through the event loop. In short, it depends on whether you add columns or rows.
In your example, if you insertColumn instead of insertRow (and switch the arguments in setItem) after checking the visibility of horizontalScrollBar you will get the proper result right away.
I could confirm this by subclassing the QTableWidget and overriding event method. It shows that when adding columns following events are executed: MetaCall (invoke call) and LayoutRequest. On the other hand, when adding rows first event passed is Timer.
I'm not Qt implementer so I'm not sure what is the purpose the difference. However, this info helps solving your problem in a more elegant way.
You can implement MyTableWidget which overrides the event method.
class MyTableWidget: public QTableWidget
{
Q_OBJECT
public:
bool event(QEvent *e) override
{
const bool result = QTableWidget::event(e);
if (e->type() == QEvent::LayoutRequest)
{
// call what you need here
// or emit layoutUpdated(); and connect some slots which perform
// processing dependent on scrollbar visibility
}
return result;
}
signals:
void layoutUpdated();
}
However, such event might get called in other situations not only when the view needs to be updated due to model data updates.
Another solution would be to avoid overriding the event method but creating your own method to trigger the required updates. For example:
void MyTableWidget::updateLayout()
{
QEvent ev{QEvent::LayoutRequest};
QTableWidget::updateGeometries();
QTableWidget::event(&ev);
}
This would call directly updateGeometries which recalculates scrollbar min/max values and perform a direct event method call for LayoutRequest (without processing through eventloop). Which if I'm correct indirectly updates scrollbar visibility.
Calling this method before checking the visibility should also fix your problem.
ui->myTableWidget->updateLayout();
qDebug() << "VerticalScrollBar : " << ui->myTableWidget->verticalScrollBar()->isVisible();
// prints "VerticalScrollBar : true false"
I wanted to get notified via a signal, when a QScrollBar gets visible. I'm writing this answer here, since this is the first google result I got and no result had a proper answer.
First of all, there is no signal for changed visibility, so we will use valueChanged.
Now you can add a function that checks if the scrollbar isVisible() or not.
Problem: It won't work (and someone with more knowledge in Qt could probably explain why, I sadly cannot).
The trick is, to use a QTimer (Python code):
self.horizontalScrollBar().valueChanged.connect(lambda x: QTimer.singleShot(0, some_func))

QToolbar force expand on too many QActions

Hi all is there any way to automatically expand a QToolbar if there is too many QActions in?
Using Qt version 5.4.1 C++11
Ive tried :ui->mainToolBar->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred)
But this only expands it horizontally. I need it to expand vertically like the Expand button does.
Always expanding a toolbar vertically is not possible as far as I know (never seen it). A solution would be to add multiple toolbars. This way, you can arrange them one under the other.
What you can try is to add a custom widget to the toolbar that grows horizontally. This was proposed here by using a QScrollArea... Not sure whether this is exactly what you want, but it may work good enough.
This is how you can make a function to expand/retract a QToolbar. Firstly using a Forloop get all the child widgets from the QToolbar. You can use a Bool to lock to only get the first Widget which is the Expanding button/Action.
bool get_first_action{true};
for(QWidget* widget : ui->myToolBar->findChildren<QWidget*>())
{
if(get_first_action)
{
get_first_action = false;
// This is the expanding action!
m_action_expand = widget;
}
}
Or you can do this which is probably a bit safer.
for(QWidget* widget : ui->myToolBar->findChildren<QWidget*>())
{
if(widget->objectName() == "qt_toolbar_ext_button")
{
// This is the expanding action!
m_action_expand = widget;
}
}
Once you have the sneaky expanding action assign it to a member varible
// Make sure to initialize this in the constructor!
// m_action_expand = new QWidget(this // parent)
QWidget* m_action_expand;
Now create a handy function with a good name;
void MainWindow::forceToolbarExpand()
{
// Grab the position of the expanding action/widget
QPointF pos(m_action_expand->pos());
// Create a fake/dummy event that replicates the mouse button press
QMouseEvent event_press(QEvent::MouseButtonPress, pos, Qt::LeftButton,0, 0);
// Create a fake/dummy event that replicates the mouse button release
QMouseEvent event_release(QEvent::MouseButtonRelease, pos, Qt::LeftButton,0, 0);
// These two events together will fire the QAction::Toggled signal.
// Make sure to send the events!
QApplication::sendEvent(m_action_expand, &event_press);
QApplication::sendEvent(m_action_expand, &event_release);
}
And there we have it your QToolbar, if it can be expanded/retracted now will when you call this function. I'm not too sure if you can directly Moc/fake the toggled event but you can try it. I know this method works so yeah.

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*));

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

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();