【问题标题】:Endless loop on QTreeView when filtering via QSortFilterProxyModel in a QSqlRelationalTableModel通过 QSqlRelationalTableModel 中的 QSortFilterProxyModel 过滤时 QTreeView 上的无限循环
【发布时间】:2019-11-20 04:34:44
【问题描述】:

每当我使用QSortFilterProxyModel() 启用过滤器并在链接到QTreeView 的QSqlRelationalTableModel() 中插入新记录时,我都会收到错误消息:

RecursionError: maximum recursion depth exceeded

标准情况是使用CTRL + N 创建一个新的数据记录 - OK。

过滤也有效 - 好的。

但如果我设置过滤器并创建新记录,python 会失败:

RecursionError: maximum recursion depth exceeded
Backend terminated (returncode: 3)
Fatal Python error: Aborted

如何重现:

  1. 设置过滤器,例如lastName 到 Smith。
  2. 点击CTRL + N 创建新记录。

=> 结果:Python 陷入无限循环,直到出现提到的错误消息。

=> 预期结果:该行应该被创建并且不被过滤器命中。删除过滤器后,应显示所有行,以及新创建的行。

完整的工作代码示例:

import sys
import re
from PyQt5 import QtWidgets, QtGui, QtCore, QtSql

db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(":memory:");
modelQuery = QtSql.QSqlQueryModel()
modelTable = QtSql.QSqlRelationalTableModel()

def _human_key(key):
    parts = re.split(r'(\d*\.\d+|\d+)', key)
    return tuple((e.swapcase() if i % 2 == 0 else float(e))
            for i, e in enumerate(parts))

