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