When and how to properly destroy QMenu context menu? - c++

I allow custom context menu to appear over a table. This is how the menu is generated, using a generic function that accepts target widget and coordinates:
#include <QMenu>
void MainWindow::makeContextMenu(const QPoint& pos, QWidget* target)
{
QMenu *menu = new QMenu(this);
menu->addAction(new QAction("Action 1", menu));
menu->addAction(new QAction("Action 2", menu));
menu->addAction(new QAction("Action 3", menu));
// Notify window about clicking
QObject::connect(menu, &QMenu::triggered, this, &MainWindow::menuClicked);
// If this is a scroll area, map coordinates to real app coordinates
if(QAbstractScrollArea* area = dynamic_cast<QAbstractScrollArea*>(target))
menu->popup(area->viewport()->mapToGlobal(pos));
else
menu->popup(pos);
}
The problem is that the QMenu* menu never gets destroyed and removed from memory. It persists as MainWindow's child even after it's hidden.
What should I do? Can I set the menu to delete itself? Or should I reuse the same instance of menu or maybe save it into same pointer?

It doesn't have to be so complicated. That's it already:
menu->setAttribute(Qt::WA_DeleteOnClose);
That way when the QMenu is closed, the class is deleted as soon as the event loop is entered again. And it doesn't matter if an action was triggered or the popup was just closed.
To prove my answer, you can test it yourself by checking when the menu is created and if the 'deleted' message is triggered with the same address:
qDebug() << "created" << (qintptr)menu;
connect(menu, &QMenu::destroyed,
this, [menu]() { qDebug() << "deleted" << (qintptr)menu; });

From your code, it seems like menu should be deleted after this event has taken place?
// Notify window about clicking
QObject::connect(menu, &QMenu::triggered, this, &MainWindow::menuClicked);
Can I set the menu to delete itself?
Yes, you can have the object delete itself like this:
// Notify window about clicking
QObject::connect(menu, &QMenu::triggered, this, &MainWindow::menuClicked);
QObject::connect(menu, &QMenu::triggered, menu, &QMenu::deleteLater);
If you are worried about the order of those slots being called, see this
Or should I reuse the same instance of menu or maybe save it into same
pointer?
Well, you can do something like
//Your constructor
MainWindow::MainWindow(....)
{
menu = nullptr;
....
}
//Make context Menu
void MainWindow::makeContextMenu(const QPoint& pos, QWidget* target)
{
if(menu)
delete menu;
menu = new QMenu(this);
....
}
As for the MainWindow::~MainWindow() destructor, it will take care of the menu's clean up. Since MainWindow (which is a QObject derived class) automatically deletes all children
Lastly, you can simply have the menu as a member of MainWindow, and whenever you need to have fresh actions for menu, you can use QMenu::clear to delete all existing actions.
//Your constructor
MainWindow::MainWindow(....)
{
menu = new QMenu(this);
....
}
void MainWindow::makeContextMenu(const QPoint& pos, QWidget* target)
{
menu->clear();
//QMenu *menu = new QMenu(this);
menu->addAction(new QAction("Action 1", menu));
menu->addAction(new QAction("Action 2", menu));
menu->addAction(new QAction("Action 3", menu));
// Notify window about clicking
QObject::connect(menu, &QMenu::triggered, this, &MainWindow::menuClicked);
// If this is a scroll area, map coordinates to real app coordinates
if(QAbstractScrollArea* area = dynamic_cast<QAbstractScrollArea*>(target))
menu->popup(area->viewport()->mapToGlobal(pos));
else
menu->popup(pos);
}

It is possible to delete QMenu when it's hidden. I designed event filter class for that purpose:
#ifndef DELETEONHIDEFILTER_H
#define DELETEONHIDEFILTER_H
#include <QObject>
#include <QEvent>
class DeleteOnHideFilter : public QObject
{
Q_OBJECT
public:
explicit DeleteOnHideFilter(QObject *parent = 0) : QObject(parent) {}
protected slots:
bool eventFilter(QObject *obj, QEvent *event) override {
if(event->type() == QEvent::Hide) {
obj->deleteLater();
}
return false;
}
};
#endif // DELETEONHIDEFILTER_H
It can be used for other objects as well.

