Custom QAction in QMenu
-
Hi to everyone,
Today I would like to custom a few QAction rendering in a QMenu.
Since now, the only way I found is to go through a QWidgetAction, that take a default widget.What I want to have, is a standard item with a little remove action at endline
What I get now with few line is that, it seems a little weird way, and I want your expertise to know if it's the best way to do that
Also, I lost some automatic style effects like hovered style etc...
I would like to be the most similar to initial style, to avoid a contrast between original and my componentsMy lines of code, for persons interested in:
QMenu* p_menu = new QMenu();
QWidgetAction* action = new QWidgetAction(p_menu);
QWidget* widget = new QWidget(p_menu);
QHBoxLayout* lay = new QHBoxLayout(widget);
QPushButton* check = new QPushButton(widget);
check->setSizePolicy(QSizePolicy::MinimumExpanding,QSizePolicy::MinimumExpanding);
QLabel* label = new QLabel(workspaces.at(i),widget);
check->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding);
QPushButton* close = new QPushButton(QIcon(":/WaveView/icon/actionClose.svg"),"",widget);
check->setSizePolicy(QSizePolicy::MinimumExpanding,QSizePolicy::MinimumExpanding);
check->setFlat(true);
close->setFlat(true);
lay->addWidget(check);
lay->addWidget(label);
lay->addWidget(close);
widget->setLayout(lay);
lay->setContentsMargins(0,0,0,0);
action->setDefaultWidget(widget);
action->setDefaultWidget(widget);
p_menu->addAction(action);Thanks!
Best regards -
Hi
If I understand what you want from the custom Action
( a widget in the menu with a button etc)
then i asked the same question a year ago
https://forum.qt.io/topic/57758/solved-qmenu-and-custom-painting-of-the-items
and if Chris Kawa suggests QWidgetAction, i doubt there be anything smarter. :)
Unless you have something else in mind. -
I'd use a QActionWidget, but if you want to make the widget look like normal menu item use the same method to draw it.
It's always good to practice, so I made a little sample that you can hopefully expand on:class CustomItem : public QWidget { Q_OBJECT QString text; bool checked; QPushButton* btnRemove; signals: void toggled(bool checked) const; void activated() const; void removeClicked() const; public: CustomItem(const QString& text, QWidget* parent = nullptr) : QWidget(parent), text(text), checked(false) { setMouseTracking(true); //so we get paint updates btnRemove = new QPushButton(QIcon(":/WaveView/icon/actionClose.svg"), QString()); btnRemove->setFlat(true); connect(btnRemove, &QPushButton::clicked, this, &CustomItem::removeClicked); auto lay = new QHBoxLayout(); lay->setContentsMargins(0,0,0,0); lay->addStretch(); lay->addWidget(btnRemove); setLayout(lay); } QSize minimumSizeHint() const override { QStyleOptionMenuItem opt; opt.initFrom(this); opt.menuHasCheckableItems = true; QSize contentSize = fontMetrics().size(Qt::TextSingleLine | Qt::TextShowMnemonic, text); return style()->sizeFromContents(QStyle::CT_MenuItem, &opt, contentSize, this) + QSize(btnRemove->minimumSizeHint().width(), 0); } void paintEvent(QPaintEvent* e) override { QPainter p(this); QStyleOptionMenuItem opt; opt.initFrom(this); opt.text = text; opt.menuHasCheckableItems = true; opt.checked = checked; opt.checkType = QStyleOptionMenuItem::NonExclusive; opt.menuItemType = QStyleOptionMenuItem::Normal; if (rect().contains(mapFromGlobal(QCursor::pos()))) opt.state |= QStyle::State_Selected; style()->drawControl(QStyle::CE_MenuItem, &opt, &p, this); } void mouseReleaseEvent(QMouseEvent* evt) override { QWidget::mouseReleaseEvent(evt); QRect checkboxRect(0,0,25,height()); //the value 25 seems to be hardcoded in the style :/ if (isEnabled()) { if (checkboxRect.contains(mapFromGlobal(QCursor::pos()))) { checked = !checked; emit toggled(checked); } else emit activated(); } } };
You can probably make tons of adjustments, like add any number of buttons dynamically, but it's a starting point.
-
Thanks to both of you!
@Chris-Kawa I try your lines of code and it fits perfectly to my needs !
Before your answered,I was trying an approach like the one you made, after reading QMenu lines of code. But without success!
Again, thank you very much! -
Some little changes to avoid redondant informations :
#ifndef QCUSTOMITEM #define QCUSTOMITEM //Other location includes #include <QWidget> #include <QString> //Forward class QPushButton; class QCustomItem : public QWidget { Q_OBJECT ////////////////////////////////////////////// //// ----------------- MEMBERS ------------ private: /** * @brief */ QAction* m_action; /** * @brief */ QPushButton* m_button; ////////////////////////////////////////////// //// ----------------- CONSTRUCTORS ------------ public: /** * @brief */ QCustomItem(QAction* p_action, const QIcon& p_icon,QWidget* p_parent = nullptr); /** * @brief */ virtual ~QCustomItem(); ////////////////////////////////////////////// //// ----------------- METHODS ------------ public: /** * @brief */ virtual QSize minimumSizeHint() const; /** * @brief */ virtual void paintEvent(QPaintEvent* e); /** * @brief */ virtual void mouseReleaseEvent(QMouseEvent* evt); signals: /** * @brief */ void toggled(bool checked) const; /** * @brief */ void activated() const; /** * @brief */ void action() const; }; #endif #include "QCustomItem.h" ///// QT #include <QAction> #include <QHBoxLayout> #include <QPainter> #include <QPushButton> #include <QStyleOptionMenuItem> ////////////////////////////////////////////// //// ----------------- CONSTRUCTORS ------------ QCustomItem::QCustomItem(QAction* p_action, const QIcon& p_icon, QWidget* p_parent) : QWidget(p_parent), m_action(p_action) { setMouseTracking(true); //so we get paint updates m_button = new QPushButton(p_icon, QString()); m_button->setFlat(true); connect(m_button, &QPushButton::clicked, this, &QCustomItem::action); QHBoxLayout* layout = new QHBoxLayout(); layout->setContentsMargins(0,0,0,0); layout->addStretch(); layout->addWidget(m_button); setLayout(layout); } QCustomItem::~QCustomItem() { } ////////////////////////////////////////////// //// ----------------- METHODS ------------ QSize QCustomItem::minimumSizeHint() const { QStyleOptionMenuItem opt; opt.initFrom(this); opt.menuHasCheckableItems = true; QSize contentSize = fontMetrics().size(Qt::TextSingleLine | Qt::TextShowMnemonic, m_action->text()); return style()->sizeFromContents(QStyle::CT_MenuItem, &opt, contentSize, this) + QSize(m_button->minimumSizeHint().width(), 0); } void QCustomItem::paintEvent(QPaintEvent* e) { QPainter p(this); QStyleOptionMenuItem opt; opt.initFrom(this); opt.text = m_action->text(); opt.menuHasCheckableItems = true; opt.checked = m_action->isChecked(); opt.checkType = QStyleOptionMenuItem::NonExclusive; opt.menuItemType = QStyleOptionMenuItem::Normal; if (rect().contains(mapFromGlobal(QCursor::pos()))) opt.state |= QStyle::State_Selected; style()->drawControl(QStyle::CE_MenuItem, &opt, &p, this); } void QCustomItem::mouseReleaseEvent(QMouseEvent* evt) { QWidget::mouseReleaseEvent(evt); QRect checkboxRect(0,0,25,height()); //the value 25 seems to be hardcoded in the style :/ if (isEnabled()) { if (checkboxRect.contains(mapFromGlobal(QCursor::pos()))) { m_action->toggle(); } else emit activated(); } }
It seems to work nice!
Many thanks! -
@Romain-C I added code tags to your last post. Please use them in the future.
Using QAction to hold a string and a bool is kinda overkill. I'm not sure what redundancies you mean but now you made the code really redundant. The widget is meant to be used as an action widget, which inherits a QAction on its own, so now you have a QAction inside a QWidget inside a QAction. -
@Chris-Kawa Sorry, didn't see that </> was for code insertion!
I my idea, I would like that the QWidget was the view of the QAction (and the QAction the model).
In this situation I hope if my QAction is check in other part of the code, the QWidget will take the modifications.
Not sure I'm right. -
It's a widget. It shouldn't really be an action. A QWidgetAction is the adapter for these.
If you want to use QAction anyway you're making the task a lot harder for yourself, because with it, to make the solution complete, you would need to sync the inner and outer actions i.e. if someone sets text on the QWidget Action, changes icon, description etc. This goes the other way too.
Of course you can ignore these issues but it makes the solution half baked. -
Ok I understand what you mean, and i forgot that QWidget have some defined methods like setText() that need to be managed.
In my first idea, the only properties that needs to be set was in relationship with the button part, all the rest would be taken from the QAction. So I admit it's a little weird. The Qaction field was just a way for my widget to know the QWidgetAction "parent" of my widget and to synchronize it. -
@Chris-Kawa
Hi again, and a question more specific and perhaps more for you,
I get 2 main issues with that solution:-
First one, when I open the QMenu the first time, my elements are truncated, but the second they are sized properly.
I put a QDebug on paintEvent, the first time width equal to 135, and the second time it's 123.
Does QMenu compute max element size, and does it exist a way to refresh it before show? -
Second one, is one highlight of items:
void QItemWithButton::paintEvent(QPaintEvent* e) { QPainter p(this); QStyleOptionMenuItem opt; opt.initFrom(this); opt.text = m_action->text(); opt.menuHasCheckableItems = true; opt.checked = m_action->isChecked(); opt.checkType = QStyleOptionMenuItem::NonExclusive; opt.menuItemType = QStyleOptionMenuItem::Normal; if (rect().contains(mapFromGlobal(QCursor::pos()))) { opt.state |= QStyle::State_Selected; } style()->drawControl(QStyle::CE_MenuItem, &opt, &p, this); }
This ssems to work properly, when item is under the mouse, but the element is never showed in highlight way?
I try to force Palette from QMenu to item, thinking that there a missing color, but nothing change.
Do you have an idea?Many thanks
-
-
The first issue is related to the fact that the size of a widget is first calculated when it is shown, not before. First show uses the size hint to calculate the size and adjusts it if needed. Have you modified the size hint in any way? In particular have you used something like
width()
in the size hint? Or maybe you're setting the text too late and the size hint uses empty text to calculate the needed width?As for the second issue I'm not sure what you mean. Could you explain a bit more or show a picture of what happens? One thing missing in the code snippet you posted is the initialization of the style options object:
QStyleOptionMenuItem opt; opt.initFrom(this); //<- that part is important
I try to force Palette from QMenu to item
That sounds like something you shouldn't do. What code is that exactly?
-
@Chris-Kawa Hi Chris,
For the first one, I didn't change sizeHint(). My code remain quite similar with the code I uploaded with the QAction field.
To explain perhaps a part of the problem, my QMenu is generated dynamically. I connect &QMenu::aboutToShow with a code of mine to refresh it each time user ask for it.For the second one, it was just a concise snippet, I edit my last post and I put the entire method. It's the same thing as you did. I have a lot of problems to explain what happens, I cannot donwload sources of Qt at this time to make a debug...
-
@Chris-Kawa Hi Chris, ok so I reviewed my code today, and to solve the problem of size I just removed the mimimumSizeHint() and it works perfectly. Thks!
I only need to fix highlight color now.