1
npm
2
node
Node.js 极简速览
1. 是什么
Node.js :基于 Chrome V8 引擎的 JavaScript 运行时
- 以前 JS 只能在浏览器运行;Node 让 JS 脱离浏览器,直接跑在服务器/本地电脑
- 前端工程化(vite、webpack、npm)全部依赖 Node
- npm 是 Node.js 自带的包管理器
2. 安装
推荐选 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"
三、注意坑
- 附加属性归属 :
ToolTipService是附加属性,不能写到 ToolTip 内部,要写在拥有ToolTip的控件 上;
❌ 错误示范:
xml
<ToolTip>
<ToolTipService.InitialShowDelay>0</ToolTipService.InitialShowDelay>
</ToolTip>
✅ 正确:放在 Button/TextBlock/TextBox 等宿主控件
-
如果使用
HandyControl/ 自定义弹窗替代原生ToolTip,该属性失效,需要自己控制悬浮弹窗延迟。 -
值类型是毫秒,不要写小数,只能整数。
如果你需要,我可以给你一份一键全局所有控件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)
●对,三步:
- var builder = WebApplication.CreateBuilder(args) ←造 builder
- builder.Services.AddSingleton<接口, 实现>() ←往 Services 里加
- 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)
●三步:
- 造 builder → var builder = WebApplication.CreateBuilder(args)
- 填表 → builder.Services.AddSingleton<接口, 实现>()
- 封表 → 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 做的,就是根据两点算旋转+平移,高中数学难度。