Solved Custom QAction in QMenu
-
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.