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

Matplotlib 3.9.0+升级后wxPython嵌入画布blit方法报错排查

Matplotlib 3.9+版本wxPython嵌入画布blit报错问题排查

问题背景

将Matplotlib从3.7.2升级至3.10.5后,在wxPython嵌入的画布中调用blit方法时触发wx断言错误,该问题从Matplotlib 3.9.0版本开始出现。

错误栈信息

Traceback (most recent call last):
  File "C:\Users\vjo\AppData\Local\Programs\Python\Python311\Lib\site-packages\matplotlib\cbook.py", line 361, in process
    func(*args, **kwargs)
  File "c:\Users\vjo\LocalData\test.py", line 51, in follow_canvas_draw_method
    self.canvas.blit(self.canvas.figure.bbox)
  File "C:\Users\vjo\AppData\Local\Programs\Python\Python311\Lib\site-packages\matplotlib\backends\backend_wxagg.py", line 26, in blit
    destDC = wx.MemoryDC(self.bitmap)
             ^^^^^^^^^^^^^^^^^^^^^^^^
wx._core.wxAssertionError: C++ assertion ""dc"" failed at ..\..\src\msw\dcmemory.cpp(59) in wxMemoryDCImpl::wxMemoryDCImpl(): NULL dc in wxMemoryDC ctor

可复现简化代码

import wx
import matplotlib
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
from matplotlib.figure import Figure
import random

matplotlib.use('Agg')


class MatplotlibFrame(wx.Frame):
    def __init__(self, parent=None, title="wx + Matplotlib Example"):
        super().__init__(parent, title=title, size=(600, 400))
        self.panel = wx.Panel(self)
        # 创建matplotlib图形和画布
        figure = Figure()
        figure.add_subplot(111)
        self.canvas = FigureCanvasWxAgg(self.panel, -1, figure)
        # 创建按钮
        self.button = wx.Button(self.panel, label="添加随机点")
        self.button.Bind(wx.EVT_BUTTON, self.on_btn)
        # 布局
        self.vbox = wx.BoxSizer(wx.VERTICAL)
        self.vbox.Add(self.button, 0, flag=wx.ALIGN_CENTER | wx.BOTTOM, border=10)
        self.vbox.Add(self.canvas, 1, flag=wx.EXPAND | wx.ALL, border=5)
        self.panel.SetSizer(self.vbox)
        self.Centre()
        self.Show()

    def on_btn(self, event):
        # 步骤1:销毁旧画布
        if hasattr(self, 'canvas'):
            self.vbox.Detach(self.canvas)
            if self.canvas.HasCapture():
                self.canvas.ReleaseMouse()
            self.canvas.Destroy()
        # 步骤2:创建新图形
        figure = Figure()
        ax = figure.add_subplot(111)
        x_data = [random.randint(0, 10) for _ in range(10)]
        y_data = [random.randint(0, 10) for _ in range(10)]
        self.artists = ax.plot(x_data, y_data, marker='o', animated=True)
        # 步骤3:创建新画布并绑定事件
        self.canvas = FigureCanvasWxAgg(self.panel, -1, figure)
        self.canvas.mpl_connect("draw_event", self.follow_canvas_draw_method)
        self.vbox.Add(self.canvas, 1, flag=wx.EXPAND | wx.ALL, border=5)
    
    def follow_canvas_draw_method(self, event):
        for art in self.artists:
            self.canvas.figure.draw_artist(art)
        self.canvas.blit(self.canvas.figure.bbox)
    

if __name__ == "__main__":
    app = wx.App(False)
    frame = MatplotlibFrame()
    app.MainLoop()

疑问

  1. 我的代码是否存在错误?
  2. 为什么Matplotlib 3.9.0版本中调用blit方法时self.bitmap会保持为None?

临时修复方案

在follow_canvas_draw_method开头添加以下代码:

if self.canvas.bitmap is None:
    setattr(self.canvas, 'bitmap', self.canvas._create_bitmap())

问题解答

1. 代码存在的问题

你的代码流程有疏漏:每次创建新画布后,仅将其添加到sizer,但没有触发wx的布局刷新流程。FigureCanvasWxAgg的内部bitmap需要等待wx触发首次绘制(比如Paint事件)才能完成初始化,而原代码中draw_event触发时,画布还没经过wx的完整初始化流程,导致bitmap为None。

另外,销毁旧画布的逻辑虽然没问题,但新画布创建后缺少主动触发布局和刷新的步骤,这是导致提前调用blit时出错的直接原因。

2. Matplotlib 3.9.0版本的变化原因

Matplotlib 3.9.0对wx后端的初始化逻辑做了调整:在此之前,bitmap可能在画布对象创建时就会被初始化;但3.9.0之后,为了优化性能,将bitmap的创建延迟到画布首次实际绘制时(由wx的Paint事件触发)。原代码中draw_event的触发早于wx的首次绘制,此时bitmap还未被创建,因此调用blit时会报错。

临时修复的正确性及优化建议

你的临时修复是有效的,_create_bitmap()是wx后端内部用于生成对应尺寸bitmap的方法,手动调用可以提前完成初始化,避免空指针错误。不过更规范的做法是在添加新画布后触发wx的布局和刷新,让画布完成正常初始化:

在self.vbox.Add(self.canvas, 1, flag=wx.EXPAND | wx.ALL, border=5)之后添加:

self.panel.Layout()
self.canvas.Refresh()

这样wx会主动触发画布的绘制流程,自动完成bitmap的初始化,不需要依赖内部方法调用,代码兼容性更好。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 10:54:49