Matplotlib 3.9.0+升级后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()
疑问
- 我的代码是否存在错误?
- 为什么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

