源码会随 ScottPlot 大版本变化。
csharp
Refresh(bool lowQuality = false, bool skipIfCurrentlyRendering = false)
这基本就是 ScottPlot 4.x 的 WinForms ScottPlot.FormsPlot 那套实现。
ScottPlot 4.x:FormsPlot.Refresh() 源码大意
在 ScottPlot 4.x 的 WinForms 控件里,Refresh() 的核心逻辑非常短,做的事情大概就是:
Application.DoEvents()(处理一下当前 WinForms 消息队列,避免界面"憋住")- 标记这是"手动渲染"
- 调
Backend.Render(lowQuality, skipIfCurrentlyRendering)去真正渲染位图/图像
这一点在 ScottPlot 仓库里有直接被引用过(Issue 讨论里贴了对应文件行号和方法体)。 (GitHub)
你也能在同一处看到另一个相关点:WinForms 控件内部某些回调里也会
DoEvents(),然后让pictureBox.Invalidate()去触发重绘。 (GitHub)
这段源码意味着什么?
1)为什么 Refresh() 会"有时长,有时耗时短"?
因为 Refresh() 最终会走到 Backend.Render(...),它会做一次完整渲染(算轴、刻度、文字、网格、画线、生成位图/拷贝到控件)。而渲染耗时会受很多因素影响:
- 这一帧是否触发了轴/刻度/文字重新布局( 每次还调用
AxisAutoY(),更容易触发) - UI 线程当时是否正忙(消息队列积压、别的 BeginInvoke 回调)
skipIfCurrentlyRendering如果为 false,可能出现"上一帧没画完又来一帧"的压力- GC/系统调度偶发抖动
所以同一句 Refresh(),渲染路径"轻/重"不同,耗时就会抖 。
2)Application.DoEvents() 为什么敏感?
DoEvents() 会让 WinForms 在你的调用栈里插入一次"处理消息" ,这在高频调用时可能带来可重入/递归调用 风险(GitHub 上那个 issue 就是围绕这点讨论的)。 (GitHub)
查源码路径
最稳的方式是:
- 直接看 项目
packages.config/ NuGet 里 ScottPlot.WinForms 的版本号 - 或在 VS 里对
FormsPlot.Refresh()F12 转到定义(若有 Source Link/符号也可能直接跳到源码)
你用的 ScottPlot 4.1.71(WinForms) 里,ScottPlot.FormsPlot 的 Refresh()(准确说是它自己定义的 Refresh(bool lowQuality=false, bool skipIfCurrentlyRendering=false))源码本身非常短 ,核心逻辑就是:先泵一次消息队列(DoEvents),再让 Backend 立刻渲染一张新位图。
根据 ScottPlot 官方仓库里 WinForms 控件的源码片段,这个函数大致是这样: (GitHub)
csharp
public void Refresh(bool lowQuality = false, bool skipIfCurrentlyRendering = false)
{
Application.DoEvents();
Backend.WasManuallyRendered = true;
Backend.Render(lowQuality, skipIfCurrentlyRendering);
}
下面把它"拆开讲",并结合 前面遇到的 UI 抖动/偷跑 现象说明它为什么会"时轻时重"。
1)Application.DoEvents():为什么放在 Refresh 里?风险是什么?
DoEvents() 会立刻在当前 UI 线程 把消息队列里"能处理的消息"先处理一遍(鼠标、绘图无效化、控件重绘、BeginInvoke 投递过来的委托......都有可能)。(GitHub)
这带来两个效果:
- 表面好处 :如果你在一个很紧的循环里频繁
Refresh(),DoEvents 可能让 UI "看起来更跟手",避免消息长期积压。 - 关键副作用 :它会导致可重入(re-entrancy) : 以为自己还在执行 A 逻辑,但 DoEvents 期间 UI 线程可能"插队"执行了 B/C/D(包括鼠标事件、别的控件 BeginInvoke 的绘图 等)。
这也是 ScottPlot 社区里明确指出的一个坑:在某些高频事件(如 MouseMove)里调用Refresh(),DoEvents()可能导致递归/栈深不断增长等问题。(GitHub)
2)Backend.WasManuallyRendered = true:这是干嘛的?
这是个"标志位",告诉 Backend:这次渲染是你手动触发的 (不是 Resize、MouseMove 内部逻辑触发的那种)。(GitHub)
它通常用于 Backend 内部决定:
- 是否记录某些状态
- 是否跳过某些自动渲染路径
- 或者给后续的 invalidation / render 流程做判断
(这一块具体怎么用要看 Backend 内部实现,但可以肯定:它是 ScottPlot 控件内部渲染状态机的一部分。)
3)Backend.Render(lowQuality, skipIfCurrentlyRendering):真正的重活在这里
Refresh() 自己并不"画图",它只是触发 Backend 立刻渲染。(GitHub)
从官方 issue 的堆栈跟踪可以看到 WinForms 的渲染调用链大致是:
FormsPlot.Refresh() → ControlBackEnd.Render() → Plot.Render() → Plot.RenderPlottables() →(各种 plottable 的 Render)(GitHub)
这说明:
- 真正耗时 (你测到的 8ms/40ms/60ms 波动)主要发生在
Backend.Render()内部:分配/复用 bitmap、GDI+ 绘制、文字测量、抗锯齿、坐标轴刻度计算、以及最终触发控件重绘。 - 你前面每次在 UI 线程里
AxisAutoY()+Refresh(),AxisAutoY()可能导致更多刻度/布局计算,从而让Render更"重"。
4)两个参数到底什么意思?为什么能缓解"消息队列积压/偷跑"?
lowQuality
低质量渲染(常见做法是减少抗锯齿/简化绘制),用来提高交互帧率。ScottPlot 维护者也提到过这类"低质量拖拽"在 v4 里能明显提速。(GitHub)
skipIfCurrentlyRendering
重点是这个 :如果当前正在渲染,又来了一个新的 Refresh() 请求,就直接跳过 这次渲染,让 UI 线程有机会去处理消息队列(鼠标/键盘/你的 MouseUp 等)。
社区讨论明确提到:开启它能减少渲染占用,让消息循环更快"消化消息",因此更不容易堆积。(GitHub)
5)小结"
就 FormsPlot.Refresh() 这个函数本体来说:是的,它核心就上面三句。 (GitHub)
"为什么耗时波动、为什么会抢占 UI、为什么会堆积",答案主要在:
Backend.Render()的内部实现(GDI+ 绘制、资源分配、锁、是否跳帧)- 以及
Application.DoEvents()带来的可重入消息泵行为