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:
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))
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!
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..."]
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]
The code is based on this YouTube tutorial [https://www.youtube.com/watch?v=NnJFi285s3M].
PyQt6 Crash Course — a new tutorial in your Inbox every day
Beginner-focused crash course explaining the basics with hands-on examples.