<div dir="ltr"><div><div><div>Summary:</div><div><br></div><div>Calls to QWidgetAction.requestWidget always result in losing the Python reference of a custom widget, including its class.<br></div><div><br></div><div>Explanation:</div><div><br></div><div>I was experimenting with a custom widget acting as a QAction container, and intended as a permanent widget in QStatusBar, so that I was able to add actions to it.</div><div><br></div><div>The intended behavior is similar to that of QToolBar, so I decided to override <span style="font-family:monospace">actionEvent()</span>.</div><div><br></div><div>If the event is ActionAdded with a standard QAction, I create a QToolButton, call <span style="font-family:monospace">setDefaultAction()</span> and add that button to the layout.</div><div><br></div><div>I'll skip the details about the lifespan consistency between the created widgets and their related actions, as it's not really important for this matter (I used a dictionary, to create/get action/widgets pairs, but I had to use <span style="font-family:monospace">sip.[un]wrapinstance()</span> as actions that are being deleted may nullify a previous python reference, leading to the known "underlying object has been deleted").</div><div><br></div><div>Now, the problem comes when using a QWidgetAction: since the action can be displayed in different places other than my custom widget (for example, a toolbar), I had to override <span style="font-family:monospace">createWidget()</span>, but since those widgets may alter their appearance/behavior and should obviously be consistent within the same action, I also implemented a custom method that, when called, updates all created widgets consistently.</div><div><br></div><div>In my previous (simple) usages of QWidgetAction, I always directly called <span style="font-family:monospace">createWidget()</span>, keeping a private list of created widgets in the instance updated: the overall context was contained, and my approach was fine for that purpose. That approach made things a bit clumsy, though: I had to ensure that the widget list and their connections were consistent, possibly using custom functions to iterate the private list of widgets and ensuring about the real existence of a possibly destroyed widget that could have been created temporarily (for instance, in a non persistent context menu).<br></div><div><br></div><div>Still, doing this is not consistent with the behavior of QToolBar, which always uses <span style="font-family:monospace">requestWidget()</span> for QWidgetAction, and keeps its internal list updated (available through <span style="font-family:monospace">createdWidgets()</span>), eventually deleting all <i>owned</i> widgets created for the actions added to it.<br></div><br></div>I therefore decided to take a more consistent approach, calling <span style="font-family:monospace">requestWidget()</span> from my custom container. That's when I realized that things got weird.<br><div><br></div><div>It looks like <span style="font-family:monospace">requestWidget()</span> properly keeps the custom widget reference, including Python implemented methods, but only when directly called from Qt, which is the case of QToolBar. If I do it from my custom class, I just get an "uncast" QWidget.<br></div><div><br></div>Not only do I get a basic QWidget from <span style="font-family:monospace">requestWidget()</span> if I try to create and add it in my container, but the original Python wrapper (meaning its full instance) is completely destroyed in the process. I know that I can do a <span style="font-family:monospace">sip.cast(widget, myclass)</span>, but that will just reinterpret the widget as the given type, not as an instance, thus ignoring its aspects and anything eventually set in: most importantly, whatever I may have done within its <span style="font-family:monospace">__init__()</span>.<br><br></div>I can ignore all of that and just go on with direct calls to <span style="font-family:monospace">createWidget()</span>, but I don't like that: first of all, it's not consistent; then, it can still create some issues when dealing with standard QAction containers created in/by Qt: not only QToolBar, but also QMenu or even QMenuBar. And, if virtual functions are overridden in the custom widget, the result is that they may not be properly called as (or when) necessary.<br><div><div><br></div><div>I created a basic MRE to allow you to test this issue.</div><div>I know it seems a bit convoluted, but I tried to keep it as simple as possible while showing the problem in its extent.<br></div><div><div><br></div><div><span style="font-family:monospace">class CustomWidget(QFrame):<br>    value = 0<br>    valueChanged = pyqtSignal(int)<br>    def __init__(self, parent):<br>        super().__init__(parent)<br>        self.setFrameStyle(self.Shape.StyledPanel|self.Shadow.Raised)<br>        self.label = QLabel(str(self.value))<br><br>        layout = QHBoxLayout(self)<br>        layout.setContentsMargins(10, 4, 10, 4)<br>        layout.addWidget(self.label)<br><br>    def mousePressEvent(self, event):<br>        print('click!', self)<br>        self.value += 1<br>        self.valueChanged.emit(self.value)<br>        self.label.setNum(self.value)<br><br>    def setValue(self, value):<br>        if self.value != value:<br>            self.value = value<br>            self.label.setNum(value)<br>            self.valueChanged.emit(value)<br><br>    def __del__(self):<br>        print('Python CustomWidget destroyed!')<br><br><br>class CustomAction(QWidgetAction):<br>    valueChanged = pyqtSignal(int)<br>    def createWidget(self, parent):<br>        custom = CustomWidget(parent)<br>        custom.valueChanged.connect(self.valueChanged)<br>        return custom<br><br>    def setValue(self, value):<br>        for widget in self.createdWidgets():<br>            try:<br>                widget.setValue(value)<br>            except:<br>                pass<br><br><br>class ActionContainer(QFrame):<br>    def __init__(self, parent=None):<br>        super().__init__(parent)<br>        self.setFrameStyle(self.Shape.StyledPanel|self.Shadow.Sunken)<br>        layout = QHBoxLayout(self)<br>        layout.setContentsMargins(0, 0, 0, 0)<br>        layout.addWidget(QLabel('Custom actions:'))<br><br>    def eventFilter(self, obj, event):<br>        if event.type() == event.Type.MouseButtonPress:<br>            print('Expecting a "click! <instance>"')<br>            QTimer.singleShot(1, lambda: print('Did it happen?'))<br>        return super().eventFilter(obj, event)<br><br>    def actionEvent(self, event):<br>        if event.type() == event.Type.ActionAdded:<br>            action = event.action()<br>            if isinstance(action, QWidgetAction):<br>                widget = event.action().requestWidget(self)<br>                print('adding custom widget action', type(widget))<br><br>                # the following is pointless, as it will just return the type<br>                # known to Qt (QFrame), not the custom class<br>                # widget = sip.cast(widget, CustomWidget)<br>                # print('cast attempt of custom widget:', type(widget))<br><br>                # the following will throw an AttributeError if uncommented<br>                # widget.valueChanged.connect(lambda: print('whatever'))<br>            else:<br>                widget = QToolButton()<br>                widget.setDefaultAction(action)<br>                widget.pressed.connect(lambda: print('click!', widget))<br>                print('adding standard action', type(widget))<br>            widget.installEventFilter(self)<br>            self.layout().addWidget(widget)<br><br><br>app = QApplication([])<br>window = QMainWindow()<br>window.setCentralWidget(QScrollArea())<br><br>tb = QToolBar('toolbar')<br>window.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb)<br><br>statusWidget = ActionContainer()<br>window.statusBar().addPermanentWidget(statusWidget)<br><br>tb.addAction(CustomAction(window))<br><br>tb.addSeparator()</span></div><div><span style="font-family:monospace"><br></span></div><div><span style="font-family:monospace">action1 = QAction('Test', window)<br>action2 = CustomAction(window)<br>action3 = CustomAction(window)<br>action3.valueChanged.connect(action2.setValue)<br>tb.addAction(action1)<br>tb.addAction(action2)<br>tb.addAction(action3)<br>statusWidget.addAction(action1)<br>statusWidget.addAction(action2)<br>statusWidget.addAction(action3)<br><br>window.show()</span></div><div><br></div><div>As said, I know I can just settle with a private list of created widgets, but I don't really like that approach. I know that it's possible to cast the returned widget based on the original Python object type (it works fine on QToolBar) but I'm not completely sure it's possible with <span style="font-family:monospace">requestWidget()</span>. If it is, I'd ask to fix it, otherwise it will still good to know that.<br><br></div><div>Note that I tested the above with recent versions of both PyQt5 and 6. They're not the latest available, though: 5.15.2 and 6.3.1.</div><div><br></div><div>Thank you!<br></div><div>MaurizioB/musicamante<br></div><div><div><div><span class="gmail_signature_prefix">-- </span><br><div dir="ltr" class="gmail_signature" data-smartmail="gmail_signature">È difficile avere una convinzione precisa quando si parla delle ragioni del cuore. - "Sostiene Pereira", Antonio Tabucchi<br><a href="http://www.jidesk.net" target="_blank">http://www.jidesk.net</a></div></div></div></div></div></div></div>