一个 Qml MenuBar 的问题

基本情况

使用 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:点击菜单栏上的菜单项,该菜单项被激活(弹出),再次点击该菜单项,菜单项退出激活状态。

一个 Qml MenuBar 的问题_第1张图片一个 Qml MenuBar 的问题_第2张图片

流程2:如果在激活状态,移动鼠标到另一个菜单项,自动激活(不需要点击)另一个菜单项,当前激活的菜单项退出激活状态。

一个 Qml MenuBar 的问题_第3张图片一个 Qml MenuBar 的问题_第4张图片

以上都符合预期,但是问题来了。

问题现象

流程3:在弹出的菜单上,点击某一个项目,弹出菜单消失,但是对应的菜单项并没有退出激活状态。

一个 Qml MenuBar 的问题_第5张图片一个 Qml MenuBar 的问题_第6张图片

尝试解决

首先想到的方法,就是针对性处理。在弹出菜单消失时,触发菜单项状态切换。

        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(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(item))
        menuBarItem->setMenu(menu);
    completeCreateItem();
    return item;
}

 后来看到一个比较新的 Qt5 代码,这个问题就是修复了的。他将 setMenu 放在了更前面。

QQuickItem *QQuickMenuBarPrivate::beginCreateItem(QQuickMenu *menu)
{
    ......
    if (QQuickMenuBarItem *menuBarItem = qobject_cast(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))
            }
        }
    }

你可能感兴趣的:(Qt/QML,qt)