【智能设备开发】C#上位机软件实战:实时通讯与智能参数配置

一、引言

随着物联网(IoT)的不断发展,智能设备在工业自动化、环境监测、新能源等领域得到广泛应用。作为智能设备的核心配套软件,上位机需要实现稳定实时通讯灵活参数配置两大核心能力。

本文将使用 C# + WPF(也可扩展至 .NET MAUI)开发一套实用型智能设备上位机,重点实现实时通讯智能参数配置功能,代码遵循生产级规范,可直接用于实际项目。

二、系统设计与架构

核心设计原则

  • 轻量且可扩展
  • 异步通讯优先
  • 配置动态化(支持保存/加载)
  • 高稳定性(断线重连、参数校验)

主要模块

  • 实时通讯模块(串口 + Modbus TCP)
  • 参数配置模块(动态UI + 持久化)
  • 数据监控与报警模块
  • 数据存储模块

三、实时通讯:与设备的数据交互

智能设备常见的通讯方式包括串口(RS232/RS485)TCP/IP(Modbus TCP)

1. 通讯服务抽象(推荐)
public interface IDeviceCommService : IAsyncDisposable
{
    Task<bool> ConnectAsync(DeviceConfig config);
    Task<Dictionary<string, object>> ReadParametersAsync();
    Task<bool> WriteParameterAsync(string paramName, object value);
    bool IsConnected { get; }
    event EventHandler<DeviceDataEventArgs>? DataReceived;
}
2. Modbus TCP 实现
public class ModbusTcpCommService : IDeviceCommService
{
    private ModbusTcpMaster? _master;
    private readonly ILogger _logger;

    public async Task<bool> ConnectAsync(DeviceConfig config)
    {
        try
        {
            _master = new ModbusTcpMaster(config.IpAddress, config.Port);
            await _master.ConnectAsync();
            _logger.Information("设备连接成功 {Ip}:{Port}", config.IpAddress, config.Port);
            return true;
        }
        catch (Exception ex)
        {
            _logger.Error(ex, "连接失败");
            return false;
        }
    }

    public async Task<Dictionary<string, object>> ReadParametersAsync()
    {
        var result = new Dictionary<string, object>();
        var values = await _master!.ReadHoldingRegistersAsync(1, 1000, 20);
        
        // 根据地址映射解析参数
        result["Temperature"] = values[0] / 10.0;
        result["Humidity"] = values[1] / 10.0;
        result["Setpoint"] = values[2];

        return result;
    }
}
3. 串口通讯(带粘包处理)

推荐直接复用之前提供的 SerialPortFrameParser 类实现稳定帧解析。

四、智能参数配置模块

1. 参数配置模型
public class DeviceParameter
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public object Value { get; set; } = null!;
    public object? DefaultValue { get; set; }
    public double? Min { get; set; }
    public double? Max { get; set; }
    public string Unit { get; set; } = "";
    public bool IsReadOnly { get; set; }
}
2. 参数配置界面(WPF示例)
<!-- ParameterConfigView.xaml -->
<DataGrid ItemsSource="{Binding Parameters}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="参数名称" Binding="{Binding Description}"/>
        <DataGridTextColumn Header="当前值" Binding="{Binding Value}"/>
        <DataGridTextColumn Header="单位" Binding="{Binding Unit}"/>
        <DataGridTemplateColumn Header="操作">
            <DataTemplate>
                <Button Content="写入" Command="{Binding WriteCommand}"/>
            </DataTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

ViewModel实现

public partial class ParameterConfigViewModel : ObservableObject
{
    private readonly IDeviceCommService _commService;

    [ObservableProperty] private ObservableCollection<DeviceParameter> parameters = new();

    [RelayCommand]
    private async Task WriteParameter(DeviceParameter param)
    {
        if (await _commService.WriteParameterAsync(param.Name, param.Value))
        {
            Log.Information("参数写入成功: {Name} = {Value}", param.Name, param.Value);
        }
    }

