在做运动控制相关的桌面软件时,经常遇到一个尴尬的局面:硬件还没到位,或者现场不方便反复插拔控制卡,但界面和交互流程又急需验证。等硬件到了再写界面,又容易手忙脚乱。
本文推荐这套 WPF 程序就是为了解决这个矛盾的——它把界面、业务逻辑和底层 API 拆开,既可以用模拟器跑通全部操作,也能无缝切换到真实的正运动控制卡。希望对正在做类似事情的朋友有参考价值。
项目介绍一个基于 C# + WPF 的运动控制界面工程,专门针对正运动系列控制卡(比如以太网接口的 ZMC 系列)设计。工程本身不依赖硬件也能完整运行——通过内置的模拟控制器,所有按钮、状态刷新、轴监控和运动指令都能在纯软件环境里演示。
一旦接上真实控制卡,只需要替换一行依赖注入的代码,底层就会切换到 zauxdll.dll 的真实调用。项目的核心目标是:让运动控制软件的界面开发和硬件调试解耦,同时保持代码整洁、分层清晰。
项目功能功能模块
具体操作
连接管理
输入 IP 地址和端口号,点击连接或断开按钮,连接状态在右上角实时显示
轴状态监控
0 到 5 号轴的位置、速度、使能状态、报警标志、运行状态以表格形式展示,每秒刷新一次
伺服控制
对当前选中的轴执行伺服使能或伺服断开
回零与清报警
触发轴回零流程,或清除当前轴的报警信息
点动控制
按住负向或正向点动按钮,轴按设定速度持续运动,松开即停止
定位运动
输入目标位置(绝对)或移动距离(相对),配合速度参数执行单轴运动
停止与急停
停止当前选中轴的运动,或一键急停全部轴
运行日志
每次操作和系统反馈都记录在日志列表中,方便追踪问题
项目特点分层清晰:界面(View)、视图模型(ViewModel)、业务服务(Service)、硬件 API(Hardware)四层分离,修改任意一层不影响其他层。
模拟与真实无缝切换:同一套界面既可以绑定模拟控制器,也可以绑定真实控制卡驱动,切换只需改动一行代码。
操作反馈即时:每个按钮点击后都有日志反馈,轴状态表格自动刷新,模拟模式下位置数值会动态变化,看起来和真实运动一样。
界面布局紧凑:左侧控制、右侧监控,信息密度适中,适合在调试现场或车间环境中使用。
易于扩展:底层 API 接口已经封装了正运动常用的几个函数,接入其他品牌控制卡只需要实现同一个接口即可。
项目技术技术项
说明
开发框架
.NET 6 / .NET 8(支持 WPF 的版本均可)
界面框架
WPF + XAML,采用 MVVM 设计模式
数据绑定
实现 INotifyPropertyChanged实现双向绑定命令绑定
通过 ICommand接口(RelayCommand)处理按钮交互依赖注入
构造函数注入 IMotionController,方便替换具体实现模拟控制卡
内部使用 Task.Delay和随机数模拟位置变化与速度变化真实控制卡 API
通过 DllImport调用zauxdll.dll,依赖 VC++ 2010 运行库通信协议
基于以太网的 ZBasic 指令集( ZAux_OpenEth/ZAux_Close)线程安全
使用 Dispatcher.Invoke在 UI 线程更新状态项目代码分层结构一览├── MainWindow.xaml // 界面布局├── MainWindow.xaml.cs // 界面后台代码(只做 ViewModel 初始化)├── ViewModels/│ └── MainViewModel.cs // 界面数据 + 所有按钮命令├── Services/│ ├── IMotionController.cs // 运动控制业务接口│ ├── MotionController.cs // 真实业务实现(调用底层 API)│ └── MockMotionController.cs // 模拟业务实现(不依赖硬件)├── Hardware/│ ├── IMotionCardApi.cs // 底层硬件 API 接口│ ├── SimulatedMotionCardApi.cs // 模拟硬件实现│ └── ZMotionCardApi.cs // 正运动真实硬件实现(DllImport)└── Helpers/├── RelayCommand.cs // ICommand 辅助类└── DispatcherHelper.cs // UI 线程调度辅助底层 API 接口public interface IMotionCardApi{int Open(string ip, int port);void Close;int SetAxisType(int axis, int type);int SetUnits(int axis, float units);int MoveSingle(int axis, float pos, float speed);int VmoveSingle(int axis, int direction, float speed);int CancelSingle(int axis, int mode);float GetDpos(int axis);}这个接口把正运动常用的几个函数抽象出来,上层业务代码只依赖接口,不关心具体是模拟还是真实。
模拟 API 实现(节选)public SimulatedMotionCardApi : IMotionCardApi{private float _positions = new float[6];private Random _rand = new Random;public int Open(string ip, int port) => 0; // 始终返回成功public float GetDpos(int axis){// 模拟运动中位置缓慢波动_positions[axis] += (float)(_rand.NextDouble - 0.5) * 0.2f;return _positions[axis];}public int MoveSingle(int axis, float pos, float speed){// 模拟到达目标位置_positions[axis] = pos;return 0;}// 其他方法类似,均返回成功}真实控制卡 API 调用(节选)[DllImport("zauxdll.dll", CallingConvention = CallingConvention.Cdecl)]public static extern int ZAux_OpenEth(string ip, int port, out IntPtr handle);[DllImport("zauxdll.dll", CallingConvention = CallingConvention.Cdecl)]public static extern int ZAux_Direct_GetDpos(IntPtr handle, int axis, out float pos);public ZMotionCardApi : IMotionCardApi{private IntPtr _handle;public int Open(string ip, int port){return ZAux_OpenEth(ip, port, out _handle);}public float GetDpos(int axis){ZAux_Direct_GetDpos(_handle, axis, out float pos);return pos;}}ViewModel 中的命令示例private void ExecuteMoveAbsolute{if (!IsConnected){AddLog("未连接控制卡,无法执行运动");return;}var axis = SelectedAxis?.Axis ?? 0;var result = _controller.MoveAbsolute(axis, TargetPosition, MoveSpeed);AddLog(result == 0? $"轴{axis} 绝对运动到 {TargetPosition:F3} mm": $"运动失败,错误码 {result}");}模拟与真实切换的位置在 MainWindow.xaml.cs中,只需改动这一行:
// 模拟模式(无硬件)_viewModel = new MainViewModel(new MockMotionController);// 真实模式(接正运动控制卡)_viewModel = new MainViewModel(new MotionController(new ZMotionCardApi));其他代码完全不用动,界面、命令、状态刷新全部照常工作。
项目效果程序启动后,界面整体是这样的布局:左上角是标题和连接状态指示,左侧面板从上到下依次是连接设置、轴选择与伺服控制、点动控制、定位运动与急停。右侧上半部分是轴状态表格,下半部分左边显示运行参数,右边是日志列表。

切换到真实控制卡时,只要 IP 和端口正确,所有操作与模拟模式完全一致,只是位置数据来自真实的编码器反馈,运动指令实际驱动了物理轴。实测在 ZMC432 上点动响应迅速,定位精度取决于控制卡本身的配置参数,界面层面没有额外的延迟或卡顿。
总结程序算不上多复杂,但它解决了一个很实际的痛点:硬件还没到的时候,界面和交互可以先跑起来。等硬件到了,改一行代码就能切换过去,不需要重新写界面,也不需要临时改逻辑。分层设计的好处就在于此——每一层只关心自己的事,接口定好了,里面怎么实现都是自由的。
如果大家也在做类似的运动控制上位机软件,建议一开始就把硬件依赖抽离出来,哪怕先用模拟器顶着,也比等硬件到了再匆匆忙忙凑界面要踏实得多。后续如果要扩展多轴插补、轨迹规划或者 IO 控制,在这套框架上加新功能也很方便,直接在 IMotionCardApi里扩充方法就行了。