现象
实时曲线面板,只要鼠标滚轮缩放就崩溃:
System.Collections.Generic.KeyNotFoundException Message = The given key 'ScottPlot.AxisPanels.DateTimeXAxis Min=∞, Max=-∞, Span=-∞' was not present in the dictionary. StackTrace: at ScottPlot.Plot.GetPanel(Pixel pixel, Boolean axesOnly) at ScottPlot.Plot.GetAxis(Pixel pixel) at ScottPlot.Interactivity.MouseAxisManipulation.MouseWheelZoom(...) at ScottPlot.Interactivity.UserActionResponses.MouseWheelZoom.Execute(...) at ScottPlot.WPF.WpfPlotBase.SKElement_MouseWheel(...)诡异的地方在于:收到数据之后滚轮正常,程序刚启动、图表还空着的时候滚轮才崩。
排查
异常抛在 ScottPlot 内部,栈很长,只能顺着栈读:
MouseWheelZoom→GetAxis→GetPanel—— 滚轮缩放要「找到鼠标下的那个坐标轴面板」;GetPanel抛的是KeyNotFoundException,而且 key 是DateTimeXAxis对象——说明它在做字典查找;- 我的代码里正好有
DateTimeTicksBottom(),它就是把默认底部轴替换成DateTimeXAxis。
方向有了,但还不够。我决定写最小重现代码来锁定因果——不去猜,直接复现。
最小重现:十几行代码复现同一个异常
using ScottPlot; class P { static void Main() { // 1) 建 plot,先渲染一次(模拟 WpfPlot 首次布局时的自动渲染) var plot = new Plot(); plot.GetImage(600, 400); Console.WriteLine("after first render: ok"); // 2) 替换底部轴为 DateTime 轴(等价于 VM 里 DateTimeTicksBottom()) var axis = plot.Axes.DateTimeTicksBottom(); Console.WriteLine("DateTimeTicksBottom done, axis=" + axis); // 3) 在「替换后、刷新前」做一次 GetPanel(就是滚轮缩放的路径) try { var panel = plot.GetPanel(new Pixel(300, 380), true); Console.WriteLine("GetPanel BEFORE refresh -> OK: " + panel); } catch (Exception ex) { Console.WriteLine("GetPanel BEFORE refresh -> " + ex.GetType().Name + ": " + ex.Message); } // 4) 刷新一次后再试 plot.GetImage(600, 400); var panel2 = plot.GetPanel(new Pixel(300, 380), true); Console.WriteLine("GetPanel AFTER refresh -> OK: " + panel2); } }实测输出(完全复现):
after first render: ok DateTimeTicksBottom done, axis=ScottPlot.AxisPanels.DateTimeXAxis Min=∞, Max=-∞, Span=-∞ GetPanel BEFORE refresh -> KeyNotFoundException: The given key 'ScottPlot.AxisPanels.DateTimeXAxis Min=∞, Max=-∞, Span=-∞' was not present in the dictionary. GetPanel AFTER refresh -> OK: ScottPlot.AxisPanels.DateTimeXAxis Min=-10, Max=10, Span=20注意那个 key 的字符串Min=∞, Max=-∞, Span=-∞——这是还没渲染过的轴的特征(范围是空的)。对照项目里的报错,一模一样。
根因:替换了坐标轴,却没有重新渲染
把因果链拼起来:
Axes.DateTimeTicksBottom()会删掉默认底部轴、新建一个DateTimeXAxis加进去,但它不会立即渲染;- 而
Plot.GetPanel(pixel)是**基于「上一次渲染的布局」**来定位面板的——内部按「面板对象」做字典查找(Plot.LastRender); - 程序刚启动、还没收到数据时,
plot.Refresh()一次都没发生过; - 此时滚轮缩放 →
GetPanel手里还是旧布局,字典里只有旧轴,找不到这个新DateTimeXAxis对象→ 抛KeyNotFoundException。
我的项目里,plot.Refresh()只在「有数据刷新时」和「重建 Streamer 时」被调用——没有数据时永远停在未渲染状态,所以只有空图时崩。
修复:初始化后立即渲染一次
public void InitializePlot(ScottPlot.WPF.WpfPlot plotControl) { if (plot != null) return; // ★ 防止 Loaded 多次触发重复初始化(重复替换坐标轴、重复添加 Streamer) plot = plotControl; plot.Plot.Axes.Bottom.Label.Text = "时间"; plot.Plot.Axes.Left.Label.Text = "值"; // 1. 把底部轴设为日期模式,拿到轴对象 var axis = plot.Plot.Axes.DateTimeTicksBottom(); // 2. 定制日期标签格式:只显示 时:分:秒 ((ScottPlot.TickGenerators.DateTimeAutomatic)axis.TickGenerator).LabelFormatter = dt => dt.ToString("HH:mm:ss"); // 3. 显示曲线 plot.Plot.ShowLegend(); // 4. ★ 立即渲染一次,把新轴纳入布局。 // DateTimeTicksBottom() 用新建的 DateTimeXAxis 替换了默认底部轴, // 但 ScottPlot 的 LastRender 布局(内部按面板对象做字典查找)里还没有这个新轴; // 若在收到第一帧数据前就滚轮缩放,GetPanel 会因字典里找不到该轴而抛 KeyNotFoundException。 plot.Refresh(); }两处改动:
plot.Refresh()—— 关键。初始化完立刻渲染一次,新轴进布局,滚轮不再「找不到键」;if (plot != null) return;—— 因为 ChartView 用的是Loaded事件(切 Tab 会重复触发),不加守卫会重复替换坐标轴、重复添加 Streamer。释的」因果链。