I have a
QSqlRelationalTableModelworking with aQTableViewandQDataWidgetMapper. Combo boxes correctly display related company names. But when I add aQSortFilterProxyModelfor column sorting, two things break: the combo boxes for related names disappear from the table, and selecting a row after sorting maps to the wrong record in theQDataWidgetMapper. How can I fix this?
This is a common stumbling block when combining QSqlRelationalTableModel, QSortFilterProxyModel, and QDataWidgetMapper. The root cause of both problems is the same: proxy models remap row indices, and if you don't account for that remapping, everything gets out of sync.
Why sorting breaks row selection
When you insert a QSortFilterProxyModel between your source model and your view, the view now works with proxy indices. Row 0 in the proxy might correspond to row 5 in the source model, depending on the current sort order.
The QDataWidgetMapper needs to work with the same model (and the same indices) as the view. If your mapper is connected to the source model but your view is displaying the proxy model, clicking row 0 in the view will tell the mapper to show source row 0 — which is the wrong record after sorting.
The fix: set the mapper's model to the proxy model
Why combo box delegates disappear
QSqlRelationalTableModel has a special method called relationModel() that provides the data for combo box delegates (via QSqlRelationalDelegate). When you place a QSortFilterProxyModel on top, the view no longer sees a QSqlRelationalTableModel directly — it sees a generic proxy model. The QSqlRelationalDelegate tries to call relationModel() on the view's model, which doesn't exist on a QSortFilterProxyModel, so the combo boxes silently fail to appear.
The fix: create a custom delegate that reaches through the proxy to the underlying relational model for its combo box data.
Setting up the source model
Let's start with a working example. We'll create two SQLite tables — companies and resources — where each resource has a foreign key pointing to a company.
import sys
from PyQt6.QtCore import Qt, QSortFilterProxyModel
from PyQt6.QtSql import (
QSqlDatabase,
QSqlQuery,
QSqlRelation,
QSqlRelationalTableModel,
QSqlRelationalDelegate,
)
from PyQt6.QtWidgets import (
QApplication,
QComboBox,
QDataWidgetMapper,
QHBoxLayout,
QFormLayout,
QLineEdit,
QMainWindow,
QTableView,
QVBoxLayout,
QWidget,
)
def create_database():
"""Create an in-memory database with sample data."""
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(":memory:")
if not db.open():
print("Cannot open database")
sys.exit(1)
query = QSqlQuery()
query.exec(
"CREATE TABLE companies ("
" id INTEGER PRIMARY KEY, "
" name TEXT NOT NULL"
")"
)
query.exec("INSERT INTO companies (name) VALUES ('Acme Corp')")
query.exec("INSERT INTO companies (name) VALUES ('Globex Inc')")
query.exec("INSERT INTO companies (name) VALUES ('Initech')")
query.exec(
"CREATE TABLE resources ("
" id INTEGER PRIMARY KEY, "
" name TEXT NOT NULL, "
" role TEXT NOT NULL, "
" company_id INTEGER NOT NULL, "
" FOREIGN KEY (company_id) REFERENCES companies(id)"
")"
)
query.exec(
"INSERT INTO resources (name, role, company_id) "
"VALUES ('Alice', 'Engineer', 1)"
)
query.exec(
"INSERT INTO resources (name, role, company_id) "
"VALUES ('Bob', 'Designer', 2)"
)
query.exec(
"INSERT INTO resources (name, role, company_id) "
"VALUES ('Charlie', 'Manager', 3)"
)
query.exec(
"INSERT INTO resources (name, role, company_id) "
"VALUES ('Diana', 'Analyst', 1)"
)
query.exec(
"INSERT INTO resources (name, role, company_id) "
"VALUES ('Eve', 'Developer', 2)"
)
return db
This gives us a small dataset to work with. Each resource belongs to a company, referenced by company_id.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!
Creating a proxy-aware relational delegate
The standard QSqlRelationalDelegate expects the view's model to be a QSqlRelationalTableModel. Since we're using a proxy, we need a delegate that can look past the proxy and find the relational model underneath.
class ProxyAwareRelationalDelegate(QSqlRelationalDelegate):
"""A delegate that works with QSqlRelationalTableModel through a proxy."""
def _source_model(self, proxy_model):
"""Walk through any chain of proxies to find the source model."""
model = proxy_model
while hasattr(model, "sourceModel"):
model = model.sourceModel()
return model
def createEditor(self, parent, option, index):
"""Create a combo box editor for relational columns."""
source_model = self._source_model(index.model())
if not isinstance(source_model, QSqlRelationalTableModel):
return super().createEditor(parent, option, index)
# Map the proxy column to the source column (column indices
# don't change with sorting, but this keeps things explicit).
column = index.column()
relation_model = source_model.relationModel(column)
if relation_model is None:
# Not a relational column — use the default editor.
return super().createEditor(parent, option, index)
combo = QComboBox(parent)
combo.setModel(relation_model)
combo.setModelColumn(
relation_model.fieldIndex(
source_model.relation(column).displayColumn()
)
)
return combo
def setEditorData(self, editor, index):
if isinstance(editor, QComboBox):
current_text = index.data(Qt.ItemDataRole.DisplayRole)
idx = editor.findText(str(current_text))
if idx >= 0:
editor.setCurrentIndex(idx)
else:
super().setEditorData(editor, index)
def setModelData(self, editor, model, index):
if isinstance(editor, QComboBox):
model.setData(index, editor.currentText())
else:
super().setModelData(editor, model, index)
The _source_model helper method walks through any number of stacked proxy models to reach the actual QSqlRelationalTableModel. This makes the delegate robust — it will work even if you add additional proxy layers later.
Mapping proxy indices for the QDataWidgetMapper
The second problem — getting the wrong record after sorting — comes from a mismatch between proxy indices and source indices. The solution is straightforward: use the proxy model for both the view and the mapper.
When you click a row in the table view, you get a proxy index. If the mapper is also using the proxy model, you can pass that row number directly to mapper.setCurrentIndex() and everything stays in sync.
Here's how to wire it up. If you're new to the Model/View architecture in PyQt6, see the PyQt6 ModelView Architecture tutorial for a thorough introduction:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Relational Model with Sorting")
self.resize(800, 400)
# --- Source model ---
self.source_model = QSqlRelationalTableModel()
self.source_model.setTable("resources")
self.source_model.setRelation(
3, # company_id column
QSqlRelation("companies", "id", "name"),
)
self.source_model.setHeaderData(0, Qt.Orientation.Horizontal, "ID")
self.source_model.setHeaderData(1, Qt.Orientation.Horizontal, "Name")
self.source_model.setHeaderData(2, Qt.Orientation.Horizontal, "Role")
self.source_model.setHeaderData(
3, Qt.Orientation.Horizontal, "Company"
)
self.source_model.select()
# --- Proxy model ---
self.proxy_model = QSortFilterProxyModel()
self.proxy_model.setSourceModel(self.source_model)
# --- Table view ---
self.table_view = QTableView()
self.table_view.setModel(self.proxy_model)
self.table_view.setSortingEnabled(True)
self.table_view.setSelectionBehavior(
QTableView.SelectionBehavior.SelectRows
)
self.table_view.setItemDelegate(
ProxyAwareRelationalDelegate(self.table_view)
)
self.table_view.hideColumn(0) # Hide the ID column
self.table_view.horizontalHeader().setStretchLastSection(True)
# --- Form widgets ---
self.name_edit = QLineEdit()
self.role_edit = QLineEdit()
self.company_combo = QComboBox()
# Populate the company combo box from the relation model.
relation_model = self.source_model.relationModel(3)
self.company_combo.setModel(relation_model)
self.company_combo.setModelColumn(
relation_model.fieldIndex("name")
)
form_layout = QFormLayout()
form_layout.addRow("Name:", self.name_edit)
form_layout.addRow("Role:", self.role_edit)
form_layout.addRow("Company:", self.company_combo)
# --- Data widget mapper (uses the PROXY model) ---
self.mapper = QDataWidgetMapper()
self.mapper.setModel(self.proxy_model)
self.mapper.setItemDelegate(
ProxyAwareRelationalDelegate(self.mapper)
)
self.mapper.addMapping(self.name_edit, 1)
self.mapper.addMapping(self.role_edit, 2)
self.mapper.addMapping(self.company_combo, 3)
# --- Connect selection to mapper ---
self.table_view.selectionModel().currentRowChanged.connect(
self.on_row_changed
)
# --- Layout ---
form_widget = QWidget()
form_widget.setLayout(form_layout)
content_layout = QHBoxLayout()
content_layout.addWidget(self.table_view, stretch=2)
content_layout.addWidget(form_widget, stretch=1)
container = QWidget()
container.setLayout(content_layout)
self.setCentralWidget(container)
# Select the first row to start.
self.table_view.selectRow(0)
def on_row_changed(self, current, _previous):
"""Update the mapper when the selected row changes."""
self.mapper.setCurrentIndex(current.row())
Notice the critical detail in on_row_changed: we pass current.row() directly to the mapper. Because both the table view and the mapper use the same proxy model, the row numbers match — regardless of how the data is sorted.
If you had the mapper connected to the source model instead, you would need to manually map indices with proxy_model.mapToSource(), which is fragile and easy to get wrong. Using the proxy model for both avoids the problem entirely.
The complete example
Here's everything assembled into a single runnable file. For more on sorting and filtering table data, see sorting a QTableView. If you want to learn how to display tabular data with numpy and pandas alongside QTableView, take a look at the PyQt6 QTableView tutorial:
import sys
from PyQt6.QtCore import Qt, QSortFilterProxyModel
from PyQt6.QtSql import (
QSqlDatabase,
QSqlQuery,
QSqlRelation,
QSqlRelationalTableModel,
QSqlRelationalDelegate,
)
from PyQt6.QtWidgets import (
QApplication,
QComboBox,
QDataWidgetMapper,
QHBoxLayout,
QFormLayout,
QLineEdit,
QMainWindow,
QTableView,
QVBoxLayout,
QWidget,
)
def create_database():
"""Create an in-memory database with sample data."""
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(":memory:")
if not db.open():
print("Cannot open database")
sys.exit(1)
query = QSqlQuery()
query.exec(
"CREATE TABLE companies ("
" id INTEGER PRIMARY KEY, "
" name TEXT NOT NULL"
")"
)
query.exec("INSERT INTO companies (name) VALUES ('Acme Corp')")
query.exec("INSERT INTO companies (name) VALUES ('Globex Inc')")
query.exec("INSERT INTO companies (name) VALUES ('Initech')")
query.exec(
"CREATE TABLE resources ("
" id INTEGER PRIMARY KEY, "
" name TEXT NOT NULL, "
" role TEXT NOT NULL, "
" company_id INTEGER NOT NULL, "
" FOREIGN KEY (company_id) REFERENCES companies(id)"
")"
)
query.exec(
"INSERT INTO resources (name, role, company_id) "
"VALUES ('Alice', 'Engineer', 1)"
)
query.exec(
"INSERT INTO resources (name, role, company_id) "
"VALUES ('Bob', 'Designer', 2)"
)
query.exec(
"INSERT INTO resources (name, role, company_id) "
"VALUES ('Charlie', 'Manager', 3)"
)
query.exec(
"INSERT INTO resources (name, role, company_id) "
"VALUES ('Diana', 'Analyst', 1)"
)
query.exec(
"INSERT INTO resources (name, role, company_id) "
"VALUES ('Eve', 'Developer', 2)"
)
return db
class ProxyAwareRelationalDelegate(QSqlRelationalDelegate):
"""A delegate that works with QSqlRelationalTableModel through a proxy."""
def _source_model(self, proxy_model):
"""Walk through any chain of proxies to find the source model."""
model = proxy_model
while hasattr(model, "sourceModel"):
model = model.sourceModel()
return model
def createEditor(self, parent, option, index):
"""Create a combo box editor for relational columns."""
source_model = self._source_model(index.model())
if not isinstance(source_model, QSqlRelationalTableModel):
return super().createEditor(parent, option, index)
column = index.column()
relation_model = source_model.relationModel(column)
if relation_model is None:
return super().createEditor(parent, option, index)
combo = QComboBox(parent)
combo.setModel(relation_model)
combo.setModelColumn(
relation_model.fieldIndex(
source_model.relation(column).displayColumn()
)
)
return combo
def setEditorData(self, editor, index):
if isinstance(editor, QComboBox):
current_text = index.data(Qt.ItemDataRole.DisplayRole)
idx = editor.findText(str(current_text))
if idx >= 0:
editor.setCurrentIndex(idx)
else:
super().setEditorData(editor, index)
def setModelData(self, editor, model, index):
if isinstance(editor, QComboBox):
model.setData(index, editor.currentText())
else:
super().setModelData(editor, model, index)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Relational Model with Sorting")
self.resize(800, 400)
# --- Source model ---
self.source_model = QSqlRelationalTableModel()
self.source_model.setTable("resources")
self.source_model.setRelation(
3, # company_id column
QSqlRelation("companies", "id", "name"),
)
self.source_model.setHeaderData(0, Qt.Orientation.Horizontal, "ID")
self.source_model.setHeaderData(1, Qt.Orientation.Horizontal, "Name")
self.source_model.setHeaderData(2, Qt.Orientation.Horizontal, "Role")
self.source_model.setHeaderData(
3, Qt.Orientation.Horizontal, "Company"
)
self.source_model.select()
# --- Proxy model ---
self.proxy_model = QSortFilterProxyModel()
self.proxy_model.setSourceModel(self.source_model)
# --- Table view ---
self.table_view = QTableView()
self.table_view.setModel(self.proxy_model)
self.table_view.setSortingEnabled(True)
self.table_view.setSelectionBehavior(
QTableView.SelectionBehavior.SelectRows
)
self.table_view.setItemDelegate(
ProxyAwareRelationalDelegate(self.table_view)
)
self.table_view.hideColumn(0)
self.table_view.horizontalHeader().setStretchLastSection(True)
# --- Form widgets ---
self.name_edit = QLineEdit()
self.role_edit = QLineEdit()
self.company_combo = QComboBox()
relation_model = self.source_model.relationModel(3)
self.company_combo.setModel(relation_model)
self.company_combo.setModelColumn(
relation_model.fieldIndex("name")
)
form_layout = QFormLayout()
form_layout.addRow("Name:", self.name_edit)
form_layout.addRow("Role:", self.role_edit)
form_layout.addRow("Company:", self.company_combo)
# --- Data widget mapper (uses the PROXY model) ---
self.mapper = QDataWidgetMapper()
self.mapper.setModel(self.proxy_model)
self.mapper.setItemDelegate(
ProxyAwareRelationalDelegate(self.mapper)
)
self.mapper.addMapping(self.name_edit, 1)
self.mapper.addMapping(self.role_edit, 2)
self.mapper.addMapping(self.company_combo, 3)
# --- Connect selection to mapper ---
self.table_view.selectionModel().currentRowChanged.connect(
self.on_row_changed
)
# --- Layout ---
form_widget = QWidget()
form_widget.setLayout(form_layout)
content_layout = QHBoxLayout()
content_layout.addWidget(self.table_view, stretch=2)
content_layout.addWidget(form_widget, stretch=1)
container = QWidget()
container.setLayout(content_layout)
self.setCentralWidget(container)
# Select the first row to start.
self.table_view.selectRow(0)
def on_row_changed(self, current, _previous):
"""Update the mapper when the selected row changes."""
self.mapper.setCurrentIndex(current.row())
if __name__ == "__main__":
app = QApplication(sys.argv)
create_database()
window = MainWindow()
window.show()
sys.exit(app.exec())
Run this and try clicking column headers to sort the table. Then click different rows — the form on the right will always show the correct record, and the Company column in the table will display names (with combo box editing) instead of raw foreign key IDs.
For a broader look at how to implement filtering and searching within table views, see the widget search bar tutorial. You can also learn more about signals and slots to understand how the selection change is communicated to the mapper.
Purchasing Power Parity
Developers in [[ country ]] get [[ discount.discount_pc ]]% OFF on all books & courses with code [[ discount.coupon_code ]]
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick
(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!