7.23:Claude code:注册->封表

1

npm

2

node

Node.js 极简速览

1. 是什么

Node.js :基于 Chrome V8 引擎的 JavaScript 运行时

  • 以前 JS 只能在浏览器运行;Node 让 JS 脱离浏览器,直接跑在服务器/本地电脑
  • 前端工程化(vite、webpack、npm)全部依赖 Node
  • npm 是 Node.js 自带的包管理器

2. 安装

官网:https://nodejs.org/

推荐选 LTS 长期支持版(稳定)

安装完成打开终端验证:

bash 复制代码
node -v    # 查看Node版本
npm -v     # 配套npm版本

3. 基础使用

① 运行js文件

bash 复制代码
C:\Windows\System32>npm install -g @anthropic-ai /claude-code
npm error code EINVALIDTAGNAME
npm error Invalid tag name "@anthropic-ai" of package "@anthropic-ai": Tags may not have any characters that encodeURIComponent encodes.
npm error A complete log of this run can be found in: C:\Users\11400\AppData\Local\npm-cache\_logs\2026-07-23T01_29_06_898Z-debug-0.log
bash 复制代码
C:\Windows\System32>npm install -g @anthropic-ai/claude-code

added 1 package in 2h
npm warn allow-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn allow-scripts   @anthropic-ai/claude-code@2.1.218 (postinstall: node install.cjs)
npm warn allow-scripts
npm warn allow-scripts Run `npm approve-scripts --allow-scripts-pending` to review, or `npm approve-scripts <pkg>` to allow.

3

4

代码作用 ```xml ``` **WPF 附加属性**:控制 `ToolTip` 弹出前的延迟时间(单位:毫秒) - `InitialShowDelay="0"`:鼠标悬浮上去**立刻显示提示框**,取消系统默认几百毫秒的等待。

一、两种常用写法

1. 写在 Style 里(全局作用于该样式控件)

xml 复制代码
<Style TargetType="Button">
    <Setter Property="ToolTipService.InitialShowDelay" Value="0"/>
    <Setter Property="ToolTip" Value="提示文本"/>
</Style>

2. 直接写在单个控件上(局部生效)

xml 复制代码
<Button Content="测试"
        ToolTipService.InitialShowDelay="0"
        ToolTip="鼠标悬浮立即显示"/>

二、配套常用 ToolTipService 属性(实用组合)

xml 复制代码
<!-- 立即弹出 -->
ToolTipService.InitialShowDelay="0"
<!-- 提示持续显示时长(ms),不设置则鼠标移走消失 -->
ToolTipService.ShowDuration="5000"
<!-- 鼠标离开后延迟多久隐藏 -->
ToolTipService.HorizontalOffset="5"
ToolTipService.VerticalOffset="5"

三、注意坑

  1. 附加属性归属ToolTipService 是附加属性,不能写到 ToolTip 内部,要写在拥有ToolTip的控件 上;
    ❌ 错误示范:
xml 复制代码
<ToolTip>
    <ToolTipService.InitialShowDelay>0</ToolTipService.InitialShowDelay>
</ToolTip>

✅ 正确:放在 Button/TextBlock/TextBox 等宿主控件

  1. 如果使用 HandyControl / 自定义弹窗替代原生ToolTip,该属性失效,需要自己控制悬浮弹窗延迟。

  2. 值类型是毫秒,不要写小数,只能整数。

如果你需要,我可以给你一份一键全局所有控件Tooltip无延迟弹出的资源样式。

5

6

●builder.Services 就是那张登记表。

var builder = WebApplication.CreateBuilder(args);

// builder.Services ← 就是 IServiceCollection,登记表本身

builder.Services.AddSingleton(); // 在表上写东西

builder.Services.AddSingleton(); // 继续写

// ↑

// 就是 services

ConfigureServices(builder.Services) 这个方法里传的就是它:

private IServiceCollection ConfigureServices(IServiceCollection services)

// ↑

// 就是 builder.Services 传进来的

{

services.AddSingleton(); // 往表里填

services.AddSingleton();

return services; // 填完返回

}

一句话:services = builder.Services = 登记表,所有注册都往它身上写。

7

Service.AddSingleton

Thought for 2s (ctrl+o to expand)

●对,这里注册了两个单例:

services.AddSingleton(); // ① 先注册具体类

services.AddSingleton(); // ② 先注册具体类

services.AddSingleton(sp => // ③ 再注册接口 → 实际指向其中一个

{

return config.FpdType switch

{

"o" => sp.GetRequiredService(),

"Rei" => sp.GetRequiredService(),

};

});

为什么两个都注册?

因为 IFPDDriver 只暴露了一个,但容器需要能按需解析具体类:

外部注入 IFPDDriver →工厂 lambda 根据配置选一个

内部也可能需要 →GetRequiredService() 直接拿具体类

为什么是 Singleton?

FPD 是硬件设备,整个应用程序生命周期内:

  • 只有一台 FPD 探测器连着
  • 连接不能重复打开
  • 图像回调只有一个注册点
  • Init() →StartStream() →StopStream() →CloseFPD() 是状态机,不能多个实例各自操作

所以必须是全局唯一的单例,否则多个实例同时操作同一块硬件会直接崩掉。

✻ Brewed for 7s

sp?

Thought for 2s (ctrl+o to expand)

●sp 就是 IServiceProvider,DI 容器的**"服务定位器"**。

// 这里 ↓

services.AddSingleton(sp => // sp = IServiceProvider

{

return config.FpdType switch

{

"o" => sp.GetRequiredService(), // 从容器拿 FpdDriver

"Rei" => sp.GetRequiredService(), // 从容器拿 SupXDriver

};

});

