我正在开发一个 PySide6/Python3 应用程序,其中包含一个带有自定义模型的 QTableView
DataFrameTableModel
。我想支持过滤渲染的表格。因此,我另外使用了 QSortFilterProxyModel
。
一个要求是根据具有不同运算符的列进行过滤,例如过滤第 x 列中值 >= 5 的所有行。为了表示过滤器,我实现了类
DataFrameFilter
,它基本上只存储类似 {column: 'Price', operator: 'eq', value: 12}
的内容。为了应用自定义过滤器格式,我创建了一个继承自 DataFrameSortFilterProxyModel
的类 QSortFilterProxyModel
。
from enum import Enum
from PySide6.QtCore import QAbstractTableModel, QSortFilterProxyModel, QModelIndex, Qt
import pandas as pd
class DataFrameTableModel(QAbstractTableModel):
def __init__(self, df: pd.DataFrame = None):
super(DataFrameTableModel, self).__init__()
self._df: pd.DataFrame = df
def rowCount(self, parent: QModelIndex = ...) -> int:
if parent.isValid() or self._df is None:
return 0
return self._df.shape[0]
def columnCount(self, parent: QModelIndex = ...) -> int:
if parent.isValid() or self._df is None:
return 0
return self._df.shape[1]
def data(self, index: QModelIndex, role: int = ...) -> object:
if index.isValid() and self._df is not None:
value = self._df.iloc[index.row(), index.column()]
if role == Qt.ItemDataRole.DisplayRole:
return str(value)
elif role == Qt.ItemDataRole.UserRole:
return value
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> object:
if self._df is not None:
if role == Qt.ItemDataRole.DisplayRole:
if orientation == Qt.Orientation.Horizontal:
return str(self._df.columns[section])
else:
return str(self._df.index[section])
elif role == Qt.ItemDataRole.UserRole:
if orientation == Qt.Orientation.Horizontal:
return self._df.columns[section]
else:
return self._df.index[section]
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
@property
def df(self) -> pd.DataFrame:
return self._df
@df.setter
def df(self, value: pd.DataFrame):
self._df = value
self.layoutChanged.emit()
class DataFrameFilterOperation(Enum):
EQUAL = "eq"
NOT_EQUAL = "ne"
GREATER_THAN = "gt"
GREATER_THAN_OR_EQUAL = "ge"
LESS_THAN = "lt"
LESS_THAN_OR_EQUAL = "le"
class DataFrameFilter:
def __init__(self, column: str, column_index: int, operation: DataFrameFilterOperation, value):
self._column = column
self._column_index = column_index
self._operation = operation
self._value = value
@property
def column(self) -> str:
return self._column
@property
def column_index(self) -> int:
return self._column_index
@property
def operation(self) -> DataFrameFilterOperation:
return self._operation
@property
def value(self):
return self._value
def __eq__(self, value: object) -> bool:
if not isinstance(value, DataFrameFilter):
return False
return self._column == value.column and self._column_index == value.column_index and self._operation == value.operation and self._value == value.value
def __ne__(self, __value: object) -> bool:
return not self.__eq__(__value)
class DataFrameSortFilterProxyModel(QSortFilterProxyModel):
def __init__(self):
super(DataFrameSortFilterProxyModel, self).__init__()
self._filters = []
def filterAcceptsRow(self, source_row: int, source_parent: QModelIndex) -> bool:
for filter in self._filters:
value = self.sourceModel().index(source_row, filter.column_index, source_parent).data(Qt.ItemDataRole.UserRole)
if filter.operation == DataFrameFilterOperation.EQUAL:
return value == filter.value
elif filter.operation == DataFrameFilterOperation.NOT_EQUAL:
return value != filter.value
elif filter.operation == DataFrameFilterOperation.GREATER_THAN:
return value > filter.value
elif filter.operation == DataFrameFilterOperation.GREATER_THAN_OR_EQUAL:
return value >= filter.value
elif filter.operation == DataFrameFilterOperation.LESS_THAN:
return value < filter.value
elif filter.operation == DataFrameFilterOperation.LESS_THAN_OR_EQUAL:
return value <= filter.value
return True
def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool:
left_value = left.data(Qt.ItemDataRole.UserRole)
right_value = right.data(Qt.ItemDataRole.UserRole)
return left_value < right_value
def add_filter(self, filter: DataFrameFilter):
self._filters.append(filter)
self.invalidateFilter()
def remove_filter(self, filter: DataFrameFilter):
self._filters.remove(filter)
self.invalidateFilter()
def clear_filters(self):
self._filters.clear()
self.invalidateFilter()
问题:基本上,对于小数据集来说,一切都非常有效。问题是,对于较大的数据集(约 60000 行),过滤显然需要很长时间,以至于 GUI 会冻结几秒钟。我考虑过将过滤逻辑移至第二个线程 (
QThread
),但 UI 只能在 GUI 线程中操作,并且由于编辑模型也会更改 UI,因此我无法从第二个线程调整模型。
如果过滤需要几秒钟,这不是问题,只是 UI 在此期间不应该冻结,以便您可以显示进度条或类似的内容。有什么建议或解决方案吗?
在 Qt 中,主线程仅为 GUI 保留。正如您之前提到的,对于小数据集,冻结不会发生,这是因为它运行得太快,您看不到冻结本身,但同样的事情发生了。您只会注意到数据集很大,因此需要更多时间来计算它。您唯一且正确的出路是将计算移动到不同的线程。
您实际上所做的是创建一个 Worker 类,您将在该类中实例化执行计算工作的类。 我会模仿你们的工人阶级和GUI。
假设您执行计算的类是 DataFrameSortFilterProxyModel 并且您不想触摸或更改该类的实际继承
class DataFrameSortFilterProxyModel():
progress = QtCore.Signal(int)
finished = QtCore.Signal()
def filtering(self):
datasetLength = 1000000000
for i in range(datasetLength):
# do the actual work
emit progress(i/datasetLength)
emit finished()
现在在你的工人类中实例化你的类:
class Worker(QObject):
finished = QtCore.Signal()
progress = QtCore.Signal(int)
def __init__(self):
self.mySortingModel = DataFrameSortFilterProxyModel()
self.mySortingModel.progress.connect(self.updateProgress)
self.mySortingModel.finished.connect(self.onFinished)
self.mySortingMode.filtering()
def updateProgress(self, numProgress):
emit progress(numProgress)
def onFinished(self):
emit finished()
现在让我们回到你的 UI 类,你说它应该有某种进度条,为了简单起见,我将使用 QLabel,但你可以自由使用 QProgressBar 或者可能是带有一些 GIF 动画的小部件:
from PySide2.QtCore import QThread
...
...
self.progressText = QLabel("0%")
...
def calculate():
# instantiate different thread
self.thread = QThread()
# create a worker
self.worker = Worker()
# move worker to the thread
self.worker.moveToThread(self.thread)
# connect all signals and slots
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.worker.progress.connect(self.updateProgress)
# start the thread so the actual work is started
self.thread.start()
# if you don't want to calculations be bullied by user you can disable action that leads to calculations for example if you have button you would say
self.calculateBtn.setEnabled(False)
#connect signals what happens in between and after the calcs are over
self.thread.finished.connect(
lambda: self.calculateBtn.setEnabled(True)
)
self.thread.finished.connect(
lambda: self.progressText.setText("0%")
)
def updateProgress(self, progressNum):
self.progressText.setText(f'{progressNum}%')
这应该可以解决你的 GUI 冻结问题,但不幸的是,你需要重构你的计算类,以便能够从中获取有关进度的信息。并且不要忘记从 self.thread.finished 更新您的实际图表以实际显示数据。