当前位置: 首页 > news >正文

WPF 四轴上机位开发笔记:限值参数、JSON 持久化、XAML 绑定与校验

WPF 四轴上机位开发笔记:限值参数、JSON 持久化、XAML 绑定与校验

基于 .NET 10 WPF / MVVM / NModbus4 的四轴运动控制项目


一、今日目标

  1. 为 4 个轴添加速度/加速度/减速度/力矩的上下限配置
  2. 限值参数持久化到 JSON 文件,重启后自动加载
  3. 在写入 PLC 前进行限值校验,确保参数不越界
  4. 修复 XAML 绑定大小写不匹配导致的静默失败

二、AxisParam 模型:限值属性定义

Models\AxisParam.cs中新增 8 个限值属性(全部为float非空类型,区别于运动参数的float?):

// ============== 参数的上限下限 ============privatefloat_velUpLimit;privatefloat_velLowerLimit;privatefloat_accelUpLimit;privatefloat_accelLowerLimit;privatefloat_decelUpLimit;privatefloat_decelLowerLimit;privatefloat_torqueUpLimit;privatefloat_torqueLowerLimit;// 公开属性(XAML 绑定目标)publicfloatVelUpLimit{get=>_velUpLimit;set{_velUpLimit=value;OnPropertyChanged();}}publicfloatVelLowerLimit{get=>_velLowerLimit;set{_velLowerLimit=value;OnPropertyChanged();}}publicfloatAccelUpLimit{get=>_accelUpLimit;set{_accelUpLimit=value;OnPropertyChanged();}}publicfloatAccelLowerLimit{get=>_accelLowerLimit;set{_accelLowerLimit=value;OnPropertyChanged();}}publicfloatDecelUpLimit{get=>_decelUpLimit;set{_decelUpLimit=value;OnPropertyChanged();}}publicfloatDecelLowerLimit{get=>_decelLowerLimit;set{_decelLowerLimit=value;OnPropertyChanged();}}publicfloatTorqueUpLimit{get=>_torqueUpLimit;set{_torqueUpLimit=value;OnPropertyChanged();}}publicfloatTorqueLowerLimit{get=>_torqueLowerLimit;set{_torqueLowerLimit=value;OnPropertyChanged();}}

语法要点

语法说明
floatvsfloat?限值用float(默认 0,永远有值);运动参数用float?(用户可选填)
OnPropertyChanged()依赖[CallerMemberName]编译器自动填入属性名,无需写字符串
{ get; set; }完整写法因为需要后接OnPropertyChanged通知,必须写完整属性体

三、JSON 序列化与反序列化(关键!)

3.1 静态文件路径

MainViewModel类顶部定义统一的路径常量,确保读写始终指向同一个文件:

privatestaticreadonlystringLimitConfigPath=Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"LimitConfig.json");

为什么必须用AppDomain.CurrentDomain.BaseDirectory

  • VS 调试时当前工作目录是项目根目录,双击 exe 运行时是 exe 所在目录
  • BaseDirectory永远返回 exe 所在目录(bin\Debug\net10.0-windows\),保证行为一致

3.2 SaveLimitsToJson — 序列化

使用匿名类型 +System.Text.Json将 4 个轴的限值写入 JSON:

