1

Problem:

I'm able to add a QPushButton to a QStyledItemDelegate just fine. I'm faking the button press inside the delegate's editorEvent method so when you press it an action happens. I'm having trouble getting my QPushButton's style sheet working though- It only reads the first background parameter which is "red" and doesn't change on mouse hover or press.

It's unclear how I should go about setting up button click and hover detection to make the button act like a real button on the delegate. Do I need to set up an eventFilter? Should I do this at the view level? Do I do this inside the delegate's paint method? A combination of everything?

Goals:

  • Mouse hover over the list time will show the button button's icon.
  • Mouse hover over the button will change its background color.
  • Mouse clicks on the button will darken the background color to show a a click happened.
  • I'd like to set these parameters in a style sheet if possible, but I also don't mind doing it all within a paint function. Whatever works!

Current implementation

The button widget is red with a folder icon. The items correctly change color on select and hover (I want to keep that), but the item's buttons don't change at all.

enter image description here

Thanks!

Here's what I've put together so far:

import sys

from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets


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

        # self.setLayout(QHBoxLayout())
        size = 50
        self.setFixedSize(size, size)
        self.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogOpenButton))
        self.setStyleSheet("""
            QPushButton{
                background:red;
                height: 30px;
                font: 12px "Roboto Thin";
                border-radius: 25
            }
            QPushButton:hover{
                background: green;
            }
            QPushButton:hover:pressed{
                background: blue;
            }
            QPushButton:pressed{
                background:  yellow;
            }
            """)


class MainWindow(QtWidgets.QWidget):
    def __init__(self):
        super(MainWindow, self).__init__()

        self.resize(300, 300)

        # Model/View
        entries = ['one', 'two', 'three']
        model = QtGui.QStandardItemModel()
        delegate = ListItemDelegate()
        self.listView = QtWidgets.QListView(self)
        self.listView.setModel(model)
        self.listView.setItemDelegate(delegate)

        for i in entries:
            item = QtGui.QStandardItem(i)
            model.appendRow(item)

        # Layout
        main_layout = QtWidgets.QVBoxLayout()
        main_layout.addWidget(self.listView)
        self.setLayout(main_layout)

        # Connections
        delegate.delegateButtonPressed.connect(self.on_delegate_button_pressed)

    def on_delegate_button_pressed(self, index):

        print('"{}" delegate button pressed'.format(index.data(QtCore.Qt.DisplayRole)))


class ListItemDelegate(QtWidgets.QStyledItemDelegate):
    delegateButtonPressed = QtCore.Signal(QtCore.QModelIndex)

    def __init__(self):
        super(ListItemDelegate, self).__init__()

        self.button = DelegateButton()

    def sizeHint(self, option, index):
        size = super(ListItemDelegate, self).sizeHint(option, index)
        size.setHeight(50)
        return size

    def editorEvent(self, event, model, option, index):

        # Launch app when launch button clicked
        if event.type() == QtCore.QEvent.MouseButtonRelease:
            click_pos = event.pos()
            rect_button = self.rect_button

            if rect_button.contains(click_pos):
                self.delegateButtonPressed.emit(index)
                return True
            else:
                return False
        else:
            return False

    def paint(self, painter, option, index):
        spacing = 10
        icon_size = 40

        # Item BG #########################################
        painter.save()
        if option.state & QtWidgets.QStyle.State_Selected:
            painter.setBrush(QtGui.QColor('orange'))
        elif option.state & QtWidgets.QStyle.State_MouseOver:
            painter.setBrush(QtGui.QColor('black'))
        else:
            painter.setBrush(QtGui.QColor('purple'))
        painter.drawRect(option.rect)
        painter.restore()

        # Item Text ########################################
        rect_text = option.rect
        QtWidgets.QApplication.style().drawItemText(painter, rect_text, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft, QtWidgets.QApplication.palette(), True, index.data(QtCore.Qt.DisplayRole))

        # Custom Button ######################################
        self.rect_button = QtCore.QRect(
            option.rect.right() - icon_size - spacing,
            option.rect.bottom() - int(option.rect.height() / 2) - int(icon_size / 2),
            icon_size,
            icon_size
        )

        option = QtWidgets.QStyleOptionButton()
        option.initFrom(self.button)
        option.rect = self.rect_button
        # Button interactive logic
        if self.button.isDown():
            option.state = QtWidgets.QStyle.State_Sunken
        else:
            pass
        if self.button.isDefault():
            option.features = option.features or QtWidgets.QStyleOptionButton.DefaultButton
        option.icon = self.button.icon()
        option.iconSize = QtCore.QSize(30, 30)

        painter.save()
        self.button.style().drawControl(QtWidgets.QStyle.CE_PushButton, option, painter, self.button)
        painter.restore()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())
Mike Bourbeau
  • 481
  • 11
  • 29

1 Answers1

1

Try: https://doc.qt.io/qt-4.8/qstyle.html#StateFlag-enum

import sys
import PySide.QtCore as core
import PySide.QtGui as gui

QPushButton#pushButton {
    background-color: yellow;
}

QPushButton#pushButton:hover {
    background-color: rgb(224, 255, 0);
}

QPushButton#pushButton:pressed {
    background-color: rgb(224, 0, 0);     
}

Your custom QStyledItemDelegate catches the mouse event, so that it is not passed to the QListView. So in the QStyledItemDelegate.editor(Event) one simply needs to add.

if event.type() == core.QEvent.MouseButtonPress:
    return False

Now the selection is recognizable in the paint()-method using option.state & gui.QStyle.State_Selected.

