6.6:Cancellation TokenSource;Messager;Timer

⏹️ _cts = new CancellationTokenSource(); 核心解析(工业上位机专用)

1. 本质定位

CancellationTokenSource = 异步任务的"停工令牌发放器" ,专门用于向EtherCAT扫描、Halcon图像采集、轴运动控制等长时间运行任务发送协作式取消信号,是工业上位机异步编程的"安全终止开关"。

2. 核心作用(上位机场景)

功能 工业场景应用 代码示例
任务取消 紧急停止EtherCAT总线扫描、中断Halcon图像分析 _cts.Cancel();
超时保护 防止轴定位/PLC通信无限阻塞 _cts.CancelAfter(5000);(5秒超时)
资源释放 配合Dispose()清理任务句柄、关闭硬件连接 _cts?.Dispose();
多任务协同 统一控制扫描+运动+图像采集三大任务同步终止 共享_cts.Token给所有任务

3. 基础用法(上位机极简模板)

csharp 复制代码
// 1. 初始化(在ViewModel构造或Start按钮事件中)
_cts = new CancellationTokenSource();
CancellationToken token = _cts.Token;

// 2. 启动后台任务(如EtherCAT扫描)
_runningScanTask = Task.Run(async () => 
{
    while (!token.IsCancellationRequested)  // 循环检查取消信号
    {
        token.ThrowIfCancellationRequested();  // 快速失败机制
        
        // 工业操作:读取轴坐标、总线状态
        var axisPos = await _ecatMaster.ReadAxisPositionAsync(token);
        
        // 切UI线程更新绑定属性
        await Application.Current.Dispatcher.InvokeAsync(() => 
        {
            AxisStatusText = $"轴位置: {axisPos}";
        });
        
        await Task.Delay(200, token);  // 200ms轮询,支持取消
    }
}, token);

4. 工业级资源释放规范(必做,防内存泄漏)

CancellationTokenSource持有内核对象,不释放会导致线程句柄泄漏、硬件资源无法回收,必须在以下时机处理:

标准Dispose实现

csharp 复制代码
private void Dispose(bool disposing)
{
    lock (_disposeLock)
    {
        if (_isDisposed) return;
        _isDisposed = true;
        
        // 1. 发送取消信号(先软停止,再硬释放)
        _cts?.Cancel();
        // 2. 等待任务优雅退出(可选,根据业务需求)
        _runningScanTask?.Wait(1000);  // 最多等1秒
        // 3. 释放令牌源(关键!)
        _cts?.Dispose();
        _cts = null;  // 置空避免重复释放
        
        // 其他资源释放...
        _statusTimer?.Dispose();
        _ecatMaster?.Dispose();
    }
}

安全重置模式(任务重启时)

csharp 复制代码
// 停止当前任务并重启
private void RestartScan()
{
    // 先清理旧令牌
    _cts?.Cancel();
    _cts?.Dispose();
    _cts = null;
    
    // 创建新令牌源并启动任务
    _cts = new CancellationTokenSource();
    StartEthercatScan(_cts.Token);
}

5. 上位机场景避坑指南

❌ 常见错误

  1. 只Cancel不Dispose:导致内核对象泄漏,长时间运行后程序崩溃
  2. 未检查取消状态 :任务无视IsCancellationRequested,无法正常终止
  3. async void中使用:异常无法捕获,取消信号可能失效
  4. 静态持有:导致对象无法被GC回收,内存泄漏

✅ 正确实践

  1. 配合using声明(局部任务)

    csharp 复制代码
    using var cts = new CancellationTokenSource();
    await RunSingleAxisMoveAsync(cts.Token);  // 自动Dispose
  2. 取消异常处理

    csharp 复制代码
    try
    {
        await _runningScanTask;
    }
    catch (OperationCanceledException ex) when (ex.CancellationToken == _cts.Token)
    {
        // 正常取消,记录日志不报错
        Log.Info("EtherCAT扫描已被用户终止");
    }
  3. 多任务联动取消(如扫描+图像采集):

    csharp 复制代码
    // 关联多个令牌源,任一取消则全部取消
    var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        _scanCts.Token, _visionCts.Token);

6. 与你的上位机代码结合点

在你的EtherCAT+Halcon项目中,_cts应:

  • 控制_runningScanTask后台扫描任务的启停
  • 配合_statusTimer实现"定时扫描+紧急停止"双保险
  • Dispose中与_ecatMaster_scanAxis等硬件资源一起释放
  • 提供UI"停止扫描"按钮的事件响应:private void OnStopButtonClick() => _cts?.Cancel();

总结

_cts = new CancellationTokenSource();是工业上位机异步任务的"安全大脑",核心价值是实现"可控、优雅、安全"的任务终止 ,避免强制终止导致的总线异常、硬件损坏和内存泄漏。记住三个关键字:创建→使用→释放,三者缺一不可。

Timer

Messenger 与注册:(工业上位机视角)

一、Messenger 是什么?(3个核心类比)

  1. 对讲机总台:不同VM/模块是不同的工人,Messenger是工地中央的对讲机总台,负责转发消息,避免工人之间互相拉扯线缆(直接引用)
  2. 工厂公告栏:一个地方贴通知(发送消息),多个部门(接收者)看通知,无需互相认识
  3. 公交车调度中心:调度中心(Messenger)统一指挥所有车辆(对象),车辆只需收听自己的线路指令

核心作用 :在MVVM架构中实现解耦通信------ViewModel之间、ViewModel与View之间无需互相引用,通过消息传递数据或指令,降低耦合度,方便维护与重构

二、什么是注册(Register)?

注册 = 订阅消息,就像:

  • 工人到总台登记:"我要听'生产线启动'的指令"
  • 部门到公告栏贴纸条:"这个通知我要"
  • 公交车到调度中心报备:"我负责3号线,只听3号线指令"

代码里的注册:告诉Messenger"我要接收XX类型的消息,收到后执行XX操作"

csharp 复制代码
// 标准注册写法(WeakReferenceMessenger)
WeakReferenceMessenger.Default.Register<VisionCommandMessage>(
    this,                  // 接收者(我)
    (recipient, message) => // 收到消息后的处理方法
    {
        // 业务逻辑:比如执行视觉检测
        ProcessVisionCommand(message.Command);
    });

三、关键操作速览(工业上位机常用)

操作 代码 类比 用途
注册 Register<TMessage>(this, 处理方法) 登记收听频道 订阅特定消息
发送 Send(new TMessage(数据)) 总台喊话 发布消息
解注册 UnregisterAll(this) 取消登记 停止接收消息,防止内存泄漏

四、为什么必须在Dispose中UnregisterAll?

想象工人下班不注销对讲机:

  1. 总台还会给他发指令(消息回调),但他已经不在岗(对象已该被回收)
  2. 占用频道资源,导致内存泄漏(GC无法回收)

修复要求 :所有注册过Messenger的类必须实现IDisposable,在Dispose中调用WeakReferenceMessenger.Default.UnregisterAll(this),彻底切断消息订阅

所有注册 Messenger 的 VM 必须实现 IDisposable,确保对称释放

释放顺序严格遵循:Cancel→Dispose CTS → Stop Timer → Halcon 资源(UI 线程)→ UnregisterAll 消息 → 标记已释放

Halcon 对象必须在 UI 线程释放,避免跨线程异常

双重释放防护:标记_disposed,防止重复调用

步骤 操作 目的 安全要点
1 CTS.Cancel() 通知所有关联任务立即停止 必须先 Cancel 再 Dispose,避免任务无法响应
2 CTS.Dispose 释放 CTS 资源 防止句柄泄漏
3 Timer.Stop() 停止定时器触发 避免 Tick 事件持续执行
4 Timer 事件解绑定 切断引用,允许 GC 回收 防止内存泄漏
5 Halcon.Close() 关闭窗口 / 句柄 UI 线程执行,避免跨线程异常
6 Halcon.Dispose() 释放 Halcon 非托管资源 配对创建与释放,防止内存泄漏
7 UnregisterAll() 取消所有消息订阅 避免消息回调导致的内存泄漏

五、WeakReferenceMessenger vs 普通事件(上位机开发重点)

对比项 WeakReferenceMessenger 普通事件 工业场景选择
引用类型 弱引用(不阻止GC回收) 强引用(易内存泄漏) 选Messenger
跨模块 无需引用,直接通信 需持有对方实例 选Messenger
解耦度 高(适合大型设备软件) 低(适合简单UI交互) 选Messenger
资源管理 需手动UnregisterAll 需手动注销事件 两者都要做

六、上位机开发中的典型应用场景

  1. 设备状态通知:PLC通信模块发送"连接成功"消息,多个VM同时更新UI状态
  2. 视觉检测结果:Halcon处理完成后发送结果消息,UI显示+数据记录+报警判断
  3. 操作权限控制:登录模块发送"权限变更"消息,所有功能模块更新按钮状态

七、极简使用步骤(直接套用)

  1. 定义消息类(传递的数据容器)

    csharp 复制代码
    public class VisionResultMessage // 视觉检测结果消息
    {
        public bool IsPass { get; }
        public double DefectArea { get; }
        public VisionResultMessage(bool isPass, double defectArea)
        {
            IsPass = isPass;
            DefectArea = defectArea;
        }
    }
  2. 接收方注册(VM构造函数中)

    csharp 复制代码
    WeakReferenceMessenger.Default.Register<VisionResultMessage>(
        this, (r, m) => UpdateVisionResult(m.IsPass, m.DefectArea));
  3. 发送方发消息(Halcon处理完成后)

    csharp 复制代码
    WeakReferenceMessenger.Default.Send(
        new VisionResultMessage(isPass: true, defectArea: 0.5));
  4. Dispose中解注册(必须)

    csharp 复制代码
    protected override void CleanupMessenger()
    {
        WeakReferenceMessenger.Default.UnregisterAll(this);
    }

总结

  • Messenger = 工业软件中的消息中转站,负责不同模块间的解耦通信
  • 注册 = 订阅消息,告诉中转站"我要接收什么消息,怎么处理"
  • 上位机开发必须:注册后在Dispose中对称解注册,防止内存泄漏
csharp 复制代码
11400@▒▒ MINGW64 ~
$ git status
fatal: not a git repository (or any of the parent directories): .git

11400@ MINGW64 ~
$ cd /d/vsprogram/WpfApp6

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   WpfApp6/ViewModel/AxisEcatVM.cs

no changes added to commit (use "git add" and/or "git commit -a")

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git checkout -b feature/cts
Switched to a new branch 'feature/cts'

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/cts)
$ git add .

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/cts)
$ git commit -m "下面给出对 AxisEcatVM 的修复补丁(完整文件),目标:统一管理 CancellationTokenSource、保证 Stop/急停能可靠生效、避免乱造 CTS 导致的竞态和资源泄露。"
[feature/cts 85db0a9] 下面给出对 AxisEcatVM 的修复补丁(完整文件),目标:统一管理 CancellationTokenSource、保证 Stop/急停能可靠生效、避免乱造 CTS 导致的竞 态和资源泄露。
 1 file changed, 104 insertions(+), 13 deletions(-)

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/cts)
$ git add .

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/cts)
$ git commit -m "下面给出对 AxisEcatVM 的修复补丁(完整文件),目标:统一管理 CancellationTokenSource、保证 Stop/急停能可靠生效、避免乱造 CTS 导致的竞态和资源泄露。"
[feature/cts f292c9f] 下面给出对 AxisEcatVM 的修复补丁(完整文件),目标:统一管理 CancellationTokenSource、保证 Stop/急停能可靠生效、避免乱造 CTS 导致的竞 态和资源泄露。
 1 file changed, 27 insertions(+), 5 deletions(-)

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/cts)
$ git add .

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/cts)
$ git commit -m "下面给出对 AxisEcatVM 的修复补丁(完整文件),目标:统一管理 CancellationTokenSource、保证 Stop/急停能可靠生效、避免乱造 CTS 导致的竞态和资源泄露。"
On branch feature/cts
nothing to commit, working tree clean

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/cts)
$ git checkout master
Switched to branch 'master'

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git merge feature/cts
Updating dacdac2..f292c9f
Fast-forward
 WpfApp6/ViewModel/AxisEcatVM.cs | 149 +++++++++++++++++++++++++++++++++++-----
 1 file changed, 131 insertions(+), 18 deletions(-)

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git grep -n "MainViewModel"||true
WpfApp6/MainWindow.xaml.cs:28:        // private readonly MainViewModel _viewModel;
WpfApp6/ViewModel/MainViewModel.cs:13:    public class MainViewModel:ViewModelBase
WpfApp6/ViewModel/MainViewModel.cs:18:        public MainViewModel(IXYScanAxis scanAxis, IEtherCATMaster ecatMaster, ILogService logService)
WpfApp6/WpfApp6.csproj:105:    <Compile Include="ViewModel\MainViewModel.cs" />

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git checkout -b archive-mainviewmodel
Switched to a new branch 'archive-mainviewmodel'

11400@ MINGW64 /d/vsprogram/WpfApp6 (archive-mainviewmodel)
$ git mv ViewModel/MainViewModel.cs archive/legacy/MainViewModel.cs
fatal: bad source, source=ViewModel/MainViewModel.cs, destination=archive/legacy/MainViewModel.cs

11400@ MINGW64 /d/vsprogram/WpfApp6 (archive-mainviewmodel)
$ git add .

