问题
My application has a QMenuBar
with a number of QMenu
s, each of which having a number of QAction
s and sub-QMenu
s.
Most of the QAction
-items are derivatives of QWidgetAction
with re-implemented QWidgetAction::createWidget
methods.
Usually, both QAction
s 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 QAction
s and a vector of their calculated QRect
s. 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