PySide GUI 在使用 QSortFilterProxyModel 进行过滤期间冻结

问题描述 投票:0回答:1

我正在开发一个 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 在此期间不应该冻结,以便您可以显示进度条或类似的内容。有什么建议或解决方案吗?

python qt pyqt pyside pyside6
1个回答
0
投票

在 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 更新您的实际图表以实际显示数据。

© www.soinside.com 2019 - 2024. All rights reserved.