跳到主要内容

第一个 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

窗口打开后:

  1. 通道状态应显示 Open。
  2. 温度应显示采集值(空虚拟从站默认 0.0 C)。
  3. 点击「模拟高温」后,温度变为 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。

下一步:了解 WPF 适配器,或继续看 串口。