Search code examples
pythonpython-3.xpyqtpyqt5qabstracttablemodel

Background color a specific table row given row number for QAbstractTableModel in PyQt5


In PyQt5, I am using the model view to display a table. The model is the QAbstractTableModel, and I want to background color say row 0. The coloring works, but all rows get colored, instead of the row that I specified. Also, when I change to Qt.Background role, I get some "tickbox" in my cell that I don't want. I guess, that it is my understanding of what actually happen in QAbstractTableModel's def data part that prevents me from achieve the desired effect.

This is my code snippet part as I have already tried. Note the status 1 and status 2 are actually True or False in my case. If True, this entire row should be colored as background green, else it should stay white.

#Make some dummy data

tabledata = list()
tabledata.append(('item 1', 'amount 1', 'price 1', 'status 1'))
tabledata.append(('item 2', 'amount 2', 'price 2', 'status 2'))

#The self.model below is QAbstractTableModel subclassed

self.model.modelTableData = tabledata

#try set data for just one cell
self.model.setData(self.model.index(0,0), QtCore.Qt.BackgroundRole)
self.model.layoutChanged.emit()

Then in my QAbstractTableModel class, I have the following in def data

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, parent, header, tabledata):
        #inhert from QAbstractTableModel
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.modelTableData = tabledata
        self.header = header

    def rowCount(self, parent):
        return len(self.modelTableData)

    def columnCount(self, parent):
        return len(self.header)

    def data(self, index, role):
        if not index.isValid():
            return None

        if role == QtCore.Qt.BackgroundRole:
            print('Qt.BackgroundRole at ' + str(index.row()))
            return QtCore.QVariant(QtGui.QColor(QtCore.Qt.green))

        print('Not Qt.BackgroundRole at ' + str(index.row()))
        return self.modelTableData[index.row()][index.column()]

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

I have googled on similar examples and studied this one in particular https://python-forum.io/Thread-Change-color-of-a-row-of-a-QTableView

What they seems to do is

if role == QtCore.Qt.BackgroundRole and "something more":
    #then do something

It is this "something more" that I don't know how to parse into the def data method. Ideally it should be my row data status 1 that either can be True or False, but my understanding is that def data part is actually returning the data for the viewer?

