【问题标题】:QTreeView with custom item delegate with Browse button带有浏览按钮的自定义项目委托的 QTreeView
【发布时间】:2020-02-13 06:34:59
【问题描述】:

使用 Qt5 框架(通过 Python 的 pyQt5),我需要创建一个带有参数 - 值列的 QTreeView 小部件,其中某些行的值项必须具有内部“浏览”按钮才能打开文件浏览对话框和将选中的文件放入对应值的字段中。

阅读关于项目委托的 Qt 手册,我整理了以下代码:

自定义 BrowseEdit 类(QLineEdit + Browse 操作)

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)

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 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)

为可浏览的编辑字段设置项目委托:

# 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)

当我通过按下编辑器中的浏览按钮调用打开文件对话框并在选择文件后尝试关闭对话框时发生错误。那时,会引发一个异常,说 BrowseEdit 对象已被删除!

我意识到发生这种情况是因为项目委托在退出编辑模式时(在启动文件浏览对话框时会发生这种情况)释放底层编辑器小部件(在我的情况下为 BrowseEdit)。但是我怎样才能避免这种情况呢?

我尝试过的另一件事是使用 QAbstractItemView::setItemDelegateForRow 方法,如下所示:

# install BrowseEditDelegate for rows 2 and 5
self.tv_plugins_3party.setItemDelegateForRow(2, BrowseEditDelegate())
self.tv_plugins_3party.setItemDelegateForRow(5, BrowseEditDelegate())

-- 但此代码会导致未知异常导致应用程序崩溃而没有任何回溯消息。

【问题讨论】:

    标签: python pyqt5 qtreeview qstyleditemdelegate qabstractitemview


    【解决方案1】:

    每个代表不能只有一个唯一的编辑器,原因有两个:

    1. 可能有更多活动的编辑器实例(使用openPersistentEditor 打开),例如一个表,其中一列的每一行都有一个组合框。
    2. 每次编辑器将其数据提交给模型时,如果它不是持久编辑器,它就会被销毁。考虑当一个 Qt 对象被分配给一个 Python 变量/属性时,它实际上是一个指向由 Qt 创建的底层 C++ 对象的指针。这意味着虽然self.editor 仍然作为 python 对象存在,但它指向的对象在编辑器被委托关闭时实际上已被删除。

    正如函数名所说,createEditor()创建一个编辑器,所以解决方案是每次调用createEditor()时创建一个新实例。

    更新

    这里有一个重要问题:一旦您打开对话框,代理编辑器就会失去焦点。对于一个item view,这和点击另一个item(改变焦点)是一样的,会导致数据提交和编辑器销毁。

    “简单”的解决方案是在要打开对话框时阻止委托信号(最重要的是closeEditor(),它将调用destroyEditor()),然后再解除阻止。

    
    class BrowseEdit(QtWidgets.QLineEdit):
        @QtCore.pyqtSlot()
        def on_btnaction(self):
            self.delegate.blockSignals(True)
            selected_path = QtWidgets.QFileDialog.getOpenFileName(self.window(), self.opendialogtitle, self.opendialogdir, self.filefilters)
            self.delegate.blockSignals(False)
            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)
    
    
    class BrowseEditDelegate(QtWidgets.QStyledItemDelegate):
        # ...
        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:
                    editor = BrowseEdit(parent=parent)
                    editor.delegate = self
                    return editor
                else:
                    return super().createEditor(parent, option, index)
            except Exception as err:
                print(err)
                return None
    

    也就是说,这是一个hack。虽然它有效,但不能保证它适用于未来的 Qt 版本,当可能引入其他信号或它们的行为发生变化时。

    更好和更优雅的解决方案是创建一个在单击浏览按钮时调用的信号,然后项目视图(或其任何父项)将负责浏览,如果文件对话框设置数据结果有效并再次开始编辑该字段:

    
    class BrowseEditDelegate(QtWidgets.QStyledItemDelegate):
        browseRequested = QtCore.pyqtSignal(QtCore.QModelIndex)
        # ...
        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:
                    editor = BrowseEdit(parent=parent)
                    editor.btnaction.triggered.connect(
                        lambda: self.browseRequested.emit(index))
                    return editor
                else:
                    return super().createEditor(parent, option, index)
            except Exception as err:
                print(err)
                return None
    
    
    class Window(QtWidgets.QWidget):
        def __init__(self):
            # ...
            delegate = BrowseEditDelegate(indices)
            self.tv_plugins_3party.setItemDelegate(delegate)
            delegate.browseRequested.connect(self.browseRequested)
    
        def browseRequested(self, index):
            selected_path = QtWidgets.QFileDialog.getOpenFileName(self.window(), 'Select file', index.data())
            if selected_path[0]:
                self.model_plugins_3party.setData(index, selected_path[0])
            self.tv_plugins_3party.edit(index)
    
    

    【讨论】:

    • 谢谢!见我的reply
    • @s0mbre 我已经更新了答案。如果他们带来更多问题并且不实际上回答,请避免创建新答案。相反,请尝试在 cmets 中解释您的问题,或者如果它们太复杂而无法编辑您的原始问题。
    • 谢谢!这种“hacky”解决方案确实对我有用,坦率地说,我看不出它为什么不能普遍适用。
    • @s0mbre 好,不客气!顺便说一句,我意识到我犯了一个相对较小的错误:blockSignals(False) 应该发生在返回函数之前,以防对话框被取消。我已经修复了代码,但请注意这一点 - 请记住,第二种解决方案无论如何都会更好,即使它看起来更复杂。
    猜你喜欢
    • 1970-01-01
    • 2017-06-26
    • 2013-06-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-11
    • 1970-01-01
    相关资源
    最近更新 更多