11400@ MINGW64 /d/vsprogram/WpfApp6 (archive-mainviewmodel)
$ git commit -m "移动MainViewModel到Legacy 感觉没用"
[archive-mainviewmodel 199e3df] 移动MainViewModel到Legacy 感觉没用
 1 file changed, 1 deletion(-)

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (archive-mainviewmodel)
$ git mv WpfApp6/ViewModel/MainViewModel.cs Archive/legacy/MainViewModel.cs

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (archive-mainviewmodel)
$ git add .

11400@ MINGW64 /d/vsprogram/WpfApp6 (archive-mainviewmodel)
$ git commit -m "移动MainViewModel到Legacy 感觉没用"
[archive-mainviewmodel 611d6f9] 移动MainViewModel到Legacy 感觉没用
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename {WpfApp6/ViewModel => Archive/legacy}/MainViewModel.cs (100%)

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (archive-mainviewmodel)
$ git checkout master
Switched to branch 'master'

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git merge archive-mainviewmodel
Updating f292c9f..611d6f9
Fast-forward
 {WpfApp6/ViewModel => Archive/legacy}/MainViewModel.cs | 0
 WpfApp6/WpfApp6.csproj                                 | 1 -
 2 files changed, 1 deletion(-)
 rename {WpfApp6/ViewModel => Archive/legacy}/MainViewModel.cs (100%)

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git checkout -b feature/bindingbug
Switched to a new branch 'feature/bindingbug'

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/bindingbug)
$ git add .

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/bindingbug)
$ git commit -m "修复绑定问题"
[feature/bindingbug 06e017d] 修复绑定问题
 3 files changed, 18 insertions(+), 6 deletions(-)

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/bindingbug)
$ git add .

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/bindingbug)
$ git commit -m "修复绑定问题"
On branch feature/bindingbug
nothing to commit, working tree clean

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/bindingbug)
$ git checkout master
Switched to branch 'master'

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git merge feature/bingingbug
merge: feature/bingingbug - not something we can merge

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git merge feature/bindingbug
Updating 611d6f9..06e017d
Fast-forward
 WpfApp6/MainWindow.xaml          |  4 ++--
 WpfApp6/ViewModel/MainShellVM.cs | 19 ++++++++++++++++---
 WpfApp6/WpfApp6.csproj           |  1 -
 3 files changed, 18 insertions(+), 6 deletions(-)

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git checkout -b feature/bug
Switched to a new branch 'feature/bug'

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git add .

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git commit -m "出现了一些问题:textbox中输入数字 自动刷新轴位置 但是图像位置没刷新等"
[feature/bug 708bd61] 出现了一些问题:textbox中输入数字 自动刷新轴位置 但是图 像位置没刷新等
 4 files changed, 74 insertions(+), 31 deletions(-)

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git add .

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git commit -m "出现了一些问题:textbox中输入数字 自动刷新轴位置 但是图像位置没刷新等"
[feature/bug c557a9e] 出现了一些问题:textbox中输入数字 自动刷新轴位置 但是图 像位置没刷新等
 2 files changed, 6 insertions(+), 4 deletions(-)

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
        WpfApp6/MainWindow.xaml
Please commit your changes or stash them before you switch branches.
Aborting

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git add .

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git commit -m "出现了一些问题:textbox中输入数字 自动刷新轴位置 但是图像位置没刷新等"
[feature/bug 712ff36] 出现了一些问题:textbox中输入数字 自动刷新轴位置 但是图 像位置没刷新等
 1 file changed, 3 insertions(+), 3 deletions(-)

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git checkout master
Switched to branch 'master'

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git merge feature/bug
Updating 06e017d..712ff36
Fast-forward
 WpfApp6/MainWindow.xaml          |  9 +++---
 WpfApp6/Messages/AppMessages.cs  | 25 ++++++++++-------
 WpfApp6/ViewModel/AxisEcatVM.cs  | 59 +++++++++++++++++++++++++++++-----------
 WpfApp6/ViewModel/MainShellVM.cs | 20 +++++++++++---
 4 files changed, 79 insertions(+), 34 deletions(-)

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git grep -n "new CancellationTokenSource" -- "*.cs" || true
Archive/Legacy/AxisControlViewModel.cs:64:            _cts = new CancellationTokenSource();
WpfApp6/ViewModel/AxisEcatVM.cs:94:                _cts = new CancellationTokenSource();
WpfApp6/ViewModel/AxisEcatVM.cs:137:                //_cts = new CancellationTokenSource();

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git grep -n -e ".Wait(" -- ".cs" || true git grep -n -e ".Result" -- ".cs" || true

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git grep -n "Dispatcher.Invoke(" -- ".cs" || true git grep -n "Dispatcher.InvokeAsync(" -- ".cs" || true

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git grep -n "WeakReferenceMessenger.Default.Register<" -- ".cs" || true git grep -n "UnregisterAll(this)" -- ".cs" || true

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git checkout -b feature/bug
fatal: a branch named 'feature/bug' already exists

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git checkout  feature/bug
M       WpfApp6/MainWindow.xaml.cs
M       WpfApp6/Messages/AppMessages.cs
M       WpfApp6/Services/LogService.cs
M       WpfApp6/ViewModel/AxisEcatVM.cs
M       WpfApp6/ViewModel/MainShellVM.cs
M       WpfApp6/ViewModel/XrayImageVM.cs
Switched to branch 'feature/bug'

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git add .

11400@ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git commit -m "继续修复:^[[200~立即修复:统一 WpfApp6\Messages\AppMessages.cs 为不可变 DTO(Start/Stop/AxisPositionReady/UpdateTarget/Log),全项目搜索并替换 Register/Send 以使用载荷~ 把输入框改为 UpdateSourceTrigger=LostFocus 或显式 Apply 按钮;统一 DataContext 策略(例如 DataContext = this 修复:统一模式:Start 创建 CTS、将 Task 保存在 _runningScanTask;Stop 调用 _cts.Cancel() 并等 待短超时后再强制停机;最后在 finally 中 Dispose CTS。AxisEcatVM 已按该方向重构(继续沿用) 把 Dispatcher.Invoke 改为 Dispatcher.InvokeAsync;在 fire-and-forget 处追加 ContinueWith 记录异常或 await。 确保 AxisEcatVM 在到位时发送 AxisPositionReadyMessage(PosX,PosY)(带坐标),并在 XrayImageVM 使用该坐标触发采集。对运行中目标更新,用 force=true 重启扫描以保证最终到位会再次触发消息。
"
[feature/bug 9ac80b7] 继续修复:立即修复:统一 WpfApp6\Messages\AppMessages.cs 为不可变 DTO(Start/Stop/AxisPositionReady/UpdateTarget/Log),全项目搜索并替换 Register/Send 以使用载荷~ 把输入框改为 UpdateSourceTrigger=LostFocus 或显式 Apply 按钮;统一 DataContext 策略(例如 DataContext = this 修复:统一模式:Start 创建 CTS、将 Task 保存在 _runningScanTask;Stop 调用 _cts.Cancel() 并等待 短超时后再强制停机;最后在 finally 中 Dispose CTS。AxisEcatVM 已按该方向重构(继续沿用) 把 Dispatcher.Invoke 改为 Dispatcher.InvokeAsync;在 fire-and-forget 处追加 ContinueWith 记录异常或 await。 确保 AxisEcatVM 在到位时发送 AxisPositionReadyMessage(PosX,PosY)(带坐标),并在 XrayImageVM 使用该坐标触发采集。对运行中目标更新,用 force=true 重启扫描以保证最终到位会再次触发消息。
 6 files changed, 141 insertions(+), 54 deletions(-)

11400@▒▒ MINGW64 /d/vsprogram/WpfApp6 (feature/bug)
$ git checkout master
Switched to branch 'master'

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$ git merge feature/bug
Updating 712ff36..9ac80b7
Fast-forward
 WpfApp6/MainWindow.xaml.cs       | 13 ++++++-
 WpfApp6/Messages/AppMessages.cs  |  2 +-
 WpfApp6/Services/LogService.cs   | 38 ++++++++++++++++----
 WpfApp6/ViewModel/AxisEcatVM.cs  | 78 ++++++++++++++++++++++++++--------------
 WpfApp6/ViewModel/MainShellVM.cs | 20 +++++++----
 WpfApp6/ViewModel/XrayImageVM.cs | 44 ++++++++++++++++-------
 6 files changed, 141 insertions(+), 54 deletions(-)

11400@ MINGW64 /d/vsprogram/WpfApp6 (master)
$
相关推荐
czhc11400756631 天前
6.5 注入
mvvm·运控
czhc11400756635 天前
6.1EtherCAT工业架构:软主站,分布式时钟DC,PDO实时通信
视觉·运控
czhc11400756636 天前
531 扫描模式
mvvm·运控
czhc11400756638 天前
529: XYScanAxis()类
mvvm·视觉·运控
czhc11400756639 天前
528:Halcon图像控件 启动轴状态实时监控
mvvm·视觉·运控
czhc114007566310 天前
5.27 :工业设备检测模式与安全防护详解
安全·mvvm·视觉·运控
Robot_Nav1 个月前
具身智能的“大脑”与“小脑”:架构剖析与小脑运控理解
具身智能·大脑·运控·小脑
无所谓จุ๊บ9 个月前
运动控制技术:自动化与智能驱动的核心
自动化·运控