基本情况
使用 QQuick.Control 中的 MenuBar 实现主菜单栏。菜单栏包括 File、Edit、View、Help 菜单项。点击菜单项,会弹出对应的菜单。
ApplicationWindow {
id: window
width: 320
height: 260
visible: true
menuBar: MenuBar {
Menu {
title: qsTr("&File")
Action { text: qsTr("&New...") }
Action { text: qsTr("&Open...") }
Action { text: qsTr("&Save") }
Action { text: qsTr("Save &As...") }
MenuSeparator { }
Action { text: qsTr("&Quit") }
}
Menu {
title: qsTr("&Edit")
Action { text: qsTr("Cu&t") }
Action { text: qsTr("&Copy") }
Action { text: qsTr("&Paste") }
}
Menu {
title: qsTr("&Help")
Action { text: qsTr("&About") }
}
}
}
流程1:点击菜单栏上的菜单项,该菜单项被激活(弹出),再次点击该菜单项,菜单项退出激活状态。
流程2:如果在激活状态,移动鼠标到另一个菜单项,自动激活(不需要点击)另一个菜单项,当前激活的菜单项退出激活状态。
以上都符合预期,但是问题来了。
问题现象
流程3:在弹出的菜单上,点击某一个项目,弹出菜单消失,但是对应的菜单项并没有退出激活状态。
尝试解决
首先想到的方法,就是针对性处理。在弹出菜单消失时,触发菜单项状态切换。
delegate: MenuBarItem {
id: menuBarItem
property bool opened: menu.opened
onOpenedChanged: {
if (!opened && highlighted) {
highlighted = false
triggered()
}
}
}
实测流程3是OK了,但是流程1有问题了。在激活状态,再次点击菜单项,没有退出激活状态。
代码分析
看来只能分析源代码了。相同的思路,其实源代码里面已经实现了。
注意下面代码的 aboutToHide 一行,在弹出菜单将要消失时,是有处理的。
void QQuickMenuBar::itemAdded(int index, QQuickItem *item)
{
Q_D(QQuickMenuBar);
QQuickContainer::itemAdded(index, item);
if (QQuickMenuBarItem *menuBarItem = qobject_cast<QQuickMenuBarItem *>(item)) {
QQuickMenuBarItemPrivate::get(menuBarItem)->setMenuBar(this);
QObjectPrivate::connect(menuBarItem, &QQuickControl::hoveredChanged, d, &QQuickMenuBarPrivate::onItemHovered);
QObjectPrivate::connect(menuBarItem, &QQuickMenuBarItem::triggered, d, &QQuickMenuBarPrivate::onItemTriggered);
if (QQuickMenu *menu = menuBarItem->menu())
QObjectPrivate::connect(menu, &QQuickPopup::aboutToHide, d, &QQuickMenuBarPrivate::onMenuAboutToHide);
}
d->updateImplicitContentSize();
emit menusChanged();
}
菜单栏里面维护了激活状态(即 popupMode 为 true),菜单消失时,退出激活状态。
void QQuickMenuBarPrivate::onMenuAboutToHide()
{
if (triggering || !currentItem || (currentItem->isHovered() && currentItem->isEnabled()) || !currentItem->isHighlighted())
return;
popupMode = false;
activateItem(nullptr);
}
那为什么没有生效呢?通过调试,发现上面的代码 menu 是空指针,所以没有与 aboutToHide 信号连接。调用栈如下:
1 QQuickMenuBar::itemAdded qquickmenubar.cpp 534 0x7ffc9884f04f
2 QQuickContainerPrivate::insertItem qquickcontainer.cpp 250 0x7ffc98814a3b
3 QQuickContainer::insertItem qquickcontainer.cpp 532 0x7ffc98813378
4 QQuickContainer::addItem qquickcontainer.cpp 507 0x7ffc9881327c
5 QQuickContainer::itemChange qquickcontainer.cpp 865 0x7ffc98813da0
6 QQuickItemPrivate::itemChange qquickitem.cpp 6231 0x7ffc7b509ed7
7 QQuickItemPrivate::addChild qquickitem.cpp 2976 0x7ffc7b505cf3
8 QQuickItem::setParentItem qquickitem.cpp 2765 0x7ffc7b4f699c
9 QQuickMenuBarPrivate::beginCreateItem qquickmenubar.cpp 100 0x7ffc9884f6d8
10 QQuickMenuBarPrivate::createItem qquickmenubar.cpp 115 0x7ffc9884f768
11 QQuickMenuBar::addMenu qquickmenubar.cpp 341 0x7ffc9884e9cc
12 QQuickMenuBar::qt_static_metacall moc_qquickmenubar_p.cpp 131 0x7ffc9884e30d
13 QQuickMenuBar::qt_metacall moc_qquickmenubar_p.cpp 230 0x7ffc9884e0f9
14 QMetaObject::metacall qmetaobject.cpp 310 0x7ffc5d08dcb4
15 QQmlObjectOrGadget::metacall qqmlpropertycache.cpp 1772 0x7ffc7140ed1b
16 CallMethod qv4qobjectwrapper.cpp 1297 0x7ffc711c923e
17 CallPrecise qv4qobjectwrapper.cpp 1557 0x7ffc711c9f56
18 QV4::QObjectMethod::callInternal qv4qobjectwrapper.cpp 2118 0x7ffc711c63da
19 QV4::QObjectMethod::virtualCall qv4qobjectwrapper.cpp 2056 0x7ffc711c5eeb
20 QV4::FunctionObject::call qv4functionobject_p.h 203 0x7ffc70f8a031
... <更多>
为什么 menu 是空的呢,原来 itemAdded 调用得比较早,这个时候还没有 setMenu。下面的代码(Qt 5.12.4) beginCreateItem 会调用 setParentItem,此时就触发了 itemAdded。
QQuickItem *QQuickMenuBarPrivate::createItem(QQuickMenu *menu)
{
QQuickItem *item = beginCreateItem();
if (QQuickMenuBarItem *menuBarItem = qobject_cast<QQuickMenuBarItem *>(item))
menuBarItem->setMenu(menu);
completeCreateItem();
return item;
}
后来看到一个比较新的 Qt5 代码,这个问题就是修复了的。他将 setMenu 放在了更前面。
QQuickItem *QQuickMenuBarPrivate::beginCreateItem(QQuickMenu *menu)
{
......
if (QQuickMenuBarItem *menuBarItem = qobject_cast<QQuickMenuBarItem *>(item))
menuBarItem->setMenu(menu);
item->setParentItem(q);
QQml_setParent_noEvent(item, q);
return item;
}
解决方案
如果不升级 Qt,有没有办法解决这个问题呢?
其实只要将 MenuItem 重新添加到 MenuBar 中就行了,这个时候 menu 就不是空的了。
看代码:
MenuBar {
id: menuBar
delegate: MenuBarItem {
id: menuBarItem
onMenuChanged: {
// MenuBar has BUG on addMenu, it can't detach menu on MenuBarItem which is set later
// Re-add the item to fix the BUG
menuBar.addItem(menuBar.takeItem(menuBar.count - 1))
}
}
}