I'm trying to use
QDataWidgetMapperwith aQSqlRelationalTableModelto map foreign key fields toQComboBoxwidgets. The regular fields (like text and numbers) work fine, but the combo boxes for my relational columns remain empty. How do I get this working correctly?
Getting QDataWidgetMapper to play nicely with QSqlRelationalTableModel and QComboBox widgets can be frustrating. The Qt documentation provides snippets in C++, but a complete, working Python example is hard to find. The main source of confusion is that each combo box needs its own model — the relation model for that specific column — rather than sharing the main table model.
Let's walk through the correct approach step by step, and then put it all together in a complete example.
Understanding the problem
When you have a database table with foreign key columns (like ssh_group_id referencing a sshgroup table), QSqlRelationalTableModel can resolve those foreign keys into human-readable values using QSqlRelation. That part usually works well with a QTableView.
The challenge comes when you want to use QDataWidgetMapper to display a single record in a form, with combo boxes for the relational fields. A common mistake is setting each combo box's model to the main relational table model:
# This won't work correctly for combo boxes
self.ssh_group_combo.setModel(self.model)
The main model represents the sshconnections table. What you actually want in each combo box is the list of options from the related table (e.g., all groups from sshgroup). QSqlRelationalTableModel provides access to these through its relationModel() method.
Setting up the relational model
First, set up the QSqlRelationalTableModel with its relations. You need to set the table and define the relations before calling select():
self.model = QSqlRelationalTableModel(db=self.sshdb)
self.model.setTable("sshconnections")
self.model.setEditStrategy(QSqlTableModel.OnManualSubmit)
# Define which columns hold foreign keys and where they point
ssh_group_col = self.model.fieldIndex("ssh_group_id")
ssh_key_col = self.model.fieldIndex("ssh_key_id")
ssh_config_col = self.model.fieldIndex("ssh_config_id")
self.model.setRelation(ssh_group_col, QSqlRelation("sshgroup", "id", "ssh_group_name"))
self.model.setRelation(ssh_key_col, QSqlRelation("sshkeys", "id", "sshkey_name"))
self.model.setRelation(ssh_config_col, QSqlRelation("sshconfig", "id", "ssh_config_name"))
self.model.select()
Notice that we're using the foreign key column names (ssh_group_id, ssh_key_id, ssh_config_id) when calling fieldIndex(), not the display column names from the related tables. These are the actual columns in the sshconnections table.
Configuring the combo boxes with relation models
Here's the part that makes everything work. After calling select() on the main model, you can retrieve the relation model for each foreign key column using relationModel(). Each combo box gets its own relation model:
# Each combo box gets the relation model for its specific column
self.ssh_group_combo.setModel(self.model.relationModel(ssh_group_col))
self.ssh_group_combo.setModelColumn(
self.model.relationModel(ssh_group_col).fieldIndex("ssh_group_name")
)
self.ssh_key_combo.setModel(self.model.relationModel(ssh_key_col))
self.ssh_key_combo.setModelColumn(
self.model.relationModel(ssh_key_col).fieldIndex("sshkey_name")
)
self.ssh_config_combo.setModel(self.model.relationModel(ssh_config_col))
self.ssh_config_combo.setModelColumn(
self.model.relationModel(ssh_config_col).fieldIndex("ssh_config_name")
)
The relationModel() method returns a QSqlTableModel representing the related table (e.g., sshgroup). By setting this as the combo box's model and pointing setModelColumn() to the display field, the combo box will show all available options from that related table.
Setting up the mapper
With the combo boxes properly configured, you can set up the QDataWidgetMapper. The QSqlRelationalDelegate is essential here — it tells the mapper how to handle the translation between foreign key IDs and the displayed relation values:
self.mapper = QDataWidgetMapper()
self.mapper.setModel(self.model)
self.mapper.setItemDelegate(QSqlRelationalDelegate(self))
self.mapper.addMapping(self.ssh_id, 0)
self.mapper.addMapping(self.ssh_group_combo, ssh_group_col)
self.mapper.addMapping(self.ssh_connection_name, 2)
# ... other mappings ...
self.mapper.addMapping(self.ssh_key_combo, ssh_key_col)
self.mapper.addMapping(self.ssh_config_combo, ssh_config_col)
self.mapper.toFirst()
The order matters
One thing to be aware of: the order in which you do things matters. Here's the sequence that works reliably:
- Create the
QSqlRelationalTableModeland set its table. - Set the relations on the appropriate columns.
- Call
select()on the model. - Set each combo box's model to the corresponding
relationModel(). - Create the
QDataWidgetMapperand add all mappings. - Navigate to a record with
toFirst().
If you try to get the relation models before calling select(), they may return None or be unpopulated.
Complete working example
Here's a full, self-contained example that creates an SQLite database, populates it with sample data, and displays a form with working combo boxes for all relational fields:
import sys
import os
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtSql import (
QSqlDatabase,
QSqlTableModel,
QSqlRelationalTableModel,
QSqlRelation,
QSqlRelationalDelegate,
QSqlQuery,
)
from PyQt5.QtWidgets import (
QApplication,
QComboBox,
QDataWidgetMapper,
QFormLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QMainWindow,
QPushButton,
QSpinBox,
QVBoxLayout,
QWidget,
)
DB_PATH = "wizardwebsshv2.db"
def create_database():
"""Create the database, tables, and insert sample data."""
db = QSqlDatabase.addDatabase("QSQLITE", "SSHCONFIG")
db.setDatabaseName(DB_PATH)
db.open()
query = QSqlQuery(db=db)
query.exec_(
"""CREATE TABLE IF NOT EXISTS sshgroup (
id INTEGER PRIMARY KEY,
ssh_group_name TEXT NOT NULL UNIQUE,
ssh_group_description TEXT
)"""
)
query.exec_(
"""CREATE TABLE IF NOT EXISTS sshkeys (
id INTEGER PRIMARY KEY,
sshkey_name VARCHAR(25) NOT NULL UNIQUE,
sshkey_passphrase TEXT,
sshkey_public_file TEXT,
sshkey_private_file TEXT
)"""
)
query.exec_(
"""CREATE TABLE IF NOT EXISTS sshconfig (
id INTEGER PRIMARY KEY,
ssh_config_name TEXT NOT NULL UNIQUE,
ssh_config_content TEXT
)"""
)
query.exec_(
"""CREATE TABLE IF NOT EXISTS sshconnections (
id INTEGER NOT NULL PRIMARY KEY,
ssh_group_id INTEGER DEFAULT 1,
ssh_connection_name VARCHAR(100) NOT NULL UNIQUE,
ssh_username VARCHAR(100),
ssh_password VARCHAR(100),
Host VARCHAR(100),
HostName VARCHAR(100),
Port INTEGER,
ProxyCommand TEXT,
ssh_key_id INTEGER DEFAULT 1,
ssh_config_id INTEGER DEFAULT 1,
FOREIGN KEY(ssh_group_id) REFERENCES sshgroup(id),
FOREIGN KEY(ssh_key_id) REFERENCES sshkeys(id),
FOREIGN KEY(ssh_config_id) REFERENCES sshconfig(id)
)"""
)
# Insert sample data (ignore errors if already exists)
query.exec_(
"INSERT OR IGNORE INTO sshgroup (id, ssh_group_name, ssh_group_description) "
"VALUES (1, 'default', 'Default SSH Group')"
)
query.exec_(
"INSERT OR IGNORE INTO sshgroup (id, ssh_group_name, ssh_group_description) "
"VALUES (2, 'production', 'Production Servers')"
)
query.exec_(
"INSERT OR IGNORE INTO sshgroup (id, ssh_group_name, ssh_group_description) "
"VALUES (3, 'staging', 'Staging Servers')"
)
query.exec_(
"INSERT OR IGNORE INTO sshkeys (id, sshkey_name) VALUES (1, 'None')"
)
query.exec_(
"INSERT OR IGNORE INTO sshkeys (id, sshkey_name) VALUES (2, 'my_deploy_key')"
)
query.exec_(
"INSERT OR IGNORE INTO sshkeys (id, sshkey_name) VALUES (3, 'personal_key')"
)
query.exec_(
"INSERT OR IGNORE INTO sshconfig (id, ssh_config_name, ssh_config_content) "
"VALUES (1, 'default', '')"
)
query.exec_(
"INSERT OR IGNORE INTO sshconfig (id, ssh_config_name, ssh_config_content) "
"VALUES (2, 'custom_config', 'ServerAliveInterval 60')"
)
query.exec_(
"INSERT OR IGNORE INTO sshconnections "
"(ssh_group_id, ssh_connection_name, ssh_username, ssh_password, "
"Host, HostName, Port, ProxyCommand, ssh_key_id, ssh_config_id) "
"VALUES (1, 'web_server', 'admin', 'secret', "
"'web', 'web.example.com', 22, '', 1, 1)"
)
query.exec_(
"INSERT OR IGNORE INTO sshconnections "
"(ssh_group_id, ssh_connection_name, ssh_username, ssh_password, "
"Host, HostName, Port, ProxyCommand, ssh_key_id, ssh_config_id) "
"VALUES (2, 'db_server', 'dbadmin', 'dbpass', "
"'db', 'db.example.com', 2222, '', 2, 2)"
)
return db
class MainWindow(QMainWindow):
def __init__(self, db):
super().__init__()
self.setWindowTitle("SSH Connection Manager")
self.setMinimumSize(QSize(450, 400))
self.db = db
# Create form widgets
self.ssh_id = QSpinBox()
self.ssh_id.setDisabled(True)
self.ssh_group_combo = QComboBox()
self.ssh_connection_name = QLineEdit()
self.ssh_username = QLineEdit()
self.ssh_password = QLineEdit()
self.ssh_password.setEchoMode(QLineEdit.Password)
self.ssh_host = QLineEdit()
self.ssh_hostname = QLineEdit()
self.ssh_port = QSpinBox()
self.ssh_port.setRange(1, 65535)
self.ssh_proxy_command = QLineEdit()
self.ssh_key_combo = QComboBox()
self.ssh_config_combo = QComboBox()
# Build the form layout
form = QFormLayout()
form.addRow(QLabel("ID"), self.ssh_id)
form.addRow(QLabel("Group"), self.ssh_group_combo)
form.addRow(QLabel("Connection Name"), self.ssh_connection_name)
form.addRow(QLabel("SSH Username"), self.ssh_username)
form.addRow(QLabel("SSH Password"), self.ssh_password)
form.addRow(QLabel("Host"), self.ssh_host)
form.addRow(QLabel("HostName"), self.ssh_hostname)
form.addRow(QLabel("Port"), self.ssh_port)
form.addRow(QLabel("Proxy Command"), self.ssh_proxy_command)
form.addRow(QLabel("SSH Key Name"), self.ssh_key_combo)
form.addRow(QLabel("SSH Config Name"), self.ssh_config_combo)
# --- Set up the relational model ---
self.model = QSqlRelationalTableModel(db=self.db)
self.model.setTable("sshconnections")
self.model.setEditStrategy(QSqlTableModel.OnManualSubmit)
# Get the column indices for the foreign key columns
ssh_group_col = self.model.fieldIndex("ssh_group_id")
ssh_key_col = self.model.fieldIndex("ssh_key_id")
ssh_config_col = self.model.fieldIndex("ssh_config_id")
# Define the relations
self.model.setRelation(
ssh_group_col,
QSqlRelation("sshgroup", "id", "ssh_group_name"),
)
self.model.setRelation(
ssh_key_col,
QSqlRelation("sshkeys", "id", "sshkey_name"),
)
self.model.setRelation(
ssh_config_col,
QSqlRelation("sshconfig", "id", "ssh_config_name"),
)
# Populate the model — this must happen before accessing relationModel()
self.model.select()
# --- Configure combo boxes with their relation models ---
# Each combo box gets the model for its own related table
group_relation_model = self.model.relationModel(ssh_group_col)
self.ssh_group_combo.setModel(group_relation_model)
self.ssh_group_combo.setModelColumn(
group_relation_model.fieldIndex("ssh_group_name")
)
key_relation_model = self.model.relationModel(ssh_key_col)
self.ssh_key_combo.setModel(key_relation_model)
self.ssh_key_combo.setModelColumn(
key_relation_model.fieldIndex("sshkey_name")
)
config_relation_model = self.model.relationModel(ssh_config_col)
self.ssh_config_combo.setModel(config_relation_model)
self.ssh_config_combo.setModelColumn(
config_relation_model.fieldIndex("ssh_config_name")
)
# --- Set up the data widget mapper ---
self.mapper = QDataWidgetMapper()
self.mapper.setModel(self.model)
self.mapper.setItemDelegate(QSqlRelationalDelegate(self))
self.mapper.addMapping(self.ssh_id, 0)
self.mapper.addMapping(self.ssh_group_combo, ssh_group_col)
self.mapper.addMapping(self.ssh_connection_name, 2)
self.mapper.addMapping(self.ssh_username, 3)
self.mapper.addMapping(self.ssh_password, 4)
self.mapper.addMapping(self.ssh_host, 5)
self.mapper.addMapping(self.ssh_hostname, 6)
self.mapper.addMapping(self.ssh_port, 7)
self.mapper.addMapping(self.ssh_proxy_command, 8)
self.mapper.addMapping(self.ssh_key_combo, ssh_key_col)
self.mapper.addMapping(self.ssh_config_combo, ssh_config_col)
self.mapper.toFirst()
# --- Navigation and action buttons ---
controls = QHBoxLayout()
prev_btn = QPushButton("Previous")
prev_btn.clicked.connect(self.mapper.toPrevious)
next_btn = QPushButton("Next")
next_btn.clicked.connect(self.mapper.toNext)
save_btn = QPushButton("Save Changes")
save_btn.clicked.connect(self.save_changes)
add_btn = QPushButton("Add")
add_btn.clicked.connect(self.add_record)
controls.addWidget(prev_btn)
controls.addWidget(next_btn)
controls.addWidget(save_btn)
controls.addWidget(add_btn)
# Status label to show current record
self.status_label = QLabel()
self.mapper.currentIndexChanged.connect(self.update_status)
self.update_status()
# Assemble the main layout
layout = QVBoxLayout()
layout.addLayout(form)
layout.addLayout(controls)
layout.addWidget(self.status_label)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
def update_status(self):
current = self.mapper.currentIndex() + 1
total = self.model.rowCount()
self.status_label.setText(f"Record {current} of {total}")
def save_changes(self):
self.mapper.submit()
if not self.model.submitAll():
print("Error saving:", self.model.lastError().text())
else:
print("Changes saved successfully.")
def add_record(self):
row = self.model.rowCount()
self.model.insertRow(row)
self.mapper.setCurrentIndex(row)
app = QApplication(sys.argv)
db = create_database()
window = MainWindow(db)
window.show()
app.exec_()
PyQt/PySide 1:1 Coaching with Martin Fitzpatrick
Save yourself time and frustration. Get one on one help with your Python GUI projects. Working together with you I'll identify issues and suggest fixes, from bugs and usability to architecture and maintainability.