Search code examples
pythonpyqtpyqt5qstyle

PyQt5 Fusion Style Button Icons are broken


I want a toggle button to show different images depending on the toggled state. The feature works fine in the default QApplication Style ('WindowsVista'). But WindowsVista style is ugly and I'd rather use Fusion style. Unfortunately, the image does not change in Fusion Style.

The code:

from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton,
                             QStyleFactory, QLabel)
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QSize
from PyQt5 import Qt, QtCore
import sys

class Test(QMainWindow):
    def __init__(self, parent=None):
        super(Test, self).__init__(parent=parent)
        self.setGeometry(50,50,300,400)
        
        icon = QIcon()
        icon.addFile('red.png', state = QIcon.Off)
        icon.addFile('green.png', state = QIcon.On)
        button = QPushButton(parent = self)
        button.setCheckable(True)
        button.setIcon(icon)
        button.setIconSize(QSize(150,150))
        button.setGeometry(50,50,200,200)
        
        verString = 'PyQt version: ' + Qt.PYQT_VERSION_STR + '\n'
        verString += 'Qt version: ' + QtCore.qVersion()
        print(verString)
        label = QLabel(verString, parent = self)
        label.setGeometry(50, 250, 200, 150)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    #app.setStyle(QStyleFactory.create('fusion'))
    app.setStyle(QStyleFactory.create('windowsvista'))
    test = Test()        
    test.show()
    sys.exit(app.exec_() )

WindowsVista Style (button up on left, button down on right):

enter image description here

Fusion Style (button up on left, button down on right):

enter image description here


Solution

  • It seems to be a default behavior because of the fusion style as seen in the source code:

    # ...
    case CE_PushButtonLabel:
        if (const QStyleOptionButton *button = qstyleoption_cast(option)) {
            QStyleOptionButton b(*button);
            // no PM_ButtonShiftHorizontal and PM_ButtonShiftVertical for fusion style
            b.state &= ~(State_On | State_Sunken);
            QCommonStyle::drawControl(element, &b, painter, widget);
        }
        break;
    // ...

    A possible solution is to implement a QProxyStyle that overrides this behavior:

    import sys
    
    from PyQt5.QtCore import QSize, qVersion, PYQT_VERSION_STR
    from PyQt5.QtGui import QIcon
    from PyQt5.QtWidgets import (
        QApplication,
        QMainWindow,
        QPushButton,
        QStyleFactory,
        QLabel,
        QProxyStyle,
        QStyle,
        QCommonStyle,
    )
    
    
    class ProxyStyle(QProxyStyle):
        def drawControl(self, control, option, painter, widget):
            if control == QStyle.CE_PushButtonLabel:
                QCommonStyle.drawControl(self, control, option, painter, widget)
            else:
                super().drawControl(control, option, painter, widget)
    
    
    class Test(QMainWindow):
        def __init__(self, parent=None):
            super(Test, self).__init__(parent=parent)
            self.setGeometry(50, 50, 300, 400)
    
            icon = QIcon()
            icon.addFile("red.png", state=QIcon.Off)
            icon.addFile("green.png", state=QIcon.On)
            button = QPushButton(parent=self)
            button.setCheckable(True)
            button.setIcon(icon)
            button.setIconSize(QSize(150, 150))
            button.setGeometry(50, 50, 200, 200)
    
            verString = "PyQt version: " + PYQT_VERSION_STR + "\n"
            verString += "Qt version: " + qVersion()
            print(verString)
            label = QLabel(verString, parent=self)
            label.setGeometry(50, 250, 200, 150)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        app.setStyle(QStyleFactory.create("fusion"))
        proxy = ProxyStyle(app.style())
        app.setStyle(proxy)
        test = Test()
        test.show()
        sys.exit(app.exec_())