C#绘制曲线示例源码:SQL Server数据库查询与看板实战

发布时间:2026/10/9 12:34:20
C#绘制曲线示例源码:SQL Server数据库查询与看板实战 简介C#绘制曲线示例源码含SQL数据库是一份面向WinForms初学者的完整小项目演示如何在C#界面中结合SQL Server存储与读取数据点并动态绘制曲线。压缩包共25个文件约50KB包含6个cs源码文件、1个sql数据库脚本、sln解决方案、resx界面资源以及编译生成的exe、pdb等目录结构清晰便于直接打开学习。项目围绕“Case06_11”展开核心代码涉及Graphics类绘制曲线、控制点定义以及通过SqlConnection、SqlCommand执行插入与查询实现数据持久化。资源已吸引253人学习适合正在学习GDI图形编程、ADO.NET数据库操作或课程设计的开发者参考。通过研读源码可以掌握曲线绘制的坐标映射、数据表设计思路以及WinForms界面与数据库联调的方法是一份轻量但完整的练手素材。1. 数据库里的数据不会自己变成曲线这个示例到底解决什么做上位机或者工控看板的人大多遇到过这个场景设备运行参数已经写进 SQL Server 了但老板要看的不是一张数据表而是一条反应趋势的曲线。把查询结果丢给 Excel 能出图可客户要的是在自家 C# 程序里点一下按钮就看到曲线最好还能跟着新数据动。于是「C#绘制曲线示例源码含SQL数据库」这类项目就成了很多人的起点——它演示的不只是画一条线的语法而是把「数据库查询 坐标换算 图形绘制」串起来的最小可行方案。这里最大的反直觉点在于真正的难点不是绘图 API 本身而是如何把数据库里可能带空值、乱序、海量的数据稳定地换算到屏幕上。这篇文章我就按做过类似示例工程的思路把方案选型、建表、取数、绘制和踩坑过一次讲透适合正在做 C# 上位机、数据看板或个人学习的人。2. 方案选型画曲线用自绘还是第三方为什么示例源码偏爱数据库直连2.1 三种常见绘制路线对比先把话说在前面C# 里画曲线没有唯一正确答案但选型错了后面会很难受。常见的有三条路线。第一条是 WinForm / WPF 里用 GDI 手绘在Paint事件里画DrawLine、DrawPolyline或GraphicsPath。优点是零第三方依赖完全可控适合把报告写得清清楚楚缺点是一旦要缩放、要多条曲线、要实时更新所有交互逻辑都得自己写工作量迅速膨胀。第二条是接第三方绘图控件最常用的是 ScottPlot 和 ZedGraph。ScottPlot 是后来居上的轻量库API 很现代散点图、折线图、实时追加性能都做得很好ZedGraph 老牌但代码风格陈旧且存在一些高 DPI 下的显示问题。第三条是把绘图交给数据库端比如 SQL Server 直接返回 JSON用前端 ECharts 画——这已经不属于 C# 绘图的讨论范围但很多现场环境不允许开前端所以不是这里讨论的重点。回到标题里的示例源码我见过的几个主流实现都用的是「SQL 数据库 GDI 自绘」或「SQL 数据库 ScottPlot」这两种组合。为什么示例喜欢数据库直连而不是把数据先导成 CSV因为现场经常要实时看今天的曲线每天的数据都在变代码里饭做到点一下按钮重新查一次就行。另外直连数据库能把查询条件时间段、设备号做成控件参数这就比静态读文件有意义得多。如果你要复现这套方案我最建议的组合是WinForm 窗体 System.Data.SqlClient或Microsoft.Data.SqlClient读 SQL Server绘图层用 ScottPlot。如果公司不允许引入第三方包再退回 GDI 自绘。2.2 示例源码的工程结构怎么组织一份完整可运行的示例结构上应该是「尿布式」的三层而不是把所有代码都塞到一个窗体事件里。常见做法是拆成三个文件DatabaseHelper.cs负责所有跟数据库有关的连接、查询ChartRenderer.cs负责把DataTable转换为绘图数据并实现具体的绘制或把数据交给控件MainForm.cs只负责布局控件、触发加载和响应用户操作。这样做的好处现场很快就体会到一开始代码全写在按钮点击事件里后来想加第二个曲线图发现复制粘贴了两百行改一处漏一处你会想重写。工程里的连接字符串一般写在App.config的connectionStrings节点里不要硬编码在源码中。示例源码通常会给一个App.config模板里面是Server.;DatabaseTestDb;Integrated SecurityTrue;这样最直观的形式。如果你的环境必须用账号密码登录那就写User Idsa;Passwordxxx;。要注意的是很多网上示例用Integrated SecurityTrue是因为本地开发方便一旦部署到客户机器上服务账户可能没有访问 SQL Server 的权限那时候你就知道为什么生产环境要用显式账号了。这是我从现场带回的第一个血泪经验先花五分钟想清楚部署环境再决定连接串怎么写不然演示一时爽部署火葬场。3. 准备数据建一个 SQL 表并写出稳定取数的方法3.1 建表和填充数据的 SQL要绘制的曲线必须来源于一张结构清晰的数据表示例里最常见的是传感器数据表。我先给出一份适合做曲线输入的建表脚本。这里最核心的设计点是一定要有SampleTime作为时间列并加上索引否则后续按时间范围查询在数据量大时会慢到让你怀疑人生数据列用FLOAT还是REAL都行但建议允许NULL因为现场设备有时会掉线漏采是常态。CREATE TABLE dbo.DeviceData ( Id INT IDENTITY(1,1) PRIMARY KEY, SampleTime DATETIME2(3) NOT NULL, Sensor1 FLOAT NULL, Sensor2 FLOAT NULL, MachineName NVARCHAR(50) NULL ); CREATE INDEX IX_DeviceData_SampleTime ON dbo.DeviceData(SampleTime ASC);这里的逻辑说明DATETIME2(3)比DATETIME精度更高且不会像DATETIME那样存在四舍五入到毫秒的诡异行为IDENTITY主键只是为了让表结构规范查询时不会用它来做过滤。索引建立在SampleTime上是因为所有的曲线查询都会用WHERE SampleTime BETWEEN start AND end有索引后哪怕表里有一百万行单次查询也能控制在几十毫秒级别没有索引就是全表扫描这是示例里最常见的性能瓶颈。如果你是拿 SQL Server Management Studio 手动准备测试数据写一个循环脚本会方便很多。下面这段 SQL 生成一天的数据每 10 秒一条模拟一个正弦波加随机噪声的传感器曲线。注意这里没有用复杂语法因为示例源码的读者多是开发者目标是要能跑起来。DECLARE start DATETIME2(3) 2025-05-01 00:00:00; DECLARE i INT 0; WHILE i 8640 BEGIN INSERT INTO dbo.DeviceData (SampleTime, Sensor1, Sensor2, MachineName) VALUES ( DATEADD(SECOND, i * 10, start), 20 10 * SIN(i * 0.01) RAND() * 0.5, 30 5 * COS(i * 0.02) RAND() * 0.3, Pump-01 ); SET i i 1; END;参数说明8640是 24 小时除以 10 秒得到的条数实际设备采样频率可能更高你可以把间隔改到 1 秒那样一天就是 86400 条。这种数据量对 GDI 自绘已经是个挑战但对 ScottPlot 来说完全不是问题。如果你的示例数据是产品真实数据注意别把敏感信息放进公开源码我通常把MachineName替换成Pump-01这种脱敏值。3.2 C# 访问 SQL Server 的稳定写法到了 C# 侧取数逻辑要写得稳定。很多初版示例会把连接字符串写死在按钮事件里每次都新建连接查一遍我也这么写过但不是好习惯。推荐的做法是封装一个DataTable LoadSensorData(DateTime start, DateTime end)方法主窗体把时间范围传进来方法返回DataTable绘图层只认DataTable。这样 UI、数据库、绘图三者解耦后续把数据换成 MySQL 或 SQLite你只需要改这一层。using System.Data; using Microsoft.Data.SqlClient; public static DataTable LoadSensorData( DateTime start, DateTime end, string connectionString) { const string sql SELECT SampleTime, Sensor1, Sensor2 FROM dbo.DeviceData WHERE SampleTime BETWEEN start AND end ORDER BY SampleTime ASC;; using var conn new SqlConnection(connectionString); using var cmd new SqlCommand(sql, conn); cmd.Parameters.Add(start, SqlDbType.DateTime2).Value start; cmd.Parameters.Add(end, SqlDbType.DateTime2).Value end; using var adapter new SqlDataAdapter(cmd); var result new DataTable(); adapter.Fill(result); return result; }这里的逻辑说明ORDER BY SampleTime ASC不是可有可无绘制曲线时数据必须按 X 轴升序否则折线会来回乱画SqlParameter强制参数化查询比拼接字符串安全得多也避免了时间格式在不同语言区解析出错的玄学问题。using关键字确保连接和命令在使用完后及时释放这是防止连接池耗尽的关键也是很多示例里没认真处理的地方。参数说明SqlDbType.DateTime2要和表里的DATETIME2(3)对应你可能会遇到.Value DateTime.Now时报「从 DateTime 到 DateTime2 的转换失败」那是因为DateTime.Now精度比DateTime2(3)低正常情况下不会出错如果担心那就把所有地方统一成DateTime2。这个方法执行完后返回的DataTable里SampleTime是DateTime类型Sensor1/Sensor2是FLOAT可能带DBNull后续处理时要留意。4. 绘制曲线把 DataTable 变成屏幕上的折线4.1 双缓冲与坐标换算数据到手后就到了绘图环节。先说自绘方案因为理解自绘之后你迁移到任何控件都会轻松很多。WinForm 自绘的本质是在OnPaint里拿Graphics对象画图而最需要认真设计的就是「数据坐标」到「屏幕像素坐标」的映射。假设窗体画图区宽clientWidth高clientHeight左上角留出paddingLeft和paddingTop作为边距那 X 轴像素位置就是paddingLeft (sampleTime - minTime) / (maxTime - minTime) * plotWidth。Y 轴同理只是 Y 轴屏幕坐标是向下增长的所以要用clientHeight - paddingBottom - (value - minValue) / (maxValue - minValue) * plotHeight。这个「Y 轴反转 留边距」的细节是新手最容易画反、导致曲线上下颠倒的直接原因。示例源码为了降低阅读门槛一般会在一个自定义控件里重写OnPaint核心代码大概长这样。我给出的是可以套用的最小实现注释已经写清楚每一步的意图protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (_points null || _points.Count 2) return; var g e.Graphics; g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 计算绘图区左右留白 var left 60f; var right 20f; var top 20f; var bottom 40f; float plotWidth ClientSize.Width - left - right; float plotHeight ClientSize.Height - top - bottom; // 找出数据范围 float minX _points.Min(p p.X); float maxX _points.Max(p p.X); float minY _points.Min(p p.Y); float maxY _points.Max(p p.Y); // 防止范围为零导致除零 if (Math.Abs(maxX - minX) 1e-6) return; using var pen new Pen(Color.SteelBlue, 2f); var mappedPoints new ListPointF(_points.Count); foreach (var p in _points) { float px left (p.X - minX) / (maxX - minX) * plotWidth; float py top plotHeight - (p.Y - minY) / (maxY - minY) * plotHeight; mappedPoints.Add(new PointF(px, py)); } g.DrawLines(pen, mappedPoints.ToArray()); }这段代码的逻辑说明先根据数据范围算出映射系数再逐点转换成屏幕坐标最后调用DrawLines一次性画完整条折线。DrawLines比每条线段单独DrawLine高效得多在几千个点的场景下差别不明显但到了上万点就非常关键。SmoothingMode.AntiAlias抗锯齿会让曲线更平滑代价是绘制速度稍慢如果数据量大到每秒重绘多次可以把抗锯齿关掉。参数说明left 60f是为了给 Y 轴刻度留出数字的空间bottom 40f是为了给 X 轴时间标签留空间。如果你用ScottPlot这些边距它会自己算但自绘就必须手动留不然刻度文字会贴到绘图区边缘甚至被裁掉。_points这个字段我建议直接存原始数据坐标比如TimeSpan转成float秒数不要在OnPaint里做转换因为OnPaint在窗口大小变化、最小化恢复时都会被触发每次都去数据库查一遍就严重浪费了只重绘就好。4.2 鼠标交互与缩放示例源码真正加分的地方是带上了简单的鼠标缩放因为用户不可能只满意于看整段时间的曲线。最轻量的做法是滚轮缩放 拖拽平移。实现思路是维护一个可视范围viewStart和viewEndOnPaint里只绘制这段时间的数据。滚轮事件里把范围缩小或放大MouseDown到MouseMove之间记录偏移量并平移范围最后Invalidate()触发重绘。private float _viewStart 0; // 可视区起始时间秒 private float _viewEnd 3600; // 可视区结束时间秒 private Point _lastMousePos; protected override void OnMouseWheel(MouseEventArgs e) { float oldRange _viewEnd - _viewStart; float newRange oldRange * (e.Delta 0 ? 0.8f : 1.25f); // 以鼠标所在位置为中心缩放 float ratio (e.X - 60f) / Math.Max(1, ClientSize.Width - 80f); float centerTime _viewStart ratio * oldRange; _viewStart centerTime - newRange / 2f; _viewEnd centerTime newRange / 2f; Invalidate(); base.OnMouseWheel(e); }这里的说明滚轮缩放必须以鼠标位置为中心否则用户滚着滚着就把想看的地方滚丢了这是交互体验上很关键的点。缩放的范围要限制一下比如最小 1 秒、最大覆盖全部数据否则用户把曲线缩到比原子还小程序就会陷入除零或绘制异常。平移拖拽的原理无非是记录鼠标按下时的位置移动时把_viewStart/_viewEnd整体平移这两段配合起来就是最简单的曲线浏览交互。如果你想在示例源码上做扩展这几个变量就是入手点再加一个十字线显示鼠标对准的时间点和数值。这个自绘方案看明白了接下来你去看任何示例里的绘图代码都不会再发怵无非就是维护数据、维护可视范围、坐标映射三步。很多示例源码为了简单直接去掉缩放点击查询后整条曲线一次画出。作为学习阶段这样没问题但如果要做成真正的看板缩放和平移躲不开。ScottPlot 则内置了这些交互所以如果你的目标不是学习 GDI 而只是想尽快做出产品直接跳到第六章用 ScottPlot 替代就好。5. 避坑数据库连接、坐标越界和性能这三类问题最多5.1 连接超时与界面假死现象点查询后界面卡住十几秒然后弹「连接超时」或「对象已释放」异常。原因排查下来几乎都是连接字符串没写好或者是把耗时数据库查询放在了 UI 线程上。解决先确认连接字符串里Connect Timeout5或更短别让默认 15 秒把用户吊着再用await Task.Run(() LoadSensorData(...))把查询丢到后台线程拿到结果后Invoke回到 UI 线程更新曲线。这个改动看似小但从「假死」到「操作流畅」是质变。血泪经验是我在一个设备演示现场因为 UI 卡死被客户当场质疑程序稳定性从那以后我所有的示例源码默认都异步访问数据库。5.2 空值、DBNull 和重复时间戳现象曲线画到一半突然断裂或者坐标系里出现一个极端值把整个曲线压扁。原因设备掉线时Sensor1是NULLDataTable里的值在foreach中直接被Convert.ToSingle转就会抛异常有的设备上报时间不严格两条数据时间戳完全相同BETWEEN查询会把它们都带回来导致 X 轴出现重叠。解决取数后在内存里过滤一遍DBNull的跳过或插值重复时间戳按Id排序去重或者在SELECT里用ROW_NUMBER()只取最新一条。我一般会写一个清理方法先把原始表变成只含有效点的ListPointF再去绘图这样绘图层永远不会遇到脏数据。5.3 数据量大时重绘闪烁和内存暴涨现象几千点以上拖动窗口时曲线闪烁严重内存不断上涨最后卡死。原因默认控件没有开启双缓冲OnPaint里频繁创建Pen/Brush又没有释放数据量上万时每次重绘都在做全量坐标映射。解决在窗体构造函数里加DoubleBuffered true自定义控件则设SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true)把Pen、Brush、Font声明成类字段统一管理用using或Dispose释放如果点超过两万先降采样再绘制比如取每个像素列上的最大值或平均值这样屏幕上看不出区别但绘制时间能降低一个量级。这里有个容易翻车的点降采样做不好会把峰值抹掉工控场景里峰值往往是最重要的报警依据所以在峰值保留上我宁可保留每段极值也不能随便抽稀。5.4 坐标系反转和边距引起的显示不全现象画出来的曲线是镜像的或者最大值被裁掉半个。原因前面提过 GDI 的 Y 轴向下是正方向数据越大像素 Y 值越小如果不做plotHeight - value的换算曲线必然上下颠倒而最大值被裁掉则是 Y 轴映射时没有给上下留 padding或者plotHeight算出来是负数控件高度太小。解决写一个MapY小函数统一处理边距至少上下各留 20 像素控件最小尺寸用AutoSize或MinimumSize限制住。这些坑在示例源码里其实都已经处理过但初学者在改造时最容易把它们改回来所以我建议不要为了「看起来简单」就删掉这些换算代码。6. 进阶把示例源码改成实时刷新的上位机曲线6.1 Timer 轮询与增量查询静态曲线跑通后下一步就是让它动起来。实时刷新最常见且稳妥的方案是System.Windows.Forms.Timer周期触发查询间隔一般设在 1 到 5 秒。注意不要全量重查整个时间段否则数据量越来越大界面迟早卡顿。我通常的做法是首次加载查最近 5 分钟以后每次只查「上次最后时间戳到现在」的记录追加到内存里再重绘。伪代码如下var newRows LoadSensorData(lastTime, DateTime.Now, connStr); foreach (DataRow row in newRows) { float t Convert.ToSingle(row[SampleTime]); float v Convert.ToSingle(row[Sensor1]); points.Add(new PointF(t, v)); } lastTime DateTime.Now; chart.Invalidate();这段逻辑说明增量查询的核心是把上次查询时间记住用WHERE SampleTime lastTime注意用避免重复读同一行。如果设备数据频率很快ListPointF无限增长也是问题滚动到窗口外可以直接丢弃或者用环形缓冲区只保留最近 N 个点。做上位机时定时器不要设在 UI 线程里做长耗时操作查询依然要异步定时器只负责「到点触发」触发后另开线程干活。6.2 用 ScottPlot 快速替代手写 GDI如果你的目标是尽快做出可用产品我强烈建议把绘图层换成 ScottPlot。ScottPlot 的 API 非常直白formsPlot1.plt.PlotScatter(xs, ys)然后formsPlot1.Render()。它自带缩放、平移、十字光标省掉第五章里那一堆交互代码。数据更新时调用plt.Clear()再重新PlotScatter一次即可几百上千点性能完全没问题实时刷新可以用它的PlotSignal或PlotScatter的manage_auto_limits功能。var xs points.Select(p p.X).ToArray(); var ys points.Select(p p.Y).ToArray(); formsPlot1.plt.Clear(); formsPlot1.plt.PlotScatter(xs, ys, label: Sensor1); formsPlot1.plt.AxisAuto(); formsPlot1.Render();这里的说明PlotScatter默认画的就是带抗锯齿的折线AxisAuto自动调整坐标范围Render触发重绘。如果你希望曲线刷新时不闪烁ScottPlot 内部已经处理好双缓冲不需要你再设置。引入方式很简单Visual Studio 的 NuGet 包里搜索 ScottPlot 安装即可WinForms 版本是 ScottPlot.WinForms。我个人习惯在项目早期先用 GDI 把数据流和业务逻辑调通到了界面展示阶段再换 ScottPlot——这样既理解了原理又能用现成交互快速收尾。这是我从一个被客户要求「三天出demo」的项目中学到的习惯晚上复盘时别急着把事情做漂亮先保证交付节奏希望这个思路也帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询