How to link custom widgets to model data
-
Hi experts!
I'd like to make a custom dialog which uses custom widgets (I made) to show the values from a QAbstractTableModel.
From what I understood (in this guide) here what I need:
- An instance of a QAbstractTableModel.
- A "view" made using widgets (including my customs).
- An instance of the QDataWidgetMapper used to "link" the model data to "these" widgets.
I read also that the widgets need to have the user property which is used to transfer the data back and forth to and from the model.
And.. I read again that: "It is possible to set an item delegate to support custom widgets. By default, a QItemDelegate is used to synchronize the model with the widgets."
Which adds another "character" to the whole picture...
Example:
Let's say that I want to show changing values of my model (a small table (2, 2)) where the first column contains floats and the other boolean values. The first 2 widgets are just a QLabel, while the others are my custom widget (LEDs).- What should I need from the above "classes" to create this app?
- How do I add the user property to my custom (widget) class?
- Sorry but I didn't find an example for that.
- What can QItemDelegate do for me?
- It seems more something related to fine tuning, isn't it?
As usual many thanks for your answers!
Kind regards,
AGA -
Hi @JonB,
@JonB said in How to link custom widgets to model data:
That would suggest QDataWidgetMapper is only interested in providing editing facilities and therefore utilizing an "editing" control. So it's not set up [by default] for working with a QLabel.
That's makes a lot of sense now...
... And yes, using theaddMapping
method as from your linked page worked as expected...Here the code that solved also this point:
... ... self.mapper.addMapping(self.lbl_val_1, 1, b'text') self.mapper.addMapping(self.lbl_val_2, 2) ... ...
I also updated as @SGaist told me my custom
Led
class (I'm using PyQt not PySide) as:... ... @pyqtProperty(str) def value(self): return self._value @value.setter def value(self, value): self._value = value if self._value == 1: self.on() else: self.off() ... ...
But didn't work. I spend basically yesterday afternoon trying to understand what could cause the issue and I think that despite I made a mistake, I think that I found a "bug".
To make the long story short the type of the Q_PROPERTY in my decorator and the data type were different.
At the begin I (wrongly) casted my returned
data
method (within the model) to be str:return str(self._data[index.row()][index.column()])
Then I removed the
str()
cast, so data were back to their original float form, but my decorator was still set to receive string:@pyqtProperty(str)
.Running this never returned any runtime error or warning, but didn't work.
Of course, since we are talking about Python (which is also dynamically typed) I'm not sure if define this behavior wrong (here the double quotes around the bug word).Changing the decorator type to int solved the issue.
I really wanted to attach my working code in order to provide help to anyone, but looks that I can't (Error: You don't have enough privileges for this action).
Please let me know how to fix this.
Thanks!Kind reagrds,
AGA -
Hi,
The delegates are used in conjunction with the views which you are not using so no need to worry about them.
For the user property, see the Property class. You essentially have to set the
user
parameter to True. -
Hi @SGaist,
thanks for your answer! I updated my widget class accordingly.
... But I discovered that I have at least another issue first.Here where I am:
- I made a simple Model/View example that plots in a QTableView the data:
- I created a custom widget LED:
I then redesigned my view in order to "remap" the table cells to my widgets.
To start easy I just added few QLabel widgets.Here my view.py
class MyView(QWidget): def __init__( self, model_table: QAbstractTableModel, parent: QObject = None, ) -> None: super().__init__(parent=parent) # create layout self.initUI(model_table=model_table) def initUI(self, model_table: QAbstractTableModel) -> None: # make the GUI vbox = QVBoxLayout(self) # make widgets self.form_layout = QFormLayout() self.lbl_txt_1 = QLabel("Cell 1 value:") self.lbl_txt_2 = QLabel("Cell 2 value:") self.lbl_txt_3 = QLabel("Cell 3 value:") self.lbl_val_1 = QLabel() self.lbl_val_2 = QLabel() self.lbl_val_3 = QLabel() # add widgets to layout self.form_layout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.lbl_txt_1) self.form_layout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.lbl_val_1) self.form_layout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.lbl_txt_2) self.form_layout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.lbl_val_2) self.form_layout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.lbl_txt_3) self.form_layout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.lbl_val_3) vbox.addLayout(self.form_layout) # add the mapper self.mapper = QDataWidgetMapper() self.mapper.setModel(model_table) self.mapper.addMapping(self.lbl_val_1, 1) self.mapper.addMapping(self.lbl_val_2, 2) self.mapper.addMapping(self.lbl_val_3, 3) self.mapper.toFirst()
But I've got just this "empty" dialog which doesn't show my data values:
I then tried to use both the QLineEdit and QSpinBox as in the documentation example, but I still have the very same behavior:
- What I'm still missing?
- One more: How my mapped value is passed to the widget?
- I think through the Q_PROPERTY, correct?
- I'm wondering then: ... what if I want/need to cast (string/float) my data before it would arrive at the widget?
- It might be this the reason why my label were empties?, But what about then the QSpinBox which uses instead int rather than string?
- I think through the Q_PROPERTY, correct?
So I still have these open questions before move to use my custom widget...
Many thanks!
-
Hi @JonB,
The model contains the same (random generated) data that I showed in the first message.
I just updated the view to contain both the current QFormLayout and the QTableView of the "standard version", to provide the evidence that the Model/View architecture is working as expected.The updated view code basically adds the table which is linked to the same model as well.
... ... self.form_layout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.lbl_txt_3) self.form_layout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.lbl_val_3) v_splitter = QSplitter(QtCore.Qt.Orientation.Vertical) self.widget = QWidget() self.widget.setLayout(self.form_layout) v_splitter.addWidget(self.widget) # insert table self.table_view = QTableView() v_splitter.addWidget(self.table_view) vbox.addWidget(v_splitter) # add the mapper self.mapper = QDataWidgetMapper() # connect dictionaries to views self.mapper.setModel(model_table) self.table_view.setModel(model_table) self.mapper.addMapping(self.lbl_val_1, 1) ... ...
Where could be the error?
I still have no clear how can I manipulated the data type of the model data before passing it to the widget.
Many thanks!
-
@superaga
I don't know, I never had any trouble getting aQDataWidgetMapper
to work. Could you start by:- Stop all the values changing. Get it working first with values in the table which do not change all the time; and
- Use the widget which is suitable for your values. Since they look like all floating point
QSpinBox
is not right, use aQDoubleSpinBox
.
-
Hi @JonB,
I update the model and the view accordingly, but it is still not working.
Now model just contains these values:
... self._data = [[0,1,2,3], [3,2,1,0]] self._rows = len(self._data) self._columns = len(self._data[0]) ... ... # reimplement the model methods def rowCount(self, parent: QModelIndex) -> int: if parent.isValid(): return 0 return self._rows def columnCount(self, parent: QModelIndex) -> int: if parent.isValid(): return 0 return self._columns def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> Any: if role != Qt.ItemDataRole.DisplayRole: return QtCore.QVariant() # in all other cases return data return str(self._data[index.row()][index.column()]) ...
Data are now just integers, so using QSpinBox or QDoubleSpinBox didn't make any difference.
Are there other tests that I can perform?
Thanks!
-
😱
That's a very bad mistake! I'm so sorry... 😔I just fixed the model
data
method to be:def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> Any: if not index.isValid(): return None if role == Qt.ItemDataRole.DisplayRole: return str(self._data[index.row()][index.column()])
Now it is correct... But it is still not working (I still have the very same picture of my previous message).
Why the QTableView shows the values instead?
I read again the ItemDataRole description and looks good now.
I tried to understand where the issue is but I couldn't find any wrong.
-
@superaga
I don't see how what you have is right or that you have heeded what @SGaist said. Your method still returns nothing for the edit role? The usual code would be:if role == Qt.ItemDataRole.DisplayRole || role == Qt.ItemDataRole.EditRole: return str(self._data[index.row()][index.column()])
It's also not good to return
str(...)
fromdata()
when the underlying data has a perfectly reasonable type of its own, such as a number. Forcing tostr
will stop all kinds of things working correctly, e.g. sorting. It may also stop aQDoubleSpinBox
working. -
Hi @JonB,
Ok, now it works.
I understood that: I didn't have a clear picture about the ItemDataRole since I "deliberately overlooked" the
EditRole
.
I was convinced that to show read only data theDisplayRole
would have been enough, but I was wrong. Could you please suggest me a link where this is well explained?
I thought thatEditRole
was necessary only to let the data be editable.Regarding the data casting on the
data
method return I completely agree: this is completely wrong and must not be done.One more (if I can) could you also explain me (or point me to) how could I cast data (i.e. from float to str)? I still miss this "hidden" step between the model data return and the mapper fetch.
Super thanks!
-
@superaga
QDataWidgetMapper
expects to allow editing I think, and doesn't particularly know/care that your model might be read-only. If @SGaist says it always usesEditRole
then that's what it does.I have never used a
QDataWidgetMapper
with aQLabel
, so not yet sure where thestr()
needs doing. If you temporarily go back todata()
doing thestr()
(for the edit case, and for the column forCell 1 value
at least) does that then show the value on the label? I would test that first, else it's maybe aQLabel
rather than astr
issue? -
I somehow missed that
QDataWidgetMapper
needs theEditRole
, I apology.I modified the
data
method for the model as you described but the QLabel is still empty. I assume than that, for some reasons, the QLabel is not suited to be used within the QDataWidgetMapper...
I'll try now to progress in the view to replace the standard widget with mine.Many thanks to both of you
-
@superaga said in How to link custom widgets to model data:
I modified the data method for the model as you described but the QLabel is still empty. I assume than that, for some reasons, the QLabel is not suited to be used within the QDataWidgetMapper...
That would suggest
QDataWidgetMapper
is only interested in providing editing facilities and therefore utilizing an "editing" control. So it's not set up [by default] for working with aQLabel
. Doubtless it can be done....Ah, yes, now this is all to do with that "user" property you were asking about :) A straightforward example showing how easy it is to change for you is QDataWidgetMapper not working with QLabels. You just need to change your
addMapping()
lines to write theirtext
correctly. And after that you might understand how the property stuff works, if it's relevant to your widget.... -
Since python does not enforce types, I would wrap all returned values in QVariant since that's the type used in C++ and especially when setting properties like QDataWidgetMapper.
-
Hi @JonB,
@JonB said in How to link custom widgets to model data:
That would suggest QDataWidgetMapper is only interested in providing editing facilities and therefore utilizing an "editing" control. So it's not set up [by default] for working with a QLabel.
That's makes a lot of sense now...
... And yes, using theaddMapping
method as from your linked page worked as expected...Here the code that solved also this point:
... ... self.mapper.addMapping(self.lbl_val_1, 1, b'text') self.mapper.addMapping(self.lbl_val_2, 2) ... ...
I also updated as @SGaist told me my custom
Led
class (I'm using PyQt not PySide) as:... ... @pyqtProperty(str) def value(self): return self._value @value.setter def value(self, value): self._value = value if self._value == 1: self.on() else: self.off() ... ...
But didn't work. I spend basically yesterday afternoon trying to understand what could cause the issue and I think that despite I made a mistake, I think that I found a "bug".
To make the long story short the type of the Q_PROPERTY in my decorator and the data type were different.
At the begin I (wrongly) casted my returned
data
method (within the model) to be str:return str(self._data[index.row()][index.column()])
Then I removed the
str()
cast, so data were back to their original float form, but my decorator was still set to receive string:@pyqtProperty(str)
.Running this never returned any runtime error or warning, but didn't work.
Of course, since we are talking about Python (which is also dynamically typed) I'm not sure if define this behavior wrong (here the double quotes around the bug word).Changing the decorator type to int solved the issue.
I really wanted to attach my working code in order to provide help to anyone, but looks that I can't (Error: You don't have enough privileges for this action).
Please let me know how to fix this.
Thanks!Kind reagrds,
AGA -