PyQt6-第15讲-数据可视化QChart

第 15 讲 | 数据可视化:QChart 三种图

本讲目标 :用 Qt 自带的 QtCharts 画出柱状图、饼图、折线图,理解 QChart 的结构和那个必踩的

attachAxis 坑。

配套代码 :cases/c15_charts.py完整下载地址请看底部


一、为什么用 QtCharts 而不是 matplotlib

在 PyQt 里画图有两条路:

QtCharts(本讲) matplotlib 嵌入
安装 pip install PyQt6-Charts pip install matplotlib
渲染引擎 和界面同一个(Qt) 另一套(自己画到 QPixmap 上)
缩放的清晰度 矢量,放大不糊 位图,放大模糊
动画 内置,一行开启 需要自己写
鼠标交互 内置(悬停高亮、缩放平移) 需要自己接事件
图表种类 常用类型齐全 极其丰富(3D、极坐标、误差棒...)
体积 小 大(拖进来几十 MB)

结论:

  • 界面里嵌图表 (仪表盘、监控面板、报表工具)→ QtCharts
  • 做数据分析图(统计分布、复杂科学图表)→ matplotlib,甚至直接生成图片存文件

还有一个实用理由 :QtCharts 的图表是 Qt 对象,能享受 Qt 的全部能力------

你可以把图表放进布局里、能响应鼠标事件、能保存 QPixmap。

而 matplotlib 只是"在一张画布上画",和界面的融合度差一层。


二、QChart 和 QChartView

先记住这两个类的关系:

类 角色 类比
QChart 画布:装数据系列、坐标轴、标题、图例 一台显示器的"画面内容"
QChartView 显示器:把 QChart 显示出来 显示器本身
python 复制代码
from PyQt6.QtCharts import QChart, QChartView

chart = QChart()
chart.setTitle("我的第一张图")
# ... 往 chart 里加数据 ...

view = QChartView(chart)
view.setRenderHint(QPainter.RenderHint.Antialiasing)   # ★ 开抗锯齿

setRenderHint(Antialiasing) 一定要加 。不加的话斜线是锯齿状的,

尤其是折线图和饼图的斜边,一眼就能看出"很土"。


三、柱状图:最常用的图

柱状图由三个对象组成:数据集合(QBarSet)→ 系列(QBarSeries)→ 画布(QChart)

python 复制代码
from PyQt6.QtCharts import QBarCategoryAxis, QBarSeries, QBarSet, QValueAxis

def build_bar_chart() -> QChart:
    rows = load_products()                     # [(名称, 分类, 单价, 库存), ...]

    bar_set = QBarSet("单价(元)")             # ① 一个 BarSet = 一组数据
    categories = []
    for name, _cat, price, _stock in rows:
        bar_set.append([price])                # 注意:参数是列表,不是单个数字
        categories.append(name)

    series = QBarSeries()                      # ② 系列
    series.append(bar_set)

    chart = QChart()                           # ③ 画布
    chart.addSeries(series)
    chart.setTitle("产品单价对比")
    chart.setAnimationOptions(QChart.AnimationOption.SeriesAnimations)

    # 横轴:文字类目轴
    axis_x = QBarCategoryAxis()
    axis_x.append(categories)
    chart.addAxis(axis_x, Qt.AlignmentFlag.AlignBottom)
    series.attachAxis(axis_x)                  # ★ 必须挂上,否则轴不显示

    # 纵轴:数值轴
    axis_y = QValueAxis()
    axis_y.setRange(0, max(r[2] for r in rows) * 1.2)
    axis_y.setTitleText("单价(元)")
    axis_y.setLabelFormat("%.0f")
    chart.addAxis(axis_y, Qt.AlignmentFlag.AlignLeft)
    series.attachAxis(axis_y)                  # ★

    return chart

四个必须记住的点

① bar_set.append([price]) 参数是列表

不是 append(price)。因为它支持多组数据系列 ,

append([12, 15, 18]) 表示这一组要画三根柱子(三组系列对比)。

写错会报参数类型错误。

② 坐标轴必须 attachAxis()

这是新手第一大坑 。chart.addAxis() 只是把轴加到画布上,

系列和轴还是两条平行线 。必须 series.attachAxis(axis) 建立关联,

数据才会按这个轴的范围映射到屏幕上。

忘了 attachAxis 的症状 :坐标轴显示出来了(有刻度和标签),

但图里什么都没有。遇到"图是空的但轴在"的情况,先检查这一行。

③ 两条轴都要挂

横轴和纵轴都要 attachAxis。只挂一条,图会画得乱七八糟。

