C#简单组态软件开发
C#简单组态软件开发
组态软件(SCADA/HMI)是工业自动化领域的核心软件,用于监控和控制工业过程。
系统架构设计
一个基本的组态软件应包含以下模块:
- 图形界面编辑器
- 设备通信模块
- 实时数据库
- 运行时引擎
- 报警系统
- 历史数据存储
开发环境搭建
-
开发工具:
- Visual Studio 2019/2022
- .NET Framework 4.7+ 或 .NET 5/6
-
主要依赖库:
<PackageReference Include="Opc.Ua.Core" Version="1.4.365" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="SharpDX" Version="4.2.0" /> <PackageReference Include="Serilog" Version="2.10.0" />
核心模块实现
1. 图形界面编辑器
// 图形元素基类
public abstract class GraphicElement : INotifyPropertyChanged
{public string Id { get; set; } = Guid.NewGuid().ToString();public string Name { get; set; }public double X { get; set; }public double Y { get; set; }public double Width { get; set; }public double Height { get; set; }public double Rotation { get; set; }public Brush Background { get; set; } = Brushes.White;public Brush Foreground { get; set; } = Brushes.Black;public Pen Border { get; set; } = new Pen(Brushes.Black, 1);public abstract void Draw(DrawingContext drawingContext);public virtual bool HitTest(Point point){return new Rect(X, Y, Width, Height).Contains(point);}public event PropertyChangedEventHandler PropertyChanged;protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}
}// 矩形元素
public class RectangleElement : GraphicElement
{public override void Draw(DrawingContext drawingContext){drawingContext.DrawRectangle(Background, Border, new Rect(X, Y, Width, Height));}
}// 文本元素
public class TextElement : GraphicElement
{public string Text { get; set; } = "Text";public string FontFamily { get; set; } = "Arial";public double FontSize { get; set; } = 12;public FontWeight FontWeight { get; set; } = FontWeights.Normal;public override void Draw(DrawingContext drawingContext){var formattedText = new FormattedText(Text,CultureInfo.CurrentCulture,FlowDirection.LeftToRight,new Typeface(FontFamily, FontStyle.Normal, FontWeight, FontStretches.Normal),FontSize,Foreground,VisualTreeHelper.GetDpi(Application.Current.MainWindow).PixelsPerDip);drawingContext.DrawText(formattedText, new Point(X, Y));}
}// 画面类
public class GraphicScreen
{public string Name { get; set; }public double Width { get; set; } = 800;public double Height { get; set; } = 600;public ObservableCollection<GraphicElement> Elements { get; set; } = new ObservableCollection<GraphicElement>();public void Render(DrawingContext drawingContext){foreach (var element in Elements){element.Draw(drawingContext);}}
}
2. 设备通信模块
// 通信驱动接口
public interface IDeviceDriver
{string Name { get; }bool IsConnected { get; }Task<bool> ConnectAsync();Task DisconnectAsync();Task<object> ReadTagAsync(string tagName);Task<bool> WriteTagAsync(string tagName, object value);event EventHandler<DataChangedEventArgs> DataChanged;
}// Modbus TCP驱动示例
public class ModbusTcpDriver : IDeviceDriver
{private ModbusFactory _factory;private IModbusMaster _master;private string _ipAddress;private int _port;public string Name => "ModbusTCP";public bool IsConnected => _master != null && _master.Transport != null && _master.Transport.IsConnected;public ModbusTcpDriver(string ipAddress, int port = 502){_ipAddress = ipAddress;_port = port;_factory = new ModbusFactory();}public async Task<bool> ConnectAsync(){try{_master = _factory.CreateMaster(new TcpClientAdapter(_ipAddress, _port));return true;}catch (Exception ex){Logger.Error(ex, "Modbus连接失败");return false;}}public async Task DisconnectAsync(){_master?.Dispose();_master = null;}public async Task<object> ReadTagAsync(string tagName){// 解析标签地址,如 "40001" 表示保持寄存器地址1if (int.TryParse(tagName, out int address)){try{ushort[] values = await _master.ReadHoldingRegistersAsync(1, (ushort)(address - 40001), 1);return values[0];}catch (Exception ex){Logger.Error(ex, "读取Modbus标签失败");return null;}}return null;}public async Task<bool> WriteTagAsync(string tagName, object value){if (int.TryParse(tagName, out int address) && value is short shortValue){try{await _master.WriteSingleRegisterAsync(1, (ushort)(address - 40001), (ushort)shortValue);return true;}catch (Exception ex){Logger.Error(ex, "写入Modbus标签失败");return false;}}return false;}public event EventHandler<DataChangedEventArgs> DataChanged;
}// OPC UA驱动示例
public class OpcUaDriver : IDeviceDriver
{private OpcUaClient _client;private string _endpointUrl;public string Name => "OPCUA";public bool IsConnected => _client != null && _client.Connected;public OpcUaDriver(string endpointUrl){_endpointUrl = endpointUrl;}public async Task<bool> ConnectAsync(){try{_client = new OpcUaClient();await _client.Connect(_endpointUrl);return true;}catch (Exception ex){Logger.Error(ex, "OPC UA连接失败");return false;}}public async Task DisconnectAsync(){_client?.Disconnect();}public async Task<object> ReadTagAsync(string tagName){try{return await _client.ReadNode(tagName);}catch (Exception ex){Logger.Error(ex, "读取OPC UA标签失败");return null;}}public async Task<bool> WriteTagAsync(string tagName, object value){try{await _client.WriteNode(tagName, value);return true;}catch (Exception ex){Logger.Error(ex, "写入OPC UA标签失败");return false;}}public event EventHandler<DataChangedEventArgs> DataChanged;
}
3. 实时数据库
// 标签点类
public class Tag : INotifyPropertyChanged
{private object _value;public string Name { get; set; }public string Address { get; set; }public string DataType { get; set; } = "Int16";public string Description { get; set; }public string DriverName { get; set; }public object Value{get => _value;set{if (!Equals(_value, value)){_value = value;OnPropertyChanged();ValueChanged?.Invoke(this, EventArgs.Empty);}}}public DateTime Timestamp { get; set; }public Quality Quality { get; set; } = Quality.Good;public event EventHandler ValueChanged;public event PropertyChangedEventHandler PropertyChanged;protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}
}// 实时数据库
public class RealTimeDatabase
{private readonly ConcurrentDictionary<string, Tag> _tags = new ConcurrentDictionary<string, Tag>();private readonly List<IDeviceDriver> _drivers = new List<IDeviceDriver>();private Timer _scanTimer;public void AddDriver(IDeviceDriver driver){_drivers.Add(driver);driver.DataChanged += OnDriverDataChanged;}public void AddTag(Tag tag){_tags[tag.Name] = tag;}public Tag GetTag(string name){return _tags.TryGetValue(name, out var tag) ? tag : null;}public void StartScan(int intervalMs = 1000){_scanTimer = new Timer(async _ => await ScanAllTagsAsync(), null, 0, intervalMs);}public void StopScan(){_scanTimer?.Dispose();}private async Task ScanAllTagsAsync(){foreach (var tag in _tags.Values){var driver = _drivers.FirstOrDefault(d => d.Name == tag.DriverName);if (driver != null && driver.IsConnected){try{var value = await driver.ReadTagAsync(tag.Address);tag.Value = value;tag.Timestamp = DateTime.Now;tag.Quality = Quality.Good;}catch (Exception ex){Logger.Error(ex, $"扫描标签{tag.Name}失败");tag.Quality = Quality.Bad;}}}}private void OnDriverDataChanged(object sender, DataChangedEventArgs e){// 处理设备主动上报的数据变化foreach (var tag in _tags.Values.Where(t => t.Address == e.Address && t.DriverName == ((IDeviceDriver)sender).Name)){tag.Value = e.Value;tag.Timestamp = DateTime.Now;tag.Quality = Quality.Good;}}public async Task<bool> WriteTag(string tagName, object value){var tag = GetTag(tagName);if (tag == null) return false;var driver = _drivers.FirstOrDefault(d => d.Name == tag.DriverName);if (driver == null || !driver.IsConnected) return false;try{return await driver.WriteTagAsync(tag.Address, value);}catch (Exception ex){Logger.Error(ex, $"写入标签{tagName}失败");return false;}}
}
4. 图形元素数据绑定
// 数据绑定系统
public class DataBindingManager
{private readonly RealTimeDatabase _database;private readonly Dictionary<GraphicElement, List<BindingInfo>> _bindings = new Dictionary<GraphicElement, List<BindingInfo>>();public DataBindingManager(RealTimeDatabase database){_database = database;}public void BindProperty(GraphicElement element, string propertyName, string tagName, BindingMode mode = BindingMode.OneWay){if (!_bindings.ContainsKey(element)){_bindings[element] = new List<BindingInfo>();}var tag = _database.GetTag(tagName);if (tag == null) return;var bindingInfo = new BindingInfo{PropertyName = propertyName,Tag = tag,Mode = mode};_bindings[element].Add(bindingInfo);// 初始值UpdateElementProperty(element, bindingInfo);// 订阅变化if (mode != BindingMode.OneTime){tag.ValueChanged += (s, e) => UpdateElementProperty(element, bindingInfo);}// 双向绑定if (mode == BindingMode.TwoWay){// 这里需要根据元素类型设置相应的事件处理if (element is ButtonElement button){button.Clicked += async (s, e) => {await _database.WriteTag(tagName, !(bool)(tag.Value ?? false));};}}}private void UpdateElementProperty(GraphicElement element, BindingInfo bindingInfo){var property = element.GetType().GetProperty(bindingInfo.PropertyName);if (property != null && property.CanWrite){// 在主线程更新UIApplication.Current.Dispatcher.Invoke(() =>{try{var convertedValue = ConvertValue(bindingInfo.Tag.Value, property.PropertyType);property.SetValue(element, convertedValue);}catch (Exception ex){Logger.Error(ex, $"更新元素属性{bindingInfo.PropertyName}失败");}});}}private object ConvertValue(object value, Type targetType){if (value == null) return targetType.IsValueType ? Activator.CreateInstance(targetType) : null;if (targetType.IsInstanceOfType(value)) return value;try{return Convert.ChangeType(value, targetType);}catch{return targetType.IsValueType ? Activator.CreateInstance(targetType) : null;}}
}public class BindingInfo
{public string PropertyName { get; set; }public Tag Tag { get; set; }public BindingMode Mode { get; set; }
}public enum BindingMode
{OneTime,OneWay,TwoWay
}
5. 主界面和编辑器
// 主窗口
public partial class MainWindow : Window
{private RealTimeDatabase _database;private DataBindingManager _bindingManager;private GraphicScreen _currentScreen;public MainWindow(){InitializeComponent();// 初始化数据库和绑定管理器_database = new RealTimeDatabase();_bindingManager = new DataBindingManager(_database);// 加载配置LoadConfiguration();// 启动扫描_database.StartScan();}private void LoadConfiguration(){// 加载设备驱动var modbusDriver = new ModbusTcpDriver("192.168.1.10");_database.AddDriver(modbusDriver);// 加载标签点var tags = ConfigLoader.LoadTags("tags.json");foreach (var tag in tags){_database.AddTag(tag);}// 加载画面_currentScreen = ConfigLoader.LoadScreen("main_screen.json");}protected override void OnRender(DrawingContext drawingContext){base.OnRender(drawingContext);_currentScreen?.Render(drawingContext);}protected override void OnMouseDown(MouseButtonEventArgs e){base.OnMouseDown(e);var position = e.GetPosition(this);// 检查是否点击了某个元素foreach (var element in _currentScreen.Elements.Reverse()){if (element.HitTest(position)){SelectElement(element);break;}}}private void SelectElement(GraphicElement element){// 显示属性面板propertyGrid.SelectedObject = element;}protected override void OnClosed(EventArgs e){base.OnClosed(e);_database.StopScan();}
}
参考代码 基于C#简单的组态软件开发 www.youwenfan.com/contentcse/111974.html
项目结构和扩展功能
项目结构建议
SCADA-Solution/
├── SCADA.Core/ # 核心库
│ ├── Drivers/ # 设备驱动
│ ├── Graphics/ # 图形元素
│ ├── Database/ # 实时数据库
│ └── Binding/ # 数据绑定
├── SCADA.Editor/ # 图形编辑器
├── SCADA.Runtime/ # 运行时环境
└── SCADA.Common/ # 公共工具类
这个简单的组态软件开发指南涵盖了核心功能和实现方法。