0

Yes, I have seen:

... but none of these approaches seem to work in my example:

Basically, this is an example that tries to use QtAwesome to provide a spinning icon to a QMessageBox; there is a class Spin in QtAwesome that does animations, which has a method _update: I would like to monkeypatch this method, so first the original method is called, and then a message is printed - so I have this:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox
import qtawesome as qta

class Example(QWidget):
  def __init__(self):
    super().__init__()
    self.initUI()

  def initUI(self):
    self.setGeometry(300, 300, 300, 220)
    self.setWindowTitle('Hello World')
    self.show()
    msgBox = QMessageBox( QMessageBox.Information, "Title", "Content ...", QMessageBox.Cancel )
    orig_ipmsize = msgBox.iconPixmap().size()
    print(orig_ipmsize.width(), orig_ipmsize.height()) # 0 0 for QMessageBox.NoIcon; 32 32 for QMessageBox.Information
    animation = qta.Spin(msgBox, autostart=True)

    DO_MONKEYPATCH = 2 # 1 or 2

    if DO_MONKEYPATCH == 1:
      old_anim_update = animation._update
      def new_anim_update(self):
        old_anim_update(self) # TypeError: Spin._update() takes 1 positional argument but 2 were given
        print("new_anim_update")
      animation._update = new_anim_update.__get__(animation, qta.Spin) # https://stackoverflow.com/a/28127947

    elif DO_MONKEYPATCH == 2:
      def update_decorator(method):
        def decorate_update(self=None):
          method(self) # TypeError: Spin._update() takes 1 positional argument but 2 were given
          print("decorate_update")
        return decorate_update
      animation._update = update_decorator(animation._update) # https://stackoverflow.com/a/8726680

    #print(animation._update)
    spin_icon = qta.icon('fa5s.spinner', color='red', animation=animation)
    msgBox.setIconPixmap(spin_icon.pixmap(orig_ipmsize)) #msgBox.setIcon(spin_icon)
    #animation.start()
    returnValue = msgBox.exec()

if __name__ == '__main__':
  app = QApplication(sys.argv)
  ex = Example()
  sys.exit(app.exec_())

However, no matter which DO_MONKEYPATCH method I choose, I get TypeError: Spin._update() takes 1 positional argument but 2 were given?


OK, just noticed how both errors apply while writing the question, and found if I change the "old method" calls to NOT use a (self) argument -- i.e. I change old_anim_update(self) / method(self), to old_anim_update() / method() -- then both DO_MONKEYPATCH methods allow for running without the positional argument error - however only DO_MONKEYPATCH method 1 seems to preserve self:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox
import qtawesome as qta

class Example(QWidget):
  def __init__(self):
    super().__init__()
    self.initUI()

  def initUI(self):
    self.setGeometry(300, 300, 300, 220)
    self.setWindowTitle('Hello World')
    self.show()
    msgBox = QMessageBox( QMessageBox.Information, "Title", "Content ...", QMessageBox.Cancel )
    orig_ipmsize = msgBox.iconPixmap().size()
    print(orig_ipmsize.width(), orig_ipmsize.height()) # 0 0 for QMessageBox.NoIcon; 32 32 for QMessageBox.Information
    animation = qta.Spin(msgBox, autostart=True)

    DO_MONKEYPATCH = 1 # 1 or 2

    if DO_MONKEYPATCH == 1:
      old_anim_update = animation._update
      def new_anim_update(self):
        old_anim_update() # no error
        print("new_anim_update {}".format(self)) # self is <qtawesome.animation.Spin object at 0x00000238f1d45f10>
      animation._update = new_anim_update.__get__(animation, qta.Spin) # https://stackoverflow.com/a/28127947

    elif DO_MONKEYPATCH == 2:
      def update_decorator(method):
        def decorate_update(self=None):
          method() # TypeError: Spin._update() takes 1 positional argument but 2 were given
          print("decorate_update {}".format(self)) # self is None
        return decorate_update
      animation._update = update_decorator(animation._update) # https://stackoverflow.com/a/8726680

    #print(animation._update)
    spin_icon = qta.icon('fa5s.spinner', color='red', animation=animation)
    msgBox.setIconPixmap(spin_icon.pixmap(orig_ipmsize)) #msgBox.setIcon(spin_icon)
    #animation.start()
    returnValue = msgBox.exec()

