西门子 S7 PLC 通信 WPF 应用分析笔记
1. 项目概述
- 技术栈 :
- WPF(Windows Presentation Foundation)用于界面展示。
- MVVM (Model-View-ViewModel)设计模式,通过
GalaSoft.MvvmLight实现。 - S7.Net 库用于与西门子 S7 系列 PLC 进行以太网通信。
- 功能 :
- 配置 PLC 连接参数(IP、机架号、槽号)。
- 连接 / 断开 PLC。
- 从指定地址读取数据。
- 向指定地址写入数据。
- 记录操作日志。
2. 整体架构(MVVM 模式)
View(视图)
- 文件 :
MainWindow.xaml - 作用 :
- 定义界面布局和控件。
- 通过 数据绑定 将控件与 ViewModel 的属性关联。
- 通过 命令绑定 将按钮点击事件绑定到 ViewModel 的命令。
ViewModel(视图模型)
- 文件 :
MainViewModel.cs - 作用 :
- 持有界面所需的数据(连接参数、读写地址、结果、日志等)。
- 实现业务逻辑(连接、断开、读写 PLC)。
- 通过
INotifyPropertyChanged通知 View 更新界面。 - 通过
RelayCommand处理用户操作。
Model(模型)
- 外部库 :
S7.Net - 作用 :
- 提供
Plc类实现与 S7 PLC 的底层通信。 - 封装了 TCP/IP 连接、数据读写等功能。
- 提供
3. 界面(View)分析
主要区域
- 标题 :
- 显示
"西门子S7 PLC通信"。
- 显示
- 连接设置 :
- IP 地址 (绑定
IpAddress)。 - 机架号 (绑定
Rack)。 - 槽号 (绑定
Slot)。 - 连接状态 (绑定
ConnectionStatus)。 - 连接按钮 (绑定
ConnectCommand,仅在未连接时可用)。 - 断开按钮 (绑定
DisconnectCommand,仅在已连接时可用)。
- IP 地址 (绑定
- 读取操作 :
- 读取地址 (绑定
ReadAddress)。 - 读取按钮 (绑定
ReadCommand,仅在已连接时可用)。 - 读取结果 (绑定
ReadValue,只读)。
- 读取地址 (绑定
- 写入操作 :
- 写入地址 (绑定
WriteAddress)。 - 写入数值 (绑定
WriteValue)。 - 写入按钮 (绑定
WriteCommand,仅在已连接时可用)。
- 写入地址 (绑定
- 操作日志 :
- 显示程序运行日志(绑定
Log,只读)。
- 显示程序运行日志(绑定
绑定方式
-
数据绑定 :
xml
<TextBox Text="{Binding IpAddress}" /> -
命令绑定 :
xml
<Button Command="{Binding ConnectCommand}" /> -
按钮可用性绑定 :
xml
<Button IsEnabled="{Binding !IsConnected}" />
4. 视图模型(ViewModel)分析
4.1 数据属性
-
连接参数 :
csharp
运行
public string IpAddress { get; set; } public short Rack { get; set; } public short Slot { get; set; } -
状态属性 :
csharp
运行
public string ConnectionStatus { get; set; } public bool IsConnected { get; set; } -
读写相关 :
csharp
运行
public string ReadAddress { get; set; } public string ReadValue { get; set; } public string WriteAddress { get; set; } public string WriteValue { get; set; } -
日志 :
csharp
运行
public string Log { get; set; }
4.2 命令
- ConnectCommand:连接 PLC。
- DisconnectCommand:断开 PLC。
- ReadCommand:读取数据。
- WriteCommand:写入数据。
4.3 核心方法
AddLog
csharp
运行
private void AddLog(string message)
{
Log = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}\n{Log}";
}
- 在日志前添加时间戳。
- 新日志显示在最上方。
Connect
csharp
运行
private async Task Connect()
{
try
{
AddLog($"正在连接PLC: {IpAddress}, 机架: {Rack}, 槽号: {Slot}");
_plc = new Plc(CpuType.S71200, IpAddress, Rack, Slot);
await Task.Run(() => _plc.Open());
IsConnected = _plc.IsConnected;
ConnectionStatus = IsConnected ? "已连接" : "连接失败";
AddLog(ConnectionStatus);
}
catch (Exception ex)
{
ConnectionStatus = "连接错误: " + ex.Message;
IsConnected = false;
AddLog(ConnectionStatus);
MessageBox.Show(ex.Message, "连接错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
- 创建
Plc对象并调用Open()连接。 - 异步执行防止 UI 阻塞。
- 更新连接状态并记录日志。
- 异常处理:提示错误信息。
Disconnect
csharp
运行
private async Task Disconnect()
{
try
{
if (_plc != null && _plc.IsConnected)
{
AddLog("正在断开PLC连接");
await Task.Run(() => _plc.Close());
IsConnected = false;
ConnectionStatus = "已断开";
AddLog(ConnectionStatus);
}
}
catch (Exception ex)
{
ConnectionStatus = "断开错误: " + ex.Message;
AddLog(ConnectionStatus);
MessageBox.Show(ex.Message, "断开错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
- 调用
Close()断开连接。 - 更新状态并记录日志。
ReadFromPLC
csharp
运行
private async Task ReadFromPLC()
{
try
{
if (_plc != null && _plc.IsConnected)
{
AddLog($"正在读取PLC地址: {ReadAddress}");
var value = await Task.Run(() => _plc.Read(ReadAddress));
ReadValue = value?.ToString() ?? "读取失败";
AddLog($"读取成功: {ReadValue}");
}
else
{
AddLog("读取失败: 请先连接PLC");
MessageBox.Show("请先连接PLC", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
ReadValue = "读取错误: " + ex.Message;
AddLog(ReadValue);
MessageBox.Show(ex.Message, "读取错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
- 检查连接状态。
- 调用
_plc.Read()读取数据。 - 更新
ReadValue并记录日志。
WriteToPLC
csharp
运行
private async Task WriteToPLC()
{
try
{
if (_plc != null && _plc.IsConnected)
{
if (double.TryParse(WriteValue, out double numericValue))
{
AddLog($"正在写入PLC地址: {WriteAddress}, 值: {numericValue}");
await Task.Run(() => _plc.Write(WriteAddress, numericValue));
AddLog("写入成功");
MessageBox.Show("写入成功", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
AddLog("写入失败: 请输入有效的数值");
MessageBox.Show("请输入有效的数值", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
else
{
AddLog("写入失败: 请先连接PLC");
MessageBox.Show("请先连接PLC", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
AddLog($"写入错误: {ex.Message}");
MessageBox.Show(ex.Message, "写入错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
- 检查连接状态。
- 验证输入是否为有效的
double。 - 调用
_plc.Write()写入数据。 - 记录日志并提示结果。
5. 关键技术点
- MVVM 模式 :
- 分离界面与业务逻辑,提高可维护性。
- 数据绑定 :
- View 与 ViewModel 自动同步数据。
- 命令模式 :
RelayCommand实现按钮点击事件与 ViewModel 方法绑定。
- 异步编程 :
async/await防止 UI 阻塞。
- S7.Net 库 :
- 简化与西门子 S7 PLC 的通信。
- 日志记录 :
- 实时显示操作过程和错误信息。
6. 运行流程示例
- 用户输入 IP 地址 、机架号 、槽号。
- 点击 连接 按钮:
ConnectCommand调用Connect()方法。- 程序尝试连接 PLC。
- 更新连接状态并记录日志。
- 输入 读取地址 ,点击 读取 :
ReadCommand调用ReadFromPLC()。- 从 PLC 读取数据并显示结果。
- 输入 写入地址 和 写入数值 ,点击 写入 :
WriteCommand调用WriteToPLC()。- 将数据写入 PLC 并提示结果。
- 点击 断开 按钮:
DisconnectCommand调用Disconnect()。- 断开与 PLC 的连接。
7. 改进建议
- 日志持久化 :
- 将日志保存到文件,方便后续分析。
- 数据类型支持 :
- 支持更多数据类型(如
int,bool)。
- 支持更多数据类型(如
- 批量读写 :
- 支持一次性读写多个地址。
- 错误处理优化 :
- 将
MessageBox替换为界面内的错误提示,提升用户体验。
- 将
- UI 美化 :
- 使用
MahApps.Metro等库美化界面。
- 使用
✅ 总结 :该项目是一个基于 WPF 和 MVVM 模式的西门子 S7 PLC 通信工具,通过 S7.Net 库实现了 PLC 的连接、数据读取和写入功能,并提供了友好的日志记录功能。代码结构清晰,易于扩展和维护。
完整代码:
cs
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using S7.Net;
using System;
using System.Threading.Tasks;
using System.Windows;
namespace WpfApp1.ViewModel
{
public class MainViewModel : ViewModelBase
{
// PLC 核心对象
private Plc _plc;
// 连接配置属性
private string _ipAddress = "192.168.0.1";
private short _rack = 0;
private short _slot = 1;
// 状态属性
private string _connectionStatus = "未连接";
private bool _isConnected;
// 读写相关属性
private string _readValue;
private string _writeValue;
private string _readAddress = "DB1.DBD0";
private string _writeAddress = "DB1.DBD0";
// 日志属性
private string _log;
// 公共属性(INotifyPropertyChanged 支持)
public string Log
{
get => _log;
set => Set(ref _log, value);
}
public string IpAddress
{
get => _ipAddress;
set => Set(ref _ipAddress, value);
}
public short Rack
{
get => _rack;
set => Set(ref _rack, value);
}
public short Slot
{
get => _slot;
set => Set(ref _slot, value);
}
public string ConnectionStatus
{
get => _connectionStatus;
set => Set(ref _connectionStatus, value);
}
public bool IsConnected
{
get => _isConnected;
set => Set(ref _isConnected, value);
}
public string ReadValue
{
get => _readValue;
set => Set(ref _readValue, value);
}
public string WriteValue
{
get => _writeValue;
set => Set(ref _writeValue, value);
}
public string ReadAddress
{
get => _readAddress;
set => Set(ref _readAddress, value);
}
public string WriteAddress
{
get => _writeAddress;
set => Set(ref _writeAddress, value);
}
// 命令属性
public RelayCommand ConnectCommand { get; private set; }
public RelayCommand DisconnectCommand { get; private set; }
public RelayCommand ReadCommand { get; private set; }
public RelayCommand WriteCommand { get; private set; }
// 构造函数
public MainViewModel()
{
// 初始化命令
ConnectCommand = new RelayCommand(async () => await Connect());
DisconnectCommand = new RelayCommand(async () => await Disconnect());
ReadCommand = new RelayCommand(async () => await ReadFromPLC());
WriteCommand = new RelayCommand(async () => await WriteToPLC());
// 启动日志
AddLog("应用程序启动");
}
/// <summary>
/// 添加日志记录(时间戳 + 消息)
/// </summary>
/// <param name="message">日志内容</param>
private void AddLog(string message)
{
Log = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}\n{Log}";
}
/// <summary>
/// 连接PLC
/// </summary>
private async Task Connect()
{
try
{
AddLog($"正在连接PLC: {IpAddress}, 机架: {Rack}, 槽号: {Slot}");
// 初始化PLC对象并连接(异步执行)
_plc = new Plc(CpuType.S71200, IpAddress, Rack, Slot);
await Task.Run(() => _plc.Open());
// 更新连接状态
IsConnected = _plc.IsConnected;
ConnectionStatus = IsConnected ? "已连接" : "连接失败";
AddLog(ConnectionStatus);
}
catch (Exception ex)
{
// 异常处理
ConnectionStatus = "连接错误: " + ex.Message;
IsConnected = false;
AddLog(ConnectionStatus);
MessageBox.Show(ex.Message, "连接错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 断开PLC连接
/// </summary>
private async Task Disconnect()
{
try
{
if (_plc != null && _plc.IsConnected)
{
AddLog("正在断开PLC连接");
// 异步断开连接
await Task.Run(() => _plc.Close());
// 更新状态
IsConnected = false;
ConnectionStatus = "已断开";
AddLog(ConnectionStatus);
}
}
catch (Exception ex)
{
// 异常处理
ConnectionStatus = "断开错误: " + ex.Message;
AddLog(ConnectionStatus);
MessageBox.Show(ex.Message, "断开错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 从PLC读取数据
/// </summary>
private async Task ReadFromPLC()
{
try
{
if (_plc != null && _plc.IsConnected)
{
AddLog($"正在读取PLC地址: {ReadAddress}");
// 异步读取数据
var value = await Task.Run(() => _plc.Read(ReadAddress));
// 更新读取结果
ReadValue = value?.ToString() ?? "读取失败";
AddLog($"读取成功: {ReadValue}");
}
else
{
// 未连接状态提示
AddLog("读取失败: 请先连接PLC");
MessageBox.Show("请先连接PLC", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
// 异常处理
ReadValue = "读取错误: " + ex.Message;
AddLog(ReadValue);
MessageBox.Show(ex.Message, "读取错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 向PLC写入数据
/// </summary>
private async Task WriteToPLC()
{
try
{
if (_plc != null && _plc.IsConnected)
{
// 验证输入是否为有效数值
if (double.TryParse(WriteValue, out double numericValue))
{
AddLog($"正在写入PLC地址: {WriteAddress}, 值: {numericValue}");
// 异步写入数据
await Task.Run(() => _plc.Write(WriteAddress, numericValue));
// 写入成功提示
AddLog("写入成功");
MessageBox.Show("写入成功", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
// 输入格式错误提示
AddLog("写入失败: 请输入有效的数值");
MessageBox.Show("请输入有效的数值", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
else
{
// 未连接状态提示
AddLog("写入失败: 请先连接PLC");
MessageBox.Show("请先连接PLC", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
// 异常处理
AddLog($"写入错误: {ex.Message}");
MessageBox.Show(ex.Message, "写入错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}
XML
<Window x:Class="WpfApp1.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
xmlns:vm="clr-namespace:WpfApp1.ViewModel"
mc:Ignorable="d"
Title="西门子S7 PLC通信" Height="546.939" Width="816.764">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 标题 -->
<TextBlock Grid.Row="0" Grid.ColumnSpan="2" Text="西门子S7 PLC通信" FontSize="24" FontWeight="Bold" Margin="0,0,0,20" HorizontalAlignment="Center" />
<!-- 连接设置 -->
<GroupBox Grid.Row="1" Grid.ColumnSpan="2" Header="连接设置" Margin="0,0,0,20">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- IP地址 -->
<Label Grid.Row="0" Grid.Column="0" Content="IP地址:" Margin="10,10,10,10" VerticalAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding IpAddress}" Margin="0,10,10,10" Height="30" />
<!-- 机架号 -->
<Label Grid.Row="0" Grid.Column="2" Content="机架:" Margin="10,10,10,10" VerticalAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="3" Text="{Binding Rack}" Margin="0,10,10,10" Width="60" Height="30" />
<!-- 槽号 -->
<Label Grid.Row="0" Grid.Column="4" Content="槽号:" Margin="10,10,10,10" VerticalAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="5" Text="{Binding Slot}" Margin="0,10,10,10" Width="60" Height="30" />
<!-- 连接状态 -->
<Label Grid.Row="1" Grid.Column="0" Content="连接状态:" Margin="10,10,10,10" VerticalAlignment="Center" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ConnectionStatus}" Margin="0,10,10,10" Height="30" VerticalAlignment="Center" FontWeight="Bold" />
<!-- 连接按钮 -->
<Button Grid.Row="1" Grid.Column="2" Content="连接" Command="{Binding ConnectCommand}" Margin="0,10,10,10" Width="80" Height="30" IsEnabled="{Binding !IsConnected}" />
<!-- 断开按钮 -->
<Button Grid.Row="1" Grid.Column="3" Content="断开" Command="{Binding DisconnectCommand}" Margin="0,10,10,10" Width="80" Height="30" IsEnabled="{Binding IsConnected}" />
</Grid>
</GroupBox>
<!-- 读取操作 -->
<GroupBox Grid.Row="2" Grid.Column="0" Header="读取操作" Margin="0,0,10,20" Width="380">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 读取地址 -->
<Label Grid.Row="0" Grid.Column="0" Content="读取地址:" Margin="10,10,10,10" VerticalAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding ReadAddress}" Margin="0,10,10,10" Height="30" />
<!-- 读取按钮 -->
<Button Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Content="读取" Command="{Binding ReadCommand}" Margin="10,10,10,10" Height="40" IsEnabled="{Binding IsConnected}" />
<!-- 读取结果 -->
<Label Grid.Row="2" Grid.Column="0" Content="读取结果:" Margin="10,10,10,10" VerticalAlignment="Center" />
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding ReadValue}" Margin="0,10,10,10" Height="30" IsReadOnly="True" Background="#F0F0F0" />
</Grid>
</GroupBox>
<!-- 写入操作 -->
<GroupBox Grid.Row="2" Grid.Column="1" Header="写入操作" Margin="0,0,0,20" Width="380">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 写入地址 -->
<Label Grid.Row="0" Grid.Column="0" Content="写入地址:" Margin="10,10,10,10" VerticalAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding WriteAddress}" Margin="0,10,10,10" Height="30" />
<!-- 写入数值 -->
<Label Grid.Row="1" Grid.Column="0" Content="写入数值:" Margin="10,10,10,10" VerticalAlignment="Center" />
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding WriteValue}" Margin="0,10,10,10" Height="30" />
<!-- 写入按钮 -->
<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Content="写入" Command="{Binding WriteCommand}" Margin="10,10,10,10" Height="40" IsEnabled="{Binding IsConnected}" />
</Grid>
</GroupBox>
<!-- 状态日志 -->
<GroupBox Grid.Row="3" Grid.ColumnSpan="2" Header="操作日志" Margin="0,0,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox Margin="10,10,10,10" IsReadOnly="True" Background="#F0F0F0" AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Text="{Binding Log}" />
</Grid>
</GroupBox>
</Grid>
</Window>