Skip to content
Advertisement

Why does my QStyleItemDelegate pop up in a new window instead of in my TableView?

I’m trying to create a pretty simple app with Pyside2 that displays some data in a table that the user can edit. I want the cells of the table to autocomplete based on other values in the table, so I implemented a custom delegate following this tutorial. It was pretty straightforward and the delegate works as expected except for some reason the delegate pops up in a new window instead of being attached to the cell in the table, illustrated by this example (the custom delegate is defined at the end):

import pandas as pd
from PySide2 import QtWidgets, QtCore
from PySide2.QtCore import Qt


class TableView(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setObjectName(u"TableView")
        self.centralwidget = QtWidgets.QWidget(self)
        self.centralwidget.setObjectName(u"centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName(u"verticalLayout")
        self.frame = QtWidgets.QFrame(self.centralwidget)
        self.frame.setObjectName(u"frame")
        self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame)
        self.verticalLayout_2.setObjectName(u"verticalLayout_2")
        
        self.tableView = QtWidgets.QTableView(self.frame)
        self.tableView.setObjectName(u"tableView")
        delegate = QAutoCompleteDelegate(self.tableView)
        self.tableView.setItemDelegate(delegate)
        self.tableModel = TableModel(self.tableView)
        self.tableView.setModel(self.tableModel)

        self.verticalLayout_2.addWidget(self.tableView)
        self.verticalLayout.addWidget(self.frame)

        self.setCentralWidget(self.centralwidget)
        self.statusbar = QtWidgets.QStatusBar(self)
        self.statusbar.setObjectName(u"statusbar")
        self.setStatusBar(self.statusbar)

        QtCore.QMetaObject.connectSlotsByName(self)
        
        
class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, parent=None):
        super(TableModel, self).__init__(parent=parent)
        self._data = pd.DataFrame([["A0", "B0", "C0"], ["A1", "B1", "C1"], ["A2", "B2", "C2"]], columns=["A", "B", "C"])

    def data(self, index, role):
        # Should only ever refer to the data by iloc in this method, unless you
        # go specifically fetch the correct loc based on the iloc
        row = index.row()
        col = index.column()
        if role == Qt.DisplayRole or role == Qt.EditRole:
            return self._data.iloc[row, col]

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data.columns)

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._data.columns[col]
        return None
    
    def unique_vals(self, index):
        """ Identify the values to include in an autocompletion delegate for the given index """
        column = index.column()
        col_name = self._data.columns[column]
        return list(self._data[col_name].unique())

    def setData(self, index, value, role):
        if role != Qt.EditRole:
            return False
        row = self._data.index[index.row()]
        column = self._data.columns[index.column()]
        self._data.loc[row, column] = value
        self.dataChanged.emit(index, index)
        return True

    def flags(self, index):
        flags = super(self.__class__, self).flags(index)
        flags |= Qt.ItemIsEditable
        flags |= Qt.ItemIsSelectable
        flags |= Qt.ItemIsEnabled
        flags |= Qt.ItemIsDragEnabled
        flags |= Qt.ItemIsDropEnabled
        return flags


class QAutoCompleteDelegate(QtWidgets.QStyledItemDelegate):

    def createEditor(self, parent: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtWidgets.QWidget:
        le = QtWidgets.QLineEdit()
        test = ["option1", "option2", "option3"]
        complete = QtWidgets.QCompleter(test)
        le.setCompleter(complete)
        return le

    def setEditorData(self, editor:QtWidgets.QLineEdit, index:QtCore.QModelIndex):
        val = index.model().data(index, Qt.EditRole)
        options = index.model().unique_vals(index)
        editor.setText(val)
        completer = QtWidgets.QCompleter(options)
        editor.setCompleter(completer)

    def updateEditorGeometry(self, editor: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex):
        editor.setGeometry(option.rect)


app = QtWidgets.QApplication()
tv = TableView()
tv.show()
app.exec_()

Image showing table behavior when a cell is active

From everything I’ve read, the updateEditorGeometry method on the custom delegate should anchor it to the active cell, but something clearly isn’t working as expected in my case. And I am still learning Qt so I can’t claim to understand what it is doing. Is there anyone out there who might be able to explain what is going on here? Much appreciated.

  • OS: Ubuntu 20.04
  • Python 3.8.5
  • PySide2 5.15.0
  • Qt 5.12.9

Advertisement

Answer

Any widget that does not have a parent will be a window and since the QLineEdit has no parent then you observe the behavior that the OP indicates, for that reason the createEditor method has “parent” as an argument.

On the other hand, it is better to create a custom QLineEdit that easily implements the handling of adding suggestions to the QCompleter through a model.

class AutoCompleteLineEdit(QtWidgets.QLineEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._completer_model = QtGui.QStandardItemModel()
        completer = QtWidgets.QCompleter()
        completer.setModel(self._completer_model)
        self.setCompleter(completer)

    @property
    def suggestions(self):
        return [
            self._completer_model.item(i).text()
            for i in range(self._completer_model.rowCount())
        ]

    @suggestions.setter
    def suggestions(self, suggestions):
        self._completer_model.clear()
        for suggestion in suggestions:
            item = QtGui.QStandardItem(suggestion)
            self._completer_model.appendRow(item)


class QAutoCompleteDelegate(QtWidgets.QStyledItemDelegate):
    def createEditor(
        self,
        parent: QtWidgets.QWidget,
        option: QtWidgets.QStyleOptionViewItem,
        index: QtCore.QModelIndex,
    ) -> QtWidgets.QWidget:
        le = AutoCompleteLineEdit(parent)
        return le

    def setEditorData(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex):
        val = index.model().data(index, Qt.EditRole)
        options = index.model().unique_vals(index)
        editor.setText(val)
        editor.suggestions = options
User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement