銈姐兗銈广倰鍙傜収

淇娴呰壊涓婚鎺т欢涓嶈窡闅 - 姣忎釜鎺т欢鐩戝惉ThemeHelper.ThemeChanged寮哄埗閲嶇粯

纾 鏇 6 鏃 鍓
銈炽儫銉冦儓
a59c6e6203

+ 3 - 11
src/YZWater.Avalonia/App.axaml.cs

@@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Markup.Xaml;
 using Avalonia.Markup.Xaml.Styling;
 using Avalonia.Threading;
+using YZWater.Avalonia.Controls;
 using YZWater.Avalonia.Views;
 using YZWater.Core.Services;
 
@@ -50,16 +51,7 @@ public partial class App : Application
 
         resources[0] = ThemeService.Instance.IsDarkTheme ? _darkTheme : _lightTheme;
 
-        // 寤惰繜閲嶇粯锛岀‘淇濊祫婧愬瓧鍏告洿鏂板畬鎴
-        Dispatcher.UIThread.Post(() =>
-        {
-            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
-            {
-                foreach (var window in desktop.Windows)
-                {
-                    window.InvalidateVisual();
-                }
-            }
-        }, DispatcherPriority.Render);
+        // 閫氱煡鎺т欢閲嶇粯
+        ThemeHelper.NotifyThemeChanged();
     }
 }

+ 26 - 20
src/YZWater.Avalonia/Controls/FanControl.cs

@@ -9,12 +9,9 @@ namespace YZWater.Avalonia.Controls;
 
 public class FanControl : Control
 {
-    public static readonly StyledProperty<bool> IsRunningProperty =
-        AvaloniaProperty.Register<FanControl, bool>(nameof(IsRunning), false);
-    public static readonly StyledProperty<double> SpeedProperty =
-        AvaloniaProperty.Register<FanControl, double>(nameof(Speed), 1.0);
-    public static readonly StyledProperty<string> TextProperty =
-        AvaloniaProperty.Register<FanControl, string>(nameof(Text), "椋庢墖");
+    public static readonly StyledProperty<bool> IsRunningProperty = AvaloniaProperty.Register<FanControl, bool>(nameof(IsRunning), false);
+    public static readonly StyledProperty<double> SpeedProperty = AvaloniaProperty.Register<FanControl, double>(nameof(Speed), 1.0);
+    public static readonly StyledProperty<string> TextProperty = AvaloniaProperty.Register<FanControl, string>(nameof(Text), "椋庢墖");
 
     public bool IsRunning { get => GetValue(IsRunningProperty); set => SetValue(IsRunningProperty, value); }
     public double Speed { get => GetValue(SpeedProperty); set => SetValue(SpeedProperty, value); }
@@ -24,13 +21,29 @@ public class FanControl : Control
     private IDisposable? _timer;
 
     static FanControl() { AffectsRender<FanControl>(IsRunningProperty, SpeedProperty, TextProperty); }
-    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); if (IsRunning) StartAnim(); }
-    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); StopAnim(); }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        ThemeHelper.ThemeChanged += OnThemeChanged;
+        if (IsRunning) StartAnim();
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        ThemeHelper.ThemeChanged -= OnThemeChanged;
+        StopAnim();
+        base.OnDetachedFromVisualTree(e);
+    }
+
+    private void OnThemeChanged() => InvalidateVisual();
+
     protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
     {
         base.OnPropertyChanged(change);
         if (change.Property == IsRunningProperty) { if (IsRunning) StartAnim(); else { StopAnim(); _angle = 0; } }
     }
+
     private void StartAnim() { StopAnim(); _timer = DispatcherTimer.Run(() => { _angle += Speed * 2; if (_angle >= 360) _angle -= 360; InvalidateVisual(); return true; }, TimeSpan.FromMilliseconds(50)); }
     private void StopAnim() { _timer?.Dispose(); _timer = null; }
 
@@ -43,26 +56,19 @@ public class FanControl : Control
         var typeface = new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold);
         var fanColor = IsRunning ? ThemeHelper.Success() : ThemeHelper.TextDisabled();
 
-        // 澶栨
         context.DrawRectangle(null, new Pen(ThemeHelper.Border(), 2), new Rect(cx - radius, cy - radius, radius * 2, radius * 2));
 
