Customize QMenu display with QProxyStyle
John Sturtz
john at sturtz.org
Fri Jun 13 19:02:21 BST 2025
Hi Maurizio.
I don't really think I'm stupid; comic self-denigration is one of my
MO's. But I was guilty of tunnel-vision on that one. I hadn't heard
the term 'XY problem', but yes, that fits. At least that carries with
it a certain degree of having tried to solve the problem on my own
before asking about it, and that's how we learn.
I've long been suspicious of the crash-on-exit problem because, like
you, I suspect it is a symptom of a deeper issue (like maybe a condition
that might cause the program to crash when it isn't exiting). I figured
the likely cause was a persistent reference to an object that no longer
existed.
I always try to create an MRE when I can. The hurdle with this
particular problem is that I've never had much clue as to what part of
my code is causing it. But now I have a hunch it might be related to
creating a style proxy, and I can look into that. (I did get many
instances of small test programs crashing in just the same way when I
was messing around trying to create a style proxy ...)
Thanks again!
/John
------ Original Message ------
>From "Maurizio Berti" <maurizio.berti at gmail.com>
To "John Sturtz" <john at sturtz.org>
Cc pyqt at riverbankcomputing.com
Date 6/9/2025 11:04:57 PM
Subject Re: Re[2]: Customize QMenu display with QProxyStyle
>Hi John!
>You're quite welcome, but you don't have to feel stupid: realizing our
>own mistakes and being able to work with them (not just around them,
>but also realizing how our awareness of them can help us) actually
>makes us *less* "stupid"...
>
>You just fell into an XY problem, which can still happen even with
>experience: it's just less common (usually) and difficult to detect
>when becoming more experienced (assuming one "knows" enough), but still
>not impossible to face.
>
>But now I'm curious: you've been experiencing a quite sporadic but
>still somehow persistent crash on shutdown. In my experience (including
>indirect one), it's common to just ignore them (the program is
>quitting, who cares?), but that's also a possible symptom of an issue
>that may cause further, and unknown problems.
>
>When coding with Python, we make a lot of assumptions, mostly related
>to the internal reference count and automatic garbage collection. That
>may still be an issue with pure C++ code, and even more when dealing
>with Python bindings, especially for large/complex programs. Qt objects
>(including non-QObjects) can still exist outside Python after they've
>been created and their references have been removed, just like their
>references can still be considered as valid even though their wrapped
>Qt objects have been deleted.
>
>I'm no C++ programmer, but I believe that the quit-crash mentioned in
>this thread would still happen when using a pure C++ program. In fact,
>it could even cause further issues before quitting when going on with
>the program implementation.
>
>It's always important to consider the multiple relations between
>objects. I'd suggest you create a proper MRE of your issue so that we
>could help you find the cause (and eventually post it as a separate
>question). It will be annoying and extenuating to create such an
>example, sure. But that process will also, most certainly, help you in
>any way, no matter what. Either you'll find the culprit on your own
>during the MRE creation, or we'll be able to help you inspect that
>issue.
>
>For what we know, you may even find it's a Qt or PyQt bug, which would
>be quite important to others too.
>
>Good luck!
>MaurizioB
>
>Il giorno mar 10 giu 2025 alle ore 05:19 John Sturtz <john at sturtz.org>
>ha scritto:
>>Hi Maurizio.
>>
>>Wow. Thank you so much for your time and assistance. This is great
>>information!
>>
>>I'm a bit embarrassed to admit this: The reason I started down the
>>path of using QProxyStyle to display menu items is because I was
>>trying to implement displaying each item's description and shortcut
>>key, with the description left-justified and the shortcut key
>>right-justified (as they typically are).
>>
>>Which is embarrassing because if I'd just looked around a little, I
>>would have discovered that capability is built into QMenu. (And not
>>just one way, but actually two: either .setShortcut() for the menu
>>item's QAction, or specify "<description>\t<shortcut>" for the
>>QAction's text).
>>
>>So, color me stupid.
>>
>>But, as always, by asking you guys, I've learned a tremendous amount,
>>and for that I am appreciative!
>>
>>In fact, in the app I've been working on (for years now, as you know),
>>I already had implemented a simple QProxyStyle class. And for some
>>time, the app has had exhibited a mysterious (sporadic, infrequent)
>>tendency to crash when shutting down. I've never been able to figure
>>out why, and I have a hunch failure to properly account for ownership
>>of the base style of the proxy might be the reason.
>>
>>And I had already begun to realize how much complexity there was going
>>to be in reimplementing QMenu's behavior. In this case, it's pretty
>>clear discretion is the better part of valor ...
>>
>>Thanks again!
>>
>>/John
>>
>>
>>------ Original Message ------
>>From "Maurizio Berti" <maurizio.berti at gmail.com>
>>To "John Sturtz" <john at sturtz.org>
>>Cc pyqt at riverbankcomputing.com
>>Date 6/9/2025 8:47:55 PM
>>Subject Re: Customize QMenu display with QProxyStyle
>>
>>>Although your question is not strictly pyqt-related (therefore a bit
>>>off-topic), I feel the urge to add further considerations, also
>>>considering what Charles has already written.
>>>
>>>First of all, a QProxyStyle always has a "base style", and takes full
>>>ownership of it. That style is used as a base for everything that the
>>>proxy doesn't implement on its own.
>>>when using the QProxyStyle constructor without arguments, it always
>>>creates a new instance of the native style (the one normally used
>>>when a new QApplication is created) as its base: doing
>>>app.setStyle(Style()) will *not* use the style eventually used with a
>>>previous app.setStyle(<some other style>) as its base;
>>>a new style instance is also created when using the string
>>>constructor: QProxyStyle('fusion') is syntactic sugar for
>>>QProxyStyle(QStyleFactory.create('fusion')) (as explained in the
>>>related docs);
>>>when explicitly using an *existing* QStyle instance for its
>>>constructor argument, it will use that specific style instance as its
>>>base;
>>>In all cases, though, the proxy always takes full ownership of the
>>>base style, no matter what.
>>>This means that you should be *very* careful in trying to use the
>>>style of a widget as a base for the proxy: doing something like
>>>"widget.setStyle(MyStyle(widget.style())" can have catastrophic
>>>results.
>>>
>>>Remember that, by default, all widgets use the application style
>>>object (meaning it's the same instance): QWidget.style() does not
>>>return a "unique" style instance for the widget, but the style the
>>>widget is using: in normal conditions, widget.style() is the *same
>>>object* as QApplication.style().
>>>
>>>Creating a proxy style with a QStyle instance as its base, that is
>>>also (or potentially) used elsewhere, is quite dangerous: the most
>>>important issue is when the proxy is eventually deleted, which is
>>>something that happens when the widget it's used on is deleted: even
>>>though you may not explicitly delete the widget, the deletion may
>>>happen whenever any parent/owner of that widget is, something that
>>>also happens when the QApplication is being quit, as it needs to
>>>properly "clean up" all QObjects before actually quitting and
>>>returning its exit code.
>>>
>>>This is exactly the reason for the delay and (possibly silent) crash
>>>you see when closing the program; while the deletion order of sibling
>>>widgets may be completely arbitrary, it always follows the object
>>>tree (a parent is deleted only as soon as all its children are): if
>>>you set a proxy style for a widget using a style that was not owned
>>>by that widget, when the widget is deleted it will also delete the
>>>proxy *and* the base style, but that style is potentially also (and
>>>still) being used by other widgets (and the application) as well,
>>>leading to a segmentation fault due to attempting to access a no-more
>>>existent object during the deletion process.
>>>
>>>It may be possible to reparent the base style after setting the proxy
>>>(for instance, reparenting to the QApplication instance), but that
>>>would probably be inappropriate anyway: not only I'm not completely
>>>sure it would still make it safe enough, but QProxyStyle also calls
>>>an internal setProxy() function, meaning that some QStyle functions
>>>may still rely on the newly set proxy even for widgets that still use
>>>the original style.
>>>
>>>If you only want to target *one* specific widget instance (or
>>>subclass instance), the safest approach is to use the string or
>>>QStyleFactory way, using the QApplication style. The following should
>>>suffice:
>>>
>>>baseStyleName = QApplication.style().objectName()
>>>myWidget.setStyle(MyStyle(baseStyleName))
>>># which is identical to:
>>>myWidget.setStyle(MyStyle(QStyleFactory.create(baseStyleName)))
>>>
>>>Note: the above relies on the style object name, it only works for
>>>standard and properly implemented QStyles (those that have object
>>>names that match the results of QStyleFactory.keys()) and when *not*
>>>using stylesheets (read more below on this).
>>>
>>>If you instead want to target all widgets of that same type (in this
>>>case, all menus), you can just create the proxy instance without
>>>arguments, and set it for the whole application.
>>>
>>>That said, as Charles wrote, overriding drawItemText alone is
>>>inappropriate, as it's almost always insufficient.
>>>The only occurrence I'm aware of a widget directly calling
>>>drawItemText() is from for QLabels that only have plain text set (or
>>>that force the PlainText textFormat, instead of the default
>>>AutoText).
>>>
>>>Any other widget type will use QStyle functions such as
>>>drawPrimitive(), drawControl() or drawComplexControl(), and it's up
>>>to the style to *eventually* call drawItemText internally: that
>>>function is just provided as a convenience that *could* be used by a
>>>style, but styles are not required to use it.
>>>Some styles do call it in some cases, but not in others (usually
>>>relying on the basic QPainter.drawText()) and there is absolutely no
>>>consistency required for that. Some styles even have their own
>>>internal functions to draw text depending on the widget or
>>>[sub]control type, as a more advanced alternative to drawItemText.
>>>
>>>QStyleSheetStyle (the private style used whenever a style sheet
>>>affects a widget) does use drawItemText for many widgets, and, in
>>>fact, that's one of the few functions that can be effectively
>>>overridden in a proxy style when using style sheets, but it's largely
>>>pointless as it doesn't provide any context of the widget that is
>>>being drawn, and also requires the stylesheet to actually affect the
>>>display of the widget: if the QSS rules don't affect the widget, then
>>>QStyleSheetStyle will just use the style it's currently based on: if
>>>its own base style is a proxy, and that proxy doesn't use
>>>drawItemText, then we're back to square one.
>>>Interestingly enough, QStyleSheetStyle does not use drawItemText for
>>>QMenu items, therefore it's completely useless for this case.
>>>
>>>Regarding the note about the code snippet above, since setting a
>>>stylesheet on the application (or on widgets) internally sets a
>>>QStyleSheetStyle as "primary" style, QApplication.style() or
>>>QWidget.style() will return a style that has an empty object name.
>>>Trying to go through the meta object system would be ineffective as
>>>well, as style().metaObject().className() will obviously return
>>>"QStyleSheetStyle". QStyleSheetStyle *does* have a baseStyle()
>>>function (similar to that of QProxyStyle), but it's unfortunately
>>>private (I've submitted https://bugreports.qt.io/browse/QTBUG-132201
>>>about this, but it's been labeled for Qt7). The only way to work
>>>around this, in case you need to apply application-wide style sheets,
>>>is to get the default style name as soon as the QApplication is
>>>created (but *before* setting any QSS) and keep a reachable reference
>>>to it, either as a global variable, or as a dynamic property of the
>>>QApplication (eg: app.setProperty('defaultStyleName',
>>>app.style().objectName()), eventually retrievable through
>>>QApplication.instance().property('defaultStyleName')).
>>>
>>>The reason for which you may see rounded corners when applying the
>>>proxy (or without setting the "fusion" style in any way) is that the
>>>default Qt style in your system does use rounded corners.
>>>There are only a few widgets that provide rounded corners (achieved
>>>through QWidget.setMask()) for top level widgets: QToolTip, the popup
>>>of QComboBox, and QMenu. This only happens if the style requires it,
>>>though: for the above classes, the widget queries QStyle.styleHint(),
>>>and eventually calls setMask() on itself with the returned value.
>>>If you get rounded corners by default (without setting any style), it
>>>means that the default style for your system uses them, therefore
>>>doing setStyle(Style()) (without arguments) will still get you those
>>>rounded corners, while setStyle(Style('fusion')) will not, because it
>>>will then follow the behavior of "fusion" as its base style (which
>>>has straight corners), just like doing setStyle('fusion') would.
>>>
>>>Considering all the above, overriding drawControl() and checking
>>>CE_MenuItem is normally appropriate, but there are many aspects you
>>>should consider.
>>>
>>>First of all, you must remember that not all widgets are completely
>>>managed by Qt. This is the case of native dialogs, or the menu bar
>>>for macos and some linux distros. I haven't used macos for years and
>>>have never used such unified menubars on Linux, so I'm not completely
>>>sure that overriding QStyle painting for menu items would work in
>>>these situations.
>>>
>>>No matter what, your attempts are quite flawed.
>>>Even though the code obviously is a "proof of concept", your
>>>drawControl() override is based on wrong and too simplistic
>>>assumptions; there are lots of issues that could or should be
>>>consider, but, at the very least, you failed to consider:
>>>the margins that the default style needs to properly paint an item
>>>*within* the menu; the vertical ones may be not that relevant, but
>>>the horizontal one are important, otherwise the text may be shown too
>>>close to the horizontal margins;
>>>actions may be disabled and therefore shown differently;
>>>actions may contain an icon and/or a check/radio indicator (the
>>>layout system of QMenu should make that unimportant, but still
>>>relevant to consider);
>>>proper color roles (disabled/highlighted actions normally have
>>>different background and text colors);
>>>border/background drawing (styles usually draw items differently
>>>depending on their enabled/disabled and/or normal/selected states);
>>>hints for actions related to submenus (some styles normally display
>>>an arrow or something similar);
>>>the font of the item (actions do have a font property!)
>>>the action shortcuts (both using the "&" prefix for "accelerators",
>>>or key sequences);
>>>
>>>At the very least, the override should try to use the default
>>>implementation without the action text, and eventually attempt to
>>>draw the text considering all possible options and some educated
>>>guesses.
>>>
>>>An example of this attempt is shown in the answer to this post:
>>>https://stackoverflow.com/q/59218378
>>>It basically calls the base implementation after clearing the option
>>>text, and then proceeds to draw the text on its own. It still makes
>>>some assumptions (eg: the margin, no shortcuts/accelerator, no text
>>>font/color), but it's certainly more appropriate.
>>>
>>>Remember: as with other complex widgets, overriding the behavior of
>>>QMenu is not an easy task. Changing the text alignment of menu items
>>>may seem a relatively easy task, but it actually implies lots of
>>>low-level aspects, most of which cannot be assumed.
>>>
>>>Best regards,
>>>MaurizioB
>>>
>>>
>>>Il giorno lun 9 giu 2025 alle ore 06:14 John Sturtz <john at sturtz.org>
>>>ha scritto:
>>>>Hello again PyQt sages. Hoping for some insight here -- despite a
>>>>few hours' time fiddling with this, I don't seem to even be getting
>>>>past square one.
>>>>
>>>>I'm trying to modify display of items in a QMenu using QProxyStyle.
>>>>Basically, I've defined a class named Style that derives from
>>>>QProxyStyle, and re-implements drawItemText() (which, just for
>>>>starters, tries to right-justify the menu item text).
>>>>
>>>>I create a QMenu object, create an object of the Style class, and
>>>>call .setStyle() to set it as the menu's style.
>>>>
>>>>It may or may not be the case that my drawItemText() implementation
>>>>successfully right-justifies the text. I'll never know, because it
>>>>never gets called. What (probably really basic thing) am I missing?
>>>>
>>>>Thanks! [short sample code attached]
>>>>
>>>>/John
>>>
>>>
>>>--
>>>È difficile avere una convinzione precisa quando si parla delle
>>>ragioni del cuore. - "Sostiene Pereira", Antonio Tabucchi
>>>http://www.jidesk.net
>
>
>--
>È difficile avere una convinzione precisa quando si parla delle ragioni
>del cuore. - "Sostiene Pereira", Antonio Tabucchi
>http://www.jidesk.net
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20250613/fd3694f9/attachment-0001.htm>
More information about the PyQt
mailing list