工厂模式的延迟解析

容器初始化时,FpdDriver 和 SupXDriver 还没创建,只有当第一次注入 IFPDDriver 时,这个 lambda 才执行,sp 才去容器里取对应的实例。

// 注册时:不创建,只登记"怎么创建"

services.AddSingleton(sp => ...);

// 第一次解析时:lambda 执行,sp 从容器拿出 FpdDriver

var driver = app.Services.GetRequiredService();

sp 能干什么

sp.GetRequiredService() // 必须拿到,没有就抛异常

sp.GetService() // 拿不到返回 null

sp.GetRequiredService<IOptions>().Value // 拿配置

就是 .NET DI 的 IServiceProvider,和你在其他地方用的 App.ServiceProvider 是同一个东西。

✻ Baked for 7s

注入IFPDDriver

Thought for 5s, searched for 4 patterns (ctrl+o t+oexpand)

●注入链路如下:


注册(入口)

XGUI/App.xaml.cs →一行搞定所有注册:

// services.AddFpdService() 内部注册了 FpdDriver + SupXDriver + IFPDDriver

services.AddFpdService();

消费方(3 个主要注入点)

①PDModeViewModel ---生产检测,最核心的消费方

// d:/coding/x/X/XGUI/ViewModels/PDModeViewModel.cs:147

public PDModeViewModel(..., IFPDDriver fpd, ...)

{

fpdDriver = fpd;

}

②ImageControlService ---图像模式切换(CCD/FPD/LDS)

// d:/coding/x/X/XHW/HWServices/ImageControlService.cs:56

public ImageControlService(IFPDDriver fd, ICameraInterface cam, ...)

{

fpdDriver = fd;

}

③MachineCalibViewModel ---机器标定(FPD 分辨率标定)

// d:/coding/x/X/XGUI/ViewModels/MachineCalibViewModel.cs

public MachineCalibViewModel(..., IFPDDriver fpd, ...)

数据流向

IFPDDriver (FpdDriver 实现)

│ ImageReceived 事件 →short\[\] 16位灰度图

├── PDModeViewModel →生产检测取图 →ProcessJob →3D重建 + 算法

├── MachineCalibViewModel →标定取图 →计算分辨率

├── ImageControlService →切FPD模式 →实时预览

├── JobRunner →作业调度

└── ProcessJob →单FOV取图

全都是构造函数注入,容器自动解析,不需要手动 new。

✻ Cogitated for 15s

注册?

Thought for 2s (ctrl+o to expand)

●整个注册流程:

第一步:调用注册

d:/coding/x/X/XGUI/App.xaml.cs 第 176 行附近:

// App 启动时

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddFpdService(); // ←注册 FPD

// ↑

// 这就是 IServiceCollection

// 等价于 services.AddFpdService()

第二步:AddFpdService 做了什么

d:/coding/x/X/XHW/FPD/IFPDDriver.cs:

public static IServiceCollection AddFpdService(this IServiceCollection services)

{

// ①注册两个具体实现(各自是单例)

services.AddSingleton();

services.AddSingleton();

复制代码
  // ②注册接口,工厂 lambda 根据配置选一个
  return services.AddSingleton<IFPDDriver>(sp =>
  {
      return config.FpdType switch
      {
          "o"     => sp.GetRequiredService<FpdDriver>(),
          "Rei" => sp.GetRequiredService<SupXDriver>(),
      };
  });

}

第三步:容器自动注入

任何地方只要构造函数声明了 IFPDDriver,容器就自动把实例塞进去:

// 你写:

public PDModeViewModel(IFPDDriver fpd, ...)

// 容器做的事:

// 1. 读配置 config.FpdType,比如 "o"

// 2. 创建 FpdDriver 单例(首次)或拿已有的

// 3. 传给你的构造函数

三步串联

App.xaml.cs IFPDDriver.cs 消费者

────────── ────────────── ──────

services.AddFpdService() ──▶ 注册 FpdDriver 容器持有

▶ 注册 SupXDriver

▶ 注册 IFPDDriver(工厂)

复制代码
                                                             PDModeViewModel
                                  IFPDDriver ◀──构造函数注入 ── MachineCalibViewModel
                                                             ImageControlService

就是一个标准的 .NET DI:注册在启动时一次性完成,注入由容器自动处理。

✻ Sautéed for 9s

builder

Thought for 6s, searched for 2 patterns, read 1 file (ctrl+o co expand)

●完整链路:

App.xaml.cs

var builder = WebApplication.CreateBuilder(args); // ASP.NET Core 的 Host builder

ConfigureServices(builder.Services) // 往容器注册服务

├── services.AddXHardwareDevices() // ←一句话注册所有硬件

│ │

│ └── HardwareRegistration.cs ───────── 实际展开为:

│ ├── AddFpdService() →FpdDriver + SupXDriver + IFPDDriver

│ ├── AddPLCervice() →PLC

│ ├── AddCameraService() →CCD 相机

│ ├── AddTubeService() →X 光管

│ ├── AddImageControlService() →图像切换

│ ├── AddLDService() →激光测距

│ └── AddTCPService() →Socket 通信

├── services.AddSingletonNavigation<...>() →页面

├── services.AddSingletonDialog<...>() →弹窗

└── ...

WebApp = builder.Build(); // 容器构建完成

ServiceProvider = WebApp.Services; // 存到静态属性

关键点

// App.cs 第 72-80 行

var builder = WebApplication.CreateBuilder(args); // ①创建 builder

ConfigureServices(builder.Services); // ②往里注册一切

WebApp = builder.Build(); // ③构建,容器生效

ServiceProvider = WebApp!.Services; // ④全局可访问

