The code below mostly does what I want: I start out with data of 2D table (list of lists), where 1st and 3rd column start out as random numbers, but 2nd column starts with None. This data is used for a table model of a QTableView, which for the cells in the 2nd None column, displays QComboBox with several entries that are also random numbers. The idea is then, that making a choice of a value in a QComboBox sets the corresponding integer value in the 2D table at the given row and column. This is done well by the combo.currentIndexChanged.connect... line.
However, I have commented that combo.currentIndexChanged.connect... line in the code below, just to emphasize the weird behavior that I'm seeing.
First, I'd like to note something that was not obvious to me, which is that
- Without
openPersistentEditor, the corresponding cell will never runComboBoxDelegate.createEditor(andComboBoxDelegate.setModelData), and correspondingly never show dropdowns/QComboBoxes. - Cells that do not have
openPersistentEditor, but return flags withQt.ItemIsEditable, will still allow for double-click and edit via the default line edit (and will only callsetDataon change).
Now, then, the weird behavior is:
- I start the code, I get the window rendered with the table, and I immediately close the application. In the printout, I get:
setModelData self.sender()=None index.row()=2 index.column()=1 editor.currentText()='70'
setData self.sender()=None row=2 col=1 value='70'
... self._data=[[67, None, 41], [99, None, 5], [27, '70', 47]]
So, without any interaction at all, somehow both QStyledItemDelegate.setModelData and QAbstractTableModel.setData triggered on application exit; and while the value of the QComboBox, at this time, at 3rd row 2nd column was indeed '70', this got written into my table - as a string - without any intention, which I do not want.
Note: since there is a call to
setDatafromsetModelData, I'd assume thesetDatais dependent onsetModelData; however, even if the methodsetModelDatais completely commented away from the code (withopenPersistentEditorleft), the same experiment prints out:setData self.sender()=None row=2 col=1 value='70' ... self._data=[[67, None, 41], [99, None, 5], [27, '70', 47]]... so
setDataof the model gets independently triggered by application close (even withoutsetModelData)
Here is another example:
I start the application, I get the window rendered with the table
I click on empty space in the application window - I get a printout:
setModelData self.sender()=None index.row()=2 index.column()=1 editor.currentText()='70' setData self.sender()=None row=2 col=1 value='70' ... self._data=[[67, None, 41], [99, None, 5], [27, '70', 47]]I close the application - there is no printout.
Another example:
- I start the application, I get the window rendered with the table
- I click on empty space in the application window - I get a printout:
setModelData self.sender()=None index.row()=2 index.column()=1 editor.currentText()='70' setData self.sender()=None row=2 col=1 value='70' ... self._data=[[67, None, 41], [99, None, 5], [27, '70', 47]] - I click on the QComboBox at row=2 col=1 (third row, second column) - popup is shown, there is no printout
- I click on the QComboBox at second row, second column - popup is shown, and exact same previous printout is repeated
What causes these triggers of QStyledItemDelegate.setModelData and QAbstractTableModel.setData on application exit, click on empty space - and every time a QComboBox opens its popup in response to mouse left-click (though only if it is not the same QComboBox reported in the last printout); and how can I prevent them, so it is only the currentIndexChanged handler that is allowed to call setData?
Here is the example:
import sys
import random
from PyQt5.QtWidgets import QApplication, QTableView, QComboBox, QStyledItemDelegate
from PyQt5.QtCore import Qt, QAbstractTableModel
class RandomDataModel(QAbstractTableModel):
def __init__(self, data, parent=None):
super().__init__(parent)
self._data = data
def rowCount(self, parent=None):
return len(self._data)
def columnCount(self, parent=None):
return len(self._data[0])
def data(self, index, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
return None
def setData(self, index, value, role=Qt.EditRole):
row, col = index.row(), index.column()
if role == Qt.EditRole:
print(f"setData {self.sender()=} {row=} {col=} {value=}")
self._data[row][col] = value
self.dataChanged.emit(index, index)
print(f"... {self._data=}")
return True
return False
def flags(self, index):
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
class ComboBoxDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
# self.parent() here is MainWindow (that is, QTableView)
#print(f"createEditor {self.parent()=} {parent=}")
combo = QComboBox(parent)
combo.addItems([str(random.randint(1, 100)) for _ in range(3)])
#combo.currentIndexChanged.connect(lambda idx, qmidx=index: self.commitComboValue(idx, qmidx))
return combo
def setModelData(self, editor, model, index):
print(f"setModelData {self.sender()=} {index.row()=} {index.column()=} {editor.currentText()=}")
model.setData(index, editor.currentText())
def commitComboValue(self, index, qmidx):
# self.parent() here is MainWindow (that is, QTableView)
# self.sender() is QComboBox
row, col = qmidx.row(), qmidx.column()
value = int(self.sender().itemText(index))
print(f"commitComboValue {row=} {col=} {index=} {value=} {self.sender()=}")
table_view = self.parent()
table_view.model().setData(qmidx, value)
class MainWindow(QTableView):
def __init__(self):
super().__init__()
data = [
[random.randint(1, 100), None, random.randint(1, 100)],
[random.randint(1, 100), None, random.randint(1, 100)],
[random.randint(1, 100), None, random.randint(1, 100)],
]
self.the_model = RandomDataModel(data)
self.delegate = ComboBoxDelegate(self)
self.setModel(self.the_model)
self.setItemDelegateForColumn(1, self.delegate)
for row in range(3):
self.openPersistentEditor(self.the_model.index(row, 1))
self.resize(300, 300)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.setWindowTitle("QTableView Example")
window.show()
sys.exit(app.exec_())
currentText, which is why you get a string set on the model. While not always desirable, it's the expected behavior (especially for cases like QtSql models or QDataWidgetMapper).commitDataorcloseEditorwhenever the editor loses focus (directly or not, except for its direct children, as long as they're not popups), which is why you get asetModelData()even on application quit. If you want to update the model based on the index of the combo or its value (not its text) and only on active popup/wheel/keyboard selection, you need to override more carefully (includingeditorEvent()andeventFilter()) and eventually add further methods connected to the QComboBox signals.