防止将工具栏添加到上下文菜单

Prevent toolbar from being added to context menu

我正在创建一个自定义工具栏,它会在初始化时自动将自己添加到父级(如果存在)。

我希望此自定义工具栏不会出现在上下文菜单中。但是,尽管使用 setContextMenuPolicy :

,但还是会出现与工具栏相关的内容(我不知道是什么)

我不知道上下文菜单项是什么。我的理解是,任何小部件都可以通过其 contextMenuPolicy 的方式添加到上下文菜单中。但是 CustomToolBar 中没有任何其他小部件。

解决方法是完全禁用 MainWindow 上的上下文菜单并创建一个切换可见性的菜单项(例如视图)。

import sys
import time
from PyQt5 import QtCore, QtWidgets


class CustomToolBar(QtWidgets.QToolBar):

    def __init__(self, parent=None):
        super().__init__(parent=parent)

        if parent:
            self.setParent(parent)
            self.parent().addToolBar(QtCore.Qt.BottomToolBarArea, self)

        self.setObjectName('Custom ToolBar')
        self.setContextMenuPolicy(QtCore.Qt.NoContextMenu)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.counter = 0
        self.resize(250, 75)

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):

        self.exit_action = QtWidgets.QAction('&Exit', self)
        self.exit_action.setShortcut('Ctrl+Q')
        self.exit_action.setToolTip('Exit application')
        self.exit_action.triggered.connect(self.close)

        self.menu = self.menuBar()

        self.menu_file = self.menu.addMenu('&File')
        self.menu_file.addAction(self.exit_action)

        # setting parent to self embeds the custom toolbar in the main window
        self.status = CustomToolBar(self)

    def init_layout(self):
        layout = QtWidgets.QVBoxLayout()

        centralWidget = QtWidgets.QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec_())

注意:如果您单击菜单栏或下方的工具栏,图像将无法正确显示。

一些小部件不会覆盖 contextMenuEvent 方法,因此使用 self.setContextMenuPolicy(QtCore.Qt.NoContextMenu) 将不起作用,QToolBar(以及 QMenuBar 也是如此)就是这种情况。在这些情况下,必须覆盖并拒绝事件方法。

假设您只是希望它不出现在 QToolBar 中,那么只需使用:

class CustomToolBar(QtWidgets.QToolBar):

    def __init__(self, parent=None):
        super().__init__(parent=parent)

        if isinstance(parent, QtWidgets.QMainWindow):
            self.setParent(parent)
            parent.addToolBar(QtCore.Qt.BottomToolBarArea, self)

        self.setObjectName('Custom ToolBar')

    def event(self, event):
        if event.type() == QtCore.QEvent.ContextMenu:
            return True
        return super().event(event)

如果您还想对 MenuBar 执行相同的操作,则必须实现类似的逻辑:

class StatusBar(QtWidgets.QMenuBar):
    def event(self, event):
        if event.type() == QtCore.QEvent.ContextMenu:
            return True
        return super().event(event)

最后,建议使用表明它是什么类型的小部件的名称,所以我将菜单更改为 menu_bar:

def init_widgets(self):

    self.exit_action = QtWidgets.QAction('&Exit', self)
    self.exit_action.setShortcut('Ctrl+Q')
    self.exit_action.setToolTip('Exit application')
    self.exit_action.triggered.connect(self.close)

    self.menu_bar = StatusBar()
    self.setMenuBar(self.menu_bar)

    self.menu_file = self.menu_bar.addMenu('&File')
    self.menu_file.addAction(self.exit_action)

    # setting parent to self embeds the custom toolbar in the main window
    self.tool_bar = CustomToolBar(self)

更新:

似乎OP的objective是QMainWindow的QMenu中实现的与QToolBar关联的QAction没有显示,所以为此最好覆盖createPopupMenu方法并删除QAction为此,没有必要实现自定义 QToolBar。

import sys
import uuid

from PyQt5 import QtCore, QtWidgets


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):

        self.exit_action = QtWidgets.QAction("&Exit", self)
        self.exit_action.setShortcut("Ctrl+Q")
        self.exit_action.setToolTip("Exit application")
        self.exit_action.triggered.connect(self.close)

        self.menu_bar = self.menuBar()

        self.menu_file = self.menu_bar.addMenu("&File")
        self.menu_file.addAction(self.exit_action)

        self.custom_toolbar = QtWidgets.QToolBar()
        self.addToolBar(QtCore.Qt.BottomToolBarArea, self.custom_toolbar)
        self.custom_toolbar.setProperty("hide_action_toolbar", True)

        self.dock = QtWidgets.QDockWidget("Dock")
        self.addDockWidget(QtCore.Qt.TopDockWidgetArea, self.dock)

    def init_layout(self):
        layout = QtWidgets.QVBoxLayout()

        centralWidget = QtWidgets.QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)

    def createPopupMenu(self):
        titles = []
        for toolbar in self.findChildren(QtWidgets.QToolBar):
            if toolbar.property("hide_action_toolbar") is None:
                continue
            if toolbar.property("hide_action_toolbar"):
                toolbar.setProperty("last_window_title", toolbar.windowTitle())
                toolbar.setWindowTitle(uuid.uuid4().hex)
                titles.append(toolbar.windowTitle())
            else:
                toolbar.setWindowTitle(toolbar.property("last_window_title") or "")
        menu = super().createPopupMenu()
        for action in menu.actions():
            if action.text() in titles:
                menu.removeAction(action)
        return menu


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()
    main_window.resize(640, 480)
    main_window.show()
    sys.exit(app.exec_())