if __name__ == '__main__':
  app = QApplication(sys.argv)
  ex = Example()
  sys.exit(app.exec_())

So, it seems that DO_MONKEYPATCH == 1 method is the answer to this question as originally posed - but I am still worried, how does my call to old_anim_update(), without any original references to self, call the old method correctly? Or is there a more correct method to do this kind of monkeypatch?

1
  • Why complicate things with monkey patching while you could just subclass qta.Spin()? Commented Apr 14, 2024 at 22:43

1 Answer 1

0

The reason for which just doing old_anim_update() without self works is because old_anim_update is already a reference to the instance method, and you don't need to call it with self.

When you do update_decorator(animation._update), you're passing a reference to the function that is already bound to the instance, thus becoming an instance method.

A simplified version that clarifies the above could be the following:

something = animation._update
something()

The above will automatically call the qta.Spin._update() with animation as its self.

A case where self would be a necessary argument is in case you were using the reference to the method in the namespace of the class:

something = qta.Spin._update
something(animation)

In the case above, the animation argument becomes the self in the def _update(self). That's how the default instance methods were normally called in Python before the advent of super(): the above would be identical to qta.Spin._update(animation).

Note that while monkey patching may generally work fine for simple aspects, it's usually better to create a subclass, especially because that makes it reusable.

A possible solution is to just use the parent_widget (which is the message box), and also get the standard pixmap size using the style's metrics, which is what QMessageBox actually does internally:

class MsgBoxSpin(qta.Spin):
    def _update(self):
        super()._update()
        iconSize = self.parent_widget.style().pixelMetric(
            QStyle.PM_MessageBoxIconSize, None, self.parent_widget)
        self.parent_widget.setIconPixmap(self.icon.pixmap(
            iconSize, iconSize))

    def setIcon(self, icon):
        self.icon = icon
...

animation = qta.Spin(msgBox, autostart=True)
spin_icon = qta.icon('fa5s.spinner', color='red', animation=animation)
animation.setIcon(spin_icon)

The above is necessary because the given parent is actually the message box, and _update() will call its QWidget.update(), which would probably have no result on the widget showing the pixmap; also, the qta.Spin object actually knows nothing about the actual icon being shown, meaning that:

  • we must explicitly "tell" it the icon to use in the override;
  • we can only use that animation with a specific icon and a specific target message box;

It works, but it also is a bit bulky.

There is another alternative, though. When I want to just display a QIcon, I sometimes use a dummy button that has no border or background set, and with any interaction disabled.

QMessageBox displays the icon in a QLabel, which has a hardcoded object name (qt_msgboxex_icon_label), so we can retrieve it using QObject.findChild(), create a layout for it, and add the dummy button to it. Finally, we set a transparent QPixmap for with the proper size (explained above), ensuring that it always requires the necessary space, even if it's empty.
All this makes using an actual QIcon object possible within a QMessageBox.

class MessageBox(QMessageBox):
    _iconWidget = None
    def setIcon(self, icon):
        if isinstance(icon, QMessageBox.Icon):
            super().setIcon(icon)
            if self._iconWidget:
                self._iconWidget.deleteLater()
                self._iconWidget = None
        elif isinstance(icon, QIcon):
            self.iconWidget().setIcon(icon)

    def iconWidget(self):
        if not self._iconWidget:
            label = self.findChild(QLabel, 'qt_msgboxex_icon_label')
            iconSize = self.style().pixelMetric(
                QStyle.PM_MessageBoxIconSize, None, self)
            pm = QPixmap(iconSize, iconSize)
            pm.fill(Qt.transparent)
            self.setIconPixmap(pm)

            layout = label.layout()
            if layout is None:
                layout = QVBoxLayout(label)
                layout.setContentsMargins(0, 0, 0, 0)

            self._iconWidget = QPushButton()
            layout.addWidget(self._iconWidget)
            self._iconWidget.setStyleSheet('''
                QPushButton {
                    border: none;
                    background: none;
                }
            ''')
            self._iconWidget.setIconSize(QSize(iconSize, iconSize))
            self._iconWidget.setFocusPolicy(Qt.NoFocus)
            self._iconWidget.setAttribute(Qt.WA_TransparentForMouseEvents)
        return self._iconWidget

Then you can create the icon using the custom message box function iconWidget() (since it needs the parent for updating), and you don't need any monkey patching or icon subclassing anymore.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.