Files
WC-CZGKJ/SCADA/Pages/UC_Statistics.cs
2026-05-11 11:09:04 +08:00

472 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}