Im developing a PyQt application which modifies buttons icons dynamically. The icons are created with QStyle, stored as class parameters and then put into the buttons with the setIcon() method.
Since the icons are modified dynamically, i want an elegant way of checking what is the current button icon. The original Qt documentation mentions the setIcon() method but i couldnt find a getIcon() method. In the code i append below, i could access an icon() property of the buttons, but it doesnt seem to be what im looking for because it does not seem useful in determining which is the current icon.
import sys from PyQt5.QtWidgets import QApplication, QGridLayout, QPushButton, QStyle, QWidget class Window(QWidget): def __init__(self): super(Window, self).__init__() self.back_arrow=self.style().standardIcon(getattr(QStyle, "SP_ArrowBack")) self.first_pushbutton=QPushButton("first") print(self.first_pushbutton.icon()) self.first_pushbutton.setIcon(self.back_arrow) self.first_pushbutton.clicked.connect(self.first_clicked) print(self.first_pushbutton.icon()) layout = QGridLayout() layout.addWidget(self.first_pushbutton) self.setLayout(layout) def first_clicked(self): print(self.sender().icon()==self.back_arrow) app = QApplication(sys.argv) w = Window() w.show() app.exec_()
After executing the code, clicking the button prints
False
In the screen, which doesnt seem intuitive to me. I also tried changing the first_clicked() function to
print(self.sender().icon().objectName())
which resulted in “AttributeError: ‘QIcon’ object has no attribute ‘objectName'”. Trying the setObjectName method results in similar error.
The prints before and after self.first_pushbutton.setIcon(self.back_arrow) in the init function always print the same result, for example
<PyQt5.QtGui.QIcon object at 0x7fc31da8ddc8> <PyQt5.QtGui.QIcon object at 0x7fc31da8ddc8>
So im not even sure that im altering the icon() property. Is there a recommended way of checking the current icon? I know i could set their initial icons and guarantee that these icons are only going to get changed when calling the apropriate method, but doing this seems sloppy to me.
Advertisement
Answer
First of all, objectName()
is a specific property of classes that inherit from QObject. QIcon is not a QObject subclass (as we can clearly see from its documentation, there is no “Inherits:” field in its header description), so that property is not available as there is no Qt property support at all.
QIcon has limited support for the name()
value, which could return an unique identifier, but not necessarily, as it depends on the OS and current style: for instance, on my system I get a "folder"
icon name for SP_DirIcon
, but an empty string for SP_DriveCDIcon
.
So, even considering the QStyle, the icon name is not a reliable way to compare icons. The name
support completely depends on the Qt implementation, underlying OS, support of the icon engine and current theme availability.
Another important aspect to consider is that, similarly to QPixmap, when you set a QIcon on a widget, you are not actually setting that QIcon instance, but rather a copy of it.
Also, QIcon is actually a sort of “abstract object”, which actually interfaces with a QIconEngine that eventually returns QPixmap or paints the icon on a given paint surface.
This means that you cannot have any actual comparison between the “original” and the icon()
getter function of the widget, as you’ll always have two different objects, even if they draw the same contents. In fact, if you look at the QIcon documentation, there is no ==
operator, exactly like QPixmap, and opposite to QImage, which is instead an “actual” image (note: it’s actually a bit more complicated than this, I’m simplifying this for the sake of explanation in Python terms).
The only way to ensure that two QIcons are exactly the same is to use the cacheKey()
. Note that, as the documentation explains, adding pixmaps (or files) to a QIcon alters the key, so even if you add the same pixmap to two previously identical QIcons, those keys won’t match anymore.
Consider that even two QIcons created from the same file (as in QIcon('path/to/image')
) will have different cache keys.
def first_clicked(self): # this will print True print(self.sender().icon().cacheKey() == self.back_arrow.cacheKey())
If you alter the QIcon by adding other images (or you created them with the file constructor), then the only alternative is to compare the results of the availableSizes()
(and states/modes) of each QIcon, and, if they do match, transform each resulting pixmap()
of both icons into QImages and then compare those images.
Of course, this has a cost in performance, especially if you’re using high resolution files: there is no direct way to know what pixmap/file has been added and for what mode/state combination they have been set, unless you create your own QIconEngine, so the QImage comparison will cycle through all icon sizes, states and modes (meaning up to 8 QImage comparisons for each added file/size, until any pixel difference has been found).
This is a possible implementation of the above:
def compareIcons(a, b): if a.cacheKey() == b.cacheKey: return True aSizes = a.availableSizes() bSizes = b.availableSizes() if aSizes != bSizes: return False for state in (QIcon.Off, QIcon.On): for mode in (QIcon.Normal, QIcon.Disabled, QIcon.Active, QIcon.Selected): for size in aSizes: if (a.pixmap(size, mode, state).toImage() != b.pixmap(size, mode, state).toImage()): return False return True
With the code above, you can also compare QIcons created with files:
class Window(QWidget): def __init__(self): super(Window, self).__init__() self.back_arrow = QIcon('someIcon.png') self.other_arrow = QIcon('someIcon.png') self.first_pushbutton = QPushButton("first") self.first_pushbutton.setIcon(self.back_arrow) self.first_pushbutton.clicked.connect(self.first_clicked) layout = QGridLayout(self) layout.addWidget(self.first_pushbutton) def first_clicked(self): buttonIcon = self.sender().icon() print('compare original icon cache keys:', buttonIcon.cacheKey() == self.back_arrow.cacheKey()) print('compare different icon cache keys:', buttonIcon.cacheKey() == self.other_arrow.cacheKey()) print('compare different icon images:', compareIcons(buttonIcon, self.other_arrow))
Actually, you can also synthesize the ==
operator with the above function, as long as it’s added at the very beginning of your imports, and along with the following:
# somewhere in the first imported modules of your main script: QIcon.__eq__ = lambda icon, other: compareIcons(icon, other)
Note for Python 2: __ne__
(which corresponds to !=
, as in Not Equal) must also be overridden, and it should return not compareIcons(icon, other)
.
Then, you can actually use the ==
or !=
operators:
class Window(QWidget): # ... def first_clicked(self): # ... print('compare different icon images:', buttonIcon == self.other_arrow)