Working with the Qt5 framework (via pyQt5 with Python), I need to create a QTreeView widget with Parameter - Value columns, where the Value items for some rows must have an internal 'Browse' button to open a file browse dialog and place the selected file into the corresponding value's field.
Reading up the Qt manuals on item delegates, I've put together the following code:
Custom BrowseEdit class (QLineEdit + Browse action)
class BrowseEdit(QtWidgets.QLineEdit):
def __init__(self, contents='', filefilters=None,
btnicon=None, btnposition=None,
opendialogtitle=None, opendialogdir=None, parent=None):
super().__init__(contents, parent)
self.filefilters = filefilters or _('All files (*.*)')
self.btnicon = btnicon or 'folder-2.png'
self.btnposition = btnposition or QtWidgets.QLineEdit.TrailingPosition
self.opendialogtitle = opendialogtitle or _('Select file')
self.opendialogdir = opendialogdir or os.getcwd()
self.reset_action()
def _clear_actions(self):
for act_ in self.actions():
self.removeAction(act_)
def reset_action(self):
self._clear_actions()
self.btnaction = QtWidgets.QAction(QtGui.QIcon(f"{ICONFOLDER}/{self.btnicon}"), '')
self.btnaction.triggered.connect(self.on_btnaction)
self.addAction(self.btnaction, self.btnposition)
#self.show()
@QtCore.pyqtSlot()
def on_btnaction(self):
selected_path = QtWidgets.QFileDialog.getOpenFileName(self.window(), self.opendialogtitle, self.opendialogdir, self.filefilters)
if not selected_path[0]: return
selected_path = selected_path[0].replace('/', os.sep)
# THIS CAUSES ERROR ('self' GETS DELETED BEFORE THIS LINE!)
self.setText(selected_path)
Custom item delegate for the QTreeView:
class BrowseEditDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, model_indices=None, thisparent=None,
**browse_edit_kwargs):
super().__init__(thisparent)
self.model_indices = model_indices
self.editor = BrowseEdit(**browse_edit_kwargs)
self.editor.setFrame(False)
def createEditor(self, parent: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem,
index: QtCore.QModelIndex) -> QtWidgets.QWidget:
try:
if self.model_indices and index in self.model_indices:
self.editor.setParent(parent)
return self.editor
else:
return super().createEditor(parent, option, index)
except Exception as err:
print(err)
return None
def setEditorData(self, editor, index: QtCore.QModelIndex):
if not index.isValid(): return
if self.model_indices and index in self.model_indices:
txt = index.model().data(index, QtCore.Qt.EditRole)
if isinstance(txt, str):
editor.setText(txt)
else:
super().setEditorData(editor, index)
def setModelData(self, editor, model: QtCore.QAbstractItemModel, index: QtCore.QModelIndex):
if self.model_indices and index in self.model_indices:
model.setData(index, editor.text(), QtCore.Qt.EditRole)
else:
super().setModelData(editor, model, index)
def updateEditorGeometry(self, editor, option: QtWidgets.QStyleOptionViewItem,
index: QtCore.QModelIndex):
editor.setGeometry(option.rect)
Create underlying model:
# create tree view
self.tv_plugins_3party = QtWidgets.QTreeView()
# underlying model (2 columns)
self.model_plugins_3party = QtGui.QStandardItemModel(0, 2)
self.model_plugins_3party.setHorizontalHeaderLabels([_('Plugin'), _('Value')])
# first root item and sub-items
item_git = QtGui.QStandardItem(QtGui.QIcon(f"{ICONFOLDER}/git.png"), 'Git')
item_git.setFlags(QtCore.Qt.ItemIsEnabled)
item_1 = QtGui.QStandardItem(_('Enabled'))
item_1.setFlags(QtCore.Qt.ItemIsEnabled)
item_2 = QtGui.QStandardItem('')
item_2.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable)
item_2.setCheckable(True)
item_2.setUserTristate(False)
item_2.setCheckState(QtCore.Qt.Checked)
item_git.appendRow([item_1, item_2])
item_1 = QtGui.QStandardItem(_('Path'))
item_1.setFlags(QtCore.Qt.ItemIsEnabled)
item_2 = QtGui.QStandardItem('')
item_2.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable)
item_git.appendRow([item_1, item_2])
self.model_plugins_3party.appendRow(item_git)
# second root item and sub-items
item_sqlite = QtGui.QStandardItem(QtGui.QIcon(f"{ICONFOLDER}/sqlite.png"), _('SQLite Editor'))
item_sqlite.setFlags(QtCore.Qt.ItemIsEnabled)
item_1 = QtGui.QStandardItem(_('Enabled'))
item_1.setFlags(QtCore.Qt.ItemIsEnabled)
item_2 = QtGui.QStandardItem('')
item_2.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable)
item_2.setCheckable(True)
item_2.setUserTristate(False)
item_2.setCheckState(QtCore.Qt.Checked)
item_sqlite.appendRow([item_1, item_2])
item_1 = QtGui.QStandardItem(_('Path'))
item_1.setFlags(QtCore.Qt.ItemIsEnabled)
item_2 = QtGui.QStandardItem('')
item_2.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable)
item_sqlite.appendRow([item_1, item_2])
item_1 = QtGui.QStandardItem(_('Commands'))
item_1.setFlags(QtCore.Qt.ItemIsEnabled)
item_2 = QtGui.QStandardItem('<db>')
item_2.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable)
item_sqlite.appendRow([item_1, item_2])
self.model_plugins_3party.appendRow(item_sqlite)
# set model
self.tv_plugins_3party.setModel(self.model_plugins_3party)
Set item delegates for browsable edit fields:
# import traceback
try:
indices = []
indices.append(self.model_plugins_3party.index(1, 1,
self.model_plugins_3party.indexFromItem(item_git)))
indices.append(self.model_plugins_3party.index(1, 1,
self.model_plugins_3party.indexFromItem(item_sqlite)))
self.tv_plugins_3party.setItemDelegate(BrowseEditDelegate(indices))
except:
traceback.print_exc(limit=None)
The error occurs when I invoke the open file dialog by pressing on the Browse button in the editor and try to close the dialog after selecting a file. At that time, an exception is raised saying that the BrowseEdit object was deleted!
I realize this happens because the item delegate frees the underlying editor widget (BrowseEdit in my case) when it goes out of the editing mode (which happens when the file browse dialog is launched). But how can I avoid this?
Another thing I've tried is using the QAbstractItemView::setItemDelegateForRow method like so:
# install BrowseEditDelegate for rows 2 and 5
self.tv_plugins_3party.setItemDelegateForRow(2, BrowseEditDelegate())
self.tv_plugins_3party.setItemDelegateForRow(5, BrowseEditDelegate())
-- but this code leads to unknown exceptions crashing the app without any traceback messages.