Related

Qt: Show popup menu, close if I click anywhere outside of it

I want to show a menu in Qt with the following functionality: clicking anywhere outside of the menu will close it, but the click will be processed by the receiving widget. As far as I know, this rules out using setWindowFlag(Qt::Popup) because the click that closed the popup will be 'eaten' and not processed any further. I'd love to know if there is a way around this.
For the time being, I'm overriding focusOutEvent like so:
class Menu : public QWidget
{
Q_OBJECT
public:
Menu(QWidget* parent = nullptr)
: QWidget(parent){
setWindowFlags(Qt::FramelessWindowHint);
resize(200, 100);
}
public slots:
void showMenu(QPoint point)
{
move(point);
show();
setFocus(); // so that focusOutEvent will be trigerred
}
protected:
void focusOutEvent(QFocusEvent *event) override
{
hide();
}
};
And I'm using it like this:
class Window : public QMainWindow
{
Q_OBJECT
public:
Window(): m_menu()
{
auto* list = new QListWidget();
list->addItems(QStringList() << "item1" << "item2" << "item3");
connect(list, &QListWidget::itemClicked, [this](){
m_menu.showMenu(QCursor::pos());
});
// Layout stuff
auto central_widget = new QWidget();
auto layout = new QVBoxLayout(central_widget);
layout->addWidget(list);
layout->addItem(new QSpacerItem(20,200, QSizePolicy::Minimum, QSizePolicy::Expanding));
setCentralWidget(central_widget);
}
public:
~Window(){}
private:
Menu m_menu;
};
Single clicking on any of the items in the list will show the menu and clicking anywhere on the grey background will close the menu.
The issue I'm facing is that when the menu is shown, focus is lost from the main window (photo on the right, the titlebar buttons and selected item are greyed out). Focus is lost because of calling setFocus on the menu but if I don't use it then the QFocusOutEvents are not trigerred and the menu doesn't close.
The same happens if I use setAttribute(Qt::WA_ShowWithoutActivating) on the menu.
So here are my questions:
Is there a way to use Qt::Popup without the 'eaten' clicks issue?
If not, is there a way to show the menu without losing the focus from the main window but still trigger the focusOutEvents as necessary?
Using Qt 5.15.1 on MacOS 11.2.3. Thanks for reading through my long post!

Have a combobox inside a message box

I want to create a combobox inside a message box and return the selected value to be used later.
I can do the same on the window itself but not sure how to do that inside a combobox.
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->comboBox->addItem("Red");
ui->comboBox->addItem("Blue");
ui->comboBox->addItem("Green");
ui->comboBox->addItem("Yellow");
ui->comboBox->addItem("Pink");
ui->comboBox->addItem("Purple");
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
QMessageBox::about(this,"Choose color of rectangle", ui->comboBox->currentText() );
}
If I understand you correct you would like to show a combobox in a separate dialog window for the user to select some option.
One of the ways to do that, would be to subclass QDialog. If a combo field and a button to accept is sufficient the class could look as below:
class CustomDialog : public QDialog
{
public:
CustomDialog(const QStringList& items)
{
setLayout(new QHBoxLayout());
box = new QComboBox;
box->addItems(items);
layout()->addWidget(box);
QPushButton* ok = new QPushButton("ok");
layout()->addWidget(ok);
connect(ok, &QPushButton::clicked, this, [this]()
{
accept();
});
}
QComboBox* comboBox() { return box; }
private:
QComboBox* box;
};
To use the class object you can call exec to display it modally. Then you can verify whether the user accepted the choice by pressing the ok button and take proper action.
QStringList itemList({"item1", "item2", "item3"});
CustomDialog dialog(itemList);
if (dialog.exec() == QDialog::Accepted)
{
// take proper action here
qDebug() << dialog.comboBox()->currentText();
}
Similar approach is implemented in the QMessageBox class where a number of options can be specified to alter the displayed contents (for example button configuration or check box existance).
EDIT:
To use the sample code in your own project you should put the latter section I posted into your on_pushButton_clicked() slot. Substitute the itemList with your color names list. Then put the CustomDialog class to a separate file which you include in main and you should be good to go.

