Python桌面应用也能开发ECharts

很多刚学 Python 的小伙伴,一提到图表,第一反应往往是:

  • Web 项目里用前端图表库
  • 数据分析里用 matplotlib
  • 报表导出时做成 Excel

但如果你正在做 Python 桌面应用,其实还有一个很值得打开视野的方向:

直接在桌面程序里嵌入 ECharts。

这件事听起来像“前端玩法”,但放到 Python 桌面应用里其实非常顺手。只要你使用 PyQt6 / PySide6,再配合 QWebEngineView,就能把一个 Chromium 内核的网页视图嵌进窗口里,然后把 ECharts 图表直接显示在桌面应用中。

这意味着什么?

  • 你可以继续用 Python 写业务逻辑、数据处理、接口调用
  • 你可以继续用 Qt 写桌面窗口、按钮、表单、菜单
  • 你还可以把 ECharts 这种前端报表能力直接拿来用

对于正在学习 Python 的同学来说,这是一个非常适合“扩展眼界”的组合。

因为它会让你意识到一件事:

Python 桌面开发,并不等于只能用传统控件画界面。很多前端生态里的能力,也能被桌面应用直接吸收。

这篇文章我就带你从 0 到 1 跑通这个方案,快速上手:

  • 什么是 QWebEngineView
  • 为什么 PyQt6 / PySide6 + ECharts 很适合桌面报表
  • 如何写一个最小可运行示例
  • Python 如何把数据传给 ECharts
  • 实际开发里有哪些坑要提前注意

1. 这个方案的核心思路是什么

先把思路讲清楚。

ECharts 本质上是一个运行在浏览器里的 JavaScript 图表库。

QWebEngineView 可以理解成 Qt 提供的“内嵌浏览器控件”。你把它放进 PyQt6 或 PySide6 的窗口里,它就能像一个网页容器一样显示 HTML、CSS、JavaScript。

于是整件事就串起来了:

  1. Python 桌面程序创建窗口
  2. 窗口中放一个 QWebEngineView
  3. QWebEngineView 加载一段包含 ECharts 的 HTML
  4. Python 再把数据传给页面里的 JavaScript
  5. JavaScript 调用 ECharts 渲染图表

这套方式最大的优点,不是“能显示图”,而是:

  • 图表能力很强,样式现代
  • 柱状图、折线图、饼图、地图、仪表盘都能做
  • 前端里成熟的图表配置思路,桌面端也能复用
  • Python 和图表展示职责分离,代码结构更清晰

你可以把它理解成:

Python 负责数据,Qt 负责桌面容器,ECharts 负责可视化。

2. 为什么这个方案对 Python 学习者很有价值

很多初学者学桌面开发时,容易只盯着“按钮、输入框、表格”这些传统控件。

但实际项目里,经常会遇到下面这些需求:

  • 设备监控面板
  • 销售统计看板
  • 学习数据趋势图
  • 库存变化报表
  • 任务执行状态面板

如果只靠传统桌面控件硬画,开发体验和视觉效果都很一般。

这时候 QWebEngineView + ECharts 的优势就出来了:

2.1 学习成本不算高

如果你已经会一点 HTML 和 JavaScript,那上手会非常快。
即使你前端基础不多,也不用害怕,因为在很多场景里,你只需要掌握:

  • HTML 容器怎么放一个 div
  • ECharts 怎么初始化
  • setOption() 怎么配置图表
  • Python 怎么把 JSON 数据传进去

会这几件事,就已经能做出很像样的桌面报表界面。

2.2 图表表现力远强于传统桌面控件

ECharts 的交互能力非常成熟,比如:

  • 鼠标悬停提示
  • 缩放
  • 图例切换
  • 动画过渡
  • 多序列对比
  • 数据更新重绘

这些放在桌面报表里会非常自然。

2.3 有助于建立“跨技术栈组合”的思维

学习 Python 不能只停留在“只会 Python 自己这一层”。

真正做项目时,往往是多种技术一起配合:

  • Python 处理数据
  • SQL 存储数据
  • Qt 负责界面
  • Web 技术负责图表或富文本

当你能把这些东西组合起来时,做项目的视角就会完全不一样。

3. PyQt6 和 PySide6 该怎么选

这两个库本质上都是 Qt for Python 的常用绑定,API 风格非常接近。

如果你只是为了学习和快速做项目,先记住下面这件事就够了:

  • PyQt6:生态资料很多,但 QWebEngineView 需要额外安装 PyQt6-WebEngine
  • PySide6:官方系绑定,直接安装 PySide6 通常就能使用 WebEngine 模块

