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
    0_1483718607190_Custom.png

    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 components

    My 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


  • Qt Champions 2016

    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.


  • Moderators

    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!


  • Moderators

    @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.


  • Moderators

    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.
      0_1484149212857_Sans titre.png
      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


  • Moderators

    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.


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.