通过 QSqlRelationalTableModel 中的 QSortFilterProxyModel 过滤时 QTreeView 上的无限循环
Endless loop on QTreeView when filtering via QSortFilterProxyModel in a QSqlRelationalTableModel
每当我使用 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
如何重现:
- 设置一个过滤器,例如
lastName
到 Smith
.
- 点击
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_())
问题出在 filterAcceptsRow
中对 self.columnCount()
的调用。 QSortFilterProxyModel 的 columnCount
需要当前代理模型的映射,这又会再次调用 filterAcceptsRow,使函数递归。
使用if 0 <= i < self.sourceModel().columnCount()
,问题解决
每当我使用 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
如何重现:
- 设置一个过滤器,例如
lastName
到Smith
. - 点击
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_())
问题出在 filterAcceptsRow
中对 self.columnCount()
的调用。 QSortFilterProxyModel 的 columnCount
需要当前代理模型的映射,这又会再次调用 filterAcceptsRow,使函数递归。
使用if 0 <= i < self.sourceModel().columnCount()
,问题解决