④ 纵轴范围要留边距

python 复制代码
axis_y.setRange(0, max(...) * 1.2)

不乘 1.2 的话,最高的那根柱子会顶到画面顶部,看起来很挤(甚至被裁掉)。

留 20% 余量是通用做法。


四、饼图:注意不要传表头

python 复制代码
from PyQt6.QtCharts import QPieSeries

def build_pie_chart() -> QChart:
    series = QPieSeries()
    data = load_category_share()               # [(分类, 金额), ...]
    total = sum(v for _k, v in data)

    for name, value in data:
        sl = series.append(name, value)
        sl.setLabel(f"{name} {value / total * 100:.1f}%")    # 标签带百分比
        # 占比最大的那块"炸开",视觉重点更清楚
        if value == max(v for _k, v in data):
            sl.setExploded(True)
            sl.setLabelVisible(True)

    chart = QChart()
    chart.addSeries(series)
    chart.setTitle("各类产品库存金额占比")
    chart.setAnimationOptions(QChart.AnimationOption.SeriesAnimations)
    chart.legend().setAlignment(Qt.AlignmentFlag.AlignRight)
    return chart

QPieSeries.append(标签, 数值) 直接两个参数 ,比柱状图简单,不需要坐标轴。

两个实用技巧:

① setExploded(True) 让某一块"炸开"

占比最大的那块单独分离出来,视觉上立刻有了重点。这是饼图最容易被忽略但效果最好的设置。

② 标签带百分比

setLabel(f"{name} {ratio:.1f}%") ------ 光看图看不出精确占比,加上数字才实用。

③ 饼图不要传表头数据

如果你把"分类名称"那一列也当数据传进去,它会变成一个巨大的扇形。

饼图的数据范围只包含数值列。


五、折线图:x 轴用序号,再用文字轴标注

折线图的 x 轴是连续的数值 ,但我们的横轴往往是"1月、2月、3月"这样的文字。

标准的处理方式是:x 用 0,1,2,... 序号,再用 QBarCategoryAxis 标注文字。

python 复制代码
def build_line_chart() -> QChart:
    months, values = load_monthly_sales()

    series = QLineSeries()
    series.setName("销售额(万元)")
    for i, v in enumerate(values):
        series.append(float(i), v)             # x 用序号

    chart = QChart()
    chart.addSeries(series)
    chart.setTitle("2026 年销售额趋势")
    chart.setAnimationOptions(QChart.AnimationOption.SeriesAnimations)

    axis_x = QBarCategoryAxis()
    axis_x.append(months)                      # ★ 用类目轴标注月份文字
    chart.addAxis(axis_x, Qt.AlignmentFlag.AlignBottom)
    series.attachAxis(axis_x)

    axis_y = QValueAxis()
    axis_y.setRange(0, max(values) * 1.2)
    axis_y.setTitleText("销售额(万元)")
    chart.addAxis(axis_y, Qt.AlignmentFlag.AlignLeft)
    series.attachAxis(axis_y)
    return chart

注意 series.append(float(i), v) 的 x 是浮点数 ,

QLineSeries 的 append 签名是 (x: float, y: float),传 int 也能跑但建议统一用 float。

折线图的样式设置(可选):

python 复制代码
pen = series.pen()
pen.setWidth(3)
pen.setColor(QColor("#2d7ff9"))
series.setPen(pen)

series.setPointsVisible(True)                  # 显示数据点

六、主题、动画、导出图片

主题

python 复制代码
chart.setTheme(QChart.ChartTheme.ChartThemeLight)      # 浅色
chart.setTheme(QChart.ChartTheme.ChartThemeDark)       # 深色
chart.setTheme(QChart.ChartTheme.ChartThemeBlueCerulean)   # 蓝色系

主题必须在加数据之前设置 ,否则部分样式不生效。

本案例的"切换主题"按钮就是重新构建图表(保证主题生效):

python 复制代码
def show_chart(self, name: str) -> None:
    builder = BUILDERS.get(name)
    chart = builder()
    chart.setTheme(QChart.ChartTheme.ChartThemeDark if self.dark
                   else QChart.ChartTheme.ChartThemeLight)
    self.view.setChart(chart)

注意要重新 setChart() :改变已有 chart 的主题会有残留样式,

重新构建最干净。

动画

python 复制代码
chart.setAnimationOptions(QChart.AnimationOption.SeriesAnimations)   # 系列动画
chart.setAnimationOptions(QChart.AnimationOption.AllAnimations)      # 所有动画
chart.setAnimationDuration(800)                                       # 时长(毫秒)