    public async Task LoadParametersAsync()
    {
        var data = await _commService.ReadParametersAsync();
        // 映射到 Parameters 集合
    }
}

五、数据监控与报警

  • 使用 OxyPlot 实现实时曲线监控
  • 建立阈值规则引擎,异常时自动报警并记录日志

六、数据存储与导出

推荐使用 ClosedXML 一键导出Excel,或 SQLite 进行本地历史数据存储。

七、部署与优化建议

  • 使用 .NET 9 Native AOT 发布单文件exe
  • 支持配置文件(JSON)保存常用设备参数
  • 增加断线自动重连机制

结语:实时通讯是上位机的基础,智能参数配置是提升设备可管理性的关键。通过C#强大的异步能力和组件生态,我们可以快速开发出稳定、易用的智能设备上位机软件。

本方案可与之前的 Modbus实战串口粘包解析ValueTask优化 等内容无缝结合,形成完整上位机开发技术栈。


需要我继续补充以下任意部分吗?

  1. 完整 ParameterConfigView + ViewModel 代码
  2. 实时通讯 + 参数配置 完整 Demo 结构
  3. 报警联动 实现示例
  4. Native AOT 发布配置

1. 实时通讯 + 参数配置 完整 Demo 工程结构

SmartDeviceUpperMonitor.WPF/
├── SmartDeviceUpperMonitor/
│   ├── App.xaml
│   ├── App.xaml.cs                          # DI 配置 + Serilog 初始化
│   ├── MainWindow.xaml                      # 主界面 (TabControl)
│   │
│   ├── Core/
│   │   ├── Models/
│   │   │   ├── DeviceConfig.cs
│   │   │   ├── DeviceParameter.cs
│   │   │   └── AlarmEventArgs.cs
│   │   └── Events/
│   │
│   ├── Interfaces/
│   │   ├── IDeviceCommService.cs
│   │   └── IAlarmService.cs
│   │
│   ├── Services/
│   │   ├── Comm/
│   │   │   ├── ModbusTcpCommService.cs
│   │   │   └── SerialCommService.cs
│   │   ├── ParameterConfigService.cs
│   │   ├── AlarmService.cs
│   │   └── ExportService.cs
│   │
│   ├── ViewModels/
│   │   ├── MainViewModel.cs
│   │   ├── DashboardViewModel.cs
│   │   └── ParameterConfigViewModel.cs      # 参数配置核心
│   │
│   ├── Views/
│   │   ├── DashboardView.xaml
│   │   └── ParameterConfigView.xaml         # 参数配置界面
│   │
│   └── Configuration/
│       └── AppSettings.json
│
├── SmartDeviceUpperMonitor.Core/            # 共享实体(可选)
└── packages/

2. 完整 ParameterConfigView + ViewModel 代码

ParameterConfigView.xaml
<UserControl x:Class="SmartDeviceUpperMonitor.Views.ParameterConfigView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">

    <Grid Margin="12">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- 工具栏 -->
        <StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,12">
            <Button Content="读取所有参数" Width="140" Margin="5" 
                    Command="{Binding ReadAllParametersCommand}"/>
            <Button Content="保存为默认配置" Width="140" Margin="5" 
                    Command="{Binding SaveAsDefaultCommand}"/>
            <Button Content="应用所有修改" Width="140" Margin="5" 
                    Command="{Binding ApplyAllCommand}" Style="{StaticResource AccentButton}"/>
        </StackPanel>

        <!-- 参数列表 -->
        <DataGrid Grid.Row="1" ItemsSource="{Binding Parameters}" 
                  AutoGenerateColumns="False" CanUserSortColumns="True">
            <DataGrid.Columns>
                <DataGridTextColumn Header="参数名称" Binding="{Binding Description}" Width="180" IsReadOnly="True"/>
                <DataGridTextColumn Header="当前值" Binding="{Binding Value}" Width="120"/>
                <DataGridTextColumn Header="单位" Binding="{Binding Unit}" Width="80" IsReadOnly="True"/>
                <DataGridTextColumn Header="范围" Binding="{Binding RangeDisplay}" Width="120" IsReadOnly="True"/>
                
                <DataGridTemplateColumn Header="操作" Width="100">
                    <DataTemplate>
                        <Button Content="写入" Padding="8,4" 
                                Command="{Binding DataContext.WriteParameterCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
                                CommandParameter="{Binding}"/>
                    </DataTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</UserControl>
