You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MacOS下PySide6右键触发QMessageBox导致UI部分冻结的异常问题

MacOS下PySide6+Matplotlib右键点击弹出对话框导致UI冻结的解决方案

问题现象

基于PySide6开发的绘图工具,在MacOS上出现以下专属异常:

  • 点击「Draw polygon」按钮后,在绘图区右键点击确认绘制,弹出警告对话框后部分UI(如「Load new random data」按钮)冻结;左键点击则完全正常
  • 使用「Ctrl+点击」可正常运行无冻结
  • 移除对话框代码后,bug直接消失
  • Windows/Linux平台无此问题,PyQt5同类代码也存在相同bug
  • UI冻结时,右键调出绘图区上下文菜单可解除冻结

原因分析

MacOS对右键事件的处理逻辑特殊:右键点击会触发上下文菜单的事件队列,而在Matplotlib的事件回调中直接弹出模态对话框,会干扰Qt的事件循环,导致UI控件的状态更新被阻塞。具体来说,右键点击的事件在Matplotlib回调中处理时,Qt的上下文菜单事件还未完成,此时弹出模态对话框会抢占事件循环,造成部分控件无法恢复启用状态。

解决方案

方案1:延迟弹出对话框(推荐)

使用QTimer.singleShot将对话框的弹出放到下一次事件循环中,避免在Matplotlib的事件回调中直接阻塞事件循环:

# 替换原有的QMessageBox代码
button_name = 'right' if event_button == 3 else 'left'
QtCore.QTimer.singleShot(0, lambda: QtWidgets.QMessageBox.warning(self, 'Message box', f'{button_name} button input detected'))

方案2:手动触发事件处理

在弹出对话框前,强制处理所有待处理的事件,确保UI状态更新完成:

button_name = 'right' if event_button == 3 else 'left'
# 强制处理所有待处理事件
QtWidgets.QApplication.processEvents()
QtWidgets.QMessageBox.warning(self, 'Message box', f'{button_name} button input detected')

方案3:禁用绘图区的上下文菜单

如果不需要绘图区的上下文菜单,可以直接禁用,避免右键事件的冲突:
在plotCanvas的__init__方法中添加:

self.setContextMenuPolicy(QtCore.Qt.NoContextMenu)

修改后的完整可运行代码

import sys

import matplotlib
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg

import numpy as np
from PySide6 import QtWidgets, QtCore, QtGui

matplotlib.use('QT5Agg')

class mainUi(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.resize(800, 800)
        self.centralwidget = QtWidgets.QWidget(self)
        self.gridLayout = QtWidgets.QGridLayout(self.centralwidget, )

        self.loadDataPB = QtWidgets.QPushButton('Load new random data', self)
        self.gridLayout.addWidget(self.loadDataPB, 0, 0)

        self.addGatePB = QtWidgets.QPushButton('Draw polygon', self)
        self.gridLayout.addWidget(self.addGatePB, 0, 1, 1, 1)

        self.mpl_canvas = plotCanvas()
        self.gridLayout.addWidget(self.mpl_canvas, 1, 0, 1, 2)

        self.action1 = QtGui.QAction('action 1')
        self.mpl_canvas.addActions([self.action1])
        self.mpl_canvas.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)

        self.setCentralWidget(self.centralwidget)

        self.loadDataPB.clicked.connect(self.draw_figure)
        self.addGatePB.clicked.connect(self.handle_draw_polygon)

        self.polygon = None
        self.draw_figure()

    def draw_figure(self):
        mock_data = np.array([10**np.random.normal(size=1000), 10**(np.random.normal(size=1000)+1)]).T

        self.mpl_canvas.ax.clear()

        self.mpl_canvas.ax.plot(mock_data[:, 0], mock_data[:, 1], 'o', label='Test sample')
        self.mpl_canvas.ax.set_xscale('log')
        self.mpl_canvas.ax.set_yscale('log')
        self.mpl_canvas.ax.set_xlim([1e-3, 1e3])
        self.mpl_canvas.ax.set_ylim([1e-2, 1e4])

        if self.polygon is not None:
            self.mpl_canvas.ax.plot([self.polygon[0], 1], [self.polygon[1], 1], marker='s', color='orange')
            
        self.mpl_canvas.draw()
        
    def handle_draw_polygon(self):
        self.gateEditor = polygonGateEditor(self.mpl_canvas.ax)
        self._disableInputForGate(True)
        self.mpl_canvas.setCursor(QtCore.Qt.CrossCursor)

        self.gateEditor.draw_confirmed.connect(self.polygon_return)
        self.gateEditor.connectInputs()

    def polygon_return(self, vert, event_button):
        self._disableInputForGate(False)
        self.mpl_canvas.unsetCursor()

        button_name = 'right' if event_button == 3 else 'left'
        # 使用QTimer延迟弹出对话框,避免事件循环冲突
        QtCore.QTimer.singleShot(0, lambda: QtWidgets.QMessageBox.warning(self, 'Message box', f'{button_name} button input detected'))
        
        self.polygon = vert
        self.mpl_canvas.ax.clear()
        self.draw_figure()

    def _disableInputForGate(self, disable=True):
        self.loadDataPB.setEnabled(not disable)
        self.addGatePB.setEnabled(not disable)

class plotCanvas(FigureCanvasQTAgg):
    def __init__(self):
        self.fig, self.ax = plt.subplots()
        self.fig.set_layout_engine("tight") 
        super().__init__(self.fig)

        self.setFocusPolicy(QtCore.Qt.ClickFocus)
        self.draw()

class polygonGateEditor(QtCore.QObject):
    draw_confirmed = QtCore.Signal(object, int)

    def __init__(self, ax) -> None:
        super().__init__()
        self.ax = ax
        self.canvas = self.ax.figure.canvas
        self.background = None

        self.pressCid, self.moveCid = (0, 0)

        self.background = self.canvas.copy_from_bbox(self.ax.bbox)
        self.line = matplotlib.lines.Line2D([], [], marker='s', color='r', animated=True)

        self.line.set_data([], [])
        self.ax.add_line(self.line)
        self.blitDraw()

    def addGate_on_press(self, event):
        if event.button == 1 or event.button == 3:
            vert = np.array([event.xdata, event.ydata])
            self.draw_confirmed.emit(vert, event.button)
            self.disconnectInputs()
            self.blitDraw()

    def addGate_on_motion(self, event):
        self.line.set_data([event.xdata, 1], [event.ydata, 1])
        self.blitDraw()

    def connectInputs(self):
        self.pressCid = self.canvas.mpl_connect('button_press_event', self.addGate_on_press)
        self.moveCid = self.canvas.mpl_connect('motion_notify_event', self.addGate_on_motion)

    def disconnectInputs(self):
        self.canvas.mpl_disconnect(self.pressCid)
        self.canvas.mpl_disconnect(self.moveCid)

    def blitDraw(self):
        self.canvas.restore_region(self.background)
        self.ax.draw_artist(self.line)
        self.canvas.blit(self.ax.bbox)

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = mainUi()
    window.show()
    sys.exit(app.exec())

内容的提问来源于stack exchange,提问作者ym3141

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 17:55:59