
简介这份C#绘制曲线示例源码面向具备一定C#基础、希望掌握数据可视化与图形编程的开发者重点解决如何将SQL数据库中的数据读取后动态绘制成曲线图的问题。压缩包共25个文件约50KB以cs源代码、csproj项目文件、sln解决方案、sql数据库脚本及resx资源文件为主另含exe可执行程序、dll类库与pdb调试文件结构完整可直接在Visual Studio中打开运行。源码演示了通过ADO.NET连接数据库、执行查询、解析数据点再借助GDI的Graphics类绘制曲线的完整流程并涉及窗体控件与资源文件的组织方式。目前已有748人学习下载适合作为课程设计、数据监控界面或报表模块的参考范例帮助读者理解数据库与图形编程的结合思路并在此基础上扩展异步加载、动态刷新与交互提示等功能。1. 从一份 WinForm 曲线源码说起C# 绘图 SQL 数据落地的真实场景很多做 C# 上位机或者工控数据采集的朋友都会遇到一个绕不开的需求把传感器或者业务系统里的数据用曲线的方式实时画出来。我最近拆了一份Case06_11的源码包它做的事情很朴素——用 C# 的 GDI 在 WinForm 窗体上画曲线数据来源是 SQL Server 数据库配套一个scriptdb.sql建库脚本。这不是什么高大上的图表库没有 WPF 的 MVVM 绑定也没有第三方控件的花哨动画但它把「数据从数据库出来、经过 ADO.NET 拿到内存、再用Graphics.DrawCurve画到窗体上」这条链路完整跑通了。对于刚接触 C# 图形编程、或者需要快速给现有 WinForm 项目加一个数据可视化模块的人来说这份源码的价值在于它足够小、足够透明你能看清每一行代码在干什么而不是被封装好的控件黑匣子挡住视线。它适合谁适合那些手头有 SQL Server 数据、想在 WinForm 里快速看到曲线效果、并且愿意自己动手改坐标轴和刷新逻辑的开发者。2. 拆解 Case06_11 工程结构从 .sln 到 Form1.cs 的代码地图拿到一个源码包我习惯先看目录结构因为文件布局往往暴露了作者的开发习惯和项目的真实复杂度。Case06_11这个包解压后根目录下是Case06_11.sln解决方案文件旁边跟着Case06_11.csproj项目文件再往下是Form1.cs、Form1.Designer.cs、Program.cs这三个核心代码文件以及Properties文件夹里的AssemblyInfo.cs、Settings.settings、Resources.resx等标准配置。最值得注意的是根目录下的scriptdb.sql这说明数据库不是靠代码里硬编码建表而是单独给了脚本部署的时候先跑脚本再跑程序思路是对的。2.1 解决方案与项目文件的组织逻辑.sln文件本身不包含代码它记录的是项目之间的引用关系和全局配置。用 Visual Studio 打开Case06_11.sln之后你会看到解决方案资源管理器里只有一个项目Case06_11。.csproj文件里定义了目标框架从这类教学案例的常见做法来看大概率是 .NET Framework 4.x因为 GDI 的System.Drawing在 .NET Core 之后的跨平台版本里虽然还能用但 WinForm 设计器的支持在旧版框架下更稳定。项目文件里还会列出所有Compile项也就是参与编译的.cs文件以及None或Content项比如scriptdb.sql通常会被标记为不参与编译但随项目保留。打开.csproj你可以关注两个地方一是TargetFrameworkVersion它决定了你能用哪些 API二是Reference节点里有没有System.Data.SqlClient或者System.Data这直接关系到数据库连接能不能编译通过。如果引用缺失哪怕代码写得再对using System.Data.SqlClient;那一行也会报红。2.2 Form1.cs 与 Designer.cs 的分工Form1.cs是业务逻辑的主战场Form1.Designer.cs是窗体设计器自动生成的布局代码。很多新手会把绘图代码写到Designer.cs里这是大忌因为下次你在设计器里拖一个按钮自动生成的代码就会把你的手写逻辑冲掉。正确的做法是所有控件声明和初始化留在Designer.cs所有绘制逻辑、数据库查询、按钮点击事件处理都放在Form1.cs。在Form1.cs里你通常会看到构造函数public Form1()调用InitializeComponent()然后可能绑定一个Load事件或者按钮的Click事件。绘图的核心入口一般是窗体的Paint事件或者你手动获取Graphics对象。如果作者用的是Paint事件那么每次窗体重绘比如被其他窗口遮挡后恢复曲线都会自动重画这是比较稳妥的做法。如果是在按钮点击里直接CreateGraphics()那窗口一最小化再恢复曲线就没了这是常见的翻车点。2.3 scriptdb.sql 里的数据库初始化线索scriptdb.sql这个文件名很直白就是建库建表插数据的脚本。用文本编辑器打开它你大概率会看到CREATE DATABASE、USE、CREATE TABLE以及若干条INSERT INTO语句。表结构通常很简单比如一张CurveData表字段可能是Id、XValue、YValue或者RecordTime、SensorValue这种更贴近工控场景的命名。插入的数据就是用来画曲线的原始点集。这里有个细节值得留意脚本里有没有GO批处理分隔符。如果有说明它是在 SQL Server Management Studio 里直接执行的如果没有用 ADO.NET 的SqlCommand一次性执行整个脚本可能会因为多语句批处理而出错。我一般会建议先在 SSMS 里把脚本跑一遍确认数据库和表都建好了再回到 Visual Studio 里跑程序。这样能把数据库问题和代码问题分开排查省得两头抓瞎。3. 用 ADO.NET 把 SQL 数据读进内存连接、查询与 DataTable 填充曲线要画得出来前提是数据得先到手。这份源码用的是 ADO.NET 那套经典组合SqlConnection、SqlCommand、SqlDataAdapter、DataTable。虽然现在 Entity Framework 和 Dapper 很流行但在这种小体量教学项目里原生 ADO.NET 反而更清晰没有隐藏的映射逻辑每一步都看得见。3.1 连接字符串的构造与安全边界连接字符串是数据库访问的第一道门槛。在这类示例里常见写法是直接拼一个string connStr Data Source.;Initial CatalogCurveDB;Integrated SecurityTrue;。Data Source.表示本机默认实例Initial Catalog指定数据库名Integrated SecurityTrue表示用 Windows 身份验证不需要在代码里写用户名密码。如果你的 SQL Server 是命名实例比如SQLEXPRESS那就要写成Data Source.\SQLEXPRESS。提示把连接字符串硬编码在Form1.cs里虽然跑得快但换一台机器就可能连不上。更稳妥的做法是放到App.config的connectionStrings节点里用ConfigurationManager.ConnectionStrings[MyConn].ConnectionString读取。这样部署的时候只改配置文件不用重新编译。如果你用的是 SQL 身份验证连接字符串里就要加User IDsa;Password你的密码;。但我不建议在示例代码里用 sa权限太大一旦泄露风险很高。本地开发用 Windows 验证就够了。3.2 查询语句与 DataTable 的填充过程数据读取的核心代码通常长这样using System.Data; using System.Data.SqlClient; // 假设连接字符串已经定义好 string connStr Data Source.;Initial CatalogCurveDB;Integrated SecurityTrue; string sql SELECT XValue, YValue FROM CurveData ORDER BY XValue; DataTable dt new DataTable(); using (SqlConnection conn new SqlConnection(connStr)) { using (SqlCommand cmd new SqlCommand(sql, conn)) { SqlDataAdapter adapter new SqlDataAdapter(cmd); adapter.Fill(dt); // 把查询结果填充到 DataTable } } // 到这里 dt 里就是按 XValue 排序好的数据点这段代码的逻辑很直白SqlConnection负责建立物理连接SqlCommand承载 SQL 语句SqlDataAdapter是连接DataSet/DataTable和数据库之间的桥梁。adapter.Fill(dt)内部会自己打开连接、执行查询、把结果集映射成行和列、然后关闭连接。你不需要手动conn.Open()Fill方法会帮你管理连接状态。参数说明方面ORDER BY XValue很重要。曲线绘制要求数据点按 X 轴顺序排列如果数据库里插入顺序是乱的画出来的线就会来回折返看起来像一团乱麻。我一般会在 SQL 里就排好序而不是拿到内存再用 LINQ 排因为数据库的排序效率通常更高而且减少 C# 端的代码量。3.3 把 DataTable 转成绘图用的点数组DataTable虽然方便但绘图的时候用DataRow逐行访问效率不高而且Graphics.DrawCurve需要的是PointF[]或者Point[]数组。所以中间要做一次转换PointF[] points new PointF[dt.Rows.Count]; for (int i 0; i dt.Rows.Count; i) { float x Convert.ToSingle(dt.Rows[i][XValue]); float y Convert.ToSingle(dt.Rows[i][YValue]); points[i] new PointF(x, y); }这里用Convert.ToSingle而不是直接强制转换是因为数据库里的数值类型可能是decimal或者double直接(float)在某些情况下会抛异常。Convert.ToSingle会做安全的类型转换失败时抛FormatException或InvalidCastException方便你定位是哪一行的数据有问题。如果数据量很大比如几万个点一次性全画出来会卡顿。常见做法是抽稀或者只画可见区域内的点。但在教学示例里数据量通常只有几十到几百个直接全画没问题。4. GDI 绘制曲线的核心Graphics、Pen 与 DrawCurve 参数调优数据准备好了接下来就是把它画到窗体上。GDI 的绘图 API 不算复杂但有几个参数如果没设对曲线要么看不见要么锯齿严重要么位置偏到窗体外面。4.1 获取 Graphics 对象的正确姿势在 WinForm 里绘图最稳妥的方式是重写OnPaint方法或者订阅Paint事件protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g e.Graphics; // 用 Paint 事件给的 Graphics不要自己 CreateGraphics g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 抗锯齿 // 后续绘图代码写在这里 }e.Graphics是系统在重绘时传进来的它已经处理好了裁剪区域和背景擦除。如果你在按钮点击里用this.CreateGraphics()那画出来的东西是临时的窗口一刷新就消失。这是新手最容易踩的坑之一。SmoothingMode.AntiAlias开启抗锯齿曲线边缘会平滑很多。代价是绘制速度稍微慢一点但对于几百个点的曲线来说这点开销可以忽略。4.2 Pen 的构造与坐标变换Pen决定了线条的颜色、粗细和样式using (Pen pen new Pen(Color.Blue, 2f)) { pen.StartCap System.Drawing.Drawing2D.LineCap.Round; pen.EndCap System.Drawing.Drawing2D.LineCap.Round; // 绘图 }Color.Blue是线条颜色2f是线宽单位是像素。StartCap和EndCap设成Round可以让线条两端圆润不会出现方头。Pen实现了IDisposable所以用using包起来避免资源泄漏。坐标变换是另一个关键点。数据库里的 XValue 和 YValue 可能是实际物理量比如温度 20~80 摄氏度压力 0~1.5 MPa。而窗体的绘图区域只有几百像素宽高。你需要把数据坐标映射到屏幕坐标// 假设数据范围 xMin~xMax, yMin~yMax绘图区域 rect float scaleX rect.Width / (xMax - xMin); float scaleY rect.Height / (yMax - yMin); PointF[] screenPoints points.Select(p new PointF( rect.Left (p.X - xMin) * scaleX, rect.Bottom - (p.Y - yMin) * scaleY // Y 轴翻转因为屏幕坐标向下增长 )).ToArray();注意 Y 轴要翻转因为数学坐标系原点在左下屏幕坐标系原点在左上。如果不翻转曲线上下就是反的。4.3 DrawCurve 与 DrawLines 的选择差异GDI 提供了两个画线方法DrawCurve和DrawLines。DrawLines是折线点与点之间用直线连接转折处有棱角。DrawCurve是平滑曲线它用 Cardinal 样条插值让曲线穿过所有数据点转折处圆滑。g.DrawCurve(pen, screenPoints); // 平滑曲线 // 或者 g.DrawLines(pen, screenPoints); // 折线DrawCurve有一个可选参数tension默认值是 0.5f。tension越大曲线越弯曲tension为 0 时DrawCurve退化成DrawLines。如果你发现曲线在数据点之间摆动太厉害可以试着把tension调小到 0.3f 左右。但要注意DrawCurve的平滑是视觉上的它并不改变数据本身所以不要用它来做数据拟合。注意DrawCurve要求至少 3 个点才能画出曲线效果如果只有 2 个点它和DrawLines没区别。如果数据点少于 2 个直接跳过绘制否则会抛ArgumentException。4.4 坐标轴与网格线的补充绘制光有曲线还不够没有坐标轴和刻度读者不知道曲线代表什么数值。常见的做法是在OnPaint里先画坐标轴using (Pen axisPen new Pen(Color.Black, 1f)) { g.DrawLine(axisPen, rect.Left, rect.Top, rect.Left, rect.Bottom); // Y 轴 g.DrawLine(axisPen, rect.Left, rect.Bottom, rect.Right, rect.Bottom); // X 轴 }然后用g.DrawString在轴旁边标注刻度值。DrawString需要Font和Brush记得也用using包起来。网格线可以用浅灰色虚线pen.DashStyle DashStyle.Dot这样不会喧宾夺主。如果窗体大小会变化记得在Resize事件里调用Invalidate()触发重绘否则曲线还是按旧尺寸画的窗口拉大后右边会留白。5. 避坑与排查数据库连不上、曲线不显示、刷新闪烁的常见问题这份源码跑起来不难但环境差异和细节疏忽会让它在你机器上翻车。下面这几条是我实际调试时遇到过的典型问题按「现象 → 原因 → 解决」整理。5.1 现象程序启动报「无法打开登录所请求的数据库」原因scriptdb.sql没有执行或者执行了但数据库名和连接字符串里的Initial Catalog不一致。也有可能是 SQL Server 服务没启动或者当前 Windows 账户没有访问该数据库的权限。解决先在 SSMS 里连上本机 SQL Server打开scriptdb.sql执行一遍确认CurveDB数据库和CurveData表都存在。然后检查连接字符串里的Data Source是否和 SSMS 里连接的实例名一致。如果是命名实例Data Source要写成.\SQLEXPRESS这种形式。最后确认 SQL Server 的 Windows 身份验证模式已启用。5.2 现象窗体上什么都没有曲线不显示原因绘图代码写在了CreateGraphics()里窗口被遮挡后重绘时曲线丢失或者Paint事件没有正确绑定或者数据点数组为空DrawCurve直接跳过。解决把绘图逻辑移到OnPaint重写方法或Paint事件处理器里。在OnPaint开头加一句Console.WriteLine(points.Length);确认数据点数量大于 2。如果数据点来自数据库检查adapter.Fill(dt)之后dt.Rows.Count是不是 0如果是 0说明 SQL 查询没返回任何行去数据库里SELECT COUNT(*) FROM CurveData看看有没有数据。5.3 现象曲线画出来了但位置偏了或者上下颠倒原因坐标变换时没有做 Y 轴翻转或者rect的Bottom和Top用反了。屏幕坐标的 Y 轴向下增长而数据坐标的 Y 轴通常向上增长。解决在计算屏幕 Y 坐标时用rect.Bottom - (p.Y - yMin) * scaleY而不是rect.Top (p.Y - yMin) * scaleY。同时确认rect的Height是正数如果rect.Bottom rect.TopHeight会是负数导致所有点跑到窗体外面。5.4 现象窗口拖动或调整大小时曲线闪烁严重原因WinForm 默认在重绘时会先用背景色擦除整个客户区然后再画曲线这个「擦除-绘制」的过程被人眼捕捉到就是闪烁。解决在窗体构造函数里开启双缓冲this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); this.UpdateStyles();AllPaintingInWmPaint禁止系统擦除背景UserPaint让控件自己负责绘制OptimizedDoubleBuffer启用双缓冲。这三者配合闪烁基本就消失了。5.5 现象数据量大了之后界面卡死原因在 UI 线程里同步执行数据库查询和绘图数据量大时Fill和DrawCurve都会阻塞消息循环。解决把数据库查询放到Task.Run里异步执行查询完成后用Invoke回到 UI 线程更新数据并调用Invalidate()。绘图本身如果点太多可以考虑只画可见区域或者做抽稀。但在这份教学源码的体量下通常不至于卡死除非你往CurveData表里插了几十万行。6. 进阶技巧让曲线动起来、可交互、能导出把静态曲线画出来只是第一步。实际项目里你往往需要曲线能实时刷新、鼠标悬停能看到具体数值、甚至导出成图片存档。下面这几个技巧是我在这个源码基础上经常加的代码量不大但实用性提升明显。6.1 用 Timer 实现数据定时刷新如果数据库里的数据是持续写入的你可以用System.Windows.Forms.Timer每隔几秒重新查询一次并重绘private Timer refreshTimer; private void InitTimer() { refreshTimer new Timer(); refreshTimer.Interval 3000; // 3 秒 refreshTimer.Tick (s, e) { LoadDataAndRefresh(); // 重新查询数据库并更新 points 数组 this.Invalidate(); // 触发重绘 }; refreshTimer.Start(); }Interval单位是毫秒3000 就是 3 秒。Tick事件里不要做太耗时的操作否则 UI 还是会卡。如果查询慢就改成异步查询查完再Invalidate。6.2 鼠标悬停显示数据点数值在OnPaint里画完曲线后可以记录每个数据点的屏幕坐标。然后在MouseMove事件里判断鼠标位置是否靠近某个点private PointF[] screenPoints; // 在 OnPaint 里赋值 private void Form1_MouseMove(object sender, MouseEventArgs e) { for (int i 0; i screenPoints.Length; i) { float dx e.X - screenPoints[i].X; float dy e.Y - screenPoints[i].Y; if (dx * dx dy * dy 25) // 距离小于 5 像素 { this.Text $X{points[i].X:F2}, Y{points[i].Y:F2}; return; } } this.Text C# 曲线绘制示例; }这里用平方距离比较避免开方运算。25是 5 像素的平方。找到最近的点后把数值显示在窗体标题栏上简单直接。如果要更美观可以用ToolTip控件。6.3 把曲线导出为 PNG 图片有时候需要把当前曲线保存下来发给别人或者插入报告。Graphics对象可以从Bitmap创建画完之后保存private void ExportToPng(string filePath) { using (Bitmap bmp new Bitmap(this.ClientSize.Width, this.ClientSize.Height)) using (Graphics g Graphics.FromImage(bmp)) { g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 这里调用和 OnPaint 里一样的绘图方法传入 g DrawChart(g); bmp.Save(filePath, System.Drawing.Imaging.ImageFormat.Png); } }关键是把绘图逻辑抽成一个DrawChart(Graphics g)方法OnPaint里调用它导出时也调用它。这样保证屏幕显示和导出图片完全一致。bmp.Save的第二个参数指定格式PNG 无损压缩适合曲线图如果要更小的文件可以用 JPEG但曲线边缘会有压缩伪影。6.4 参数配置表常用调优项速查参数位置推荐值说明SmoothingModeGraphicsAntiAlias抗锯齿曲线更平滑Pen.WidthPen 构造1.5f ~ 2f太细看不清太粗遮挡数据DrawCurve tensionDrawCurve0.3f ~ 0.5f越小越接近折线越大越弯曲Timer.IntervalTimer1000 ~ 5000根据数据更新频率调整双缓冲窗体构造全部开启消除刷新闪烁这份源码的骨架很清晰数据库、查询、绘图三层分得开你可以在任何一层做扩展。我自己的习惯是每次拿到这类示例先跑通默认流程确认数据库和绘图都正常然后再动代码加功能。如果一上来就改连接字符串和绘图参数出了问题很难判断是环境问题还是代码问题。从那以后我每次调试 WinForm 绘图项目都强制先跑一遍原始版本再逐步替换成自己的逻辑。希望这份拆解帮到你。本文还有配套的精品资源点击获取