用的是 ASP.NET Core 的 WebApplication 做 DI 容器,而非传统 WPF 的 ServiceCollection。因为需要用到 IOptionsMonitor、ILogger 等 ASP.NET 生态的功能。

✻ Churned for 18s

啥意思

Thought for 2s (ctrl+o to expand)

●简单说:


类比:餐厅后厨

var builder = WebApplication.CreateBuilder(args);

// ↑ 相当于:"我要开一家餐厅"

ConfigureServices(builder.Services);

// ↑相当于:把"厨师、食材、菜谱"都登记好

// "FPD探测器 = 昊博牌" 登记在册

// "CCD相机 = 海康牌" 登记在册

WebApp = builder.Build();

// ↑相当于:餐厅正式开业,后厨就绪

之后任何地方想用 FPD

// 不用 new,直接说"我要用 FPD"

public PDModeViewModel(IFPDDriver fpd)

{

// 容器自动把唯一的 FpdDriver 实例给你

}

就这么回事

登记 →构建 →自动获取

注册时:告诉容器"类型A →实例B"

使用时:构造函数参数写类型,容器自动塞进来

不需要 new FpdDriver(),容器帮你管理一切。这就是 依赖注入(DI)。

✻ Cooked for 6s

builder?

Thought for 1s (ctrl+o to expand)

●对,正常 WPF 桌面程序不应该用 WebApplication。

这段代码的特殊之处

var builder = WebApplication.CreateBuilder(args);

WebApplication.CreateBuilder 是 ASP.NET Core Web 应用 的启动方式。这个项目是个 WPF 桌面程序,但硬用了 Web 的 builder,原因可能是:

┌─────────────────────────────┬───────────────────────────────────────┐

│ 为什么用 │ 说明 │

├─────────────────────────────┼───────────────────────────────────────┤

│ IOptionsMonitor │ 配置热更新,修改 JSON 文件立刻生效 │

├─────────────────────────────┼───────────────────────────────────────┤

│ ILogger + Serilog │ ASP.NET 的日志管道比 WPF 原生好 │

├─────────────────────────────┼───────────────────────────────────────┤

│ AddSingleton / AddTransient │ 全套 DI,WPF 原生没有 │

├─────────────────────────────┼───────────────────────────────────────┤

│ Keyed Service │ 同接口多实现,WPF 原生不支持 │

├─────────────────────────────┼───────────────────────────────────────┤

│ IConfiguration │ 读 appsettings.json,WPF 原生要自己写 │

└─────────────────────────────┴───────────────────────────────────────┘

一句话:借 ASP.NET 的基础设施用,图个省事。

传统 WPF 做法是 new ServiceCollection(),但功能少。这个项目直接拿 Web 的 WebApplication.CreateBuilder(),底层其实也是 ServiceCollection,只是多了一层包装,附带更多开箱即用的功能。

✻ Worked for 6s

没懂

Thought for 6s (ctrl+o to expand)

●没关系,直接看代码对比就清楚了。


不用 DI(自己 new)

// 每次要用 FPD,自己创建

var fpd = new FpdDriver();

fpd.Init();

fpd.StartStream();

// 另一个地方又要用

var fpd2 = new FpdDriver(); // ←又 new 了一个!两个实例打架

问题:FPD 是硬件,只能有一个连接,new 两次就崩了。


用 DI(容器管理)

// 启动时,告诉容器:FPD 只创建一次,大家共用

services.AddSingleton<IFPDDriver, FpdDriver>();

// 以后任何地方要用,写在构造函数参数里就行

public class 生产检测(IFPDDriver fpd) // ←容器自动给你

{

// 用就行了,不用 new

}


就这么简单

不用 DI:到处 new,自己管创建、自己管销毁、自己保管不重复

用 DI: 容器替你管理,你只管在构造函数里声明要什么

builder 就是那个"容器",AddSingleton 就是往容器里注册。后面调用时容器自动给你,不用自己 new。

✻ Cooked for 11s

哦哦

Thought for 1s (ctrl+o to expand)

●对,就是这个意思 👍

还有其他想看的随时说。

✻ Brewed for 2s

所以容器的构造是:var builder=webapplication.CreateBuilder(args)

Thought for 1s (ctrl+o to expand)

●对,这一行创建的是 builder(建造器),还不是容器本身:

var builder = WebApplication.CreateBuilder(args);

// ↑ builder = 半成品,可以往里加东西

builder.Services.AddSingleton<...> // 往里注册

builder.Services.AddSingleton<...> // 继续注册

var app = builder.Build();

// ↑app = 成品容器,注册结束,可以用了

app.Services.GetRequiredService(); // 从容器拿东西

三步:

CreateBuilder() → 在 builder 里注册服务 → Build() 锁定

(盖房子打地基) (往里塞家具) (封顶入住)

理解了就没什么神秘的了。

✻ Cooked for 5s

没懂

Thought for 2s (ctrl+o to expand)

●换个方式:


就是写了个单子

var builder = WebApplication.CreateBuilder(args);

// 拿出一张空白登记表

builder.Services.AddSingleton();

// 在表上写:FpdDriver →只造一个,谁要都给同一个

builder.Services.AddSingleton<IFPDDriver, FpdDriver>();

// 在表上写:有人要 IFPDDriver →给他 FpdDriver

var app = builder.Build();

// 登记表封存,以后按表办事

以后取东西

var fpd = ServiceProvider.GetRequiredService();

// 查表 →"你要 IFPDDriver?登记表写着给 FpdDriver,而且只造一个" →给你


本质上就是登记表模式:先填表,后按表取东西。和去食堂打饭先办饭卡一个道理,办卡 = 注册,刷卡 = 获取。

