I have a QTableView with a QAbstractTableModel and a ComboBox delegate set on one of the columns using
setItemDelegateForColumn. Everything works fine until I introduce aQSortFilterProxyModel. Once the proxy model is applied, the delegate disappears from the column. How do I fix this?
If you've set up a custom delegate on a QTableView and it works perfectly — until you add a QSortFilterProxyModel — the delegate seemingly vanishing can be confusing. The proxy model shouldn't affect your delegate at all, so what's going on?
The answer comes down to which widget you pass as the parent when creating your delegate.
The problem
Here's a typical setup that runs into this issue:
self.table_model = DictionaryTableModel()
self.delegate = ComboDelegate(self) # 'self' is the QDialog or QMainWindow
self.ui.table_view.setItemDelegateForColumn(2, self.delegate)
self.proxy_model = QSortFilterProxyModel(self)
self.proxy_model.setSourceModel(self.table_model)
self.ui.table_view.setModel(self.proxy_model)
At first glance this looks reasonable. You create the delegate, assign it to a column, set up the proxy model, and point the view at it. But the delegate disappears.
The cause is subtle: when you pass self (your dialog or main window) as the parent of the delegate, the delegate's parent is not the table view. When the view later sets the proxy model, it may clean up or fail to find delegates that aren't parented to it. The delegate object can be garbage collected or simply not be associated with the view correctly.
The fix
Pass the table view itself as the parent of your delegate:
self.delegate = ComboDelegate(self.ui.table_view)
That's it. By making the table view the parent of the delegate, Qt correctly keeps track of the delegate throughout the lifetime of the view — even when you swap in a proxy model.
A complete working example
Let's put together a full example that demonstrates a QTableView with a combo box delegate, a source model, and a QSortFilterProxyModel — all working together. If you're new to working with table models and views in PyQt6, you may want to review the QTableView with Model/View architecture tutorial first.
import sys
from PyQt6.QtCore import (
QAbstractTableModel,
QSortFilterProxyModel,
Qt,
)
from PyQt6.QtWidgets import (
QApplication,
QComboBox,
QMainWindow,
QStyledItemDelegate,
QTableView,
QVBoxLayout,
QWidget,
QLineEdit,
)
COLORS = ["Red", "Green", "Blue", "Yellow", "Orange"]
DATA = [
["Alice", "Engineer", "Red"],
["Bob", "Designer", "Blue"],
["Charlie", "Manager", "Green"],
["Diana", "Analyst", "Yellow"],
]
class SimpleTableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
self._headers = ["Name", "Role", "Favorite Color"]
def rowCount(self, parent=None):
return len(self._data)
def columnCount(self, parent=None):
return len(self._headers)
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
return self._data[index.row()][index.column()]
return None
def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
if role == Qt.ItemDataRole.EditRole:
self._data[index.row()][index.column()] = value
self.dataChanged.emit(index, index, [role])
return True
return False
def flags(self, index):
return (
Qt.ItemFlag.ItemIsSelectable
| Qt.ItemFlag.ItemIsEnabled
| Qt.ItemFlag.ItemIsEditable
)
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
if role == Qt.ItemDataRole.DisplayRole:
if orientation == Qt.Orientation.Horizontal:
return self._headers[section]
return str(section + 1)
return None
class ComboDelegate(QStyledItemDelegate):
def __init__(self, parent=None, items=None):
super().__init__(parent)
self.items = items or []
def createEditor(self, parent, option, index):
combo = QComboBox(parent)
combo.addItems(self.items)
return combo
def setEditorData(self, editor, index):
value = index.data(Qt.ItemDataRole.EditRole)
idx = editor.findText(value)
if idx >= 0:
editor.setCurrentIndex(idx)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentText(), Qt.ItemDataRole.EditRole)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Delegate + Proxy Model Example")
self.resize(500, 300)
# Set up the source model.
self.table_model = SimpleTableModel(DATA)
# Set up the proxy model for filtering/sorting.
self.proxy_model = QSortFilterProxyModel(self)
self.proxy_model.setSourceModel(self.table_model)
self.proxy_model.setFilterKeyColumn(0) # Filter by the Name column.
self.proxy_model.setFilterCaseSensitivity(
Qt.CaseSensitivity.CaseInsensitive
)
# Set up the table view.
self.table_view = QTableView()
self.table_view.setModel(self.proxy_model)
self.table_view.setSortingEnabled(True)
# Create the delegate with the TABLE VIEW as parent.
self.delegate = ComboDelegate(self.table_view, items=COLORS)
self.table_view.setItemDelegateForColumn(2, self.delegate)
# Add a filter input field.
self.filter_input = QLineEdit()
self.filter_input.setPlaceholderText("Type to filter by name...")
self.filter_input.textChanged.connect(
self.proxy_model.setFilterFixedString
)
# Layout.
layout = QVBoxLayout()
layout.addWidget(self.filter_input)
layout.addWidget(self.table_view)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Run this and you'll see a table with four rows. The "Favorite Color" column uses a combo box delegate — double-click any cell in that column and a dropdown appears with color options. You can type in the filter box at the top to filter rows by name, and click column headers to sort. The delegate stays in place throughout.
Why the parent matters
In Qt, the parent of a widget determines its lifetime and ownership. When you call setItemDelegateForColumn, the view expects to manage that delegate. If the delegate is parented to something else (like the main window), the view doesn't fully "own" it. In certain situations — particularly when models are swapped or proxy models are introduced — this mismatch can cause the delegate to be lost or garbage collected by Python.
By parenting the delegate to the table view, you ensure that:
- The delegate lives as long as the view does.
- The view can properly manage the delegate when models change.
- Python's garbage collector won't clean up the delegate unexpectedly.
This is a good habit to follow any time you create a delegate: always pass the table view as the parent.
Summary
When using a QSortFilterProxyModel with a QTableView that has custom delegates, make sure you create your delegate with the table view as its parent:
# Do this:
self.delegate = ComboDelegate(self.table_view)
# Instead of this:
self.delegate = ComboDelegate(self)
The proxy model itself doesn't interfere with delegates at all — delegates are a view-level concept, completely independent of the model. The real issue is object ownership in Qt's parent-child hierarchy. Getting the parent right keeps everything working smoothly.
For more on sorting and filtering tables, see our guide on sorting and filtering in QTableView. If you want to understand the broader Model/View architecture that underpins QTableView and proxy models, take a look at the PyQt6 Model/View Architecture tutorial. You can also learn more about editing data in a PyQt6 QTableView with custom delegates.
Packaging Python Applications with PyInstaller by Martin Fitzpatrick
This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.