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" 表示保持寄存器地址1
if (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)
{
// 在主线程更新UI
Application.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/ # 公共工具类
这个简单的组态软件开发指南涵盖了核心功能和实现方法。