✻ Crunched for 5s

哦哦

Thought for 2s (ctrl+o to expand)

●明白了就好 👍 这个搞懂了,代码里很多地方就通了。

✻ Worked for 2s

所以如果要用容器,就要先造出来webapplication.CreateBuilder(args),然后注册,譬如注册fpdDriver,可以选择全局唯一或者其他,然后呢

Thought for 2s (ctrl+o to expand)

●然后 Build → 取出来用:

// ===== 启动阶段 =====

// ① 创建 builder

var builder = WebApplication.CreateBuilder(args);

// ② 注册服务(填表)

builder.Services.AddSingleton<IFPDDriver, FpdDriver>(); // 全局唯一

builder.Services.AddTransient(); // 每次新建

builder.Services.AddSingleton(); // 全局唯一

// ③Build(封表)

var app = builder.Build();

// ④入口:手动取出第一个

var shell = app.Services.GetRequiredService();

shell.Show(); // 启动界面

// ===== 运行阶段 =====

// ⑤之后全部自动------构造函数写什么,容器给什么

public class PDModeViewModel(IFPDDriver fpd, // ←容器自动给 FpdDriver

ShellViewModel shell) // ←容器自动给

{

// fpd 直接用,不用 new

}

一句话总结:注册完→Build→第一个手动取,后面的全部自动注入。

✻ Churned for 5s

builder.services.add就是注册,注册完叫做Build

Thought for 1s (ctrl+o to expand)

●对,就是:

builder.Services.AddXXX() → 注册(填表)

builder.Build() → 封表(锁定)

简单直接。

✻ Cogitated for 2s

然后就可以用了

Thought for 2s (ctrl+o to expand)

●对。Build 之后,容器就活了,随用随取。

✻ Cogitated for 2s

譬如我想调用fpd,因为注册、封表过了,所以现在直接取这个实例,这个实例就可以

Thought for 1s (ctrl+o to expand)

●var fpd = app.Services.GetRequiredService();

// ↑ 拿到 FpdDriver 实例

fpd.Init(); // 初始化连接

fpd.StartStream(); // 开始采集

// ... 用就行了

但在 WPF 里一般不用手动 GetRequiredService,构造函数声明就够了:

public class PDModeViewModel

{

public PDModeViewModel(IFPDDriver fpd) // ←这里直接拿到

{

fpd.Init();

fpd.StartStream();

}

}

理解完全正确 ✅

✻ Brewed for 3s

没懂

Thinking for 4s, reading 1 file... (ctrl+o to expand)

Thought for 7s, read 1 file (ctrl+o to expand)

●用实际代码来看,就清楚了:

─────

App.xaml.cs 第 225 行,程序启动时:

MainWindow = ServiceProvider!.GetRequiredService();

// ↑ 手动取 ShellWindow

这一句话就触发了整条链:

手动取 ShellWindow

→容器看 ShellWindow 构造函数:需要 ShellViewModel

→容器看 ShellViewModel 构造函数:需要 IFPDDriver

→容器:查表,IFPDDriver →FpdDriver

→创建 FpdDriver(唯一的一个)

→塞给 ShellViewModel

→创建 ShellViewModel

→创建 ShellWindow

你只手动取了一个 ShellWindow,容器自动把里面所有依赖都递归解决了。

之后你写的每一个 ViewModel,都不用再手动取。 导航到 PDModeView 时,框架自动 GetRequiredService,它的构造函数里写 IFPDDriver fpd,容器自动把那个唯一的 FpdDriver 塞进去。


一句话

手动取一次入口 →其内部的所有依赖全部自动注入 →不用再管

✻ Cogitated for 12s

没懂

Thought for 3s (ctrl+o to expand)

●不聊代码了,换个角度。


你平时怎么写代码?

FpdDriver fpd = new FpdDriver(); // ① 创建

fpd.Init(); // ② 初始化

fpd.StartStream(); // ③ 使用


DI 就是不做第①步

// 不写 new,别人帮你 new 好了

public class PDModeViewModel(IFPDDriver fpd) // fpd 已经 new 好了,直接用

{

fpd.Init();

fpd.StartStream();

}


DI = 自己不 new,等别人送过来。

就这么简单。注册和 Build 只是告诉那个"别人"怎么帮你 new。

✻ Cooked for 6s

这里pfd就是封表好的,可以直接用,代表全局唯一

Thought for 2s (ctrl+o to expand)

●对,就是这个意思!

注册时:告诉容器 "FpdDriver 是全局唯一的"

封表后:每次拿到都是同一个 FpdDriver

PDModeViewModel 拿到的 FpdDriver ↖

MachineCalibViewModel 拿到的 FpdDriver → 三个指向同一个实例

JobRunner 拿到的 FpdDriver ↗

理解正确 ✅

✻ Crunched for 3s

哦哦

Thought for 2s (ctrl+o to expand)

● 👍

✻ Crunched for 2s

所以一般用构造函数去用这个封表

Thought for 3s (ctrl+o to expand)

●对,构造函数就是入口。

// 你需要什么,就在构造函数里写什么

public PDModeViewModel(IFPDDriver fpd, // 要 FPD

ICameraInterface cam, // 要 CCD

IPLCInterface plc) // 要 PLC

{

// 三个都是封表好的,进来直接用

}

容器看到构造函数有三个参数,自动查表,逐个塞进来。你只管声明"我要什么",不用管它怎么来的。

✻ Sautéed for 4s

哦哦

Thought for 1s (ctrl+o to expand)

●通了 👍

✻ Baked for 1s

所以这几个fpd,ccd,plc都是封表好

Thought for 2s (ctrl+o to expand)

