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
相关产品推荐
相关产品推荐