ParameterConfigViewModel.cs(使用 CommunityToolkit.Mvvm)
public partial class ParameterConfigViewModel : ObservableObject
{
    private readonly IDeviceCommService _commService;
    private readonly ILogger<ParameterConfigViewModel> _logger;

    [ObservableProperty] private ObservableCollection<DeviceParameter> parameters = new();
    [ObservableProperty] private bool isConnected;
    [ObservableProperty] private bool isBusy;

    public ParameterConfigViewModel(IDeviceCommService commService, ILogger<ParameterConfigViewModel> logger)
    {
        _commService = commService;
        _logger = logger;
    }

    [RelayCommand]
    private async Task ReadAllParametersAsync()
    {
        IsBusy = true;
        try
        {
            var data = await _commService.ReadParametersAsync();
            Parameters.Clear();

            foreach (var kv in data)
            {
                Parameters.Add(new DeviceParameter
                {
                    Name = kv.Key,
                    Description = GetDescription(kv.Key),
                    Value = kv.Value,
                    Unit = GetUnit(kv.Key)
                });
            }

            _logger.Information("成功读取 {Count} 个设备参数", data.Count);
        }
        catch (Exception ex)
        {
            _logger.Error(ex, "读取参数失败");
        }
        finally
        {
            IsBusy = false;
        }
    }

    [RelayCommand]
    private async Task WriteParameterAsync(DeviceParameter param)
    {
        if (param == null) return;

        try
        {
            bool success = await _commService.WriteParameterAsync(param.Name, param.Value);
            if (success)
                _logger.Information("参数写入成功 → {Name} = {Value}", param.Description, param.Value);
        }
        catch (Exception ex)
        {
            _logger.Error(ex, "写入参数失败");
        }
    }

    [RelayCommand]
    private async Task ApplyAllAsync()
    {
        foreach (var param in Parameters)
            await WriteParameterAsync(param);
    }

    // 辅助方法:根据参数名返回描述和单位(可改为配置化)
    private string GetDescription(string key) => key switch
    {
        "Temperature" => "目标温度",
        "Humidity" => "湿度设定",
        _ => key
    };

    private string GetUnit(string key) => key switch
    {
        "Temperature" => "°C",
        "Humidity" => "%RH",
        _ => ""
    };
}

3. 报警联动 实现示例

AlarmService.cs

public class AlarmService
{
    private readonly List<AlarmRule> _rules = new();
    private readonly IEventAggregator _eventAggregator;
    private readonly ILogger _logger;

    public void AddRule(AlarmRule rule) => _rules.Add(rule);

    public void Evaluate(DeviceData data)
    {
        foreach (var rule in _rules.Where(r => r.Enabled))
        {
            if (rule.DeviceId != data.DeviceId) continue;

            double value = data.GetValue(rule.Parameter);

            if ((rule.ThresholdHigh.HasValue && value > rule.ThresholdHigh) ||
                (rule.ThresholdLow.HasValue && value < rule.ThresholdLow))
            {
                var alarm = new AlarmEventArgs
                {
                    DeviceId = data.DeviceId,
                    Parameter = rule.Parameter,
                    Value = value,
                    Severity = rule.Severity,
                    Description = $"{rule.Parameter} 超出阈值 ({value})"
                };

                _eventAggregator.GetEvent<AlarmRaisedEvent>().Publish(alarm);
                _logger.Warning("报警触发: {Desc}", alarm.Description);
            }
        }
    }
}

