QTreeView с пользовательским делегатом элемента с кнопкой Обзор - PullRequest
0 голосов
/ 13 февраля 2020

Работая с каркасом Qt5 (через pyQt5 с Python), мне нужно создать виджет QTreeView со столбцами «Параметр - Значение», где элементы «Значение» для некоторых строк должны иметь внутреннюю кнопку «Обзор», чтобы открыть просмотр файла диалоговое окно и поместите выбранный файл в поле соответствующего значения.

Читая руководства Qt для делегатов элементов, я собрал следующий код:

Пользовательский класс BrowseEdit (QLineEdit + Действие просмотра)

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

- но этот код приводит к неизвестным исключениям, вызывающим сбой приложения без каких-либо сообщений трассировки.

1 Ответ

1 голос
/ 13 февраля 2020

Не может быть только одного уникального редактора для каждого делегата, и это по двум причинам:

  1. Может быть больше активных экземпляров редактора (которые открываются с помощью openPersistentEditor), например, таблица, в которой столбец содержит поле со списком для каждой строки.
  2. Каждый раз, когда редактор представляет свои данные в модель, он уничтожается, если не является постоянным редактором. Учтите, что когда объект Qt назначается переменной / атрибуту python, он фактически является указателем на базовый объект C ++, который был создан Qt. Это означает, что хотя self.editor все еще существует как объект python, оно указывает на объект, который был фактически удален, когда редактор был закрыт делегатом.

Как следует из названия функции, createEditor() создает редактор, поэтому решение заключается в создании нового экземпляра при каждом вызове createEditor().

ОБНОВЛЕНИЕ

Там Здесь важна проблема: как только вы открываете диалог, редактор делегатов теряет фокус. Для представления элемента это то же самое, что щелкнуть другой элемент (изменение фокуса), что приведет к отправке данных и уничтожению редактора.

«Простое» решение заключается в блокировке сигналов делегата (что наиболее важно closeEditor(), который будет вызывать destroyEditor()), когда откроется диалоговое окно, и впоследствии разблокировать их.


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

Тем не менее, это это взломать . Хотя работает , не гарантируется, что так будет с будущими версиями Qt, когда могут появиться другие сигналы или изменить их поведение.

Лучшим и более элегантным решением было бы создание сигнал, который вызывается при нажатии кнопки обзора, тогда просмотр элемента (или любой его родительский элемент) позаботится о просмотре, установит данные, если результат диалога файла является действительным, и снова начнет редактирование поля:


class BrowseEditDelegate(QtWidgets.QStyledItemDelegate):
    <b>browseRequested = QtCore.pyqtSignal(QtCore.QModelIndex)</b>
    # ...
    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)
                <b>editor.btnaction.triggered.connect(
                    lambda: self.browseRequested.emit(index))</b>
                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)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...