python - 从关系模型填充并连接到 QDataWidgetMapper 的 QComboBox 在 setEditable(True) 时行为异常
问题描述
我有一个从 QSqlRelationalTableModel 的关系模型填充并连接到 QDataWidgetMapper 的 QComboBox。
我在 QTableView 中选择一行,该行(记录)映射到 QLineEdit 和 QComboBox 小部件,然后我进行一些更改并保存。
如果我选择另一行并保存而不更改 QComboBox 值,则该值会更改并提交给模型。
我使用可编辑组合框不是为了将项目添加到列表中,而是在我有一个大列表而不是下拉组合框视图时使用完成功能
创建数据库:
import sqlite3
conn = sqlite3.connect('customers.db')
c = conn.cursor()
c.execute("PRAGMA foreign_keys=on;")
c.execute("""CREATE TABLE IF NOT EXISTS provinces (
ProvinceId TEXT PRIMARY KEY,
Name TEXT NOT NULL
)""")
c.execute("""CREATE TABLE IF NOT EXISTS customers (
CustomerId TEXT PRIMARY KEY,
Name TEXT NOT NULL,
ProvinceId TEXT,
FOREIGN KEY (ProvinceId) REFERENCES provinces (ProvinceId)
ON UPDATE CASCADE
ON DELETE RESTRICT
)""")
c.execute("INSERT INTO provinces VALUES ('N', 'Northern')")
c.execute("INSERT INTO provinces VALUES ('E', 'Eastern')")
c.execute("INSERT INTO provinces VALUES ('W', 'Western')")
c.execute("INSERT INTO provinces VALUES ('S', 'Southern')")
c.execute("INSERT INTO provinces VALUES ('C', 'Central')")
c.execute("INSERT INTO customers VALUES ('1', 'customer1', 'N')")
c.execute("INSERT INTO customers VALUES ('2', 'customer2', 'E')")
c.execute("INSERT INTO customers VALUES ('3', 'customer3', 'W')")
c.execute("INSERT INTO customers VALUES ('4', 'customer4', 'S')")
c.execute("INSERT INTO customers VALUES ('5', 'customer5', 'C')")
conn.commit()
conn.close()
这是窗口:
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
class Window(QWidget):
def __init__(self):
super().__init__()
self.db = QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("customers.db")
self.db.open()
self.model = QSqlRelationalTableModel(self, self.db)
self.model.setTable("customers")
self.model.setRelation(2, QSqlRelation("provinces", "ProvinceId", "Name"))
self.model.setEditStrategy(QSqlTableModel.EditStrategy.OnManualSubmit)
self.model.select()
self.id = QLineEdit()
self.name = QLineEdit()
self.province = QComboBox()
# stuck here
self.province.setEditable(True)
self.province.setModel(self.model.relationModel(2))
self.province.setModelColumn(1)
self.province.setView(QTableView())
self.mapper = QDataWidgetMapper()
self.mapper.setItemDelegate(QSqlRelationalDelegate())
self.mapper.setModel(self.model)
self.mapper.addMapping(self.id, 0)
self.mapper.addMapping(self.name, 1)
self.mapper.addMapping(self.province, 2)
save = QPushButton("Save")
save.clicked.connect(self.submit)
self.tableView = QTableView()
self.tableView.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.tableView.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
self.tableView.setModel(self.model)
self.tableView.clicked.connect(lambda: self.mapper.setCurrentModelIndex(self.tableView.currentIndex()))
vBox = QVBoxLayout()
vBox.addWidget(self.id)
vBox.addWidget(self.name)
vBox.addWidget(self.province)
vBox.addSpacing(20)
vBox.addWidget(save)
vBox.addWidget(self.tableView)
self.setLayout(vBox)
self.mapper.toFirst()
def submit(self):
self.mapper.submit()
self.model.submitAll()
def main():
import sys
App = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(App.exec_())
if __name__ == '__main__':
main()
解决方案
需要考虑的重要一点是项目委托(特别是 QSqlRelationalDelegate)使用小部件的用户属性从小部件读取数据和向小部件写入数据。
QComboBox 的用户属性是currentText
; 如果它不可编辑,则其值为空字符串(用于 -1 索引)或当前项目的文本,设置该属性会导致组合尝试查找与该文本完全匹配的第一个项目,并在以下情况下更改当前索引找到匹配项。
但是,当组合可编辑时,仅更改文本,而不更改当前索引,也可以设置
现在,经过一番挖掘,我发现了您所面临问题的各种“罪魁祸首”。
QDataWidgetMapper 使用EditRole
来提交数据和填充小部件。这显然代表了一个问题,因为编辑角色是关系模型用于模型上的实际数据集的角色(例如,“S”代表 Southern),而显示角色是用于显示相关值的角色。
以上所有方面的结果是,假设用户没有更改组合:
- 映射器尝试根据当前委托编辑器设置数据
setModelData()
; - 委托使用当前索引(不是当前文本!)来获取要在模型上设置的显示和编辑角色;
- 该模型尝试设置这两个值,但由于其关系性质,只能设置编辑角色;
- 更改的数据会导致映射器重新填充小部件;
- 然后使用 将基于组合索引的显示值设置为小部件
setEditorData()
;
另请注意,在 Qt 5.12 之前(请参阅QTBUG-59632setEditorData
),由于默认实现使用编辑角色,上述导致进一步的问题,因此可编辑组合也将获得相关字母而不是实际值显示。
考虑到上述情况,有两种选择:
- 子类 QSqlRelationalDelegate 并
setModelData()
通过匹配当前文本并使用关系模型正确实现
from PyQt5.QtCore import *
_version = tuple(map(int, QT_VERSION_STR.split('.')))
class Delegate(QSqlRelationalDelegate):
def setModelData(self, editor, model, index):
if isinstance(editor, QComboBox):
value = editor.currentText()
if not value:
return
childModel = model.relationModel(index.column())
for column in range(2):
match = childModel.match(childModel.index(0, column),
Qt.DisplayRole, value, Qt.MatchStartsWith)
if match:
match = match[0]
displayValue = match.sibling(match.row(), 1).data()
editValue = match.sibling(match.row(), 0).data()
model.setData(index, displayValue, Qt.DisplayRole)
model.setData(index, editValue, Qt.EditRole)
return
super().setModelData(editor, model, index)
if _version[1] < 12:
# fix for old Qt versions that don't properly update the QComboBox
def setEditorData(self, editor, index):
if isinstance(editor, QComboBox):
value = index.data()
if isinstance(value, str):
propName = editor.metaObject().userProperty().name()
editor.setProperty(propName, value)
else:
super().setEditorData(editor, index)
- 子类 QComboBox 并确保它使用新的用户属性正确地使用当前文本更新索引;这仍然需要实现
setModelData
以覆盖 QComboBox 的默认行为
class MapperCombo(QComboBox):
@pyqtProperty(str, user=True)
def mapperText(self):
text = self.currentText()
if text == self.currentData(Qt.DisplayRole):
return text
model = self.model()
for column in range(2):
match = model.match(model.index(0, column),
Qt.DisplayRole, text, Qt.MatchStartsWith)
if match:
self.setCurrentIndex(match[0].row())
return self.currentText()
return self.itemText(self.currentIndex())
@mapperText.setter
def mapperText(self, text):
model = self.model()
for column in range(2):
match = model.match(model.index(0, column),
Qt.DisplayRole, text, Qt.MatchStartsWith)
if match:
index = match[0].row()
break
else:
index = 0
if index != self.currentIndex():
self.setCurrentIndex(index)
else:
self.setCurrentText(self.currentData(Qt.DisplayRole))
@property
def mapperValue(self):
return self.model().data(self.model().index(
self.currentIndex(), 0), Qt.DisplayRole)
class Delegate(QSqlRelationalDelegate):
def setModelData(self, editor, model, index):
if isinstance(editor, MapperCombo):
model.setData(index, editor.mapperText, Qt.DisplayRole)
model.setData(index, editor.mapperValue, Qt.EditRole)
else:
super().setModelData(editor, model, index)
最后,可以使用带有正确 QCompleter 的 QLineEdit,但这仍然需要对委托进行子类化,因为setModelData
需要使用正确的字符串。
class Delegate(QSqlRelationalDelegate):
def setModelData(self, editor, model, index):
if model.relation(index.column()).isValid():
value = editor.text()
if value:
childModel = model.relationModel(index.column())
match = childModel.match(childModel.index(0, 1),
Qt.DisplayRole, value, Qt.MatchStartsWith)
if match:
childIndex = match[0]
model.setData(index, childIndex.data(), Qt.DisplayRole)
model.setData(index,
childIndex.sibling(childIndex.row(), 0).data(), Qt.EditRole)
editor.setText(childIndex.data())
else:
super().setModelData(editor, model, index)
一些进一步的说明和建议:
- 如果映射的数据是可见的,最好使用
ManualSubmit
策略 (self.mapper.setSubmitPolicy(self.mapper.ManualSubmit)
),或者,您可以对模型进行子类化并找到方法以可视方式显示修改的单元格,直到提交更改; - 点击时不需要 lambda 更新当前索引,因为
clicked
已经提供了新索引:self.tableView.clicked.connect(self.mapper.setCurrentModelIndex)
- 提交模型将导致映射器重置当前索引,结果将忽略进一步的编辑(不从表中选择新项目),因此您应该在应用更改后恢复它:
def submit(self):
current = self.mapper.currentIndex()
self.mapper.submit()
self.model.submitAll()
self.mapper.setCurrentIndex(current)
推荐阅读
- reactjs - 为自动完成 Ant Design 添加去抖动的示例
- twilio - Twilio 发送错误请求
- forms - 如何通过相机元素拍照并将其正确附加到表单中?
- c++ - 使用 C++ 在 Mac 上读取/写入 DOCX 文件 (OpenXML)
- javascript - React Native Paper 按钮图标大小
- php - 如何使用 PHP 获取自动生成的 postgres id 的值?
- oracle - 数据库序列——Oracle Golden Gate 双向复制
- apache-kafka - 查看和操作kafka存储数据的快速方法?#isoBlue #isoBus
- excel - 将公式复制到动态列(无标题)
- ruby-on-rails - 如何按rails中两个变量的降序排序?