Search code examples
pythonpyqt5qt5qtablewidget

Background color of the particular cell is not changing after clicking specific cells in qTableWidget pyqt5


So I have a table with 1 row and multiple columns and I use the custom delegate function for the image thumbnail. I also included the set background colour function when I click it. However, it doesn't change the colour of the item background. I have to use the custom delegate function so that my icon image is sticking with each other from different cell without any additional spaces between.

my code is as below

import random

from PyQt5 import QtCore, QtGui, QtWidgets


imagePath2 = "arrowREDHead.png"
imagePath = "arrowREDBody.png"
class IconDelegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        icon = index.data(QtCore.Qt.DecorationRole)
        mode = QtGui.QIcon.Normal
        if not (option.state & QtWidgets.QStyle.State_Enabled):
            mode = QtGui.QIcon.Disabled
        elif option.state & QtWidgets.QStyle.State_Selected:
            mode = QtGui.QIcon.Selected
        state = (
            QtGui.QIcon.On
            if option.state & QtWidgets.QStyle.State_Open
            else QtGui.QIcon.Off
        )
        pixmap = icon.pixmap(option.rect.size(), mode, state)
        painter.drawPixmap(option.rect, pixmap)

    def sizeHint(self, option, index):
        return QtCore.QSize(20, 20)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        table = QtWidgets.QTableWidget(1, 10)
        delegate = IconDelegate(table)
        table.setItemDelegate(delegate)

        self.setCentralWidget(table)

        for j in range(table.columnCount()):

            if(j % 2 == 0):

                pixmap = QtGui.QPixmap(imagePath)
                icon = QtGui.QIcon(pixmap)

                it = QtWidgets.QTableWidgetItem()
                it.setIcon(icon)
                table.setItem(0, j, it)
            
            else:
                pixmap = QtGui.QPixmap(imagePath2)
                icon = QtGui.QIcon(pixmap)
                it = QtWidgets.QTableWidgetItem()
                it.setIcon(icon)
                table.setItem(0, j, it)
                table.item(0, 1).setBackground(QtGui.QColor(100,100,150))

                
        table.resizeRowsToContents()
        table.resizeColumnsToContents()
        table.setShowGrid(False)
        table.itemClicked.connect(self.sequenceClicked)

    def sequenceClicked(self, item): 
        self.itemClicked = item
        print("item", item)
        item.setBackground(QtGui.QColor(255, 215, 0))


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

This is the image with the gap between icons and it form not a perfect looking arrow thats why I used the custom delegate function to stick the icon together and let it look like a perfect arrow

First image

This is the image that has the perfect look of the arrow as it doesnt has any gap between the cells by using the delegate function. However, it doesnt allow me to change the background colour for the cell.

Second image

This is the image of the arrowHead

arrowHead

This is the image of the arrowBody

arrowBody


Solution

  • As with any overridden function, if the base implementation is not called, the default behavior is ignored.

    paint() is the function responsible of drawing any aspect of the item, including its background. Since in your override you are just drawing the pixmap, everything else is missing, so the solution would be to call the base implementation and then draw the pixmap.

    In this specific case, though, this wouldn't be the right choice, as the base implementation already draws the item's decoration, and in certain conditions (assuming that the pixmap has an alpha channel), it will probably result in having the images overlapping.

    There are three possible solutions.

    Use QStyle functions ignoring the icon

    In this case, we do what the base implementation would, which is calling the style drawControl function with the CE_ItemViewItem, but we modify the option before that, removing anything related to the icon:

        def paint(self, painter, option, index):
            # create a new option (as the existing one should not be modified)
            # and initialize it for the current index
            option = QtWidgets.QStyleOptionViewItem(option)
            self.initStyleOption(option, index)
    
            # remove anything related to the icon
            option.features &= ~option.HasDecoration
            option.decorationSize = QtCore.QSize()
            option.icon = QtGui.QIcon()
    
            if option.widget:
                style = option.widget.style()
            else:
                style = QtWidgets.QApplication.style()
            # call the drawing function for view items
            style.drawControl(style.CE_ItemViewItem, option, painter, option.widget)
    
            # proceed with the custom drawing of the pixmap
            icon = index.data(QtCore.Qt.DecorationRole)
            # ...
    

    Update the icon information in the delegate

    A QStyledItemDelegate always calls initStyleOption at the beginning of most its functions, including paint and sizeHint. By overriding that function, we can change some aspects of the drawing, including the icon alignment and positioning.

    In this specific case, knowing that we have only two types of icons, and they must be right or left aligned depending on the image, it's enough to update the proper decoration size (based on the optimal size of the icon), and then the alignment based on the column.

    class IconDelegate(QtWidgets.QStyledItemDelegate):
        def initStyleOption(self, option, index):
            super().initStyleOption(option, index)
            option.decorationSize = option.icon.actualSize(option.rect.size())
            if index.column() & 1:
                option.decorationPosition = option.Left
                option.decorationAlignment = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter
            else:
                option.decorationPosition = option.Right
                option.decorationAlignment = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
    

    Note: this approach only works as long as the images all have the same ratio, and the row height is not manually changed.

    Use a custom role for the image

    Qt uses roles to store and retrieve various aspects of each index (font, background, etc.), and user roles can be defined to store custom data.

    setIcon() actually sets a QIcon in the item using the DecorationRole, and the delegate uses it to draw it. If the data returned for that role is empty/invalid, no icon is drawn.

    If we don't use setIcon() but store the icon in the item with a custom role instead, the default drawing functions will obviously not draw any decoration, but we can still access it from the delegate.

    # we usually define roles right after the imports, as they're constants
    ImageRole = QtCore.Qt.UserRole + 1
    
    class IconDelegate(QtWidgets.QStyledItemDelegate):
        def paint(self, painter, option, index):
            super().paint(painter, option, index)
            icon = index.data(ImageRole)
            # ...
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            # ...
                    it = QtWidgets.QTableWidgetItem()
                    it.setData(ImageRole, pixmap)