在工业监控场景中,一个直观、高效的设备监控面板能极大提升运维效率——操作工通过仪表盘快速判断设备状态,通过趋势图分析参数变化规律,通过数据导出追溯历史异常。WPF凭借强大的UI渲染能力和数据绑定机制,成为开发这类面板的理想选择。
本文从零开始,手把手开发一个工业级设备监控面板,包含3个核心功能:自定义仪表盘(显示温度/压力等关键参数)、实时趋势图(动态刷新数据曲线)、数据导出Excel(带时间戳的历史记录)。代码兼顾“视觉效果”和“工业实用性”,所有控件支持数据绑定,可直接适配真实设备数据。
最终效果预览
完成后的监控面板分为4个区域:
顶部状态栏:显示设备在线状态、当前时间、系统版本;参数监控区:3个自定义仪表盘(温度、压力、转速)+ 数值显示,超限时自动变红;趋势图区:实时绘制温度和压力的变化曲线,支持鼠标悬停查看具体值;操作区:包含“开始监控”“停止”“导出Excel”按钮,以及数据保存路径设置。
界面支持窗口缩放(自适应布局),仪表盘有平滑动画效果,趋势图每秒刷新一次,整体响应流畅无卡顿。
开发准备
技术栈与库
开发环境:Visual Studio 2022 + .NET 6(兼容.NET Framework 4.7.2+);UI框架:WPF(原生控件+自定义控件);图表库:OxyPlot(轻量、开源,适合实时数据可视化,NuGet搜索);Excel导出:EPPlus(处理.xlsx格式,支持大数据量,NuGet搜索
OxyPlot.Wpf)。
EPPlus
项目结构
按“功能模块化”组织代码,便于维护:
DeviceMonitor/
├─ Views/ # 界面相关
│ ├─ MainWindow.xaml # 主窗口(监控面板)
│ └─ CustomControls/ # 自定义控件
│ └─ GaugeControl.xaml # 仪表盘控件
├─ ViewModels/ # 数据与逻辑(MVVM模式)
│ └─ MainViewModel.cs # 主窗口数据上下文
├─ Models/ # 数据模型
│ ├─ DeviceData.cs # 设备实时数据模型
│ └─ HistoricalData.cs # 历史数据模型(带时间戳)
└─ Helpers/ # 工具类
├─ ExcelExporter.cs # Excel导出工具
└─ DataSimulator.cs # 设备数据模拟工具(测试用)
步骤1:搭建主界面布局(XAML)
用WPF的Grid和DockPanel实现自适应布局,确保窗口缩放时控件比例合理:
<!-- MainWindow.xaml -->
<Window x:Class="DeviceMonitor.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DeviceMonitor.Views"
xmlns:oxy="http://oxyplot.org/wpf"
Title="设备监控面板" Height="800" Width="1200"
WindowStartupLocation="CenterScreen">
<!-- 数据上下文(MVVM绑定) -->
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid Margin="10">
<!-- 主布局:3行(状态栏、监控区、操作区) -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- 状态栏(高度自适应) -->
<RowDefinition Height="*"/> <!-- 监控区(占剩余空间) -->
<RowDefinition Height="Auto"/> <!-- 操作区(高度自适应) -->
</Grid.RowDefinitions>
<!-- 1. 顶部状态栏 -->
<DockPanel Grid.Row="0" Background="#F0F0F0" Padding="10" Margin="0 0 0 10">
<TextBlock Text="设备状态:" VerticalAlignment="Center"/>
<TextBlock Text="{Binding DeviceStatus}" Foreground="{Binding StatusColor}"
VerticalAlignment="Center" Margin="5 0 20 0"/>
<TextBlock Text="当前时间:" VerticalAlignment="Center" Margin="20 0 0 0"/>
<TextBlock Text="{Binding CurrentTime, StringFormat='yyyy-MM-dd HH:mm:ss'}"
VerticalAlignment="Center" Margin="5 0 20 0"/>
<TextBlock Text="系统版本:V1.0" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</DockPanel>
<!-- 2. 中间监控区 -->
<Grid Grid.Row="1" Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <!-- 左侧:仪表盘区域 -->
<ColumnDefinition Width="*"/> <!-- 右侧:趋势图区域 -->
</Grid.ColumnDefinitions>
<!-- 2.1 左侧:3个仪表盘(温度、压力、转速) -->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 温度仪表盘 -->
<local:GaugeControl Grid.Row="0"
Title="温度 (℃)"
Value="{Binding TempValue}"
MinValue="0"
MaxValue="100"
WarningValue="80" <!-- 80℃警告 -->
DangerValue="90" <!-- 90℃超限 -->
Margin="10"/>
<!-- 压力仪表盘 -->
<local:GaugeControl Grid.Row="1"
Title="压力 (MPa)"
Value="{Binding PressValue}"
MinValue="0"
MaxValue="2.5"
WarningValue="2.0"
DangerValue="2.3"
Margin="10"/>
<!-- 转速仪表盘 -->
<local:GaugeControl Grid.Row="2"
Title="转速 (rpm)"
Value="{Binding SpeedValue}"
MinValue="0"
MaxValue="3000"
WarningValue="2800"
DangerValue="2900"
Margin="10"/>
</Grid>
<!-- 2.2 右侧:实时趋势图 -->
<Border Grid.Column="1" BorderBrush="Gray" BorderThickness="1" CornerRadius="5" Margin="10">
<oxy:PlotView Model="{Binding PlotModel}" Margin="5"/>
</Border>
</Grid>
<!-- 3. 底部操作区 -->
<Grid Grid.Row="2" Background="#F0F0F0" Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Content="开始监控" Command="{Binding StartCommand}" Width="100" Margin="5"/>
<Button Content="停止监控" Command="{Binding StopCommand}" Width="100" Margin="5"/>
<Button Content="导出Excel" Command="{Binding ExportCommand}" Width="100" Margin="5"/>
<TextBlock Text="数据保存路径:" Grid.Column="3" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<TextBox Text="{Binding ExportPath}" Grid.Column="4" Margin="5"/>
<Button Content="浏览..." Command="{Binding BrowsePathCommand}" Grid.Column="5" Margin="5"/>
</Grid>
</Grid>
</Window>
步骤2:开发自定义仪表盘控件
工业监控中,传统数字显示不够直观,自定义仪表盘能通过颜色和指针位置快速传递状态信息。仪表盘需实现:刻度绘制、指针动画、数值绑定、状态色切换(正常/警告/超限)。
2.1 仪表盘XAML(样式定义)
<!-- CustomControls/GaugeControl.xaml -->
<UserControl x:Class="DeviceMonitor.Views.CustomControls.GaugeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DeviceMonitor.Views.CustomControls"
d:DesignHeight="200" d:DesignWidth="300">
<Grid>
<!-- 1. 标题 -->
<TextBlock Text="{Binding Title}" HorizontalAlignment="Center"
VerticalAlignment="Top" FontSize="16" FontWeight="Bold"/>
<!-- 2. 仪表盘背景(圆弧) -->
<Canvas Width="260" Height="160" HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- 底部圆弧(灰色底色) -->
<Path Stroke="#E0E0E0" StrokeThickness="15" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
<Path.Data>
<ArcSegment Size="100,100" Point="200,100" IsLargeArc="True"
SweepDirection="Clockwise" />
</Path.Data>
<Path.RenderTransform>
<TranslateTransform X="10" Y="100"/>
</Path.RenderTransform>
</Path>
<!-- 正常区域(绿色:0 ~ WarningValue) -->
<Path Stroke="Green" StrokeThickness="15" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
<Path.Data>
<ArcSegment x:Name="NormalArc" Size="100,100" Point="200,100"
IsLargeArc="False" SweepDirection="Clockwise" />
</Path.Data>
<Path.RenderTransform>
<TranslateTransform X="10" Y="100"/>
</Path.RenderTransform>
</Path>
<!-- 警告区域(黄色:WarningValue ~ DangerValue) -->
<Path Stroke="Orange" StrokeThickness="15" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
<Path.Data>
<ArcSegment x:Name="WarningArc" Size="100,100" Point="200,100"
IsLargeArc="False" SweepDirection="Clockwise" />
</Path.Data>
<Path.RenderTransform>
<TranslateTransform X="10" Y="100"/>
</Path.RenderTransform>
</Path>
<!-- 超限区域(红色:DangerValue ~ MaxValue) -->
<Path Stroke="Red" StrokeThickness="15" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
<Path.Data>
<ArcSegment x:Name="DangerArc" Size="100,100" Point="200,100"
IsLargeArc="False" SweepDirection="Clockwise" />
</Path.Data>
<Path.RenderTransform>
<TranslateTransform X="10" Y="100"/>
</Path.RenderTransform>
</Path>
<!-- 刻度线(每10%一个主刻度,5%一个次刻度) -->
<ItemsControl x:Name="TicksContainer">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Line X1="0" Y1="0" X2="0" Y2="{Binding Length}"
Stroke="Gray" StrokeThickness="{Binding Thickness}">
<Line.RenderTransform>
<RotateTransform Angle="{Binding Angle}" CenterX="0" CenterY="100"/>
</Line.RenderTransform>
</Line>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 指针(三角形+轴点) -->
<Polygon x:Name="Pointer" Points="0,-70 5,0 -5,0" Fill="Black">
<Polygon.RenderTransform>
<RotateTransform x:Name="PointerRotate" Angle="0" CenterX="0" CenterY="0"/>
</Polygon.RenderTransform>
</Polygon>
<Ellipse Width="10" Height="10" Fill="Black" Canvas.Left="100" Canvas.Top="0"/>
</Canvas>
<!-- 3. 当前值显示 -->
<TextBlock Text="{Binding Value, StringFormat={}{0:F1}}" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0 0 0 20"
Foreground="{Binding ValueColor}"/>
</Grid>
</UserControl>
2.2 仪表盘后台逻辑(C#)
实现数值到角度的转换、刻度生成、指针动画、状态颜色切换:
// CustomControls/GaugeControl.xaml.cs
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace DeviceMonitor.Views.CustomControls
{
public partial class GaugeControl : UserControl
{
// 依赖属性(支持数据绑定)
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(GaugeControl));
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double), typeof(GaugeControl),
new PropertyMetadata(0.0, OnValueChanged));
public static readonly DependencyProperty MinValueProperty =
DependencyProperty.Register("MinValue", typeof(double), typeof(GaugeControl),
new PropertyMetadata(0.0, OnRangeChanged));
public static readonly DependencyProperty MaxValueProperty =
DependencyProperty.Register("MaxValue", typeof(double), typeof(GaugeControl),
new PropertyMetadata(100.0, OnRangeChanged));
public static readonly DependencyProperty WarningValueProperty =
DependencyProperty.Register("WarningValue", typeof(double), typeof(GaugeControl),
new PropertyMetadata(80.0, OnRangeChanged));
public static readonly DependencyProperty DangerValueProperty =
DependencyProperty.Register("DangerValue", typeof(double), typeof(GaugeControl),
new PropertyMetadata(90.0, OnRangeChanged));
// 属性封装
public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
public double Value { get => (double)GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
public double MinValue { get => (double)GetValue(MinValueProperty); set => SetValue(MinValueProperty, value); }
public double MaxValue { get => (double)GetValue(MaxValueProperty); set => SetValue(MaxValueProperty, value); }
public double WarningValue { get => (double)GetValue(WarningValueProperty); set => SetValue(WarningValueProperty, value); }
public double DangerValue { get => (double)GetValue(DangerValueProperty); set => SetValue(DangerValueProperty, value); }
// 值颜色(根据状态变化)
public Brush ValueColor => Value >= DangerValue ? Brushes.Red :
Value >= WarningValue ? Brushes.Orange : Brushes.Green;
public GaugeControl()
{
InitializeComponent();
DataContext = this; // 绑定到自身(简化示例,实际可用MVVM)
GenerateTicks(); // 生成刻度
UpdateArcs(); // 更新颜色区域
}
// 数值变化时,更新指针角度(带动画)
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gauge = (GaugeControl)d;
double newValue = (double)e.NewValue;
// 限制值在Min-Max范围内
newValue = Math.Clamp(newValue, gauge.MinValue, gauge.MaxValue);
// 计算角度(仪表盘显示180度,从左到右对应Min到Max)
double angle = 180 * (newValue - gauge.MinValue) / (gauge.MaxValue - gauge.MinValue);
// 指针动画(0.5秒平滑过渡)
var animation = new DoubleAnimation(angle, TimeSpan.FromSeconds(0.5))
{
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseInOut }
};
gauge.PointerRotate.BeginAnimation(RotateTransform.AngleProperty, animation);
// 通知值颜色变化
gauge.OnPropertyChanged(nameof(ValueColor));
}
// 范围参数变化时,重新生成刻度和颜色区域
private static void OnRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gauge = (GaugeControl)d;
gauge.GenerateTicks();
gauge.UpdateArcs();
}
// 生成刻度线(主刻度长20,次刻度长10)
private void GenerateTicks()
{
var ticks = new List<TickData>();
double range = MaxValue - MinValue;
// 每5%一个刻度
for (int i = 0; i <= 20; i++)
{
double ratio = i / 20.0;
double angle = 180 * ratio; // 角度从0到180度
bool isMajorTick = i % 2 == 0; // 每10%一个主刻度
ticks.Add(new TickData
{
Angle = angle,
Length = isMajorTick ? 20 : 10,
Thickness = isMajorTick ? 2 : 1
});
}
TicksContainer.ItemsSource = ticks;
}
// 更新颜色区域(正常/警告/超限)
private void UpdateArcs()
{
double range = MaxValue - MinValue;
// 计算各区域占比(0-180度)
double warningRatio = (WarningValue - MinValue) / range;
double dangerRatio = (DangerValue - MinValue) / range;
// 正常区域(0 ~ WarningValue)
NormalArc.Point = GetArcEndPoint(warningRatio * 180);
// 警告区域(WarningValue ~ DangerValue)
WarningArc.Point = GetArcEndPoint(dangerRatio * 180);
WarningArc.StartPoint = NormalArc.Point;
// 超限区域(DangerValue ~ MaxValue)
DangerArc.Point = GetArcEndPoint(180);
DangerArc.StartPoint = WarningArc.Point;
}
// 计算圆弧终点坐标(基于角度)
private Point GetArcEndPoint(double angleDegrees)
{
// 角度转弧度(0度在右侧,180度在左侧)
double angleRadians = (angleDegrees - 180) * Math.PI / 180.0;
double x = 100 * Math.Cos(angleRadians) + 100; // 圆心在(100,100)
double y = 100 * Math.Sin(angleRadians) + 100;
return new Point(x, y);
}
// 刻度数据模型
private class TickData
{
public double Angle { get; set; }
public double Length { get; set; }
public double Thickness { get; set; }
}
// 通知属性变化(用于ValueColor)
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
}
}
步骤3:实现实时趋势图(OxyPlot)
实时趋势图需要动态添加数据点、自动缩放坐标轴、支持多曲线显示(温度+压力)。用OxyPlot的和
PlotModel实现,配合定时器刷新数据。
LineSeries
3.1 趋势图数据绑定(ViewModel)
在中初始化图表模型,定义曲线系列,定时添加数据:
MainViewModel
// ViewModels/MainViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
using OxyPlot;
using OxyPlot.Series;
using OxyPlot.Axes;
using Microsoft.Win32;
using System.Windows;
namespace DeviceMonitor.ViewModels
{
public class MainViewModel : INotifyPropertyChanged
{
// 实时数据
private double _tempValue;
private double _pressValue;
private double _speedValue;
private string _deviceStatus;
private Brush _statusColor;
private DateTime _currentTime;
private string _exportPath;
// 图表模型
private PlotModel _plotModel;
private LineSeries _tempSeries; // 温度曲线
private LineSeries _pressSeries; // 压力曲线
private int _dataCount = 0; // 数据点计数(X轴)
// 历史数据缓存(用于导出Excel)
public ObservableCollection<HistoricalData> HistoryData { get; } = new ObservableCollection<HistoricalData>();
// 命令
public ICommand StartCommand { get; }
public ICommand StopCommand { get; }
public ICommand ExportCommand { get; }
public ICommand BrowsePathCommand { get; }
// 数据模拟与刷新定时器
private readonly DataSimulator _simulator = new DataSimulator();
private readonly System.Timers.Timer _refreshTimer = new System.Timers.Timer(1000); // 1秒刷新一次
private bool _isMonitoring;
// 属性(支持绑定)
public double TempValue { get => _tempValue; set { _tempValue = value; OnPropertyChanged(); } }
public double PressValue { get => _pressValue; set { _pressValue = value; OnPropertyChanged(); } }
public double SpeedValue { get => _speedValue; set { _speedValue = value; OnPropertyChanged(); } }
public string DeviceStatus { get => _deviceStatus; set { _deviceStatus = value; OnPropertyChanged(); } }
public Brush StatusColor { get => _statusColor; set { _statusColor = value; OnPropertyChanged(); } }
public DateTime CurrentTime { get => _currentTime; set { _currentTime = value; OnPropertyChanged(); } }
public string ExportPath { get => _exportPath; set { _exportPath = value; OnPropertyChanged(); } }
public PlotModel PlotModel { get => _plotModel; set { _plotModel = value; OnPropertyChanged(); } }
public MainViewModel()
{
// 初始化命令
StartCommand = new RelayCommand(StartMonitoring);
StopCommand = new RelayCommand(StopMonitoring);
ExportCommand = new RelayCommand(ExportToExcel);
BrowsePathCommand = new RelayCommand(BrowseExportPath);
// 初始化状态
DeviceStatus = "未监控";
StatusColor = Brushes.Gray;
CurrentTime = DateTime.Now;
ExportPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\设备数据.xlsx";
// 初始化图表
InitializePlotModel();
// 定时器事件(刷新时间和数据)
_refreshTimer.Elapsed += (s, e) =>
{
// UI线程更新
Application.Current.Dispatcher.Invoke(() =>
{
CurrentTime = DateTime.Now;
if (_isMonitoring)
{
// 模拟采集数据
var data = _simulator.GetNextData();
TempValue = data.Temperature;
PressValue = data.Pressure;
SpeedValue = data.Speed;
// 添加到趋势图
AddDataToPlot(data.Temperature, data.Pressure);
// 缓存历史数据
HistoryData.Add(new HistoricalData
{
Time = CurrentTime,
Temperature = data.Temperature,
Pressure = data.Pressure,
Speed = data.Speed
});
// 限制缓存数量(只保留最新1000条)
while (HistoryData.Count > 1000)
HistoryData.RemoveAt(0);
}
});
};
}
// 初始化图表
private void InitializePlotModel()
{
PlotModel = new PlotModel { Title = "实时趋势图(温度/压力)" };
// X轴(时间/数据点)
PlotModel.Axes.Add(new LinearAxis
{
Position = AxisPosition.Bottom,
Title = "数据点",
MaximumPadding = 0.05,
MinimumPadding = 0.05
});
// Y轴(左侧:温度)
PlotModel.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "温度 (℃)",
Minimum = 0,
Maximum = 100,
Key = "TempAxis" // 用于绑定温度曲线
});
// Y轴(右侧:压力)
PlotModel.Axes.Add(new LinearAxis
{
Position = AxisPosition.Right,
Title = "压力 (MPa)",
Minimum = 0,
Maximum = 2.5,
Key = "PressAxis" // 用于绑定压力曲线
});
// 温度曲线
_tempSeries = new LineSeries
{
Title = "温度",
Color = OxyColors.Red,
YAxisKey = "TempAxis"
};
// 压力曲线
_pressSeries = new LineSeries
{
Title = "压力",
Color = OxyColors.Blue,
YAxisKey = "PressAxis"
};
PlotModel.Series.Add(_tempSeries);
PlotModel.Series.Add(_pressSeries);
}
// 添加数据到趋势图
private void AddDataToPlot(double temp, double press)
{
_dataCount++;
_tempSeries.Points.Add(new DataPoint(_dataCount, temp));
_pressSeries.Points.Add(new DataPoint(_dataCount, press));
// 只显示最新50个点(避免曲线过密)
if (_tempSeries.Points.Count > 50)
{
_tempSeries.Points.RemoveAt(0);
_pressSeries.Points.RemoveAt(0);
}
PlotModel.InvalidatePlot(true); // 刷新图表
}
// 开始监控
private void StartMonitoring()
{
_isMonitoring = true;
_refreshTimer.Start();
DeviceStatus = "监控中";
StatusColor = Brushes.Green;
}
// 停止监控
private void StopMonitoring()
{
_isMonitoring = false;
_refreshTimer.Stop();
DeviceStatus = "已停止";
StatusColor = Brushes.Orange;
}
// 浏览导出路径
private void BrowseExportPath()
{
var dialog = new SaveFileDialog
{
Filter = "Excel文件 (*.xlsx)|*.xlsx",
FileName = "设备数据.xlsx",
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
};
if (dialog.ShowDialog() == true)
{
ExportPath = dialog.FileName;
}
}
// 导出到Excel
private void ExportToExcel()
{
if (HistoryData.Count == 0)
{
MessageBox.Show("没有可导出的历史数据");
return;
}
try
{
ExcelExporter.Export(HistoryData, ExportPath);
MessageBox.Show($"导出成功:{ExportPath}");
}
catch (Exception ex)
{
MessageBox.Show($"导出失败:{ex.Message}");
}
}
// INotifyPropertyChanged实现
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
3.2 辅助类实现
数据模型:定义设备数据结构;数据模拟:生成随机数据模拟设备采集;命令实现:简化WPF命令绑定。
// Models/DeviceData.cs
namespace DeviceMonitor.Models
{
// 实时设备数据
public class DeviceData
{
public double Temperature { get; set; } // 温度
public double Pressure { get; set; } // 压力
public double Speed { get; set; } // 转速
}
// 带时间戳的历史数据
public class HistoricalData
{
public DateTime Time { get; set; }
public double Temperature { get; set; }
public double Pressure { get; set; }
public double Speed { get; set; }
}
}
// Helpers/DataSimulator.cs(模拟设备数据)
using System;
using DeviceMonitor.Models;
namespace DeviceMonitor
{
public class DataSimulator
{
private Random _random = new Random();
private double _baseTemp = 50; // 基准温度
private double _basePress = 1.0; // 基准压力
private double _baseSpeed = 1500; // 基准转速
public DeviceData GetNextData()
{
// 模拟小范围波动(±5%)
var temp = _baseTemp + (_random.NextDouble() - 0.5) * 10;
var press = _basePress + (_random.NextDouble() - 0.5) * 0.2;
var speed = _baseSpeed + (_random.NextDouble() - 0.5) * 300;
// 偶尔出现较大波动(模拟异常)
if (_random.Next(100) < 5) // 5%概率
{
temp += (_random.NextDouble() - 0.5) * 20;
press += (_random.NextDouble() - 0.5) * 0.5;
}
// 限制范围
return new DeviceData
{
Temperature = Math.Clamp(temp, 0, 100),
Pressure = Math.Clamp(press, 0, 2.5),
Speed = Math.Clamp(speed, 0, 3000)
};
}
}
}
// Helpers/RelayCommand.cs(命令实现)
using System;
using System.Windows.Input;
namespace DeviceMonitor
{
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object parameter) => _execute();
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}
步骤4:数据导出Excel(EPPlus)
使用EPPlus将历史数据导出为Excel,包含表头、时间戳、温度/压力/转速列,支持中文显示。
// Helpers/ExcelExporter.cs
using System.Collections.ObjectModel;
using OfficeOpenXml;
using DeviceMonitor.Models;
using System.IO;
namespace DeviceMonitor.Helpers
{
public static class ExcelExporter
{
public static void Export(ObservableCollection<HistoricalData> data, string filePath)
{
// 必须设置许可证上下文(EPPlus 5+要求)
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
using (var package = new ExcelPackage(new FileInfo(filePath)))
{
// 添加工作表
var worksheet = package.Workbook.Worksheets.Add("设备历史数据");
// 表头
worksheet.Cells[1, 1].Value = "时间";
worksheet.Cells[1, 2].Value = "温度 (℃)";
worksheet.Cells[1, 3].Value = "压力 (MPa)";
worksheet.Cells[1, 4].Value = "转速 (rpm)";
// 设置表头样式(加粗)
worksheet.Cells[1, 1, 1, 4].Style.Font.Bold = true;
// 填充数据
for (int i = 0; i < data.Count; i++)
{
var item = data[i];
worksheet.Cells[i + 2, 1].Value = item.Time;
worksheet.Cells[i + 2, 1].Style.Numberformat.Format = "yyyy-MM-dd HH:mm:ss"; // 时间格式
worksheet.Cells[i + 2, 2].Value = item.Temperature;
worksheet.Cells[i + 2, 2].Style.Numberformat.Format = "0.0"; // 保留1位小数
worksheet.Cells[i + 2, 3].Value = item.Pressure;
worksheet.Cells[i + 2, 3].Style.Numberformat.Format = "0.00"; // 保留2位小数
worksheet.Cells[i + 2, 4].Value = item.Speed;
}
// 自动调整列宽
worksheet.Cells.AutoFitColumns();
// 保存文件
package.Save();
}
}
}
}
工业级优化与避坑指南
1. 性能优化(避免卡顿)
趋势图数据限制:只保留最新50-100个点,避免曲线绘制卡顿;UI更新频率控制:实时数据1秒刷新一次足够(人眼无法分辨更高频率);仪表盘动画优化:指针动画时长控制在0.3-0.5秒,避免过度动画消耗资源。
2. 异常处理(提升稳定性)
Excel导出异常:捕获文件占用、权限不足等错误,提示用户重试;数据范围校验:确保仪表盘数值在范围内,避免指针角度异常;线程安全:后台数据采集线程更新UI时,必须用
MinValue-MaxValue切换到UI线程。
Dispatcher.Invoke
3. 易用性增强
导出路径记忆:保存用户上次选择的路径,下次打开自动填充;趋势图交互:OxyPlot支持鼠标悬停显示数据点值,添加图例区分曲线;超限报警:数值超限时,除颜色变化外,可添加声音提示()。
System.Media.SoundPlayer
结语:从Demo到工业产品的关键
本文实现的监控面板已具备工业场景的基础功能,但要成为产品级应用,还需根据实际需求扩展:
数据来源:替换,对接真实设备(Modbus/OPC UA等协议);用户权限:添加登录功能,区分操作工/管理员权限;报警记录:单独记录超限事件,支持查询和导出;多设备支持:扩展为多标签页,同时监控多台设备。
DataSimulator
WPF的优势在于“UI与逻辑分离”和“自定义控件灵活性”,通过MVVM模式可轻松扩展功能,而无需大幅修改界面。掌握本文的仪表盘、趋势图、Excel导出三大核心模块,就能应对80%以上的工业监控场景。
你在开发监控面板时遇到过哪些UI或性能问题?欢迎在评论区分享解决方案~
