
简介本资源是一份面向C#初学者与中级开发者的图形可视化实践项目聚焦Windows Forms环境下曲线绘制与SQL Server数据库协同应用解决数据驱动图形展示这一典型工程问题。压缩包共25个文件含6个核心C#源码文件如Form1.cs、Program.cs、1个SQL建库脚本scriptdb.sql、1个解决方案文件Case06_11.sln及配套编译产物exe、dll、pdb等整体仅50KB轻量易部署适合快速理解GDI绘图流程与ADO.NET数据交互逻辑。已有253人学习下载。读者可直接运行可执行程序观察动态曲线渲染效果通过源码掌握Pen/DrawCurve等绘图API用法结合SQL脚本理解曲线点坐标存取设计并借助完整项目结构含resx资源、settings配置、csproj工程定义建立标准WinForms开发认知框架。1. C#绘制曲线示例源码含SQL数据库不是画图而是把实时工况数据“活”起来你手头有一台PLC采集的温度、压力、电流数据存进了SQL Server或者产线MES系统每5秒写入一条设备振动幅值又或者你在做储能系统的SOC-电压校准实验需要把几十组实测点连成平滑曲线验证模型——这时候光靠Excel截图发邮件不行。用户要的是双击exe就能看到带坐标轴、可缩放、能拖拽、数据从数据库实时刷新的曲线图。这个标题里的“C#绘制曲线示例源码含SQL数据库”核心不是教你怎么用Graphics.DrawLine画线而是提供一套可嵌入工业上位机、支持SQL直连、带基础交互能力的曲线可视化最小可行方案。它面向的是C#初级工程师、自动化集成商、设备厂商的软件支持岗——不需要你懂WPF动画原理但要求你能改3个连接字符串、换2个字段名、加1个查询条件就让曲线跑起来。后面章节会拆解为什么选Chart控件而非第三方库、SQL查询如何避免阻塞UI线程、时间戳字段怎么自动适配X轴、以及——最常翻车的“曲线不显示”背后90%是数据类型或空值惹的祸。2. 用Windows Forms Chart控件搭建最小曲线框架5分钟跑通本地SQL数据Windows Forms Chart控件是.NET Framework自带的轻量级绘图组件无需NuGet安装、不依赖OpenGL、兼容.NET Core 3.1通过System.Windows.Forms.DataVisualization对工业现场老旧Win7/Win10系统友好。它比WPF的OxyPlot学习成本低比第三方库如LiveCharts更可控——尤其当你需要把曲线控件嵌进现有WinForms主窗体时Chart控件的设计器拖拽属性面板配置能省掉3小时调试布局。2.1 创建项目并添加Chart控件新建一个Windows Forms App (.NET Framework)项目注意选.NET Framework而非.NET Core因Chart控件在.NET Core中需额外引用System.Windows.Forms.DataVisualization包且设计器支持弱。在Form设计器中从工具箱拖入一个Chart控件到窗体命名为chartMain。右键控件 → “属性” → 找到Dock属性设为Fill使其铺满窗体。提示不要用ChartArea的默认名称ChartArea1在属性面板中将其Name改为caMain。后续代码中所有区域操作都基于此名称避免因设计器自动生成名称变更导致运行时异常。2.2 配置ChartArea与Series坐标轴、图例、样式一步到位Chart控件的核心是ChartArea绘图区和Series数据系列。以下代码在Form_Load事件中初始化private void Form1_Load(object sender, EventArgs e) { // 1. 清空默认Series设计器可能已生成 chartMain.Series.Clear(); // 2. 配置ChartArea var ca chartMain.ChartAreas[caMain]; ca.AxisX.Title 时间 (秒); ca.AxisY.Title 温度 (℃); ca.AxisX.MajorGrid.LineColor Color.LightGray; ca.AxisY.MajorGrid.LineColor Color.LightGray; ca.AxisX.LabelStyle.Format 0.0; // X轴标签保留1位小数 ca.AxisY.LabelStyle.Format 0.00; // Y轴标签保留2位小数 // 3. 创建Series并绑定到ChartArea var series new Series(Temperature); series.ChartType SeriesChartType.Line; // 折线图 series.BorderWidth 2; // 线宽 series.Color Color.FromArgb(255, 79, 129, 189); // 蓝色系 series.ShadowOffset 1; // 微阴影增强立体感 series.XValueType ChartValueType.Double; // X轴为数值型时间戳转double series.YValueType ChartValueType.Double; // Y轴为数值型 chartMain.Series.Add(series); }这段代码做了三件事清空冗余Series避免叠加混乱、设置坐标轴标题和网格线提升可读性、创建名为Temperature的折线系列并指定颜色与线宽。关键参数说明ChartType SeriesChartType.Line明确指定为折线图而非柱状图或散点图XValueType/YValueType必须显式声明为Double否则从数据库读取的decimal或float会触发类型转换异常ShadowOffset 1微小阴影让曲线在浅色背景上更易识别工业屏常用技巧。2.3 从SQL Server读取数据并绑定到Series避免UI线程阻塞的正确姿势直接在Form_Load里执行SqlDataAdapter.Fill()会导致界面卡死。正确做法是使用BackgroundWorker或Task.Run异步加载。以下是安全的异步填充方案private async void Form1_Load(object sender, EventArgs e) { await LoadDataAsync(); } private async Task LoadDataAsync() { string connectionString Data Source.;Initial CatalogCurveDB;Integrated Securitytrue;; string query SELECT TOP 1000 [Timestamp], [Temperature] FROM [SensorData] ORDER BY [Timestamp]; try { await Task.Run(() { using (var conn new SqlConnection(connectionString)) { conn.Open(); using (var cmd new SqlCommand(query, conn)) using (var adapter new SqlDataAdapter(cmd)) { var dt new DataTable(); adapter.Fill(dt); // 数据库查询在此执行 // 主线程更新UI必须 this.Invoke((MethodInvoker)delegate { BindDataToChart(dt); }); } } }); } catch (Exception ex) { MessageBox.Show($数据加载失败{ex.Message}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void BindDataToChart(DataTable dt) { var series chartMain.Series[Temperature]; series.Points.Clear(); // 清空旧数据点 foreach (DataRow row in dt.Rows) { double x Convert.ToDouble(row[Timestamp]); // 假设Timestamp是秒级时间戳 double y Convert.ToDouble(row[Temperature]); series.Points.AddXY(x, y); } // 自动调整坐标轴范围 chartMain.ChartAreas[caMain].AxisX.Minimum dt.Rows.Count 0 ? Convert.ToDouble(dt.Rows[0][Timestamp]) : 0; chartMain.ChartAreas[caMain].AxisX.Maximum dt.Rows.Count 0 ? Convert.ToDouble(dt.Rows[dt.Rows.Count - 1][Timestamp]) : 100; }逻辑说明await Task.Run(...)将耗时的数据库IO移出UI线程this.Invoke(...)确保BindDataToChart在UI线程执行避免跨线程调用异常series.Points.Clear()防止重复加载时数据点叠加AxisX.Minimum/Maximum手动设置范围避免Chart自动缩放导致首尾数据被裁剪——这是工业场景常见需求如查看整班次数据不能只显示局部峰值。3. SQL数据库设计与查询优化让曲线数据“准、快、稳”曲线绘制的源头是数据库。一个设计不良的表结构会让查询变慢、数据不准、甚至无法绘图。本节以实际工业场景为例给出可直接复用的建表脚本与查询策略。3.1 工业传感器数据表结构时间戳、多通道、状态标记缺一不可-- 创建数据库 CREATE DATABASE CurveDB; GO USE CurveDB; GO -- 创建传感器数据表 CREATE TABLE SensorData ( ID BIGINT IDENTITY(1,1) PRIMARY KEY, Timestamp DATETIME2 NOT NULL, -- 推荐用DATETIME2(3)精度毫秒兼容性好 Temperature DECIMAL(9,3) NULL, -- 温度允许NULL传感器偶发断线 Pressure DECIMAL(9,3) NULL, -- 压力 Current DECIMAL(9,3) NULL, -- 电流 Status TINYINT NOT NULL DEFAULT 0, -- 0正常, 1超限, 2故障 CreatedTime DATETIME2 DEFAULT GETUTCDATE() -- 记录入库时间用于排查延迟 ); -- 添加索引按时间查询是高频操作 CREATE INDEX IX_SensorData_Timestamp ON SensorData(Timestamp DESC); GO -- 插入测试数据模拟1000条每秒1条 DECLARE i INT 0; WHILE i 1000 BEGIN INSERT INTO SensorData (Timestamp, Temperature, Pressure, Current, Status) VALUES ( DATEADD(SECOND, i, 2024-01-01 00:00:00), ROUND(20 SIN(i * 0.05) * 10 RAND() * 2, 3), -- 模拟正弦波动噪声 ROUND(100 COS(i * 0.03) * 20, 3), ROUND(5 (i % 100) * 0.1, 3), CASE WHEN i % 200 0 THEN 1 ELSE 0 END ); SET i i 1; END关键设计点说明DATETIME2(3)替代DATETIME精度更高毫秒级存储更紧凑且无DATETIME的2038年问题DECIMAL(9,3)替代FLOAT避免浮点数精度丢失如0.10.2≠0.3工业数据必须精确Status字段为后续曲线着色提供依据如超限时Y轴标红IX_SensorData_Timestamp索引按时间倒序使ORDER BY Timestamp DESC查询速度提升10倍以上实测100万行数据查询最新1000条从2.3s降至0.15s。3.2 查询语句的3个避坑写法别让SQL拖垮曲线刷新场景错误写法正确写法原因查最新N条SELECT TOP 1000 * FROM SensorData ORDER BY ID DESCSELECT TOP 1000 * FROM SensorData ORDER BY Timestamp DESCID递增不代表时间递增批量插入、主从同步延迟必须用Timestamp排序查某时段数据WHERE Timestamp 2024-01-01 AND Timestamp 2024-01-02WHERE Timestamp 2024-01-01T00:00:00.000 AND Timestamp 2024-01-02T00:00:00.000字符串比较易出错用ISO8601格式左闭右开区间避免边界时间点遗漏多通道合并查询SELECT Timestamp, Temperature, Pressure FROM SensorDataSELECT Timestamp, ISNULL(Temperature, 0) AS Temperature, ISNULL(Pressure, 0) AS Pressure FROM SensorDataNULL值传入Convert.ToDouble()会抛异常ISNULL提前转为0曲线不断点注意ISNULL在SQL Server中效率高于COALESCE且语法更简洁。若需保留NULL语义如表示传感器未启用则应在C#层判断row[Temperature] DBNull.Value ? 0 : Convert.ToDouble(row[Temperature])。4. 曲线绘制的5个致命避坑指南90%的“不显示”都是这几点曲线控件不显示、数据点错位、坐标轴乱跳……这些问题看似随机实则高度规律。以下是我在3个工业项目中踩过的坑按发生频率排序每条附带现象、根因与一招解决。4.1 现象曲线完全空白但Series.Points.Count显示有数据原因Series.XValueType或YValueType未设置或设置为ChartValueType.Auto。Chart控件无法自动推断数值类型将所有点视为字符串导致X轴按字典序排列如1,10,2变成1,10,2而非1,2,10Y轴无法映射到坐标系。解决在创建Series后立即显式设置XValueType ChartValueType.Double; YValueType ChartValueType.Double;哪怕数据是int也强制设为Double。4.2 现象X轴显示为1.00E06等科学计数法且刻度密集难读原因X轴数据范围过大如时间戳用DateTime.ToOADate()得到大数值Chart控件自动启用科学计数法且默认刻度间隔不合理。解决在BindDataToChart中添加X轴格式化chartMain.ChartAreas[caMain].AxisX.LabelStyle.Format 0.0; chartMain.ChartAreas[caMain].AxisX.Interval (maxX - minX) / 10; // 强制10个主刻度 // 若X为时间戳改用DateTime格式chartMain.ChartAreas[caMain].AxisX.LabelStyle.Format HH:mm:ss;4.3 现象曲线显示但严重抖动像心电图而非平滑趋势原因数据库中存在NULL或极值异常点如-9999未清洗直接绘图。Chart控件会将NULL渲染为0将极值拉伸坐标轴。解决在SQL查询中过滤异常值SELECT Timestamp, Temperature FROM SensorData WHERE Temperature BETWEEN 0 AND 100 -- 根据物理量程设定合理范围 AND Temperature IS NOT NULL ORDER BY Timestamp;4.4 现象切换不同传感器通道时曲线重叠不清图例文字被截断原因多个Series共用同一ChartArea但未设置LegendText或Legend位置未调整。解决为每个Series设置唯一图例名并调整图例位置series.LegendText 温度(℃); chartMain.Legends[Default].Docking Docking.Top; // 图例停靠顶部 chartMain.Legends[Default].Alignment StringAlignment.Center;4.5 现象程序启动后曲线正常但几分钟后CPU飙升至100%界面冻结原因启用了Chart.Update()循环刷新但未控制刷新频率或在Timer.Tick中未Dispose旧Series。解决禁用自动刷新改用受控定时器private Timer refreshTimer new Timer { Interval 5000 }; // 5秒刷新一次 private void refreshTimer_Tick(object sender, EventArgs e) { // 先Clear再Add避免内存泄漏 chartMain.Series[Temperature].Points.Clear(); // ... 重新查询并绑定数据 } // 启动时启用refreshTimer.Start(); // 关闭时停用refreshTimer.Stop();5. 实现动态曲线与多通道联动让曲线真正服务生产决策工业场景中静态曲线只是起点。真正的价值在于当操作员点击“压力”曲线时自动高亮对应时间点的“温度”和“电流”值当检测到连续10个点超限自动弹窗告警并保存截图当用户拖拽X轴范围后台SQL自动查询该区间数据——这些能力让曲线从“图表”升级为“决策界面”。本节给出3个可直接复用的进阶技巧。5.1 响应鼠标事件点击曲线获取精确数据点Chart控件提供HitTest方法可将屏幕坐标映射到数据点。以下代码实现点击任意位置显示最近点的原始值private void chartMain_MouseClick(object sender, MouseEventArgs e) { var result chartMain.HitTest(e.X, e.Y); if (result.ChartElementType ChartElementType.DataPoint) { var dp result.Object as DataPoint; var series dp.Series; string msg $时间: {dp.XValue:F3}s, {series.Name}: {dp.YValues[0]:F3}; MessageBox.Show(msg, 数据点详情, MessageBoxButtons.OK, MessageBoxIcon.Information); } }提示HitTest返回ChartElementType.DataPoint时result.Object才是DataPoint若返回ChartElementType.AxisX或ChartElementType.PlottingArea说明没点中数据点。此功能在故障溯源时极有用——操作员直接点报警时刻立刻看到三参数值。5.2 多Series联动一个查询驱动多条曲线工业数据常需对比分析。例如储能系统放电时需同时看“电池电压”、“放电电流”、“环境温度”。传统做法是写3个SQL查询效率低且时间点难对齐。正确做法是单次查询返回多列用DataBindXY一次性绑定// 查询返回Timestamp, Voltage, Current, Temp三列 string query SELECT TOP 500 Timestamp, Voltage, Current, Temp FROM BatteryLog WHERE DeviceID deviceID ORDER BY Timestamp; using (var cmd new SqlCommand(query, conn)) { cmd.Parameters.AddWithValue(deviceID, BAT-001); var dt new DataTable(); new SqlDataAdapter(cmd).Fill(dt); // 一次性绑定多Series chartMain.Series[Voltage].Points.DataBindXY( dt.Rows, Timestamp, dt.Rows, Voltage); chartMain.Series[Current].Points.DataBindXY( dt.Rows, Timestamp, dt.Rows, Current); chartMain.Series[Temp].Points.DataBindXY( dt.Rows, Timestamp, dt.Rows, Temp); }DataBindXY比循环AddXY快5倍以上实测500点耗时从12ms降至2.3ms且保证所有Series的时间戳严格对齐——这是多参数联合分析的基础。5.3 动态缩放与区间查询让曲线自己“说话”用户常需放大查看某段细节。Chart控件支持ScaleView缩放但需配合SQL查询实现“所见即所得”private void chartMain_AxisViewChanged(object sender, ViewEventArgs e) { if (e.AxisName AxisX) // 仅响应X轴缩放 { double min e.Axis.ScaleView.ViewMinimum; double max e.Axis.ScaleView.ViewMaximum; // 构造区间查询SQL注意此处min/max是double型时间戳 string query $ SELECT Timestamp, Temperature FROM SensorData WHERE Timestamp {min} AND Timestamp {max} ORDER BY Timestamp; // 执行查询并重绘此处省略具体执行逻辑同2.3节 ReloadDataForRange(query); } }血泪经验ViewMinimum/ViewMaximum返回的是double对应DateTime.ToOADate()值。若数据库存的是DATETIME2需在SQL中转换WHERE Timestamp DATEADD(SECOND, {min * 86400}, 1899-12-30)。但更稳妥的做法是在C#层将double转回DateTime再拼SQL避免浮点误差。最后说个习惯我从不在Form_Load里写业务逻辑而是封装成InitializeChart()、LoadDatabaseConfig()、StartRealtimeMonitor()三个方法每个方法职责单一。这样当客户说“要把曲线改成每10秒刷新一次”我只需改StartRealtimeMonitor()里的Timer.Interval其他代码纹丝不动。希望帮到你。本文还有配套的精品资源点击获取