一个 Qml MenuBar 的问题

在使用QQuick.Control的MenuBar实现主菜单时,遇到一个问题:点击菜单项弹出子菜单后,菜单项未退出激活状态。通过修改源代码和监听菜单项状态来尝试解决,但导致其他流程出现问题。问题的根本原因在于菜单项添加时,menu为空指针。解决方案是当菜单改变时,重新添加菜单项到MenuBar中,以确保menu非空。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本情况

使用 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))
            }
        }
    }

### QML MenuBar 使用方法及示例 #### 创建基本的 MenuBar 和 Menu 控件 在 QML 中,`MenuBar` 是一种用于创建顶部菜单栏的控件。该控件可以放置于 `ApplicationWindow` 的特定区域,并且可以通过设置其属性来自定义外观和行为。 ```qml import QtQuick 2.15 import QtQuick.Controls 2.15 ApplicationWindow { visible: true width: 640 height: 480 // 定义一个简单的 MenuBar 并添加到 ApplicationWindow 上 menuBar: MenuBar { id: mainMenuBar // 添加文件菜单 Menu { title: "File" MenuItem { text: "New"; onTriggered: console.log("New file") } MenuItem { text: "Open..."; shortcut: "Ctrl+O" } MenuItem { text: "Save"; shortcut: "Ctrl+S" } MenuItem { text: "Exit"; onTriggered: Qt.quit() } } // 编辑菜单 Menu { title: "Edit" MenuItem { text: "Cut"; shortcut: "Ctrl+X" } MenuItem { text: "Copy"; shortcut: "Ctrl+C" } MenuItem { text: "Paste"; shortcut: "Ctrl+V" } } // 帮助菜单 Menu { title: "Help" MenuItem { text: "About..." } } } } ``` 这段代码展示了如何在一个应用窗口中加入三个不同类型的子菜单——“文件”,“编辑”以及“帮助”。每个菜单都包含了若干个 `MenuItem` 来表示具体的命令选项[^1]。 #### 自定义 MenuBar 样式 对于希望进一步调整视觉样式的开发者来说,可以通过修改组件的各种属性来达到目的。例如改变字体大小、颜色或者其他 UI 特性: ```qml // 修改整个 MenuBar 或者个别 Menu 的样式 style: MenuItemStyle { textColor: "#FFFFFF" backgroundColor: "#3F51B5" } // 更改单个 MenuItem 的样式 MenuItem { text: "Custom Style Item"; style: MenuItemStyle { textColor: "red" highlightedTextColor: "white" backgroundColor: "blue" highlightedBackgroundColor: "darkBlue" } } ``` 这里展示的是通过内联的方式直接给定样式规则;当然也可以考虑利用更复杂的主题方案来进行全局性的定制化处理[^4]。 #### 注意事项 需要注意的是,`MenuBar`, `Header`, `Footer` 这些特性仅适用于 `ApplicationWindow` 而不是普通的 `Window` 类型。如果尝试在一个非 `ApplicationWindow` 的容器里使用它们,则不会生效[^2]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting Horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值