-        // 鍙剁墖
-        var bladeCount = 4;
-        var bladeLen = radius * 0.8;
-        var bladeW = radius * 0.25;
-        for (int i = 0; i < bladeCount; i++)
+        for (int i = 0; i < 4; i++)
         {
-            var angle = _angle + (360.0 / bladeCount) * i;
-            var bx = cx + Math.Cos(angle * Math.PI / 180) * bladeLen * 0.4;
-            var by = cy + Math.Sin(angle * Math.PI / 180) * bladeLen * 0.4;
-            context.DrawRectangle(fanColor, null, new Rect(bx - bladeW / 2, by - bladeLen / 2, bladeW, bladeLen));
+            var angle = _angle + 90.0 * i;
+            var bx = cx + Math.Cos(angle * Math.PI / 180) * radius * 0.8 * 0.4;
+            var by = cy + Math.Sin(angle * Math.PI / 180) * radius * 0.8 * 0.4;
+            context.DrawRectangle(fanColor, null, new Rect(bx - radius * 0.125, by - radius * 0.4, radius * 0.25, radius * 0.8));
         }
 
-        // 涓績
         var cr = radius * 0.15;
         context.DrawEllipse(fanColor, null, new Rect(cx - cr, cy - cr, cr * 2, cr * 2));
 
-        // 鏂囧瓧
         if (!string.IsNullOrEmpty(Text))
         {
             var t = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 10, ThemeHelper.TextPrimary());

+ 11 - 23
src/YZWater.Avalonia/Controls/GaugeControl.cs

@@ -8,18 +8,12 @@ namespace YZWater.Avalonia.Controls;
 
 public class GaugeControl : Control
 {
-    public static readonly StyledProperty<double> ValueProperty =
-        AvaloniaProperty.Register<GaugeControl, double>(nameof(Value), 0.0);
-    public static readonly StyledProperty<double> MinValueProperty =
-        AvaloniaProperty.Register<GaugeControl, double>(nameof(MinValue), 0.0);
-    public static readonly StyledProperty<double> MaxValueProperty =
-        AvaloniaProperty.Register<GaugeControl, double>(nameof(MaxValue), 100.0);
-    public static readonly StyledProperty<string> TitleProperty =
-        AvaloniaProperty.Register<GaugeControl, string>(nameof(Title), "娴侀噺");
-    public static readonly StyledProperty<string> UnitProperty =
-        AvaloniaProperty.Register<GaugeControl, string>(nameof(Unit), "m鲁/h");
-    public static readonly StyledProperty<IBrush> ValueColorProperty =
-        AvaloniaProperty.Register<GaugeControl, IBrush>(nameof(ValueColor));
+    public static readonly StyledProperty<double> ValueProperty = AvaloniaProperty.Register<GaugeControl, double>(nameof(Value), 0.0);
+    public static readonly StyledProperty<double> MinValueProperty = AvaloniaProperty.Register<GaugeControl, double>(nameof(MinValue), 0.0);
+    public static readonly StyledProperty<double> MaxValueProperty = AvaloniaProperty.Register<GaugeControl, double>(nameof(MaxValue), 100.0);
+    public static readonly StyledProperty<string> TitleProperty = AvaloniaProperty.Register<GaugeControl, string>(nameof(Title), "娴侀噺");
+    public static readonly StyledProperty<string> UnitProperty = AvaloniaProperty.Register<GaugeControl, string>(nameof(Unit), "m鲁/h");
+    public static readonly StyledProperty<IBrush> ValueColorProperty = AvaloniaProperty.Register<GaugeControl, IBrush>(nameof(ValueColor));
 
     public double Value { get => GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
     public double MinValue { get => GetValue(MinValueProperty); set => SetValue(MinValueProperty, value); }
@@ -30,6 +24,10 @@ public class GaugeControl : Control
 
     static GaugeControl() { AffectsRender<GaugeControl>(ValueProperty, MinValueProperty, MaxValueProperty, TitleProperty, UnitProperty, ValueColorProperty); }
 
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); ThemeHelper.ThemeChanged += OnThemeChanged; }
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { ThemeHelper.ThemeChanged -= OnThemeChanged; base.OnDetachedFromVisualTree(e); }
+    private void OnThemeChanged() => InvalidateVisual();
+
     public override void Render(DrawingContext context)
     {
         base.Render(context);
@@ -38,20 +36,14 @@ public class GaugeControl : Control
         var radius = Math.Min(bounds.Width, bounds.Height) / 2 - 20;
         var typeface = new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold);
 
-        // 鑳屾櫙
         context.DrawEllipse(ThemeHelper.PanelBg(), null, new Rect(cx - radius - 5, cy - radius - 5, (radius + 5) * 2, (radius + 5) * 2));