How to lock all toolbars via menu in Qt?

I am experimenting with Qt Creator and the Application Example.
I would like to add a checkable menu entry to a toolbar menu that reads "Lock toolbars" and, when checked, locks the positions of all tool bars. I guess that it is a quite common feature.
I have managed to find a command that locks single bars via :
toolBar->setMovable(false);
But I cannot figure out how to lock all toolbars.
Edit
This question used to contain an inquiry concerning the toolbar context menu rather than the standard menu. Since I got an answer concerning the context menu elsewhere I removed it from this question.
How to add an entry to toolbar context menu in qt?
Here is an example of how you can achieve it. First, add a QAction and a QMenu; also, declare all your toolbars private :
private:
QMenu* m_pLockMenu;
QToolBar* m_pFileToolBar;
QToolBar* m_pEditToolBar;
QToolBar* m_pHelpToolBar;
QAction* m_pLockAction;
Also, declare a slot where you will manage the locking of your toolbars when the action will be triggered :
public slots :
void lockActionTriggered();
Implement your slot. You just need to lock all the toolbars :
void lockActionTriggered()
{
m_pFileToolBar->setMovable(false);
m_pEditToolbar->setMovable(false);
m_pHelpToolBar->setMovable(false);
}
Now, you just have to declare your main window in your .cpp, and add the menu, the toolbars and the action in it :
QMainWindow* mainWindow = new QMainWindow();
m_pLockMenu = mainWindow->menuBar()->addMenu("Lock Toolbars");
m_pFileToolBar = mainWindow->addToolBar("File");
m_pEditToolBar = mainWindow->addToolBar("Edit");
m_pHelpToolBar = mainWindow->addToolBar("Help");
m_pLockAction = new QAction("Lock", this);
Now, add the action to the menu :
m_pLockMenu->addAction(m_pLockAction);
And connect the QAction's signal triggered() to your slot :
connect(m_pLockAction, SIGNAL(triggered()), this, SLOT(lockActionTriggered()));
Don't forget to show() your main window :
mainWindow->show();
And it should be working now!
EDIT
Your code must look like this :
In the mainwindow.h :
class MainWindow : public QMainWindow
{
...
private:
QMenu* m_pLockMenu;
QToolBar* m_pFileToolBar;
QToolBar* m_pEditToolBar;
QToolBar* m_pHelpToolBar;
QAction* m_pLockAction;
public slots :
void lockActionTriggered();
};
In the main.cpp:
int main(int argc, char *argv[])
{
...
QApplication app(argc, argv);
MainWindow window;
window.show();
app.exec();
}
In the mainwindow.cpp:
void MainWindow::createActions()
{
m_pLockMenu = menuBar()->addMenu("Lock Toolbars");
m_pFileToolBar = addToolBar("File");
m_pEditToolBar = addToolBar("Edit");
m_pHelpToolBar = addToolBar("Help");
m_pLockAction = new QAction("Lock", this);
m_pLockMenu->addAction(m_pLockAction);
connect(m_pLockAction, SIGNAL(triggered()), this, SLOT(lockActionTriggered()));
...
}
void MainWindow::lockActionTriggered()
{
m_pFileToolBar->setMovable(false);
m_pEditToolbar->setMovable(false);
m_pHelpToolBar->setMovable(false);
}
If you have a pointer to your mainwindow, you can simply find all the toolbars and iterate over them:
// in slot
for (auto *t: mainWindow->findChildren<QToolBar*>())
t->setMovable(false);
Alternatively, you might want to connect all toolbars to a single toggle action, when you initialise the UI:
// in constructor
for (auto *t: this->findChildren<QToolBar*>())
connect(action, &QAction::toggled, t, &QToolBar::setMovable);
One common example with flexible lock options
# Created by BaiJiFeiLong#gmail.com at 2022/2/15 22:56
import typing
from PySide2 import QtWidgets, QtGui, QtCore
class Window(QtWidgets.QMainWindow):
def createPopupMenu(self) -> QtWidgets.QMenu:
menu = super().createPopupMenu()
menu.addSeparator()
toolBars: typing.Sequence[QtWidgets.QToolBar] = self.findChildren(QtWidgets.QToolBar)
for toolbar in toolBars:
if toolbar.rect().contains(toolbar.mapFromGlobal(QtGui.QCursor.pos())):
title = "%s %s" % ("Lock" if toolbar.isMovable() else "Unlock", toolbar.windowTitle())
menu.addAction(title, lambda toolbar=toolbar: toolbar.setMovable(not toolbar.isMovable()))
menu.addSeparator()
if any(x.isMovable() for x in toolBars):
menu.addAction("Lock All", lambda: list(x.setMovable(False) for x in toolBars))
if any(not x.isMovable() for x in toolBars):
menu.addAction("Unlock All", lambda: list(x.setMovable(True) for x in toolBars))
return menu
app = QtWidgets.QApplication()
window = Window()
window.setWindowTitle("Toolbar Example")
for i in range(1, 6):
toolBar = window.addToolBar(f"ToolBar{i}")
toolBar.setMovable(i % 2 == 0)
toolBar.addWidget(QtWidgets.QPushButton(f"ToolBar {i}"))
label = QtWidgets.QLabel("Ready")
label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
window.setCentralWidget(label)
window.resize(800, 600)
window.show()
app.exec_()

