QDataWidgetMapper how to clear mapped widgets when model is empty
-
When a QTableView's model is set to a QSortFilterProxyModel and that model is filtered such that no rows match and the model is empty then the QTableView is empty. This is what I would expect.
I have a QDataWidgetMapper's model set to a QSortFilterProxyModel. I've connected the proxy model's rowsRemoved signal to the mapper's .toFirst() (for lack of a better option)
In this configuration when the model is filtered down to no more matching rows, when the model is empty, the mapper leaves data populated in the mapped widgets. I would expect the mapped widgets to be cleared or emptied just like the table view is emptied.
What's the proper way to handle this?
Below is a full example that demonstrates this behavior:
import sys from PySide6.QtCore import QSortFilterProxyModel from PySide6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QDataWidgetMapper, QGridLayout, QLabel, QLineEdit, QWidget, QTableView) from PySide6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel class MyWindow(QMainWindow): def __init__(self, parent=None): super().__init__(parent) # --------------- SETUP THE UI self.id_label = QLabel("ID") self.id_edit = QLineEdit() self.id_label.setBuddy(self.id_edit) self.name_label = QLabel("Name") self.name_edit = QLineEdit() self.name_label.setBuddy(self.name_edit) self.filter_label = QLabel("Filter") self.filter_edit = QLineEdit() self.filter_label.setBuddy(self.filter_edit) self.table_view = QTableView() glayout = QGridLayout() glayout.addWidget(self.id_label, 0, 0) glayout.addWidget(self.id_edit, 0, 1) glayout.addWidget(self.name_label, 1, 0) glayout.addWidget(self.name_edit, 1, 1) hlayout = QHBoxLayout() hlayout.addWidget(self.filter_label) hlayout.addWidget(self.filter_edit) vlayout = QVBoxLayout() vlayout.addLayout(glayout) vlayout.addWidget(self.table_view) vlayout.addLayout(hlayout) widget = QWidget() widget.setLayout(vlayout) self.setCentralWidget(widget) # --------------- SETUP THE DATABASE self.con = QSqlDatabase.addDatabase("QSQLITE") self.con.setDatabaseName(":memory:") self.con.open() query = QSqlQuery() query.exec("CREATE TABLE person (person_id INTEGER PRIMARY KEY, person_name TEXT);") query.exec("INSERT INTO person (person_name) VALUES ('Aaron');") query.exec("INSERT INTO person (person_name) VALUES ('Brittany');") query.exec("INSERT INTO person (person_name) VALUES ('Chris');") query.exec("INSERT INTO person (person_name) VALUES ('Darren');") query.exec("INSERT INTO person (person_name) VALUES ('Danny');") query.exec("INSERT INTO person (person_name) VALUES ('Ellie');") # --------------- SETUP THE MODEL self.model = QSqlQueryModel() self.model.setQuery("SELECT * FROM person") self.filter = QSortFilterProxyModel() self.filter.setSourceModel(self.model) self.filter.setFilterKeyColumn(1) self.filter_edit.textEdited.connect(self.filter.setFilterRegularExpression) # --------------- SETUP THE MAPPER self.mapper = QDataWidgetMapper() self.mapper.setModel(self.filter) self.mapper.addMapping(self.id_edit, 0) self.mapper.addMapping(self.name_edit, 1) self.mapper.toFirst() self.filter.rowsRemoved.connect(lambda: self.mapper.toFirst()) # --------------- SETUP THE TABLE VIEW self.table_view.setModel(self.filter) if __name__ == '__main__': app = QApplication() win = MyWindow() win.show() sys.exit(app.exec())
-
@BamboozledBaboon
When you first create aQDataWidgetMapper
and either the model is not bound or contains no rows, what is the value ofQDataWidgetMapper ::currentIndex()
? I would guess-1
? I would expect you to callQDataWidgetMapper::setCurrentIndex(-1)
to "unbind" it from any row, does that work? -
@JonB Your guess is correct, the initial currentIndex of the mapper is -1, however setting the currentIndex to -1 fails to "depopulate" the mapped controls.
I've come up with a generic work around (or maybe this is how it should be done?) iterate through each of the mapped widgets and "reset" them one-by-one:
def update_mapped_widgets(self): if self.filter.rowCount(): self.mapper.toFirst() else: self.mapper.setCurrentIndex(-1) for section in range(self.filter.columnCount()): widget = self.mapper.mappedWidgetAt(section) if not widget: # this column doesn't have a widget mapped to it pass elif isinstance(widget, QComboBox): # clear the combo selection, but don't clear the combo items widget.setCurrentIndex(-1) else: # clear the contents of lineedit, textedit, etc widget.clear() # add more conditions for other widget types if needed
-
@BamboozledBaboon said in QDataWidgetMapper how to clear mapped widgets when model is empty:
however setting the currentIndex to -1 fails to "depopulate" the mapped controls.
That's a shame. I would say that is an "omission", if you felt like reporting it as a "bug". Your code looks like as good as it gets.
-
@BamboozledBaboon, I confirm your statements - my application suffer from the same problem but I hadn't enough time to search for a workaround. Thank you for your efforts.
I agree with @JonB - it looks really like a bug. I just want to add that it is similar to the one I reported recently - https://bugreports.qt.io/browse/QTBUG-115144 - it appears QDataWidgetMapper have some problems with internal logic when there are no data. It doesn't mean that problems are connected but I feel that a person who created the code somehow overlooked these scenarios with NULL or absent data.
-
@StarterKit said in QDataWidgetMapper how to clear mapped widgets when model is empty:
@BamboozledBaboon, I confirm your statements - my application suffer from the same problem but I hadn't enough time to search for a workaround. Thank you for your efforts.
I agree with @JonB - it looks really like a bug. I just want to add that it is similar to the one I reported recently - https://bugreports.qt.io/browse/QTBUG-115144 - it appears QDataWidgetMapper have some problems with internal logic when there are no data. It doesn't mean that problems are connected but I feel that a person who created the code somehow overlooked these scenarios with NULL or absent data.
I just read your bug report, I wouldn't be surprised at all if the problems were connected. I'd bet they are.
Your bug report mentions that integer presenting widgets should display 0 for null values, I can't help be wonder if rather the widgets should somehow be able to be set to null?
-
@StarterKit
I looked at your bug report. It is rather different from the issue we were discussing, viz. "unbind" aQDataWidgetMapper
in the sense of return it to its original state.You are now asking for all Qt data-mappable widgets to support the display/entry of backend database NULL values. Possibly regrettably that has never been the case, and I would guess is unlikely to be introduced as a result of your bug report. You are expected to write this yourself via an item delegate and suitable code in
setEditorData()
&setModelData()
.See also https://forum.qt.io/topic/131088/pyside6-qdatawidgetmapper-how-to-store-null-from-mapped-widget-into-db and https://www.qtcentre.org/threads/35832-Custom-QLineEdit-to-store-NULL-with-QDataWidgetMapper.
-
@JonB , I would be happy if you will show a small example of how to handle my case with help of delegate.
Because either I miss something crucial here or it isn't possible. My opinion is backed with the fact that I have a custom widget with integer property that handles value from a database. And everything works fine until it comes to a row with NULL value - the problem is that widget is never notified by QDataWidgetMapper in this case and it simply have no idea that it should display something new.
So I belive it isn't possible to handle this case via delegate. I would be happy to be wrong and learn something new from you.I remember out discussion happened by link you posted. But there we talked about opposite direction - when widget has NULL value and should put this value into database table. This indeed may be overridden (and I implemented it) because I know the moment when data are being saved to the database.
But here I'm talking about displaying the data from database (and actually more complex actions that might be hanled by custom widget) - and widget isn't notified about data change at all. And this is much bigger problem - you have no idea when row was changed in database. Yes, you may make some solution that will track user activity in some place and notify widgets - but it makesQDataWidgetMapper
a useless thing because it is big part of its destiny. -
@BamboozledBaboon said in QDataWidgetMapper how to clear mapped widgets when model is empty:
Your bug report mentions that integer presenting widgets should display 0 for null values, I can't help be wonder if rather the widgets should somehow be able to be set to null?
Some of the are. As you may see from an example in my bug report - it works fine for strings - QDataWidgetMapper gives an empty string for them. There were a discussion in the past where more experienced guys pointed it to the fact that there are no other way to translate NULL QVariant into python value. And ok, maybe it isn't perfect but it is as it is. With integer the problem is - this translation of NULL to anything doesn't happen at all. And yes, the same problem is here - it isn't possible to translate QVariant to the integer but... using the analogy with strings I believe it should be translated to 0. If not - then I think QDataWidgetMapper should have a signal that would be fired upon table row change.
-
@StarterKit
If the model emits a signal likedataChanged()
for whatever your "NULL" case is, then if you sayQDataWidgetMapper
does not act on the signal maybe you can subclass and handle it? If it does not emit a signal then it does not sound like aQDataWidgetMapper
issue. I suggest you test this. -
@JonB, but data haven't been changed. Why would a model emit anything?
I mean for the case of topic starter it might be a solution. But my case is slightly different. I have a table of data and user may move a cursor by one way or another (I mean data cursor, that tracks active row/record in the database table). As result different data should be shown in views and widgets. I.e. no change of data, only change of view should happen. So model has no reason to emit anything - it is precisely why QDataWidgetMapper was created - to make widgets aware of cursor's position change and supply a new piece of data to them. -
@StarterKit said in QDataWidgetMapper how to clear mapped widgets when model is empty:
until it comes to a row with NULL value - the problem is that widget is never notified by QDataWidgetMapper in this case and it simply have no idea that it should display something new.
Then why don't you implement this?
-
@JonB, I feel I lost the point.
Lets me explain it again and then you may feel the gap if I have it somewhere.
For simplicity, let's have only 1 table with data.
This table is displayed in a couple of different table views.
There is a panel that has several widgets linked to currently selected row in a table and displays something to the user based on data present in current row.
I.e. user or application may "randomly" select a row in the table. And after this selection something is displayed to the user.I have widgets from the panel linked to the table via
QDataWidgetMapper
- so if row is changed then widgets are updated by means ofQDataWidgetMapper
and perform their logic (And this happens well until we hit a NULL integer value).
I might be wrong but AFAIKQSqlTableModel
orQDataWidgetMapper
doesn't have a signal that would be fired after row change.
So currently I have quite a simple link - widgets-mapper-table that works on its own. What you propose me is to monitor status of views linked to the table and then update widgets? is it right? Then it will create a lot of interconnections between unrelated modules that I really would like to avoid.... -
@StarterKit QDataWidgetMapper emits the currentIndexChanged signal. I don't believe a model would have a reason to track "current index". I think what Jon is suggesting is to subclass QDataWidgetMapper to get it to properly update when it comes across nulls?
-
@StarterKit You've peaked my curiosity... because I realized your specific spinbox problem was going to be my problem as well very soon. I've come up with a work around that seems to work at the moment. I subclassed QSpinBox and created a custome property, value2, that accepts a string instead of an integer. If the mapper attempts to call the setter function with an empty string then the function will intercept this and convert it to zero.
from PySide6.QtCore import QByteArray, Property from PySide6.QtWidgets import QSpinBox class MySpin(QSpinBox): def readValue2(self): return self.value() def setValue2(self, val): if not val: self.setValue(0) else: self.setValue(int(val)) value2 = Property(str, readValue2, setValue2)
Use the overloaded addMapping function to map to this custom property like this:
mapper.addMapping(my_custom_spin, 3, QByteArray("value2"))
The root of the issue seems to be is that the datamapper by default maps to the 'user' property, which is 'value' for a spinbox. The value property is strictly an integer type. Because mapper sees an integer property and it doesn't know how to convert null to integer it doesn't bother and skips updating the value property. But we've observed mapper will convert nulls to an empty string for text boxes... So trick mappper to sending a string by mapping to a string type property instead.
Edit: realizing Jon suggested the approach should be to create a custom delegate Id really like to see how a pro would handle this! I still haven't fully wrapped my head around how/when to use delegates.
-
@JonB said in QDataWidgetMapper how to clear mapped widgets when model is empty:
...You are expected to write this yourself via an item delegate and suitable code in
setEditorData()
&setModelData()
.Could it be this simple?:
class MyDelegate(QStyledItemDelegate): def setEditorData(self, editor, index): if isinstance(editor, QSpinBox) and not index.data(): editor.setValue(0) return super().setEditorData(editor, index)
I suppose I should read through the links you posted.
-
@BamboozledBaboon said in QDataWidgetMapper how to clear mapped widgets when model is empty:
I think what Jon is suggesting is to subclass QDataWidgetMapper to get it to properly update when it comes across nulls?
If the mapper attempts to call the setter function with an empty string then the function will intercept this and convert it to zero.
Because mapper sees an integer property and it doesn't know how to convert null to integer it doesn't bother and skips updating the value property.
Yes to all of these!
Could it be this simple?:
Does it work? If so, yes!
The only thing I would say: You are actually setting the editor to
0
when data is "NULL". This means the user cannot see the difference between a genuine0
versus a "NULL" shown as a0
. And particularly when the user is in edit mode and submitting the record code (e.g. viasetModelData()
) won't know when it sees a0
whether to submit that or "NULL" to the database.In the case of a
QSpinBox
I believe you could empty out the text of the number to show as "blank", and accept "blank" input as meaning submit "NULL" to the database. It would mean that you must not use any standard integer validator on it.I think you may have to use
findChild<QLineEdit *>(spinBox)
to access the text. Another possibility might be to employ an unused value, e.g.-1
if appropriate, and combine withspecialVlaueText()
. There is alsovalueFromText()
andtextFromValue()
.Sometimes this does not work for the widget type. I recall having a
QDateEdit
for dates. But these could be left "unfilled" on the form, stored as "NULL" in the database.QDateEdit
does not allow you to "blank it out" in any way. Then you need the delegate to add something to allow for "NULL". For example, a checkbox for "NULL"/"empty"/"no date". And maybe disable/enable the date edit according as that box is checked/unchecked. -
Hi guys,
@JonB, @BamboozledBaboon thank you for discussion. Things your mentioned are clear for me and I did it previously one way or another so these things should work well.Just to add some comments from my side.
@JonB, you are right - we should distinguish "Zero" and "NULL" values at a widget level. But it isn't a big issue - if your design requires DB to have both, "Zero" and "NULL" values, then your widget should be capable to distinguish and handle it. It is up to you how to implement it - for example, I did a separate button (that is part of custom widget) that sets the widget to NULL value implicitly.
@BamboozledBaboon, you proposal with a string property is clear and it should work for sure (just I would prefer to use
fieldIndex()
method to get an index of a field foraddMapping()
as names are a bit more consistent then order of fields).But... I dislike an idea of having second string property very much. While it works it makes code really dirty with unnecessary duplication.
My strong opinion - it is a bug, because it isQDataWidgetMapper
who does the job and it should do it for any value that may be present in database. And as NULL is a valid value - it should handle it one way or another, not ignore it.Anyway, thanks both of you for this discussion and ideas how to overcome it because looking at number of unresolved bugs in Qt Widgets I feel it might take a looong time to have it corrected.
-
@StarterKit said in QDataWidgetMapper how to clear mapped widgets when model is empty:
if your design requires DB to have both, "Zero" and "NULL" values, then your widget should be capable to distinguish and handle it.
Just to wrap up. This is the nub of the problem. There seem to be just 3 possibilities:
- The widget itself should allow for a "NULL" value, somehow.
QDataWidgetMapper
supplied should "modify" all widgets it uses to add a facility for showing/entering "NULL", somehow.- You should modify
QDataWidgetMapper
(e.g. in an item delegate) to deal with "NULL" yourself.
Now,
QDataWidgetMapper
simply is not supplied with #2. In the Qt approach of "KISS" QDWM just deals with values which can be mapped to the widget, as supplied. Since the widget does not handle "NULL" the straightforward implementation of QDWM does not either. I do not see that as a failing of QDWM.Of course the "best" would be if all the base widgets allowed for "NULL"/empty. But they don't. Like I said, you may be able to do it for a
QSpinBox
by leaving it "empty", but take the case ofQDateEdit
and it's impossible. And if it did allow for this you could not simply have a widget value type ofint
orQDate
, you would either have to change that to e.g. aQVariant
or have someisEmpty()
method. So it would be a not inconsiderable change to all widgets. -
@JonB I respect your summary and I think it is a correct one.
But let me disagree with you aboutQDataWidgetMapper
behavior. I have nothing against "KISS" but the behavior should be also consistent and clear.
As I see it is:
A) inconsistent - as for some datatypes we have a mapping for NULL and for others have not. There might be different views but down to underlying bits - empty string and NULL are not the same values, so here we definitely have an adaptation and translation. But we don't have it for integer.
B) unclear - the documentation says nothing about someting won't be updated. There are only this text "Every time the current index changes, each widget is updated with data from the model via the property specified when its mapping was made." So it is always a surprise that calls for some kind of ad-hoc workarounds (instead of a proper design).Summarizing - I think the best what should be done:
- all datatypes should be treated equally (either with some translation or ingnoring NULL for all of them).
- documentation should have a clear statement about how it actually works.
I think I should update my bug reports with this kind of proposal.