Home > OS >  PySide: QStyledItemDelegate button's stylesheet Issue
PySide: QStyledItemDelegate button's stylesheet Issue

Time:03-16

I'm using a QStyledItemDelegate to set a custom Button on a model in a tableView. I set a styleSheet to the button with a blue background-color and red background-color when the button will be pressed.

The tableview with the button and its blue background shows correctly, but when I press the button, the button's stylesheet doesn't works, its background color doesn't change to red color.

Window with blue buttons:

Expected behaivor: when a button is pressed its Background should change to red

enter image description here

This is my code:


import sys

from PySide2 import QtCore, QtWidgets, QtGui
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *


class DelegateButton(QStyledItemDelegate):
    def __init__(self, parent=None):
        super().__init__(parent)

        self._mybtn = None

        self._rfbutton = QPushButton()
        self._rfbutton.setStyleSheet(''' QPushButton{
                                                    background-color: blue;
                                                    border:1px solid blue;
                                                    }

                                         QPushButton:pressed{
                                                    background-color: red;
                                                    border:1px solid red;
                                                    }
                                    ''')
        
        
    def createEditor(self, parent, option, index):
        return None


    def paint(self, painter, option, index):

        self._mybtn = QStyleOptionButton()
        
        
        top_span_height = (option.rect.height() - 24)/2
        pos_x = 8   option.rect.x()
        pos_y = option.rect.y()   top_span_height

        self._mybtn.rect = QRect(pos_x, pos_y, 24, 24)
        self._mybtn.state = QStyle.State_Enabled


        self._rfbutton.style().drawControl(QStyle.CE_PushButton, self._mybtn, painter, self._rfbutton)
        


    def editorEvent(self, event, model, option, index):
        if event.type() == QEvent.MouseButtonPress:
            if self._mybtn.rect.contains(event.x(), event.y()):
                self._mybtn.state = QStyle.State_Sunken | QStyle.State_Enabled
            return False

        return False



if __name__ == '__main__':

    app = QApplication(sys.argv)
    rows = 5
    cols = 2

    model = QStandardItemModel(rows, cols)
    tableView = QTableView()
    tableView.setWindowTitle("Delegate Issue")
    tableView.setModel(model)

    delegate = DelegateButton()

    tableView.setItemDelegateForColumn(1, delegate)
    tableView.setColumnWidth(0, 200)
    tableView.setColumnWidth(1, 200)
    tableView.verticalHeader().setDefaultSectionSize(32)
    tableView.setSelectionBehavior(QAbstractItemView.SelectRows)

 
    for row in range(rows):
        index = model.index(row, 1, QModelIndex())
        model.setData(index, 1)

    tableView.show()
    tableView.resize(450,300)

    sys.exit(app.exec_())

How could I solve this issue?

CodePudding user response:

As is intended to just change the background color when the "button" is pressed as described in the stylesheet. I found out the reason it didn't change:

The code starts with self._mybtn.state set as QStyle.State_Enabled and buttons are painted blue as describe the stylesheet, when any mouse button is pressed in the "button" area, in the EvenEditor the MousePressed event changes the state to QStyle.State_Sunken | QStyle.State_Enabled , but when the paint function is called again and draws the items, the self._mybtn.state is set to be enable, so it changes instantly the state ending up with button colored blue.

I declare a self._is_pressed variable where QEvent.MouseButtonPress will store the position of the cursor when a button is pressed in the button's area. And in paint function, before it draws items, I evaluate if the self._is_pressed have stored the position, if it do, I evaluated the position is cointained inside the buttons area, so set the state to QStyle.State_Sunken | QStyle.State_Enabled and the stylesheet now can be applied and color red the button. Otherwise the state is set to QStyle.State_Enabled

Background to red:

The code ends up:

import sys

from PySide2 import QtCore, QtWidgets, QtGui
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *


class DelegateButton(QStyledItemDelegate):
    def __init__(self, parent=None):
        super().__init__(parent)

        self._mybtn = None
        self._is_pressed = None

        self._rfbutton = QPushButton()
        self._rfbutton.setStyleSheet(''' QPushButton{
                                                    background-color: blue;
                                                    border:1px solid blue;
                                                    }

                                         QPushButton:pressed{
                                                    background-color: red;
                                                    border:1px solid red;
                                                    }
                                    ''')
        
        
    def createEditor(self, parent, option, index):
        return None


    def paint(self, painter, option, index):

        self._mybtn = QStyleOptionButton()
        
        
        top_span_height = (option.rect.height() - 24)/2
        pos_x = 8   option.rect.x()
        pos_y = option.rect.y()   top_span_height

        self._mybtn.rect = QRect(pos_x, pos_y, 24, 24)

        
        
        if self._is_pressed and self._mybtn.rect.contains(self._is_pressed):
            self._mybtn.state = QStyle.State_Enabled | QStyle.State_Sunken
        else:
            self._mybtn.state = QStyle.State_Enabled


        self._rfbutton.style().drawControl(QStyle.CE_PushButton, self._mybtn, painter, self._rfbutton)
        




    def editorEvent(self, event, model, option, index):
        if event.type() == QEvent.MouseButtonPress:
            if self._mybtn.rect.contains(event.x(), event.y()):

                self._is_pressed = QPoint(event.x(), event.y())

                
            return True

        return False