Switching between one QToolButton to another in QT

I want to switch from one QToolButton to another in QToolBar. I have used QStackedWidget, their it is too simple to move from one widget to other but here I am not able to get how to move via using keyReleaseEvent.
mywindow::mywindow() : QMainWindow()
{
widget = new QWidget();
setCentralWidget(widget);
tool = new QToolBar();
vertical = new QVBoxLayout();
button1 = new QToolButton();
connect( button1, SIGNAL(clicked()), this, SLOT(fileNew()) );
button2 = new QToolButton();
button3 = new QToolButton();
button1->setIcon(QIcon("download.jpg"));
button1->setGeometry(0,0,100,200);
button2->setIcon(QIcon("images.jpg"));
button3->setIcon(QIcon("settings-icon.jpg"));
//stack1->addWidget(button1);
//stack1->addWidget(button2);
//stack1->addWidget(button3);
tool->addWidget(button1);
tool->addWidget(button2);
tool->addWidget(button3);
//tool->addWidget(stack1);
vertical->addWidget(tool);
widget->setLayout(vertical);
}
void mywindow::keyReleaseEvent(KeyEvent *event)
{
switch(event->key())
{
case:Qt::Key_Left:
}
}
You need to check against the focus, and shift that as appropriate. I would write something like this:
void mywindow::keyReleaseEvent(KeyEvent *event)
{
switch(event->key())
{
case:Qt::Key_Left:
if (button3->hasFocus())
button2->setFocus();
else if (button2->hasFocus())
button1->setFocus();
break;
case:Qt::Key_Right:
if (button1->hasFocus())
button2->setFocus();
else if (button2->hasFocus())
button3->setFocus();
break;
}
}
Note that this code can go tedious easily if you keep adding further buttons. I would place them into a container. Then, I would iterate through that container forward and reverse order depending on the focus switching logic.
See the documentation for further details:
focus : const bool
This property holds whether this widget (or its focus proxy) has the keyboard input focus.
By default, this property is false.
Note: Obtaining the value of this property for a widget is effectively equivalent to checking whether QApplication::focusWidget() refers to the widget.
Access functions:
bool hasFocus() const