+        DrawArc(context, new Pen(ThemeHelper.Border(), 8), cx, cy, radius, 135, 270);
 
-        // 寮х嚎鑳屾櫙
-        var arcBg = new Pen(ThemeHelper.Border(), 8);
-        DrawArc(context, arcBg, cx, cy, radius, 135, 270);
-
-        // 鍊煎姬绾
         var normalized = Math.Max(0, Math.Min(1, (Value - MinValue) / (MaxValue - MinValue)));
         var valueAngle = 135 + normalized * 270;
         var valueColor = ValueColor ?? ThemeHelper.Success();
         DrawArc(context, new Pen(valueColor, 8), cx, cy, radius, 135, valueAngle - 135);
 
-        // 鍒诲害
         for (int i = 0; i <= 10; i++)
         {
             var angle = 135 + (270.0 * i / 10);
@@ -63,21 +55,17 @@ public class GaugeControl : Control
                 new Point(cx + Math.Cos(rad) * radius, cy + Math.Sin(rad) * radius));
         }
 
-        // 鎸囬拡
         var pAngle = valueAngle * Math.PI / 180;
         var pLen = radius * 0.7;
         context.DrawLine(new Pen(valueColor, 3), new Point(cx, cy), new Point(cx + Math.Cos(pAngle) * pLen, cy + Math.Sin(pAngle) * pLen));
         context.DrawEllipse(valueColor, null, new Rect(cx - 6, cy - 6, 12, 12));
 
-        // 鏍囬
         var titleText = new FormattedText(Title, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12, ThemeHelper.TextPrimary());
         context.DrawText(titleText, new Point(cx - titleText.Width / 2, bounds.Y + 5));
 
-        // 鏁板
         var valText = new FormattedText($"{Value:F1}", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold), 20, ThemeHelper.TextPrimary());
         context.DrawText(valText, new Point(cx - valText.Width / 2, cy + radius * 0.3));
 
-        // 鍗曚綅
         var unitText = new FormattedText(Unit, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Microsoft YaHei"), 10, ThemeHelper.TextDisabled());
         context.DrawText(unitText, new Point(cx - unitText.Width / 2, cy + radius * 0.3 + valText.Height + 2));
     }

+ 26 - 22
src/YZWater.Avalonia/Controls/PipeLineControl.cs

@@ -8,18 +8,12 @@ namespace YZWater.Avalonia.Controls;
 
 public class PipeLineControl : Control
 {
-    public static readonly StyledProperty<bool> IsFlowProperty =
-        AvaloniaProperty.Register<PipeLineControl, bool>(nameof(IsFlow), false);
-    public static readonly StyledProperty<double> FlowSpeedProperty =
-        AvaloniaProperty.Register<PipeLineControl, double>(nameof(FlowSpeed), 1.0);
-    public static readonly StyledProperty<IBrush> PipeColorProperty =
-        AvaloniaProperty.Register<PipeLineControl, IBrush>(nameof(PipeColor));
-    public static readonly StyledProperty<IBrush> WaterColorProperty =
-        AvaloniaProperty.Register<PipeLineControl, IBrush>(nameof(WaterColor));
-    public static readonly StyledProperty<double> PipeWidthProperty =
-        AvaloniaProperty.Register<PipeLineControl, double>(nameof(PipeWidth), 20.0);
-    public static readonly StyledProperty<bool> IsHorizontalProperty =
-        AvaloniaProperty.Register<PipeLineControl, bool>(nameof(IsHorizontal), true);
+    public static readonly StyledProperty<bool> IsFlowProperty = AvaloniaProperty.Register<PipeLineControl, bool>(nameof(IsFlow), false);
+    public static readonly StyledProperty<double> FlowSpeedProperty = AvaloniaProperty.Register<PipeLineControl, double>(nameof(FlowSpeed), 1.0);
+    public static readonly StyledProperty<IBrush> PipeColorProperty = AvaloniaProperty.Register<PipeLineControl, IBrush>(nameof(PipeColor));
+    public static readonly StyledProperty<IBrush> WaterColorProperty = AvaloniaProperty.Register<PipeLineControl, IBrush>(nameof(WaterColor));
+    public static readonly StyledProperty<double> PipeWidthProperty = AvaloniaProperty.Register<PipeLineControl, double>(nameof(PipeWidth), 20.0);
+    public static readonly StyledProperty<bool> IsHorizontalProperty = AvaloniaProperty.Register<PipeLineControl, bool>(nameof(IsHorizontal), true);
 
     public bool IsFlow { get => GetValue(IsFlowProperty); set => SetValue(IsFlowProperty, value); }
     public double FlowSpeed { get => GetValue(FlowSpeedProperty); set => SetValue(FlowSpeedProperty, value); }
@@ -32,13 +26,29 @@ public class PipeLineControl : Control
     private IDisposable? _timer;
 
     static PipeLineControl() { AffectsRender<PipeLineControl>(IsFlowProperty, FlowSpeedProperty, PipeColorProperty, WaterColorProperty, PipeWidthProperty, IsHorizontalProperty); }
-    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); if (IsFlow) StartAnim(); }
-    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); StopAnim(); }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        ThemeHelper.ThemeChanged += OnThemeChanged;
+        if (IsFlow) StartAnim();
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        ThemeHelper.ThemeChanged -= OnThemeChanged;
+        StopAnim();
+        base.OnDetachedFromVisualTree(e);
+    }
+
+    private void OnThemeChanged() => InvalidateVisual();
+
     protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
     {
         base.OnPropertyChanged(change);
         if (change.Property == IsFlowProperty) { if (IsFlow) StartAnim(); else { StopAnim(); _flowOffset = 0; } }
     }