if __name__ == '__main__':


    app = gui.QApplication(sys.argv)
    app.setStyleSheet('QListView::item:hover {background: none;}')
    mw = gui.QMainWindow()

    model = MyListModel()
    view = gui.QListView()
    view.setItemDelegate(MyListDelegate(parent=view))
    view.setSpacing(5)
    view.setModel(model)

    mw.setCentralWidget(view)
    mw.show()

    sys.exit(app.exec_())

class MyListDelegate(gui.QStyledItemDelegate):

    w = 300
    imSize = 90
    pad = 5
    h = imSize + 2*pad
    sepX = 10

    def __init__(self, parent=None):
        super(MyListDelegate, self).__init__(parent)

    def paint(self, painter, option, index):
        mouseOver = option.state in [73985, 73729]

        if option.state & QStyle.State_MouseOver::
            painter.fillRect(option.rect, painter.brush())

        pen = painter.pen()
        painter.save()

        x,y = (option.rect.x(), option.rect.y())
        dataRef = index.data()
        pixmap = dataRef.pixmap()
        upperLabel = dataRef.upperLabel()
        lowerLabel = dataRef.lowerLabel()

        if mouseOver:
            newPen = gui.QPen(core.Qt.green, 1, core.Qt.SolidLine)
            painter.setPen(newPen)
        else:
            painter.setPen(pen)
        painter.drawRect(x, y, self.w, self.h)
        painter.setPen(pen)

        x += self.pad
        y += self.pad

        painter.drawPixmap(x, y, pixmap)

        font = painter.font()
        textHeight  = gui.QFontMetrics(font).height()

        sX = self.imSize + self.sepX
        sY = textHeight/2

        font.setBold(True)
        painter.setFont(font)
        painter.drawText(x+sX, y-sY, 
                         self.w-self.imSize-self.sepX, self.imSize,
                         core.Qt.AlignVCenter,
                         upperLabel)
        font.setBold(False)
        font.setItalic(True)
        painter.setFont(font)
        painter.drawText(x+sX, y+sY,
                         self.w-self.imSize-self.sepX, self.imSize,
                         core.Qt.AlignVCenter,
                         lowerLabel)

        painter.restore()

    def sizeHint(self, option, index):
        return core.QSize(self.w, self.imSize+2*self.pad)

    def editorEvent(self, event, model, option, index):
        if event.type() == core.QEvent.MouseButtonRelease:
            print 'Clicked on Item', index.row()
        if event.type() == core.QEvent.MouseButtonDblClick:
            print 'Double-Clicked on Item', index.row()
        return True


window.button->setAutoFillBackground(false);
window.button->setAutoFillBackground(true);
window.button->setPalette(*palette_red);

Another Solution To Set CSS:

import sys
from PyQt5 import Qt as qt 

class TopLabelNewProject(qt.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        layout = qt.QHBoxLayout(self)
        layout.setContentsMargins(40, 0, 32, 0)
        self.setLayout(layout)
        self.setFixedHeight(80)

        self.label = qt.QLabel("Dashboard")

        layout.addWidget(self.label, alignment=qt.Qt.AlignLeft)

#        self.newProjectButton = Buttons.DefaultButton("New project", self)
        self.newProjectButton = qt.QPushButton("New project", self)
        layout.addWidget(self.newProjectButton, alignment=qt.Qt.AlignRight)


style = '''
QWidget {
    background-color: white;
} 

QLabel {
    font: medium Ubuntu;
    font-size: 20px;
    color: #006325;     
}        

QPushButton {
    background-color: #006325;
    color: white;

    min-width:  70px;
    max-width:  70px;
    min-height: 70px;
    max-height: 70px;

    border-radius: 35px;        
    border-width: 1px;
    border-color: #ae32a0;
    border-style: solid;
}
QPushButton:hover {
    background-color: #328930;
}
QPushButton:pressed {
    background-color: #80c342;
}    

'''


if __name__ == '__main__':
    app = qt.QApplication(sys.argv)

    app.setStyleSheet(style)

    ex = TopLabelNewProject()
    ex.show()
    sys.exit(app.exec_())  
Mahsa Hassankashi
  • 2,086
  • 1
  • 15
  • 25
  • Thank you Mahsa. I've updated my question to clarify the issue. As you can the item background is reacting to the mouse correctly (I want to keep that). The QStyleOptionButton on the widget (the button with a red background and folder icon) isn't doing anything though. I'm using `initFrom` to pass a custom QPushButton and its style sheet to the delegate, but it it isn't taking the full QPushButton style sheet. It only takes the background color for some reason. I'm also using PySide 2 if that changes anything. Editing my code to make it work would help me better understand as well! Thanks – Mike Bourbeau Apr 25 '20 at 21:38
  • You are most welcome, I am glad to help, try to set false this attribute, I corrected my answer. – Mahsa Hassankashi Apr 25 '20 at 21:45
  • You have "QPushButton:hover:pressed" make them separate as I wrote in my last solution. – Mahsa Hassankashi Apr 25 '20 at 21:53
  • Your answer isn't quite making sense to me still :/ It would really help if you showed me what you're trying to do with the code I've provided. I tried making the changes you suggested and it didn't help anything. Also your second example is how to setup a style sheet on a QPushButton within a QWidget. I'm not sure how that's applicable to delegates? From what I've read delegates shouldn't have actual widgets in them and instead I should use QStyleOptionButton (which is why I'm having trouble getting this setup to work correctly). – Mike Bourbeau Apr 25 '20 at 23:59