【问题标题】:QComboBox populated from a relation model and connected to a QDataWidgetMapperbehaves strangely when setEditable(True)从关系模型填充并连接到 QDataWidgetMapper 的 QComboBox 在 setEditable(True) 时行为异常
【发布时间】:2021-10-08 19:55:15
【问题描述】:

我有一个 QComboBox 从 QSqlRelationalTableModel 的关系模型填充并连接到 QDataWidgetMapper。

我在 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()

【问题讨论】:

  • 你能不能解释清楚,有什么问题?您可以详细说明重现错误的过程,指出您希望获得的内容也很有用。可编辑的 QComboBox 的用途是什么?据了解,处于可编辑模式的 QComboBox 用于将项目添加到模型(在本例中为数据库),它这样做了,但问题(至少在查看数据库时)是没有创建有效的“provinceId”
  • @eyllanesc 我用更多细节更新帖子以产生问题。这个可重现的示例并不完全是我正在实施的,但它完全重现了我面临的问题。
  • 我认为使用 QLineEdit + QCompleter 而不是可编辑的 QComboBox 更好,但我有一个问题:假设用户没有写整个单词,例如“Eas”或写一个不相关的单词例如“xxx”,当按下保存按钮时会发生什么?是否应该保存更改?
  • @eyllanesc 如果输入的单词不在列表中,则不应保存。
  • @embabi 你有什么版本的Qt?是否早于 5.12?

标签: python pyqt5 qcombobox qsqlrelationaltablemodel qdatawidgetmapper


【解决方案1】:

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

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

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

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

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

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

还要注意,在 Qt 5.12 之前(请参阅 QTBUG-59632),由于 setEditorData 的默认实现使用编辑角色,上述导致进一步的问题,因此可编辑组合也将获得相关字母而不是实际价值展示。

考虑到上述情况,有两种选择:

  • 继承 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 并通过使用 new 用户属性确保它使用当前文本正确更新索引;这仍然需要实现 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 需要使用 proper 字符串。

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)

【讨论】:

    猜你喜欢
    • 2012-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-04
    • 1970-01-01
    • 2016-11-27
    • 2021-07-30
    • 2023-03-08
    相关资源
    最近更新 更多