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