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

PyQt6 WebEngineView无法加载本地Bokeh服务器嵌入图表问题

问题分析与解决方案

问题概述

开发PyQt6应用,支持用户选择CSV文件并指定列生成图表:

  • 启动本地Bokeh服务器,将图表嵌入PyQt6的WebEngineView新窗口
  • 本地Bokeh地址http://localhost:6005/base无法在WebEngineView完成加载,不触发loadFinished信号;加载http://google.com正常
  • 终端出现'doh set to "" -- SystemOnly'提示,不确定是否相关
  • 使用show(p)时图表能在浏览器正常显示,需确认该方案可行性,以及是否应改用components实现(要求100%离线)

核心问题原因

  1. Bokeh文档更新逻辑错误:当前代码在PyQt主线程直接调用modify_doc,而非在Bokeh服务器的回调上下文内执行,导致生成的图表未同步到服务器的文档实例,WebEngineView请求页面时服务器返回空文档,加载流程挂起。
  2. 信号连接时机滞后:设置URL之后才连接loadStarted/loadFinished信号,可能错过事件触发节点。

两种实现方案

方案一:修复Bokeh服务器嵌入方案(可行,保留交互性)

若需要Bokeh的动态交互特性(如数据更新、工具操作),该方案可正常运行,只需修正以下几点:

代码修改要点

def create_bokeh_server(self):
    from bokeh.application import Application
    from bokeh.application.handlers.function import FunctionHandler
    from bokeh.server.server import Server
    from threading import Thread

    def bokeh_app(doc):
        # 初始化空文档,后续通过会话更新
        doc.title = "CSV Plot"
        self.bokeh_doc = doc
        doc.add_root(figure(title="Waiting for data...", height=400, width=600))

    handler = FunctionHandler(bokeh_app)
    bokeh_application = Application(handler)
    bokeh_server = Server({"/base": bokeh_application}, port=6005)
    bokeh_server.start()
    # 启动服务器后台线程,避免阻塞PyQt主线程
    Thread(target=bokeh_server.io_loop.start, daemon=True).start()
    self.url = f"http://localhost:{bokeh_server.port}/base"
    print(f"Bokeh server running at: {self.url}")
    return bokeh_server

def plot_data(self):
    from PyQt6.QtCore import QUrl
    
    if not self.file_path:
        self.label.setText("Please choose a CSV file first.")
        return

    column_name, ok = QInputDialog.getText(self, "Enter Column", "Enter column name:")
    if not ok:
        return

    self.column_name = column_name

    try:
        df = pd.read_csv(self.file_path)
        if self.column_name not in df.columns:
            self.label.setText(f"Column '{self.column_name}' not found in the CSV file.")
            return

        # 在Bokeh服务器IO循环中更新文档,确保上下文正确
        def update_doc():
            self.bokeh_doc.clear()
            plot_title = f"Plot of {self.column_name} from {self.file_path}"
            source = ColumnDataSource(df)
            x_values = [str(val) for val in df.index]
            p = figure(
                title=plot_title,
                x_range=x_values,
                height=400,
                width=600,
                toolbar_location="right",
                tools="pan,box_zoom,reset"
            )
            p.line(x="index", y=self.column_name, source=source, line_width=2)
            self.bokeh_doc.add_root(p)
        
        self.bokeh_server.io_loop.add_callback(update_doc)
        
        # 先连接信号再设置URL,避免错过事件
        self.bokeh_plot_window.web_view.loadStarted.connect(lambda: self.test("loadStarted"))
        self.bokeh_plot_window.web_view.loadProgress.connect(lambda: self.test("loadProgress"))
        self.bokeh_plot_window.web_view.loadFinished.connect(lambda: self.test("loadFinished"))
        
        self.bokeh_plot_window.web_view.setUrl(QUrl(self.url))
        self.bokeh_plot_window.show()
        
    except Exception as e:
        import traceback
        traceback.print_exc()
        self.label.setText(f"Error plotting data: {str(e)}")

方案二:改用components实现(100%离线,推荐满足离线需求时使用)

若无需Bokeh服务器的动态交互,直接生成HTML/JS代码嵌入WebEngineView,完全无需启动本地服务器:

实现代码示例

def plot_data(self):
    from PyQt6.QtCore import QUrl
    from bokeh.embed import components
    
    if not self.file_path:
        self.label.setText("Please choose a CSV file first.")
        return

    column_name, ok = QInputDialog.getText(self, "Enter Column", "Enter column name:")
    if not ok:
        return

    self.column_name = column_name

    try:
        df = pd.read_csv(self.file_path)
        if self.column_name not in df.columns:
            self.label.setText(f"Column '{self.column_name}' not found in the CSV file.")
            return

        plot_title = f"Plot of {self.column_name} from {self.file_path}"
        source = ColumnDataSource(df)
        x_values = [str(val) for val in df.index]
        p = figure(
            title=plot_title,
            x_range=x_values,
            height=400,
            width=600,
            toolbar_location="right",
            tools="pan,box_zoom,reset"
        )
        p.line(x="index", y=self.column_name, source=source, line_width=2)
        
        # 生成HTML组件
        script, div = components(p)
        # 构建完整HTML,替换为本地资源实现离线
        html = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <link rel="stylesheet" href="./bokeh.min.css" type="text/css" />
            <script src="./bokeh.min.js"></script>
            {script}
        </head>
        <body>
            {div}
        </body>
        </html>
        """
        
        # 加载HTML到WebEngineView
        self.bokeh_plot_window.web_view.setHtml(html)
        self.bokeh_plot_window.show()
        
    except Exception as e:
        import traceback
        traceback.print_exc()
        self.label.setText(f"Error plotting data: {str(e)}")

离线化步骤

  1. 从Bokeh官网下载对应版本的bokeh.min.js和bokeh.min.css
  2. 将文件放到应用目录下,替换HTML中的CDN链接为本地路径

关于'doh set to "" -- SystemOnly'提示

该提示是Qt WebEngine的DNS-over-HTTPS配置信息,与当前加载问题无关,可忽略。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 09:26:05