快速安装可以这样做。

如果你想用 PyQt6

pip install PyQt6 PyQt6-WebEngine

如果你想用 PySide6

pip install PySide6

初学阶段不用把选择问题想得太复杂。

你完全可以先学一套写法,因为这篇文章下面给你的示例会同时兼容这两种方案。

4. 先看一个最小可运行示例

下面这个示例的目标很明确:

  • 创建一个桌面窗口
  • 中间放一个 QWebEngineView
  • 加载一段 HTML
  • HTML 里初始化 ECharts
  • Python 点击按钮后刷新图表数据

我已经把完整示例整理成单独文件,方便你本地直接运行:

pyqt_qwebengine_echarts_demo.py

完整代码如下:

import json
import random
import sys


USING_PYQT6 = False

try:
    from PyQt6.QtCore import QTimer, QUrl
    from PyQt6.QtWidgets import (
        QApplication,
        QHBoxLayout,
        QLabel,
        QMainWindow,
        QPushButton,
        QVBoxLayout,
        QWidget,
    )
    from PyQt6.QtWebEngineWidgets import QWebEngineView

    USING_PYQT6 = True
except ImportError:
    from PySide6.QtCore import QTimer, QUrl
    from PySide6.QtWidgets import (
        QApplication,
        QHBoxLayout,
        QLabel,
        QMainWindow,
        QPushButton,
        QVBoxLayout,
        QWidget,
    )
    from PySide6.QtWebEngineWidgets import QWebEngineView


HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>ECharts Desktop Demo</title>
  <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
  <style>
    html, body {
      margin: 0;
      height: 100%;
      background: #f5f7fb;
      font-family: "Microsoft YaHei", sans-serif;
    }

    #chart {
      width: 100%;
      height: 100%;
    }
  </style>
</head>
<body>
  <div id="chart"></div>
  <script>
    const chart = echarts.init(document.getElementById("chart"));

    window.renderChart = function (payload) {
      const option = {
        title: {
          text: payload.title,
          left: "center"
        },
        tooltip: {
          trigger: "axis"
        },
        grid: {
          left: 48,
          right: 24,
          top: 70,
          bottom: 40
        },
        xAxis: {
          type: "category",
          data: payload.categories
        },
        yAxis: {
          type: "value"
        },
        series: [
          {
            type: "bar",
            data: payload.values,
            barWidth: 34,
            itemStyle: {
              borderRadius: [6, 6, 0, 0],
              color: "#3b82f6"
            }
          }
        ]
      };

      chart.setOption(option, true);
    };

    window.addEventListener("resize", function () {
      chart.resize();
    });
  </script>
