QAbstractListModel not updating row count in QML when rows are added outside of my MainWindow's constructor.
-
wrote on 19 Oct 2021, 00:58 last edited by cray12399
Hello all,
To start off, while I have a bit of experience with QtWidgets, I am a newb when it comes to QML and Qt Quick. I am developing an app using PySide2 + QML and have been dealing with an issue with the QAbstractListModel for 2 days so far. When I append rows to my model anywhere outside of the constructor of my MainWindow class (which is a QObject), while the rows are added, the rowCount() remains zero on the QML side of my code. If I append rows within the constructor, the model works perfectly as expected. Here is the code for my model:
class PhoneDataModel(QAbstractListModel): NameRole = Qt.UserRole + 1000 AddressRole = Qt.UserRole + 1001 BtSocketConnectedRole = Qt.UserRole + 1002 def __init__(self, phone_data=None, parent=None): super(PhoneDataModel, self).__init__(parent) if phone_data is None: self.__phone_data = [] else: self.__phone_data = phone_data def rowCount(self, parent=QModelIndex()): return len(self.__phone_data) def data(self, index, role=Qt.DisplayRole): if 0 <= index.row() < self.rowCount() and index.isValid(): if role == PhoneDataModel.NameRole: return self.__phone_data[index.row()]["name"] elif role == PhoneDataModel.AddressRole: return self.__phone_data[index.row()]["address"] elif role == PhoneDataModel.BtSocketConnectedRole: return self.__phone_data[index.row()]["btSocketConnected"] @Slot(int, result='QVariant') def get(self, row): if 0 <= row < self.rowCount(): return self.__phone_data[row] def roleNames(self): roles = super().roleNames() roles[PhoneDataModel.NameRole] = b"name" roles[PhoneDataModel.AddressRole] = b"address" roles[PhoneDataModel.BtSocketConnectedRole] = b"btSocketConnected" return roles def appendRow(self, name, address, bt_socket_connected): self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.__phone_data.append({'name': name, 'address': address, 'btSocketConnected': bt_socket_connected}) print(self.__phone_data) self.endInsertRows() def removeRow(self, row, parent=None, *args, **kwargs): self.beginRemoveRows(QModelIndex(), row, row) self.__phone_data.pop(row) self.endRemoveRows() def setData(self, index, value, role=Qt.EditRole): if not index.isValid(): return False if role == Qt.EditRole: item = index.internalPointer() item.set_name(value) return True
And here all of the code from my MainWindow that interacts with the model:
class MainWindow(QObject): def __init__(self, backend_socket): super().__init__(None) self.__phone_connection_data = {} self.__phone_data_model = PhoneDataModel([]) self.__phone_data_model.appendRow('phone 1', '1001', True) # This works perfectly. self.__backend_socket = backend_socket self.__thread_pool = QThreadPool(self) self.__initialize_gui() self.__initialize_looper_thread(backend_socket) self.log = lambda level, message, exception=False: \ log(self.__backend_socket, level, message, exception) self.log('i', "GUI started successfully!") ... def __initialize_looper_thread(self, backend_socket): self.__looper_thread = LooperThread(backend_socket, self) self.__looper_thread.set_phone_data_signal.connect( lambda phone_connection_data: self.set_phone_data(phone_connection_data)) # Signal that connects to method that adds row to model self.__looper_thread.start() @Property(QObject) def phoneDataModel(self): return self.__phone_data_model ... def set_phone_data(self, phone_connection_data): self.__phone_connection_data = phone_connection_data for phone_address in phone_connection_data.keys(): name = phone_connection_data[phone_address]['name'] bt_socket_connected = phone_connection_data[phone_address]['socket_connected'] # self.__phone_data_model_provider.add_phone(name, phone_address, bt_socket_connected) # Original code, commented out for testing purposes. self.__phone_data_model.appendRow('phone 3', '1001', True) # This doesnt work.
The main window receives a signal from my looper thread with the data of a new phone, which triggers the set_phone_data() method. The signal is working perfectly fine and the method gets called every time my looper thread emits the signal.
Since my QML isn't formatting right, here are the pastebins:
MainWindow:
https://pastebin.com/gPNRk2wcPhoneSelector (Essentially my own custom ComboBox because the stock combobox wasn't doin' it for me):
https://pastebin.com/AAqtq4HjHere are things I have tried so far:
-
Putting the count into a separate property using the Property class from QtCore.
-
Providing the model via a separate QObject
-
Making the main window the parent of the model
-
Modifying the model using a QThread (in case it was a problem with the MainWindow QObject)
Expected behavior:
- Once a new phone is added, rowCount() is greater than one and the label goes from "No phones connected..." to the name of the current phone in the model. The MouseArea becomes enabled so the user can interact with the PhoneSelecter
Actual behavior:
- PhoneSelector behaves as if the rowCount() is 0. Mouse area is disabled and the label says "No Phones Connected..." If I make the MouseArea always enabled, the ListView shows the phones in my model, but the count is still zero. UNLESS, I add the phones within my main window's constructor. If I add a phone in the constructor and add a phone in my set_phone_data method, both phones are shown in my list, but the count on the QML side is just 1 rather than 2.
-
-
@eyllanesc Alright, I implemented the code. Here is the output I got:
file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: Failed to initialize QSettings instance. Status code is: 1 file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: The following application identifiers have not been set: QVector("organizationName", "organizationDomain") Icon theme "elementary" not found. Icon theme "gnome" not found. <_MainThread(MainThread, started 139841449985856)> The row count is 1 This print statement is in set_phone_data right before the line appending a row to the model. This statement is in set_phone_data right after the line appending a row to the model: The row count is 2
I kept the same code as above where a phone was added in the constructor, and the the set_phone_data statement. I dont know if I mentioned it in the original post, but I actually tried this same test and the row count increments on the Python side fine. However, on the QML side, it doesn't increment when a phone is added via the set_phone_data method. The way I know this is in my PhoneSelector QML, I set the QLabel to show the rowCount() of the model with the following code:
Label { id: currentItemText anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.right: downArrow.left anchors.rightMargin: 15 anchors.leftMargin: 15 color: "black" // text: count > 0 ? phoneSelector.currentText : "No Phones Connected..." text: "Row count is " + phoneSelector.model.rowCount() elide: Text.ElideRight }
Using the code from my example, where two phones are added, while the rowCount() returns 2 in the python code, it is 1 in the actual GUI, despite two phones being listed in my list model. Here is a picture:
This picture was taken after the set_phone_data method was run. Using the same QML code to output the row count, if I don't add the phone in the constructor, it shows 0 instead of 1, but it will still show the phone in the list if I enable the mouse area so that the listview can be expanded. So it seems that the row count is not updating when a phone is added outside the constructor.
wrote on 19 Oct 2021, 04:25 last edited by eyllanesc@cray12399 I can't tell you where the error is since I can't analyze it completely since they are just pieces of code but I can point out some observations:
- rowCount is not a property but a method, so they are not used to make a binding, that means that if the number of rows is changed then the property will not be reevaluated. There are 2 options: Use the rowsInserted, rowsRemoved, etc tokens to update the qml property, or create a qproperty called count associated with a signal so that it can be used in bindings.
first option:
Connections{ target: <FOO>.phoneDataModel function onRowsInserted(){ currentItemText.text = "Row count is " + <FOO>.phoneDataModel.rowCount() } // function onRowsRemoved(){} }
-
Hello all,
To start off, while I have a bit of experience with QtWidgets, I am a newb when it comes to QML and Qt Quick. I am developing an app using PySide2 + QML and have been dealing with an issue with the QAbstractListModel for 2 days so far. When I append rows to my model anywhere outside of the constructor of my MainWindow class (which is a QObject), while the rows are added, the rowCount() remains zero on the QML side of my code. If I append rows within the constructor, the model works perfectly as expected. Here is the code for my model:
class PhoneDataModel(QAbstractListModel): NameRole = Qt.UserRole + 1000 AddressRole = Qt.UserRole + 1001 BtSocketConnectedRole = Qt.UserRole + 1002 def __init__(self, phone_data=None, parent=None): super(PhoneDataModel, self).__init__(parent) if phone_data is None: self.__phone_data = [] else: self.__phone_data = phone_data def rowCount(self, parent=QModelIndex()): return len(self.__phone_data) def data(self, index, role=Qt.DisplayRole): if 0 <= index.row() < self.rowCount() and index.isValid(): if role == PhoneDataModel.NameRole: return self.__phone_data[index.row()]["name"] elif role == PhoneDataModel.AddressRole: return self.__phone_data[index.row()]["address"] elif role == PhoneDataModel.BtSocketConnectedRole: return self.__phone_data[index.row()]["btSocketConnected"] @Slot(int, result='QVariant') def get(self, row): if 0 <= row < self.rowCount(): return self.__phone_data[row] def roleNames(self): roles = super().roleNames() roles[PhoneDataModel.NameRole] = b"name" roles[PhoneDataModel.AddressRole] = b"address" roles[PhoneDataModel.BtSocketConnectedRole] = b"btSocketConnected" return roles def appendRow(self, name, address, bt_socket_connected): self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.__phone_data.append({'name': name, 'address': address, 'btSocketConnected': bt_socket_connected}) print(self.__phone_data) self.endInsertRows() def removeRow(self, row, parent=None, *args, **kwargs): self.beginRemoveRows(QModelIndex(), row, row) self.__phone_data.pop(row) self.endRemoveRows() def setData(self, index, value, role=Qt.EditRole): if not index.isValid(): return False if role == Qt.EditRole: item = index.internalPointer() item.set_name(value) return True
And here all of the code from my MainWindow that interacts with the model:
class MainWindow(QObject): def __init__(self, backend_socket): super().__init__(None) self.__phone_connection_data = {} self.__phone_data_model = PhoneDataModel([]) self.__phone_data_model.appendRow('phone 1', '1001', True) # This works perfectly. self.__backend_socket = backend_socket self.__thread_pool = QThreadPool(self) self.__initialize_gui() self.__initialize_looper_thread(backend_socket) self.log = lambda level, message, exception=False: \ log(self.__backend_socket, level, message, exception) self.log('i', "GUI started successfully!") ... def __initialize_looper_thread(self, backend_socket): self.__looper_thread = LooperThread(backend_socket, self) self.__looper_thread.set_phone_data_signal.connect( lambda phone_connection_data: self.set_phone_data(phone_connection_data)) # Signal that connects to method that adds row to model self.__looper_thread.start() @Property(QObject) def phoneDataModel(self): return self.__phone_data_model ... def set_phone_data(self, phone_connection_data): self.__phone_connection_data = phone_connection_data for phone_address in phone_connection_data.keys(): name = phone_connection_data[phone_address]['name'] bt_socket_connected = phone_connection_data[phone_address]['socket_connected'] # self.__phone_data_model_provider.add_phone(name, phone_address, bt_socket_connected) # Original code, commented out for testing purposes. self.__phone_data_model.appendRow('phone 3', '1001', True) # This doesnt work.
The main window receives a signal from my looper thread with the data of a new phone, which triggers the set_phone_data() method. The signal is working perfectly fine and the method gets called every time my looper thread emits the signal.
Since my QML isn't formatting right, here are the pastebins:
MainWindow:
https://pastebin.com/gPNRk2wcPhoneSelector (Essentially my own custom ComboBox because the stock combobox wasn't doin' it for me):
https://pastebin.com/AAqtq4HjHere are things I have tried so far:
-
Putting the count into a separate property using the Property class from QtCore.
-
Providing the model via a separate QObject
-
Making the main window the parent of the model
-
Modifying the model using a QThread (in case it was a problem with the MainWindow QObject)
Expected behavior:
- Once a new phone is added, rowCount() is greater than one and the label goes from "No phones connected..." to the name of the current phone in the model. The MouseArea becomes enabled so the user can interact with the PhoneSelecter
Actual behavior:
- PhoneSelector behaves as if the rowCount() is 0. Mouse area is disabled and the label says "No Phones Connected..." If I make the MouseArea always enabled, the ListView shows the phones in my model, but the count is still zero. UNLESS, I add the phones within my main window's constructor. If I add a phone in the constructor and add a phone in my set_phone_data method, both phones are shown in my list, but the count on the QML side is just 1 rather than 2.
wrote on 19 Oct 2021, 01:21 last edited by eyllanesc-
Change to
self.__looper_thread.set_phone_data_signal.connect(self.set_phone_data)
, lambda is useless. -
Have you verified that the
set_phone_data
method is called?
-
-
wrote on 19 Oct 2021, 02:16 last edited by
-
Thank you for the advice on the lambda. I thought I needed it to get the data from the signal to go into the method, but it works perfectly fine without lambda.
-
Yes, I verified that the set_phone_data method is called by putting print(phone_connection_data) in the method. The print statement shows in the output with the correct data, so I know it is working as intended (besides the updating the model part, of course.)
-
-
-
Thank you for the advice on the lambda. I thought I needed it to get the data from the signal to go into the method, but it works perfectly fine without lambda.
-
Yes, I verified that the set_phone_data method is called by putting print(phone_connection_data) in the method. The print statement shows in the output with the correct data, so I know it is working as intended (besides the updating the model part, of course.)
wrote on 19 Oct 2021, 02:32 last edited by@cray12399 Then the error may be elsewhere. Run your script from the console and let me know if any error messages are shown, don't use an IDE as many don't know how to handle Qt errors and hide them.
-
-
@cray12399 Then the error may be elsewhere. Run your script from the console and let me know if any error messages are shown, don't use an IDE as many don't know how to handle Qt errors and hide them.
wrote on 19 Oct 2021, 02:49 last edited by cray12399@eyllanesc Thanks for the tip! I wish I would've known about the terminal thing earlier, it would've saved me some headache on other issues I have dealt with. I ran it in the terminal and here is what I got:
QQmlExpression: Expression file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:16:5 depends on non-NOTIFYable properties: MainWindow::phoneDataModel file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: Failed to initialize QSettings instance. Status code is: 1 file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: The following application identifiers have not been set: QVector("organizationName", "organizationDomain") Icon theme "elementary" not found. Icon theme "gnome" not found. This print statement is in set_phone_data right before the line appending a row to the model.
I added a print statement right before the appendRow() code in the set_phone_data method as you can see above, and no errors popped up after the print statement. However, I am assuming the problem has to do with this line:
QQmlExpression: Expression file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:16:5 depends on non-NOTIFYable properties: MainWindow::phoneDataModel
But I am not sure what the error means.
-
@eyllanesc Thanks for the tip! I wish I would've known about the terminal thing earlier, it would've saved me some headache on other issues I have dealt with. I ran it in the terminal and here is what I got:
QQmlExpression: Expression file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:16:5 depends on non-NOTIFYable properties: MainWindow::phoneDataModel file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: Failed to initialize QSettings instance. Status code is: 1 file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: The following application identifiers have not been set: QVector("organizationName", "organizationDomain") Icon theme "elementary" not found. Icon theme "gnome" not found. This print statement is in set_phone_data right before the line appending a row to the model.
I added a print statement right before the appendRow() code in the set_phone_data method as you can see above, and no errors popped up after the print statement. However, I am assuming the problem has to do with this line:
QQmlExpression: Expression file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:16:5 depends on non-NOTIFYable properties: MainWindow::phoneDataModel
But I am not sure what the error means.
wrote on 19 Oct 2021, 02:56 last edited by@cray12399 Change:
@Property(QObject) def phoneDataModel(self): return self.__phone_data_model
to
def get_phoneDataModel(self): return self.__phone_data_model phoneDataModel = Property(QObject, fget=get_phoneDataModel, constant=True)
-
@cray12399 Change:
@Property(QObject) def phoneDataModel(self): return self.__phone_data_model
to
def get_phoneDataModel(self): return self.__phone_data_model phoneDataModel = Property(QObject, fget=get_phoneDataModel, constant=True)
wrote on 19 Oct 2021, 03:29 last edited by@eyllanesc I changed the code you said and it fixed the error in the terminal, but I am still having the same problem, unfortunately. This is the terminal output now:
file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: Failed to initialize QSettings instance. Status code is: 1 file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: The following application identifiers have not been set: Icon theme "elementary" not found. Icon theme "gnome" not found. This print statement is in set_phone_data right before the line appending a row to the model.
-
@eyllanesc I changed the code you said and it fixed the error in the terminal, but I am still having the same problem, unfortunately. This is the terminal output now:
file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: Failed to initialize QSettings instance. Status code is: 1 file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: The following application identifiers have not been set: Icon theme "elementary" not found. Icon theme "gnome" not found. This print statement is in set_phone_data right before the line appending a row to the model.
wrote on 19 Oct 2021, 03:36 last edited byIf you add:
import threading
def set_phone_data(self, phone_connection_data): print(threading.current_thread()) # <---- # ... self.__phone_data_model.appendRow('phone 3', '1001', True) # This doesnt work. print(self.__phone_data_model.rowCount()) # <----
What is printed on the console?
How to verify that the number of rows does not change?
-
If you add:
import threading
def set_phone_data(self, phone_connection_data): print(threading.current_thread()) # <---- # ... self.__phone_data_model.appendRow('phone 3', '1001', True) # This doesnt work. print(self.__phone_data_model.rowCount()) # <----
What is printed on the console?
How to verify that the number of rows does not change?
wrote on 19 Oct 2021, 04:09 last edited by cray12399@eyllanesc Alright, I implemented the code. Here is the output I got:
file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: Failed to initialize QSettings instance. Status code is: 1 file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: The following application identifiers have not been set: QVector("organizationName", "organizationDomain") Icon theme "elementary" not found. Icon theme "gnome" not found. <_MainThread(MainThread, started 139841449985856)> The row count is 1 This print statement is in set_phone_data right before the line appending a row to the model. This statement is in set_phone_data right after the line appending a row to the model: The row count is 2
I kept the same code as above where a phone was added in the constructor, and the the set_phone_data statement. I dont know if I mentioned it in the original post, but I actually tried this same test and the row count increments on the Python side fine. However, on the QML side, it doesn't increment when a phone is added via the set_phone_data method. The way I know this is in my PhoneSelector QML, I set the QLabel to show the rowCount() of the model with the following code:
Label { id: currentItemText anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.right: downArrow.left anchors.rightMargin: 15 anchors.leftMargin: 15 color: "black" // text: count > 0 ? phoneSelector.currentText : "No Phones Connected..." text: "Row count is " + phoneSelector.model.rowCount() elide: Text.ElideRight }
Using the code from my example, where two phones are added, while the rowCount() returns 2 in the python code, it is 1 in the actual GUI, despite two phones being listed in my list model. Here is a picture:
This picture was taken after the set_phone_data method was run. Using the same QML code to output the row count, if I don't add the phone in the constructor, it shows 0 instead of 1, but it will still show the phone in the list if I enable the mouse area so that the listview can be expanded. So it seems that the row count is not updating when a phone is added outside the constructor.
-
@eyllanesc Alright, I implemented the code. Here is the output I got:
file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: Failed to initialize QSettings instance. Status code is: 1 file:///home/chris/Documents/Programming/Synchrony/Desktop/GUI/MainWindow.qml:26:5: QML Settings: The following application identifiers have not been set: QVector("organizationName", "organizationDomain") Icon theme "elementary" not found. Icon theme "gnome" not found. <_MainThread(MainThread, started 139841449985856)> The row count is 1 This print statement is in set_phone_data right before the line appending a row to the model. This statement is in set_phone_data right after the line appending a row to the model: The row count is 2
I kept the same code as above where a phone was added in the constructor, and the the set_phone_data statement. I dont know if I mentioned it in the original post, but I actually tried this same test and the row count increments on the Python side fine. However, on the QML side, it doesn't increment when a phone is added via the set_phone_data method. The way I know this is in my PhoneSelector QML, I set the QLabel to show the rowCount() of the model with the following code:
Label { id: currentItemText anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.right: downArrow.left anchors.rightMargin: 15 anchors.leftMargin: 15 color: "black" // text: count > 0 ? phoneSelector.currentText : "No Phones Connected..." text: "Row count is " + phoneSelector.model.rowCount() elide: Text.ElideRight }
Using the code from my example, where two phones are added, while the rowCount() returns 2 in the python code, it is 1 in the actual GUI, despite two phones being listed in my list model. Here is a picture:
This picture was taken after the set_phone_data method was run. Using the same QML code to output the row count, if I don't add the phone in the constructor, it shows 0 instead of 1, but it will still show the phone in the list if I enable the mouse area so that the listview can be expanded. So it seems that the row count is not updating when a phone is added outside the constructor.
wrote on 19 Oct 2021, 04:25 last edited by eyllanesc@cray12399 I can't tell you where the error is since I can't analyze it completely since they are just pieces of code but I can point out some observations:
- rowCount is not a property but a method, so they are not used to make a binding, that means that if the number of rows is changed then the property will not be reevaluated. There are 2 options: Use the rowsInserted, rowsRemoved, etc tokens to update the qml property, or create a qproperty called count associated with a signal so that it can be used in bindings.
first option:
Connections{ target: <FOO>.phoneDataModel function onRowsInserted(){ currentItemText.text = "Row count is " + <FOO>.phoneDataModel.rowCount() } // function onRowsRemoved(){} }
-
@cray12399 I can't tell you where the error is since I can't analyze it completely since they are just pieces of code but I can point out some observations:
- rowCount is not a property but a method, so they are not used to make a binding, that means that if the number of rows is changed then the property will not be reevaluated. There are 2 options: Use the rowsInserted, rowsRemoved, etc tokens to update the qml property, or create a qproperty called count associated with a signal so that it can be used in bindings.
first option:
Connections{ target: <FOO>.phoneDataModel function onRowsInserted(){ currentItemText.text = "Row count is " + <FOO>.phoneDataModel.rowCount() } // function onRowsRemoved(){} }
wrote on 19 Oct 2021, 14:04 last edited by@eyllanesc Thank you my friend! Your method works and I learned a new thing about using QML. :)
1/11