004001003_IoIndicator 控件完整配套代码摘要本文围绕 WPF 工业指示灯控件IoIndicator的完整配套使用展开依次覆盖默认样式配置、XAML 引用与静态/MVVM/动态创建示例、颜色与闪烁效果等进阶自定义并给出常见问题排查与工业现场优化建议所有代码均可直接复制到项目中开箱即用。包含默认样式配置、完整使用示例、进阶自定义、常见问题排查四部分所有代码可直接复制到你的项目中开箱即用。一、第一步配置默认样式必须完成1.1 项目结构要求在你的项目根目录下创建Themes文件夹然后在其中添加Generic.xaml文件结构如下plaintextIndustrialVisionTemplate/ ├── Controls/ │ └── IoIndicator.cs ← 你之前写的控件类 ├── Themes/ │ └── Generic.xaml ← 新增默认样式文件 ├── Properties/ │ └── AssemblyInfo.cs ← 需要修改 └── IndustrialVisionTemplate.csproj1.2 修改 Generic.xaml 的生成操作这是 90% 的人控件不显示的原因右键点击Generic.xaml→ 属性将生成操作设置为Page将复制到输出目录设置为不复制1.3 配置 AssemblyInfo.cs打开Properties/AssemblyInfo.cs添加以下代码如果没有的话csharpusing System.Windows; [assembly: ThemeInfo( ResourceDictionaryLocation.None, // 主题特定资源字典的位置 ResourceDictionaryLocation.SourceAssembly // 通用资源字典的位置 )]1.4 编写默认样式代码将以下代码复制到Themes/Generic.xaml中xaml:ResourceDictionary xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml xmlns:controlsclr-namespace:IndustrialVisionTemplate.Controls !-- IoIndicator 工业级默认样式 -- Style TargetType{x:Type controls:IoIndicator} !-- 默认大小20x20工业标准尺寸 -- Setter PropertyWidth Value20/ Setter PropertyHeight Value20/ Setter PropertyHorizontalAlignment ValueCenter/ Setter PropertyVerticalAlignment ValueCenter/ !-- 控件模板定义指示灯的外观 -- Setter PropertyTemplate Setter.Value ControlTemplate TargetType{x:Type controls:IoIndicator} Grid !-- 外阴影增加立体感 -- Ellipse Width{TemplateBinding Width} Height{TemplateBinding Height} Fill#22000000 Margin1,1,0,0/ !-- 主指示灯 -- Ellipse x:NamePART_Indicator Width{TemplateBinding Width} Height{TemplateBinding Height} Fill{TemplateBinding IndicatorColor} StrokeBlack StrokeThickness1 Ellipse.Effect DropShadowEffect Color{TemplateBinding IndicatorColor} ShadowDepth0 BlurRadius4 Opacity0.7/ /Ellipse.Effect /Ellipse !-- 高光效果模拟玻璃质感 -- Ellipse Width{TemplateBinding Width * 0.6} Height{TemplateBinding Height * 0.3} Fill#66FFFFFF VerticalAlignmentTop Margin0,2,0,0/ /Grid !-- 控件状态触发器 -- ControlTemplate.Triggers !-- 禁用状态变灰 -- Trigger PropertyIsEnabled ValueFalse Setter TargetNamePART_Indicator PropertyFill Value#9E9E9E/ Setter TargetNamePART_Indicator PropertyEffect Value{x:Null}/ /Trigger /ControlTemplate.Triggers /ControlTemplate /Setter.Value /Setter /Style /ResourceDictionary二、第二步完整使用示例2.1 在 XAML 中引用控件首先在窗口或页面的顶部添加命名空间引用xaml:Window x:ClassIndustrialVisionTemplate.Views.IoMonitorView xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml xmlns:controlsclr-namespace:IndustrialVisionTemplate.Controls2.2 静态使用示例XAML:StackPanel OrientationHorizontal Margin10 !-- 输入点1ON状态绿色 -- TextBlock Text输入1 VerticalAlignmentCenter Margin0,0,5,0/ controls:IoIndicator IsOnTrue Margin0,0,20,0/ !-- 输入点2OFF状态红色 -- TextBlock Text输入2 VerticalAlignmentCenter Margin0,0,5,0/ controls:IoIndicator IsOnFalse Margin0,0,20,0/ !-- 输入点3报警状态闪烁红色 -- TextBlock Text输入3 VerticalAlignmentCenter Margin0,0,5,0/ controls:IoIndicator IsOnTrue IsBlinkingTrue Margin0,0,20,0/ !-- 自定义大小30x30 -- TextBlock Text输出1 VerticalAlignmentCenter Margin0,0,5,0/ controls:IoIndicator IsOnTrue Width30 Height30/ /StackPanel2.3 MVVM 数据绑定示例工业项目标准用法第一步编写 ViewModelcsharp:using IndustrialVisionTemplate.Controls; using Prism.Mvvm; using System; using System.Timers; namespace IndustrialVisionTemplate.ViewModels { public class IoMonitorViewModel : BindableBase { // 输入点状态 private bool _input1State; public bool Input1State { get _input1State; set SetProperty(ref _input1State, value); } private bool _input2State; public bool Input2State { get _input2State; set SetProperty(ref _input2State, value); } private bool _input2Alarm; public bool Input2Alarm { get _input2Alarm; set SetProperty(ref _input2Alarm, value); } // 输出点状态 private bool _output1State; public bool Output1State { get _output1State; set SetProperty(ref _output1State, value); } public IoMonitorViewModel() { // 模拟PLC实时更新IO状态实际项目中替换为真实PLC读取 var timer new Timer(1000); timer.Elapsed (s, e) { // 随机更新状态模拟PLC数据变化 Input1State new Random().Next(2) 0; Input2State new Random().Next(2) 0; Input2Alarm Input2State new Random().Next(10) 0; // 10%概率报警 Output1State new Random().Next(2) 0; }; timer.Start(); } } }第二步XAML 绑定xaml:Window.DataContext viewModels:IoMonitorViewModel/ /Window.DataContext Grid Margin20 Grid.RowDefinitions RowDefinition HeightAuto/ RowDefinition HeightAuto/ RowDefinition HeightAuto/ /Grid.RowDefinitions Grid.ColumnDefinitions ColumnDefinition WidthAuto/ ColumnDefinition WidthAuto/ ColumnDefinition WidthAuto/ ColumnDefinition WidthAuto/ /Grid.ColumnDefinitions !-- 输入点区域 -- TextBlock Text输入点 Grid.Row0 Grid.Column0 FontWeightBold Margin0,0,10,10/ TextBlock Text输入1 Grid.Row1 Grid.Column0 VerticalAlignmentCenter Margin0,0,5,10/ controls:IoIndicator Grid.Row1 Grid.Column1 IsOn{Binding Input1State} Margin0,0,20,10/ TextBlock Text输入2 Grid.Row1 Grid.Column2 VerticalAlignmentCenter Margin0,0,5,10/ controls:IoIndicator Grid.Row1 Grid.Column3 IsOn{Binding Input2State} IsBlinking{Binding Input2Alarm} Margin0,0,0,10/ !-- 输出点区域 -- TextBlock Text输出点 Grid.Row2 Grid.Column0 FontWeightBold Margin0,0,10,0/ TextBlock Text输出1 Grid.Row3 Grid.Column0 VerticalAlignmentCenter Margin0,0,5,0/ controls:IoIndicator Grid.Row3 Grid.Column1 IsOn{Binding Output1State} Margin0,0,20,0/ /Grid2.4 动态创建控件示例如果需要根据 PLC 的 IO 点数动态创建控件csharp:private void CreateIoControls() { // 从PLC获取IO点列表 var ioPoints Device.PLC.GetIoPoints(); foreach (var ioPoint in ioPoints) { // 创建IO指示灯 var indicator new IoIndicator(); indicator.Width 20; indicator.Height 20; // 创建标签 var label new TextBlock(); label.Text ${ioPoint.Name}; label.VerticalAlignment VerticalAlignment.Center; // 添加到布局 var stackPanel new StackPanel(); stackPanel.Orientation Orientation.Horizontal; stackPanel.Margin new Thickness(5); stackPanel.Children.Add(label); stackPanel.Children.Add(indicator); // 数据绑定 indicator.SetBinding( IoIndicator.IsOnProperty, new Binding($IoPoints[{ioPoint.Id}].State)); indicator.SetBinding( IoIndicator.IsBlinkingProperty, new Binding($IoPoints[{ioPoint.Id}].IsAlarm)); IoPanel.Children.Add(stackPanel); } }三、进阶自定义用法3.1 自定义颜色如果需要修改默认的颜色方案可以在 App.xaml 中重写样式xaml:Application.Resources Style TargetType{x:Type controls:IoIndicator} BasedOn{StaticResource {x:Type controls:IoIndicator}} !-- 自定义ON状态颜色蓝色 -- Setter PropertyOnColor Value#2196F3/ !-- 自定义OFF状态颜色橙色 -- Setter PropertyOffColor Value#FF9800/ !-- 自定义闪烁颜色黄色 -- Setter PropertyBlinkOffColor Value#FFEB3B/ /Style /Application.Resources3.2 区分输入输出指示灯可以创建两个派生类分别用于输入和输出csharp:/// summary /// 输入点指示灯绿色ON红色OFF /// /summary public class InputIndicator : IoIndicator { static InputIndicator() { DefaultStyleKeyProperty.OverrideMetadata( typeof(InputIndicator), new FrameworkPropertyMetadata(typeof(IoIndicator))); } } /// summary /// 输出点指示灯蓝色ON灰色OFF /// /summary public class OutputIndicator : IoIndicator { static OutputIndicator() { DefaultStyleKeyProperty.OverrideMetadata( typeof(OutputIndicator), new FrameworkPropertyMetadata(typeof(IoIndicator))); } public OutputIndicator() { // 重写颜色逻辑 if (IsOn) IndicatorColor Brushes.Blue; else IndicatorColor Brushes.Gray; } }3.3 自定义闪烁频率修改IoIndicator.cs中的闪烁间隔csharp:// 闪烁间隔300ms更快的闪烁 _blinkTimer.Interval TimeSpan.FromMilliseconds(300);3.4 自定义闪烁效果除了默认的开关闪烁工业现场通常还需要不同节奏的提示效果。可以在IoIndicator.cs中增加BlinkMode枚举通过定时器逻辑实现常亮、慢闪、快闪、呼吸四种模式。1. 定义闪烁模式枚举csharp:public enum BlinkMode { Steady, // 常亮保持 IsOn 状态不闪烁 SlowBlink, // 慢闪500ms 切换一次适合一般提醒 FastBlink, // 快闪200ms 切换一次适合报警状态 Breathing // 呼吸约 3 秒完成一次明暗渐变适合待机提示 }2. 在控件中增加 BlinkMode 依赖属性csharp:public static readonly DependencyProperty BlinkModeProperty DependencyProperty.Register( nameof(BlinkMode), typeof(BlinkMode), typeof(IoIndicator), new PropertyMetadata(BlinkMode.Steady, OnBlinkModeChanged)); public BlinkMode BlinkMode { get (BlinkMode)GetValue(BlinkModeProperty); set SetValue(BlinkModeProperty, value); } private static void OnBlinkModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var indicator (IoIndicator)d; if (indicator.IsOn (BlinkMode)e.NewValue ! BlinkMode.Steady) { indicator.StartBlinkTimer(); } }3. 改造定时器回调按模式执行csharp:private DispatcherTimer _blinkTimer; private bool _lightState; private int _breathValue; private int _breathDirection 1; private void StartBlinkTimer() { if (_blinkTimer null) { _blinkTimer new DispatcherTimer(); _blinkTimer.Tick OnBlinkTimerTick; } _blinkTimer.Stop(); switch (BlinkMode) { case BlinkMode.Steady: Opacity 1; _lightState IsOn; UpdateIndicatorColor(); return; case BlinkMode.SlowBlink: _blinkTimer.Interval TimeSpan.FromMilliseconds(500); break; case BlinkMode.FastBlink: _blinkTimer.Interval TimeSpan.FromMilliseconds(200); break; case BlinkMode.Breathing: _breathValue 0; _breathDirection 1; _blinkTimer.Interval TimeSpan.FromMilliseconds(30); break; } _lightState false; _blinkTimer.Start(); } private void OnBlinkTimerTick(object sender, EventArgs e) { if (BlinkMode BlinkMode.Breathing) { _breathValue _breathDirection * 5; if (_breathValue 100) { _breathValue 100; _breathDirection -1; } else if (_breathValue 0) { _breathValue 0; _breathDirection 1; } Opacity _breathValue / 100.0; return; } _lightState !_lightState; UpdateIndicatorColor(); }4. XAML 使用示例xaml:StackPanel OrientationVertical Margin10 !-- 常亮正常运行时稳定显示 -- controls:IoIndicator IsOnTrue BlinkModeSteady Margin0,0,0,10/ !-- 慢闪设备待机提示 -- controls:IoIndicator IsOnTrue BlinkModeSlowBlink Margin0,0,0,10/ !-- 快闪紧急故障报警 -- controls:IoIndicator IsOnTrue BlinkModeFastBlink Margin0,0,0,10/ !-- 呼吸通信连接中 -- controls:IoIndicator IsOnTrue BlinkModeBreathing Margin0,0,0,10/ /StackPanel提示若原有控件已存在_blinkTimer和OnBlinkTimerTick请把原来直接切换IsOn的逻辑迁移到OnBlinkTimerTick中避免多个定时器互相影响。四、常见问题排查问题 1控件显示不出来最常见排查步骤✅ 确认Themes/Generic.xaml的生成操作是Page✅ 确认AssemblyInfo.cs中添加了ThemeInfo特性✅ 确认控件类是public的✅ 确认静态构造函数中写了DefaultStyleKeyProperty.OverrideMetadata✅ 确认 XAML 中的命名空间与控件类的命名空间完全一致包括大小写问题 2颜色不更新排查步骤✅ 确认依赖属性的变更回调正确调用了UpdateIndicatorColor()✅ 确认IndicatorColor是依赖属性而不是普通属性✅ 确认模板中使用了{TemplateBinding IndicatorColor}✅ 确认没有在后台代码中直接修改 Ellipse 的 Fill 属性问题 3内存泄漏排查步骤✅ 确认在Unloaded事件中停止了定时器并释放了引用✅ 确认没有在静态变量中引用控件实例✅ 确认及时取消了事件订阅✅ 确认没有在定时器回调中持有强引用问题 4闪烁不流畅解决方案使用DispatcherTimer而不是System.Timers.Timer闪烁间隔不要小于 200ms人眼无法区分更快的闪烁避免在闪烁回调中执行耗时操作五、工业现场优化建议颜色标准化严格遵循工业现场颜色规范绿色运行、导通、正常红色停机、断开、故障黄色警告、待机蓝色输出、动作大小标准化普通 IO 点20x20重要 IO 点30x30报警指示灯40x40性能优化同一页面不要超过 100 个指示灯状态更新频率不要超过 10Hz批量更新时使用BeginInvoke批量处理