</body>
</html>
"""


class MainWindow(QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.setWindowTitle("Python 桌面应用中的 ECharts")
        self.resize(960, 640)

        self.browser = QWebEngineView()
        self.status_label = QLabel("准备加载图表...")
        self.refresh_button = QPushButton("刷新随机数据")
        self.refresh_button.clicked.connect(self.update_chart)

        top_bar = QHBoxLayout()
        top_bar.addWidget(QLabel("PyQt6 / PySide6 + QWebEngineView + ECharts"))
        top_bar.addStretch()
        top_bar.addWidget(self.refresh_button)

        layout = QVBoxLayout()
        layout.addLayout(top_bar)
        layout.addWidget(self.browser, stretch=1)
        layout.addWidget(self.status_label)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        self.browser.loadFinished.connect(self.on_load_finished)
        self.browser.setHtml(HTML_TEMPLATE, QUrl("https://local.chart/"))

    def generate_chart_payload(self) -> dict[str, object]:
        categories = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
        values = [random.randint(40, 180) for _ in categories]
        return {
            "title": "最近 7 天学习时长统计",
            "categories": categories,
            "values": values,
        }

    def on_load_finished(self, ok: bool) -> None:
        if not ok:
            self.status_label.setText("页面加载失败,请检查 ECharts 资源是否可访问。")
            return
        self.update_chart()

    def update_chart(self) -> None:
        payload = self.generate_chart_payload()
        script = f"window.renderChart({json.dumps(payload, ensure_ascii=False)});"
        self.browser.page().runJavaScript(script)
        self.status_label.setText(
            f"图表已更新,当前运行环境:{'PyQt6' if USING_PYQT6 else 'PySide6'}"
        )


def main() -> None:
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()

    QTimer.singleShot(200, window.update_chart)
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

5. 这段代码到底做了什么

很多同学第一次看这种写法,会觉得“Python 和网页怎么连起来的?”

别急,我们一层一层拆开看。

5.1 QWebEngineView 就是嵌入窗口的网页容器

下面这句是核心:

self.browser = QWebEngineView()

它创建了一个可以显示网页内容的控件。

然后这句:

self.browser.setHtml(HTML_TEMPLATE, QUrl("https://local.chart/"))

表示把一段 HTML 字符串直接塞进这个网页控件里显示。

也就是说,你并不一定非得先准备一个单独的 .html 文件,完全可以在 Python 里直接拼出页面。

对初学者来说,这是最容易上手的方式。

5.2 HTML 中真正负责画图的是 ECharts

这句是加载 ECharts:

<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>

这句是图表容器:

<div id="chart"></div>

这句是初始化图表:

const chart = echarts.init(document.getElementById("chart"));

然后我们把渲染逻辑封装成了一个全局函数:

window.renderChart = function (payload) {
    chart.setOption(...);
};

这么做的好处是,Python 后面可以直接调用这个函数,把数据传进去。

5.3 Python 用 runJavaScript() 把数据送进页面

这一句是整套方案的关键连接点:

self.browser.page().runJavaScript(script)

这里的 script 本质上是一段要在网页里执行的 JavaScript。

例如:

payload = {
    "title": "最近 7 天学习时长统计",
    "categories": ["周一", "周二", "周三"],
    "values": [80, 120, 95],
}

script = f"window.renderChart({json.dumps(payload, ensure_ascii=False)});"

执行之后,就相当于网页里运行了:

window.renderChart({
  title: "最近 7 天学习时长统计",
  categories: ["周一", "周二", "周三"],
  values: [80, 120, 95]
});

这样图表就刷新出来了。

你可以把这个过程理解成:

Python 把字典转成 JSON,再让浏览器控件执行对应的 JavaScript 函数。

6. 实战里最常见的两种组织方式

学会最小示例之后,真正开发时通常会用下面两种方式。

6.1 方式一:setHtml() 内嵌页面

这就是本文示例采用的方式。

优点:

  • 上手快
  • 代码集中
  • 做原型非常方便

缺点:

  • HTML、CSS、JavaScript 都写在 Python 字符串里,项目变大后不太好维护

适合场景:

  • 学习
  • Demo
  • 小工具
  • 原型验证

6.2 方式二:加载本地 HTML 文件

项目稍微正式一点,更推荐把前端页面拆出去,比如:

project/
  main.py
  web/
    index.html
    echarts.min.js
    app.js
    style.css

然后在 Python 中这样加载:

from pathlib import Path
from PyQt6.QtCore import QUrl

html_path = Path(__file__).parent / "web" / "index.html"
self.browser.load(QUrl.fromLocalFile(str(html_path)))

这种方式更像一个小型前后端分层项目。

优点很明显:

  • HTML / CSS / JS 分离,维护更舒服
  • 可以把 echarts.min.js 放本地,避免依赖外网 CDN
  • 图表页面复杂时更容易扩展

如果你准备长期维护一个桌面项目,我更建议你往这个方向走。

7. 如何让 Python 和 ECharts 做更灵活的交互

初学阶段,掌握 runJavaScript() 已经够用了。

但你可能很快会遇到更进一步的需求,比如:

  • 点击柱子后把名称回传给 Python
  • 桌面按钮切换不同图表
  • 定时刷新监控数据
  • 列表选中哪一行,图表就高亮哪一项

这时候你可以用两种思路继续升级。

7.1 Python 主动推送数据

这是最简单也最常见的。

比如点击刷新按钮后,重新查询数据库,再次调用:

self.browser.page().runJavaScript(...)

本质上就是让 Python 继续做“主导方”。

7.2 用 QWebChannel 做双向通信

如果你想让 JavaScript 主动调用 Python,就可以引入 QWebChannel

思路大概是这样:

Python 端:

from PyQt6.QtCore import QObject, pyqtSlot
from PyQt6.QtWebChannel import QWebChannel


class Bridge(QObject):
    @pyqtSlot(str)
    def on_bar_clicked(self, name: str) -> None:
        print("用户点击了:", name)


bridge = Bridge()
channel = QWebChannel()
channel.registerObject("bridge", bridge)
self.browser.page().setWebChannel(channel)

JavaScript 端:

new QWebChannel(qt.webChannelTransport, function (channel) {
  window.bridge = channel.objects.bridge;
});

chart.on("click", function (params) {
  window.bridge.on_bar_clicked(params.name);
});

这时候 ECharts 图表的点击事件就能直接通知 Python。

如果你以后要做实时监控、交互式看板、带钻取逻辑的桌面报表,这一步会非常有用。

8. 开发中最容易踩的坑

这部分很重要,因为很多人第一次失败,不是思路不会,而是细节没处理好。

8.1 安装了 Qt,但没装 WebEngine

尤其是 PyQt6,很多人装完:

pip install PyQt6

然后发现:

from PyQt6.QtWebEngineWidgets import QWebEngineView

导入失败。

原因通常就是少装了:

pip install PyQt6-WebEngine

8.2 页面还没加载完,就急着执行 JavaScript

如果你在页面尚未准备完成时就调用:

runJavaScript(...)

很可能没有任何效果。

所以更稳妥的做法是监听:

self.browser.loadFinished.connect(self.on_load_finished)

等页面加载成功后再推送数据。

8.3 直接手写 JS 字符串,结果引号把自己绕晕了

很多初学者会这样拼:

script = "window.renderChart({'title': '测试'})"

这样很容易因为引号、转义、中文字符导致 JavaScript 执行失败。

更稳妥的方式永远是:

json.dumps(payload, ensure_ascii=False)

把 Python 数据先序列化成标准 JSON,再拼进脚本。

8.4 只在开发机能跑,换机器就不行

因为本文示例使用了 CDN:

https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js

这很适合演示,但如果你要发布给别人使用,更建议把 echarts.min.js 下载到本地项目目录,再从本地 HTML 引用。

这样即使目标机器离线,也能正常显示图表。

8.5 窗口大小变化后图表不自适应

这个问题在桌面端很常见。

所以页面里最好加上:

window.addEventListener("resize", function () {
  chart.resize();
});

否则窗口拖动后,图表可能显示不完整。

9. 这套方案适合哪些项目

如果你正在做下面这些类型的桌面程序,这个方案会非常顺手:

  • 数据分析工具
  • 教学演示软件
  • 设备状态监控客户端
  • ERP / MES 辅助报表
  • 金融、销售、库存统计面板
  • 带图形趋势展示的内部管理工具

尤其是当你的项目同时满足这两个条件时,非常值得考虑:

  1. Python 适合处理你的业务逻辑
  2. 传统桌面控件不够优雅地展示图表

这时候 PyQt6 / PySide6 + QWebEngineView + ECharts 基本就是一个很自然的答案。

10. 给初学者的建议:先跑通,再升级

如果你是第一次接触这个方向,不要一上来就做复杂架构。

更推荐这样学:

  1. 先跑通本文这个最小示例
  2. 把柱状图改成折线图、饼图、双轴图
  3. 把随机数据改成你自己的真实数据
  4. 再把内嵌 HTML 拆成独立文件
  5. 最后再研究 QWebChannel 做双向交互

你会发现,这条学习路线其实很顺:

  • 第一步理解 Qt 里怎么嵌网页
  • 第二步理解 Python 怎么传数据
  • 第三步理解桌面和前端如何协作

一旦这套思路打通,你以后做桌面报表、监控看板、混合式客户端,都会轻松很多。

11. 总结

很多人学 Python 桌面开发时,会默认把自己限制在传统 GUI 控件的范围里。

QWebEngineView 提供了一个非常实用的突破口:

你可以在桌面应用里直接借力 Web 可视化生态。

ECharts 恰好就是这个方向里非常值得优先掌握的工具之一。

记住这套组合的核心分工:

  • Python:处理数据和业务逻辑
  • Qt:提供桌面容器和交互界面
  • ECharts:负责图表渲染和视觉表达

如果你现在正在学习 Python,又希望做出更像“真实项目”的桌面程序,那么这套方案非常值得你亲手试一遍。

先把示例跑起来,再把数据换成自己的,成长会非常快。

如果你愿意,我后面还可以继续给你展开这一系列内容,比如:

  • Python 桌面应用中如何本地离线部署 ECharts
  • QWebChannel 双向通信完整实战
  • PyQt6 仪表盘、监控大屏、统计后台的图表封装思路
  • 如何把多个 ECharts 卡片组织成桌面数据看板

从“会写 Python”走到“能做像样的 Python 桌面项目”,这类跨栈能力非常关键。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