PumpStation2DControl.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. using Avalonia;
  2. using Avalonia.Controls;
  3. using Avalonia.Media;
  4. using Avalonia.Threading;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Globalization;
  8. using YZWater.Core.ViewModels;
  9. namespace YZWater.Avalonia.Controls;
  10. /// <summary>
  11. /// 通用泵站 2D 工艺流程图控件 (SCADA 风格)。
  12. /// 设计画布 960×540,等比缩放适配实际尺寸。
  13. /// </summary>
  14. public class PumpStation2DControl : Control
  15. {
  16. // ═══ Styled Properties(接口不变,保持 ViewAView.axaml 绑定兼容) ═══
  17. public static readonly StyledProperty<double> Tank1LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank1Level));
  18. public static readonly StyledProperty<double> Tank2LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank2Level));
  19. public static readonly StyledProperty<double> Tank3LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank3Level));
  20. public static readonly StyledProperty<double> Tank4LevelProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Tank4Level));
  21. public static readonly StyledProperty<double> InflowRateProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(InflowRate));
  22. public static readonly StyledProperty<double> OutflowRateProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(OutflowRate));
  23. public static readonly StyledProperty<bool> Pump1RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump1Running));
  24. public static readonly StyledProperty<bool> Pump2RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump2Running));
  25. public static readonly StyledProperty<bool> Pump3RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump3Running));
  26. public static readonly StyledProperty<bool> Pump4RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump4Running));
  27. public static readonly StyledProperty<bool> Pump5RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Pump5Running));
  28. public static readonly StyledProperty<bool> Fan1RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Fan1Running));
  29. public static readonly StyledProperty<bool> Fan2RunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(Fan2Running));
  30. public static readonly StyledProperty<ValveStatus> Valve1StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve1Status));
  31. public static readonly StyledProperty<ValveStatus> Valve2StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve2Status));
  32. public static readonly StyledProperty<ValveStatus> Valve3StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve3Status));
  33. public static readonly StyledProperty<ValveStatus> Valve4StatusProperty = AvaloniaProperty.Register<PumpStation2DControl, ValveStatus>(nameof(Valve4Status));
  34. public static readonly StyledProperty<bool> IsInflowRunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(IsInflowRunning));
  35. public static readonly StyledProperty<bool> IsOutflowRunningProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(IsOutflowRunning));
  36. public static readonly StyledProperty<bool> HasAlarmProperty = AvaloniaProperty.Register<PumpStation2DControl, bool>(nameof(HasAlarm));
  37. public static readonly StyledProperty<int> AlarmCountProperty = AvaloniaProperty.Register<PumpStation2DControl, int>(nameof(AlarmCount));
  38. public static readonly StyledProperty<int> RunningDeviceCountProperty = AvaloniaProperty.Register<PumpStation2DControl, int>(nameof(RunningDeviceCount));
  39. public static readonly StyledProperty<double> EfficiencyProperty = AvaloniaProperty.Register<PumpStation2DControl, double>(nameof(Efficiency));
  40. public double Tank1Level { get => GetValue(Tank1LevelProperty); set => SetValue(Tank1LevelProperty, value); }
  41. public double Tank2Level { get => GetValue(Tank2LevelProperty); set => SetValue(Tank2LevelProperty, value); }
  42. public double Tank3Level { get => GetValue(Tank3LevelProperty); set => SetValue(Tank3LevelProperty, value); }
  43. public double Tank4Level { get => GetValue(Tank4LevelProperty); set => SetValue(Tank4LevelProperty, value); }
  44. public double InflowRate { get => GetValue(InflowRateProperty); set => SetValue(InflowRateProperty, value); }
  45. public double OutflowRate { get => GetValue(OutflowRateProperty); set => SetValue(OutflowRateProperty, value); }
  46. public bool Pump1Running { get => GetValue(Pump1RunningProperty); set => SetValue(Pump1RunningProperty, value); }
  47. public bool Pump2Running { get => GetValue(Pump2RunningProperty); set => SetValue(Pump2RunningProperty, value); }
  48. public bool Pump3Running { get => GetValue(Pump3RunningProperty); set => SetValue(Pump3RunningProperty, value); }
  49. public bool Pump4Running { get => GetValue(Pump4RunningProperty); set => SetValue(Pump4RunningProperty, value); }
  50. public bool Pump5Running { get => GetValue(Pump5RunningProperty); set => SetValue(Pump5RunningProperty, value); }
  51. public bool Fan1Running { get => GetValue(Fan1RunningProperty); set => SetValue(Fan1RunningProperty, value); }
  52. public bool Fan2Running { get => GetValue(Fan2RunningProperty); set => SetValue(Fan2RunningProperty, value); }
  53. public ValveStatus Valve1Status { get => GetValue(Valve1StatusProperty); set => SetValue(Valve1StatusProperty, value); }
  54. public ValveStatus Valve2Status { get => GetValue(Valve2StatusProperty); set => SetValue(Valve2StatusProperty, value); }
  55. public ValveStatus Valve3Status { get => GetValue(Valve3StatusProperty); set => SetValue(Valve3StatusProperty, value); }
  56. public ValveStatus Valve4Status { get => GetValue(Valve4StatusProperty); set => SetValue(Valve4StatusProperty, value); }
  57. public bool IsInflowRunning { get => GetValue(IsInflowRunningProperty); set => SetValue(IsInflowRunningProperty, value); }
  58. public bool IsOutflowRunning { get => GetValue(IsOutflowRunningProperty); set => SetValue(IsOutflowRunningProperty, value); }
  59. public bool HasAlarm { get => GetValue(HasAlarmProperty); set => SetValue(HasAlarmProperty, value); }
  60. public int AlarmCount { get => GetValue(AlarmCountProperty); set => SetValue(AlarmCountProperty, value); }
  61. public int RunningDeviceCount { get => GetValue(RunningDeviceCountProperty); set => SetValue(RunningDeviceCountProperty, value); }
  62. public double Efficiency { get => GetValue(EfficiencyProperty); set => SetValue(EfficiencyProperty, value); }
  63. // ═══ 动画 ═══
  64. private double _phase;
  65. private double _pumpAngle;
  66. private double _fanAngle;
  67. private IDisposable? _timer;
  68. // ═══ 静态画刷缓存(避免每帧 new SolidColorBrush) ═══
  69. private static readonly SolidColorBrush PipeWallBrush = new(Color.FromRgb(55, 65, 81));
  70. private static readonly SolidColorBrush PipeInnerBrush = new(Color.FromRgb(97, 111, 134));
  71. private static readonly SolidColorBrush FlowDotBrush = new(Color.FromRgb(3, 169, 244), 0.85);
  72. static PumpStation2DControl()
  73. {
  74. AffectsRender<PumpStation2DControl>(
  75. Tank1LevelProperty, Tank2LevelProperty, Tank3LevelProperty, Tank4LevelProperty,
  76. InflowRateProperty, OutflowRateProperty,
  77. Pump1RunningProperty, Pump2RunningProperty, Pump3RunningProperty, Pump4RunningProperty, Pump5RunningProperty,
  78. Fan1RunningProperty, Fan2RunningProperty,
  79. Valve1StatusProperty, Valve2StatusProperty, Valve3StatusProperty, Valve4StatusProperty,
  80. IsInflowRunningProperty, IsOutflowRunningProperty, HasAlarmProperty, AlarmCountProperty,
  81. RunningDeviceCountProperty, EfficiencyProperty);
  82. }
  83. protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
  84. {
  85. base.OnAttachedToVisualTree(e);
  86. ThemeHelper.ThemeChanged += OnThemeChanged;
  87. _timer = DispatcherTimer.Run(() =>
  88. {
  89. var needsAnimation = AnyPumpRunning || Fan1Running || Fan2Running
  90. || IsInflowRunning || IsOutflowRunning;
  91. if (needsAnimation)
  92. {
  93. _phase = (_phase + 1) % 360;
  94. if (AnyPumpRunning) _pumpAngle = (_pumpAngle + 4) % 360;
  95. if (Fan1Running || Fan2Running) _fanAngle = (_fanAngle + 6) % 360;
  96. InvalidateVisual();
  97. }
  98. return true;
  99. }, TimeSpan.FromMilliseconds(33));
  100. }
  101. protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
  102. {
  103. ThemeHelper.ThemeChanged -= OnThemeChanged;
  104. _timer?.Dispose();
  105. _timer = null;
  106. base.OnDetachedFromVisualTree(e);
  107. }
  108. private void OnThemeChanged() => InvalidateVisual();
  109. private bool AnyPumpRunning => Pump1Running || Pump2Running || Pump3Running || Pump4Running || Pump5Running;
  110. // ═══ 设计坐标常量 (960 × 540) ═══
  111. private const double DW = 960, DH = 540;
  112. // 水箱位置: (x, y, width, height)
  113. private static readonly Rect DTank1 = new(55, 130, 120, 140);
  114. private static readonly Rect DTank2 = new(275, 85, 195, 210);
  115. private static readonly Rect DTank3 = new(275, 400, 155, 105);
  116. private static readonly Rect DTank4 = new(720, 130, 120, 140);
  117. // 泵位置 (5 台泵在集水井下方)
  118. private static readonly Point[] DPumps = { new(300, 340), new(338, 340), new(376, 340), new(414, 340), new(452, 340) };
  119. // 风机位置
  120. private static readonly Point DFan1 = new(590, 450);
  121. private static readonly Point DFan2 = new(660, 450);
  122. // 阀门位置 (管道上的关键节点)
  123. private static readonly Point DV1 = new(218, 200); // Tank1 → Tank2
  124. private static readonly Point DV2 = new(555, 195); // Header → Tank4
  125. private static readonly Point DV3 = new(420, 395); // Header → Tank3 分支
  126. private static readonly Point DV4 = new(230, 450); // Tank2 → Tank3
  127. // 流量计位置
  128. private static readonly Point DFM1 = new(22, 195);
  129. private static readonly Point DFM2 = new(870, 195);
  130. // ═══ 主渲染 ═══
  131. public override void Render(DrawingContext context)
  132. {
  133. base.Render(context);
  134. var bounds = new Rect(Bounds.Size);
  135. if (bounds.Width < 120 || bounds.Height < 120) return;
  136. // 背景
  137. context.DrawRectangle(ThemeHelper.PanelBg, null, bounds);
  138. // 计算缩放:设计坐标 → 屏幕坐标
  139. var margin = 8.0;
  140. var scaleX = (bounds.Width - 2 * margin) / DW;
  141. var scaleY = (bounds.Height - 2 * margin) / DH;
  142. var scale = Math.Min(scaleX, scaleY);
  143. var offsetX = margin + (bounds.Width - 2 * margin - DW * scale) / 2;
  144. var offsetY = margin + (bounds.Height - 2 * margin - DH * scale) / 2;
  145. // 地面线
  146. DrawLine(context, offsetX, offsetY, scale, 0, 520, 960, 520, ThemeHelper.Border, 1);
  147. // ═══ 管道(先画,这样设备覆盖在上面)═══
  148. // 进水管 → FM1 → Tank1
  149. DrawPipeH(context, offsetX, offsetY, scale, 0, 200, 55, IsInflowRunning, 10);
  150. DrawFlowMeter(context, offsetX, offsetY, scale, DFM1, "进水", InflowRate, IsInflowRunning);
  151. DrawPipeH(context, offsetX, offsetY, scale, 48, 200, 55, IsInflowRunning, 10);
  152. // Tank1 → V1 → Tank2
  153. DrawPipeH(context, offsetX, offsetY, scale, 175, 200, 195, IsInflowRunning, 8);
  154. DrawValve(context, offsetX, offsetY, scale, DV1, "V1", Valve1Status);
  155. DrawPipeH(context, offsetX, offsetY, scale, 240, 200, 275, IsInflowRunning, 8);
  156. // Tank2 底部 → 泵吸入管
  157. DrawPipeV(context, offsetX, offsetY, scale, 372, 295, 325, AnyPumpRunning, 8);
  158. // 泵排出总管
  159. DrawPipeH(context, offsetX, offsetY, scale, 285, 365, 470, AnyPumpRunning, 10);
  160. // 总管上弯 → V2 → Tank4
  161. DrawPipeV(context, offsetX, offsetY, scale, 500, 365, 195, IsOutflowRunning, 10);
  162. DrawPipeH(context, offsetX, offsetY, scale, 500, 195, 530, IsOutflowRunning, 10);
  163. DrawValve(context, offsetX, offsetY, scale, DV2, "V2", Valve2Status);
  164. DrawPipeH(context, offsetX, offsetY, scale, 580, 195, 720, IsOutflowRunning, 10);
  165. // Tank4 → FM2 → 出水
  166. DrawPipeH(context, offsetX, offsetY, scale, 840, 200, 860, IsOutflowRunning, 10);
  167. DrawFlowMeter(context, offsetX, offsetY, scale, DFM2, "出水", OutflowRate, IsOutflowRunning);
  168. DrawPipeH(context, offsetX, offsetY, scale, 896, 200, 960, IsOutflowRunning, 10);
  169. // V3 分支:总管 → Tank3
  170. DrawPipeV(context, offsetX, offsetY, scale, 420, 365, 378, false, 8);
  171. DrawValve(context, offsetX, offsetY, scale, DV3, "V3", Valve3Status);
  172. DrawPipeV(context, offsetX, offsetY, scale, 420, 412, 400, false, 8);
  173. // V4 分支:Tank2 → Tank3
  174. DrawPipeV(context, offsetX, offsetY, scale, 230, 295, 430, false, 8);
  175. DrawPipeH(context, offsetX, offsetY, scale, 230, 450, 275, false, 8);
  176. DrawValve(context, offsetX, offsetY, scale, DV4, "V4", Valve4Status);
  177. // ═══ 水箱 ═══
  178. DrawTank(context, offsetX, offsetY, scale, DTank1, Tank1Level, "进水池", "IN",
  179. Color.FromRgb(0, 137, 123)); // Teal
  180. DrawTank(context, offsetX, offsetY, scale, DTank2, Tank2Level, "集水井", "SUMP",
  181. Color.FromRgb(33, 150, 243)); // Blue
  182. DrawTank(context, offsetX, offsetY, scale, DTank3, Tank3Level, "调蓄池", "BUF",
  183. Color.FromRgb(126, 87, 194)); // Purple
  184. DrawTank(context, offsetX, offsetY, scale, DTank4, Tank4Level, "出水池", "OUT",
  185. Color.FromRgb(0, 150, 136)); // Green
  186. // ═══ 泵组 ═══
  187. bool[] pumpRunning = { Pump1Running, Pump2Running, Pump3Running, Pump4Running, Pump5Running };
  188. string[] pumpLabels = { "P1", "P2", "P3", "P4", "P5" };
  189. for (int i = 0; i < 5; i++)
  190. DrawPump(context, offsetX, offsetY, scale, DPumps[i], pumpLabels[i], pumpRunning[i]);
  191. // ═══ 风机 ═══
  192. DrawFan(context, offsetX, offsetY, scale, DFan1, "F1", Fan1Running);
  193. DrawFan(context, offsetX, offsetY, scale, DFan2, "F2", Fan2Running);
  194. // 风机区域框
  195. var fanAreaX = S(offsetX, scale, 555);
  196. var fanAreaY = S(offsetY, scale, 418);
  197. var fanAreaW = W(scale, 180);
  198. var fanAreaH = H(scale, 75);
  199. context.DrawRectangle(null, new Pen(ThemeHelper.Border, 1),
  200. new Rect(fanAreaX, fanAreaY, fanAreaW, fanAreaH), 3, 3);
  201. DrawLabel(context, offsetX, offsetY, scale, 560, 422, "风机区", 10, ThemeHelper.TextTertiary);
  202. // ═══ 报警边框 ═══
  203. if (HasAlarm)
  204. {
  205. var sceneRect = new Rect(margin, margin, bounds.Width - 2 * margin, bounds.Height - 2 * margin);
  206. context.DrawRectangle(null, new Pen(ThemeHelper.Danger, 2), sceneRect, 4);
  207. }
  208. // ═══ HUD 状态面板 ═══
  209. DrawHud(context, bounds, scale);
  210. }
  211. // ═══ 坐标转换辅助 ═══
  212. private static double S(double offset, double scale, double designVal) => offset + designVal * scale;
  213. private static double W(double scale, double designW) => designW * scale;
  214. private static double H(double scale, double designH) => designH * scale;
  215. private static double R(double scale, double designR) => designR * scale;
  216. // ═══ 水箱绘制 ═══
  217. private void DrawTank(DrawingContext context, double ox, double oy, double sc,
  218. Rect designRect, double level, string name, string code, Color waterColor)
  219. {
  220. var x = S(ox, sc, designRect.X);
  221. var y = S(oy, sc, designRect.Y);
  222. var w = W(sc, designRect.Width);
  223. var h = H(sc, designRect.Height);
  224. var rect = new Rect(x, y, w, h);
  225. // 箱体
  226. context.DrawRectangle(ThemeHelper.SurfaceBg, new Pen(ThemeHelper.Border, 1.5), rect, 3, 3);
  227. // 水位填充
  228. var clampedLevel = Math.Clamp(level, 0, 100);
  229. if (clampedLevel > 0.5)
  230. {
  231. var waterH = (h - 4) * clampedLevel / 100.0;
  232. var waterRect = new Rect(x + 2, y + h - 2 - waterH, w - 4, waterH);
  233. var waterBrush = new SolidColorBrush(waterColor, 0.65);
  234. context.DrawRectangle(waterBrush, null, waterRect);
  235. // 水面波纹
  236. var waveY = waterRect.Y;
  237. var waveBrush = new SolidColorBrush(waterColor, 0.35);
  238. for (int i = 0; i < 3; i++)
  239. {
  240. var wx = waterRect.X + 4 + i * (waterRect.Width / 3.0);
  241. context.DrawLine(new Pen(waveBrush, 1),
  242. new Point(wx, waveY), new Point(wx + waterRect.Width / 6.0, waveY + 2));
  243. }
  244. }
  245. // 名称标签
  246. var fontSize = Math.Max(8, 11 * sc);
  247. DrawLabel(context, ox, oy, sc, designRect.Center.X - 20, designRect.Y + 6, name, 11, ThemeHelper.TextPrimary, FontWeight.Bold);
  248. DrawLabel(context, ox, oy, sc, designRect.Center.X - 14, designRect.Y + 20, code, 9, ThemeHelper.TextTertiary);
  249. // 液位百分比
  250. var pctText = $"{clampedLevel:F1}%";
  251. var pctColor = clampedLevel > 80 ? ThemeHelper.Warning :
  252. clampedLevel < 20 ? ThemeHelper.Danger :
  253. new SolidColorBrush(waterColor);
  254. DrawLabel(context, ox, oy, sc, designRect.Center.X - 18, designRect.Center.Y + 8, pctText, 13, pctColor, FontWeight.Bold);
  255. }
  256. // ═══ 管道绘制 ═══
  257. private void DrawPipeH(DrawingContext context, double ox, double oy, double sc,
  258. double x1Design, double yDesign, double x2Design, bool isFlowing, double thicknessDesign)
  259. {
  260. var x1 = S(ox, sc, x1Design);
  261. var y = S(oy, sc, yDesign);
  262. var x2 = S(ox, sc, x2Design);
  263. var thickness = R(sc, thicknessDesign);
  264. // 管壁
  265. context.DrawLine(new Pen(PipeWallBrush, thickness),
  266. new Point(x1, y), new Point(x2, y));
  267. // 内管
  268. context.DrawLine(new Pen(PipeInnerBrush, thickness * 0.45),
  269. new Point(x1, y), new Point(x2, y));
  270. // 流动动画
  271. if (isFlowing && x2 > x1 + 2)
  272. DrawFlowDots(context, x1, y, x2, y);
  273. }
  274. private void DrawPipeV(DrawingContext context, double ox, double oy, double sc,
  275. double xDesign, double y1Design, double y2Design, bool isFlowing, double thicknessDesign)
  276. {
  277. var x = S(ox, sc, xDesign);
  278. var y1 = S(oy, sc, y1Design);
  279. var y2 = S(oy, sc, y2Design);
  280. var thickness = R(sc, thicknessDesign);
  281. context.DrawLine(new Pen(PipeWallBrush, thickness),
  282. new Point(x, y1), new Point(x, y2));
  283. context.DrawLine(new Pen(PipeInnerBrush, thickness * 0.45),
  284. new Point(x, y1), new Point(x, y2));
  285. if (isFlowing && Math.Abs(y2 - y1) > 2)
  286. DrawFlowDots(context, x, y1, x, y2);
  287. }
  288. private void DrawFlowDots(DrawingContext context, double x1, double y1, double x2, double y2)
  289. {
  290. var dx = x2 - x1;
  291. var dy = y2 - y1;
  292. var len = Math.Sqrt(dx * dx + dy * dy);
  293. if (len < 4) return;
  294. var count = Math.Max(2, (int)(len / 18));
  295. for (int i = 0; i < count; i++)
  296. {
  297. var t = ((_phase / 360.0) + (double)i / count) % 1.0;
  298. var px = x1 + dx * t;
  299. var py = y1 + dy * t;
  300. var r = Math.Max(1.5, 3);
  301. context.DrawEllipse(FlowDotBrush, null, new Rect(px - r, py - r, r * 2, r * 2));
  302. }
  303. }
  304. // ═══ 泵绘制 ═══
  305. private void DrawPump(DrawingContext context, double ox, double oy, double sc,
  306. Point designPos, string label, bool running)
  307. {
  308. var cx = S(ox, sc, designPos.X);
  309. var cy = S(oy, sc, designPos.Y);
  310. var r = R(sc, 18);
  311. var statusColor = running ? GetColor(ThemeHelper.Success) : GetColor(ThemeHelper.TextDisabled);
  312. var statusBrush = new SolidColorBrush(statusColor);
  313. // 运行光晕
  314. if (running)
  315. context.DrawEllipse(new SolidColorBrush(statusColor, 0.18), null,
  316. new Rect(cx - r * 1.4, cy - r * 1.4, r * 2.8, r * 2.8));
  317. // 泵体圆
  318. context.DrawEllipse(ThemeHelper.SurfaceBg, new Pen(statusBrush, running ? 2 : 1.2),
  319. new Rect(cx - r, cy - r, r * 2, r * 2));
  320. // 旋转叶片
  321. var bladeCount = 4;
  322. for (int i = 0; i < bladeCount; i++)
  323. {
  324. var angle = (running ? _pumpAngle : 0) + i * (360.0 / bladeCount);
  325. var rad = angle * Math.PI / 180.0;
  326. var bx = cx + Math.Cos(rad) * r * 0.65;
  327. var by = cy + Math.Sin(rad) * r * 0.65;
  328. var bladeBrush = new SolidColorBrush(statusColor, running ? 0.9 : 0.45);
  329. context.DrawLine(new Pen(bladeBrush, Math.Max(1.5, 2.5 * sc)),
  330. new Point(cx, cy), new Point(bx, by));
  331. }
  332. // 中心圆
  333. context.DrawEllipse(statusBrush, null,
  334. new Rect(cx - r * 0.22, cy - r * 0.22, r * 0.44, r * 0.44));
  335. // 标签
  336. DrawLabelAt(context, cx, cy + r + 3, label, Math.Max(8, 10 * sc), ThemeHelper.TextPrimary, FontWeight.Bold);
  337. }
  338. // ═══ 风机绘制 ═══
  339. private void DrawFan(DrawingContext context, double ox, double oy, double sc,
  340. Point designPos, string label, bool running)
  341. {
  342. var cx = S(ox, sc, designPos.X);
  343. var cy = S(oy, sc, designPos.Y);
  344. var r = R(sc, 20);
  345. var statusColor = running ? GetColor(ThemeHelper.Info) : GetColor(ThemeHelper.TextDisabled);
  346. var statusBrush = new SolidColorBrush(statusColor);
  347. // 外壳
  348. context.DrawEllipse(ThemeHelper.SurfaceBg, new Pen(statusBrush, running ? 2 : 1.2),
  349. new Rect(cx - r, cy - r, r * 2, r * 2));
  350. // 旋转叶片 (5 片)
  351. var bladeCount = 5;
  352. for (int i = 0; i < bladeCount; i++)
  353. {
  354. var angle = (running ? _fanAngle : 0) + i * (360.0 / bladeCount);
  355. var rad = angle * Math.PI / 180.0;
  356. var bx = cx + Math.Cos(rad) * r * 0.72;
  357. var by = cy + Math.Sin(rad) * r * 0.72;
  358. var bladeBrush = new SolidColorBrush(statusColor, running ? 0.85 : 0.4);
  359. context.DrawLine(new Pen(bladeBrush, Math.Max(2, 3 * sc)),
  360. new Point(cx, cy), new Point(bx, by));
  361. }
  362. // 中心
  363. context.DrawEllipse(statusBrush, null,
  364. new Rect(cx - r * 0.18, cy - r * 0.18, r * 0.36, r * 0.36));
  365. // 标签
  366. DrawLabelAt(context, cx, cy + r + 4, label, Math.Max(8, 10 * sc), ThemeHelper.TextPrimary, FontWeight.Bold);
  367. }
  368. // ═══ 阀门绘制(蝴蝶阀符号)═══
  369. private void DrawValve(DrawingContext context, double ox, double oy, double sc,
  370. Point designPos, string label, ValveStatus status)
  371. {
  372. var cx = S(ox, sc, designPos.X);
  373. var cy = S(oy, sc, designPos.Y);
  374. var s = R(sc, 12);
  375. var color = status switch
  376. {
  377. ValveStatus.Open => GetColor(ThemeHelper.Success),
  378. ValveStatus.Middle => GetColor(ThemeHelper.Warning),
  379. ValveStatus.Closed => GetColor(ThemeHelper.Danger),
  380. _ => GetColor(ThemeHelper.TextDisabled)
  381. };
  382. var colorBrush = new SolidColorBrush(color);
  383. // 蝴蝶阀:两个三角形
  384. var top = new Point(cx, cy - s);
  385. var bottom = new Point(cx, cy + s);
  386. var left = new Point(cx - s * 0.7, cy);
  387. var right = new Point(cx + s * 0.7, cy);
  388. // 上三角
  389. var tri1 = new StreamGeometry();
  390. using (var ctx = tri1.Open())
  391. {
  392. ctx.BeginFigure(left, true);
  393. ctx.LineTo(top);
  394. ctx.LineTo(right);
  395. ctx.EndFigure(true);
  396. }
  397. context.DrawGeometry(new SolidColorBrush(color, 0.7), null, tri1);
  398. // 下三角(倒置)
  399. var tri2 = new StreamGeometry();
  400. using (var ctx = tri2.Open())
  401. {
  402. ctx.BeginFigure(left, true);
  403. ctx.LineTo(bottom);
  404. ctx.LineTo(right);
  405. ctx.EndFigure(true);
  406. }
  407. context.DrawGeometry(new SolidColorBrush(color, 0.7), null, tri2);
  408. // 阀杆
  409. context.DrawLine(new Pen(colorBrush, Math.Max(1, 1.5 * sc)),
  410. new Point(cx - s * 0.5, cy), new Point(cx + s * 0.5, cy));
  411. // 状态指示点
  412. var lampR = Math.Max(2, 3 * sc);
  413. context.DrawEllipse(colorBrush, null,
  414. new Rect(cx - lampR, cy - s - lampR * 2.5, lampR * 2, lampR * 2));
  415. // 标签
  416. DrawLabelAt(context, cx, cy + s + 3, label, Math.Max(7, 9 * sc), ThemeHelper.TextSecondary, FontWeight.Bold);
  417. }
  418. // ═══ 流量计绘制 ═══
  419. private void DrawFlowMeter(DrawingContext context, double ox, double oy, double sc,
  420. Point designPos, string label, double value, bool active)
  421. {
  422. var cx = S(ox, sc, designPos.X);
  423. var cy = S(oy, sc, designPos.Y);
  424. var w = W(sc, 56);
  425. var h = H(sc, 32);
  426. var color = active ? GetColor(ThemeHelper.Success) : GetColor(ThemeHelper.TextDisabled);
  427. var colorBrush = new SolidColorBrush(color);
  428. // 外框
  429. var rect = new Rect(cx - w / 2, cy - h / 2, w, h);
  430. context.DrawRectangle(new SolidColorBrush(GetColor(ThemeHelper.SurfaceBg), 0.95),
  431. new Pen(colorBrush, 1.2), rect, 2, 2);
  432. // 标签
  433. DrawLabelAt(context, cx, cy - h * 0.18, label, Math.Max(7, 8 * sc), ThemeHelper.TextTertiary);
  434. // 数值
  435. DrawLabelAt(context, cx, cy + h * 0.12, $"{value:F1}", Math.Max(8, 10 * sc), colorBrush, FontWeight.Bold);
  436. }
  437. // ═══ HUD 状态面板 ═══
  438. private void DrawHud(DrawingContext context, Rect bounds, double scale)
  439. {
  440. var panelW = Math.Max(120, 185 * Math.Min(scale, 1.5));
  441. var panelH = Math.Max(45, 58 * Math.Min(scale, 1.5));
  442. var panelX = bounds.Right - panelW - 10;
  443. var panelY = bounds.Top + 8;
  444. var panelRect = new Rect(panelX, panelY, panelW, panelH);
  445. context.DrawRectangle(new SolidColorBrush(GetColor(ThemeHelper.SurfaceBg), 0.92),
  446. new Pen(ThemeHelper.Border, 1), panelRect, 3, 3);
  447. var fontSize = Math.Max(8, 10 * Math.Min(scale, 1.5));
  448. var smallFont = Math.Max(7, 9 * Math.Min(scale, 1.5));
  449. DrawLabelAt(context, panelX + 8, panelY + 6, "泵站总览", fontSize * 1.15, ThemeHelper.TextPrimary, FontWeight.Bold);
  450. DrawLabelAt(context, panelX + 8, panelY + 6 + fontSize * 1.5,
  451. $"运行 {RunningDeviceCount} 效率 {Efficiency:F0}%", smallFont, ThemeHelper.TextSecondary);
  452. var alarmColor = HasAlarm ? ThemeHelper.Danger : ThemeHelper.Success;
  453. DrawLabelAt(context, panelX + 8, panelY + 6 + fontSize * 1.5 + smallFont * 1.5,
  454. $"报警 {AlarmCount}", smallFont, alarmColor, FontWeight.Bold);
  455. }
  456. // ═══ 文字绘制辅助 ═══
  457. private void DrawLabel(DrawingContext context, double ox, double oy, double sc,
  458. double designX, double designY, string text, double designFontSize,
  459. IBrush brush, FontWeight weight = FontWeight.Normal)
  460. {
  461. var x = S(ox, sc, designX);
  462. var y = S(oy, sc, designY);
  463. var size = Math.Max(8, designFontSize * sc);
  464. DrawLabelAt(context, x, y, text, size, brush, weight);
  465. }
  466. private static void DrawLabelAt(DrawingContext context, double x, double y, string text,
  467. double size, IBrush brush, FontWeight weight = FontWeight.Normal)
  468. {
  469. var formatted = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
  470. new Typeface("Microsoft YaHei", FontStyle.Normal, weight), size, brush);
  471. context.DrawText(formatted, new Point(x, y));
  472. }
  473. private static void DrawLine(DrawingContext context, double ox, double oy, double sc,
  474. double x1Design, double y1Design, double x2Design, double y2Design, IBrush brush, double thickness)
  475. {
  476. context.DrawLine(new Pen(brush, thickness),
  477. new Point(S(ox, sc, x1Design), S(oy, sc, y1Design)),
  478. new Point(S(ox, sc, x2Design), S(oy, sc, y2Design)));
  479. }
  480. private static Color GetColor(IBrush brush) => brush is SolidColorBrush solid ? solid.Color : Colors.Gray;
  481. protected override Size MeasureOverride(Size availableSize) => new(760, 420);
  482. }