Also, I get confused about that in my code, when I executed the print, it seems that even though I stated in my data that only one cell at QModelIndex (0,0) is set to green, the next row is also set to green. What is the reason for this behaviour?

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'main_v00.ui'
#
# Created by: PyQt5 UI code generator 5.9.2
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets
import POSTools as tool
import json 

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(1680, 1050)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.cb_OrderList = QtWidgets.QComboBox(self.centralwidget)
        self.cb_OrderList.setGeometry(QtCore.QRect(160, 30, 111, 31))
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(2)
        sizePolicy.setHeightForWidth(self.cb_OrderList.sizePolicy().hasHeightForWidth())
        self.cb_OrderList.setSizePolicy(sizePolicy)
        self.cb_OrderList.setObjectName("cb_OrderList")
        self.le_NewTable = QtWidgets.QLineEdit(self.centralwidget)
        self.le_NewTable.setGeometry(QtCore.QRect(30, 30, 113, 35))
        self.le_NewTable.setObjectName("le_NewTable")
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setGeometry(QtCore.QRect(30, 10, 60, 16))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(14)
        self.label.setFont(font)
        self.label.setObjectName("label")
        self.label_2 = QtWidgets.QLabel(self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(170, 10, 60, 16))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(14)
        self.label_2.setFont(font)
        self.label_2.setObjectName("label_2")
        self.le_ItemName = QtWidgets.QLineEdit(self.centralwidget)
        self.le_ItemName.setGeometry(QtCore.QRect(30, 100, 171, 35))
        self.le_ItemName.setObjectName("le_ItemName")


        self.le_ItemAmount = QtWidgets.QLineEdit(self.centralwidget)
        self.le_ItemAmount.setGeometry(QtCore.QRect(220, 100, 113, 35))
        self.le_ItemAmount.setObjectName("le_ItemAmount")
        self.le_UnitPrice = QtWidgets.QLineEdit(self.centralwidget)
        self.le_UnitPrice.setGeometry(QtCore.QRect(350, 100, 113, 35))
        self.le_UnitPrice.setObjectName("le_UnitPrice")
        self.label_3 = QtWidgets.QLabel(self.centralwidget)
        self.label_3.setGeometry(QtCore.QRect(30, 70, 101, 21))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(14)
        self.label_3.setFont(font)
        self.label_3.setObjectName("label_3")
        self.label_4 = QtWidgets.QLabel(self.centralwidget)
        self.label_4.setGeometry(QtCore.QRect(220, 70, 101, 21))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(14)
        self.label_4.setFont(font)
        self.label_4.setObjectName("label_4")
        self.label_5 = QtWidgets.QLabel(self.centralwidget)
        self.label_5.setGeometry(QtCore.QRect(350, 70, 101, 21))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(14)
        self.label_5.setFont(font)
        self.label_5.setObjectName("label_5")
        self.label_6 = QtWidgets.QLabel(self.centralwidget)
        self.label_6.setGeometry(QtCore.QRect(480, 70, 101, 21))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(14)
        self.label_6.setFont(font)
        self.label_6.setObjectName("label_6")
        self.tableView = QtWidgets.QTableView(self.centralwidget)
        self.tableView.setGeometry(QtCore.QRect(30, 150, 711, 461))
        self.tableView.setObjectName("tableView")
        self.pb_remove = QtWidgets.QPushButton(self.centralwidget)
        self.pb_remove.setGeometry(QtCore.QRect(750, 250, 151, 101))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(18)
        self.pb_remove.setFont(font)
        self.pb_remove.setObjectName("pb_remove")
        self.pb_receipt = QtWidgets.QPushButton(self.centralwidget)
        self.pb_receipt.setGeometry(QtCore.QRect(590, 620, 151, 101))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(18)
        self.pb_receipt.setFont(font)
        self.pb_receipt.setObjectName("pb_receipt")
        self.label_TotalPrice = QtWidgets.QLabel(self.centralwidget)
        self.label_TotalPrice.setGeometry(QtCore.QRect(480, 100, 121, 35))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(18)
        self.label_TotalPrice.setFont(font)
        self.label_TotalPrice.setObjectName("label_TotalPrice")
        self.le_Discount = QtWidgets.QLineEdit(self.centralwidget)
        self.le_Discount.setGeometry(QtCore.QRect(610, 100, 131, 35))
        self.le_Discount.setObjectName("le_Discount")
        self.label_7 = QtWidgets.QLabel(self.centralwidget)
        self.label_7.setGeometry(QtCore.QRect(610, 70, 101, 21))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(14)
        self.label_7.setFont(font)
        self.label_7.setObjectName("label_7")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(750, 150, 151, 101))
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(18)
        self.pushButton.setFont(font)
        self.pushButton.setObjectName("pushButton")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 1680, 22))
        self.menubar.setObjectName("menubar")
        self.menuMore_Options = QtWidgets.QMenu(self.menubar)
        self.menuMore_Options.setObjectName("menuMore_Options")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.actionCount_Up = QtWidgets.QAction(MainWindow)
        self.actionCount_Up.setObjectName("actionCount_Up")
        self.menuMore_Options.addSeparator()
        self.menuMore_Options.addAction(self.actionCount_Up)
        self.menubar.addAction(self.menuMore_Options.menuAction())

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)
        MainWindow.setTabOrder(self.le_NewTable, self.cb_OrderList)
        MainWindow.setTabOrder(self.cb_OrderList, self.le_ItemName)
        MainWindow.setTabOrder(self.le_ItemName, self.le_ItemAmount)
        MainWindow.setTabOrder(self.le_ItemAmount, self.le_UnitPrice)
        MainWindow.setTabOrder(self.le_UnitPrice, self.le_Discount)
        MainWindow.setTabOrder(self.le_Discount, self.pushButton)
        MainWindow.setTabOrder(self.pushButton, self.pb_remove)
        MainWindow.setTabOrder(self.pb_remove, self.pb_receipt)
        MainWindow.setTabOrder(self.pb_receipt, self.tableView)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.label.setText(_translate("MainWindow", "Order"))
        self.label_2.setText(_translate("MainWindow", "Order List"))
        self.label_3.setText(_translate("MainWindow", "Item Name"))
        self.label_4.setText(_translate("MainWindow", "Item Amount"))
        self.label_5.setText(_translate("MainWindow", "Unit Price"))
        self.label_6.setText(_translate("MainWindow", "Price"))
        self.pb_remove.setText(_translate("MainWindow", "Remove"))
        self.pb_receipt.setText(_translate("MainWindow", "Receipt"))
        self.label_TotalPrice.setText(_translate("MainWindow", "0"))
        self.label_7.setText(_translate("MainWindow", "Discount [%]"))
        self.pushButton.setText(_translate("MainWindow", "Print Kitchen"))
        self.menuMore_Options.setTitle(_translate("MainWindow", "More Options"))
        self.actionCount_Up.setText(_translate("MainWindow", "Count Up"))

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        super(MainWindow,self).__init__()
        self.setupUi(self)
        #Start by reading in the menu items
        self.URL_MenuDB = "somewhere"
        self.path_OrderDB = "somewhere"
        header, menudb = tool.load_MenuDB(self.URL_MenuDB)
        self.MenuHeader = header

        #Prepare the completer by first creating the model
        self.completermodel = QtGui.QStandardItemModel()
        for item in menudb:
            row = list()
            for col in item:
                cell = QtGui.QStandardItem(str(col))
                row.append(cell)
            self.completermodel.appendRow(row)

        self.completer = QtWidgets.QCompleter()
        self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
        self.completer.setCompletionColumn(0)
        self.completer.setModel(self.completermodel)
        self.completer.setFilterMode(QtCore.Qt.MatchContains)
        self.completer.activated[QtCore.QModelIndex].connect(self.onActivated)
        self.le_ItemName.setCompleter(self.completer)

        #Setup model view
        self.TableViewHeader = ['Item', 'Qty', 'Price', 'Print status']
        self.TableData = list()
        self.model = TableModel(self, header = self.TableViewHeader, tabledata = self.TableData)
        self.model.layoutChanged.emit()
        self.tableView.setModel(self.model)

        #Upon program starts up, check if OrderDB.txt exists
        status, AllTables = tool.importOrderDB(self.path_OrderDB)
        if status is True: #OrderDB.txt exists
            #check if there is incomplete orders
            self.AllTables = AllTables
            #A list of all active tables
            tablenames = tool.getincompleteOrder(AllTables)
            if tablenames:
                #update the list of tablenames into drop down list
                self.cb_OrderList.clear()
                self.cb_OrderList.addItems(tablenames)
                #get the index of the current active tablename
                self.cb_OrderList.currentText()

        #Define what happens when the user press enter og return for item amount
        self.le_ItemAmount.returnPressed.connect(self.ItemAmountEnterKeyPress)

        #If enter is pressed at unit price, it also connects with self.ItemAmountEnterKeyPress
        self.le_UnitPrice.returnPressed.connect(self.ItemAmountEnterKeyPress)

        #Define what happens when input table edit field is activated
        self.le_NewTable.returnPressed.connect(self.input_newTable)

    def input_newTable(self): #When the user create a new order
        if not self.le_NewTable.text():
            return
        else: 
            #check if OrderDB already exists, if not one will be created. If exists is True, AllTables will be returned
            status, AllTables, tablename, nameclash = tool.ExistOrderDB(self.path_OrderDB, self.le_NewTable.text().strip())

            if nameclash is True:
                tool.msgbox(self,'Bord navn eksisterer. Valg et nyt!')
                self.le_NewTable.clear()
                return

            if status is False: #OrderDB.txt has just been created, and AllTables containing the tableName is returned
                self.AllTables = AllTables
                #Sort all the incomplete tables from All Tables and return the sorted tablename as pandas DataFrame
                tablename = tool.getincompleteOrder(AllTables)
                #insert the tablename as list to drop down list
                self.cb_OrderList.clear()
                self.cb_OrderList.addItems(tablename)
                self.le_NewTable.clear()

            else: #OrderDB.txt exists, continue to create the new table
                #create the tabledict
                tabledict = tool.CreateTableDict(self.le_NewTable.text())
                self.AllTables.append(tabledict)
                #save to data base
                tool.saveOrderDB(self.path_OrderDB, self.AllTables)

                tablename = tabledict["Name"]
                #get a list of all incomplete order names
                ordernames = tool.getincompleteOrder(self.AllTables)
                self.cb_OrderList.clear()
                self.cb_OrderList.addItems(ordernames)
                index = self.cb_OrderList.findText(tablename, QtCore.Qt.MatchFixedString)
                #set the drop down list to the current active index
                self.cb_OrderList.setCurrentIndex(index)

            #Set focus to item field
            self.le_ItemName.setFocus()
            self.le_NewTable.clear()

    def ItemAmountEnterKeyPress(self): #insert the item into the table and update the data base behind the scene
        if not self.cb_OrderList.currentText():
            tool.msgbox(self, 'Select an order first')
            return
        else:
            #Update the selected item into the AllTable
            #Do a match to see if self.selected matches the fields
            inputtext = self.le_ItemName.text()
            if inputtext.strip() == self.selected[0]:
                #the selected is the same as what appears in the field. Check the remaining fields
                qty = tool.isfloat(self.le_ItemAmount.text())
                price = tool.isfloat(self.le_UnitPrice.text())
                if qty is not False and price is not False:
                    #submit the fields to be input into the modelview
                    index = tool.getTableDict(self)

                    #Do the visualization
                    price = [item for item in self.AllTables[index]["orderPrice"]]

                    totalprice = sum(price)
                    self.TableData = list(
                        zip(self.AllTables[index]['itemName'],
                        self.AllTables[index]['orderQty'],
                        price,
                        self.AllTables[index]['PrintStatus_send2kitchen']
                    ))
                    #Update into the model
                    tabledata = list()
                    tabledata.append(('item 1', 'amount 1', 'price 1', 'status 1'))
                    tabledata.append(('item 2', 'amount 2', 'price 2', 'status 2'))

                    self.model.modelTableData = tabledata
                    #self.model.modelTableData = self.TableData

                    #try set data for just one cell
                    self.model.setData(self.model.index(0,0), 
                    QtCore.Qt.BackgroundRole)

                    self.model.layoutChanged.emit()

                    #tool.plotTable(self, totalprice)

                    #clear
                    self.clearInputFields()
                    self.le_ItemName.setFocus()

                else: #the item in the fields is different from self.selected, ask the user to try again
                    tool.msgbox(self, 'Item er ikke korrekt valgt, prøv igen.')
                    self.clearInputFields(self)
                    self.le_ItemName.setFocus()
                    return

            else: #User has not selected from the list
                tool.msgbox(self,'Prøv igen. Du skal vælge fra listen når du indsætter item')
                self.clearInputFields(self)
                self.le_ItemName.setFocus()
                return

    def clearInputFields(self):
        self.le_ItemName.clear()
        self.le_ItemAmount.clear()
        self.le_UnitPrice.clear()

    @QtCore.pyqtSlot(QtCore.QModelIndex)
    def onActivated(self, index):
        self.selected = list()
        for i in range(0, len(self.MenuHeader) +1):
            self.selected.append(index.sibling(index.row(),i).data())

        #display the selected item in the editfield
        self.le_UnitPrice.setText(self.selected[3])

        #change focus to amount
        self.le_ItemAmount.setFocus()

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, parent, header, tabledata):
        #inhert from QAbstractTableModel
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.modelTableData = tabledata
        self.header = header

    def rowCount(self, parent):
        return len(self.modelTableData)

    def columnCount(self, parent):
        return len(self.header)

    def data(self, index, role):
        if not index.isValid():
            return None

        if role == QtCore.Qt.BackgroundRole:
            return QtCore.QVariant(QtGui.QColor(QtCore.Qt.green))

        return self.modelTableData[index.row()][index.column()]

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

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())