动画不只是好看 :数据刷新时,柱子从旧高度长到新高度,

用户能直观感受到"数值变了"。在实时监控面板里这个效果很有价值(下一讲)。

导出图片

python 复制代码
self.view.grab().save("图表导出.png")

QWidget.grab() 抓的是"控件当前显示的样子" ,分辨率跟控件大小一致。

想要高分辨率,可以在抓之前临时放大控件,或者用 QChart 的 QPixmap 导出能力。


七、一个完整案例的界面

复制代码
┌──────────────┬────────────────────────────────┐
│ 图表类型      │                                │
│ ▸ 柱状图      │                                │
│   饼图        │         QChartView             │
│   折线图      │       (显示选中的图)           │
│              │                                │
│ [导出为图片]   │                                │
│ [切换主题]     │                                │
└──────────────┴────────────────────────────────┘

左侧列表切换图表,右侧一个 QChartView 复用:

python 复制代码
self.list_types.currentTextChanged.connect(self.show_chart)

def show_chart(self, name: str) -> None:
    chart = BUILDERS[name]()               # ★ 每次重新构建
    chart.setTheme(...)
    self.view.setChart(chart)

用字典管理构建函数是个好模式:

python 复制代码
BUILDERS = {
    CHART_TYPES[0]: build_bar_chart,
    CHART_TYPES[1]: build_pie_chart,
    CHART_TYPES[2]: build_line_chart,
}

要加新图表类型,只需写一个函数 + 加一行字典,不用改界面逻辑。

这是"可扩展"的具体写法,比一串 if-elif 好得多。

八、四个坑

坑 1:图是空的,但坐标轴显示出来了

忘记 series.attachAxis(axis_x) 和 attachAxis(axis_y)。

坑 2:QBarSet.append 传了单个数字

参数必须是列表形式 append([12]),因为它支持多系列对比。

坑 3:饼图把标签列当数据传了

饼图只传数值列。

坑 4:忘了装 PyQt6-Charts

ModuleNotFoundError: No module named 'PyQt6.QtCharts'。

QtCharts 不在 PyQt6 主包里 ,必须单独 pip install PyQt6-Charts。


九、小结

  • QChart 是画布,QChartView 是显示器 ,setChart() 把两者连起来
  • 柱状图三步 :QBarSet → QBarSeries → QChart,两条轴都要 attachAxis
  • 饼图最简单 :series.append(标签, 值) + setExploded 突出重点
  • 折线图 x 用序号,文字标注交给 QBarCategoryAxis
  • 主题要在加数据前设;切换主题最干净的做法是重建图表

现在你能画出静态的图表了。但真实场景里,数据是一直在变 的------

设备温度每秒上报一次、股价一直在跳、日志一直在滚动。

下一讲 :实时数据监控面板。我们会用 QTimer 驱动界面刷新,

画出"一直在滚动的曲线",并讲清两个性能关键点:

为什么 series.replace() 比逐点 append 快得多 ,以及

QTimer 的间隔该怎么定。


本讲是《PyQt6 客户端编程从入门到实战》专栏的第 15 讲。专栏共 22 讲、22 个可运行案例。

**完整下载地址:git clone https://gitee.com/wuyikui555/pyqt6-client.git**。

相关推荐
天天被压力23 分钟前
【A股分钟级高频数据完全手册 #04】指数分钟级行情数据接口:沪深指数Pro版最新分时取数
java·人工智能·python
日日大王学编程29 分钟前
2026主流AI Agent产品分类、核心差异与技术特性全解析
人工智能·后端·python
天天被压力33 分钟前
【A股分钟级高频数据完全手册 #03】股票历史1分钟K线数据接口怎么用:Pro版批量拉取与本地缓存
java·人工智能·python
武天旭16 小时前
AI安全--AI大模型的毒性偏见与安全对齐实践
人工智能·python·深度学习
视觉小萌新17 小时前
AI Class 3 · Chapter 2-3:函数、异常、文件读写+JSON / JSONL 读写(小白版)
人工智能·python·json
小小张说故事18 小时前
Python 的 obj.x 到底按什么顺序找?拆开数据描述符、实例字典和 __getattr__ 那条优先级链
python
用户114818678948419 小时前
水印的实现方式逻辑
前端·python
贾公子19 小时前
从单条内容到账号维度——AIGC 风险评分的工程实现
后端·python
木头左21 小时前
【Python】不点浏览器发博客:逆向 CSDN 创作中心接口做 md-publish 技能
python·自动化·api