Automatically populate "Edit" menu in menubar with current focus widget context menu

I've been looking for ways to implement the "Edit" menu of Qt application. The "Edit" menu contains items such as "Copy", "Cut", "Paste", etc. and which need to forward to the currently active widget.
I can't seem to find a standard or elegant way to do this. According to this question, it's not possible:
How to implement the "Edit" menu with "Undo", "Cut", "Paste" and "Copy"?
I recently had the idea to trigger a context menu event on the current active widget when the "Edit" menu is shown, via:
// create menus in MainWindow constructor
...
edit_menu = menuBar()->addMenu(tr("&Edit"));
connect(edit_menu, SIGNAL(aboutToShow()), this, SLOT(showEditMenu()));
...
// custom slot to handle the edit menu
void MainWindow::showEditMenu()
{
QWidget* w = QApplication::focusWidget();
// show the context menu of current focus widget in the menubar spot
QPoint global_pos = edit_menu->mapToGlobal(edit_menu->rect().bottomLeft());
QPoint pos = w->mapFromGlobal(global_pos);
QApplication::sendEvent(w, new QContextMenuEvent(QContextMenuEvent::Keyboard, pos, global_pos));
}
This shows the a context menu for the current widget great, but has some problems. For example, it takes focus away from the menubar, or if you click a different menubar item first, the menubar has focus, etc.
One partial solution would be to grab the context menu from the widget and copy it's items into the edit menu dynamically. Is there a way to do this?
Is there a better way build the edit menu in Qt?
Thanks for your help.
well, if you just need to create menu, you can always take actions from actions of a widget. To create edit actions for widgets, you can do something like this:
void MainWindow::addActions (QWidget* widget)
{
QAction * copyAction = new QAction("copy",widget);
if(connect(copyAction,SIGNAL(triggered()),widget,SLOT(copy())))
{
widget->addAction(copyAction);
qDebug()<<"success connection";
}
}
and
foreach (QObject * obj, centralWidget()->children())
{
QWidget * w = dynamic_cast<QWidget*>(obj);
if (w)
addActions(w);
}
then you always can update actions of edit menu with focused widget's actions
This may be not elegant, but it better, than imporssible. The main bad assumption in example is that copy slot is named copy
An elegant solution I guess would be to have a base class for the widgets you need to have copy/paste/... functionality, and have them register themselves with some parent class upon becoming active and unregistering when being deactivated. The actions can then just be connected to slots in the main window, which forwards them to the registered widget. You could even gray out the menu items if no widget is currently registered (e.g. because the active widget does not have the required functionality).
An example for registering/unregistering (untested):
class ActionWidget;
class ActionWidgetManager
{
public:
ActionWidgetManager() : actionWidget_(0){}
void registerWidget(ActionWidget* widget){ actionWidget_ = widget; }
void unregisterWidget(ActionWidget* widget)
{ if (actionWidget_ == widget) actionWidget_ = 0; }
bool hasActiveWidget() const{ return actionWidget_ != 0; }
ActionWidget* getActiveWidget(){ return actionWidget_; }
private:
ActionWidget* actionWidget_;
};
class ActionWidget : public QWidget
{
public:
ActionWidget(ActionWidgetManager* manager, QWidget* parent=0)
: manager_(manager), QWidget(parent) {}
~ActionWidget(){ manager_->unregisterWidget(this); }
void Widget::changeEvent(QEvent *event)
{
QWidget::changeEvent(event);
if(event->type() == QEvent::ActivationChange){
if(isActiveWindow()) {
manager_->registerWidget(this);
}
else {
manager_->unregisterWidget(this);
}
}
}
virtual void doCopy() = 0;
virtual void doPaste() = 0;
virtual void doUndo() = 0;
virtual void doCut() = 0;
private:
ActionWidgetManager* manager_;
};
Or something equivalent using signals and slots.