【问题标题】:Prevent toolbar from being added to context menu防止工具栏被添加到上下文菜单
【发布时间】:2021-08-13 19:58:13
【问题描述】:

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

我希望这个自定义工具栏出现在上下文菜单中。然而,尽管使用了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_())

【问题讨论】:

    标签: pyqt pyside


    【解决方案1】:

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

    某些小部件不会覆盖 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 更改为 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的目标是没有显示与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_())
    

    【讨论】:

    • 感谢更新。它仍然不能完全满足我的要求,但我相信那是 Qt/Python,而不是你!似乎让 CustomToolBar 执行删除的唯一方法是通过猴子修补。正如您所指出的,这可能很危险。我接受这个是因为它回答了书面的问题,感谢您的时间和精力,并且我学到了一些东西。谢谢! :)
    【解决方案2】:

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

    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 没有出现在上下文菜单中,那就更复杂了。一种选择是通过父级(即 MainWindow)使用自定义上下文菜单。 This post 解释了如何做到这一点。

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

    createContextPopupMenu 方法由 MainWindow 的 contextMenuEvent handler 调用。因此,利用 Python 将函数视为一等公民这一事实,我们可以将 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_())
    
    

    【讨论】:

    • 不要做foo.fooEvent = some_function,因为有些可能会失败。 PyQt(以及 PySide)缓存这些方法,因此如果相同的逻辑适用于您的代码中的其他地方,那么您的解决方案将失败。
    • 另一方面,您的代码很危险,因为例如 QToolBar 可以有 QWidget 的父级,因此调用 self.parent().addToolBar 可能会引发异常。在我的代码中,我验证父级是 QMainWindow。
    • 感谢您的提醒。有了这些知识,以及所需行为的明确示例,是否有一种安全的方法来实现相同的结果?当我运行您的代码时,我没有观察到预期的结果。
    • 嗯,我的代码阻止了弹出窗口的显示,这不是您所期望的吗?
    • 可能你没有正确实现我的demo。为了使其更明显,我将 QToolBar 的背景颜色设置为红色,因此如果您右键单击弹出窗口将不会显示。 gist.github.com/eyllanesc/6590727e5e30db8df2025b8a7c2ee35d
    猜你喜欢
    • 2010-09-17
    • 2011-10-20
    • 1970-01-01
    • 1970-01-01
    • 2011-06-21
    • 2016-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多