| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- 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;
- /// <summary>
- /// 通用泵站 2D 工艺流程图控件 (SCADA 风格)。
- /// 设计画布 960×540,等比缩放适配实际尺寸。
- /// </summary>
- public class PumpStation2DControl : Control
- {
- // ═══ Styled Properties(接口不变,保持 ViewAView.axaml 绑定兼容) ═══
- public static readonly StyledProperty<double> Tank1LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank1Level));
- public static readonly StyledProperty<double> Tank2LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank2Level));
- public static readonly StyledProperty<double> Tank3LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank3Level));
- public static readonly StyledProperty<double> Tank4LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank4Level));
- public static readonly StyledProperty<double> InflowRateProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(InflowRate));
- public static readonly StyledProperty<double> OutflowRateProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(OutflowRate));
- public static readonly StyledProperty<bool> Pump1RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump1Running));
- public static readonly StyledProperty<bool> Pump2RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump2Running));
- public static readonly StyledProperty<bool> Pump3RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump3Running));
- public static readonly StyledProperty<bool> Pump4RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump4Running));
- public static readonly StyledProperty<bool> Pump5RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump5Running));
- public static readonly StyledProperty<bool> Fan1RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Fan1Running));
- public static readonly StyledProperty<bool> Fan2RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Fan2Running));
- public static readonly StyledProperty<ValveStatus> Valve1StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve1Status));
- public static readonly StyledProperty<ValveStatus> Valve2StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve2Status));
- public static readonly StyledProperty<ValveStatus> Valve3StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve3Status));
- public static readonly StyledProperty<ValveStatus> Valve4StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve4Status));
- public static readonly StyledProperty<bool> IsInflowRunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(IsInflowRunning));
- public static readonly StyledProperty<bool> IsOutflowRunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(IsOutflowRunning));
- public static readonly StyledProperty<bool> HasAlarmProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(HasAlarm));
- public static readonly StyledProperty<int> AlarmCountProperty = AvaloniaProperty.Register<PumpStation2DControl, int>(nameof(AlarmCount));
- public static readonly StyledProperty<int> RunningDeviceCountProperty = AvaloniaProperty.Register<PumpStation2DControl, int>(nameof(RunningDeviceCount));
- public static readonly StyledProperty<double> EfficiencyProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(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<PumpStation2DControl>(
- 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);
- }
|