if __name__ == '__main__':

    app = QApplication(sys.argv)
    rows = 5
    cols = 2

    model = QStandardItemModel(rows, cols)
    tableView = QTableView()
    tableView.setWindowTitle("Delegate Issue")
    tableView.setModel(model)

    delegate = DelegateButton()

    tableView.setItemDelegateForColumn(1, delegate)
    tableView.setColumnWidth(0, 200)
    tableView.setColumnWidth(1, 200)
    tableView.verticalHeader().setDefaultSectionSize(32)
    tableView.setSelectionBehavior(QAbstractItemView.SelectRows)

 
    for row in range(rows):
        index = model.index(row, 1, QModelIndex())
        model.setData(index, 1)

    tableView.show()
    tableView.resize(450,300)

    sys.exit(app.exec_())

The index could be used to achive the same result but i rather opted to use the position.

in the paint function:

if self._is_pressed and self._is_pressed == (index.row(), index.column()):
   self._mybtn.state = QStyle.State_Enabled | QStyle.State_Sunken
else:
    self._mybtn.state = QStyle.State_Enabled

in eventEditor:

if event.type() == QEvent.MouseButtonPress:
    if self._mybtn.rect.contains(event.x(), event.y()):
      self._is_pressed = (index.row(), index.column())
    return True

if it is wanted to restore to blue the background after the button is released then the QEvent.MouseButtonRelease must be implemented.

Blue Background after release button:

import sys

from PySide2 import QtCore, QtWidgets, QtGui
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *

class DelegateButton(QStyledItemDelegate):
    def __init__(self, parent=None):
        super().__init__(parent)

        self._mybtn = None
        self._is_pressed = None

        self._rfbutton = QPushButton()
        self._rfbutton.setStyleSheet(''' QPushButton{
                                                    background-color: blue;
                                                    border:1px solid blue;
                                                    }

                                         QPushButton:pressed{
                                                    background-color: red;
                                                    border:1px solid red;
                                                    }
                                    ''')
        
        
    def createEditor(self, parent, option, index):
        return None


    def paint(self, painter, option, index):

        self._mybtn = QStyleOptionButton()
        
        
        top_span_height = (option.rect.height() - 24)/2
        pos_x = 8   option.rect.x()
        pos_y = option.rect.y()   top_span_height

        self._mybtn.rect = QRect(pos_x, pos_y, 24, 24)


        if self._is_pressed and self._mybtn.rect.contains(self._is_pressed):
            self._mybtn.state = QStyle.State_Enabled | QStyle.State_Sunken
        else:
            self._mybtn.state = QStyle.State_Enabled | QStyle.State_Raised


        self._rfbutton.style().drawControl(QStyle.CE_PushButton, self._mybtn, painter, self._rfbutton)
        


    def editorEvent(self, event, model, option, index):
        if event.type() == QEvent.MouseButtonPress:
            if self._mybtn.rect.contains(event.x(), event.y()):

                self._is_pressed = QPoint(event.x(), event.y())
            return True
        
        
        if event.type() == QEvent.MouseButtonRelease:
            if self._mybtn.rect.contains(event.x(), event.y()):
                self._is_pressed = None
            return True
        

        return False

I hope this help to anyone in aplying styles when drawing styled delegates.

CodePudding user response:

A fundamental aspect that must be kept in mind is that delegate are abstract entities that do not represent a specific index in the model. They are used by the view to provide proper user interaction and item display depending on the indexes those delegates are set for.

This means that the delegate functions are called indistinctly and at very different moments depending on their purpose, and there could be no "persistent" reference to a specific index, since the view could call those functions on its own and completely out of our control.

Specifying coordinates for a unique "button" is wrong, as the paint function can be called for other items for other reasons: for instance, when the user hovers another index, or minimizes and restores the window.

The only viable solution (as long as you are actually and exclusively interested in appearance) is to keep a reference to the pressed buttons, possibly by using the index they refer to.

Note that while using a "ghost button" to use the QStyle functions might be a possibility, that is completely pointless if you are only drawing a simple rectangle: just use the basic drawRect() function of QPainter.

class DelegateButton(QStyledItemDelegate):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._pressed_rows = set()

    def _buttonRect(self, optionRect):
        return QRect(
            optionRect.x()   8, 
            optionRect.y()   (optionRect.height() - 24) // 2, 
            24, 24
        )
    def paint(self, painter, option, index):
        if index.row() in self._pressed_rows:
            color = QColor('red')
        else:
            color = QColor('blue')
        painter.save()
        painter.setPen(color)
        painter.setBrush(color)
        painter.drawRect(self._buttonRect(option.rect))
        painter.restore()

    def createEditor(self, *args):
        return

    def editorEvent(self, event, model, option, index):
        if event.type() == event.MouseButtonPress:
            if self._buttonRect(option.rect).contains(event.pos()):
                self._pressed_rows.add(index.row())
            else:
                self._pressed_rows.discard(index.row())
            return True
        # comment the following block to make the pressed state persistent
        elif event.type() == event.MouseButtonRelease:
            self._pressed_rows.discard(index.row())
            return True
        return super().editorEvent(event, model, option, index)

Note that the above will not work properly if the model changes during runtime, specifically if rows are inserted/removed before any already "pressed index" (including using filter models) and if the model is sorted.

A proper implementation should use alternative ways to achieve so, for instance by setting basic bool value for a custom role in the model: in this way the "pressed" indexes will always be consistent, no matter what happens to the model; the fact that you don't actually use those values for the data model is irrelevant, the important thing is to keep consistency of indexes (which are not persistent by nature). Another alternative is to check/store the rows of the QPersistentModelIndex of each index, both for the editorEvent() and the paint() functions.

  • Related