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);
}