privatevoidSaveLimitsToJson(){vardata=new{Axis1=new{VelUp=Axis1Data.Data.VelUpLimit,VelLow=Axis1Data.Data.VelLowerLimit,AccelUp=Axis1Data.Data.AccelUpLimit,AccelLow=Axis1Data.Data.AccelLowerLimit,DecelUp=Axis1Data.Data.DecelUpLimit,DecelLow=Axis1Data.Data.DecelLowerLimit,TorqueUp=Axis1Data.Data.TorqueUpLimit,TorqueLow=Axis1Data.Data.TorqueLowerLimit,},// Axis2~Axis4 同上...};varjson=System.Text.Json.JsonSerializer.Serialize(data,newSystem.Text.Json.JsonSerializerOptions{WriteIndented=true});File.WriteAllText(LimitConfigPath,json);}

生成的 JSON 格式:

{"Axis1":{"VelUp":1000,"VelLow":100,"AccelUp":500,"AccelLow":50,"DecelUp":500,"DecelLow":50,"TorqueUp":100,"TorqueLow":10},...}

语法要点:

  • 匿名类型new { Axis1 = new { ... } }— 不需要定义专门的 DTO 类
  • File.WriteAllText— 覆盖写入,不是追加。每次保存都是完整写出全部 4 轴数据
  • WriteIndented = true— 格式化 JSON,方便人工查看

3.3 LoadLimitFromJson — 反序列化

由于保存时用了匿名类型,反序列化时无法直接用泛型方法,需要用JsonDocument手动解析:

privatevoidLoadLimitFromJson(){if(!File.Exists(LimitConfigPath))return;// 首次运行没有文件,直接跳过varjson=File.ReadAllText(LimitConfigPath);usingvardoc=System.Text.Json.JsonDocument.Parse(json);varroot=doc.RootElement;varaxes=new[]{Axis1Data,Axis2Data,Axis3Data,Axis4Data};string[]axisNames={"Axis1","Axis2","Axis3","Axis4"};for(inti=0;i<4;i++){varel=root.GetProperty(axisNames[i]);axes[i].Data.VelUpLimit=el.GetProperty("VelUp").GetSingle();axes[i].Data.VelLowerLimit=el.GetProperty("VelLow").GetSingle();axes[i].Data.AccelUpLimit=el.GetProperty("AccelUp").GetSingle();axes[i].Data.AccelLowerLimit=el.GetProperty("AccelLow").GetSingle();axes[i].Data.DecelUpLimit=el.GetProperty("DecelUp").GetSingle();axes[i].Data.DecelLowerLimit=el.GetProperty("DecelLow").GetSingle();axes[i].Data.TorqueUpLimit=el.GetProperty("TorqueUp").GetSingle();axes[i].Data.TorqueLowerLimit=el.GetProperty("TorqueLow").GetSingle();}}

语法要点:

语法说明
JsonDocument.Parse(json)解析 JSON 字符串为可查询的文档对象
using var doc确保JsonDocument使用完后释放非托管内存
root.GetProperty("Axis1")获取 JSON 对象中的指定属性
el.GetProperty("VelUp").GetSingle()获取属性值并转换为float
if (!File.Exists(...)) return;首次运行没有 JSON 文件时优雅退出

3.4 加载时机

MainViewModel构造函数中调用一次,保证程序启动时限值恢复到上次保存的值:

publicMainViewModel(){LoadLimitFromJson();// ← 启动时加载ConnectionCommand=newRelayCommand(Connect);// ... 其他命令绑定}

ExecuteLimitParam(进入限值设置页面前)再调一次,保证看到最新数据:

privatevoidExecuteLimitParam(){LoadLimitFromJson();// ← 进页面前重新加载(防外部修改)varpage=newLimitAxesPage();page.DataContext=this;NavigateToPage?.Invoke(page);}

四、XAML 绑定大坑:大小写敏感

问题描述

XAML 中写的是:

<TextBoxText="{Binding Axis1Data.Data.accelUpLimit}"/>

但 C# 属性定义是:

publicfloatAccelUpLimit{...}// 大写 A

WPF 绑定是大小写敏感的!绑定失败时没有报错,不会编译失败,只在输出窗口有警告。TextBox 的值永远是 0,导致限值校验永远认为「下限为 0」而报错。

修复

XAML 所有绑定路径必须与 C# 属性名完全一致:

错误(小写 a)正确(大写 A)
Data.accelUpLimitData.AccelUpLimit
Data.accelLowerLimitData.AccelLowerLimit

共 4 个轴 × 2 个属性 = 8 处。

教训

  • C# 属性命名建议统一大写开头(PascalCase)
  • XAML 绑定写完后可以用 Snoop / 输出窗口检查 Binding 是否成功
  • 或者先用FallbackValue测试绑定链是否连通

五、限值校验逻辑

5.1 ExecuteAxisLimit — 保存前的验证

在 LimitAxesPage 点击 Confirm 按钮时调用:

privatevoidExecuteAxisLimit(intindex){if(_service==null||!IsConnected)return;varaxes=new[]{Axis1Data,Axis2Data,Axis3Data,Axis4Data};vardata=axes[index].Data;varcheck=new(floatup,floatlow,stringname)[]{(data.VelUpLimit,data.VelLowerLimit,"VelLimit"),(data.AccelUpLimit,data.AccelLowerLimit,"AccelLimit"),(data.DecelUpLimit,data.DecelLowerLimit,"DecelLimit"),(data.TorqueUpLimit,data.TorqueLowerLimit,"TorqueLimit"),};foreach(var(up,low,name)incheck){if(low<=0||up<=low){System.Windows.MessageBox.Show($"轴{index+1}{name}限值无效(下限>0,上限>下限)");return;}}SaveLimitsToJson();System.Windows.MessageBox.Show($"轴{index+1}限值已保存");}

语法要点:

  • (float up, float low, string name)[]— C# 7.0+ 值元组数组,比定义类更轻量
  • foreach (var (up, low, name) in check)— 元组解构,直接取元组元素
  • 验证条件low <= 0 || up <= low— 下限必须 > 0,上限必须 > 下限

5.2 ValidateAxisParam — 写入前的参数值校验

在写入 PLC 前(Apply / Confirm)验证运动参数是否超出限值:

publicboolValidateAxisParam(intindex,outstringmsg,stringactionMode){msg="";varaxes=new[]{Axis1Data,Axis2Data,Axis3Data,Axis4Data};vardata=axes[index].Data;// 根据模式选择要检查的字段组(float?v,stringn)[]fields;if(actionMode=="Rel")fields=new(float?v,stringn)[]{(data.RelPos,"RelPos"),(data.RelVel,"RelVel"),(data.RelAccel,"RelAccel"),(data.RelDecel,"RelDecel")};elseif(actionMode=="Abso")fields=new(float?v,stringn)[]{(data.AbsoPos,"AbsoPos"),(data.AbsoVel,"AbsoVel"),(data.AbsoAccel,"AbsoAccel"),(data.AbsoDecel,"AbsoDecel")};else{msg="未知模式";returnfalse;}foreach(var(v,n)infields){// 第一步:检查空值和零值if(v==null||v==0f){msg=$"Axis{index+1}{n}无效(为空或0)";returnfalse;}// 第二步:根据字段名匹配对应的限值if(n.Contains("Vel")){if(v<data.VelLowerLimit||v>data.VelUpLimit){msg=$"Axis{index+1}{n}超出速度限值({data.VelLowerLimit}~{data.VelUpLimit})";returnfalse;}}elseif(n.Contains("Accel")){if(v<data.AccelLowerLimit||v>data.AccelUpLimit){msg=$"Axis{index+1}{n}超出加速度限值({data.AccelLowerLimit}~{data.AccelUpLimit})";returnfalse;}}elseif(n.Contains("Decel")){if(v<data.DecelLowerLimit||v>data.DecelUpLimit){msg=$"Axis{index+1}{n}超出减速度限值({data.DecelLowerLimit}~{data.DecelUpLimit})";returnfalse;}}// RelPos / AbsoPos 没有对应的限值,跳过}returntrue;}

语法要点:

语法说明
out string msg输出参数,方法内部赋值,调用方直接获取错误信息
(float? v, string n)值元组,同时携带值和名称,方便错误消息拼接
n.Contains("Vel")用字段名模糊匹配来确定对应限值,同时覆盖 RelVel 和 AbsoVel
v < data.VelLowerLimit编译时float?float可隐式比较,但赋值给float时必须用.Value

5.3 校验流程图

用户输入值 → 点击按钮 ↓ ValidateAxisParam 检查 null / 0 ↓ (通过) 检查字段名是否包含 "Vel"/"Accel"/"Decel" ↓ 获取对应的 upLimit / lowerLimit ↓ v < lowerLimit 或 v > upLimit ? ├─ 是 → 弹窗报错,不写入 └─ 否 → 写入 PLC

六、今日踩坑总结

坑 1:XAML 绑定大小写

  • WPF 绑定路径区分大小写(accelUpLimitAccelUpLimit
  • 绑定失败不抛异常,只在 VS 输出窗口有 BindingWarning
  • 解决方案:写绑定前确认 C# 属性名,或先用 FallbackValue 测试

坑 2:JSON 读写路径不一致

  • 保存用LimitConfigPath(指向 exe 目录),加载用"LimitConfig.json"(指向工作目录)
  • VS F5 调试时工作目录 ≠ exe 目录,导致保存和加载去了不同位置
  • 解决方案:统一使用AppDomain.CurrentDomain.BaseDirectory拼接路径

坑 3:限值默认值为 0

  • float类型默认值为 0
  • 如果用户没设置限值就去 Apply/Confirm,验证v > 0永远不成立
  • 建议:限值校验只在限值 > 0 时才生效,或引导用户先配置限值

坑 4:ValidateAxisParam验证限值的前提

  • 限值必须已经由用户设置并通过ExecuteAxisLimit保存
  • LoadLimitFromJson必须在构造函数调用,保证AccelUpLimit等不是 0
  • 如果限值文件不存在,所有限值 = 0,Vel/Accel/Decel 的非零值都会报超限

七、相关文件路径

文件说明
Models\AxisParam.cs限值属性定义(AccelUpLimit等 8 个)
ViewModels\MainViewModel.cs序列化/反序列化/校验逻辑
View\LimitAxesPage.xaml限值编辑页面(8 个 TextBox × 4 轴)
View\AxisParamSettingsPage.xaml绝对参数设置页面
View\ManualAdjustPage.xaml手动参数设置页面
Services\ModbusServiceBase.csModbus 读写服务
Helpers\ModbusHelper.cs浮点数大端转换
bin\Debug\net10.0-windows\LimitConfig.json限值持久化文件

八、完整调用链路

启动 App └→ MainViewModel 构造函数 ├→ LoadLimitFromJson() ← 从磁盘恢复限值 └→ 绑定所有 RelayCommand 用户点击 "Limit Axes Param" 按钮 └→ ExecuteLimitParam() ├→ LoadLimitFromJson() ← 刷新限值 └→ NavigateToPage(LimitAxesPage) ← 跳转编辑页 用户设置 VelUpLimit=1000, VelLowerLimit=100 ... 用户点击 "Axis1 Confirm" └→ ExecuteAxisLimit(0) ├→ 校验 low>0 && up>low ├→ SaveLimitsToJson() ← 序列化到文件 └→ MessageBox("已保存") 用户回到主页面,点击 "轴参数设置" → 输入 AbsoVel=500 用户点击 "Apply Settings" └→ ExecuteSettingAbso() ├→ ValidateAxisParam(i, "Abso") │ ├→ 检查 null/0 │ ├→ "AbsoVel".Contains("Vel") → 检查 500 > VelLowerLimit=100 && < VelUpLimit=1000 ✓ │ └→ 通过 └→ WriteMultipleRegisters() ← 写入 PLC

九、性能与注意事项

  1. 限值只用在上位机— 限值不会写入 PLC,只用于上位机前端校验(防止用户误操作)
  2. JSON 文件很小— 4 轴 × 8 个 float ≈ 128 字节,读写无性能问题
  3. using var doc— 尽早释放JsonDocument占用的内存(每次 Load 都要创建新实例)
  4. 不要混淆 Rel/Abso 模式ValidateAxisParamactionMode参数决定校验哪些字段
  5. 写入前停轮询— 所有导航方法都调_pollingTimer?.Stop(),防止轮询覆盖用户输入的参数
http://www.jsqmd.com/news/1091141/

相关文章:

  • 学术会议全流程实战指南:从投稿到社交的研究生进阶手册
  • Groove音乐播放器:用Python打造的跨平台音乐体验新方式
  • 26.16-26
  • Cookies 是最早的客户端存储机制,每次请求都会自动携带,适合服务器端识别用户身份或维持会话;
  • 从零构建Web漏洞扫描器:架构设计与工程实践指南
  • AMD Ryzen处理器调试完全指南:免费开源工具SMUDebugTool终极教程
  • 写论文的神助攻!全能一键生成论文工具,秒出初稿不费力
  • Python QQ机器人实战指南:5分钟构建智能消息处理系统
  • 让每个命令都能精准路由:HagiCode Preset Task 的多技能支持实战
  • 如何实现网易云音乐自动化打卡:技术方案与实战指南
  • 信息学奥赛经典算法精讲:从“冒泡排序”例题看降序排列的实现与优化
  • llamafactory sft微调坑 继续训练 ,为什么 `save_steps: 40` 没有生效,实际 100 步才保存
  • AI驱动测试:技术路径、工具链与落地实践全解析
  • 滑档了还想走师范/教育方向,征集志愿该怎么填
  • 不要把 AI 编程当许愿池:用 Karpathy 四原则搭建可验证的编码工作流
  • [AI][昇腾950]SIMT 编程
  • 为什么你开了 ChatGPT 会员却觉得不值?真正拉开差距的是使用方法
  • 终极自动化中文字幕下载方案:ChineseSubFinder完整指南
  • UdpSocket
  • C++:STL:Vector
  • 想把语雀、飞书、知识星球资料导入 ima?可以这样做
  • 解决毕业论文起步难问题:gradpaper 的全流程辅助模式太实用了
  • 计算机专业学习情况分析系统的设计与实现
  • Obsidian + Claude Code + 微信AI,我把这三个系统缝进了一个软件
  • Gliding Horse 给 Agent OS 装上双曲空间引擎与默克尔树边云同步
  • Mode-Step 网格如何拆开工作流边界
  • 将工作流引擎接入 AI 编排平台的实践
  • 大学生暑假必自学、入职直接能用的编程技巧(2026求职向)
  • 从零搭建Metasploitable2靶机:深入理解漏洞原理与安全加固实践
  • Bugzilla 实战:从零构建高效缺陷管理流程