首页 > 解决方案 > 从关系模型填充并连接到 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()

标签: pythonpyqt5qcomboboxqsqlrelationaltablemodelqdatawidgetmapper

解决方案


需要考虑的重要一点是项目委托(特别是 QSqlRelationalDelegate)使用小部件的用户属性从小部件读取数据和向小部件写入数据。

QComboBox 的用户属性是currentText; 如果它不可编辑,则其值为空字符串(用于 -1 索引)或当前项目的文本,设置该属性会导致组合尝试查找与该文本完全匹配的第一个项目,并在以下情况下更改当前索引找到匹配项。
但是,当组合可编辑时,仅更改文本,而不更改当前索引,也可以设置

现在,经过一番挖掘,我发现了您所面临问题的各种“罪魁祸首”。

QDataWidgetMapper 使用EditRole来提交数据和填充小部件。这显然代表了一个问题,因为编辑角色是关系模型用于模型上的实际数据集的角色(例如,“S”代表 Southern),而显示角色是用于显示相关值的角色。

以上所有方面的结果是,假设用户没有更改组合:

  1. 映射器尝试根据当前委托编辑器设置数据setModelData()
  2. 委托使用当前索引不是当前文本!)来获取要在模型上设置的显示和编辑角色;
  3. 该模型尝试设置这两个值,但由于其关系性质,只能设置编辑角色;
  4. 更改的数据会导致映射器重新填充小部件;
  5. 然后使用 将基于组合索引的显示值设置为小部件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)

一些进一步的说明和建议:

  1. 如果映射的数据是可见的,最好使用ManualSubmit策略 ( self.mapper.setSubmitPolicy(self.mapper.ManualSubmit)),或者,您可以对模型进行子类化并找到方法以可视方式显示修改的单元格,直到提交更改;
  2. 点击时不需要 lambda 更新当前索引,因为clicked已经提供了新索引:self.tableView.clicked.connect(self.mapper.setCurrentModelIndex)
  3. 提交模型将导致映射器重置当前索引,结果将忽略进一步的编辑(不从表中选择新项目),因此您应该在应用更改后恢复它:
    def submit(self):
        current = self.mapper.currentIndex()
        self.mapper.submit()
        self.model.submitAll()
        self.mapper.setCurrentIndex(current)

推荐阅读