472 lines
22 KiB
C#
472 lines
22 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 统计分析页面 — 残留量趋势分析 + 统计指标计算 + 明细数据
|
||
/// 使用 GDI+ 绘制趋势折线图(含平均线 + 数据标签)
|
||
/// </summary>
|
||
public partial class UC_Statistics : UserControl
|
||
{
|
||
/// <summary>图表数据缓存</summary>
|
||
private List<(DateTime time, double value, string label)> _chartData = new List<(DateTime, double, string)>();
|
||
/// <summary>平均值(用于绘制参考线)</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 预设明细表格列
|
||
/// </summary>
|
||
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 });
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统计分析按钮
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 填充表格序号列(与图表数据点编号对应)
|
||
/// 图表按时间升序排列,表格按ID降序,序号在数据排序后重新映射
|
||
/// </summary>
|
||
private void FillRowNumbers()
|
||
{
|
||
// 表格数据按ID降序排列(最新在前),序号直接按行号赋值
|
||
for (int i = 0; i < dgv_Data.Rows.Count; i++)
|
||
{
|
||
dgv_Data.Rows[i].Cells["序号"].Value = i + 1;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从DataTable构建图表数据(确保所有行都被处理)
|
||
/// </summary>
|
||
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原始行顺序(与表格序号一致)
|
||
}
|
||
|
||
/// <summary>
|
||
/// GDI+ 绘制折线图(增强版)
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算统计指标(基于残留量列)
|
||
/// </summary>
|
||
private void CalculateStatistics(DataTable dt)
|
||
{
|
||
var values = new List<double>();
|
||
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";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导出数据
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|