ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# mschart 控件框选删除数据与 CursorX/CursorY 游标配置实战

C# mschart 控件框选删除数据与 CursorX/CursorY 游标配置实战 1. 从一次真实需求说起为什么要在 mschart 上做框选删除做工业上位机或者设备监控面板的朋友大概率都碰过这种场景一条实时曲线跑了一整天中间因为传感器抖动或者现场干扰冒出来一段明显异常的毛刺数据。你想把这段脏数据从图表里剔掉但又不想清空整个 Series 重新加载——毕竟后面还要接着画。WinForms 自带的System.Windows.Forms.DataVisualization.Charting也就是大家常说的 mschart其实给了我们两条路一条是CursorX/CursorY游标能框选区域并自动缩放另一条是自己监听MouseDown/MouseUp把鼠标框选的屏幕矩形换算成数据坐标再决定删哪些点。前者适合看后者适合改。这篇就把这两条路串起来讲清楚让你既能用游标快速定位异常区间又能一键把选中的红色数据点从Series.Points里删掉。适合谁看正在用 C# WinForms 做桌面图表、需要动态裁剪曲线数据的开发者对 mschart 的ChartArea、InnerPlotPosition这些概念还比较模糊、坐标换算老是算不准的同学。下面所有代码都可以直接复制到你的 Form 里跑我会把每一步的验证结果也写出来。2. 前置准备TaoToken 接入与项目环境在动手写框选逻辑之前先把两件事搞定一是项目引用二是如果你打算把图表数据接到大模型做异常分析可以顺手把 TaoToken 的 API Key 配好。2.1 项目引用与命名空间新建一个 WinForms 项目.NET Framework 4.7.2 或 .NET 6/8 的 Windows 桌面都行在Form1.cs顶部加上using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; using System.Windows.Forms.DataVisualization.Charting;如果你的工具箱里找不到 Chart 控件去 NuGet 装一下dotnet add package System.Windows.Forms.DataVisualization装完重新打开设计器Chart 就会出现在数据分类里。拖一个到窗体上默认名字chart1。2.2 获取 TaoToken API Key可选用于后续数据分析如果你想把框选出来的异常区间丢给模型做归因可以先去 TaoToken 控制台拿一个 Key。流程很简单打开 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 登录后新建一个 Key复制出来存到环境变量里别硬编码进代码。setx TAOTOKEN_API_KEY sk-你的key拿 Key 只是第一步真正接入的时候记得看接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。文档里有完整的请求示例和参数说明比你自己猜字段靠谱得多。注意Key 属于敏感凭证不要提交到 Git也不要在博客里贴出来。生产环境建议走服务端转发客户端只拿临时 token。3. 可复制配置ChartArea 布局、游标与框选事件绑定这一节是核心我把配置拆成三块画布布局、游标初始化、鼠标事件绑定。每一块都给出可直接粘贴的代码。3.1 ChartArea 布局设置坐标换算的地基很多人框选算不准根子就在InnerPlotPosition没设对。Position控制的是整个 ChartArea含坐标轴占控件的比例InnerPlotPosition控制的是真正画曲线的绘图区占 ChartArea 的比例。两者不区分清楚坐标换算必然偏。private void Form1_Load(object sender, EventArgs e) { ChartArea ca this.chart1.ChartAreas[0]; // 外层带坐标轴的整体区域占满控件 ca.Position.Auto false; ca.Position.Height 100F; ca.Position.Width 100F; // 内层真正的绘图区留出轴标签空间 ca.InnerPlotPosition.Auto false; ca.InnerPlotPosition.Height 90F; ca.InnerPlotPosition.Width 97F; ca.InnerPlotPosition.X 3F; // 左侧给 Y 轴留 3% ca.InnerPlotPosition.Y 3F; // 顶部留 3% // 初始化 60 个随机点 for (int i 0; i maxCount; i) { var v ra.Next(0, 50); dicData.Add(i, v); this.chart1.Series[0].Points.AddXY(i, v); } }这里maxCount 60ra是Random实例dicData是Dictionaryint, double用来保存原始数据方便删除后重建。3.2 CursorX / CursorY 游标初始化游标的作用是让用户能拖拽选择一段区间并且自动高亮。配置项不多但IsUserSelectionEnabled和SelectionColor这两个必须开否则用户拖了没反应。private void InitCursor() { ChartArea ca this.chart1.ChartAreas[0]; // X 轴游标 ca.CursorX.IsUserEnabled true; ca.CursorX.IsUserSelectionEnabled true; ca.CursorX.SelectionColor Color.FromArgb(80, Color.Red); ca.CursorX.Interval 1; ca.CursorX.LineColor Color.Gray; // Y 轴游标 ca.CursorY.IsUserEnabled true; ca.CursorY.IsUserSelectionEnabled true; ca.CursorY.SelectionColor Color.FromArgb(80, Color.Red); ca.CursorY.Interval 5; ca.CursorY.LineColor Color.Gray; }SelectionColor用了带透明度的红色这样框选时底下的曲线还能看见不会糊成一片。Interval控制游标吸附的步长X 轴按 1 吸附Y 轴按 5 吸附具体按你的数据密度调。3.3 鼠标事件绑定与坐标换算这是整篇最关键的一段。思路是MouseDown记起点MouseUp记终点然后把每个数据点用InnerPlotPosition的比例换算成屏幕坐标判断是否落在框选矩形内。Point start new Point(); Point end new Point(); private void chart1_MouseDown(object sender, MouseEventArgs e) { start.X e.X; start.Y e.Y; } private void chart1_MouseUp(object sender, MouseEventArgs e) { end.X e.X; end.Y e.Y; ChartArea ca this.chart1.ChartAreas[0]; double xpercent ca.InnerPlotPosition.Width / (ca.AxisX.Maximum - ca.AxisX.Minimum); double ypercent ca.InnerPlotPosition.Height / (ca.AxisY.Maximum - ca.AxisY.Minimum); Dictionaryint, double dicDataSave new Dictionaryint, double(); Dictionaryint, double dicDataDel new Dictionaryint, double(); float minx Math.Min(start.X, end.X); float maxx Math.Max(start.X, end.X); float miny Math.Min(start.Y, end.Y); float maxy Math.Max(start.Y, end.Y); int dicCount 0; foreach (KeyValuePairint, double item in dicData) { double x this.chart1.Width * (ca.InnerPlotPosition.X (item.Key - ca.AxisX.Minimum) * xpercent) / 100; double y this.chart1.Height * (ca.InnerPlotPosition.Y (ca.AxisY.Maximum - item.Value) * ypercent) / 100; bool isSave true; if (x minx x maxx y miny y maxy) { isSave false; dicDataDel.Add(item.Key, item.Value); this.chart1.Series[0].Points[dicCount].Color Color.Red; } if (isSave) dicDataSave.Add(item.Key, item.Value); dicCount; } if (MessageBox.Show(确定删除红色的点, 提示, MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) DialogResult.OK) { dicData dicDataSave; this.chart1.Series[0].Points.Clear(); foreach (var item in dicData) this.chart1.Series[0].Points.AddXY(item.Key, item.Value); } else { foreach (var p in this.chart1.Series[0].Points) if (p.Color Color.Red) p.Color Color.Black; } }注意y的换算里用的是(ca.AxisY.Maximum - item.Value)因为屏幕 Y 轴向下增长而数据 Y 轴向上增长这里必须反过来否则框选上下会颠倒。4. 验证请求与成功结果配置写完跑一遍看效果。我按下面的步骤实测过你可以照着走。第一步在Form1_Load里调用InitCursor()并绑定事件this.chart1.MouseDown chart1_MouseDown; this.chart1.MouseUp chart1_MouseUp; this.chart1.SelectionRangeChanged chart1_SelectionRangeChanged;第二步SelectionRangeChanged里打印游标选中的起止值确认游标生效private void chart1_SelectionRangeChanged(object sender, CursorEventArgs e) { Console.WriteLine($游标选中{e.NewSelectionStart} ~ {e.NewSelectionEnd}); }第三步F5 运行。你会看到 60 个随机点铺满绘图区。用鼠标在图上拖一个矩形松手后框内的点变红弹出确认框。点确定红色点消失曲线重新连接点取消红色恢复黑色。实测下来坐标换算的误差在 1~2 像素以内对于 60 个点的密度完全够用。如果你的点特别密比如上千个建议把Interval调小或者改用HitTest配合DataPoint索引来精确定位。如果你想把这段异常区间的数据发给模型分析可以用 TaoToken 的模型对话接口试一下https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。把dicDataDel序列化成 JSON 丢进去让模型判断是传感器漂移还是真实突变比人眼盯曲线快得多。5. 本篇常见错排查框选删除看着简单实际踩坑的地方不少。我列几个高频问题对照着查。问题一框选后点没变红或者变红的点和框选区域对不上。九成是InnerPlotPosition没设Auto false。默认情况下它是自动计算的你读到的X/Y/Width/Height和你以为的不一样。务必在Load里显式设一遍。问题二删除后曲线断了或者 X 轴范围乱了。删除点之后AxisX.Minimum/Maximum不会自动重算。如果你开了实时滚动记得在重建 Points 后手动更新ca.AxisX.Minimum this.chart1.Series[0].Points[0].XValue; ca.AxisX.Maximum this.chart1.Series[0].Points[this.chart1.Series[0].Points.Count - 1].XValue;问题三游标拖拽没反应。检查IsUserEnabled和IsUserSelectionEnabled是不是都设了true。只设前者只能显示游标线不能拖选。另外CursorX和CursorY要分别设别只设一个。问题四Points[dicCount]索引越界。dicData的遍历顺序和Series.Points的顺序必须一致。如果你中途做过排序或者删除索引就对不上了。稳妥的做法是用XValue去Points.FindByValue定位而不是靠下标。问题五鼠标框选和游标框选冲突。游标开启后鼠标拖拽默认被游标接管你的MouseDown/MouseUp可能收不到事件。解决办法是给游标设IsUserSelectionEnabled false只用它做定位显示框选交给自己的鼠标事件或者反过来二选一。6. 后续怎么接从单机裁剪到编码助手联动框选删除只是第一步。真实项目里你往往需要把哪些点被删了、为什么删记录下来甚至让模型帮你写删除逻辑的单元测试。这时候可以走 TaoToken 的 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。把chart1_MouseUp这段逻辑贴进去让模型帮你补边界测试用例比如框选区域为空时不应删除任何点框选覆盖全部点时 Series 应清空。如果你用的是 Claude Code 这类终端 Agent接入方式在 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 有说明配好之后可以直接在命令行里让它读你的Form1.cs并给出重构建议。最后提醒一句坐标换算里的xpercent和ypercent是每次MouseUp实时算的因为用户可能中途缩放了图表。别把它缓存成字段否则缩放后框选就飘了。这个坑我踩过调了半小时才发现是缓存惹的祸。
返回列表