通过QSqlRelationalTableModel中的QSortFilterProxyModel进行过滤时,QTreeView上的无穷循环

时间:2019-07-10 14:10:00

标签: python pyqt5 qtreeview qsortfilterproxymodel qsqlrelationaltablemodel

每当我使用QSortFilterProxyModel()启用过滤器并在与QSqlRelationalTableModel()链接的QTreeView中插入一条新记录时,都会出现错误:

RecursionError: maximum recursion depth exceeded

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

也可以过滤-好的。

但是,如果我设置过滤器并创建新记录,则python将失败并显示:

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

如何复制:

  1. 设置一个过滤器,例如lastNameSmith
  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_())    

1 个答案:

答案 0 :(得分:0)

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

使用if 0 <= i < self.sourceModel().columnCount()即可解决问题。