+
     private void StartAnim() { StopAnim(); _timer = DispatcherTimer.Run(() => { _flowOffset += FlowSpeed; if (_flowOffset >= 20) _flowOffset -= 20; InvalidateVisual(); return true; }, TimeSpan.FromMilliseconds(50)); }
     private void StopAnim() { _timer?.Dispose(); _timer = null; }
 
@@ -54,19 +64,13 @@ public class PipeLineControl : Control
         {
             context.DrawRectangle(pipeColor, null, new Rect(0, (bounds.Height - pw) / 2, bounds.Width, pw));
             if (IsFlow)
-            {
-                var dashPen = new Pen(waterColor, pw * 0.6, new DashStyle(new double[] { 8, 12 }, _flowOffset));
-                context.DrawLine(dashPen, new Point(0, bounds.Height / 2), new Point(bounds.Width, bounds.Height / 2));
-            }
+                context.DrawLine(new Pen(waterColor, pw * 0.6, new DashStyle(new double[] { 8, 12 }, _flowOffset)), new Point(0, bounds.Height / 2), new Point(bounds.Width, bounds.Height / 2));
         }
         else
         {
             context.DrawRectangle(pipeColor, null, new Rect((bounds.Width - pw) / 2, 0, pw, bounds.Height));
             if (IsFlow)
-            {
-                var dashPen = new Pen(waterColor, pw * 0.6, new DashStyle(new double[] { 8, 12 }, _flowOffset));
-                context.DrawLine(dashPen, new Point(bounds.Width / 2, 0), new Point(bounds.Width / 2, bounds.Height));
-            }
+                context.DrawLine(new Pen(waterColor, pw * 0.6, new DashStyle(new double[] { 8, 12 }, _flowOffset)), new Point(bounds.Width / 2, 0), new Point(bounds.Width / 2, bounds.Height));
         }
     }
 

+ 26 - 26
src/YZWater.Avalonia/Controls/PumpControl.cs

@@ -32,13 +32,24 @@ public class PumpControl : Control
     private double _rotationAngle;
     private IDisposable? _timer;
 
-    static PumpControl()
+    static PumpControl() { AffectsRender<PumpControl>(IsRunningProperty, SpeedProperty, FrequencyProperty, TextProperty, RunningColorProperty, StoppedColorProperty); }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        ThemeHelper.ThemeChanged += OnThemeChanged;
+        if (IsRunning) StartAnim();
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
     {
-        AffectsRender<PumpControl>(IsRunningProperty, SpeedProperty, FrequencyProperty, TextProperty, RunningColorProperty, StoppedColorProperty);
+        ThemeHelper.ThemeChanged -= OnThemeChanged;
+        StopAnim();
+        base.OnDetachedFromVisualTree(e);
     }
 
