Customize QMenu display with QProxyStyle
Maurizio Berti
maurizio.berti at gmail.com
Tue Jun 10 05:04:57 BST 2025
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/20250610/32234759/attachment-0001.htm>
More information about the PyQt
mailing list