第一个 WPF 应用
这一篇带你做一个最小 WPF 点表面板:从 zeus.json 装载虚拟 Modbus,用 MVVM 显示通道状态、炉温和报警。窗口只设置 DataContext,控件只绑定 ViewModel。
完整项目骨架请用 dotnet new zeus-wpf。仓库示例 Zeus.Samples.Wpf.QuickStart 与模板同一条路径。通道回显请看 控制台示例。
先运行示例
在仓库的 code 目录执行:
dotnet run --project samples/Zeus.Samples.Wpf.QuickStart
窗口打开后:
- 通道状态应显示
Open。 - 温度应显示采集值(空虚拟从站默认
0.0 C)。 - 点击「模拟高温」后,温度变为
96.5 C,报警状态变为High。
这说明宿主、JSON 点表、采集循环、Dispatcher 封送和 XAML 绑定都通了。
你的项目需要哪些包
dotnet add package Zeus.Communications
dotnet add package Zeus.Configuration
dotnet add package Zeus.Presentation.Wpf
dotnet add package Zeus.Protocols.Modbus
dotnet add package CommunityToolkit.Mvvm
dotnet add package Microsoft.Extensions.DependencyInjection
项目文件需要是 Windows 桌面目标:
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
把 zeus.json 复制到输出目录。
XAML 界面
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Zeus WPF QuickStart"
Height="380"
Width="520">
<StackPanel Margin="16">
<TextBlock Text="{Binding Bus.StateText}" />
<TextBlock Text="{Binding Temperature.Text}" />
<TextBlock Text="{Binding Temperature.AlarmState}" />
<Button Content="{Binding ToggleTemperatureText}"
Command="{Binding ToggleTemperatureCommand}" />
</StackPanel>
</Window>
窗口代码
先让 App 做组合根。App.xaml 中不要再写 StartupUri="MainWindow.xaml",窗口由容器创建。
using System.IO;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Zeus;
public partial class App : Application
{
private ServiceProvider? _services;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
_services = ConfigureServices();
MainWindow = _services.GetRequiredService<MainWindow>();
MainWindow.Show();
}
protected override void OnExit(ExitEventArgs e)
{
_services?.DisposeAsync().AsTask().GetAwaiter().GetResult();
base.OnExit(e);
}
private static ServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<IUiDispatcher>(_ => WpfUiDispatcher.Current());
services.AddSingleton<IZeusHost>(_ => ZeusHost.Create(builder =>
builder.AddJsonFile(Path.Combine(AppContext.BaseDirectory, "zeus.json"))));
services.AddTransient<MainWindowViewModel>();
services.AddTransient<MainWindow>();
return services.BuildServiceProvider();
}
}
窗口只接收依赖、挂接宿主并设置 DataContext:
using System.Windows;
using Zeus;
public partial class MainWindow : Window
{
private readonly MainWindowViewModel _viewModel;
public MainWindow(IZeusHost host, MainWindowViewModel viewModel)
{
InitializeComponent();
this.AttachZeus(host);
_viewModel = viewModel;
DataContext = _viewModel;
Closed += (_, _) => _viewModel.Dispose();
}
}
ViewModel
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Zeus;
public sealed partial class MainWindowViewModel : ObservableObject, IDisposable
{
private readonly ZeusBindingContext _ui;
private readonly ModbusDevice _oven;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ToggleTemperatureText))]
private bool _highTemperature;
public MainWindowViewModel(IZeusHost host, IUiDispatcher dispatcher)
{
_ui = host.Bind(dispatcher);
_oven = host.Devices.Get<ModbusDevice>("oven");
Bus = _ui.Channel("bus");
Temperature = _ui.Point("temperature", value => $"{value:0.0} C");
}
public ChannelBindingSource Bus { get; }
public PointBindingSource Temperature { get; }
public string ToggleTemperatureText => HighTemperature ? "恢复正常" : "模拟高温";
public void Dispose() => _ui.Dispose();
[RelayCommand]
private async Task ToggleTemperatureAsync()
{
var next = !HighTemperature;
await _oven.WriteSingleRegisterAsync(0, next ? (ushort)965 : (ushort)725);
HighTemperature = next;
}
}
这段代码怎么理解
| 代码 | 作用 |
|---|---|
AddJsonFile("zeus.json") | 装载通道、虚拟 Modbus 和点表 |
this.AttachZeus(host) | 窗口加载时启动宿主,关闭时释放 |
host.Bind(dispatcher) | 调度器只传一次,后面用 _ui.Channel / _ui.Point |
_ui.Point("temperature", ...) | 把采集值投影成 XAML 可绑定属性 |
WriteSingleRegisterAsync | 改虚拟从站寄存器,下一轮采集刷新界面 |
为什么不用自己 Dispatcher.Invoke
采集回调可能来自 IO 线程。绑定源负责封送,XAML 只做普通数据绑定。
换成真实串口
模板和本页示例都读 zeus.json。把通道从虚拟从站改成串口即可,ViewModel 和 XAML 不用动:
{
"name": "bus",
"type": "serial",
"options": {
"portName": "COM3",
"baudRate": 9600
}
}
校验位、数据位、停止位目前只能在代码里用 AddSerialPort(name, options => …) 设置;JSON 串口参数写在 options.portName 和 options.baudRate。