PyQt QMenu

Summary: in this tutorial, you’ll learn how to use the PyQt QMenu class to create a menu for the application.

Introduction to PyQt QMenu class

The QMenu class allows you to create a menu widget in menu bars, context menus, and popup menus. This tutorial focuses on how to use the QMenu class to create menus in menu bars.

To create a menu and add it to a menu bar, you follow these steps:

  • Get the menu bar of the main window by calling the menuBar() method of the QMainWindow object.
  • Add a menu to the menu bar using the addMenu() method. The addMenu() returns a new instance of the QMenu class.

The following shows how to add three menus to the menu bar of the main window including File, Edit, and Help:

menu_bar = self.menuBar()

file_menu = menu_bar.addMenu('&File')
edit_menu = menu_bar.addMenu('&Edit')
help_menu = menu_bar.addMenu('&Help')Code language: Python (python)

Note that the ampersand (&) defines a shortcut to jump to the menu when pressing the Alt key. For example, to jump to the File menu, you press the Alt-F keyboard shortcut.

Once having a menu, you can add items to it. Typically, you create a QAction and use the addAction() method of the QMenu object to add actions to the menu.

To add a separator between menu items, you use the addSeparator() method of the QMenu object.

PyQt QMenu example

We’ll create a text editor application to demonstrate how to use the QMenu class:

PyQt QMenu Example

Note that the icons used in this application are from icon8.com website. Also, you can download them here.

Here’s the complete program:

import sys
from pathlib import Path
from PyQt6.QtWidgets import QApplication, QMainWindow, QTextEdit, QFileDialog, QMessageBox, QWidget, QVBoxLayout
from PyQt6.QtGui import QIcon, QAction


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setWindowIcon(QIcon('./assets/editor.png'))
        self.setGeometry(100, 100, 500, 300)
        m = 30

        self.title = 'Editor'
        self.filters = 'Text Files (*.txt)'

        self.set_title()

        self.path = None

        self.text_edit = QTextEdit(self)
        # self.setCentralWidget(self.text_edit)

        container = QWidget(self)
        container.setLayout(QVBoxLayout())
        container.layout().addWidget(self.text_edit)
        self.setCentralWidget(container)
        # container.setContentsMargins(5, 5, 5, 5)

        menu_bar = self.menuBar()

        file_menu = menu_bar.addMenu('&File')
        edit_menu = menu_bar.addMenu('&Edit')
        help_menu = menu_bar.addMenu('&Help')

        # new menu item
        new_action = QAction(QIcon('./assets/new.png'), '&New', self)
        new_action.setStatusTip('Create a new document')
        new_action.setShortcut('Ctrl+N')
        new_action.triggered.connect(self.new_document)
        file_menu.addAction(new_action)

        # open menu item
        open_action = QAction(QIcon('./assets/open.png'), '&Open...', self)
        open_action.triggered.connect(self.open_document)
        open_action.setStatusTip('Open a document')
        open_action.setShortcut('Ctrl+O')
        file_menu.addAction(open_action)

        # save menu item
        save_action = QAction(QIcon('./assets/save.png'), '&Save', self)
        save_action.setStatusTip('Save the document')
        save_action.setShortcut('Ctrl+S')
        save_action.triggered.connect(self.save_document)
        file_menu.addAction(save_action)

        file_menu.addSeparator()

        # exit menu item
        exit_action = QAction(QIcon('./assets/exit.png'), '&Exit', self)
        exit_action.setStatusTip('Exit')
        exit_action.setShortcut('Alt+F4')
        exit_action.triggered.connect(self.quit)
        file_menu.addAction(exit_action)

        # edit menu
        undo_action = QAction(QIcon('./assets/undo.png'), '&Undo', self)
        undo_action.setStatusTip('Undo')
        undo_action.setShortcut('Ctrl+Z')
        undo_action.triggered.connect(self.text_edit.undo)
        edit_menu.addAction(undo_action)

        redo_action = QAction(QIcon('./assets/redo.png'), '&Redo', self)
        redo_action.setStatusTip('Redo')
        redo_action.setShortcut('Ctrl+Y')
        redo_action.triggered.connect(self.text_edit.redo)
        edit_menu.addAction(redo_action)

        about_action = QAction(QIcon('./assets/about.png'), 'About', self)
        help_menu.addAction(about_action)
        about_action.setStatusTip('About')
        about_action.setShortcut('F1')

        # status bar
        self.status_bar = self.statusBar()
        self.show()

    def set_title(self, filename=None):
        title = f"{filename if filename else 'Untitled'} - {self.title}"
        self.setWindowTitle(title)

    def confirm_save(self):
        if not self.text_edit.document().isModified():
            return True

        message = f"Do you want to save changes to {self.path if self.path else 'Untitled'}?"
        MsgBoxBtn = QMessageBox.StandardButton
        MsgBoxBtn = MsgBoxBtn.Save | MsgBoxBtn.Discard | MsgBoxBtn.Cancel

        button = QMessageBox.question(
            self, self.title, message, buttons=MsgBoxBtn
        )

        if button == MsgBoxBtn.Cancel:
            return False

        if button == MsgBoxBtn.Save:
            self.save_document()

        return True

    def new_document(self):
        if self.confirm_save():
            self.text_edit.clear()
            self.set_title()

    def save_document(self):
        # save the currently openned file
        if (self.path):
            return self.path.write_text(self.text_edit.toPlainText())

        # save a new file
        filename, _ = QFileDialog.getSaveFileName(
            self, 'Save File', filter=self.filters
        )

        if not filename:
            return

        self.path = Path(filename)
        self.path.write_text(self.text_edit.toPlainText())
        self.set_title(filename)

    def open_document(self):
        filename, _ = QFileDialog.getOpenFileName(self, filter=self.filters)
        if filename:
            self.path = Path(filename)
            self.text_edit.setText(self.path.read_text())
            self.set_title(filename)

    def quit(self):
        if self.confirm_save():
            self.destroy()