●对,全部在 HardwareRegistration.cs 里一次注册好的:

services.AddFpdService() →IFPDDriver (FpdDriver 单例)

.AddPLCervice() → IPLCInterface (PLCService 单例)

.AddCameraService() →ICameraInterface (CameraService 单例)

.AddTubeService() → ITubeInterface (TubeService 单例)

.AddLDService() → ILDSInterface (LDService单例)

.AddImageControlService()

.AddTCPService();

所以构造函数里写 IFPDDriver fpd、ICameraInterface cam、IPLCInterface plc,三个都是封表好的单例,直接拿直接用。

✻ Worked for 4s

services.AddFpdService?

Thought for 2s (ctrl+o to expand)

●AddFpdService 是 AddXHardwareDevices 内部调的第一个:

// HardwareRegistration.cs

public static IServiceCollection AddXHardwareDevices(this IServiceCollection services)

{

services.AddFpdService() // ← 就是它

.AddPLCervice()

.AddCameraService()

.AddTubeService()

.AddImageControlService()

.AddLDService()

.AddTCPService();

return services;

}

而 AddFpdService 展开就是:

// IFPDDriver.cs

public static IServiceCollection AddFpdService(this IServiceCollection services)

{

services.AddSingleton(); // 注册 FpdDriver

services.AddSingleton(); // 注册 SupXDriver

services.AddSingleton(sp => // 注册 IFPDDriver →根据配置选一个

{

return config.FpdType switch

{

"o" => sp.GetRequiredService(),

"Rei" => sp.GetRequiredService(),

};

});

return services;

}

链路:

App.xaml.cs →AddXHardwareDevices() →AddFpdService() →注册 FpdDriver + IFPDDriver

一层包一层,最后都是往 services 里加东西。

✻ Worked for 5s

嗯嗯

Thought for 1s (ctrl+o to expand)

●👍

✻ Baked for 1s

所以我想用这种,必须先注册 WebApplication.create,然后继续,然后封表,然后就可以

Thought for 2s (ctrl+o to expand)

●对,三步走:

① WebApplication.CreateBuilder(args) 建表

②builder.Services.AddXXX() 填表

③ builder.Build() 封表

④构造函数里直接用 拿表

其实不用 WebApplication 也行,WPF 标准写法是:

var services = new ServiceCollection(); // ①建表

services.AddSingleton(); // ②填表

var provider = services.BuildServiceProvider(); // ③封表

这个项目用 WebApplication 纯粹是因为附带的功能多(配置热更新、日志管道等),本质是一样的:建表→填表→封表→用。

✻ Worked for 4s

builder是webApplication.CreateBuilder

Thought for 1s (ctrl+o to expand)

●对:

var builder = WebApplication.CreateBuilder(args);

// ↑ builder 的类型是 WebApplicationBuilder

// 里面的 builder.Services 就是 IServiceCollection(登记表)

✻ Churned for 2s

哦哦

Thought for 2s (ctrl+o to expand)

●通了 👍

✻ Brewed for 2s

其他的怎么用的

Thought for 3s, searched for 1 pattern (ctrl+o t+oexpand)

●全部一样,都是 AddSingleton<接口, 实现>:

// 每个都在自己的文件里注册

CCD: services.AddSingleton<ICameraInterface, CameraService>(); // ICameraInterface.cs

PLC: services.AddSingleton<IPLCInterface, PlcService_new>(); // IPLCInterface.cs

Tube: services.AddSingleton<ITubeInterface, TubeService>(); // ITubeInterface.cs

LDS: services.AddSingleton<ILDSInterface, LDService>(); // ILDSInterface.cs

Image: services.AddSingleton<IImageControlService, ImageControlService>();

TCP: services.AddSingleton<ITcpServer, TcpServer>();

然后 HardwareRegistration.cs 把它们串起来:

services.AddFpdService()

.AddPLCervice()

.AddCameraService()

.AddTubeService()

.AddImageControlService()

.AddLDService()

.AddTCPService();

全部全局唯一,全部构造函数注入,用法完全一样。

✻ Baked for 7s

比如ccd

Thought for 3s (ctrl+o to expand)

●注册

d:/coding/x/X/XHW/CCD/ICameraInterface.cs:

public static IServiceCollection AddCameraService(this IServiceCollection services)

{

return services.AddSingleton<ICameraInterface, CameraService>();

}

接口

public interface ICameraInterface : IDisposable

{

Task Read(); // 拍一张

Task StartStreaming(); // 开始连续拍

Task StopStreaming(); // 停止

float GetExposureTime(); // 获取曝光

void SetExposureTime(float ExposureTime); // 设置曝光

}

注入使用

// 跟 FPD 一样,构造函数声明就行

public PDModeViewModel(ICameraInterface cam) // ←容器自动给

{

await cam.StartStreaming();

var img = await cam.Read(); // 拍照

}

和 FPD 比,CCD 更简单:只有一个实现,没有品牌选择,直接 接口 →实现。

✻ Churned for 5s

services是啥?是WebApplication.create

Thought for 1s (ctrl+o to expand)

●builder.Services 就是那张登记表。

var builder = WebApplication.CreateBuilder(args);

// builder.Services ← 就是 IServiceCollection,登记表本身

builder.Services.AddSingleton(); // 在表上写东西

builder.Services.AddSingleton(); // 继续写

// ↑

// 就是 services

ConfigureServices(builder.Services) 这个方法里传的就是它:

private IServiceCollection ConfigureServices(IServiceCollection services)

// ↑

// 就是 builder.Services 传进来的

{

services.AddSingleton(); // 往表里填

services.AddSingleton();

return services; // 填完返回

}

一句话:services = builder.Services = 登记表,所有注册都往它身上写。

