using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace MesWork.Pages { /// /// 统计分析页面 — 残留量趋势分析 + 统计指标计算 + 明细数据 /// 使用 GDI+ 绘制趋势折线图(含平均线 + 数据标签) /// public partial class UC_Statistics : UserControl { /// 图表数据缓存 private List<(DateTime time, double value, string label)> _chartData = new List<(DateTime, double, string)>(); /// 平均值(用于绘制参考线) private double _avgValue = 0; // ─── 配色常量 ─── private static readonly Color CLR_PRIMARY = Color.FromArgb(74, 144, 217); private static readonly Color CLR_PRIMARY_DARK = Color.FromArgb(30, 58, 95); private static readonly Color CLR_AVG_LINE = Color.FromArgb(245, 158, 11); // 橙色平均线 private static readonly Color CLR_GRID = Color.FromArgb(236, 239, 243); private static readonly Color CLR_TEXT = Color.FromArgb(55, 65, 81); private static readonly Color CLR_TEXT_LIGHT = Color.FromArgb(107, 114, 128); private static readonly Color CLR_DOT_FILL = Color.FromArgb(74, 144, 217); private static readonly Color CLR_CARD_BG = Color.FromArgb(248, 250, 252); public UC_Statistics() { InitializeComponent(); InitColumns(); dtp_Start.Value = DateTime.Today.AddDays(-30); dtp_End.Value = DateTime.Today; // 订阅图表区域绘制 pnl_Chart.Paint += pnl_Chart_Paint; pnl_Chart.Resize += (s, ev) => pnl_Chart.Invalidate(); // 为筛选框加占位提示 SetHint(txt_ModelNo, "机型号筛选..."); SetHint(txt_OrderNo, "订货号筛选..."); } private void SetHint(TextBox txt, string hint) { txt.Tag = hint; txt.Text = hint; txt.ForeColor = Color.FromArgb(156, 163, 175); txt.GotFocus += (s, e) => { if (txt.Text == hint) { txt.Text = ""; txt.ForeColor = CLR_PRIMARY_DARK; } }; txt.LostFocus += (s, e) => { if (string.IsNullOrWhiteSpace(txt.Text)) { txt.Text = hint; txt.ForeColor = Color.FromArgb(156, 163, 175); } }; } private string GetHintValue(TextBox txt) { return txt.Text == txt.Tag?.ToString() ? "" : txt.Text.Trim(); } /// /// 预设明细表格列 /// private void InitColumns() { dgv_Data.AutoGenerateColumns = false; dgv_Data.Columns.Clear(); dgv_Data.Columns.Add(new DataGridViewTextBoxColumn { Name = "序号", HeaderText = "序号", Width = 50, FillWeight = 30 }); dgv_Data.Columns.Add(new DataGridViewTextBoxColumn { Name = "ID", HeaderText = "ID", DataPropertyName = "ID", Visible = false }); dgv_Data.Columns.Add(new DataGridViewTextBoxColumn { Name = "测试时间", HeaderText = "检测时间", DataPropertyName = "测试时间", FillWeight = 100 }); dgv_Data.Columns.Add(new DataGridViewTextBoxColumn { Name = "发动机号", HeaderText = "工件编号", DataPropertyName = "发动机号", FillWeight = 100 }); dgv_Data.Columns.Add(new DataGridViewTextBoxColumn { Name = "订单号", HeaderText = "订货号", DataPropertyName = "订单号", FillWeight = 80 }); dgv_Data.Columns.Add(new DataGridViewTextBoxColumn { Name = "机型号", HeaderText = "机型号", DataPropertyName = "机型号", FillWeight = 70 }); dgv_Data.Columns.Add(new DataGridViewTextBoxColumn { Name = "残留量", HeaderText = "机油残留(mg)", DataPropertyName = "残留量", FillWeight = 80 }); dgv_Data.Columns.Add(new DataGridViewTextBoxColumn { Name = "设备编号", HeaderText = "设备编号", DataPropertyName = "设备编号", FillWeight = 60 }); dgv_Data.Columns.Add(new DataGridViewTextBoxColumn { Name = "操作员", HeaderText = "操作员", DataPropertyName = "操作员", FillWeight = 60 }); } /// /// 统计分析按钮 /// private void btn_Analyze_Click(object sender, EventArgs e) { try { var pageCountParam = new SqlParameter("@PageCount", SqlDbType.Int) { Direction = ParameterDirection.Output }; var itemCountParam = new SqlParameter("@ItemCount", SqlDbType.Int) { Direction = ParameterDirection.Output }; var parms = new SqlParameter[] { new SqlParameter("@发动机号", ""), new SqlParameter("@机型号", GetHintValue(txt_ModelNo)), new SqlParameter("@工单号", GetHintValue(txt_OrderNo)), new SqlParameter("@开始时间", dtp_Start.Value.ToString("yyyy-MM-dd")), new SqlParameter("@结束时间", dtp_End.Value.AddDays(1).ToString("yyyy-MM-dd")), new SqlParameter("@PageCurrent", 1), new SqlParameter("@PageSize", 9999), pageCountParam, itemCountParam }; SqlOperation.ExecuteStoredProcedure("称重记录_分页查询", parms, out DataTable dt, out string err); if (dt != null && dt.Rows.Count > 0) { dgv_Data.DataSource = dt; FillRowNumbers(); CalculateStatistics(dt); BuildChartData(dt); pnl_Chart.Invalidate(); } else { _chartData.Clear(); ClearStatLabels(); pnl_Chart.Invalidate(); MessageBox.Show("查询范围内无数据", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } } catch (Exception ex) { MessageBox.Show($"查询失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 填充表格序号列(与图表数据点编号对应) /// 图表按时间升序排列,表格按ID降序,序号在数据排序后重新映射 /// private void FillRowNumbers() { // 表格数据按ID降序排列(最新在前),序号直接按行号赋值 for (int i = 0; i < dgv_Data.Rows.Count; i++) { dgv_Data.Rows[i].Cells["序号"].Value = i + 1; } } /// /// 从DataTable构建图表数据(确保所有行都被处理) /// private void BuildChartData(DataTable dt) { _chartData.Clear(); // 查找时间列和数值列 string timeCol = dt.Columns.Contains("进站时间") ? "进站时间" : dt.Columns.Contains("测试时间") ? "测试时间" : null; string valCol = dt.Columns.Contains("残油量") ? "残油量" : dt.Columns.Contains("残留量") ? "残留量" : dt.Columns.Contains("放油量") ? "放油量" : null; string labelCol = dt.Columns.Contains("发动机号") ? "发动机号" : null; if (timeCol == null || valCol == null) return; foreach (DataRow row in dt.Rows) { DateTime t; double v; string lbl = labelCol != null ? row[labelCol]?.ToString() ?? "" : ""; // 时间解析:如果失败则使用当前时间递增 if (!DateTime.TryParse(row[timeCol]?.ToString(), out t)) t = DateTime.Now; // 数值解析:如果失败或为空,默认为0(也是有效数据点) object rawVal = row[valCol]; if (rawVal == null || rawVal == DBNull.Value || !double.TryParse(rawVal.ToString(), out v)) v = 0; _chartData.Add((t, v, lbl)); } // 不排序,保持DataTable原始行顺序(与表格序号一致) } /// /// GDI+ 绘制折线图(增强版) /// private void pnl_Chart_Paint(object sender, PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; var rect = pnl_Chart.ClientRectangle; int padL = 80, padR = 40, padT = 50, padB = 60; int chartW = rect.Width - padL - padR; int chartH = rect.Height - padT - padB; if (chartW < 50 || chartH < 50) return; // 绘制背景 using (var bgBrush = new SolidBrush(Color.White)) g.FillRectangle(bgBrush, rect); // 空数据提示 if (_chartData.Count == 0) { using (var font = new Font("微软雅黑", 14F)) using (var brush = new SolidBrush(Color.FromArgb(156, 163, 175))) { var msg = "📊 设置筛选条件后点击「统计分析」查看趋势图"; var sz = g.MeasureString(msg, font); g.DrawString(msg, font, brush, (rect.Width - sz.Width) / 2, (rect.Height - sz.Height) / 2); } return; } // 计算数据范围 double minVal = _chartData.Min(d => d.value); double maxVal = _chartData.Max(d => d.value); double range = maxVal - minVal; if (range < 0.001) { range = Math.Max(Math.Abs(maxVal) * 0.2, 1); } minVal -= range * 0.12; maxVal += range * 0.12; if (minVal < 0 && _chartData.Min(d => d.value) >= 0) minVal = 0; // 不让Y轴负数(如果原始数据非负) int totalPts = _chartData.Count; // ─── 绘制网格线和Y轴标签 ─── using (var gridPen = new Pen(CLR_GRID, 1) { DashStyle = DashStyle.Dot }) using (var axisFont = new Font("微软雅黑", 9F)) using (var axisBrush = new SolidBrush(CLR_TEXT_LIGHT)) using (var axisPen = new Pen(Color.FromArgb(200, 210, 220), 1)) { // Y轴线 g.DrawLine(axisPen, padL, padT, padL, padT + chartH); // X轴线 g.DrawLine(axisPen, padL, padT + chartH, padL + chartW, padT + chartH); for (int i = 0; i <= 5; i++) { int y = padT + chartH - (int)(chartH * i / 5.0); if (i > 0) g.DrawLine(gridPen, padL + 1, y, padL + chartW, y); double val = minVal + (maxVal - minVal) * i / 5.0; string valStr = val.ToString("F2"); var sz = g.MeasureString(valStr, axisFont); g.DrawString(valStr, axisFont, axisBrush, padL - sz.Width - 6, y - sz.Height / 2); } // X轴标签 — 序号均匀排布(1, 2, 3...) // 计算合适的标签间隔,避免过于密集 int maxLabels = Math.Max(1, chartW / 40); // 每个标签至少40px空间 int labelStep = Math.Max(1, (totalPts - 1) / Math.Max(maxLabels - 1, 1)); // 如果数据点少于maxLabels,每个都显示 if (totalPts <= maxLabels) labelStep = 1; for (int i = 0; i < totalPts; i += labelStep) { float xPos = totalPts == 1 ? padL + chartW / 2f : padL + (float)chartW * i / (totalPts - 1); string label = (i + 1).ToString(); var sz = g.MeasureString(label, axisFont); g.DrawString(label, axisFont, axisBrush, xPos - sz.Width / 2, padT + chartH + 8); g.DrawLine(axisPen, (int)xPos, padT + chartH, (int)xPos, padT + chartH + 5); } // 确保最后一个序号显示 if (totalPts > 1 && (totalPts - 1) % labelStep != 0) { float lastX = padL + chartW; string lastLabel = totalPts.ToString(); var lastSz = g.MeasureString(lastLabel, axisFont); g.DrawString(lastLabel, axisFont, axisBrush, lastX - lastSz.Width / 2, padT + chartH + 8); g.DrawLine(axisPen, (int)lastX, padT + chartH, (int)lastX, padT + chartH + 5); } } // ─── 计算所有数据点坐标(X轴按序号均匀排布)─── var points = new PointF[totalPts]; for (int i = 0; i < totalPts; i++) { float x = totalPts == 1 ? padL + chartW / 2f : padL + (float)chartW * i / (totalPts - 1); float y = padT + chartH - (float)(chartH * ((_chartData[i].value - minVal) / (maxVal - minVal))); points[i] = new PointF(x, y); } // ─── 渐变填充区域 ─── if (points.Length >= 2) { using (var fillPath = new GraphicsPath()) { fillPath.AddLines(points); fillPath.AddLine(points.Last(), new PointF(points.Last().X, padT + chartH)); fillPath.AddLine(new PointF(points.Last().X, padT + chartH), new PointF(points.First().X, padT + chartH)); fillPath.CloseFigure(); using (var fillBrush = new LinearGradientBrush( new Point(0, padT), new Point(0, padT + chartH), Color.FromArgb(50, 74, 144, 217), Color.FromArgb(3, 74, 144, 217))) { g.FillPath(fillBrush, fillPath); } } // 折线(粗线+阴影效果) using (var shadowPen = new Pen(Color.FromArgb(30, 74, 144, 217), 5f)) { shadowPen.LineJoin = LineJoin.Round; g.DrawLines(shadowPen, points); } using (var linePen = new Pen(CLR_PRIMARY, 2.5f)) { linePen.LineJoin = LineJoin.Round; g.DrawLines(linePen, points); } } // ─── 平均线(红色虚线 + 箭头 + 标签)─── if (_chartData.Count > 0) { float avgY = padT + chartH - (float)(chartH * ((_avgValue - minVal) / (maxVal - minVal))); if (avgY >= padT && avgY <= padT + chartH) { using (var avgPen = new Pen(CLR_AVG_LINE, 1.8f) { DashStyle = DashStyle.Dash }) { g.DrawLine(avgPen, padL, avgY, padL + chartW, avgY); } // 平均线标签(右侧带箭头标识) using (var avgFont = new Font("微软雅黑", 9F, FontStyle.Bold)) using (var avgBrush = new SolidBrush(CLR_AVG_LINE)) { string avgText = $"▶ 平均 {_avgValue:F2}"; var sz = g.MeasureString(avgText, avgFont); // 背景 float lblX = padL + chartW - sz.Width - 4; float lblY = avgY - sz.Height - 3; using (var bgBrush = new SolidBrush(Color.FromArgb(220, 255, 255, 255))) g.FillRectangle(bgBrush, lblX - 3, lblY, sz.Width + 6, sz.Height + 2); g.DrawString(avgText, avgFont, avgBrush, lblX, lblY); // 小三角箭头指向线 var arrowPts = new PointF[] { new PointF(padL + chartW + 2, avgY), new PointF(padL + chartW + 10, avgY - 5), new PointF(padL + chartW + 10, avgY + 5) }; g.FillPolygon(avgBrush, arrowPts); } } } // ─── 数据点(带光晕效果)─── for (int i = 0; i < points.Length; i++) { var pt = points[i]; // 光晕 using (var glowBrush = new SolidBrush(Color.FromArgb(40, 74, 144, 217))) g.FillEllipse(glowBrush, pt.X - 8, pt.Y - 8, 16, 16); // 白色底 g.FillEllipse(Brushes.White, pt.X - 5, pt.Y - 5, 10, 10); // 蓝色圆点 using (var dotBrush = new SolidBrush(CLR_DOT_FILL)) g.FillEllipse(dotBrush, pt.X - 4, pt.Y - 4, 8, 8); // 数值标签(只有当数据点不太密时显示) if (_chartData.Count <= 15) { using (var valFont = new Font("微软雅黑", 8F)) using (var valBrush = new SolidBrush(CLR_PRIMARY_DARK)) { string valText = _chartData[i].value.ToString("F1"); var sz = g.MeasureString(valText, valFont); g.DrawString(valText, valFont, valBrush, pt.X - sz.Width / 2, pt.Y - sz.Height - 6); } } } // ─── 图表右下角水印 ─── using (var wmFont = new Font("微软雅黑", 8F)) using (var wmBrush = new SolidBrush(Color.FromArgb(80, 156, 163, 175))) { string wm = $"共 {_chartData.Count} 个样本"; var sz = g.MeasureString(wm, wmFont); g.DrawString(wm, wmFont, wmBrush, padL + chartW - sz.Width, padT + 4); } } /// /// 计算统计指标(基于残留量列) /// private void CalculateStatistics(DataTable dt) { var values = new List(); string colName = dt.Columns.Contains("残油量") ? "残油量" : dt.Columns.Contains("残留量") ? "残留量" : null; if (colName == null) { foreach (DataColumn col in dt.Columns) { if (col.ColumnName.Contains("残留") || col.ColumnName.Contains("残油") || col.ColumnName.Contains("重量")) { colName = col.ColumnName; break; } } } if (colName != null) { foreach (DataRow row in dt.Rows) { object rawVal = row[colName]; if (rawVal != null && rawVal != DBNull.Value && double.TryParse(rawVal.ToString(), out double v)) values.Add(v); else values.Add(0); // 空值也算0,确保样本数一致 } } if (values.Count == 0) { ClearStatLabels(); return; } values.Sort(); double max = values.Max(); double min = values.Min(); double avg = values.Average(); _avgValue = avg; // 保存平均值用于绘图 double median = values.Count % 2 == 0 ? (values[values.Count / 2 - 1] + values[values.Count / 2]) / 2.0 : values[values.Count / 2]; double rangeVal = max - min; double variance = values.Sum(v => Math.Pow(v - avg, 2)) / values.Count; double stdDev = Math.Sqrt(variance); // 更新统计指标标签 lbl_StatMax.Text = $"最大值 {max:F4}"; lbl_StatMin.Text = $"最小值 {min:F4}"; lbl_StatAvg.Text = $"平均值 {avg:F4}"; lbl_StatMedian.Text = $"中位数 {median:F4}"; lbl_StatRange.Text = $"极差 {rangeVal:F4}"; lbl_StatVariance.Text = $"方差 {variance:F4}"; lbl_StatStdDev.Text = $"标准差 {stdDev:F4}"; lbl_StatCount.Text = $"样本数 {values.Count}"; } private void ClearStatLabels() { lbl_StatMax.Text = "最大值 —"; lbl_StatMin.Text = "最小值 —"; lbl_StatAvg.Text = "平均值 —"; lbl_StatMedian.Text = "中位数 —"; lbl_StatRange.Text = "极差 —"; lbl_StatVariance.Text = "方差 —"; lbl_StatStdDev.Text = "标准差 —"; lbl_StatCount.Text = "样本数 0"; } /// /// 导出数据 /// private void btn_Export_Click(object sender, EventArgs e) { CrudHelper.ExportToExcel("统计分析", "统计分析", () => { var (pageCountParam, itemCountParam) = PagerBar.CreateOutputParams(); var parms = new SqlParameter[] { new SqlParameter("@发动机号", ""), new SqlParameter("@机型号", GetHintValue(txt_ModelNo)), new SqlParameter("@工单号", GetHintValue(txt_OrderNo)), new SqlParameter("@开始时间", dtp_Start.Value.ToString("yyyy-MM-dd")), new SqlParameter("@结束时间", dtp_End.Value.AddDays(1).ToString("yyyy-MM-dd")), new SqlParameter("@PageCurrent", 1), new SqlParameter("@PageSize", 9999999), pageCountParam, itemCountParam }; SqlOperation.ExecuteStoredProcedure("称重记录_分页查询", parms, out DataTable dt, out string err); return dt; }, dgv_Data.Rows.Count > 0); } } }