if __name__ == '__main__':
    try:
        import ctypes
        myappid = 'mycompany.myproduct.subproduct.version'
        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
    finally:
        app = QApplication(sys.argv)
        window = MainWindow()
        sys.exit(app.exec())
Code language: Python (python)

How it works.

First, create the main window using the QMainWindow class:

class MainWindow(QMainWindow):Code language: Python (python)

Second, set the window’s icon and geometry:

self.setWindowIcon(QIcon('./assets/editor.png'))
self.setGeometry(100, 100, 500, 300)Code language: Python (python)

Third, initialize the text file filters, and window title, and call the set_title() method to set the title for the window:

self.filters = 'Text Files (*.txt)'
self.title = 'Editor'
self.set_title()Code language: Python (python)

The set_title() method accepts a filename. If the filename is omitted, the set_title() method sets the window’s title as Untitled - Editor. Otherwise, it sets the window title using the format filename - Editor:

def set_title(self, filename=None):
    title = f"{filename if filename else 'Untitled'} - {self.title}"
    self.setWindowTitle(title)Code language: Python (python)

For example, when you launch the program for the first time or create a new file, the window’s title will be:

PyQt QMenu - Default Window Title

If you open a file e.g., C:/temp/test.txt, the window’s title will change to:

PyQt QMenu - Window Title with Filename

Fourth, initialize a variable that will hold the path of the file that is being opened for editing:

self.path = NoneCode language: Python (python)

Note that we’ll use the Path class from the pathlib module to manage the file path, reading from a text file, and writing to the text file.

Fifth, create a QTextEdit widget and set it as the central widget of the main window:

self.text_edit = QTextEdit(self)
self.setCentralWidget(self.text_edit)Code language: Python (python)

Sixth, create a QMenuBar object by calling the menuBar() method of the QMainWindow object:

menu_bar = self.menuBar()Code language: Python (python)

Seventh, create new, open, save, and exit actions and add them to the file_menu using the addAction() method.

# new menu item
new_action = QAction(QIcon('./assets/new.png'), '&New', self)
new_action.setStatusTip('Create a new document')
new_action.setShortcut('Ctrl+N')
new_action.triggered.connect(self.new_document)
file_menu.addAction(new_action)

# open menu item
open_action = QAction(QIcon('./assets/open.png'), '&Open...', self)
open_action.triggered.connect(self.open_document)
open_action.setStatusTip('Open a document')
open_action.setShortcut('Ctrl+O')
file_menu.addAction(open_action)

# save menu item
save_action = QAction(QIcon('./assets/save.png'), '&Save', self)
save_action.setStatusTip('Save the document')
save_action.setShortcut('Ctrl+S')
save_action.triggered.connect(self.save_document)
file_menu.addAction(save_action)

file_menu.addSeparator()

# exit menu item
exit_action = QAction(QIcon('./assets/exit.png'), '&Exit', self)
exit_action.setStatusTip('Exit')
exit_action.setShortcut('Alt+F4')
exit_action.triggered.connect(self.quit)
file_menu.addAction(exit_action)Code language: Python (python)

It’ll result in the following menu:

