Customize QMenu display with QProxyStyle

Maurizio Berti maurizio.berti at gmail.com
Tue Jun 10 02:47:55 BST 2025


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
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20250610/cc46dd6f/attachment-0001.htm>


More information about the PyQt mailing list