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

如何实现PySide6中Plotly绘图窗口不阻塞数据分析脚本运行?

实现非阻塞的Plotly独立绘图窗口(类似Matplotlib)

我运行数据分析脚本时更偏好Plotly而非Matplotlib,想实现类似Matplotlib的非阻塞绘图窗口效果,不会阻塞脚本运行。

编写了如下代码,通过PySide6的WebEngineWidget显示离线Plotly图表:

import numpy as np
import plotly.graph_objs as go
import plotly.offline

fig = go.Figure()
fig.add_scatter(x=np.random.rand(100), y=np.random.rand(100), mode='markers',
                marker={'size': 30, 'color': np.random.rand(100), 'opacity': 0.6,
                        'colorscale': 'Viridis'});

import os, sys
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QUrl
from PySide6 import QtWebEngineWidgets


class PlotlyViewer(QtWebEngineWidgets.QWebEngineView):
    def __init__(self, fig, exec=True):
        # 创建QApplication实例或复用已有实例
        self.app = QApplication.instance() if QApplication.instance() else QApplication(sys.argv)

        super().__init__()

        self.file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "temp.html"))
        plotly.offline.plot(fig, filename=self.file_path, auto_open=False)
        self.load(QUrl.fromLocalFile(self.file_path))
        self.setWindowTitle("Plotly Viewer")
        self.show()

        if exec:
            self.app.exec()

    def closeEvent(self, event):
        os.remove(self.file_path)


win = PlotlyViewer(fig)

但执行以下代码时,win2要等第一个窗口关闭后才会弹出:

win = PlotlyViewer(fig)
win2 = PlotlyViewer(fig2)
win3 = PlotlyViewer(fig3)

我知道可以制作包含多图表的部件,但有时只需查看单个图表,有时需要多个,想知道能不能通过进程实现脚本持续执行,或者这是否和Python GIL有关?


问题原因

核心问题是**QApplication.exec()会阻塞主线程**:每次创建PlotlyViewer实例时,如果exec=True,就会启动Qt的事件循环并卡住主线程,直到窗口关闭才会继续执行后续代码。这和GIL无关,是Qt事件循环的运行机制导致的。

解决方法

方法1:统一管理Qt事件循环(推荐)

修改PlotlyViewer类,不在实例化时调用app.exec(),而是在所有窗口创建完成后统一启动事件循环,这样所有窗口会同时显示,且脚本在事件循环启动前不会被阻塞。

修改后的代码:

import numpy as np
import plotly.graph_objs as go
import plotly.offline
import os, sys
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QUrl
from PySide6 import QtWebEngineWidgets


class PlotlyViewer(QtWebEngineWidgets.QWebEngineView):
    def __init__(self, fig):
        self.app = QApplication.instance() if QApplication.instance() else QApplication(sys.argv)
        super().__init__()
        
        # 为每个窗口生成独立临时HTML文件,避免互相覆盖
        self.file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), f"temp_{id(self)}.html"))
        plotly.offline.plot(fig, filename=self.file_path, auto_open=False)
        self.load(QUrl.fromLocalFile(self.file_path))
        self.setWindowTitle("Plotly Viewer")
        self.show()

    def closeEvent(self, event):
        os.remove(self.file_path)


# 创建多个测试图表
fig = go.Figure()
fig.add_scatter(x=np.random.rand(100), y=np.random.rand(100), mode='markers',
                marker={'size':30, 'color':np.random.rand(100), 'opacity':0.6, 'colorscale':'Viridis'})

fig2 = go.Figure()
fig2.add_bar(x=[1,2,3], y=[4,5,6])

fig3 = go.Figure()
fig3.add_pie(values=[30,20,50])

# 一次性创建所有窗口
win = PlotlyViewer(fig)
win2 = PlotlyViewer(fig2)
win3 = PlotlyViewer(fig3)

# 统一启动事件循环(这一步会阻塞,但所有窗口已完成创建)
QApplication.instance().exec()

方法2:多进程实现完全非阻塞

如果需要脚本在显示窗口后继续执行其他任务(无需等待窗口关闭),可以用multiprocessing模块为每个窗口启动独立进程,每个窗口的Qt事件循环在各自进程中运行,完全不阻塞主脚本。

示例代码:

import numpy as np
import plotly.graph_objs as go
import plotly.offline
import os, sys
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QUrl
from PySide6 import QtWebEngineWidgets
import multiprocessing


def show_plot(fig):
    app = QApplication(sys.argv)
    viewer = QtWebEngineWidgets.QWebEngineView()
    # 用进程ID生成唯一临时文件
    file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), f"temp_{multiprocessing.current_process().pid}.html"))
    plotly.offline.plot(fig, filename=file_path, auto_open=False)
    viewer.load(QUrl.fromLocalFile(file_path))
    viewer.setWindowTitle(f"Plotly Viewer - PID {multiprocessing.current_process().pid}")
    viewer.show()
    
    # 程序退出时清理临时文件
    def cleanup():
        os.remove(file_path)
    app.aboutToQuit.connect(cleanup)
    
    app.exec()


# 创建测试图表
fig = go.Figure()
fig.add_scatter(x=np.random.rand(100), y=np.random.rand(100), mode='markers',
                marker={'size':30, 'color':np.random.rand(100), 'opacity':0.6, 'colorscale':'Viridis'})

fig2 = go.Figure()
fig2.add_bar(x=[1,2,3], y=[4,5,6])

fig3 = go.Figure()
fig3.add_pie(values=[30,20,50])

# 启动多进程显示窗口
processes = []
for f in [fig, fig2, fig3]:
    p = multiprocessing.Process(target=show_plot, args=(f,))
    p.start()
    processes.append(p)

# 主脚本继续执行其他任务(示例:打印进度)
print("主脚本继续运行,不会被窗口阻塞")
for i in range(10):
    print(f"主脚本执行任务 {i+1}/10")
    import time
    time.sleep(1)

# 可选:等待所有窗口关闭后结束主脚本
# for p in processes:
#     p.join()

关于GIL的说明

这个问题和Python GIL无关。GIL主要影响多线程中CPU密集型任务的并行执行,而你的问题是Qt事件循环阻塞主线程导致的。多进程方案之所以有效,是因为每个进程有独立的Python解释器和GIL,完全隔离了各个窗口的事件循环。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 22:01:36