✻ Baked for 4s

哦哦

Thought for 1s (ctrl+o to expand)

●通了 ✅

✻ Worked for 1s

所以这又是一种形式

Thought for 1s (ctrl+o to expand)

●对,本质上都一样,只是写法不同:

// 形式1:最简单的

services.AddSingleton<ICameraInterface, CameraService>();

// 形式2:带工厂的

services.AddSingleton(sp =>

{

return config.FpdType switch { "o" => ..., "Rei" => ... };

});

// 形式3:注册具体类

services.AddSingleton();

但都是同一件事:往 services 表上登记,接口→实现,单例。

✻ Crunched for 3s

没懂

Thought for 3s (ctrl+o to expand)

●只一种写法就够了:

services.AddSingleton<接口, 实现>();

其他都是变形,不用管。FPD 用工厂是因为要二选一(昊博/睿芯微),特殊情况而已。

✻ Brewed for 4s

哦哦

Thought for 2s (ctrl+o to expand)

●👍

✻ Sautéed for 2s

我理解的是1:先造builder=webapplicaiton.create(args),这个builder 后面要.service.add

Thought for 1s (ctrl+o to expand)

●对,三步:

  1. var builder = WebApplication.CreateBuilder(args) ←造 builder
  2. builder.Services.AddSingleton<接口, 实现>() ←往 Services 里加
  3. builder.Build() ←封表

就这三步,没别的了。

✻ Crunched for 2s

然后ccd把builder.add 放到了一个构造方法里

Thought for 2s (ctrl+o to expand)

●不是构造方法,是扩展方法:

// ICameraInterface.cs 里

public static IServiceCollection AddCameraService(this IServiceCollection services)

// ↑ 方法名 ↑ this = 扩展 IServiceCollection

{

return services.AddSingleton<ICameraInterface, CameraService>();

}

就是给 IServiceCollection 加了一个快捷方式,让外面写起来简洁:

// 没有扩展方法时,只能这样写

services.AddSingleton<ICameraInterface, CameraService>();

// 有扩展方法后,简洁一点

services.AddCameraService();

效果一样,只是一个包装。你可以理解成:给登记表预先印好了一个 CCD 的格,直接勾上就行。

✻ Cooked for 4s

没懂

Thought for 2s (ctrl+o to expand)

●不扯代码了。


就是提前印好的表格

登记表 services:

☐ FPD _________

☐ CCD _________

☐ PLC _________

☐ Tube _________

AddCameraService() 就是直接在表上找到 CCD 那一栏,填上 CameraService。

不用自己画格、不用自己写表头,一个方法搞定。


HardwareRegistration.cs 里把七个硬件全部勾一遍,表就填完了:

☑ FPD →FpdDriver

☑ CCD →CameraService

☑ PLC →PlcService_new

☑ Tube →TubeService

☑ LDS →LDService

☑ Image →ImageControlService

☑ TCP →TcpServer

然后 Build 封表,搞定。

✻ Crunched for 5s

※ recap: 我们刚才在搞懂依赖注入(DI):builder.Services.Add 注册 → Build 封表 → 构造函数里直接用。你已经理解到 CCD 的 AddCameraService 就是提前印好的表格快捷方式。要继续吗?

嗯嗯

Thought for 2s (ctrl+o to expand)

● 👍

✻ Churned for 2s

所以我封表好的,可以在整个解决方案里用

Thought for 1s (ctrl+o to expand)

●对,只要在同一个 builder.Build() 出来的容器里,整个项目任何地方都能用。

但有个前提:你得能拿到容器。

这个项目里容器存在静态属性里:

// App.xaml.cs

public static IServiceProvider? ServiceProvider { get; private set; }

所以任何地方都能:

var fpd = App.ServiceProvider.GetRequiredService();

或者更常见的,写在构造函数里让容器自动给:

// XGUI 项目、XHW 项目、任何项目,只要注册过的都能拿

public SomeClass(IFPDDriver fpd) { ... }

整个方案共享同一个容器、同一套封表。

✻ Brewed for 4s

builder.build封表

Thought for 1s (ctrl+o to expand)

●对,builder.Build() 一执行,表就封了,之后只读不写。

builder.Build() // 咔嚓,锁死

之后只能取,不能加。

✻ Crunched for 2s

没懂

Thought for 1s (ctrl+o to expand)

●---

Build 之前:表可以改

☐ FPD 待填

☐ CCD 待填

☐ PLC 待填

Build 之后:表封死

☑ FPD FpdDriver ←锁

☑ CCD CameraService ←锁

☑ PLC PlcService ←锁


Build 之前 = 还能往里加东西

Build 之后 = 表定了,只能拿出来用,不能再加新东西

没别的意思,就是一个"确认完毕、不再改动"的动作。

✻ Baked for 3s

拿的到容器

Thought for 3s (ctrl+o to expand)

●对,容器就是 ServiceProvider,全局就一个,存在 App.ServiceProvider。

// 任何地方,随时拿

var fpd = App.ServiceProvider.GetRequiredService();

就像一张饭卡,走到哪都能刷。

✻ Brewed for 4s

静态方法?

Thought for 1s (ctrl+o to expand)

●// App.xaml.cs 第 61 行

public static IServiceProvider? ServiceProvider { get; private set; }

// ↑ 静态属性,类名直接访问

不是静态方法,是静态属性。

调用时用类名直接访问,不需要 new App:

App.ServiceProvider.GetRequiredService();

// ↑类名.属性

全局就一个 App 实例,启动时赋值一次,之后整个程序共用。

✻ Churned for 3s

哦哦

Thought for 3s (ctrl+o to expand)

●通了 👍

✻ Churned for 3s

所以这个容器是关键

Thought for 2s (ctrl+o to expand)

●对,容器就是整个程序的骨架。

一切围着容器转:

硬件层 →往容器注册 ←FPD/CCD/PLC/Tube/LDS

UI层 → 往容器注册 ← ShellWindow/PDModeView/...

服务层 → 往容器注册 ← 日志/数据库/权限/...

算法层 →往容器注册 ←CalPixelSize/RigidTransformHelper/...

Build 之后,整个程序就活了

所有模块靠容器串在一起,各自独立,要什么声明什么。这就是这个项目最核心的设计思路。

✻ Sautéed for 4s

总结下:1、造builder

Thought for 1s (ctrl+o to expand)

●三步:

  1. 造 builder → var builder = WebApplication.CreateBuilder(args)
  2. 填表 → builder.Services.AddSingleton<接口, 实现>()
  3. 封表 → builder.Build()

封表后,整个程序用 App.ServiceProvider 拿容器,构造函数里声明什么拿什么。

✻ Churned for 2s

builde=webapplicaition.createBuilder

Thought for 2s (ctrl+o to expand)

●对:

var builder = WebApplication.CreateBuilder(args);

// ↑ 注意拼写:WebApplication,不是 webapplicaition

WebApplication 是 ASP.NET Core 提供的类,CreateBuilder 是静态方法。

✻ Cogitated for 3s

然后是注册 builder.services.addSingleton

Thought for 2s (ctrl+o to expand)

●对:

// 方式1:直接写

builder.Services.AddSingleton<ICameraInterface, CameraService>();

// 方式2:用包装好的扩展方法(项目里实际用的)

builder.Services.AddFpdService();

builder.Services.AddCameraService();

两种一样,你喜欢哪种都行。

✻ Churned for 3s

没懂

Thought for 2s (ctrl+o to expand)

●不绕了:

builder.Services.AddSingleton<接口, 实现>();

// ↑ ↑ ↑ ↑

// 登记表 往里加 要什么 给什么

就这一句话,没了。

✻ Sautéed for 3s

Services是容器

Thought for 1s (ctrl+o to expand)

●不完全是。

builder.Services →登记表(可写,Build 前)

builder.Build() → 容器(只读,Build 后)

Thought for 4s (ctrl+o to expand)

●不完全是:

builder.Services → IServiceCollection (登记表,Build 前)

WebApp.Services → IServiceProvider (容器,Build 后)

两个东西。前面是填的表,后面是最终容器。

builder.Services.AddSingleton(); // 往 IServiceCollection 写

var app = builder.Build(); // 变成 IServiceProvider

App.ServiceProvider = app.Services; // 存的是 IServiceProvider

✻ Cooked for 5s

没懂

Thought for 1s (ctrl+o to expand)

●不用分了,就当同一个东西。

builder.Services.AddSingleton(); // 往里加

builder.Services.AddSingleton();

// Build 之后就当它封死了

// 之后全用这个

App.ServiceProvider

反正你记住:加完 →Build →用,别管它两个名字。

✻ Cooked for 3s

App.ServiceProvider是容器

Thought for 4s (ctrl+o to expand)

●对,App.ServiceProvider 就是最终的容器。

// App.xaml.cs 里存好的

public static IServiceProvider? ServiceProvider { get; private set; }

// Build 之后赋值

ServiceProvider = WebApp!.Services;

全局就这一个,封表之后用它拿一切。

✻ Cooked for 5s

哦哦

Thought for 2s (ctrl+o to expand)

● ✅

✻ Worked for 2s

赋值?

Thought for 1s (ctrl+o to expand)

●// App.xaml.cs 第 79-80 行

WebApp = builder.Build(); // Build 封表,返回容器

ServiceProvider = WebApp!.Services; // 把容器存到静态属性

就是把 Build 出来的容器存到 ServiceProvider 变量里,方便全局访问。以后就不用记 WebApp.Services 了,直接用 App.ServiceProvider。

✻ Cooked for 3s

所以WebApp.Service容器

Thought for 2s (ctrl+o to expand)

●对:

builder.Build() →WebApp →WebApp.Services = 容器 = App.ServiceProvider

就是同一个东西,存了两份引用而已。

✻ Sautéed for 2s

不懂

Thought for 1s (ctrl+o to expand)

●不绕了,就一句话:

App.ServiceProvider // ← 这个就是容器,Build 完就有了,一行搞定

别管中间怎么来的,记住这一个就够了。

✻ Baked for 2s

webapp.Service=容器,那IserviceProvider

Thought for 1s (ctrl+o to expand)

●WebApp.Services // 类型是 IServiceProvider

App.ServiceProvider // 类型也是 IServiceProvider

// 它们是同一个东西:

WebApp.Services == App.ServiceProvider // true

IServiceProvider 是类型名,WebApp.Services 和 App.ServiceProvider 是同一个容器的两个名字。就像一个人有俩称呼,还是那个人。

✻ Brewed for 2s

oo

Thought for 17s (ctrl+o to expand)

●通了 👍

✻ Churned for 17s

为啥要存下

Thought for 3s (ctrl+o to expand)

●因为 WebApp 是 App 类的私有字段,外面拿不到:

// App.xaml.cs

public partial class App : Application

{

private WebApplication? WebApp; // ← private,外面访问不了

public static IServiceProvider? ServiceProvider { get; private set; } // ← public,全局可见

}

WebApp 藏起来,ServiceProvider 公开出去。外面直接用 App.ServiceProvider 就行,不需要知道 WebApp 是什么。

✻ Brewed for 4s

哦哦

Thought for 1s (ctrl+o to expand)

●👍

✻ Crunched for 1s

