using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; using System; using System.Collections.Generic; using System.Globalization; using YZWater.Core.ViewModels; namespace YZWater.Avalonia.Controls; /// /// 通用泵站 2D 工艺流程图控件 (SCADA 风格)。 /// 设计画布 960×540,等比缩放适配实际尺寸。 /// public class PumpStation2DControl : Control { // ═══ Styled Properties(接口不变,保持 ViewAView.axaml 绑定兼容) ═══ public static readonly StyledProperty Tank1LevelProperty = AvaloniaProperty.Register(nameof(Tank1Level)); public static readonly StyledProperty Tank2LevelProperty = AvaloniaProperty.Register(nameof(Tank2Level)); public static readonly StyledProperty Tank3LevelProperty = AvaloniaProperty.Register(nameof(Tank3Level)); public static readonly StyledProperty Tank4LevelProperty = AvaloniaProperty.Register(nameof(Tank4Level)); public static readonly StyledProperty InflowRateProperty = AvaloniaProperty.Register(nameof(InflowRate)); public static readonly StyledProperty OutflowRateProperty = AvaloniaProperty.Register(nameof(OutflowRate)); public static readonly StyledProperty Pump1RunningProperty = AvaloniaProperty.Register(nameof(Pump1Running)); public static readonly StyledProperty Pump2RunningProperty = AvaloniaProperty.Register(nameof(Pump2Running)); public static readonly StyledProperty Pump3RunningProperty = AvaloniaProperty.Register(nameof(Pump3Running)); public static readonly StyledProperty Pump4RunningProperty = AvaloniaProperty.Register(nameof(Pump4Running)); public static readonly StyledProperty Pump5RunningProperty = AvaloniaProperty.Register(nameof(Pump5Running)); public static readonly StyledProperty Fan1RunningProperty = AvaloniaProperty.Register(nameof(Fan1Running)); public static readonly StyledProperty Fan2RunningProperty = AvaloniaProperty.Register(nameof(Fan2Running)); public static readonly StyledProperty Valve1StatusProperty = AvaloniaProperty.Register(nameof(Valve1Status)); public static readonly StyledProperty Valve2StatusProperty = AvaloniaProperty.Register(nameof(Valve2Status)); public static readonly StyledProperty Valve3StatusProperty = AvaloniaProperty.Register(nameof(Valve3Status)); public static readonly StyledProperty Valve4StatusProperty = AvaloniaProperty.Register(nameof(Valve4Status)); public static readonly StyledProperty IsInflowRunningProperty = AvaloniaProperty.Register(nameof(IsInflowRunning)); public static readonly StyledProperty IsOutflowRunningProperty = AvaloniaProperty.Register(nameof(IsOutflowRunning)); public static readonly StyledProperty HasAlarmProperty = AvaloniaProperty.Register(nameof(HasAlarm)); public static readonly StyledProperty AlarmCountProperty = AvaloniaProperty.Register(nameof(AlarmCount)); public static readonly StyledProperty RunningDeviceCountProperty = AvaloniaProperty.Register(nameof(RunningDeviceCount)); public static readonly StyledProperty EfficiencyProperty = AvaloniaProperty.Register(nameof(Efficiency)); public double Tank1Level { get => GetValue(Tank1LevelProperty); set => SetValue(Tank1LevelProperty, value); } public double Tank2Level { get => GetValue(Tank2LevelProperty); set => SetValue(Tank2LevelProperty, value); } public double Tank3Level { get => GetValue(Tank3LevelProperty); set => SetValue(Tank3LevelProperty, value); } public double Tank4Level { get => GetValue(Tank4LevelProperty); set => SetValue(Tank4LevelProperty, value); } public double InflowRate { get => GetValue(InflowRateProperty); set => SetValue(InflowRateProperty, value); } public double OutflowRate { get => GetValue(OutflowRateProperty); set => SetValue(OutflowRateProperty, value); } public bool Pump1Running { get => GetValue(Pump1RunningProperty); set => SetValue(Pump1RunningProperty, value); } public bool Pump2Running { get => GetValue(Pump2RunningProperty); set => SetValue(Pump2RunningProperty, value); } public bool Pump3Running { get => GetValue(Pump3RunningProperty); set => SetValue(Pump3RunningProperty, value); } public bool Pump4Running { get => GetValue(Pump4RunningProperty); set => SetValue(Pump4RunningProperty, value); } public bool Pump5Running { get => GetValue(Pump5RunningProperty); set => SetValue(Pump5RunningProperty, value); } public bool Fan1Running { get => GetValue(Fan1RunningProperty); set => SetValue(Fan1RunningProperty, value); } public bool Fan2Running { get => GetValue(Fan2RunningProperty); set => SetValue(Fan2RunningProperty, value); } public ValveStatus Valve1Status { get => GetValue(Valve1StatusProperty); set => SetValue(Valve1StatusProperty, value); } public ValveStatus Valve2Status { get => GetValue(Valve2StatusProperty); set => SetValue(Valve2StatusProperty, value); } public ValveStatus Valve3Status { get => GetValue(Valve3StatusProperty); set => SetValue(Valve3StatusProperty, value); } public ValveStatus Valve4Status { get => GetValue(Valve4StatusProperty); set => SetValue(Valve4StatusProperty, value); } public bool IsInflowRunning { get => GetValue(IsInflowRunningProperty); set => SetValue(IsInflowRunningProperty, value); } public bool IsOutflowRunning { get => GetValue(IsOutflowRunningProperty); set => SetValue(IsOutflowRunningProperty, value); } public bool HasAlarm { get => GetValue(HasAlarmProperty); set => SetValue(HasAlarmProperty, value); } public int AlarmCount { get => GetValue(AlarmCountProperty); set => SetValue(AlarmCountProperty, value); } public int RunningDeviceCount { get => GetValue(RunningDeviceCountProperty); set => SetValue(RunningDeviceCountProperty, value); } public double Efficiency { get => GetValue(EfficiencyProperty); set => SetValue(EfficiencyProperty, value); } // ═══ 动画 ═══ private double _phase; private double _pumpAngle; private double _fanAngle; private IDisposable? _timer; // ═══ 静态画刷缓存(避免每帧 new SolidColorBrush) ═══ private static readonly SolidColorBrush PipeWallBrush = new(Color.FromRgb(55, 65, 81)); private static readonly SolidColorBrush PipeInnerBrush = new(Color.FromRgb(97, 111, 134)); private static readonly SolidColorBrush FlowDotBrush = new(Color.FromRgb(3, 169, 244), 0.85); static PumpStation2DControl() { AffectsRender( Tank1LevelProperty, Tank2LevelProperty, Tank3LevelProperty, Tank4LevelProperty, InflowRateProperty, OutflowRateProperty, Pump1RunningProperty, Pump2RunningProperty, Pump3RunningProperty, Pump4RunningProperty, Pump5RunningProperty, Fan1RunningProperty, Fan2RunningProperty, Valve1StatusProperty, Valve2StatusProperty, Valve3StatusProperty, Valve4StatusProperty, IsInflowRunningProperty, IsOutflowRunningProperty, HasAlarmProperty, AlarmCountProperty, RunningDeviceCountProperty, EfficiencyProperty); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); ThemeHelper.ThemeChanged += OnThemeChanged; _timer = DispatcherTimer.Run(() => { var needsAnimation = AnyPumpRunning || Fan1Running || Fan2Running || IsInflowRunning || IsOutflowRunning; if (needsAnimation) { _phase = (_phase + 1) % 360; if (AnyPumpRunning) _pumpAngle = (_pumpAngle + 4) % 360; if (Fan1Running || Fan2Running) _fanAngle = (_fanAngle + 6) % 360; InvalidateVisual(); } return true; }, TimeSpan.FromMilliseconds(33)); } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { ThemeHelper.ThemeChanged -= OnThemeChanged; _timer?.Dispose(); _timer = null; base.OnDetachedFromVisualTree(e); } private void OnThemeChanged() => InvalidateVisual(); private bool AnyPumpRunning => Pump1Running || Pump2Running || Pump3Running || Pump4Running || Pump5Running; // ═══ 设计坐标常量 (960 × 540) ═══ private const double DW = 960, DH = 540; // 水箱位置: (x, y, width, height) private static readonly Rect DTank1 = new(55, 130, 120, 140); private static readonly Rect DTank2 = new(275, 85, 195, 210); private static readonly Rect DTank3 = new(275, 400, 155, 105); private static readonly Rect DTank4 = new(720, 130, 120, 140); // 泵位置 (5 台泵在集水井下方) private static readonly Point[] DPumps = { new(300, 340), new(338, 340), new(376, 340), new(414, 340), new(452, 340) }; // 风机位置 private static readonly Point DFan1 = new(590, 450); private static readonly Point DFan2 = new(660, 450); // 阀门位置 (管道上的关键节点) private static readonly Point DV1 = new(218, 200); // Tank1 → Tank2 private static readonly Point DV2 = new(555, 195); // Header → Tank4 private static readonly Point DV3 = new(420, 395); // Header → Tank3 分支 private static readonly Point DV4 = new(230, 450); // Tank2 → Tank3 // 流量计位置 private static readonly Point DFM1 = new(22, 195); private static readonly Point DFM2 = new(870, 195); // ═══ 主渲染 ═══ public override void Render(DrawingContext context) { base.Render(context); var bounds = new Rect(Bounds.Size); if (bounds.Width < 120 || bounds.Height < 120) return; // 背景 context.DrawRectangle(ThemeHelper.PanelBg, null, bounds); // 计算缩放:设计坐标 → 屏幕坐标 var margin = 8.0; var scaleX = (bounds.Width - 2 * margin) / DW; var scaleY = (bounds.Height - 2 * margin) / DH; var scale = Math.Min(scaleX, scaleY); var offsetX = margin + (bounds.Width - 2 * margin - DW * scale) / 2; var offsetY = margin + (bounds.Height - 2 * margin - DH * scale) / 2; // 地面线 DrawLine(context, offsetX, offsetY, scale, 0, 520, 960, 520, ThemeHelper.Border, 1); // ═══ 管道(先画,这样设备覆盖在上面)═══ // 进水管 → FM1 → Tank1 DrawPipeH(context, offsetX, offsetY, scale, 0, 200, 55, IsInflowRunning, 10); DrawFlowMeter(context, offsetX, offsetY, scale, DFM1, "进水", InflowRate, IsInflowRunning); DrawPipeH(context, offsetX, offsetY, scale, 48, 200, 55, IsInflowRunning, 10); // Tank1 → V1 → Tank2 DrawPipeH(context, offsetX, offsetY, scale, 175, 200, 195, IsInflowRunning, 8); DrawValve(context, offsetX, offsetY, scale, DV1, "V1", Valve1Status); DrawPipeH(context, offsetX, offsetY, scale, 240, 200, 275, IsInflowRunning, 8); // Tank2 底部 → 泵吸入管 DrawPipeV(context, offsetX, offsetY, scale, 372, 295, 325, AnyPumpRunning, 8); // 泵排出总管 DrawPipeH(context, offsetX, offsetY, scale, 285, 365, 470, AnyPumpRunning, 10); // 总管上弯 → V2 → Tank4 DrawPipeV(context, offsetX, offsetY, scale, 500, 365, 195, IsOutflowRunning, 10); DrawPipeH(context, offsetX, offsetY, scale, 500, 195, 530, IsOutflowRunning, 10); DrawValve(context, offsetX, offsetY, scale, DV2, "V2", Valve2Status); DrawPipeH(context, offsetX, offsetY, scale, 580, 195, 720, IsOutflowRunning, 10); // Tank4 → FM2 → 出水 DrawPipeH(context, offsetX, offsetY, scale, 840, 200, 860, IsOutflowRunning, 10); DrawFlowMeter(context, offsetX, offsetY, scale, DFM2, "出水", OutflowRate, IsOutflowRunning); DrawPipeH(context, offsetX, offsetY, scale, 896, 200, 960, IsOutflowRunning, 10); // V3 分支:总管 → Tank3 DrawPipeV(context, offsetX, offsetY, scale, 420, 365, 378, false, 8); DrawValve(context, offsetX, offsetY, scale, DV3, "V3", Valve3Status); DrawPipeV(context, offsetX, offsetY, scale, 420, 412, 400, false, 8); // V4 分支:Tank2 → Tank3 DrawPipeV(context, offsetX, offsetY, scale, 230, 295, 430, false, 8); DrawPipeH(context, offsetX, offsetY, scale, 230, 450, 275, false, 8); DrawValve(context, offsetX, offsetY, scale, DV4, "V4", Valve4Status); // ═══ 水箱 ═══ DrawTank(context, offsetX, offsetY, scale, DTank1, Tank1Level, "进水池", "IN", Color.FromRgb(0, 137, 123)); // Teal DrawTank(context, offsetX, offsetY, scale, DTank2, Tank2Level, "集水井", "SUMP", Color.FromRgb(33, 150, 243)); // Blue DrawTank(context, offsetX, offsetY, scale, DTank3, Tank3Level, "调蓄池", "BUF", Color.FromRgb(126, 87, 194)); // Purple DrawTank(context, offsetX, offsetY, scale, DTank4, Tank4Level, "出水池", "OUT", Color.FromRgb(0, 150, 136)); // Green // ═══ 泵组 ═══ bool[] pumpRunning = { Pump1Running, Pump2Running, Pump3Running, Pump4Running, Pump5Running }; string[] pumpLabels = { "P1", "P2", "P3", "P4", "P5" }; for (int i = 0; i < 5; i++) DrawPump(context, offsetX, offsetY, scale, DPumps[i], pumpLabels[i], pumpRunning[i]); // ═══ 风机 ═══ DrawFan(context, offsetX, offsetY, scale, DFan1, "F1", Fan1Running); DrawFan(context, offsetX, offsetY, scale, DFan2, "F2", Fan2Running); // 风机区域框 var fanAreaX = S(offsetX, scale, 555); var fanAreaY = S(offsetY, scale, 418); var fanAreaW = W(scale, 180); var fanAreaH = H(scale, 75); context.DrawRectangle(null, new Pen(ThemeHelper.Border, 1), new Rect(fanAreaX, fanAreaY, fanAreaW, fanAreaH), 3, 3); DrawLabel(context, offsetX, offsetY, scale, 560, 422, "风机区", 10, ThemeHelper.TextTertiary); // ═══ 报警边框 ═══ if (HasAlarm) { var sceneRect = new Rect(margin, margin, bounds.Width - 2 * margin, bounds.Height - 2 * margin); context.DrawRectangle(null, new Pen(ThemeHelper.Danger, 2), sceneRect, 4); } // ═══ HUD 状态面板 ═══ DrawHud(context, bounds, scale); } // ═══ 坐标转换辅助 ═══ private static double S(double offset, double scale, double designVal) => offset + designVal * scale; private static double W(double scale, double designW) => designW * scale; private static double H(double scale, double designH) => designH * scale; private static double R(double scale, double designR) => designR * scale; // ═══ 水箱绘制 ═══ private void DrawTank(DrawingContext context, double ox, double oy, double sc, Rect designRect, double level, string name, string code, Color waterColor) { var x = S(ox, sc, designRect.X); var y = S(oy, sc, designRect.Y); var w = W(sc, designRect.Width); var h = H(sc, designRect.Height); var rect = new Rect(x, y, w, h); // 箱体 context.DrawRectangle(ThemeHelper.SurfaceBg, new Pen(ThemeHelper.Border, 1.5), rect, 3, 3); // 水位填充 var clampedLevel = Math.Clamp(level, 0, 100); if (clampedLevel > 0.5) { var waterH = (h - 4) * clampedLevel / 100.0; var waterRect = new Rect(x + 2, y + h - 2 - waterH, w - 4, waterH); var waterBrush = new SolidColorBrush(waterColor, 0.65); context.DrawRectangle(waterBrush, null, waterRect); // 水面波纹 var waveY = waterRect.Y; var waveBrush = new SolidColorBrush(waterColor, 0.35); for (int i = 0; i < 3; i++) { var wx = waterRect.X + 4 + i * (waterRect.Width / 3.0); context.DrawLine(new Pen(waveBrush, 1), new Point(wx, waveY), new Point(wx + waterRect.Width / 6.0, waveY + 2)); } } // 名称标签 var fontSize = Math.Max(8, 11 * sc); DrawLabel(context, ox, oy, sc, designRect.Center.X - 20, designRect.Y + 6, name, 11, ThemeHelper.TextPrimary, FontWeight.Bold); DrawLabel(context, ox, oy, sc, designRect.Center.X - 14, designRect.Y + 20, code, 9, ThemeHelper.TextTertiary); // 液位百分比 var pctText = $"{clampedLevel:F1}%"; var pctColor = clampedLevel > 80 ? ThemeHelper.Warning : clampedLevel < 20 ? ThemeHelper.Danger : new SolidColorBrush(waterColor); DrawLabel(context, ox, oy, sc, designRect.Center.X - 18, designRect.Center.Y + 8, pctText, 13, pctColor, FontWeight.Bold); } // ═══ 管道绘制 ═══ private void DrawPipeH(DrawingContext context, double ox, double oy, double sc, double x1Design, double yDesign, double x2Design, bool isFlowing, double thicknessDesign) { var x1 = S(ox, sc, x1Design); var y = S(oy, sc, yDesign); var x2 = S(ox, sc, x2Design); var thickness = R(sc, thicknessDesign); // 管壁 context.DrawLine(new Pen(PipeWallBrush, thickness), new Point(x1, y), new Point(x2, y)); // 内管 context.DrawLine(new Pen(PipeInnerBrush, thickness * 0.45), new Point(x1, y), new Point(x2, y)); // 流动动画 if (isFlowing && x2 > x1 + 2) DrawFlowDots(context, x1, y, x2, y); } private void DrawPipeV(DrawingContext context, double ox, double oy, double sc, double xDesign, double y1Design, double y2Design, bool isFlowing, double thicknessDesign) { var x = S(ox, sc, xDesign); var y1 = S(oy, sc, y1Design); var y2 = S(oy, sc, y2Design); var thickness = R(sc, thicknessDesign); context.DrawLine(new Pen(PipeWallBrush, thickness), new Point(x, y1), new Point(x, y2)); context.DrawLine(new Pen(PipeInnerBrush, thickness * 0.45), new Point(x, y1), new Point(x, y2)); if (isFlowing && Math.Abs(y2 - y1) > 2) DrawFlowDots(context, x, y1, x, y2); } private void DrawFlowDots(DrawingContext context, double x1, double y1, double x2, double y2) { var dx = x2 - x1; var dy = y2 - y1; var len = Math.Sqrt(dx * dx + dy * dy); if (len < 4) return; var count = Math.Max(2, (int)(len / 18)); for (int i = 0; i < count; i++) { var t = ((_phase / 360.0) + (double)i / count) % 1.0; var px = x1 + dx * t; var py = y1 + dy * t; var r = Math.Max(1.5, 3); context.DrawEllipse(FlowDotBrush, null, new Rect(px - r, py - r, r * 2, r * 2)); } } // ═══ 泵绘制 ═══ private void DrawPump(DrawingContext context, double ox, double oy, double sc, Point designPos, string label, bool running) { var cx = S(ox, sc, designPos.X); var cy = S(oy, sc, designPos.Y); var r = R(sc, 18); var statusColor = running ? GetColor(ThemeHelper.Success) : GetColor(ThemeHelper.TextDisabled); var statusBrush = new SolidColorBrush(statusColor); // 运行光晕 if (running) context.DrawEllipse(new SolidColorBrush(statusColor, 0.18), null, new Rect(cx - r * 1.4, cy - r * 1.4, r * 2.8, r * 2.8)); // 泵体圆 context.DrawEllipse(ThemeHelper.SurfaceBg, new Pen(statusBrush, running ? 2 : 1.2), new Rect(cx - r, cy - r, r * 2, r * 2)); // 旋转叶片 var bladeCount = 4; for (int i = 0; i < bladeCount; i++) { var angle = (running ? _pumpAngle : 0) + i * (360.0 / bladeCount); var rad = angle * Math.PI / 180.0; var bx = cx + Math.Cos(rad) * r * 0.65; var by = cy + Math.Sin(rad) * r * 0.65; var bladeBrush = new SolidColorBrush(statusColor, running ? 0.9 : 0.45); context.DrawLine(new Pen(bladeBrush, Math.Max(1.5, 2.5 * sc)), new Point(cx, cy), new Point(bx, by)); } // 中心圆 context.DrawEllipse(statusBrush, null, new Rect(cx - r * 0.22, cy - r * 0.22, r * 0.44, r * 0.44)); // 标签 DrawLabelAt(context, cx, cy + r + 3, label, Math.Max(8, 10 * sc), ThemeHelper.TextPrimary, FontWeight.Bold); } // ═══ 风机绘制 ═══ private void DrawFan(DrawingContext context, double ox, double oy, double sc, Point designPos, string label, bool running) { var cx = S(ox, sc, designPos.X); var cy = S(oy, sc, designPos.Y); var r = R(sc, 20); var statusColor = running ? GetColor(ThemeHelper.Info) : GetColor(ThemeHelper.TextDisabled); var statusBrush = new SolidColorBrush(statusColor); // 外壳 context.DrawEllipse(ThemeHelper.SurfaceBg, new Pen(statusBrush, running ? 2 : 1.2), new Rect(cx - r, cy - r, r * 2, r * 2)); // 旋转叶片 (5 片) var bladeCount = 5; for (int i = 0; i < bladeCount; i++) { var angle = (running ? _fanAngle : 0) + i * (360.0 / bladeCount); var rad = angle * Math.PI / 180.0; var bx = cx + Math.Cos(rad) * r * 0.72; var by = cy + Math.Sin(rad) * r * 0.72; var bladeBrush = new SolidColorBrush(statusColor, running ? 0.85 : 0.4); context.DrawLine(new Pen(bladeBrush, Math.Max(2, 3 * sc)), new Point(cx, cy), new Point(bx, by)); } // 中心 context.DrawEllipse(statusBrush, null, new Rect(cx - r * 0.18, cy - r * 0.18, r * 0.36, r * 0.36)); // 标签 DrawLabelAt(context, cx, cy + r + 4, label, Math.Max(8, 10 * sc), ThemeHelper.TextPrimary, FontWeight.Bold); } // ═══ 阀门绘制(蝴蝶阀符号)═══ private void DrawValve(DrawingContext context, double ox, double oy, double sc, Point designPos, string label, ValveStatus status) { var cx = S(ox, sc, designPos.X); var cy = S(oy, sc, designPos.Y); var s = R(sc, 12); var color = status switch { ValveStatus.Open => GetColor(ThemeHelper.Success), ValveStatus.Middle => GetColor(ThemeHelper.Warning), ValveStatus.Closed => GetColor(ThemeHelper.Danger), _ => GetColor(ThemeHelper.TextDisabled) }; var colorBrush = new SolidColorBrush(color); // 蝴蝶阀:两个三角形 var top = new Point(cx, cy - s); var bottom = new Point(cx, cy + s); var left = new Point(cx - s * 0.7, cy); var right = new Point(cx + s * 0.7, cy); // 上三角 var tri1 = new StreamGeometry(); using (var ctx = tri1.Open()) { ctx.BeginFigure(left, true); ctx.LineTo(top); ctx.LineTo(right); ctx.EndFigure(true); } context.DrawGeometry(new SolidColorBrush(color, 0.7), null, tri1); // 下三角(倒置) var tri2 = new StreamGeometry(); using (var ctx = tri2.Open()) { ctx.BeginFigure(left, true); ctx.LineTo(bottom); ctx.LineTo(right); ctx.EndFigure(true); } context.DrawGeometry(new SolidColorBrush(color, 0.7), null, tri2); // 阀杆 context.DrawLine(new Pen(colorBrush, Math.Max(1, 1.5 * sc)), new Point(cx - s * 0.5, cy), new Point(cx + s * 0.5, cy)); // 状态指示点 var lampR = Math.Max(2, 3 * sc); context.DrawEllipse(colorBrush, null, new Rect(cx - lampR, cy - s - lampR * 2.5, lampR * 2, lampR * 2)); // 标签 DrawLabelAt(context, cx, cy + s + 3, label, Math.Max(7, 9 * sc), ThemeHelper.TextSecondary, FontWeight.Bold); } // ═══ 流量计绘制 ═══ private void DrawFlowMeter(DrawingContext context, double ox, double oy, double sc, Point designPos, string label, double value, bool active) { var cx = S(ox, sc, designPos.X); var cy = S(oy, sc, designPos.Y); var w = W(sc, 56); var h = H(sc, 32); var color = active ? GetColor(ThemeHelper.Success) : GetColor(ThemeHelper.TextDisabled); var colorBrush = new SolidColorBrush(color); // 外框 var rect = new Rect(cx - w / 2, cy - h / 2, w, h); context.DrawRectangle(new SolidColorBrush(GetColor(ThemeHelper.SurfaceBg), 0.95), new Pen(colorBrush, 1.2), rect, 2, 2); // 标签 DrawLabelAt(context, cx, cy - h * 0.18, label, Math.Max(7, 8 * sc), ThemeHelper.TextTertiary); // 数值 DrawLabelAt(context, cx, cy + h * 0.12, $"{value:F1}", Math.Max(8, 10 * sc), colorBrush, FontWeight.Bold); } // ═══ HUD 状态面板 ═══ private void DrawHud(DrawingContext context, Rect bounds, double scale) { var panelW = Math.Max(120, 185 * Math.Min(scale, 1.5)); var panelH = Math.Max(45, 58 * Math.Min(scale, 1.5)); var panelX = bounds.Right - panelW - 10; var panelY = bounds.Top + 8; var panelRect = new Rect(panelX, panelY, panelW, panelH); context.DrawRectangle(new SolidColorBrush(GetColor(ThemeHelper.SurfaceBg), 0.92), new Pen(ThemeHelper.Border, 1), panelRect, 3, 3); var fontSize = Math.Max(8, 10 * Math.Min(scale, 1.5)); var smallFont = Math.Max(7, 9 * Math.Min(scale, 1.5)); DrawLabelAt(context, panelX + 8, panelY + 6, "泵站总览", fontSize * 1.15, ThemeHelper.TextPrimary, FontWeight.Bold); DrawLabelAt(context, panelX + 8, panelY + 6 + fontSize * 1.5, $"运行 {RunningDeviceCount} 效率 {Efficiency:F0}%", smallFont, ThemeHelper.TextSecondary); var alarmColor = HasAlarm ? ThemeHelper.Danger : ThemeHelper.Success; DrawLabelAt(context, panelX + 8, panelY + 6 + fontSize * 1.5 + smallFont * 1.5, $"报警 {AlarmCount}", smallFont, alarmColor, FontWeight.Bold); } // ═══ 文字绘制辅助 ═══ private void DrawLabel(DrawingContext context, double ox, double oy, double sc, double designX, double designY, string text, double designFontSize, IBrush brush, FontWeight weight = FontWeight.Normal) { var x = S(ox, sc, designX); var y = S(oy, sc, designY); var size = Math.Max(8, designFontSize * sc); DrawLabelAt(context, x, y, text, size, brush, weight); } private static void DrawLabelAt(DrawingContext context, double x, double y, string text, double size, IBrush brush, FontWeight weight = FontWeight.Normal) { var formatted = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Microsoft YaHei", FontStyle.Normal, weight), size, brush); context.DrawText(formatted, new Point(x, y)); } private static void DrawLine(DrawingContext context, double ox, double oy, double sc, double x1Design, double y1Design, double x2Design, double y2Design, IBrush brush, double thickness) { context.DrawLine(new Pen(brush, thickness), new Point(S(ox, sc, x1Design), S(oy, sc, y1Design)), new Point(S(ox, sc, x2Design), S(oy, sc, y2Design))); } private static Color GetColor(IBrush brush) => brush is SolidColorBrush solid ? solid.Color : Colors.Gray; protected override Size MeasureOverride(Size availableSize) => new(760, 420); }