上下文菜单中显示的项目实际上是工具栏。菜单项来自相应的操作文本。这反过来又来自工具栏 window 标题。由于默认为空字符串,因此在菜单中显示为空白。

将行 self.setObjectName('Custom ToolBar') 更改为 self.setWindowTitle('Custom ToolBar') 将按预期显示工具栏名称。

import sys
import time
from PyQt5 import QtCore, QtWidgets


class CustomToolBar(QtWidgets.QToolBar):

    def __init__(self, parent=None, title='CustomToolBar'):
        super().__init__(parent=parent)

        # Sets the action text in the context menu
        self.setWindowTitle(title)

        if parent:
            self.setParent(parent)
            self.parent().addToolBar(QtCore.Qt.BottomToolBarArea, self)

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.counter = 0
        self.resize(250, 75)

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):

        self.exit_action = QtWidgets.QAction('&Exit', self)
        self.exit_action.setShortcut('Ctrl+Q')
        self.exit_action.setToolTip('Exit application')
        self.exit_action.triggered.connect(self.close)

        # self.menu_bar = CustomMenuBar()
        # self.setMenuBar(self.menu_bar)
        self.menu_bar = self.menuBar()

        self.menu_file = self.menu_bar.addMenu('&File')
        self.menu_file.addAction(self.exit_action)

        # self embeds the toolbar in the main window
        self.status = CustomToolBar(self)

    def init_layout(self):
        layout = QtWidgets.QVBoxLayout()

        centralWidget = QtWidgets.QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()

    main_window.show()
    sys.exit(app.exec_())

至于CustomToolBar没有出现在上下文菜单中,那就比较复杂了。一种选择是通过 parent(即 MainWindow)使用自定义上下文菜单。 This post 解释如何做到这一点。

然而,想要让 CustomToolBar 自动删除自身对用户来说更好,但更具挑战性。上下文菜单实际上是由 QMainWindow 通过 createContextPopupMenu 方法动态创建的。 CustomToolBar 必须重写此方法,以便将其自身从结果列表中删除。理想情况下,这应该以保留上下文菜单行为的方式完成。

createContextPopupMenu 方法由 MainWindow 的 contextMenuEvent handler 调用。因此,利用 Python 将函数视为第一个 class 公民这一事实,我们可以将 createPopupMenu 方法替换为从中删除 CustomToolBar 的版本。下面的示例添加了一个 QDockWidget 以演示 CustomToolBar 已自行删除。

import sys
import time
from PyQt5 import QtCore, QtWidgets


class CustomToolBar(QtWidgets.QToolBar):

    def __init__(self, parent=None, title='CustomToolBar'):
        super().__init__(parent=parent)

        # Sets the action text in the context menu
        self.setWindowTitle(title)

        if parent:
            self.setParent(parent)
            self.parent().addToolBar(QtCore.Qt.BottomToolBarArea, self)

            ############################################################
            # Remove the custom toolbar from the parent's context menu #
            ############################################################
            original_popup = self.parent().createPopupMenu
            original_handler = self.parent().contextMenuEvent

            # Create custom contextMenuEvent which repurposes the
            # original createPopupMenu method through use of closures
            def customContextMenuEvent(event):

                def custom_popup():
                    popup = original_popup()

                    # Because this is being handled by CustomToolBar,
                    # which adds itself to the MainWindow, there is
                    # guaranteed to be at least one item
                    for action in popup.actions():
                        if action.text() == self.windowTitle():
                            popup.removeAction(action)
                    return popup

                # call the original handler with its call to
                # createPopupMenu sneakily replaced with our own
                self.parent().createPopupMenu = custom_popup
                original_handler(event)

            # Replace the MainWindow's contextMenuEvent with our own
            self.parent().contextMenuEvent = customContextMenuEvent

        # Prevents right click on CustomToolBar from showing context menu
        self.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.counter = 0
        self.resize(250, 75)

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):

        self.exit_action = QtWidgets.QAction('&Exit', self)
        self.exit_action.setShortcut('Ctrl+Q')
        self.exit_action.setToolTip('Exit application')
        self.exit_action.triggered.connect(self.close)

        self.menu_bar = self.menuBar()

        self.menu_file = self.menu_bar.addMenu('&File')
        self.menu_file.addAction(self.exit_action)

        # self embeds the status toolbar in the main window
        self.status = CustomToolBar(self)

        self.dock = QtWidgets.QDockWidget('Dock')
        self.addDockWidget(QtCore.Qt.TopDockWidgetArea, self.dock)


    def init_layout(self):
        layout = QtWidgets.QVBoxLayout()

        centralWidget = QtWidgets.QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    main_window = MainWindow()

    main_window.show()
    sys.exit(app.exec_())