4. Native AOT 发布配置(.csproj)

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net9.0-windows</TargetFramework>
    <OutputType>WinExe</OutputType>
    <UseWPF>true</UseWPF>
    
    <!-- Native AOT 核心配置 -->
    <PublishAot>true</PublishAot>
    <SelfContained>true</SelfContained>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    
    <OptimizationPreference>Speed</OptimizationPreference>
    <PublishTrimmed>true</PublishTrimmed>
    <TrimMode>full</TrimMode>
    <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
  </PropertyGroup>

</Project>

发布命令

dotnet publish -c Release -r win-x64 --self-contained true /p:PublishAot=true

1. 优化后的 Demo 工程结构(推荐最终版)

SmartDeviceUpperMonitor.WPF/
├── SmartDeviceUpperMonitor/                  # 主项目
│   ├── App.xaml
│   ├── App.xaml.cs                           # DI 配置 + Serilog + 启动逻辑
│   ├── MainWindow.xaml                       # 主界面布局(TabControl)
│   ├── MainWindow.xaml.cs
│   │
│   ├── Core/                                 # 领域核心
│   │   ├── Models/                           # DeviceConfig、DeviceParameter、AlarmEventArgs
│   │   ├── Enums/
│   │   └── Events/                           # 自定义事件(如 AlarmRaisedEvent)
│   │
│   ├── Interfaces/                           # 接口定义层
│   │   ├── IDeviceCommService.cs
│   │   ├── IParameterService.cs
│   │   └── IAlarmService.cs
│   │
│   ├── Services/                             # 业务实现
│   │   ├── Comm/
│   │   │   ├── ModbusTcpCommService.cs
│   │   │   └── SerialCommService.cs
│   │   ├── ParameterService.cs
│   │   ├── AlarmService.cs
│   │   └── LoggingService.cs
│   │
│   ├── ViewModels/                           # MVVM
│   │   ├── MainViewModel.cs
│   │   ├── DashboardViewModel.cs
│   │   └── ParameterConfigViewModel.cs
│   │
│   ├── Views/                                # 界面
│   │   ├── DashboardView.xaml
│   │   └── ParameterConfigView.xaml
│   │
│   ├── Controls/                             # 自定义控件(可选)
│   ├── Configuration/                        # 配置
│   │   └── appsettings.json
│   └── Helpers/
│
├── SmartDeviceUpperMonitor.Core/             # 可复用核心类库(推荐)
├── SmartDeviceUpperMonitor.Tests/
└── README.md

2. 完整 App.xaml.cs(DI 配置)

public partial class App : Application
{
    public static IServiceProvider Services { get; private set; } = null!;

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // 配置日志
        LoggingService.Configure();

        // 配置依赖注入
        var services = new ServiceCollection();

        ConfigureServices(services);

        Services = services.BuildServiceProvider();

        var mainWindow = Services.GetRequiredService<MainWindow>();
        mainWindow.Show();
    }

    private static void ConfigureServices(IServiceCollection services)
    {
        // 配置绑定
        services.AddSingleton<IConfiguration>(new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .Build());

        // 服务注册
        services.AddSingleton<IDeviceCommService, ModbusTcpCommService>();
        services.AddSingleton<IAlarmService, AlarmService>();
        services.AddSingleton<IParameterService, ParameterService>();

        // ViewModels
        services.AddTransient<MainViewModel>();
        services.AddTransient<DashboardViewModel>();
        services.AddTransient<ParameterConfigViewModel>();

        // 主窗口
        services.AddTransient<MainWindow>();
    }
}

3. MainWindow.xaml 布局(推荐主界面)