Eighth, create undo and redo actions and add them to the edit menu:

# edit menu
undo_action = QAction(QIcon('./assets/undo.png'), '&Undo', self)
undo_action.setStatusTip('Undo')
undo_action.setShortcut('Ctrl+Z')
undo_action.triggered.connect(self.text_edit.undo)
edit_menu.addAction(undo_action)

redo_action = QAction(QIcon('./assets/redo.png'), '&Redo', self)
redo_action.setStatusTip('Redo')
redo_action.setShortcut('Ctrl+Y')
redo_action.triggered.connect(self.text_edit.redo)
edit_menu.addAction(redo_action)Code language: Python (python)

It’ll result in the following Edit menu:

Ninth, create the about action and add it to the Help menu:

about_action = QAction(QIcon('./assets/about.png'), 'About', self)
help_menu.addAction(about_action)
about_action.setStatusTip('About')
about_action.setShortcut('F1')Code language: Python (python)

It’ll result in the following menu:

Tenth, add the status bar to the main window using the statusBar() method of the QMainWindow object:

self.status_bar = self.statusBar()Code language: Python (python)

Note that you’ll learn more about the status bar widget in the QStatusBar tutorial.

Eleventh, define the confirm_save() method that prompts the user whether to save the document or not. If the user clicks the Yes button, call the save_document() method to save the text of the QTextEdit widget into a file.

The confirm_save() method returns False if the user clicks the Cancel button or True if the user clicks either Yes or No button:

def confirm_save(self):
    if not self.text_edit.document().isModified():
        return True

    message = f"Do you want to save changes to {self.path if self.path else 'Untitled'}?"
    MsgBoxBtn = QMessageBox.StandardButton
    MsgBoxBtn = MsgBoxBtn.Save | MsgBoxBtn.Discard | MsgBoxBtn.Cancel

    button = QMessageBox.question(
        self, self.title, message, buttons=MsgBoxBtn
    )

    if button == MsgBoxBtn.Cancel:
        return False

    if button == MsgBoxBtn.Save:
        self.save_document()

    return TrueCode language: Python (python)

Twelfth, define the new_document() method that runs when the user selects the New menu item:

def new_document(self):
    if self.confirm_save():
        self.text_edit.setText('')
        self.set_title()Code language: Python (python)

The new_document() method calls the confirm_save() method to save the document and set the text of the QTextEdit to blank. Also, it resets the title of the main window.

Thirteenth, define the save_document() method to save the text of the QTextEdit widget to a text file:

def save_document(self):
    # save the currently openned file
    if (self.path):
        return self.path.write_text(self.text_edit.toPlainText())

    # save a new file
    filename, _ = QFileDialog.getSaveFileName(
        self, 'Save File', filter=self.filters
    )

    if not filename:
        return

    self.path = Path(filename)
    self.path.write_text(self.text_edit.toPlainText())
    self.set_title(filename)Code language: Python (python)

If the user opens a file, then the self.path is not None, it gets the text of the QTextEdit widget by calling the toPlainText() method and saves the text to the file specified by the Path object using the write_text() method.

If the user has not opened a file, the method shows a Save File Dialog using the QFileDialog and writes the text to the file that is currently opened.

Fourteenth, define the open_document() method that shows the Open File Dialog and loads the contents from a text file into the QTextEdit widget:

def open_document(self):
    filename, _ = QFileDialog.getOpenFileName(self, filter=self.filters)
    if filename:
        self.path = Path(filename)
        self.text_edit.setText(self.path.read_text())
        self.set_title(filename)Code language: Python (python)

Since the filename changes, it calls the set_title() method to set the title of the QMainWindow.

Fifteenth, define the quit() method that runs when the user selects the Exit menu item:

def quit(self):
    if self.confirm_save():
        self.destroy()Code language: Python (python)

Finally, if you run the program on Windows, the taskbar will not display the main window icon correctly. To fix it, you use the following code:

import ctypes
myappid = 'mycompany.myproduct.subproduct.version'
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)Code language: JavaScript (javascript)

If you execute the program in macOS or Linux, this code will raise an import error. Therefore, we wrap it into a try block:

try:
    import ctypes
    myappid = 'mycompany.myproduct.subproduct.version'
    ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
finally:
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())Code language: JavaScript (javascript)

Summary

  • Qt uses the QMenu class to represent a menu widget.
  • Use the menuBar() method of the QMainWindow to create a menu bar and addMenu() method to add a new menu bar.
  • Use the addAction() method of the QMenu object to add an item to a menu.
Did you find this tutorial helpful ?