所以整个注册就是为了用容器,先是往build里面塞东西,然后

Thought for 9s (ctrl+o to expand)

●对,流程你已经清楚了:

往 builder.Services 里塞东西(注册)

builder.Build() 封表

App.ServiceProvider 容器就绪

构造函数里声明,自动获取

✻ Worked for 9s

哦哦

Thought for 2s (ctrl+o to expand)

●搞定 ✅

✻ Crunched for 2s

所以我用的时候就直接仿照

Thought for 2s (ctrl+o to expand)

●对,三步照抄:

// ① 注册(一次)

builder.Services.AddSingleton<IMyService, MyService>();

// ② 封表(一次)

var app = builder.Build();

// ③使用(随便哪里)

public class MyViewModel(IMyService myService)

{

// 直接用

}

照这个模板来,不会错的。

✻ Cogitated for 3s

哦哦哦哦

Thought for 8s (ctrl+o to expand)

●终于通了 🎉

✻ Brewed for 8s

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────>

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

⏸ manual mode on ·? for shortcuts ·←for agents

8

那我后面直接用这个fpd

Thought for 2s (ctrl+o to expand)

●public class MyViewModel(IFPDDriver fpd)

{

public async Task 开始拍图()

{

await fpd.Init(); // 初始化连接

await fpd.StartStream(true); // 开始连续采集

复制代码
      fpd.ImageReceived += (data, w, h, gray) =>
      {
          // 每帧图像来的时候触发的操作
          // data = short[] 16位灰度
      };

      await fpd.StopStream();      // 停止
      fpd.CloseFPD();              // 关闭
  }

}

就是构造函数里拿到,然后直接调它的方法。不用 new,不用管理连接,容器保证全局就一个。

✻ Cooked for 4s

哦哦

Thought for 3s (ctrl+o to expand)

●行了,整个套路都通了 👍

有具体代码想看的时候随时说。

✻ Worked for 4s

这个+=()里面

Thought for 11s (ctrl+o to expand)

●就是订阅事件 ------ 当 FPD 拍了一张图,自动通知你。

fpd.ImageReceived += (data, w, h, gray) =>

{

// 收到图了,干点什么

};

// ↑ 事件 ↑ 订阅(+=) ↑ lambda:收到的数据

拆开:

// 事件定义(在 IFPDDriver 里)

public event Action<short\[\], int, int, int> ImageReceived;

// ↑图像数据 ↑宽 ↑高 ↑灰度

// += 的意思:我订阅这个事件,"有新图了就叫我"

// => 的意思是:收到后做这个操作

// (data, w, h, gray) 都是事件带来的参数

类比

fpd 就像门铃

+= 就像你留了电话:"有人按门铃打我电话"

=> 就是电话里的内容:"开门"

┌───────────────────────┬────────────────────────────────┐

│ 代码 │ 含义 │

├───────────────────────┼────────────────────────────────┤

│ fpd.ImageReceived │ FPD 的"有图了"事件 │

├───────────────────────┼────────────────────────────────┤

│ += │ 订阅,加监听 │

├───────────────────────┼────────────────────────────────┤

│ (data, w, h, gray) => │ 收参数:图像数据、宽、高、灰度 │

├───────────────────────┼────────────────────────────────┤

│ { ... } │ 收到后执行的代码 │

└───────────────────────┴────────────────────────────────┘

✻ Cogitated for 16s

哦哦

Thought for 1s (ctrl+o to expand)

●👍 全通了。想深入看什么随时说。

✻ Cogitated for 2s

─────────────────────────────────────────────────────────────────

9

刚性变换

Thought for 3s (ctrl+o to expand)

●你之前看过的,用通俗方式再说一遍:


就是算"偏了多少"

PCB 放上去,不可能每次都完美对齐:

复制代码
      理论位置                实际位置
         │                       ╱
         │                     ╱
         │            ←→   ╱
         │                 ╱ 偏了 0.3mm,转了 2°
         │               ╱

两个 Mark 就够了

Mark1 应该在 (100, 200) 实际在 (103, 201) ←偏了

Mark2 应该在 (300, 200) 实际在 (304, 203) ←偏了

两个 Mark 的偏差 →算出旋转角 + 平移量

算出来的结果

RigidTransformResult:

RotationAngle: 2.1° // 顺时针转了 2.1°

TranslateX: 3.0mm // X 方向偏了 3mm

TranslateY: 1.5mm // Y 方向偏了 1.5mm

之后所有坐标自动修正

// 每个 FOV 坐标都要过一遍矩阵

修正后坐标 = TransformPoint(刚性变换矩阵, 原始坐标)


公式底层是 OpenCV 做的,就是根据两点算旋转+平移,高中数学难度。

相关推荐
心平气和量大福大15 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
listening7771 天前
HarmonyOS 6.1 全场景协同实战:从“单机”到“超级终端”的分布式重构
wpf
心平气和量大福大2 天前
C#-WPF-布局-Grid
c#·wpf·visual studio
心平气和量大福大3 天前
C#-WPF-Window主窗体
开发语言·c#·wpf
FuckPatience3 天前
Telerik UI for WPF 值不能为null。参数名:key
ui·wpf
lindexi3 天前
WPF 笔迹延迟优化从硬件到软件的全链路分析
wpf
weixin_727535624 天前
双Token认证体系深度拆解:Spring Security + JWT + Redis
redis·spring·wpf
liuxiaowei34 天前
Winform+WPF双框架实战:喷涂工艺SCADA上位机从0到1搭建(附采集监控源码+车间踩坑实录)
大数据·hadoop·wpf
贺国亚5 天前
模型训练-分布式与GPU调度
分布式·wpf