In this working example, I have a SQL Lite db ("in memory"). The QtableView
is setup on a QSortFilterProxyModel
with one of the column, "product" which can be filtered via user selection through a QComboBox
.
If the values in the "product" column of the table is edited, then the items of the QComboBox
are updated using pyqtSignal
. This ensures that the Combobox items are always updated to the unique contents of the product column.
The app works fine as long as the user edits values in the table when no filter selection is made with the dropdown. As soon as you filter a product in using the combobox and alter its name in the tableview the program hangs and crashes without an error traceback.
I think it may be something to do with dataChanged
signal that the proxymodel is emitting . This may be leading to a recursion issue. Tried self.setUpdatesEnabled(False)
, blockSignals
on proxmodel, delaying its update signal qWait
etc. but the issue was not resolved
import sys
import re
from functools import partial
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QComboBox, QTableView, QVBoxLayout, QWidget
from PyQt5.QtSql import QSqlDatabase, QSqlTableModel
from PyQt5.QtCore import Qt, QSortFilterProxyModel
from PyQt5.QtTest import QTest
class AppDemo(QMainWindow):
# Custom Signals
UpdateComboList = QtCore.pyqtSignal()
UpdateComboListFlag = QtCore.pyqtSignal()
UpdateFilterList = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
# Set up the main widget
self.main_widget = QWidget()
self.setCentralWidget(self.main_widget)
self.layout = QVBoxLayout()
self.main_widget.setLayout(self.layout)
# Create and set up the QComboBox
self.combo_box = QComboBox()
self.layout.addWidget(self.combo_box)
# Set up the QTableView
self.table_view = QTableView()
self.layout.addWidget(self.table_view)
# Set up the database
self.setup_database()
# Set the model for the QTableView
self.model = QSqlTableModel()
self.model.setTable("MainTable")
self.model.select()
# Model Sorting
proxymodel = MultiFilterProxyModel()
proxymodel.setSourceModel(self.model)
proxymodel.setDynamicSortFilter(True)
self.table_view.setModel(proxymodel)
self.table_view.setSortingEnabled(True)
self.setFixedWidth(1000)
self.setFixedHeight(300)
# Get name of headers
header_name = []
for x in range(proxymodel.columnCount()):
header_name.append(proxymodel.headerData(x, Qt.Horizontal))
# Setup items for ComboBox
header_elem = "product"
rows = proxymodel.rowCount()
col_indx = header_name.index(header_elem)
region_list = self.row_items(rows, col_indx)
region_list = list(dict.fromkeys(region_list)) # Remove duplicates
self.combo_box.addItems(region_list)
self.combo_box.currentIndexChanged[str].connect(
partial(self.filter_table_view, col_indx, proxymodel, header_elem)
)
self.model.dataChanged.connect(
partial(self.update_combo_items, header_name, proxymodel, "product")
)
def setup_database(self):
# Connect to an in-memory SQLite database
self.db = QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName(":memory:")
if not self.db.open():
print("Unable to open data source")
query = self.db.exec()
# Create table
query.exec(
"""
CREATE TABLE MainTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
age INTEGER,
product TEXT,
price REAL
)
"""
)
# Insert sample data into the table
query.exec("INSERT INTO MainTable (name, age, product, price) VALUES ('John Doe', 30, 'Product1', 9.99)")
query.exec("INSERT INTO MainTable (name, age, product, price) VALUES ('Jane Smith', 28, 'Product2', 19.99)")
query.exec("INSERT INTO MainTable (name, age, product, price) VALUES ('Dave Ryan', 55, 'Product1', 11.0)")
def row_items(self, rows, col_indx):
rows = self.model.rowCount() # Don't depend on proxy model
cb_field_list = []
for i in range(rows):
value = self.model.index(i, col_indx)
cb_field_list.append(value.data())
# Detect empty elements and replace multiple with a single one
# Remove empty elements
if "" in cb_field_list:
empty_elems = True
else:
empty_elems = False
cb_field_list = list(filter(None, cb_field_list))
if empty_elems:
cb_field_list.append("")
# Insert additional element
cb_field_list.append("All")
return cb_field_list
def filter_table_view(self, col_indx, proxymodel, header_elem):
#rows = proxymodel.rowCount()
# QTest.qWait(500)
if header_elem == "product":
cbbx_item = self.combo_box.currentText()
if cbbx_item == "All" or cbbx_item == "":
proxymodel.setFilterByColumn(col_indx, "")
else:
proxymodel.setFilterByColumn(col_indx, cbbx_item)
self.UpdateComboList.emit()
def update_combo_items(self, header_name, proxymodel, header_elem, indx1, indx2):
# rows = proxymodel.rowCount()
rows = self.model.rowCount() # Don't depend on proxy model
col_indx =indx2.column()
region_list = self.row_items(rows, col_indx)
region_list = list(dict.fromkeys(region_list)) # Remove duplicates
if header_name[col_indx] == "product":
cbbx_item = self.combo_box.currentText()
self.combo_box.blockSignals(True)
self.combo_box.clear()
self.combo_box.addItems(region_list)
self.combo_box.setCurrentText(cbbx_item)
self.combo_box.blockSignals(False)
class MultiFilterMode:
AND = 0
OR = 1
class MultiFilterProxyModel(QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
QSortFilterProxyModel.__init__(self, *args, **kwargs)
self.column_filters = {}
self.filters = {}
self.multi_filter_mode = MultiFilterMode.AND
def setFilterByColumn(self, column, regex):
if isinstance(regex, str):
regex = re.compile(regex)
self.filters[column] = regex
self.invalidateFilter()
def clearFilter(
self,
column,
):
del self.filters[column]
self.invalidateFilter()
def clearFilters(self):
self.filters = {}
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
if not self.filters:
return True
results = []
for key, regex in self.filters.items():
text = ""
index = self.sourceModel().index(source_row, key, source_parent)
if index.isValid():
text = self.sourceModel().data(index, Qt.DisplayRole)
if text is None:
text = ""
results.append(regex.match(text))
if self.multi_filter_mode == MultiFilterMode.OR:
return any(results)
return all(results)
if __name__ == "__main__":
app = QApplication(sys.argv)
demo = AppDemo()
demo.show()
sys.exit(app.exec_())
The problem is most probably caused by setDynamicSortFilter(True)
:
Note that you should not update the source model through the proxy model when dynamicSortFilter is true.
While the documentation doesn't completely address the cause of the problem (nor it mentions risking freeze/crash), the reason is probably based on the dynamic sorting/filtering that would invalidate the index that is being edited before the action is complete on the Qt side, including delegate and view handling of what should happen after the editing.
Since the index should be maintained valid throughout the whole process until it's finished, the intermediate invalidation probably causes some memory address problems.
One possibility is to temporarily disable the filter by overriding setData()
on the proxy:
class MultiFilterProxyModel(QSortFilterProxyModel):
...
def setData(self, index, value, role=Qt.EditRole):
isDynamic = self.dynamicSortFilter()
if isDynamic:
self.setDynamicSortFilter(False)
res = super().setData(index, value, role)
if isDynamic:
self.setDynamicSortFilter(True)
return res
I'm not 100% sure of the above, though. In case you see further problems, you may consider using an internal QTimer that resets the filter after returning setData()
:
class MultiFilterProxyModel(QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.column_filters = {}
self.filters = {}
self.multi_filter_mode = MultiFilterMode.AND
self.dynamicTimer = QTimer(self, singleShot=True,
interval=0, timeout=self.resetDynamic)
def resetDynamic(self):
self.setDynamicSortFilter(True)
def setData(self, index, value, role=Qt.EditRole):
if self.dynamicSortFilter():
self.setDynamicSortFilter(False)
self.dynamicTimer.start()
return super().setData(index, value, role)