highlighting custom QWidgetAction on hover

旧街凉风 提交于 2021-02-07 13:34:14

问题


My application has a QMenuBar with a number of QMenus, each of which having a number of QActions and sub-QMenus. Most of the QAction-items are derivatives of QWidgetAction with re-implemented QWidgetAction::createWidget methods.

Usually, both QActions and QMenu become highlighted on mouse hover. Even a QWidgetAction doesn't make trouble until here:

But as soon as I override QWidgetAction::createWidget to return a custom QWidget

QWidget* MyWidgetAction::createWidget(QWidget* parent) { return new MyWidget(parent); }

the highlighting does not work anymore. So I implemented it myself:

void MyWidget::set_highlighted(bool h)
{
  setBackgroundRole(h ? QPalette::Highlight : QPalette::Window);
  setAutoFillBackground(h);
}
void MyWidget::enterEvent(QEvent*) override { set_highlighted(true); }
void MyWidget::leaveEvent(QEvent*) override { set_highlighted(false); }

However, it does not behave as expected:

I already figured out that the enterEvent method is not called until all sub-menus are closed, which only happens with some delay after mouse leaves the sub menu or its action (btw, how can I change the delay?). Same with mouse-move events.

Question: How can I re-implement highlight-on-hover properly? The user shall not notice that custom widget and standard QAction behave differently. What does the default QWidgetAction::createWidget do and how can I reproduce it? I've already looked at Qt's source but it's quite confusing.

Code to reproduce the animations

Actual production code


回答1:


I think that the reason is you don't enable the mouse tracking on your widget, so the parent menu can't be notify that the mouse cursor change his position.

I suggest to add in the constructor of your MyWidget class this line:

setMousetracking(true);

Edit #1:
I found an ugly trick but it seems to be working:

// You WidgetAction class
class MyWidgetAction : public QWidgetAction
{
public:
    MyWidgetAction(QObject *parent = nullptr);
    QWidget* createWidget(QWidget* parent) override {
        w = new MyWidget(parent);
        return w;
    }
    void highlight(bool hl) { w->set_highlighted(hl); }

private:
    MyWidget *w;
};

// In your code
QMenu *menu = ui->menuBar->addMenu("The Menu");
menu->addAction("Standard QAction 1");
menu->addAction("Standard QAction 2");
menu->addMenu("submenu")->addAction("subaction1");
QWidgetAction *a = new MyWidgetAction();
a->setText("My action 1");
a->setParent(menu); // Needed for the trick
menu->addAction(a);
menu->addAction("Standard QAction 3");
menu->addAction("Standard QAction 4");

// The ugly trick
connect(menu, &QMenu::hovered, this, [menu](QAction *act){
    QList<MyWidgetAction*> lCustomActions = menu->findChildren<MyWidgetAction*>();
    for (MyWidgetAction *mwa : lCustomActions){
        mwa->highlight(mwa == act);
    }
});

I saw that the hovered signal is always send correctly, so I connect this to a lambda to check for each custom WidgetAction if it's the current hovered item and manually highlight in this case.


Edit #2:
To avoid the for loop in the lambda in my first edit, you can also create an eventfilter to manage the highlight on mouse move:

class WidgetActionFilterObject : public QObject
{
    Q_OBJECT
public:
    explicit WidgetActionFilterObject(QObject *parent = nullptr);

protected:
    bool eventFilter(QObject *obj, QEvent *evt) override {
        if (evt->type() == QEvent::Type::MouseMove){
            QMouseEvent *mouse_evt = static_cast<QMouseEvent*>(evt);
            QAction *a = static_cast<QMenu*>(obj)->actionAt(mouse_evt->pos());
            MyWidgetAction *mwa = dynamic_cast<MyWidgetAction*>(a);
            if (mwa){
                if (last_wa && mwa != last_wa){
                    last_wa->highlight(false);
                }
                mwa->highlight(true);
                last_wa = mwa;
            } else {
                if (last_wa){
                    last_wa->highlight(false);
                    last_wa = nullptr;
                }
            }
        }
        return QObject::eventFilter(obj, evt);
    }

private:
    MyWidgetAction *last_wa = nullptr;
};

Then the only thing you have to do is to install an event filter on each menu that contain your custom WidgetAction:

menu->installEventFilter(new WidgetActionFilterObject(this));

And you will obtain the same result without a loop on each hovered signal.




回答2:


Note that this is not a complete answer (I'm not aiming for that bounty), but rather some solid background on QMenu and how it handles widgets.

You have two problems:

1) The default implementation of QWidgetAction::create() does nothing. You are intended to override this in your implementation, so it would be surprising if your original code did work.

2) Once you overrode QWidgetAction you ran into the larger issue: QMenu does not contain QWidgets with a QLayout. It's a container of pointers to QActions and a vector of their calculated QRects. It maintains state, but it's not a traditional Qt widget container.

"But wait!" you say, what about QMenuPrivate::widgetItems? Good question. QMenu does track the widgets added to it, but it's not tightly integrated. All QMenu really does is reserve the widget's sizeHint and ensure that it doesn't paint where a widget is probably located based on its sizeHint.

"I've already tried that. It does not solve the problem. In general, mouse-move, enter- and leave events are received, unless a sub-menu is shown. FYI: I once had enabled mouseTracking on each widget in the MWE, without any difference."

I mentioned above that QMenu doesn't really integrate with the widgets added to it? This is where it starts really telling. Sub-menus are platform-specific spaghetti code and tightly bound to QMenu's relevant QPA code. Things like the internal margins, the offset for painting the menu (on some platforms the submenu is offset by several pixels), which submenu currently has focus, all of these are not propagated to a QWidget added to a menu. Toss in a submenu that takes your focus (and creates its own event loop!) and suddenly you're in uncharted waters.

If you want to add a button to a single-level menu, QWidgetAction will do it. Actions that act like actions and display submenus? You're entering into "include qmenu_p.h" territory.

"The more I think about your solution, the more I feel it is the proper solution already. What should be wrong to make QMenu responsible for highlighting its actions? I think my desire to make each action widget responsible of its own highlighting is what is inappropriate..."

Correct. QMenu makes no special consideration for your widget. It just makes space for it and communicates primarily through its associated QAction.



来源:https://stackoverflow.com/questions/55086498/highlighting-custom-qwidgetaction-on-hover

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!