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/ # 公共工具類
這個簡單的組態軟件開發指南涵蓋了核心功能和實現方法。