PyQt QMenu в Python — создание меню приложения

Класс PyQt QMenu позволяет создавать виджеты меню в строках меню, контекстных меню и всплывающих меню.

Знакомство с PyQt QMenu

В этом руководстве основное внимание уделяется использованию класса QMenu для создания меню в строках меню.

Чтобы создать меню и добавить его в строку меню, выполните следующие действия:

  • Получите строку меню главного окна, вызвав метод menuBar() объекта QMainWindow.
  • Добавьте меню в строку меню с помощью метода addMenu(). AddMenu() возвращает новый экземпляр класса QMenu.

Ниже показано, как добавить три меню в строку меню главного окна, включая «File», «Edit» и «Help»:

menu_bar = self.menuBar()

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

Обратите внимание, что амперсанд(&) определяет сочетание клавиш для перехода в меню при нажатии клавиши Alt. Например, чтобы перейти в меню Файл, нажмите сочетание клавиш Alt-F.

После создания меню вы можете добавлять в него элементы. Обычно вы создаете QAction и используете метод addAction() объекта QMenu для добавления действий в меню.

Чтобы добавить разделитель между пунктами меню, используйте метод addSeparator() объекта QMenu.

Пример PyQt QMenu

Мы создадим приложение текстового редактора, чтобы продемонстрировать, как использовать класс QMenu:

Пример PyQt QMenu

Обратите внимание, что иконки, используемые в этом приложении, взяты с сайта icon8.com.

Вот полная программа:

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())

Как это работает.

  • Сначала создайте главное окно с помощью класса QMainWindow:
class MainWindow(QMainWindow):
  • Во-вторых, задайте значок и геометрию окна:
self.setWindowIcon(QIcon('./assets/editor.png'))
self.setGeometry(100, 100, 500, 300)
  • В-третьих, инициализируйте фильтры текстового файла и заголовок окна, а затем вызовите метод set_title(), чтобы задать заголовок окна:
self.filters = 'Text Files(*.txt)'
self.title = 'Editor'
self.set_title()

Метод set_title() принимает имя файла. Если имя файла опущено, метод set_title() устанавливает заголовок окна как Untitled — Editor. В противном случае он устанавливает заголовок окна, используя формат filename — Editor:

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

Например, при первом запуске программы или создании нового файла заголовок окна будет следующим:

PyQt QMenu — заголовок окна по умолчанию

Если вы откроете файл, например, C:/temp/test.txt, заголовок окна изменится на:

PyQt QMenu — заголовок окна с именем файла

  • В-четвертых, инициализируйте переменную, которая будет содержать путь к файлу, открываемому для редактирования:
self.path = None

Обратите внимание, что мы будем использовать класс Path из модуля pathlib для управления путем к файлу, чтения из текстового файла и записи в текстовый файл.

  • В-пятых, создайте виджет QTextEdit и установите его в качестве центрального виджета главного окна:
self.text_edit = QTextEdit(self)
self.setCentralWidget(self.text_edit)
  • В-шестых, создайте объект QMenuBar, вызвав метод menuBar() объекта QMainWindow:
menu_bar = self.menuBar()
  • В-седьмых, создайте действия «new», «open», «save» и «exit» и добавьте их в file_menu с помощью метода addAction().
# 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)

В результате появится следующее меню:

Использование метода addAction()

  • В-восьмых, создайте действия отмены и повтора и добавьте их в меню редактирования:
# 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)

В результате появится следующее меню «Edit»:

Пример меню Edit

  • В-девятых, создайте действие «about action» и добавьте его в меню «Help»:
about_action = QAction(QIcon('./assets/about.png'), 'About', self)
help_menu.addAction(about_action)
about_action.setStatusTip('About')
about_action.setShortcut('F1')

В результате появится следующее меню:

Пример добавления действия «about action»

  • В-десятых, добавьте строку состояния в главное окно, используя метод statusBar() объекта QMainWindow:
self.status_bar = self.statusBar()

Обратите внимание, что более подробную информацию о виджете строки состояния вы найдете в руководстве QStatusBar.

  • Одиннадцатое, определите метод confirmed_save(), который запрашивает у пользователя, сохранять документ или нет. Если пользователь нажимает кнопку Yes, вызовите метод save_document(), чтобы сохранить текст виджета QTextEdit в файл.

Метод confirmed_save() возвращает значение False, если пользователь нажимает кнопку «Отмена», или значение True, если пользователь нажимает кнопку «Да» или «Нет»:

Метод confirmed_save()

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
  • Двенадцатое, определите метод new_document(), который запускается, когда пользователь выбирает пункт меню «New»:
def new_document(self):
    if self.confirm_save():
        self.text_edit.setText('')
        self.set_title()

Метод new_document() вызывает метод confirmed_save() для сохранения документа и установки текста QTextEdit пустым. Также он сбрасывает заголовок главного окна.

  • Тринадцатое, определите метод save_document() для сохранения текста виджета QTextEdit в текстовый файл:
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)

Если пользователь открывает файл, то self.path не равен None, он получает текст виджета QTextEdit, вызывая метод toPlainText(), и сохраняет текст в файле, указанном объектом Path, с помощью метода write_text().

Если пользователь не открыл файл, метод отображает диалоговое окно сохранения файла с помощью QFileDialog и записывает текст в файл, который в данный момент открыт.

  • В-четырнадцатых, определите метод open_document(), который отображает диалоговое окно открытия файла и загружает содержимое из текстового файла в виджет QTextEdit:
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)

Поскольку имя файла изменяется, вызывается метод set_title() для установки заголовка QMainWindow.

  • Пятнадцатое, определите метод quit(), который запускается, когда пользователь выбирает пункт меню «Exit»:
def quit(self):
    if self.confirm_save():
        self.destroy()

Наконец, если вы запустите программу на Windows, на панели задач не будет правильно отображаться значок главного окна. Чтобы исправить это, используйте следующий код:

import ctypes
myappid = 'mycompany.myproduct.subproduct.version'
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)

Если вы запустите программу в macOS или Linux, этот код вызовет ошибку импорта. Поэтому мы обернем его в блок try:

try:
    import ctypes
    myappid = 'mycompany.myproduct.subproduct.version'
    ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
finally:
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
Похожие посты
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *