Search code examples
pythonmodelpyqtqtableviewqabstracttablemodel

How to make QTableView refresh Background color after it loads data again


i have the following doubt regarding QTableView, i have added some code that changes the row background depending on what string the dataframe contains on the last column.

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.BackgroundRole:
                if df.iloc[index.row(),5] == "Ready for QC":
                    return QBrush(Qt.yellow)
                if df.iloc[index.row(),5] == "In Progress":
                    return QBrush(Qt.green)
            if role == Qt.DisplayRole:
                return str(self._data.iloc[index.row(), index.column()])
        return None

When the table loads the first time it properly paints the Table, the thing is that i have a specific part of the code that always runs every 5 seconds as to refresh the Table with fresh information.

def printit():
    threading.Timer(5.0, printit).start()
    weekNumber = date.today().isocalendar()[1]
    aux = pd.read_excel('PCS tasks 2020.xlsm',sheet_name='W'+str(weekNumber))
    today = datetime.today()
    df = aux[aux['Date Received'] == today.strftime("%Y-%d-%m")]
    df = df[["Requestor","Subject","Task type","Created by","QC Executive","Status"]].fillna("")
    df = df[df['Status'] != "Completed"]
    model = pandasModel(df)
    view.setModel(None)
    view.setModel(model)

The thing is that when the above code runs, the table does in fact update the data, but it does not change the colors. I currently have tried different methods like defining setData and in there update the colors but with no avail. Now im asking you if someone knows something regarding updating colors on a QTableView.

By the way, I'm attaching the entire code for the python program below as to give context.

import sys
import pandas as pd
from PyQt5.QtWidgets import QApplication, QTableView
from PyQt5.QtCore import QAbstractTableModel, Qt
from PyQt5.QtGui import QBrush
from datetime import date, datetime
import threading

weekNumber = date.today().isocalendar()[1]
aux = pd.read_excel('PCS tasks 2020.xlsm',sheet_name='W'+str(weekNumber))
today = datetime.today()
df = aux[aux['Date Received'] == today.strftime("%Y-%d-%m")]
df = df[["Requestor","Subject","Task type","Created by","QC Executive","Status"]].fillna("")
df = df[df['Status'] != "Completed"]

def printit():
    threading.Timer(5.0, printit).start()
    weekNumber = date.today().isocalendar()[1]
    aux = pd.read_excel('PCS tasks 2020.xlsm',sheet_name='W'+str(weekNumber))
    today = datetime.today()
    df = aux[aux['Date Received'] == today.strftime("%Y-%d-%m")]
    df = df[["Requestor","Subject","Task type","Created by","QC Executive","Status"]].fillna("")
    df = df[df['Status'] != "Completed"]
    model = pandasModel(df)
    view.setModel(None)
    view.setModel(model)

class pandasModel(QAbstractTableModel):

    def __init__(self, data):
        QAbstractTableModel.__init__(self)
        self._data = data

    def rowCount(self, parent=None):
        return self._data.shape[0]

    def columnCount(self, parent=None):
        return self._data.shape[1] -1

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.BackgroundRole:
                if df.iloc[index.row(),5] == "Ready for QC":
                    return QBrush(Qt.yellow)
                if df.iloc[index.row(),5] == "In Progress":
                    return QBrush(Qt.green)
            if role == Qt.DisplayRole:
                return str(self._data.iloc[index.row(), index.column()])
        return None

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._data.columns[col]
        return None

if __name__ == '__main__':
    app = QApplication(sys.argv)
    model = pandasModel(df)
    view = QTableView()
    view.setModel(model)
    view.resize(523, 300)
    printit()
    view.show()
    sys.exit(app.exec_())

Solution

  • I understand that you are using threading.Timer() because the process of loading and processing the dataframe is very time consuming and you want to do a periodic task (if the task does not consume much time then another option is to use QTimer) but the problem is that you are creating a model and adding information in sight that is part of the GUI from another thread which is prohibited by Qt as indicated by the docs.

    Considering the above it is better to send the information of the secondary thread to the main thread through signals, I have also implemented a method that resets the information of the model avoiding the need to create new models, and finally I have added verification code so that the code don't fail.

    import sys
    import pandas as pd
    
    from PyQt5.QtCore import pyqtSignal, pyqtSlot, QAbstractTableModel, QObject, Qt
    from PyQt5.QtGui import QBrush
    from PyQt5.QtWidgets import QApplication, QTableView
    
    import threading
    
    
    class PandasManager(QObject):
        dataFrameChanged = pyqtSignal(pd.DataFrame)
    
        def start(self):
            self.t = threading.Timer(0, self.load)
            self.t.start()
    
        def load(self):
            import random
    
            headers = list("ABCDEFG")
            data = [random.sample(range(255), len(headers)) for _ in headers]
    
            for d in data:
                d[5] = random.choice(["Ready for QC", "In Progress", "Another option"])
    
            df = pd.DataFrame(data, columns=headers,)
    
            self.dataFrameChanged.emit(df)
            self.t = threading.Timer(5.0, self.load)
            self.t.start()
    
        def stop(self):
            self.t.cancel()
    
    
    class PandasModel(QAbstractTableModel):
        def __init__(self, df=pd.DataFrame()):
            QAbstractTableModel.__init__(self)
            self._df = df
    
        @pyqtSlot(pd.DataFrame)
        def setDataFrame(self, df):
            self.beginResetModel()
            self._df = df
            self.endResetModel()
    
        def rowCount(self, parent=None):
            return self._df.shape[0]
    
        def columnCount(self, parent=None):
            return self._df.shape[1]
    
        def data(self, index, role=Qt.DisplayRole):
            if index.isValid():
                if role == Qt.BackgroundRole:
                    if self.columnCount() >= 6:
                        it = self._df.iloc[index.row(), 5]
                        if it == "Ready for QC":
                            return QBrush(Qt.yellow)
                        if it == "In Progress":
                            return QBrush(Qt.green)
                if role == Qt.DisplayRole:
                    return str(self._df.iloc[index.row(), index.column()])
    
        def headerData(self, col, orientation, role):
            if orientation == Qt.Horizontal and role == Qt.DisplayRole:
                return self._df.columns[col]
            return None
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        w = QTableView()
        model = PandasModel()
        w.setModel(model)
        w.show()
    
        manager = PandasManager()
        manager.dataFrameChanged.connect(model.setDataFrame)
        manager.start()
    
        ret = app.exec_()
    
        manager.stop()
    
        sys.exit(ret)
    

    As you can see I created dataframes randomly for my test but if you want to use your code then you must replace it as follows:

    def load(self):
        weekNumber = date.today().isocalendar()[1]
        aux = pd.read_excel("PCS tasks 2020.xlsm", sheet_name="W" + str(weekNumber))
        today = datetime.today()
        df = aux[aux["Date Received"] == today.strftime("%Y-%d-%m")]
        df = df[
            [
                "Requestor",
                "Subject",
                "Task type",
                "Created by",
                "QC Executive",
                "Status",
            ]
        ].fillna("")
        df = df[df["Status"] != "Completed"]
        self.dataFrameChanged.emit(df)
        self.t = threading.Timer(5.0, self.load)
        self.t.start()