<Window x:Class="SmartDeviceUpperMonitor.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="智能设备上位机 - 实时通讯与参数配置" 
        Height="720" Width="1280"
        WindowStartupLocation="CenterScreen">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- 顶部状态栏 -->
        <DockPanel Grid.Row="0" Background="#2C3E50" Height="40">
            <TextBlock Text="智能设备上位机" Foreground="White" FontSize="16" FontWeight="SemiBold"
                       VerticalAlignment="Center" Margin="15,0"/>
            
            <StackPanel Orientation="Horizontal" DockPanel.Dock="Right" Margin="0,0,15,0">
                <TextBlock x:Name="ConnectionStatus" Text="已断开" Foreground="#E74C3C" 
                           VerticalAlignment="Center" Margin="10,0"/>
                <Button Content="连接设备" Padding="12,6" Margin="8,0" 
                        Command="{Binding ConnectCommand}"/>
            </StackPanel>
        </DockPanel>

        <!-- Tab 控制区域 -->
        <TabControl Grid.Row="1" Margin="8" Style="{StaticResource ModernTabControl}">
            
            <!-- Tab 1: 实时监控 -->
            <TabItem Header="实时监控">
                <ContentControl Content="{Binding DashboardView}"/>
            </TabItem>

            <!-- Tab 2: 参数配置(核心) -->
            <TabItem Header="参数配置">
                <ContentControl Content="{Binding ParameterConfigView}"/>
            </TabItem>

            <!-- Tab 3: 报警中心 -->
            <TabItem Header="报警中心">
                <ContentControl Content="{Binding AlarmCenterView}"/>
            </TabItem>

            <!-- Tab 4: 历史数据 -->
            <TabItem Header="历史数据">
                <ContentControl Content="{Binding HistoryView}"/>
            </TabItem>
        </TabControl>
    </Grid>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow(MainViewModel viewModel)
    {
        InitializeComponent();
        DataContext = viewModel;
    }
}

MainViewModel.cs(简化版)

public partial class MainViewModel : ObservableObject
{
    public DashboardViewModel DashboardView { get; }
    public ParameterConfigViewModel ParameterConfigView { get; }

    public MainViewModel(DashboardViewModel dashboardVM, ParameterConfigViewModel paramVM)
    {
        DashboardView = dashboardVM;
        ParameterConfigView = paramVM;
    }

    [RelayCommand]
    private async Task ConnectAsync() { /* ... */ }
}

优化说明

  • 采用依赖注入 + MVVM 标准结构
  • MainWindow 作为 Shell,使用 TabControl 组织功能模块
  • 各功能模块独立为 View + ViewModel,便于维护和扩展
  • 支持后续轻松扩展到 .NET MAUI

需要我继续补充以下任意内容吗?

  • DashboardView.xaml + 实时曲线 完整代码
  • appsettings.json 配置示例
  • AlarmCenterView 报警中心界面

1. DashboardView.xaml

<UserControl x:Class="SmartDeviceUpperMonitor.Views.DashboardView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:oxy="http://oxyplot.org/wpf"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">

    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- 标题栏 + 控制按钮 -->
        <DockPanel Grid.Row="0" Margin="0,0,0,12">
            <TextBlock Text="实时数据监控" FontSize="18" FontWeight="SemiBold" 
                       VerticalAlignment="Center"/>
            
            <StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
                <Button Content="开始采集" Margin="5" Padding="12,6" 
                        Command="{Binding StartAcquisitionCommand}"/>
                <Button Content="停止采集" Margin="5" Padding="12,6" 
                        Command="{Binding StopAcquisitionCommand}"/>
                <Button Content="清除曲线" Margin="5" Padding="12,6" 
                        Command="{Binding ClearChartCommand}"/>
            </StackPanel>
        </DockPanel>

        <!-- OxyPlot 实时曲线 -->
        <oxy:PlotView Grid.Row="1" 
                      Model="{Binding TrendModel}"
                      Background="Transparent"
                      Margin="0,8"
                      Controller="{Binding PlotController}"
                      Width="Auto" Height="Auto"/>
    </Grid>
</UserControl>

2. DashboardViewModel.cs(完整版)

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using System;
using System.Threading;
using System.Threading.Tasks;

public partial class DashboardViewModel : ObservableObject
{
    private readonly IDeviceCommService _commService;
    private readonly IAlarmService _alarmService;
    private CancellationTokenSource? _cts;

