Avoid gray background for selected icons

Heads up! You've already completed this tutorial.

ERIC_HUYGEN | 2021-04-23 18:38:21 UTC | #1

Hi,

I have a question about a checkable QAction with two custom icons. In a toolbar, I have a number of actions/icon that are checkable. The actions have two states and the icons are different for each state. The code looks like this:

python
    switch_on_pix = QPixmap(str(get_resource(":/icons/switch-on.svg")))
    switch_off_pix = QPixmap(str(get_resource(":/icons/switch-off.svg")))
    switch_icon = QIcon()
    switch_icon.addPixmap(switch_on_pix, QIcon.Normal, QIcon.On)
    switch_icon.addPixmap(switch_off_pix, QIcon.Normal, QIcon.Off)

    self.control_action = QAction(switch_icon, "Control", self)
    self.control_action.setToolTip("Control ON/OFF")
    self.control_action.setCheckable(True)
    self.control_action.triggered.connect(partial(self.onClick, switch_icon))

image|250x50 image|250x50

The two screenshots above show the 'on' switch in the selected state and the 'off' switch not selected. The selected state has a gray background which is added by Qt. The question is if there is a way to get rid of the gray background because this doesn't bring any added value in this case.

Thanks, Rik


ERIC_HUYGEN | 2021-05-11 21:05:30 UTC | #2

Create GUI Applications with Python & Qt5 by Martin Fitzpatrick — (PyQt5 Edition) The hands-on guide to making apps with Python — Over 10,000 copies sold!

More info Get the book

I have solved this issue in the mean time. I created my own button, ToggleButton. This button inherits from QCheckBox and implements it's own paintEvent() method. The ToggleButton can take either two or three states and an additional 'disabled' state. For each of the states you will have to provide an icon, an SVG file. When you run the example code, it opens a small window as shown below. When you click the button, it toggles between the different states and changes the icon to the current state. When you press the 'disable' button, the icon corresponding to the disabled state will be painted and the icon will also be disabled. No disturbing background will be drawn.

Enjoy!

[details="Click here to see the code..."]

python
import sys
from pathlib import Path
from typing import Union

from PyQt5.QtCore import QPoint
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter
from PyQt5.QtSvg import QSvgRenderer
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QCheckBox
from PyQt5.QtWidgets import QPushButton

BUTTON_DISABLED = 0
BUTTON_SELECTED = 1
BUTTON_NOT_SELECTED = 2
BUTTON_NO_CHANGE = 3
BUTTON_ACTIONED = 4


class ToggleButton(QCheckBox):
    def __init__(
            self,
            width: int = 32,
            height: int = 32,
            name: str = None,
            status_tip: str = None,
            selected: Union[str, Path] = None,
            not_selected: Union[str, Path] = None,
            no_change: Union[str, Path] = None,
            disabled: Union[str, Path] = None,
            tristate: bool = False
    ):
        super().__init__()

        self.setFixedSize(width, height)
        self.setCursor(Qt.PointingHandCursor)
        self.setTristate(tristate)
        self.setText(name)
        self.setStatusTip(status_tip)

        self.status_tip = status_tip

        self.max_states = 3 if tristate else 2

        self.button_selected = selected
        self.button_not_selected = not_selected
        self.no_change = no_change
        self.button_disabled = disabled

        self.resource = {
            BUTTON_DISABLED: self.button_disabled,
            BUTTON_SELECTED: self.button_selected,
            BUTTON_NOT_SELECTED: self.button_not_selected,
            BUTTON_NO_CHANGE: self.no_change,
        }

        self.state = BUTTON_SELECTED
        self.disabled = False

        self.clicked.connect(self.handle_clicked)

    def handle_clicked(self):
        self.state = 1 if self.state == self.max_states else self.state + 1
        self.repaint()

    def setDisabled(self, flag: bool = True):
        self.disabled = flag
        super().setDisabled(flag)
        self.setStatusTip(f"{self.status_tip or ''} {'[DISABLED]' if flag else ''}")

    def disable(self):
        self.setDisabled(True)

    def enable(self):
        self.setDisabled(False)

    def is_selected(self):
        return self.state == BUTTON_SELECTED

    def set_selected(self, on: bool = True):
        self.state = BUTTON_SELECTED if on else BUTTON_NOT_SELECTED
        self.repaint()

    def hitButton(self, pos: QPoint):
        return self.contentsRect().contains(pos)

    def paintEvent(self, *args, **kwargs):

        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setPen(Qt.NoPen)

        self.drawCustomWidget(painter)
        painter.end()

    def drawCustomWidget(self, painter):
        renderer = QSvgRenderer()
        resource = self.resource[self.state if not self.disabled else BUTTON_DISABLED]
        renderer.load(resource)
        renderer.render(painter)


if __name__ == "__main__":

    from PyQt5.QtWidgets import QMainWindow
    from PyQt5.QtWidgets import QFrame
    from PyQt5.QtWidgets import QVBoxLayout


    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            self.resize(200, 200)

            self.container = QFrame()
            self.container.setObjectName("Container")
            self.layout = QVBoxLayout()

            button_selected = "cs-connected.svg"
            button_not_selected = "cs-not-connected.svg"
            button_no_change = "cs-connected-alert.svg"
            button_disabled = "cs-connected-disabled.svg"

            self.toggle = ToggleButton(
                name="CS-CONNECT",
                status_tip="connect-disconnect hexapod control server",
                selected=button_selected,
                not_selected=button_not_selected,
                no_change=button_no_change,
                disabled=button_disabled,
                tristate=True,
            )

            self.layout.addWidget(self.toggle, Qt.AlignCenter, Qt.AlignCenter)
            self.layout.addWidget(pb := QPushButton("disable"))
            self.container.setLayout(self.layout)
            self.setCentralWidget(self.container)

            self.pb = pb
            self.pb.setCheckable(True)
            self.pb.clicked.connect(self.toggle_disable)

            self.toggle.clicked.connect(self.toggle_clicked)

            self.statusBar()

        def toggle_disable(self, checked: bool):
            self.toggle.disable() if checked else self.toggle.enable()
            self.pb.setText("enable" if checked else "disable")

        def toggle_clicked(self, *args, **kwargs):
            sender = self.sender()
            print(f"clicked: {args=}, {kwargs=}")
            print(f"         {sender.state=}")
            print(f"         {sender.text()=}")


    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()

    sys.exit(app.exec_())

[/details]

image|312x340 image|312x340 image|312x340 image|312x340

The code is based on this YouTube tutorial [https://www.youtube.com/watch?v=NnJFi285s3M].


Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Avoid gray background for selected icons was written by Martin Fitzpatrick .

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt.