QWidgetAction.requestWidget doesn't keep the Python object

Phil Thompson phil at riverbankcomputing.com
Sat Nov 18 08:19:57 GMT 2023


On 18/11/2023 06:12, Maurizio Berti wrote:
> Summary:
> 
> Calls to QWidgetAction.requestWidget always result in losing the Python
> reference of a custom widget, including its class.
> 
> Explanation:
> 
> 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.
> 
> The intended behavior is similar to that of QToolBar, so I decided to
> override actionEvent().
> 
> If the event is ActionAdded with a standard QAction, I create a
> QToolButton, call setDefaultAction() and add that button to the layout.
> 
> 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 sip.[un]wrapinstance() as actions that are being deleted may 
> nullify
> a previous python reference, leading to the known "underlying object 
> has
> been deleted").
> 
> 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 createWidget(), 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.
> 
> In my previous (simple) usages of QWidgetAction, I always directly 
> called
> createWidget(), 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).
> 
> Still, doing this is not consistent with the behavior of QToolBar, 
> which
> always uses requestWidget() for QWidgetAction, and keeps its internal 
> list
> updated (available through createdWidgets()), eventually deleting all
> *owned* widgets created for the actions added to it.
> 
> I therefore decided to take a more consistent approach, calling
> requestWidget() from my custom container. That's when I realized that
> things got weird.
> 
> It looks like requestWidget() 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.
> 
> Not only do I get a basic QWidget from requestWidget() 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 sip.cast(widget, myclass), 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
> __init__().
> 
> I can ignore all of that and just go on with direct calls to 
> createWidget(),
> 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.
> 
> I created a basic MRE to allow you to test this issue.
> I know it seems a bit convoluted, but I tried to keep it as simple as
> possible while showing the problem in its extent.
> 
> class CustomWidget(QFrame):
>     value = 0
>     valueChanged = pyqtSignal(int)
>     def __init__(self, parent):
>         super().__init__(parent)
>         self.setFrameStyle(self.Shape.StyledPanel|self.Shadow.Raised)
>         self.label = QLabel(str(self.value))
> 
>         layout = QHBoxLayout(self)
>         layout.setContentsMargins(10, 4, 10, 4)
>         layout.addWidget(self.label)
> 
>     def mousePressEvent(self, event):
>         print('click!', self)
>         self.value += 1
>         self.valueChanged.emit(self.value)
>         self.label.setNum(self.value)
> 
>     def setValue(self, value):
>         if self.value != value:
>             self.value = value
>             self.label.setNum(value)
>             self.valueChanged.emit(value)
> 
>     def __del__(self):
>         print('Python CustomWidget destroyed!')
> 
> 
> class CustomAction(QWidgetAction):
>     valueChanged = pyqtSignal(int)
>     def createWidget(self, parent):
>         custom = CustomWidget(parent)
>         custom.valueChanged.connect(self.valueChanged)
>         return custom
> 
>     def setValue(self, value):
>         for widget in self.createdWidgets():
>             try:
>                 widget.setValue(value)
>             except:
>                 pass
> 
> 
> class ActionContainer(QFrame):
>     def __init__(self, parent=None):
>         super().__init__(parent)
>         self.setFrameStyle(self.Shape.StyledPanel|self.Shadow.Sunken)
>         layout = QHBoxLayout(self)
>         layout.setContentsMargins(0, 0, 0, 0)
>         layout.addWidget(QLabel('Custom actions:'))
> 
>     def eventFilter(self, obj, event):
>         if event.type() == event.Type.MouseButtonPress:
>             print('Expecting a "click! <instance>"')
>             QTimer.singleShot(1, lambda: print('Did it happen?'))
>         return super().eventFilter(obj, event)
> 
>     def actionEvent(self, event):
>         if event.type() == event.Type.ActionAdded:
>             action = event.action()
>             if isinstance(action, QWidgetAction):
>                 widget = event.action().requestWidget(self)
>                 print('adding custom widget action', type(widget))
> 
>                 # the following is pointless, as it will just return 
> the
> type
>                 # known to Qt (QFrame), not the custom class
>                 # widget = sip.cast(widget, CustomWidget)
>                 # print('cast attempt of custom widget:', type(widget))
> 
>                 # the following will throw an AttributeError if 
> uncommented
>                 # widget.valueChanged.connect(lambda: 
> print('whatever'))
>             else:
>                 widget = QToolButton()
>                 widget.setDefaultAction(action)
>                 widget.pressed.connect(lambda: print('click!', widget))
>                 print('adding standard action', type(widget))
>             widget.installEventFilter(self)
>             self.layout().addWidget(widget)
> 
> 
> app = QApplication([])
> window = QMainWindow()
> window.setCentralWidget(QScrollArea())
> 
> tb = QToolBar('toolbar')
> window.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb)
> 
> statusWidget = ActionContainer()
> window.statusBar().addPermanentWidget(statusWidget)
> 
> tb.addAction(CustomAction(window))
> 
> tb.addSeparator()
> 
> action1 = QAction('Test', window)
> action2 = CustomAction(window)
> action3 = CustomAction(window)
> action3.valueChanged.connect(action2.setValue)
> tb.addAction(action1)
> tb.addAction(action2)
> tb.addAction(action3)
> statusWidget.addAction(action1)
> statusWidget.addAction(action2)
> statusWidget.addAction(action3)
> 
> window.show()
> 
> 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 
> requestWidget().
> If it is, I'd ask to fix it, otherwise it will still good to know that.
> 
> 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.
> 
> Thank you!
> MaurizioB/musicamante

Are you able to build PyQt yourself? If so please try the attached.

Phil
-------------- next part --------------
A non-text attachment was scrubbed...
Name: qwidgetaction.sip
Type: text/x-c++
Size: 966 bytes
Desc: not available
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20231118/d046498d/attachment.bin>


More information about the PyQt mailing list