-    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); if (IsRunning) StartAnim(); }
-    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); StopAnim(); }
+    private void OnThemeChanged() => InvalidateVisual();
+
     protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
     {
         base.OnPropertyChanged(change);
@@ -52,57 +63,46 @@ public class PumpControl : Control
     {
         base.Render(context);
         var bounds = new Rect(Bounds.Size);
-        var cx = bounds.Width / 2;
-        var cy = bounds.Height / 2 - 5;
+        var cx = bounds.Width / 2; var cy = bounds.Height / 2 - 5;
         var radius = Math.Min(bounds.Width, bounds.Height) / 2 - 15;
         var typeface = new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold);
 
         var statusColor = (IsRunning ? RunningColor : StoppedColor) ?? (IsRunning ? ThemeHelper.Success() : ThemeHelper.TextDisabled());
-        var statusBrush = statusColor as SolidColorBrush ?? ThemeHelper.Success();
+        var statusBrush = statusColor as SolidColorBrush ?? (IsRunning ? ThemeHelper.Success() as SolidColorBrush : ThemeHelper.TextDisabled() as SolidColorBrush);
+        var statusColorValue = (statusBrush as SolidColorBrush)?.Color ?? Colors.Gray;
 
-        // 鍙戝厜
-        if (IsRunning)
-            context.DrawEllipse(new SolidColorBrush((statusBrush as SolidColorBrush)?.Color ?? Colors.Gray, 0.2), null, new Rect(cx - radius - 8, cy - radius - 8, (radius + 8) * 2, (radius + 8) * 2));
+        if (IsRunning) context.DrawEllipse(new SolidColorBrush(statusColorValue, 0.2), null, new Rect(cx - radius - 8, cy - radius - 8, (radius + 8) * 2, (radius + 8) * 2));
 
-        // 澶栧3
         context.DrawEllipse(null, new Pen(ThemeHelper.Border(), 3), new Rect(cx - radius, cy - radius, radius * 2, radius * 2));
         context.DrawEllipse(ThemeHelper.PanelBg(), null, new Rect(cx - radius + 3, cy - radius + 3, (radius - 3) * 2, (radius - 3) * 2));
 
-        // 鍙剁墖
-        var bladeCount = 6;
-        var bladeLen = radius * 0.65;
-        var bladeW = radius * 0.18;
-        for (int i = 0; i < bladeCount; i++)
+        for (int i = 0; i < 6; i++)
         {
-            var angle = _rotationAngle + (360.0 / bladeCount) * i;
-            var bx = cx + Math.Cos(angle * Math.PI / 180) * bladeLen * 0.5;
-            var by = cy + Math.Sin(angle * Math.PI / 180) * bladeLen * 0.5;
-            var bladeBrush = IsRunning ? new SolidColorBrush((statusBrush as SolidColorBrush)?.Color ?? Colors.Gray, 200) : ThemeHelper.TextDisabled();
+            var angle = _rotationAngle + 60.0 * i;
+            var bx = cx + Math.Cos(angle * Math.PI / 180) * radius * 0.65 * 0.5;
+            var by = cy + Math.Sin(angle * Math.PI / 180) * radius * 0.65 * 0.5;
+            var bladeBrush = IsRunning ? new SolidColorBrush(statusColorValue, 200) : ThemeHelper.TextDisabled();
             var rad = angle * Math.PI / 180;
             var cos = Math.Cos(rad); var sin = Math.Sin(rad);
             var transform = new Matrix(cos, sin, -sin, cos, bx - bx * cos + by * sin, by - bx * sin - by * cos);
             using (context.PushTransform(transform))
-                context.DrawRectangle(bladeBrush, null, new Rect(bx - bladeW / 2, by - bladeLen / 2, bladeW, bladeLen));
+                context.DrawRectangle(bladeBrush, null, new Rect(bx - radius * 0.09, by - radius * 0.325, radius * 0.18, radius * 0.65));
         }
 
-        // 涓績
         var cr = radius * 0.25;
         context.DrawEllipse(IsRunning ? statusColor : ThemeHelper.Border(), null, new Rect(cx - cr, cy - cr, cr * 2, cr * 2));
         context.DrawEllipse(new SolidColorBrush(Colors.White, 0.3), null, new Rect(cx - cr * 0.5, cy - cr * 0.5, cr, cr));
 
-        // 鎸囩ず鐏
         var indColor = IsRunning ? ThemeHelper.Success() : ThemeHelper.TextDisabled();
-        if (IsRunning) context.DrawEllipse(new SolidColorBrush((statusBrush as SolidColorBrush)?.Color ?? Colors.Gray, 0.3), null, new Rect(bounds.Width - 23, 7, 16, 16));
+        if (IsRunning) context.DrawEllipse(new SolidColorBrush(statusColorValue, 0.3), null, new Rect(bounds.Width - 23, 7, 16, 16));
         context.DrawEllipse(indColor, null, new Rect(bounds.Width - 20, 10, 10, 10));
 
-        // 鏂囧瓧
         if (!string.IsNullOrEmpty(Text))
         {
             var t = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 11, ThemeHelper.TextPrimary());
             context.DrawText(t, new Point(cx - t.Width / 2, bounds.Bottom - t.Height - 5));
         }
 
