using DevExpress.Data.Linq.Helpers; using DevExpress.Utils; using DevExpress.XtraCharts; using DevExpress.XtraEditors; using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Windows.Forms; namespace KepGridEditor { public delegate void CalibrationResultHandler(double k, double b, string formula); public partial class FormFittingTool : XtraForm { // 三次多项式拟合算法辅助类(y = ax ^ 3 + bx ^ 2 + cx + d) public class CubicPolynomialFit { public double A { get; private set; } public double B { get; private set; } public double C { get; private set; } public double D { get; private set; } public void Fit(List x, List y) { int n = x.Count; if (n < 4) return; // 构造正规方程组 (Least Squares) double s0 = n; double s1 = x.Sum(); double s2 = x.Sum(v => v * v); double s3 = x.Sum(v => Math.Pow(v, 3)); double s4 = x.Sum(v => Math.Pow(v, 4)); double s5 = x.Sum(v => Math.Pow(v, 5)); double s6 = x.Sum(v => Math.Pow(v, 6)); double sy = y.Sum(); double sxy = x.Zip(y, (a, b) => a * b).Sum(); double sx2y = x.Zip(y, (a, b) => a * a * b).Sum(); double sx3y = x.Zip(y, (a, b) => Math.Pow(a, 3) * b).Sum(); // 4x4 矩阵 double[,] m = { { s0, s1, s2, s3 }, { s1, s2, s3, s4 }, { s2, s3, s4, s5 }, { s3, s4, s5, s6 } }; double[] r = { sy, sxy, sx2y, sx3y }; double[] res = SolveLinearSystem(m, r, 4); // 结果对应 d, c, b, a D = res[0]; C = res[1]; B = res[2]; A = res[3]; } public double Eval(double v) { return A * Math.Pow(v, 3) + B * Math.Pow(v, 2) + C * v + D; } public string GetFormula(string varName) { return $"{A:F8}*{varName}*{varName}*{varName} + {B:F8}*{varName}*{varName} + {C:F6}*{varName} + {D:F6}"; } private double[] SolveLinearSystem(double[,] matrix, double[] rightPart, int n) { double[] result = new double[n]; double[,] a = new double[n, n + 1]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) a[i, j] = matrix[i, j]; a[i, n] = rightPart[i]; } for (int i = 0; i < n; i++) { int maxRow = i; for (int k = i + 1; k < n; k++) if (Math.Abs(a[k, i]) > Math.Abs(a[maxRow, i])) maxRow = k; for (int k = i; k <= n; k++) { double tmp = a[maxRow, k]; a[maxRow, k] = a[i, k]; a[i, k] = tmp; } for (int k = i + 1; k < n; k++) { double c = -a[k, i] / a[i, i]; for (int j = i; j <= n; j++) { if (i == j) a[k, j] = 0; else a[k, j] += c * a[i, j]; } } } for (int i = n - 1; i >= 0; i--) { result[i] = a[i, n] / a[i, i]; for (int k = i - 1; k >= 0; k--) a[k, n] -= a[k, i] * result[i]; } return result; } } private BindingList PointDatas = new BindingList(); // 原始数据 private List TrendKnobsPoint = new List(); // 趋势控制点 (红色方块) // 交互状态 private bool IsDragging = false; private PointData EditingKnobPiont = null; // 当前拖拽的曲线控制点 private SeriesPoint DraggedVisualPoint = null; // 当前拖拽的线性手柄 // 相对位移计算 (解决出界问题) private int StartMouseY; private double StartDataY; private double PixelsPerUnitY; // 数据范围 private double meshZmin = 0, meshZmax = 0; private double pointZmin = 0, pointZmax = 0; // 拟合参数 private double LinearK = 1; private double LinearB = 0; private CubicPolynomialFit PolyFit = new CubicPolynomialFit(); public event CalibrationResultHandler OnCalibrationComplete; public event EventHandler OnRestore; public FormFittingTool() { InitializeComponent(); this.StartPosition = FormStartPosition.CenterParent; if (cboFitType != null) { cboFitType.Properties.Items.Clear(); cboFitType.Properties.Items.AddRange(new object[] { "直线拟合", "曲线拟合" }); cboFitType.SelectedIndex = 0; } InitializeChartConfiguration(); InitializeEvents(); } private void InitializeChartConfiguration() { chartControl.Series.Clear(); chartControl.RuntimeHitTesting = true; // 原始数据 (蓝色点) Series sOrg = new Series("原始数据", ViewType.Point); sOrg.DataSource = PointDatas; sOrg.ArgumentDataMember = "X"; sOrg.ValueDataMembers.AddRange("Y"); sOrg.FilterString = "[IsVirtual] = false"; PointSeriesView vOrg = (PointSeriesView)sOrg.View; vOrg.Color = Color.SteelBlue; vOrg.PointMarkerOptions.Kind = MarkerKind.Circle; vOrg.PointMarkerOptions.Size = 8; // 虚拟点 (橙色点) Series sVirt = new Series("虚拟点", ViewType.Point); sVirt.DataSource = PointDatas; sVirt.ArgumentDataMember = "X"; sVirt.ValueDataMembers.AddRange("Y"); sVirt.FilterString = "[IsVirtual] = true"; PointSeriesView vVirt = (PointSeriesView)sVirt.View; vVirt.Color = Color.Orange; vVirt.PointMarkerOptions.Kind = MarkerKind.Circle; vVirt.PointMarkerOptions.Size = 8; // 拟合线 (线本身) Series sLine = new Series("拟合线", ViewType.Line); sLine.ToolTipEnabled = DefaultBoolean.False; sLine.CrosshairEnabled = DefaultBoolean.False; LineSeriesView vLine = (LineSeriesView)sLine.View; vLine.LineStyle.Thickness = 2; vLine.MarkerVisibility = DefaultBoolean.False; // 控制手柄 (红色方块 - 用于交互) Series sKnobs = new Series("控制点", ViewType.Point); sKnobs.ShowInLegend = false; PointSeriesView vKnobs = (PointSeriesView)sKnobs.View; vKnobs.Color = Color.Red; vKnobs.PointMarkerOptions.Kind = MarkerKind.Square; vKnobs.PointMarkerOptions.Size = 12; vKnobs.PointMarkerOptions.BorderColor = Color.White; chartControl.Series.AddRange(new Series[] { sOrg, sVirt, sLine, sKnobs }); // 标题坐标轴 chartControl.Titles.Clear(); ChartTitle title = new ChartTitle(); title.Text = "拟合校正"; title.Font = new Font("宋体", 18, FontStyle.Bold); title.Dock = ChartTitleDockStyle.Top; chartControl.Titles.Add(title); if (chartControl.Diagram is XYDiagram diagram) { diagram.AxisX.Title.Text = "X图值"; diagram.AxisX.Title.Visibility = DefaultBoolean.True; diagram.AxisY.Title.Text = "Y井值"; diagram.AxisY.Title.Visibility = DefaultBoolean.True; diagram.EnableAxisXScrolling = true; diagram.EnableAxisYScrolling = true; } chartControl.ToolTipEnabled = DefaultBoolean.True; chartControl.Legend.VerticalIndent = 8; gridControl.DataSource = PointDatas; sOrg.ToolTipPointPattern = "{HINT}"; sVirt.ToolTipPointPattern = "{HINT}"; gridControl.ForceInitialize(); if (gridView.Columns["X"] != null) { gridView.Columns["X"].DisplayFormat.FormatType = DevExpress.Utils.FormatType.Numeric; gridView.Columns["X"].DisplayFormat.FormatString = "F2"; // F2 表示保留2位小数 } if (gridView.Columns["Y"] != null) { gridView.Columns["Y"].DisplayFormat.FormatType = DevExpress.Utils.FormatType.Numeric; gridView.Columns["Y"].DisplayFormat.FormatString = "F2"; // F2 表示保留2位小数 } gridView.OptionsSelection.MultiSelect = true; gridView.OptionsSelection.MultiSelectMode = DevExpress.XtraGrid.Views.Grid.GridMultiSelectMode.CheckBoxRowSelect; gridView.OptionsSelection.CheckBoxSelectorColumnWidth = 30; gridView.OptionsSelection.ShowCheckBoxSelectorInColumnHeader = DevExpress.Utils.DefaultBoolean.True; if (gridView.Columns["IsVirtual"] != null) gridView.Columns["IsVirtual"].Visible = false; } private void InitializeEvents() { PointDatas.ListChanged += (s, e) => chartControl.RefreshData(); btnCalibrate.Click += BtnCalibrate_Click; if (btnAddPoint != null) btnAddPoint.Click += BtnAddPoint_Click; if (btnRemovePoint != null) btnRemovePoint.Click += BtnRemovePoint_Click; if (btnRestore != null) btnRestore.Click += BtnRestore_Click; if (cboFitType != null) cboFitType.SelectedIndexChanged += (s, e) => PerformFitting(false); chartControl.MouseDown += ChartControl_MouseDown; chartControl.MouseMove += ChartControl_MouseMove; chartControl.MouseUp += ChartControl_MouseUp; chartControl.CustomDrawCrosshair += ChartControl_CustomDrawCrosshair; // 线性拖拽时手柄变红 chartControl.CustomDrawSeriesPoint += (s, e) => { if (cboFitType != null && cboFitType.SelectedIndex == 0 && e.Series.Name == "拟合线") e.SeriesDrawOptions.Color = Color.Red; }; gridView.PopupMenuShowing += (s, e) => e.Allow = false; } private void PerformFitting(bool isUserClick) { if (PointDatas.Count < 2) return; int fitMode = cboFitType != null ? cboFitType.SelectedIndex : 0; // 基础线性回归 (获取趋势) double n = PointDatas.Count; double sumX = PointDatas.Sum(p => p.X); double sumY = PointDatas.Sum(p => p.Y); double sumXY = PointDatas.Sum(p => p.X * p.Y); double sumXX = PointDatas.Sum(p => p.X * p.X); double denom = (n * sumXX - sumX * sumX); if (Math.Abs(denom) > 1e-9) { LinearK = (n * sumXY - sumX * sumY) / denom; LinearB = (sumY - LinearK * sumX) / n; } else { LinearK = 0; LinearB = PointDatas.Average(p => p.Y); } // 模式处理 if (fitMode == 0) { // 线性模式:清空趋势点 TrendKnobsPoint.Clear(); } else { // 曲线模式:若无控制点,根据线性趋势初始化 if (TrendKnobsPoint.Count == 0) { InitializeKnobsFromTrend(); } // 计算三次多项式 var xList = TrendKnobsPoint.Select(p => p.X).ToList(); var yList = TrendKnobsPoint.Select(p => p.Y).ToList(); PolyFit.Fit(xList, yList); } UpdateLineSeries(fitMode); UpdateFormulaLabel(fitMode); if (isUserClick) SubmitResult(fitMode); } private void InitializeKnobsFromTrend() { TrendKnobsPoint.Clear(); if (PointDatas.Count == 0) return; double minX = PointDatas.Min(p => p.X); double maxX = PointDatas.Max(p => p.X); double padding = (maxX - minX) * 0.1; double startX = minX - padding; double endX = maxX + padding; int knobCount = 5; double step = (endX - startX) / (knobCount - 1); for (int i = 0; i < knobCount; i++) { double x = startX + i * step; double y = LinearK * x + LinearB; // 落在回归线上 TrendKnobsPoint.Add(new PointData { X = x, Y = y }); } } private void UpdateLineSeries(int fitMode) { Series sLine = chartControl.Series["拟合线"]; Series sKnobs = chartControl.Series["控制点"]; if (sLine == null || PointDatas.Count == 0) return; sLine.Points.Clear(); sKnobs.Points.Clear(); double minX = PointDatas.Min(p => p.X); double maxX = PointDatas.Max(p => p.X); double padding = (maxX - minX) > 0 ? (maxX - minX) * 0.1 : 1.0; double startX = minX - padding; double endX = maxX + padding; LineSeriesView view = (LineSeriesView)sLine.View; if (fitMode == 0) { // 线性 view.Color = Color.CornflowerBlue; view.LineStyle.DashStyle = DashStyle.Dash; double y1 = LinearK * startX + LinearB; double y2 = LinearK * endX + LinearB; sKnobs.Points.Add(new SeriesPoint(startX, y1)); sKnobs.Points.Add(new SeriesPoint(endX, y2)); sLine.Points.Add(new SeriesPoint(startX, y1)); sLine.Points.Add(new SeriesPoint(endX, y2)); } else { // 曲线 view.Color = Color.Purple; view.LineStyle.DashStyle = DashStyle.Solid; foreach (var knob in TrendKnobsPoint) sKnobs.Points.Add(new SeriesPoint(knob.X, knob.Y)); int segments = 100; double step = (endX - startX) / segments; for (int i = 0; i <= segments; i++) { double x = startX + i * step; sLine.Points.Add(new SeriesPoint(x, PolyFit.Eval(x))); } } } private void UpdateFormulaLabel(int fitMode) { if (fitMode == 0) { lblFormula.Text = $"z = {LinearK:F4} * z + {LinearB:F4}"; } else { // z = Az^3 + Bz^2 + Cz + D lblFormula.Text = $"z = {PolyFit.A:F6}*z^3 + {PolyFit.B:F6}*z^2 + {PolyFit.C:F6}*z + {PolyFit.D:F4}"; } } private void SubmitResult(int fitMode) { string formulaZ; double kOut = double.NaN; double bOut = double.NaN; if (fitMode == 0) { formulaZ = $"{LinearK:F4} * z + {LinearB:F4}"; kOut = LinearK; bOut = LinearB; } else { // 曲线公式 formulaZ = PolyFit.GetFormula("z"); } OnCalibrationComplete?.Invoke(kOut, bOut, formulaZ); } // 相对位移拖拽 private void ChartControl_MouseDown(object sender, MouseEventArgs e) { int fitMode = cboFitType != null ? cboFitType.SelectedIndex : 0; ChartHitInfo hit = chartControl.CalcHitInfo(e.Location); Series s = hit.Series as Series; // 查找拖拽对象 if (s != null && s.Name == "控制点" && hit.SeriesPoint != null) { if (fitMode == 0) { DraggedVisualPoint = hit.SeriesPoint; } else { double mx = hit.SeriesPoint.NumericalArgument; EditingKnobPiont = TrendKnobsPoint.OrderBy(k => Math.Abs(k.X - mx)).FirstOrDefault(); } IsDragging = true; } else { // 模糊捕捉 XYDiagram diag = (XYDiagram)chartControl.Diagram; DiagramCoordinates c = diag.PointToDiagram(e.Location); if (c != null && !c.IsEmpty) { double mx = c.NumericalArgument; if (fitMode == 1 && TrendKnobsPoint.Count > 0) { EditingKnobPiont = TrendKnobsPoint.OrderBy(k => Math.Abs(k.X - mx)).First(); IsDragging = true; } else if (fitMode == 0) { Series sKnobs = chartControl.Series["控制点"]; if (sKnobs != null && sKnobs.Points.Count >= 2) { SeriesPoint p1 = sKnobs.Points[0], p2 = sKnobs.Points[1]; DraggedVisualPoint = Math.Abs(p1.NumericalArgument - mx) < Math.Abs(p2.NumericalArgument - mx) ? p1 : p2; IsDragging = true; } } } } // 初始化相对拖拽参数 if (IsDragging) { XYDiagram diagram = (XYDiagram)chartControl.Diagram; StartMouseY = e.Location.Y; double refX = 0; if (fitMode == 0 && DraggedVisualPoint != null) { StartDataY = DraggedVisualPoint.Values[0]; refX = DraggedVisualPoint.NumericalArgument; } else if (fitMode == 1 && EditingKnobPiont != null) { StartDataY = EditingKnobPiont.Y; refX = EditingKnobPiont.X; } ControlCoordinates c1 = diagram.DiagramToPoint(refX, StartDataY); ControlCoordinates c2 = diagram.DiagramToPoint(refX, StartDataY + 1.0); PixelsPerUnitY = Math.Abs(c1.Point.Y - c2.Point.Y); if (PixelsPerUnitY < 0.001) PixelsPerUnitY = 1.0; // 禁用坐标轴 diagram.EnableAxisXScrolling = false; diagram.EnableAxisYScrolling = false; diagram.EnableAxisXZooming = false; diagram.EnableAxisYZooming = false; chartControl.Cursor = Cursors.Hand; } } private void ChartControl_MouseMove(object sender, MouseEventArgs e) { if (!IsDragging) { ChartHitInfo hit = chartControl.CalcHitInfo(e.Location); Series s = hit.Series as Series; bool hover = s != null && s.Name == "控制点"; chartControl.Cursor = hover ? Cursors.Hand : Cursors.Default; return; } int fitMode = cboFitType != null ? cboFitType.SelectedIndex : 0; // 相对位移计算 double deltaPixels = e.Location.Y - StartMouseY; double deltaValue = deltaPixels / PixelsPerUnitY; double newYValue = StartDataY - deltaValue; if (fitMode == 1 && EditingKnobPiont != null) { // 曲线 EditingKnobPiont.Y = newYValue; PerformFitting(false); chartControl.RefreshData(); } else if (fitMode == 0 && DraggedVisualPoint != null) { // 线性 DraggedVisualPoint.Values[0] = newYValue; // 反算 K, B Series sKnobs = chartControl.Series["控制点"]; if (sKnobs != null && sKnobs.Points.Count >= 2) { SeriesPoint p1 = sKnobs.Points[0], p2 = sKnobs.Points[1]; double x1 = p1.NumericalArgument, y1 = p1.Values[0]; double x2 = p2.NumericalArgument, y2 = p2.Values[0]; if (Math.Abs(x2 - x1) > 1e-6) { LinearK = (y2 - y1) / (x2 - x1); LinearB = y1 - LinearK * x1; } Series sLine = chartControl.Series["拟合线"]; if (sLine.Points.Count >= 2) { sLine.Points[0].Values[0] = y1; sLine.Points[1].Values[0] = y2; } } chartControl.RefreshData(); UpdateFormulaLabel(0); } } private void ChartControl_MouseUp(object sender, MouseEventArgs e) { if (IsDragging) { IsDragging = false; EditingKnobPiont = null; DraggedVisualPoint = null; chartControl.Cursor = Cursors.Default; if (chartControl.Diagram is XYDiagram d) { d.EnableAxisXScrolling = true; d.EnableAxisYScrolling = true; d.EnableAxisXZooming = true; d.EnableAxisYZooming = true; } } } public void LoadData(List inputY, List inputZ, List nameList, double zmin, double zmax) { meshZmin = zmin; meshZmax = zmax; PointDatas.Clear(); TrendKnobsPoint.Clear(); int count = Math.Min(inputY.Count, inputZ.Count); for (int i = 0; i < count; i++) { double val = inputY[i]; if (val < pointZmin) pointZmin = val; if (val > pointZmax) pointZmax = val; string currentName = (nameList != null && i < nameList.Count) ? nameList[i] : $"数据{i + 1}"; PointDatas.Add(new PointData { Name = currentName, X = inputY[i], Y = inputZ[i], IsVirtual = false }); } if (PointDatas.Count <= 2) { PointDatas.Add(new PointData { Name = "虚拟点1", X = zmin, Y = pointZmin, IsVirtual = true }); PointDatas.Add(new PointData { Name = "虚拟点2", X = zmax, Y = pointZmax, IsVirtual = true }); } PerformFitting(false); } private void BtnAddPoint_Click(object sender, EventArgs e) { double dx = 0, dy = 0; if (Math.Abs(meshZmax - meshZmin) > 1e-6) { dx = (meshZmin + meshZmax) / 2.0; dy = (pointZmin + pointZmax) / 2.0; } else if (PointDatas.Count > 0) { dx = PointDatas.Average(p => p.X); dy = PointDatas.Average(p => p.Y); } PointDatas.Add(new PointData { Name = $"虚拟点{PointDatas.Count + 1}", X = Math.Round(dx, 2), Y = Math.Round(dy, 2), IsVirtual = true }); PerformFitting(false); } private void BtnRemovePoint_Click(object sender, EventArgs e) { //if (gridView.GetFocusedRow() is PointData row && XtraMessageBox.Show($"删除 {row.Name}?", "确认", MessageBoxButtons.YesNo) == DialogResult.Yes) //{ // PointDatas.Remove(row); // PerformFitting(false); //} int[] selectedRows = gridView.GetSelectedRows(); if (selectedRows.Length == 0) { XtraMessageBox.Show("请先勾选要删除的数据点。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (XtraMessageBox.Show($"确定删除选中的 {selectedRows.Length} 个点?", "确认", MessageBoxButtons.YesNo) == DialogResult.Yes) { var itemsToDelete = new List(); // 收集对象 foreach (int handle in selectedRows) { if (handle >= 0) itemsToDelete.Add(gridView.GetRow(handle) as PointData); } // 执行删除 foreach (var item in itemsToDelete) { if (item != null) PointDatas.Remove(item); } PerformFitting(false); } } private void ChartControl_CustomDrawCrosshair(object sender, CustomDrawCrosshairEventArgs e) { // 遍历所有被十字准线捕捉到的元素组 foreach (CrosshairElementGroup group in e.CrosshairElementGroups) { // 遍历组内的每个元素 (对应每个 Series 的点) foreach (CrosshairElement element in group.CrosshairElements) { Series series = element.Series; // 只处理 "原始数据" 和 "虚拟点" if (series.Name == "原始数据" || series.Name == "虚拟点") { // 获取绑定的数据对象 PointData p = element.SeriesPoint.Tag as PointData; if (p != null) { double zVal = p.Y; double tzVal = p.X; double diff = zVal - tzVal; element.LabelElement.Text = $"{zVal:0.##}({diff:0.##})\n{p.Name}"; } } else if (series.Name == "拟合线" || series.Name == "控制点") { element.Visible = false; } } } } private void BtnRestore_Click(object sender, EventArgs e) { TrendKnobsPoint.Clear(); PerformFitting(false); OnRestore?.Invoke(this, EventArgs.Empty); } private void BtnCalibrate_Click(object sender, EventArgs e) => PerformFitting(true); } }