Qt: Change application QMenuBar contents on Mac OS X
Asked Answered
J

2

2

My application uses a QTabWidget for multiple 'pages', where the top-level menu changes depending on what page the user is on.

My issue is that attempting to re-create the contents of the menu bar results in major display issues. It works as expected with the first and third style (haven't tested second, but I'd rather not use that style) on all platforms except for Mac OS X.

The first menu is created in the way I create most in the application, and they receive the correct title, but disappear as soon as the menu is re-created.

The second menu appears both on the initial population and the re-population of the menu bar, but in both cases has the label "Untitled". The style for the second menu was only created when trying to solve this, so it's the only way I've been able to have a menu stick around.

The third dynamic menu never appears, period. I use this style for dynamically populating menus that are about to show.

I have tried deleting the QMenuBar and re-creating one with

m_menuBar = new QMenuBar(0);

and using that as opposed to m_menuBar->clear() but it has the same behavior.

I don't have enough reputation to post images inline, so I'll include the imgur links:

Launch behavior: https://i.sstatic.net/V4N6O.png

Post button-click behavior: https://i.sstatic.net/fytDc.png

I have created a minimal example to reproduce this behavior on Mac OS X 10.9.4 with Qt 5.3.

mainwindow.cpp

#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    m_menuBar = new QMenuBar(0);
    m_dynamicMenu = new QMenu("Dynamic");
    connect(m_dynamicMenu, SIGNAL(aboutToShow()), this, SLOT(updateDynamicMenu()));

    changeMenuBar();

    QPushButton *menuBtn = new QPushButton("Test");
    connect(menuBtn, SIGNAL(clicked()), this, SLOT(changeMenuBar()));

    setCentralWidget(menuBtn);
}

void MainWindow::changeMenuBar() {
    m_menuBar->clear();

    // Disappears as soon as this is called a second time
    QMenu *oneMenu = m_menuBar->addMenu("One");
    oneMenu->addAction("foo1");
    oneMenu->addAction("bar1");
    oneMenu->addAction("baz1");

    // Stays around but has 'Untitled' for title in menu bar
    QMenu *twoMenu = new QMenu("Two");
    twoMenu->addAction("foo2");
    twoMenu->addAction("bar2");
    twoMenu->addAction("baz2");
    QAction *twoMenuAction = m_menuBar->addAction("Two");
    twoMenuAction->setMenu(twoMenu);

    // Never shows up
    m_menuBar->addMenu(m_dynamicMenu);
}

void MainWindow::updateDynamicMenu() {
    m_dynamicMenu->clear();
    m_dynamicMenu->addAction("foo3");
    m_dynamicMenu->addAction("bar3");
    m_dynamicMenu->addAction("baz3");
}

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QtWidgets>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = 0);

private slots:
    void changeMenuBar();
    void updateDynamicMenu();

private:
    QMenuBar *m_menuBar;
    QMenu *m_dynamicMenu;
};

#endif // MAINWINDOW_H
Joist answered 23/9, 2014 at 21:13 Comment(0)
E
4

All this looks like Qt bug on OS X. And it's very old bug, actually.

You can do workaround and don't work with QMenu via QMenuBar::addMenu function calls, as you do here:

m_menuBar->addMenu("One");

Instead of this work with QAction retrieved from QMenu by creation of the QMenu instance dynamically and then calling of QMenuBar::addAction for the QAction instance retrieved by QMenu::menuAction, as following:

m_menuBar->addAction(oneMenu->menuAction());

Beside QMenuBar::addAction you can use QMenuBar::removeAction and QMenuBar::insertAction if you want to make creation only of some specific menu items dynamically.

Based on your source code here it's modified version of it which deals with all menus dynamic creation on every button click (you do this in your source code) and the menu 'Dynamic' is populated with different count of items every time you click the button.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QtWidgets>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = 0);

private slots:
    void changeMenuBar();

private:
    QMenuBar *m_menuBar;
    QMenu *m_dynamicMenu;
    int m_clickCounter;

};

#endif // MAINWINDOW_H
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent),
      m_clickCounter(1)
{
    m_menuBar = new QMenuBar(this);

    connect(m_dynamicMenu, SIGNAL(aboutToShow()), this, SLOT(updateDynamicMenu()));

    changeMenuBar();

    QPushButton *menuBtn = new QPushButton("Test");
    connect(menuBtn, SIGNAL(clicked()), this, SLOT(changeMenuBar()));

    setCentralWidget(menuBtn);
}

void MainWindow::changeMenuBar() {
    ++m_clickCounter;

    m_menuBar->clear();

    QMenu *oneMenu = new QMenu("One");

    oneMenu->addAction("bar1");
    oneMenu->addAction("baz1");
    m_menuBar->addAction(oneMenu->menuAction());

    QMenu *twoMenu = new QMenu("Two");
    twoMenu->addAction("foo2");
    twoMenu->addAction("bar2");
    twoMenu->addAction("baz2");

    m_menuBar->addAction(twoMenu->menuAction());

    m_dynamicMenu = new QMenu("Dynamic");
    for (int i = 0; i < m_clickCounter; ++i) {
        m_dynamicMenu->addAction(QString("foo%1").arg(i));
    }

    m_menuBar->addAction(m_dynamicMenu->menuAction());
}

Additionally while developing menus logic for OS X it's good to remember:

  • It's possible to disable QMenuBar native behavior by using QMenuBar::setNativeMenuBar
  • Because of turned on by default QMenuBar native behavior, QActions with standard OS X titles("About","Quit") will be placed automatically by Qt in the predefined way on the screen; Empty QMenu instances will be not showed at all.
Extenuation answered 24/9, 2014 at 4:32 Comment(4)
I'd like to point out that while adding oneMenu->menuAction() is correct and works for the first 2, it is apparently mandatory (on Mac OS X) for a menu to have existing actions when added to the QMenuBar. Since my dynamic menu example doesn't add actions until the aboutToShow() signal is emitted, my work-around is to populate the dynamic menu with 1 QAction that serves no purpose other than to get it to initially show. In my updateDynamicMenu() slot I then clear and re-populate the dynamic menu.Joist
@syrius, just for note, it's possible to disable menu bar platform native behavior by using QMenuBar::setNativeMenuBar. But this can bring much more headache. Also in real application we use QActions to listen for their signals. To avoid overhead, optimal solution turns into keeping of all QActions as private members and populating dynamic menus with them every time we need to change QMenu.Extenuation
but in a real application, such as a 'Window' menu for an editor, you won't have private member actions for each file currently open. It is why I populate the menu in the slot connected to the aboutToShow() signal. I was just mentioning that in your example for the dynamic menu, if you don't populate it until the aboutToShow() signal is emitted, it will never appear in the menu bar, which is my reason for posting my initial comment about requiring an empty QAction (because you don't yet know the contents). I think it would be helpful to others to include this in the answer.Joist
@Joist "it is apparently mandatory (on Mac OS X) for a menu to have existing actions when added to the QMenuBar" can you provide a reference on that? I'm trying to solve this exact issue.Bertram
Q
1

I think your problem is this line:

QMenu *oneMenu = m_menuBar->addMenu("One");

To add menus to a menubar you'd want code as follows:

QMenuBar *m = new QMenuBar;
m->addMenu( new QMenu("Hmmm") );
m->show();

To create menus, and then add actions, and then add the menu to the menu bar:

QMenu *item = new QMenu( "Test1" );
item->addAction( "action1" );

QMenuBar *t = new QMenuBar;
t->addMenu( item );
t->show();
Quickfreeze answered 24/9, 2014 at 1:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.