-        // 棰戠巼
         if (IsRunning)
         {
             var f = new FormattedText($"{Frequency:F0}Hz", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Microsoft YaHei"), 9, statusColor);

+ 17 - 20
src/YZWater.Avalonia/Controls/StatusCard.cs

@@ -7,18 +7,12 @@ namespace YZWater.Avalonia.Controls;
 
 public class StatusCard : Control
 {
-    public static readonly StyledProperty<string> TitleProperty =
-        AvaloniaProperty.Register<StatusCard, string>(nameof(Title), "璁惧");
-    public static readonly StyledProperty<string> StatusProperty =
-        AvaloniaProperty.Register<StatusCard, string>(nameof(Status), "RUNNING");
-    public static readonly StyledProperty<string> IconProperty =
-        AvaloniaProperty.Register<StatusCard, string>(nameof(Icon), "P1");
-    public static readonly StyledProperty<bool> IsActiveProperty =
-        AvaloniaProperty.Register<StatusCard, bool>(nameof(IsActive), false);
-    public static readonly StyledProperty<IBrush> ActiveColorProperty =
-        AvaloniaProperty.Register<StatusCard, IBrush>(nameof(ActiveColor));
-    public static readonly StyledProperty<IBrush> InactiveColorProperty =
-        AvaloniaProperty.Register<StatusCard, IBrush>(nameof(InactiveColor));
+    public static readonly StyledProperty<string> TitleProperty = AvaloniaProperty.Register<StatusCard, string>(nameof(Title), "璁惧");
+    public static readonly StyledProperty<string> StatusProperty = AvaloniaProperty.Register<StatusCard, string>(nameof(Status), "RUNNING");
+    public static readonly StyledProperty<string> IconProperty = AvaloniaProperty.Register<StatusCard, string>(nameof(Icon), "P1");
+    public static readonly StyledProperty<bool> IsActiveProperty = AvaloniaProperty.Register<StatusCard, bool>(nameof(IsActive), false);
+    public static readonly StyledProperty<IBrush> ActiveColorProperty = AvaloniaProperty.Register<StatusCard, IBrush>(nameof(ActiveColor));
+    public static readonly StyledProperty<IBrush> InactiveColorProperty = AvaloniaProperty.Register<StatusCard, IBrush>(nameof(InactiveColor));
 
     public string Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
     public string Status { get => GetValue(StatusProperty); set => SetValue(StatusProperty, value); }
@@ -29,30 +23,33 @@ public class StatusCard : Control
 
     static StatusCard() { AffectsRender<StatusCard>(TitleProperty, StatusProperty, IconProperty, IsActiveProperty, ActiveColorProperty, InactiveColorProperty); }
 
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); ThemeHelper.ThemeChanged += OnThemeChanged; }
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { ThemeHelper.ThemeChanged -= OnThemeChanged; base.OnDetachedFromVisualTree(e); }
+    private void OnThemeChanged() => InvalidateVisual();
+
     public override void Render(DrawingContext context)
     {
         base.Render(context);
         var bounds = new Rect(Bounds.Size);
         var statusColor = IsActive ? (ActiveColor ?? ThemeHelper.Success()) : (InactiveColor ?? ThemeHelper.TextDisabled());
-        var statusBrush = statusColor as SolidColorBrush ?? ThemeHelper.Success();
+        var statusBrush = statusColor as SolidColorBrush;
+        var statusColorValue = statusBrush?.Color ?? Colors.Gray;
         var monoTypeface = new Typeface("Consolas, monospace");
         var labelTypeface = new Typeface("Microsoft YaHei");
 
-        // 鍗$墖鑳屾櫙
         context.DrawRectangle(ThemeHelper.PanelBg(), null, bounds);
-        // 宸︿晶鐘舵佹潯
         context.DrawRectangle(statusColor, null, new Rect(0, 0, 3, bounds.Height));
-        // LED
+
         var ledX = 12.0; var ledY = bounds.Height / 2 - 5;
-        if (IsActive) context.DrawEllipse(new SolidColorBrush((statusBrush as SolidColorBrush)?.Color ?? Colors.Gray, 0.3), null, new Rect(ledX - 3, ledY - 3, 16, 16));
+        if (IsActive) context.DrawEllipse(new SolidColorBrush(statusColorValue, 0.3), null, new Rect(ledX - 3, ledY - 3, 16, 16));
         context.DrawEllipse(statusColor, null, new Rect(ledX, ledY, 10, 10));
-        // 鍥炬爣
+
         var iconText = new FormattedText(Icon, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, monoTypeface, 11, ThemeHelper.TextPrimary());
         context.DrawText(iconText, new Point(28, 6));
-        // 鏍囬
+
         var titleText = new FormattedText(Title, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, labelTypeface, 10, ThemeHelper.TextSecondary());
         context.DrawText(titleText, new Point(28, 22));
-        // 鐘舵
+
         var statusText = new FormattedText(Status, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, monoTypeface, 9, statusColor);
         context.DrawText(statusText, new Point(bounds.Width - statusText.Width - 10, bounds.Height / 2 - statusText.Height / 2));
     }

+ 13 - 16
src/YZWater.Avalonia/Controls/ThemeHelper.cs

@@ -10,6 +10,19 @@ namespace YZWater.Avalonia.Controls;
 /// </summary>
 internal static class ThemeHelper
 {
+    /// <summary>
+    /// 涓婚鍙樻洿浜嬩欢
+    /// </summary>
+    public static event Action? ThemeChanged;
+
+    /// <summary>
+    /// 瑙﹀彂涓婚鍙樻洿锛堢敱 App.axaml.cs 璋冪敤锛
+    /// </summary>
+    public static void NotifyThemeChanged()
+    {
+        ThemeChanged?.Invoke();
+    }
+
     /// <summary>
     /// 浠庡簲鐢ㄨ祫婧愯幏鍙栫敾鍒凤紝鎵句笉鍒板垯杩斿洖榛樿鍊
     /// </summary>
@@ -17,7 +30,6 @@ internal static class ThemeHelper
     {
         try
         {
-            // 閬嶅巻鎵鏈 MergedDictionaries 鏌ユ壘璧勬簮
             var app = Application.Current;
             if (app?.Resources?.MergedDictionaries != null)
             {
@@ -27,7 +39,6 @@ internal static class ThemeHelper
                         return brush;
                 }
             }
-            // 灏濊瘯浠庢牴璧勬簮鏌ユ壘
             if (app != null && app.Resources.TryGetResource(key, null, out var rootValue) && rootValue is IBrush rootBrush)
                 return rootBrush;
         }
@@ -35,20 +46,6 @@ internal static class ThemeHelper
         return new SolidColorBrush(Color.Parse(fallbackHex));
     }
 
-    /// <summary>
-    /// 閫氱煡鎵鏈夋帶浠堕噸缁橈紙鍦ㄤ富棰樺垏鎹㈠悗璋冪敤锛
-    /// </summary>
-    public static void InvalidateAll()
-    {
-        if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
-        {
-            foreach (var window in desktop.Windows)
-            {
-                window.InvalidateVisual();
-            }
-        }
-    }
-
     // 甯哥敤蹇嵎鏂规硶
     public static IBrush AppBg() => GetBrush("AppBgBrush", "#0A0E14");
     public static IBrush SurfaceBg() => GetBrush("SurfaceBgBrush", "#111820");

+ 14 - 4
src/YZWater.Avalonia/Controls/ValveControl.cs

@@ -25,6 +25,20 @@ public class ValveControl : Control
 
     static ValveControl() { AffectsRender<ValveControl>(StatusProperty, TextProperty, ValveColorProperty, BackgroundColorProperty); }
 
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        ThemeHelper.ThemeChanged += OnThemeChanged;
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        ThemeHelper.ThemeChanged -= OnThemeChanged;
+        base.OnDetachedFromVisualTree(e);
+    }
+
+    private void OnThemeChanged() => InvalidateVisual();
+
     public override void Render(DrawingContext context)
     {
         base.Render(context);
@@ -33,17 +47,14 @@ public class ValveControl : Control
         var size = Math.Min(bounds.Width, bounds.Height) * 0.8;
         var typeface = new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold);
 