The expected outcome is only the specified row get colored. Also, the tick mark at each cell that I assume is related to Qt.Background is removed.

Thanks!


Solution

  • The logic of the data() method is to filter the information, in this case I will use a dictionary also considering that you want the complete row to be painted then save a QPersistentModel associated to the chosen row but to column 0, finally the method setData() does nothing by default so you have to override.

    The error that indicates about the checkboxes is because you are returning the text for any other role like the Qt::CheckStateRole, instead you must filter the information as I already indicated

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    
    class TableModel(QtCore.QAbstractTableModel):
        def __init__(self, parent, header, tabledata):
            QtCore.QAbstractTableModel.__init__(self, parent)
            self.modelTableData = tabledata
            self.header = header
    
            self.background_colors = dict()
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            return len(self.modelTableData)
    
        def columnCount(self, parent=QtCore.QModelIndex()):
            return len(self.header)
    
        def data(self, index, role):
            if not index.isValid():
                return None
            if (
                0 <= index.row() < self.rowCount()
                and 0 <= index.column() < self.columnCount()
            ):
                if role == QtCore.Qt.BackgroundRole:
                    ix = self.index(index.row(), 0)
                    pix = QtCore.QPersistentModelIndex(ix)
                    if pix in self.background_colors:
                        color = self.background_colors[pix]
                        return color
                elif role == QtCore.Qt.DisplayRole:
                    return self.modelTableData[index.row()][index.column()]
    
        def setData(self, index, value, role):
            if not index.isValid():
                return False
            if (
                0 <= index.row() < self.rowCount()
                and 0 <= index.column() < self.columnCount()
            ):
                if role == QtCore.Qt.BackgroundRole and index.isValid():
                    ix = self.index(index.row(), 0)
                    pix = QtCore.QPersistentModelIndex(ix)
                    self.background_colors[pix] = value
                    return True
            return False
    
        def headerData(self, col, orientation, role):
            if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
                return self.header[col]
            return None
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        w = QtWidgets.QTableView()
        header = ["Item", "Qty", "Price", "Print status"]
    
        tabledata = [
            ("item 1", "amount 1", "price 1", "status 1"),
            ("item 2", "amount 2", "price 2", "status 2"),
        ]
        model = TableModel(None, header, tabledata)
        model.setData(
            model.index(0, 0), QtGui.QColor(QtCore.Qt.green), QtCore.Qt.BackgroundRole
        )
        w.setModel(model)
        w.show()
        sys.exit(app.exec_())