    // OxyPlot 模型
    public PlotModel TrendModel { get; } = new PlotModel 
    { 
        Title = "实时趋势监控",
        Background = OxyColors.Transparent 
    };

    private readonly LineSeries _temperatureSeries = new LineSeries
    {
        Title = "温度 (°C)",
        Color = OxyColors.OrangeRed,
        StrokeThickness = 2.5,
        MarkerType = MarkerType.None
    };

    private readonly LineSeries _humiditySeries = new LineSeries
    {
        Title = "湿度 (%RH)",
        Color = OxyColors.DodgerBlue,
        StrokeThickness = 2.5,
        MarkerType = MarkerType.None
    };

    public PlotController PlotController { get; } = new PlotController();

    public DashboardViewModel(IDeviceCommService commService, IAlarmService alarmService)
    {
        _commService = commService;
        _alarmService = alarmService;

        InitializeChart();
    }

    private void InitializeChart()
    {
        // X轴(时间)
        var dateAxis = new DateTimeAxis
        {
            Position = AxisPosition.Bottom,
            StringFormat = "HH:mm:ss",
            Title = "时间",
            MajorGridlineStyle = LineStyle.Solid,
            MinorGridlineStyle = LineStyle.Dot
        };

        // Y轴(温度)
        var tempAxis = new LinearAxis
        {
            Position = AxisPosition.Left,
            Title = "温度 (°C)",
            MajorGridlineStyle = LineStyle.Solid
        };

        TrendModel.Axes.Add(dateAxis);
        TrendModel.Axes.Add(tempAxis);
        TrendModel.Series.Add(_temperatureSeries);
        TrendModel.Series.Add(_humiditySeries);
    }

    [RelayCommand]
    private async Task StartAcquisitionAsync()
    {
        if (_cts != null) return;

        _cts = new CancellationTokenSource();

        try
        {
            while (!_cts.Token.IsCancellationRequested)
            {
                var data = await _commService.ReadParametersAsync();

                double temp = Convert.ToDouble(data.GetValueOrDefault("Temperature", 0));
                double humidity = Convert.ToDouble(data.GetValueOrDefault("Humidity", 0));

                var now = DateTime.Now;

                // 更新曲线
                _temperatureSeries.Points.Add(new DataPoint(DateTimeAxis.ToDouble(now), temp));
                _humiditySeries.Points.Add(new DataPoint(DateTimeAxis.ToDouble(now), humidity));

                // 滚动窗口(保持曲线性能)
                if (_temperatureSeries.Points.Count > 1200)
                {
                    _temperatureSeries.Points.RemoveAt(0);
                    _humiditySeries.Points.RemoveAt(0);
                }

                // 轻量刷新
                TrendModel.InvalidatePlot(false);

                // 报警判断
                _alarmService.Evaluate(new DeviceData 
                { 
                    DeviceId = "MainDevice", 
                    Values = data 
                });

                await Task.Delay(80, _cts.Token); // ≈12.5Hz
            }
        }
        catch (OperationCanceledException) { }
        catch (Exception ex)
        {
            // 日志记录
        }
    }

    [RelayCommand]
    private void StopAcquisition()
    {
        _cts?.Cancel();
        _cts?.Dispose();
        _cts = null;
    }

    [RelayCommand]
    private void ClearChart()
    {
        _temperatureSeries.Points.Clear();
        _humiditySeries.Points.Clear();
        TrendModel.InvalidatePlot(true);
    }
}

使用说明

  1. MainViewModel 中注入 DashboardViewModel 并绑定到 Tab 内容。
  2. 确保项目已安装 OxyPlot.Wpf NuGet 包。
  3. 可根据需要增加更多曲线(如压力、流量等)。

需要我继续补充以下内容吗?

  • AlarmCenterView.xaml + ViewModel(报警列表)
  • appsettings.json 配置示例
  • MainViewModel.cs 完整代码(协调各子 ViewModel)

更多推荐