-        // 鑳屾櫙鍦
         var bg = BackgroundColor ?? ThemeHelper.Border();
         context.DrawEllipse(bg, null, new Rect(cx - size / 2, cy - size / 2, size, size));
 
-        // 鎵嬫焺
         var handleLen = size * 0.35;
         var handleW = size * 0.15;
         var vc = ValveColor ?? ThemeHelper.Warning();
         context.DrawRectangle(vc, null, new Rect(cx - handleW / 2, cy - handleLen, handleW, handleLen * 2));
 
-        // 鐘舵佹寚绀虹伅
         var indSize = size * 0.15;
         var indColor = Status switch
         {
@@ -54,7 +65,6 @@ public class ValveControl : Control
         };
         context.DrawEllipse(indColor, null, new Rect(cx - indSize / 2, cy - size / 2 - indSize - 5, indSize, indSize));
 
-        // 鏂囧瓧
         if (!string.IsNullOrEmpty(Text))
         {
             var t = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 10, ThemeHelper.TextPrimary());

+ 19 - 13
src/YZWater.Avalonia/Controls/WaterTankControl.cs

@@ -22,11 +22,22 @@ public class WaterTankControl : Control
     public IBrush WaterColor { get => GetValue(WaterColorProperty); set => SetValue(WaterColorProperty, value); }
     public IBrush BorderColor { get => GetValue(BorderColorProperty); set => SetValue(BorderColorProperty, value); }
 
