尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ZedGraphControl截图实现:C# WinForm图表高清导出完整方案

ZedGraphControl截图实现:C# WinForm图表高清导出完整方案 1. 需求场景为什么“截图”是ZedGraphControl绕不开的功能做上位机开发的朋友应该都遇到过这个需求界面上画了一条实时曲线客户看完之后来一句“把这图给我导出来”。ZedGraphControl作为WinForm下最老牌的免费绘图控件绘图能力没得说但截图这个功能反而成了高频需求里最容易被忽视的一环。我早期做数据采集系统时每次到验收阶段都要临时补截图导出功能后来干脆整理出一套标准实现方案这次直接公开分享给大家。先说说这个需求的典型场景。工业上位机里ZedGraphControl一般用来显示温度曲线、压力曲线、流量趋势、设备运行状态等实时数据客户要把这些曲线放到报告里、发到群里、或者作为设备故障时的现场凭证。这时候“截图”就不只是截个屏幕那么简单而是要把当前图表按照指定尺寸、指定分辨率、指定文件格式完整地保存下来。还有一类场景是把历史数据回放时截成对比图多张图放一起看趋势变化。适合看这篇文章的朋友包括正在用ZedGraph做数据可视化的C#上位机开发者、实验室仪器软件开发者、工控HMI开发人员以及刚接触ZedGraphControl的新手。我会从最简单的方案讲到带配置界面的完整方案顺带把截图相关的常见坑全部踩一遍。这套方案的核心价值在于不依赖外部截图工具比如Snipaste、微信截图直接在程序内部生成图片这样导出的图表一定是高清、完整、带坐标轴和标题的比屏幕截图专业得多。而且代码量不大加上注释也就百来行完全可以“抄作业”式地搬进自己的项目。2. 截图前的准备ZedGraphControl控件初始化与图像边界确认2.1 控件的核心对象关系ZedGraphControl的截图为什么容易被搞晕根源在于很多人没分清控件的几个核心对象。这里我用最朴素的方式理一遍。一个完整的ZedGraph图表由这么几层组成最外层是Control本身也就是你在窗体上拖的那个控件内部有一个MasterPane主面板MasterPane下面可以挂多个GraphPane图形面板。绝大多数情况下我们只用了一个GraphPane所以看起来MasterPane和GraphPane像是同一个东西但实际上它们有严格的父子关系。截图时必须搞清楚你截的是哪一层。如果只截GraphPane那么MasterPane的标题、图例位置、背景色就会丢失如果截整个Control又会把控件周边的空白区域也截进去。实操中我的建议是截图优先用MasterPane的GetImage方法因为它能保证图表标题、图例、坐标轴都完整包含在内而且能自动根据pane的矩形边界来计算尺寸。另一个容易忽略的地方是“绘图区域的实际尺寸”和“控件显示尺寸”可能不一致。尤其是当窗体有DPI缩放、控件被嵌套在TableLayoutPanel里时控件显示尺寸会被拉伸但内部绘图缓冲区还是按原始像素来的。截图时如果直接用Control.Width和Control.Height很容易拿到一张尺寸不对的图。2.2 初始化时埋好截图的“基石”要想截图效果好控件初始化时就该把一些属性定好。我在项目里通常会在窗体Load事件或者ZedGraphControl的ZedGraphControl_Load事件里做这几件事private void InitZedGraph(ZedGraphControl zgc) { // 设置反锯齿保证截出来的曲线边缘平滑 zgc.IsAntiAlias true; zgc.IsShowHScrollBar false; zgc.IsShowVScrollBar false; zgc.IsShowPointValues true; GraphPane myPane zgc.GraphPane; // 设置图表标题与坐标轴标题 myPane.Title.Text 温度实时监控曲线; myPane.Title.FontSpec.Size 16f; myPane.Title.FontSpec.FontColor Color.FromArgb(50, 50, 50); myPane.XAxis.Title.Text 时间; myPane.YAxis.Title.Text 温度℃; // 设置坐标轴字体避免截图出来字体太小 myPane.XAxis.Title.FontSpec.Size 12f; myPane.YAxis.Title.FontSpec.Size 12f; myPane.XAxis.Scale.FontSpec.Size 10f; myPane.YAxis.Scale.FontSpec.Size 10f; // 设置边框和背景 myPane.Border.Color Color.FromArgb(180, 180, 180); myPane.Fill.Color Color.White; // 设置图例字体大小 myPane.Legend.FontSpec.Size 10f; myPane.Legend.Position LegendPos.TopCenter; // 让控件自适应大小变化 zgc.IsSynchronizeXAxes false; zgc.IsAutoScrollRange true; zgc.AxisChange(); }这段代码的核心逻辑是把所有影响截图观感的视觉元素提前配置好。很多人截图出来发现字体特别小就是因为Scale.FontSpec和Title.FontSpec没单独设置默认字体在保存为图片时会被压缩。还有一个细节ZedGraphControl.IsShowPointValues这个属性会影响到鼠标悬停时显示数值和截图没有直接关系但开起来能方便调试所以我习惯顺手打开。值得强调的是AxisChange()方法很关键。它会让ZedGraph重新计算所有坐标轴的刻度范围和标签布局如果你改完数据后忘了调用这个方法截图会得到一张坐标轴范围不对、标签被截断的图。建议每次添加数据后都调一次AxisChange()这是ZedGraph开发中最重要的一个习惯。2.3 为什么优先用MasterPane.GetImage而不是直接截屏对比几种截图方案之后我最终锁定了MasterPane.GetImage方案。这里把几个方案的优劣势列出来方便大家按场景选择。我见过有人直接用Control.DrawToBitmap()来截图这个方法的优点是简单缺点也很明显它截的是控件在屏幕上的渲染结果如果控件被其他窗口遮挡过、或者发生过部分重绘截出来的图片可能有残影而且DrawToBitmap在控件没有完全显示时会抛出异常。还有人在用Graphics.CopyFromScreen()做屏幕区域截取这个方法更糟糕它依赖屏幕坐标一旦窗体被移动、最小化或者缩放截图内容就全乱了。截图方式优点缺点推荐场景MasterPane.GetImage不依赖屏幕显示任意清晰度可控制尺寸需要写一点代码正式导出、报告用图Control.DrawToBitmap代码少受屏幕显示状态影响可能失真快速预览、临时保存Graphics.CopyFromScreen完全不需要控件API依赖窗口位置最小化就失效不推荐使用从工程化角度来说MasterPane.GetImage是唯一能保证“所见即所得”且不受窗口状态影响的方法。它直接调用ZedGraph内部的绘图逻辑把整个面板渲染成一张位图无论窗体是否被遮挡、是否最小化只要控件在内存中的对象还活着就能生成图片。这一点对于需要在后台线程里批量导出图表的需求来说尤其重要。3. 核心实现三种截图方案与参数细节3.1 方案一最简代码保存PNG文件如果只是要把当前图表保存成一张PNG代码简洁到令人发指using System.Drawing; using System.Drawing.Imaging; using ZedGraph; public void SaveChartToPng(ZedGraphControl zgc, string filePath) { using (Bitmap bmp zgc.GraphPane.GetImage()) { bmp.Save(filePath, ImageFormat.Png); } }就这么几行图表就保存下来了。GraphPane.GetImage()会返回一个Bitmap对象里面包含了当前的曲线、坐标轴、标题、图例等所有绘制内容。默认的分辨率和控件显示分辨率一致但对于大多数报告用途已经够用。注意这里的using不是语法糖随便写的。Bitmap对象内部持有GDI资源的句柄如果不调用Dispose文件会被占用、内存会缓慢泄漏长时间跑上位机软件就会出问题。我见过有同事在循环导出几百张图片时不释放Bitmap最后整个程序变得异常卡顿就是这个原因。如果想把默认的文件格式改成JPG只需要把ImageFormat.Png换成ImageFormat.Jpeg。但JPG有损压缩曲线图里如果有密集的数据点压缩后会出现明显的锯齿和色块所以做科学计算和工业数据图表时我强烈建议用PNG格式。3.2 方案二支持设置图片尺寸和缩放倍率很多时候客户会要求“图片放大两倍我要打印出来”或者“给我一张1920宽的大图我要贴到大屏上看”。遇到这种情况可以用MasterPane的GetImage重载方法public Bitmap CreateChartImage(ZedGraphControl zgc, int width, int height, float scale) { MasterPane master zgc.MasterPane; // 临时保存原始尺寸最后要还原 double origWidth master.Rect.Width; double origHeight master.Rect.Height; // 重新设置绘制区域的大小 master.Rect new RectangleF(0, 0, width, height); // scale1.0 时就是实际像素2.0 时就是2倍超采样 using (Graphics g master.CreateGraphics()) { master.SetLayout(g, PaneLayout.SingleChart); master.Draw(g); } Bitmap bmp new Bitmap(width, height); using (Graphics g Graphics.FromImage(bmp)) { g.Clear(Color.White); master.Draw(g); } // 还原MasterPane的原始尺寸 master.Rect new RectangleF(0, 0, (float)origWidth, (float)origHeight); return bmp; }这段代码的坑在于MasterPane.Rect是绘图矩形的真实尺寸直接改它会影响控件的布局。所以必须记得在处理完之后把原来的值还回去否则控件会“变形”。有人会问为什么不直接改zgc.Size因为控件的尺寸由WinForm布局系统管着你改了可能立刻又被布局引擎拉回去而且会影响界面显示。改MasterPane.Rect是更底层的做法临时修改、导出、还原不影响UI。关于scale这个参数如果你真的需要一张超大尺寸的图比如3000像素宽建议处理完高度超3000像素。但要注意一点超过一定尺寸后GDI绘制的性能会明显下降。我曾经导出一张10000像素宽的曲线图耗时接近10秒内存占用飙到几百MB这种需求不太建议在普通上位机上直接做。通常控制在2000-4000像素最稳妥。3.3 方案三带文件选择对话框的完整保存流程真正在实际项目中用的最多的还是“点击按钮弹出SaveFileDialog选好路径保存”这个流程。我把它封装成一个完整的方法private void btnSaveChart_Click(object sender, EventArgs e) { using (SaveFileDialog dlg new SaveFileDialog()) { dlg.Filter PNG图片|*.png|JPG图片|*.jpg|BMP图片|*.bmp; dlg.FileName $Chart_{DateTime.Now:yyyyMMdd_HHmmss}; dlg.RestoreDirectory true; if (dlg.ShowDialog() DialogResult.OK) { try { bool isSaved SaveChartFromControl(zedGraphControl1, dlg.FileName); if (isSaved) { MessageBox.Show(图表保存成功\n dlg.FileName, 提示, MessageBoxButtons.OK, MessageBoxIcon.Information); } else { MessageBox.Show(图表保存失败, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } } catch (Exception ex) { MessageBox.Show(保存图表时发生异常\n ex.Message, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } } } } public bool SaveChartFromControl(ZedGraphControl zgc, string filePath) { try { // 根据扩展名选择图片格式 ImageFormat format ImageFormat.Png; string ext Path.GetExtension(filePath).ToLower(); switch (ext) { case .jpg: case .jpeg: format ImageFormat.Jpeg; break; case .bmp: format ImageFormat.Bmp; break; case .png: default: format ImageFormat.Png; break; } using (Bitmap bmp zgc.GraphPane.GetImage()) { bmp.Save(filePath, format); } return true; } catch { return false; } }这里有个小细节值得单独说文件名里加时间戳yyyyMMdd_HHmmss是我从第一版项目就开始有的习惯。工业现场经常出现同一个班组在同一个时间段内多次导出曲线图的情况如果没有时间戳文件名重复后保存的就会覆盖先保存的导致前面导出的数据丢失。虽然是个小细节但真出问题时客户是不管你代码怎么写的他只知道图没了。还有dlg.RestoreDirectory true这个属性。它保证用户每次打开对话框时都定位到上次的目录不会每次都在默认的文档文件夹。对于每天要导出几十张图的场景能省下不少时间。4. 截图效果优化字号、颜色与高清输出的关键参数4.1 为什么截出来的图字特别小这是被问得最多的一个问题“我在界面上看到的字挺正常的怎么截出来就成蚂蚁字了”原因在于ZedGraph控件的字体渲染和图像渲染是两套逻辑。在屏幕上控件是按显示DPI渲染的比如你的显示器是96DPI控件实际宽度可能是800像素截图时如果我们把图片分辨率设置成1600像素宽但FontSpec的Size没跟着放大那字体就是按1600像素宽的画面比例来显示的自然显得比屏幕上的小。解决办法有两个方向手动计算缩放倍数或者使用ZedGraph自带的Scale缩放。我的方案是在保存前遍历所有文本元素按缩放比例调整字号public Bitmap GetScaledChartImageWithFont(ZedGraphControl zgc, float scale) { double oldTitleSize zgc.GraphPane.Title.FontSpec.Size; double oldXTitleSize zgc.GraphPane.XAxis.Title.FontSpec.Size; double oldYTitleSize zgc.GraphPane.YAxis.Title.FontSpec.Size; double oldXScaleSize zgc.GraphPane.XAxis.Scale.FontSpec.Size; double oldYScaleSize zgc.GraphPane.YAxis.Scale.FontSpec.Size; double oldLegendSize zgc.GraphPane.Legend.FontSpec.Size; zgc.GraphPane.Title.FontSpec.Size oldTitleSize * scale; zgc.GraphPane.XAxis.Title.FontSpec.Size oldXTitleSize * scale; zgc.GraphPane.YAxis.Title.FontSpec.Size oldYTitleSize * scale; zgc.GraphPane.XAxis.Scale.FontSpec.Size oldXScaleSize * scale; zgc.GraphPane.YAxis.Scale.FontSpec.Size oldYScaleSize * scale; zgc.GraphPane.Legend.FontSpec.Size oldLegendSize * scale; // 同时设置边的宽度和标记大小 zgc.GraphPane.XAxis.MajorTic.Size * scale; zgc.GraphPane.YAxis.MajorTic.Size * scale; zgc.GraphPane.XAxis.MinorTic.Size * scale; zgc.GraphPane.YAxis.MinorTic.Size * scale; zgc.AxisChange(); Bitmap bmp zgc.GraphPane.GetImage(); // 还原字体设置避免影响界面显示 zgc.GraphPane.Title.FontSpec.Size oldTitleSize; zgc.GraphPane.XAxis.Title.FontSpec.Size oldXTitleSize; zgc.GraphPane.YAxis.Title.FontSpec.Size oldYTitleSize; zgc.GraphPane.XAxis.Scale.FontSpec.Size oldXScaleSize; zgc.GraphPane.YAxis.Scale.FontSpec.Size oldYScaleSize; zgc.GraphPane.Legend.FontSpec.Size oldLegendSize; zgc.AxisChange(); return bmp; }这个方法的核心思想是临时放大字体和刻度生成图片然后立刻还原。需要注意的一点是缩放系数不要超过3.0否则字体放太大坐标轴刻度之间的距离会显得过密画面反而不好看。我一般建议1.5到2.0倍既保证清晰度又不会破坏比例。4.2 曲线颜色与线宽在截图中的表现很多人在屏幕上调出了一条“很好看”的蓝色曲线截图发到客户那边客户看了说“颜色太淡了”。这是因为屏幕上的颜色经过显示器背光加成看着饱满但保存成图片后颜色的饱和度会和屏幕有差异尤其是浅色系。给曲线设颜色时我总结了一条经验尽量使用深色系比如深蓝Color.FromArgb(30, 80, 180)、深红Color.FromArgb(200, 50, 50)、深绿Color.FromArgb(20, 120, 60)不要用Color.LightBlue、Color.LightGreen这种浅色。另外把Line的宽度设置成2.0以上默认的1.0在导出图上会看起来偏细特别是在缩放之后更明显。public void AddLineToGraph(GraphPane pane, string legendText, Color lineColor, double[] xData, double[] yData) { LineItem curve pane.AddCurve(legendText, xData, yData, lineColor, SymbolType.None); curve.Line.Width 2.5f; // 线宽推荐2.0~3.0 curve.Line.IsSmooth true; // 启用曲线平滑 curve.Line.SmoothTension 0.7f; // 平滑张力0~1之间 // 如果需要显示数据点 curve.Symbol.Type SymbolType.Circle; curve.Symbol.Size 5f; curve.Symbol.Fill new Fill(Color.White); curve.Symbol.Border.Color lineColor; curve.Symbol.Border.Width 1.5f; }IsSmooth这个属性我到了很后期才发现。默认是false会导致绘制的折线在数据点密集时出现明显的尖角严重影响截图的美观度。打开曲线平滑后ZedGraph会使用样条插值把折线变得圆滑视觉效果提升非常明显。但要注意平滑适合温度、压力这类连续物理量如果数据本身就带阶跃性质比如阀门开关状态开了平滑反而会误导看图的人。还有一个和截图直接相关的参数SymbolType。如果你画的是点线图数据点过多时超过5000个点导出的图片会显得很凌乱。建议数据量大时直接把Symbol设为None只画线。我通常的做法是数据点少于200个时显示圆形标记大于200个时只画线这样导出的图既简洁又有层次。4.3 背景色与边框让导出的图片能直接出报告有时候客户拿着截图回来说“这张图太素了”。说的就是背景色和边框的问题。ZedGraph默认的背景是白色这个没问题但图表外层的Pane底色、标题栏、图例框如果全是白色在图里就分不清层次。我建议做这样的调整GraphPane的填充色用白色MasterPane的填充色用浅灰色Color.FromArgb(245, 245, 245)这样图片外围会有一圈浅色的“画框”效果整体看起来像一张正式的图表。透明背景这种需求如果你要导出成PDF或者放进深色网页里可以把Fill设置为Fill(null, Color.Transparent)但这个用法对白底报告不推荐。// 设置MasterPane的背景为浅灰色形成画框效果 zgc.MasterPane.Fill new Fill(Color.FromArgb(245, 245, 245)); zgc.MasterPane.Border.Color Color.FromArgb(200, 200, 200); zgc.MasterPane.Border.Width 1f; // GraphPane内部保持白色 zgc.GraphPane.Fill new Fill(Color.White); zgc.GraphPane.Chart.Fill new Fill(Color.White);这里要注意zgc.GraphPane.Chart.Fill和zgc.GraphPane.Fill的区别。前者是曲线绘制区域也就是坐标轴围起来的内部区域的背景后者是整个GraphPane包括坐标轴和标题在内的背景。两个都设成白色是为了避免保存图片时出现暗色底纹。边框的话MasterPane.Border会沿着图片最外边缘画一条细线这个在报告里效果挺好。GraphPane的Border则画在坐标轴外围如果设成和MasterPane一样的浅灰色层次感会更强。不想要边框的直接把Border.IsVisible false即可。5. 动态数据刷新下的截图技巧冻结与锁定5.1 实时刷新的曲线怎么截“当前瞬间”这是上位机开发中极容易出错的场景曲线一直在实时刷新点保存按钮时数据还在往里加截图出来发现曲线尾部是截断的或者带半个点。如果你用的是UI线程直接刷新数据保存按钮的Click事件和刷新事件是同一个消息循环里的点击保存的瞬间ZedGraph还没来得及重绘截到的图往往滞后一帧。解决这个问题有两个思路。第一个是加一个“截图锁存”标志位在保存前暂停数据刷新保存完成后恢复。第二个是用一个独立Bitmap专门负责保存操作。我比较推荐第一种因为实现简单且直观private bool _isCapturing false; private void AddDataPoint(double x, double y) { if (_isCapturing) return; // 截图时丢弃新数据保证画面完整 GraphPane pane zedGraphControl1.GraphPane; LineItem curve pane.CurveList[0] as LineItem; curve.AddPoint(x, y); // 只保留最新2000个点防止内存无限增长 if (curve.Points.Count 2000) { curve.RemovePoint(0); } zedGraphControl1.AxisChange(); zedGraphControl1.Invalidate(); } private void btnCapture_Click(object sender, EventArgs e) { _isCapturing true; // 确保待绘制的消息已经处理完 Application.DoEvents(); using (Bitmap bmp zedGraphControl1.GraphPane.GetImage()) { bmp.Save(snapshot.png, ImageFormat.Png); } _isCapturing false; }_isCapturing是个简单的锁加在数据入口处保证截图瞬间不会有新点混进去。Application.DoEvents()的作用是先把队列里的重绘消息处理完避免截到半渲染的状态。这里说一句DoEvents()在复杂UI里经常被诟病因为它会导致重入问题但在这个场景下是安全的因为我们的锁能防重入。那种“截到半条线”的问题本质上是数据更新和图像渲染不同步。用锁存之后截图的前一刻会拒绝新增数据UI刷新也完成了得到的图片就是完整的当前状态。5.2 历史数据回放时如何导出对比图工业数据分析经常会遇到“对比两天同一时段的数据”这类需求这时候不只是截当前屏幕而是要把视图缩放到指定范围再导出。ZedGraph提供了XAxis.Scale.Min和XAxis.Scale.Max属性来手动控制坐标轴范围。在导出前临时设定范围导出后恢复即可。public Bitmap ExportZoomedChart(ZedGraphControl zgc, double xMin, double xMax, string savePath) { GraphPane pane zgc.GraphPane; // 保存原始坐标轴范围 double origMin pane.XAxis.Scale.Min; double origMax pane.XAxis.Scale.Max; // 设置目标范围 pane.XAxis.Scale.Min xMin; pane.XAxis.Scale.Max xMax; zgc.AxisChange(); using (Bitmap bmp pane.GetImage()) { bmp.Save(savePath, ImageFormat.Png); } // 恢复坐标轴范围 pane.XAxis.Scale.Min origMin; pane.XAxis.Scale.Max origMax; zgc.AxisChange(); return bmp; }注意一个坑设置完XAxis.Scale.Min之后如果不调用AxisChange()坐标轴的标签不会刷新截出来的图上可能还是旧的刻度值。我最初写的时候漏了这一步图倒是截出来了但坐标轴刻度对不上白白浪费了一个下午排查。所以记住改了范围之后一定要调AxisChange()。另一个实战细节是Y轴范围。如果只看X轴范围而不设置Y轴范围ZedGraph会根据当前X范围内的数据自动调整Y轴范围这会导致不同时间段的图Y轴刻度不一致对比的时候很难看出趋势差异。建议对比图都用同样的Y轴范围代码里只需要加上pane.YAxis.Scale.Min和pane.YAxis.Scale.Max的设置就行。5.3 后台线程导出时小心“跨线程访问控件”异常上位机有个常见需求批量导出当天所有批次的曲线图每次导出要一两秒如果直接在UI线程里做界面就会卡死。所以通常要开一个后台线程去导出。但ZedGraphControl是WinForm控件它不允许在非UI线程中直接访问。这是很多新手会踩的坑。我的做法是把导出时要用的数据先复制一份在后台线程里新建一个独立的ZedGraphControl实例不添加到窗体上然后在这个隐藏控件上绘图并导出。这样既绕开了跨线程访问问题又不会阻塞UI。private void BatchExportInBackground(ListDataBatch batches) { Thread worker new Thread(() { foreach (var batch in batches) { using (Bitmap bmp RenderChartFromData(batch)) { string path Path.Combine(exportDir, ${batch.BatchId}.png); bmp.Save(path, ImageFormat.Png); } } }); worker.IsBackground true; worker.Start(); } private Bitmap RenderChartFromData(DataBatch batch) { // 创建一个不显示的ZedGraphControl using (ZedGraphControl tempZgc new ZedGraphControl()) { GraphPane pane tempZgc.GraphPane; pane.Title.Text $批次{batch.BatchId}温度曲线; pane.XAxis.Title.Text 时间; pane.YAxis.Title.Text 温度℃; LineItem curve pane.AddCurve(温度, batch.TimeArray, batch.TempArray, Color.Blue, SymbolType.None); curve.Line.Width 2f; pane.XAxis.Scale.Min batch.StartTime; pane.XAxis.Scale.Max batch.EndTime; tempZgc.AxisChange(); tempZgc.IsAntiAlias true; return tempZgc.GraphPane.GetImage(); } }用隐藏控件实例的好处是每个导出任务都有自己独立的绘图上下文互不干扰不用担心屏幕上正在显示的图表被改动。这个模式我用在批量生成日报曲线、周报曲线已经两年多了一直很稳定。注意using包裹ZedGraphControl用完后释放资源避免GDI句柄泄漏。隐藏控件的实例化成本并不高一般几十毫秒在批量导出场景里完全可以接受。如果你要一次性导出几千张图可以考虑复用同一个隐藏控件每次清空数据再更新但这时候要自己加锁避免多个批量任务同时操弄同一个实例。6. 常见问题与排查技巧实录6.1 截图黑色背景或者闪烁问题有段时间我的截图程序导出后图片是黑色的曲线完全看不到。排查下来问题出在Bitmap创建之后直接调用了Graphics.FromImage但没有先清空背景色。在GDI里新创建的Bitmap默认是未初始化的内存里面可能是任何垃圾数据显示出来就是黑色或者乱码。解决办法非常简单在绘图前调用g.Clear(Color.White)Bitmap bmp new Bitmap(width, height); using (Graphics g Graphics.FromImage(bmp)) { g.Clear(Color.White); // 后续绘图操作 }至于截图时界面闪烁多半是因为截图的瞬间窗体正在重绘。可以先调用一次zedGraphControl1.Invalidate()加上Application.DoEvents()等界面刷新完成后再截图。这个我前面提到过但值得再强调一次DoEvents()使用要谨慎在按钮点击事件里安全但千万不要放在数据循环里。6.2 上传到服务器或者发给别人后图片变模糊这其实不是图片变模糊而是屏幕分辨率不同导致的观感差异。你自己在2K屏上看挺清晰发给用1080P的客户他用看图软件放大后就显得虚。唯一的解法是导出时使用固定尺寸而非屏幕尺寸。我推荐导出的最小尺寸是1920x1080如果需要打印或者投屏至少给到2560x1440。用前面提到过的MasterPane.Rect临时修改法就能做到zgc.MasterPane.Rect new RectangleF(0, 0, 1920, 1080);在修改完MasterPane.Rect以后务必返回原尺寸并再次AxisChange()我已经见到太多同事因为忘记还原导致控件布局错乱的案例了。6.3 图片导出了但坐标轴刻度标签重叠数据区间比较宽刻度标签又多导出大图时经常出现X轴时间标签挤成一团。解决办法是在导出前临时设置刻度步长pane.XAxis.Scale.MajorStep 30 * 60; // 30分钟一个刻度 pane.XAxis.Scale.MinorStep 10 * 60; // 10分钟一个小刻度 pane.XAxis.Scale.Format HH:mm;MajorStep的值要结合当前坐标轴范围来定。比如时间范围是0到2小时设成30分钟一个主刻度就会显示4个刻度刚好合适。如果范围扩大到24小时还设30分钟就会把图挤满这时候应该改成2小时或4小时一个刻度。这个改完也要调AxisChange()才能生效。刻度重叠的问题在大尺寸导出时特别常见因为图片宽度变大后ZedGraph的默认刻度策略不一定能自动适配。先试着手动设置步长如果实在密集也可以把Scale.MajorStepAuto设为false强制使用你给定的步长。7. 截图功能的边界与扩展思路ZedGraphControl的截图功能其实还有不少进阶玩法。比如导出的图片要加公司logo、加时间水印、加数据签名这些都可以在得到Bitmap之后用GDI直接在上层绘制。我在工业项目里经常这么做生成图表后在右下角绘制一排小字内容是采样时间、设备编号、操作员ID这样截图导出后本身就是一条完整的追溯记录。public static void DrawWatermark(Bitmap bmp, string watermarkText) { using (Graphics g Graphics.FromImage(bmp)) { Font font new Font(Microsoft YaHei, 9f); SizeF textSize g.MeasureString(watermarkText, font); float x bmp.Width - textSize.Width - 10; float y bmp.Height - textSize.Height - 10; using (SolidBrush brush new SolidBrush(Color.FromArgb(128, 100, 100, 100))) { g.DrawString(watermarkText, font, brush, x, y); } font.Dispose(); } }还有一类需求是把曲线图导出到Excel或者PDF里。如果只是把图片放到Excel里直接插入图片就行如果是PDF可以结合PdfSharp或者iTextSharp把Bitmap插入到PDF页面中。基本的思路都是先生成Bitmap再做后续处理。另外有一个使用频率不高但确实有用的功能MasterPane.GetImage不仅能把当前显示内容渲染出来还可以通过设置IsVisible临时隐藏某些曲线或标题从而导出不同版本的图。比如导出“不包含设备编号”的客户版、和包含详细参数的内审版这一招很实用。LineItem curve pane.CurveList[0] as LineItem; bool originalVisible curve.IsVisible; curve.IsVisible false; // 临时隐藏 // 导出客户版 curve.IsVisible originalVisible; // 恢复 zgc.AxisChange();关于ZedGraphControl截图最后再说一个我踩过的大坑在加载大量历史数据10万点以上的情况下GetImage()的耗时可能长达数秒如果你在UI线程里执行界面会“假死”很长时间。这种场景下一定要把导出操作放到后台线程并用一个进度条提示用户不要让用户体验停留在“不知道是不是卡死了”的状态。后面我封装了一个带有BackgroundWorker的导出组件配合进度条基本上所有数据量的情况都能优雅应付。我实际操作中的经验是ZedGraphControl的截图功能看着简单真正用起来还是有不少隐藏门槛。把MasterPane和GraphPane的关系理清把字体和颜色配置提前做好把后台导出的跨线程问题解决掉这三个点吃透了这个功能基本不会再出幺蛾子。希望这篇整理对你有帮助少走点弯路。需要多批次批量导出的可以重点参考5.3节的后台渲染方案那是目前我见过鲁棒性最好的实现方式。
返回列表