class FilterHeader(QtWidgets.QHeaderView):
    filterActivated = QtCore.pyqtSignal()

    def __init__(self, parent):
        super().__init__(QtCore.Qt.Horizontal, parent)
        self._editors = []
        self._padding = 4
        self.setStretchLastSection(True)        
        self.setDefaultAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
        self.setSortIndicatorShown(False)
        self.sectionResized.connect(self.adjustPositions)
        parent.horizontalScrollBar().valueChanged.connect(self.adjustPositions)

    def setFilterBoxes(self, count):
        while self._editors:
            editor = self._editors.pop()
            editor.deleteLater()
        for index in range(count):
            editor = QtWidgets.QLineEdit(self.parent())            
            editor.setPlaceholderText('Filter')
            editor.setClearButtonEnabled(True)            
            editor.textChanged.connect(self.textChanged)

            self._editors.append(editor)
        self.adjustPositions()

    def textChanged(self):        
        self.filterActivated.emit()

    def sizeHint(self):
        size = super().sizeHint()
        if self._editors:
            height = self._editors[0].sizeHint().height()
            size.setHeight(size.height() + height + self._padding)
        return size

    def updateGeometries(self):
        if self._editors:
            height = self._editors[0].sizeHint().height()
            self.setViewportMargins(0, 0, 0, height + self._padding)
        else:
            self.setViewportMargins(0, 0, 0, 0)
        super().updateGeometries()
        self.adjustPositions()

    def adjustPositions(self):
        for index, editor in enumerate(self._editors):
            height = editor.sizeHint().height()
            editor.move(
                self.sectionPosition(index) - self.offset() + 2,
                height + (self._padding // 2))
            editor.resize(self.sectionSize(index), height)

    def filterText(self, index):        
        if 0 <= index < len(self._editors):
            return self._editors[index].text()
        return ''

    def setFilterText(self, index, text):
        if 0 <= index < len(self._editors):
            self._editors[index].setText(text)

    def clearFilters(self):        
        for editor in self._editors:
            editor.clear()


class HumanProxyModel(QtCore.QSortFilterProxyModel):
    def lessThan(self, source_left, source_right):
        data_left = source_left.data()
        data_right = source_right.data()
        if type(data_left) == type(data_right) == str:
            return _human_key(data_left) < _human_key(data_right)
        return super(HumanProxyModel, self).lessThan(source_left, source_right)

    @property
    def filters(self):        
        if not hasattr(self, "_filters"):
            self._filters = []        
        return self._filters

    @filters.setter
    def filters(self, filters):
        self._filters = filters
        self.invalidateFilter()

    def filterAcceptsRow(self, sourceRow, sourceParent):            
        for i, text in self.filters:
            if 0 <= i < self.columnCount():            
                ix = self.sourceModel().index(sourceRow, i, sourceParent)
                data = ix.data()
                if text not in data:                    
                    return False        
        return True                

class winMain(QtWidgets.QMainWindow):
    cur_row = -1
    row_id = -1

    def __init__(self, parent=None):        
        super().__init__(parent)                
        self.setupUi()
        self.setGeometry(300,200,700,500)

        self.treeView.selectionModel().selectionChanged.connect(self.item_selection_changed_slot)        
        self.center()
        self.show()                

    def new_dataset(self):
        print("new_dataset() called.")            

        # get new row
        row = modelTable.rowCount()
        new_row = row+1
        self.cur_row = new_row        

        # get next free row id
        model = QtSql.QSqlQueryModel()
        model.setQuery("SELECT max(id)+1 FROM person")
        self.row_id = model.data(model.index(0, 0))        

        # insert a new row with dummy data
        modelTable.insertRow(row)
        modelTable.setData(modelTable.index(row,0), self.row_id, QtCore.Qt.EditRole)
        modelTable.setData(modelTable.index(row,1), "new" + str(self.row_id), QtCore.Qt.EditRole)
        modelTable.setData(modelTable.index(row,2), "new" + str(self.row_id), QtCore.Qt.EditRole)
        modelTable.setData(modelTable.index(row,3), "new" + str(self.row_id), QtCore.Qt.EditRole)
        modelTable.setData(modelTable.index(row,4), 2, QtCore.Qt.EditRole)

        modelTable.submitAll()        

    def handleFilterActivated(self):        
        header = self.treeView.header()
        filters = []
        for i in range(header.count()):
            text = header.filterText(i)
            if text:
                filters.append((i, text))
        proxy = self.treeView.model()        
        proxy.filters = filters

    QtCore.pyqtSlot()
    def item_selection_changed_slot(self):
        selected = self.treeView.selectionModel()
        indexes = selected.selectedIndexes()        

        sourceIdx = self.treeView.currentIndex()
        ix = self.treeView.model().index(sourceIdx.row(), 0)  # column which contains the id

        self.cur_row = sourceIdx.row()
        self.row_id = ix.data()

        record = modelTable.record(self.cur_row)

        persId = record.value("persId")
        lastName = record.value("lastName")
        firstName = record.value("firstName")
        country = record.value("name")
        print(f"{persId} - {lastName}, {firstName} from {country} selected.")

    def keyReleaseEvent(self, eventQKeyEvent):                
        key = eventQKeyEvent.key()
        modifiers = QtWidgets.QApplication.keyboardModifiers()
        if modifiers == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_Escape:            
                self.clear_all_filters()


    def center(self):
        frameGm = self.frameGeometry()
        screen = QtWidgets.QApplication.desktop().screenNumber(QtWidgets.QApplication.desktop().cursor().pos())
        centerPoint = QtWidgets.QApplication.desktop().screenGeometry(screen).center()
        frameGm.moveCenter(centerPoint)
        self.move(frameGm.topLeft())

    def setupUi(self):
        self.centralwidget = QtWidgets.QWidget(self)
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)

        self.treeView = QtWidgets.QTreeView(self.centralwidget)      

        self.treeView.setRootIsDecorated(False)                      
        self.treeView.setSortingEnabled(True)
        self.treeView.setAlternatingRowColors(True)        
        self.treeView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.treeView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.treeView.header().setStretchLastSection(True)           

        self.horizontalLayout.addWidget(self.treeView)
        self.setCentralWidget(self.centralwidget)

        header = FilterHeader(self.treeView)        
        self.treeView.setHeader(header)         

        # ToolBar        
        newDatasetAct = QtWidgets.QAction(QtGui.QIcon('img/icons8-new-file-50.png'), 'New dataset (CTRL+N)', self)
        newDatasetAct.setShortcut('Ctrl+N')
        newDatasetAct.triggered.connect(self.new_dataset)

        self.toolbar = self.addToolBar('Main')        
        self.toolbar.addAction(newDatasetAct)

        modelTable.setTable("person")

        modelTable.setRelation(4, QtSql.QSqlRelation("country", "id", "name"));

        modelTable.setEditStrategy(QtSql.QSqlTableModel.OnManualSubmit)

        self.treeView.setModel(modelTable) # display data of the SQLTableModel into the QTreeView      

        # enable human sorting                
        proxy = HumanProxyModel(self)
        proxy.setSourceModel(modelTable)
        self.treeView.setModel(proxy)

        # enable filtering
        header.setFilterBoxes(modelTable.columnCount())
        header.filterActivated.connect(self.handleFilterActivated)        

def create_sample_data():     
    modelQuery.setQuery("""CREATE TABLE IF NOT EXISTS country (                                    
                                    id   INTEGER PRIMARY KEY UNIQUE NOT NULL,
                                    name TEXT
                                    )""")

    # id         INTEGER PRIMARY KEY UNIQUE,
    modelQuery.setQuery("""CREATE TABLE IF NOT EXISTS person (
                                   id         INTEGER PRIMARY KEY UNIQUE NOT NULL,
                                   persId     TEXT,
                                   lastName   TEXT,
                                   firstName  TEXT,
                                   country_id INTEGER NOT NULL DEFAULT 3,
              FOREIGN KEY (country_id) REFERENCES country(id)
                                   )""")

    # create some sample data for our model
    modelQuery.setQuery("INSERT INTO country (id, name) VALUES (0, 'None')")    
    modelQuery.setQuery("INSERT INTO country (id, name) VALUES (1, 'Angola')")    
    modelQuery.setQuery("INSERT INTO country (id, name) VALUES (2, 'Serbia')")
    modelQuery.setQuery("INSERT INTO country (id, name) VALUES (3, 'Georgia')")

    modelQuery.setQuery("INSERT INTO person (id, persId, lastName, firstName, country_id) VALUES (1, '1001', 'Martin', 'Robert', 1)")
    modelQuery.setQuery("INSERT INTO person (id, persId, lastName, firstName, country_id) VALUES (2, '1002', 'Smith', 'Brad', 2)")
    modelQuery.setQuery("INSERT INTO person (id, persId, lastName, firstName, country_id) VALUES (3, '1003', 'Smith', 'Angelina', 3)")

if __name__ == '__main__':                         
    app = QtWidgets.QApplication(sys.argv)             

    create_sample_data()

    window = winMain()    
    sys.exit(app.exec_())    

【问题讨论】:

    标签: python pyqt5 qtreeview qsortfilterproxymodel qsqlrelationaltablemodel


    【解决方案1】:

    问题来自在filterAcceptsRow 中对self.columnCount() 的调用。 QSortFilterProxyModel 的columnCount 需要当前代理模型的映射,然后再次调用 filterAcceptsRow,使函数递归。

    使用if 0 &lt;= i &lt; self.sourceModel().columnCount(),问题就解决了。

    【讨论】:

    • 就是这样!精彩的!太感谢了!你现在知道我花了多少时间在这上面!
    • 也许你也有这个想法? stackoverflow.com/questions/56902372/…
    • @ProfP30 不客气,是的,我知道在您完全了解模型的工作原理之前,跟踪这些问题可能有多么困难(提示:无论何时查找source code不明白 Qt 在做什么)。关于另一个问题,很抱歉,除非您能够提供不需要 peewee 的示例,否则我将无法测试它,因为在我当前的设置中我无法安装它。
    • 我现在重新编写了代码,使其独立于 peewee 模块。请参阅此处之前发布的代码示例:stackoverflow.com/questions/56902372/…
    猜你喜欢
    • 2015-06-16
    • 1970-01-01
    • 2015-11-17
    • 2018-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-13
    • 2021-03-25
    相关资源
    最近更新 更多