-    static WaterTankControl()
+    static WaterTankControl() { AffectsRender<WaterTankControl>(WaterLevelProperty, TextProperty, WaterColorProperty, BorderColorProperty); }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
     {
-        AffectsRender<WaterTankControl>(WaterLevelProperty, TextProperty, WaterColorProperty, BorderColorProperty);
+        base.OnAttachedToVisualTree(e);
+        ThemeHelper.ThemeChanged += OnThemeChanged;
     }
 
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        ThemeHelper.ThemeChanged -= OnThemeChanged;
+        base.OnDetachedFromVisualTree(e);
+    }
+
+    private void OnThemeChanged() => InvalidateVisual();
+
     public override void Render(DrawingContext context)
     {
         base.Render(context);
@@ -34,13 +45,10 @@ public class WaterTankControl : Control
         var typeface = new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold);
         var edgeWidth = 3.0;
 
-        // 鑳屾櫙
         context.DrawRectangle(ThemeHelper.PanelBg(), null, bounds);
-        // 杈规
         var borderColor = BorderColor ?? ThemeHelper.Border();
         context.DrawRectangle(null, new Pen(borderColor, edgeWidth), bounds);
 
-        // 姘翠綅
         var waterHeight = (bounds.Height - edgeWidth * 2) * Math.Max(0, Math.Min(100, WaterLevel)) / 100.0;
         if (waterHeight > 0)
         {
@@ -49,18 +57,16 @@ public class WaterTankControl : Control
             context.DrawRectangle(waterBrush, null, new Rect(bounds.X + edgeWidth, bounds.Y + bounds.Height - edgeWidth - waterHeight, bounds.Width - edgeWidth * 2, waterHeight));
         }
 
-        // 鏍囬
         if (!string.IsNullOrEmpty(Text))
         {
-            var titleText = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12, ThemeHelper.TextPrimary());
-            context.DrawText(titleText, new Point(bounds.X + (bounds.Width - titleText.Width) / 2, bounds.Y + edgeWidth + 5));
+            var t = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12, ThemeHelper.TextPrimary());
+            context.DrawText(t, new Point(bounds.X + (bounds.Width - t.Width) / 2, bounds.Y + edgeWidth + 5));
         }
 
-        // 鐧惧垎姣
-        var levelText = new FormattedText($"{WaterLevel:F1}%", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 14, ThemeHelper.TextPrimary());
-        var bgRect = new Rect(bounds.X + (bounds.Width - levelText.Width - 10) / 2, bounds.Y + bounds.Height - edgeWidth - levelText.Height - 10, levelText.Width + 10, levelText.Height + 6);
-        context.DrawRectangle(new SolidColorBrush(Colors.Black, 0.5), null, bgRect);
-        context.DrawText(levelText, new Point(bgRect.X + 5, bgRect.Y + 3));
+        var lt = new FormattedText($"{WaterLevel:F1}%", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 14, ThemeHelper.TextPrimary());
+        var bg = new Rect(bounds.X + (bounds.Width - lt.Width - 10) / 2, bounds.Y + bounds.Height - edgeWidth - lt.Height - 10, lt.Width + 10, lt.Height + 6);
+        context.DrawRectangle(new SolidColorBrush(Colors.Black, 0.5), null, bg);
+        context.DrawText(lt, new Point(bg.X + 5, bg.Y + 3));
     }
 
     protected override Size MeasureOverride(Size availableSize) => new Size(120, 160);