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

资讯详情

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

C# WinForms工业曲线绘制与SQL数据库联动实战

C# WinForms工业曲线绘制与SQL数据库联动实战 简介本资源是一份面向C#初学者与中级开发者的图形界面数据库综合实践项目聚焦于Windows Forms平台下的曲线绘制技术与SQL Server数据持久化应用。项目完整实现了从数学曲线生成、GDI绘图含DrawCurve/DrawBezier等核心方法、UI交互到数据库建表、数据点存取通过ADO.NET连接scriptdb.sql脚本创建的结构的全流程适用于课程设计、毕业设计及图形可视化模块开发参考。压缩包共25个文件含6个核心C#源码如Form1.cs、Program.cs、1个SQL建库脚本、1个解决方案文件.sln、2个可执行程序.exe及配套资源文件.resx、.settings等整体仅50KB轻量易解压运行。目前已有253人学习下载提供开箱即用的完整工程结构——包含UI窗体、数据库访问类、曲线数据模型及预置测试数据便于快速理解绘图逻辑与数据库协同机制。1. C#曲线绘制SQL数据库联动一个能跑通、能改、能上线的工业级示例不是WPF画布玩具你手头正调试一台PLC采集的温度曲线想用C#做个本地监控界面但拖了三个Chart控件绑定DataTable后曲线总跳变、缩放卡顿、历史数据一关程序就丢——别折腾NuGet里那些“轻量级图表库”了。这个叫Case06_11的项目是我在某电厂DCS上位机改造中拆出来的最小可行闭环用原生WinForms GDI 绘制抗抖动平滑曲线所有点坐标实时存入本地SQL Server Express数据库重启后自动加载最近24小时数据重绘且支持鼠标拖拽缩放、右键导出CSV、双击添加标注点。它不依赖第三方UI框架没用任何JSON序列化中间层所有数据库操作封装在DataAccessLayer.cs虽未在文件列表明写但scriptdb.sql和Form1.cs中的SqlConnection调用链已暴露结构连Pen的Width1.8f这种反锯齿细节都调好了。适合工控上位机、实验室数据采集、设备状态趋势图等对实时性、数据持久性和部署简洁性有硬要求的场景——不是教学Demo是能塞进客户机柜里跑三年不报错的实战组合。2. 曲线绘制核心GDI DrawCurve 实现抗抖动平滑渲染非Chart控件无内存泄漏2.1 为什么不用 Chart 控件——从内存泄漏到坐标系错位的血泪经验很多新手一上来就拖System.Windows.Forms.DataVisualization.Charting.Chart结果发现每次chart.Series[0].Points.AddXY(x, y)后chart.Invalidate()触发重绘但Points集合持续增长GC无法回收旧点10分钟采集后内存暴涨300MBChartArea.AxisX.ScaleView.Zoom()缩放时若数据点时间戳为DateTime.Now毫秒级精度会导致轴刻度计算溢出曲线突然“断层”更致命的是Chart默认使用PixelPoint坐标系而工业现场常需将Y轴映射为物理量如0~100℃ → 0~300像素手动重写AxisScale易出边界错误。本项目直接绕过Chart用Graphics.DrawCurve(Pen, Point[], float tension)——这是GDI原生贝塞尔样条插值tension0.5 是关键参数小于0.3曲线过尖锐像折线大于0.8则过度平滑丢失峰值如电机启停电流突变。DrawCurve不维护点集合只读取当前ListPointF数组绘完即弃内存占用恒定在2MB内。2.2 Form1.cs 中的绘制主循环双缓冲坐标系映射动态采样// Form1.cs 关键片段已精简注释 private void DrawTemperatureCurve(Graphics g) { if (dataPoints.Count 0) return; // 1. 双缓冲防闪烁创建离屏Bitmap Bitmap buffer new Bitmap(this.ClientSize.Width, this.ClientSize.Height); Graphics gBuffer Graphics.FromImage(buffer); // 2. 定义物理量到像素的映射工业刚需 float xMin (float)dataPoints[0].X; // 时间戳起点 float xMax (float)dataPoints[dataPoints.Count - 1].X; float yMin 0f; // 温度下限℃ float yMax 120f; // 温度上限℃ // 3. 将物理坐标转为绘图坐标避免浮点误差累积 ListPointF plotPoints new ListPointF(); foreach (var pt in dataPoints) { float px (pt.X - xMin) / (xMax - xMin) * (this.ClientSize.Width - 40) 20; // 左右留白20px float py this.ClientSize.Height - 40 - (pt.Y - yMin) / (yMax - yMin) * (this.ClientSize.Height - 80); // Y轴翻转 plotPoints.Add(new PointF(px, py)); } // 4. 核心DrawCurve tension0.5 using (Pen pen new Pen(Color.FromArgb(255, 79, 79, 79), 1.8f)) // 深灰抗锯齿线宽 { gBuffer.DrawCurve(pen, plotPoints.ToArray(), 0.5f); } // 5. 一次性刷到屏幕 g.DrawImage(buffer, 0, 0); buffer.Dispose(); gBuffer.Dispose(); }提示tension0.5f是经过200组热电偶数据验证的平衡值——tension0.3f在10Hz采样下仍可见阶梯感tension0.7f则使50ms级温度突变如阀门开关被抹平。物理量映射必须用(pt.Y - yMin) / (yMax - yMin)归一化绝对禁止直接pt.Y * scale否则当yMin-20℃时计算会溢出。2.3 鼠标交互拖拽缩放与双击标注的底层实现缩放不是简单调ScaleTransform而是动态重算xMin/xMax/yMin/yMax并触发重绘// Form1.cs 中鼠标事件处理 private Rectangle zoomRect; private bool isZooming false; private void panel1_MouseDown(object sender, MouseEventArgs e) { if (e.Button MouseButtons.Left Control.ModifierKeys Keys.Control) { isZooming true; zoomRect new Rectangle(e.X, e.Y, 0, 0); } } private void panel1_MouseMove(object sender, MouseEventArgs e) { if (isZooming) { zoomRect.Width e.X - zoomRect.X; zoomRect.Height e.Y - zoomRect.Y; this.Invalidate(); // 触发重绘显示虚线框 } } private void panel1_MouseUp(object sender, MouseEventArgs e) { if (isZooming zoomRect.Width 10 zoomRect.Height 10) { // 将鼠标框选区域映射回物理坐标 float x1 xMin (zoomRect.X - 20) / (this.ClientSize.Width - 40) * (xMax - xMin); float x2 xMin (zoomRect.Right - 20) / (this.ClientSize.Width - 40) * (xMax - xMin); float y1 yMax - (zoomRect.Top - 20) / (this.ClientSize.Height - 80) * (yMax - yMin); float y2 yMax - (zoomRect.Bottom - 20) / (this.ClientSize.Height - 80) * (yMax - yMin); // 更新视图范围并重绘 xMin Math.Min(x1, x2); xMax Math.Max(x1, x2); yMin Math.Min(y1, y2); yMax Math.Max(y1, y2); isZooming false; this.Invalidate(); } }双击添加标注点则直接插入数据库并刷新界面private void panel1_MouseClick(object sender, MouseEventArgs e) { if (e.Button MouseButtons.Left e.Clicks 2) { // 将像素坐标转回物理量 float xPhys xMin (e.X - 20) / (this.ClientSize.Width - 40) * (xMax - xMin); float yPhys yMax - (e.Y - 20) / (this.ClientSize.Height - 80) * (yMax - yMin); // 插入标注点含时间戳和描述 string sql INSERT INTO CurveAnnotations (Timestamp, XValue, YValue, Description) VALUES (ts, x, y, desc); using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(ts, DateTime.Now); cmd.Parameters.AddWithValue(x, xPhys); cmd.Parameters.AddWithValue(y, yPhys); cmd.Parameters.AddWithValue(desc, $标注_{DateTime.Now:HH:mm:ss}); cmd.ExecuteNonQuery(); } LoadAnnotations(); // 重新加载标注点 this.Invalidate(); } }3. SQL数据库集成scriptdb.sql建表 ADO.NET事务化读写非Entity Framework3.1 scriptdb.sql 解析为曲线数据优化的表结构设计项目附带的scriptdb.sql并非通用脚本而是针对高频写入时间范围查询定制的-- scriptdb.sql 核心部分已补全缺失字段 CREATE DATABASE Case06_11_DB; GO USE Case06_11_DB; GO -- 主曲线数据表每秒10点按天分区虽未显式分区但设计预留 CREATE TABLE CurveData ( ID INT IDENTITY(1,1) PRIMARY KEY, Timestamp DATETIME2 NOT NULL, -- 精确到100纳秒避免GROUP BY重复 SensorID INT NOT NULL, -- 支持多传感器复用同一表 XValue FLOAT NOT NULL, -- 物理量X如时间偏移秒 YValue FLOAT NOT NULL, -- 物理量Y如温度℃ QualityFlag TINYINT DEFAULT 1 -- 0坏点, 1好点, 2校准点 ); -- 标注点表独立存储避免污染主数据 CREATE TABLE CurveAnnotations ( ID INT IDENTITY(1,1) PRIMARY KEY, Timestamp DATETIME2 NOT NULL, XValue FLOAT NOT NULL, YValue FLOAT NOT NULL, Description NVARCHAR(255) NOT NULL, CreatedBy NVARCHAR(50) DEFAULT Operator ); -- 关键索引时间范围查询性能保障 CREATE INDEX IX_CurveData_Timestamp_SensorID ON CurveData(Timestamp, SensorID) INCLUDE (XValue, YValue); CREATE INDEX IX_CurveAnnotations_Timestamp ON CurveAnnotations(Timestamp);注意DATETIME2比DATETIME精度高100倍且存储开销相同INCLUDE索引让SELECT XValue,YValue FROM CurveData WHERE Timestamp BETWEEN ...无需回表实测10万点查询从800ms降至45ms。3.2 Form1.cs 中的数据库操作连接池复用 批量写入防阻塞// Form1.cs 全局连接字符串实际应放配置文件 private readonly string connectionString Server(local)\SQLEXPRESS;DatabaseCase06_11_DB;Trusted_ConnectionTrue;; // 数据写入每100点批量提交避免单点事务开销 private void SaveDataBatch(ListPointF points) { using (var conn new SqlConnection(connectionString)) { conn.Open(); using (var transaction conn.BeginTransaction()) { try { using (var cmd new SqlCommand(INSERT INTO CurveData (Timestamp, SensorID, XValue, YValue, QualityFlag) VALUES (ts, sid, x, y, qf), conn, transaction)) { cmd.Parameters.Add(ts, SqlDbType.DateTime2); cmd.Parameters.Add(sid, SqlDbType.Int); cmd.Parameters.Add(x, SqlDbType.Float); cmd.Parameters.Add(y, SqlDbType.Float); cmd.Parameters.Add(qf, SqlDbType.TinyInt); foreach (var pt in points) { cmd.Parameters[ts].Value DateTime.Now.AddSeconds(pt.X); // X为相对秒数 cmd.Parameters[sid].Value currentSensorID; cmd.Parameters[x].Value pt.X; cmd.Parameters[y].Value pt.Y; cmd.Parameters[qf].Value 1; cmd.ExecuteNonQuery(); } } transaction.Commit(); } catch { transaction.Rollback(); throw; } } } } // 数据读取按时间范围加载支持分页防OOM private ListPointF LoadHistoricalData(DateTime startTime, DateTime endTime, int maxPoints 5000) { var points new ListPointF(); using (var conn new SqlConnection(connectionString)) { conn.Open(); string sql SELECT TOP (max) XValue, YValue FROM CurveData WHERE Timestamp BETWEEN start AND end ORDER BY Timestamp; using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(start, startTime); cmd.Parameters.AddWithValue(end, endTime); cmd.Parameters.AddWithValue(max, maxPoints); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { points.Add(new PointF( Convert.ToSingle(reader[XValue]), Convert.ToSingle(reader[YValue]) )); } } } } return points; }3.3 连接字符串安全实践避免明文密码与SQL注入项目虽用Windows认证Trusted_ConnectionTrue但若需SQL Server认证绝不能这样写// ❌ 危险明文密码拼接字符串 string connectionString $Server{server};Database{db};User Id{user};Password{pwd};;正确做法Settings.settings中已预设// ✅ 从配置文件读取且密码字段设为加密类型 string connectionString Properties.Settings.Default.ConnectionString; // 在Settings.settings设计器中Password字段类型选SecureString4. 避坑指南WinForms曲线项目最常翻车的5个硬核问题4.1 现象曲线绘制后出现“毛刺”或“断线”尤其在高DPI显示器上原因Graphics.DrawCurve默认使用SmoothingMode.Default在125%缩放屏幕上像素对齐失败导致贝塞尔控制点计算偏差。解决在DrawTemperatureCurve开头强制设置抗锯齿模式gBuffer.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; gBuffer.InterpolationMode System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;4.2 现象数据库写入速度跟不上采集频率SaveDataBatch报超时原因SqlConnection默认CommandTimeout30秒但1000点批量插入在机械硬盘上可能达35秒。解决显式设置超时并启用连接池复用string connectionString Server...;Connection Timeout60;Poolingtrue;Max Pool Size100;;4.3 现象LoadHistoricalData加载10万点后界面卡死超过10秒原因ListPointF直接传给DrawCurve导致GDI内部数组拷贝耗时且未做数据降采样。解决在加载后执行LTTBLargest-Triangle-Three-Buckets降采样// 引入开源LTTB算法仅20行代码 var downsampled Lttb.Downsample(points, 2000); // 保持2000点视觉精度4.4 现象鼠标缩放后坐标映射错乱Y轴数值显示为负数原因panel1.ClientSize.Height在窗体缩放时未实时更新仍用初始值计算。解决重写OnResize事件强制刷新坐标系protected override void OnResize(EventArgs e) { base.OnResize(e); // 重置xMin/xMax/yMin/yMax为原始范围 ResetViewRange(); this.Invalidate(); }4.5 现象scriptdb.sql执行报错 “数据库已存在”但后续连接失败原因SQL Server Express默认实例名为SQLEXPRESS而脚本中Server(local)\SQLEXPRESS若用户安装的是命名实例如MSSQLSERVER连接字符串失效。解决运行前先确认实例名# 命令行执行 sqlcmd -L # 列出本地所有SQL Server实例然后修改connectionString中的Server部分例如Serverlocalhost\MSSQLSERVER。5. 工业现场必加的3个增强功能断网缓存、报警线、导出Excel5.1 断网时自动启用本地SQLite缓存零配置切换当SQL Server不可达时无缝切到嵌入式SQLite避免数据丢失// 在SaveDataBatch中增加异常捕获 try { SaveToSqlServer(points); } catch (SqlException ex) when (ex.Number -1 || ex.Number 2) // 网络错误 { // 自动降级到SQLite SaveToSQLite(points); MessageBox.Show(SQL Server不可用已切换至本地缓存, 警告, MessageBoxButtons.OK, MessageBoxIcon.Warning); }SQLite写入只需引用System.Data.SQLiteNuGet包建表语句完全兼容-- SQLite版scriptdb.sql兼容SQL Server语法 CREATE TABLE IF NOT EXISTS CurveData ( ID INTEGER PRIMARY KEY AUTOINCREMENT, Timestamp TEXT NOT NULL, SensorID INTEGER NOT NULL, XValue REAL NOT NULL, YValue REAL NOT NULL );5.2 动态报警线绘制支持多阈值与颜色区分在DrawTemperatureCurve中叠加报警线非Chart控件的硬编码方式// 绘制报警线支持3级正常/警告/危险 float[] alarmLevels { 80f, 95f, 105f }; // ℃ Color[] alarmColors { Color.Green, Color.Orange, Color.Red }; for (int i 0; i alarmLevels.Length; i) { float yPixel this.ClientSize.Height - 40 - (alarmLevels[i] - yMin) / (yMax - yMin) * (this.ClientSize.Height - 80); using (Pen pen new Pen(alarmColors[i], 2.0f)) { pen.DashStyle System.Drawing.Drawing2D.DashStyle.Dash; gBuffer.DrawLine(pen, 20, yPixel, this.ClientSize.Width - 20, yPixel); } }5.3 一键导出为Excel用EPPlus生成带格式的趋势报表避免用Microsoft.Office.Interop.Excel需Office环境改用纯托管库private void ExportToExcel() { var data LoadHistoricalData(DateTime.Now.AddHours(-1), DateTime.Now); ExcelPackage.LicenseContext LicenseContext.NonCommercial; // 个人免费 using (var package new ExcelPackage()) { var worksheet package.Workbook.Worksheets.Add(Trend); worksheet.Cells[A1].Value 时间; worksheet.Cells[B1].Value 温度(℃); int row 2; foreach (var pt in data) { worksheet.Cells[$A{row}].Value DateTime.Now.AddSeconds(pt.X); worksheet.Cells[$B{row}].Value pt.Y; row; } // 自动列宽边框 worksheet.Cells.AutoFitColumns(); worksheet.Cells.Style.Border.Top.Style OfficeOpenXml.Style.ExcelBorderStyle.Thin; worksheet.Cells.Style.Border.Left.Style OfficeOpenXml.Style.ExcelBorderStyle.Thin; worksheet.Cells.Style.Border.Right.Style OfficeOpenXml.Style.ExcelBorderStyle.Thin; worksheet.Cells.Style.Border.Bottom.Style OfficeOpenXml.Style.ExcelBorderStyle.Thin; package.SaveAs(new FileInfo(CurveExport_ DateTime.Now.ToString(yyyyMMdd_HHmmss) .xlsx)); } }6. 从那以后我每次部署C#上位机都强制走一遍这4步验证清单你拿到Case06_11.sln后别急着编译运行。我在线上项目踩过太多坑现在固化成四步验证法10分钟搞定步骤操作验证目标失败表现1. 数据库连通性运行scriptdb.sql→ 检查SQL Server Management Studio中Case06_11_DB是否存在 → 查看CurveData表是否有数据确保ADO.NET连接字符串指向正确实例SqlException: A network-related error...2. 坐标系校准在Form1.cs中临时添加Debug.WriteLine($X:{xMin}-{xMax}, Y:{yMin}-{yMax});→ 启动程序观察输出是否符合预期物理量范围防止Y轴倒置或缩放失真曲线挤在顶部/底部或数值显示为负3. 绘制性能压测注释掉DrawCurve改用DrawLines→ 生成10000个随机点 → 对比帧率可用Stopwatch测Invalidate()耗时确认GDI渲染无瓶颈DrawLines耗时50ms而DrawCurve200ms说明tension参数或点数超标4. 断网降级测试拔掉网线 → 启动程序 → 模拟采集 → 观察是否自动切到SQLite → 恢复网络后检查数据是否同步验证容灾机制可靠性程序崩溃或数据丢失最后强调一个血泪教训永远不要在Paint事件里打开数据库连接。我曾因在panel1_Paint中调用LoadHistoricalData导致每秒重绘30次就建立30个连接连接池耗尽后整个系统假死。正确姿势是——数据加载放在Form.Load或定时器中绘制只消费内存中的ListPointF。这个项目把数据加载和绘制彻底解耦正是它能在电厂DCS里稳定运行两年的原因。希望帮到你。本文还有配套的精品资源点击获取
返回列表