#0052 补充依赖
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# RaySourceOperateView 集成技术路线
|
||||
|
||||
## 整体架构
|
||||
|
||||
采用 **DLL 直接引用 + Prism DI 容器手动注册 + AutoWireViewModel 自动装配** 的集成方式。
|
||||
|
||||
DLL 提供完整的 MVVM 三层(View / ViewModel / Service),主项目负责 DI 注册和 XAML 布局嵌入,数据通过注入的服务接口和 Prism EventAggregator 在两侧流动。
|
||||
|
||||
---
|
||||
|
||||
## 分层说明
|
||||
|
||||
### 1. DLL 引用层
|
||||
|
||||
`XP.Hardware.RaySource.dll` 放置于 `Libs/Hardware/` 目录,通过 `.csproj` 的 `<Reference HintPath>` 引用。
|
||||
|
||||
DLL 内部包含:
|
||||
|
||||
| 类型 | 名称 | 说明 |
|
||||
|------|------|------|
|
||||
| UserControl | `RaySourceOperateView` | 射线源操作界面 |
|
||||
| ViewModel | `RaySourceOperateViewModel` | 对应 ViewModel |
|
||||
| 服务接口/实现 | `IRaySourceService` / `RaySourceService` | 射线源业务逻辑 |
|
||||
| 工厂 | `IRaySourceFactory` / `RaySourceFactory` | 策略工厂,支持 Comet225/320、Spellman225 |
|
||||
| 服务 | `IFilamentLifetimeService` | 灯丝寿命管理 |
|
||||
| 配置模型 | `RaySourceConfig` | 从 App.config 加载的配置 |
|
||||
|
||||
---
|
||||
|
||||
### 2. DI 注册层(App.xaml.cs → AppBootstrapper)
|
||||
|
||||
主项目在 `RegisterTypes()` 中**手动注册** DLL 内所有服务,未走 Prism 的 `ConfigureModuleCatalog` 自动模块加载,目的是避免模块加载顺序问题,确保 DryIoc 容器在 Shell 创建前已具备所有依赖。
|
||||
|
||||
```csharp
|
||||
// 注册 ViewModel(供 ViewModelLocator 自动装配)
|
||||
containerRegistry.Register<XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel>();
|
||||
|
||||
// 注册配置(从 App.config 读取 RaySource:xxx 键值)
|
||||
var raySourceConfig = XP.Hardware.RaySource.Config.ConfigLoader.LoadConfig();
|
||||
containerRegistry.RegisterInstance(raySourceConfig);
|
||||
|
||||
// 注册核心服务(全部单例)
|
||||
containerRegistry.RegisterSingleton<IRaySourceFactory, RaySourceFactory>();
|
||||
containerRegistry.RegisterSingleton<IRaySourceService, RaySourceService>();
|
||||
containerRegistry.RegisterSingleton<IFilamentLifetimeService, FilamentLifetimeService>();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. XAML 嵌入层(MainWindow.xaml)
|
||||
|
||||
通过 XML 命名空间直接引用 DLL 中的 View:
|
||||
|
||||
```xml
|
||||
xmlns:views1="clr-namespace:XP.Hardware.RaySource.Views;assembly=XP.Hardware.RaySource"
|
||||
```
|
||||
|
||||
在主窗口右侧面板顶部(Grid.Row="0",固定高度 250px)放置控件:
|
||||
|
||||
```xml
|
||||
<views1:RaySourceOperateView Grid.Row="0" Grid.ColumnSpan="2" />
|
||||
```
|
||||
|
||||
控件内部已设置 `prism:ViewModelLocator.AutoWireViewModel="True"`,Prism 按命名约定自动从 DI 容器解析 `RaySourceOperateViewModel` 并绑定为 DataContext。
|
||||
|
||||
---
|
||||
|
||||
### 4. 数据传递路线
|
||||
|
||||
数据流分四条路径:
|
||||
|
||||
**路径 A:配置数据(启动时,单向下行)**
|
||||
|
||||
```
|
||||
App.config (RaySource:xxx 键值)
|
||||
→ ConfigLoader.LoadConfig()
|
||||
→ RaySourceConfig 实例
|
||||
→ 注入到 RaySourceService / RaySourceOperateViewModel
|
||||
```
|
||||
|
||||
App.config 中的关键配置项:
|
||||
|
||||
```xml
|
||||
<add key="RaySource:PlcIpAddress" value="192.168.1.100" />
|
||||
<add key="RaySource:MinVoltage" value="20" />
|
||||
<add key="RaySource:MaxVoltage" value="225" />
|
||||
<add key="RaySource:StatusPollingInterval" value="500" />
|
||||
<add key="RaySource:EnableAutoStatusMonitoring" value="true" />
|
||||
```
|
||||
|
||||
**路径 B:用户操作(UI → DLL 服务层)**
|
||||
|
||||
```
|
||||
RaySourceOperateView(按钮点击)
|
||||
→ RaySourceOperateViewModel(Command 绑定)
|
||||
→ IRaySourceService.SetVoltageAsync() / TurnOnAsync() / ...
|
||||
→ IXRaySource(具体策略实现,如 Comet225)
|
||||
→ 硬件通讯(B&R PVI / BR.AN.PviServices.dll)
|
||||
```
|
||||
|
||||
**路径 C:状态回传(DLL 服务层 → UI)**
|
||||
|
||||
```
|
||||
硬件状态轮询(StatusPollingInterval = 500ms)
|
||||
→ RaySourceService 内部更新
|
||||
→ RaySourceOperateViewModel 属性变更(INotifyPropertyChanged)
|
||||
→ RaySourceOperateView 数据绑定自动刷新
|
||||
|
||||
同时:
|
||||
→ AppStateService 订阅 IRaySourceService 事件
|
||||
→ 更新 RaySourceState(IsOn, Voltage, Power)
|
||||
→ Dispatcher.BeginInvoke 调度到 UI 线程
|
||||
→ 其他 ViewModel 通过 IAppStateService 读取全局射线源状态
|
||||
```
|
||||
|
||||
`RaySourceState` 为不可变 record,定义于 `Models/StateModels.cs`:
|
||||
|
||||
```csharp
|
||||
public record RaySourceState(
|
||||
bool IsOn, // 开关状态
|
||||
double Voltage, // 电压 (kV)
|
||||
double Power // 功率 (W)
|
||||
)
|
||||
{
|
||||
public static readonly RaySourceState Default = new(false, 0, 0);
|
||||
}
|
||||
```
|
||||
|
||||
**路径 D:跨模块事件通讯(Prism EventAggregator)**
|
||||
|
||||
```
|
||||
DLL 内部发布事件(XP.Hardware.RaySource.Abstractions.Events):
|
||||
XrayStateChangedEvent — 射线开关状态变化
|
||||
StatusUpdatedEvent — 实时电压/电流数据
|
||||
ErrorOccurredEvent — 错误通知
|
||||
OperationResultEvent — 操作结果回调
|
||||
|
||||
主项目任意 ViewModel 订阅示例:
|
||||
_eventAggregator.GetEvent<StatusUpdatedEvent>().Subscribe(data => { ... });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 完整依赖关系
|
||||
|
||||
```
|
||||
RaySourceOperateView(DLL 中的 UserControl)
|
||||
└─ AutoWire → RaySourceOperateViewModel(DLL,主项目注册到 DI)
|
||||
├─ IRaySourceService ← 单例,DLL 实现
|
||||
├─ RaySourceConfig ← App.config 加载
|
||||
├─ IFilamentLifetimeService ← 单例,DLL 实现
|
||||
├─ IEventAggregator ← Prism 内置
|
||||
├─ ILoggerService ← 主项目 LoggerServiceAdapter 适配 Serilog
|
||||
└─ ILocalizationService ← 主项目注册,DLL 消费
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 资源释放
|
||||
|
||||
`App.OnExit()` 中显式从容器解析并释放资源:
|
||||
|
||||
```csharp
|
||||
var appStateService = bootstrapper.Container.Resolve<IAppStateService>();
|
||||
appStateService?.Dispose();
|
||||
|
||||
var raySourceService = bootstrapper.Container.Resolve<IRaySourceService>();
|
||||
raySourceService?.Dispose();
|
||||
```
|
||||
|
||||
确保硬件连接在应用退出时正确断开。
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using Prism.Mvvm;
|
||||
@@ -272,11 +276,20 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
try
|
||||
{
|
||||
// 使用程序名称作为文件名 | Use program name as file name
|
||||
var filePath = $"{_currentProgram.Name}.xp";
|
||||
await _cncProgramService.SaveAsync(_currentProgram, filePath);
|
||||
var dlg = new SaveFileDialog
|
||||
{
|
||||
Title = "保存 CNC 程序",
|
||||
Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*",
|
||||
DefaultExt = ".xp",
|
||||
FileName = _currentProgram.Name
|
||||
};
|
||||
|
||||
if (dlg.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
|
||||
IsModified = false;
|
||||
_logger.Info("程序已保存 | Program saved: {FilePath}", filePath);
|
||||
_logger.Info("程序已保存 | Program saved: {FilePath}", dlg.FileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -291,9 +304,17 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
try
|
||||
{
|
||||
// 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path
|
||||
var filePath = $"{ProgramName}.xp";
|
||||
_currentProgram = await _cncProgramService.LoadAsync(filePath);
|
||||
var dlg = new OpenFileDialog
|
||||
{
|
||||
Title = "加载 CNC 程序",
|
||||
Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*",
|
||||
DefaultExt = ".xp"
|
||||
};
|
||||
|
||||
if (dlg.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
_currentProgram = await _cncProgramService.LoadAsync(dlg.FileName);
|
||||
ProgramName = _currentProgram.Name;
|
||||
IsModified = false;
|
||||
RefreshNodes();
|
||||
@@ -319,12 +340,82 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出 CSV(占位实现)| Export CSV (placeholder)
|
||||
/// 导出当前程序为 CSV 文件 | Export current program to CSV file
|
||||
/// </summary>
|
||||
private void ExecuteExportCsv()
|
||||
{
|
||||
// TODO: 实现 CSV 导出功能 | Implement CSV export functionality
|
||||
_logger.Info("CSV 导出功能尚未实现 | CSV export not yet implemented");
|
||||
if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
|
||||
{
|
||||
_logger.Warn("无法导出 CSV:当前无程序或节点为空 | Cannot export CSV: no program or empty nodes");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var dlg = new SaveFileDialog
|
||||
{
|
||||
Title = "导出 CSV",
|
||||
Filter = "CSV 文件 (*.csv)|*.csv|所有文件 (*.*)|*.*",
|
||||
DefaultExt = ".csv",
|
||||
FileName = _currentProgram.Name
|
||||
};
|
||||
|
||||
if (dlg.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
// CSV 表头 | CSV header
|
||||
sb.AppendLine("Index,NodeType,Name,XM,YM,ZT,ZD,TiltD,Dist,Voltage_kV,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline");
|
||||
|
||||
var inv = CultureInfo.InvariantCulture;
|
||||
|
||||
foreach (var node in _currentProgram.Nodes)
|
||||
{
|
||||
var row = node switch
|
||||
{
|
||||
ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.XM.ToString(inv)},{rp.YM.ToString(inv)},{rp.ZT.ToString(inv)},{rp.ZD.ToString(inv)},{rp.TiltD.ToString(inv)},{rp.Dist.ToString(inv)},,,,,,,,,,,,,,",
|
||||
|
||||
SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.XM.ToString(inv)},{sni.MotionState.YM.ToString(inv)},{sni.MotionState.ZT.ToString(inv)},{sni.MotionState.ZD.ToString(inv)},{sni.MotionState.TiltD.ToString(inv)},{sni.MotionState.Dist.ToString(inv)},{sni.RaySourceState.Voltage.ToString(inv)},{sni.RaySourceState.Power.ToString(inv)},{sni.RaySourceState.IsOn},{sni.DetectorState.IsConnected},{sni.DetectorState.FrameRate.ToString(inv)},{Esc(sni.DetectorState.Resolution)},{Esc(sni.ImageFileName)},,,,,,",
|
||||
|
||||
SaveNodeNode sn => $"{sn.Index},{sn.NodeType},{Esc(sn.Name)},{sn.MotionState.XM.ToString(inv)},{sn.MotionState.YM.ToString(inv)},{sn.MotionState.ZT.ToString(inv)},{sn.MotionState.ZD.ToString(inv)},{sn.MotionState.TiltD.ToString(inv)},{sn.MotionState.Dist.ToString(inv)},{sn.RaySourceState.Voltage.ToString(inv)},{sn.RaySourceState.Power.ToString(inv)},{sn.RaySourceState.IsOn},{sn.DetectorState.IsConnected},{sn.DetectorState.FrameRate.ToString(inv)},{Esc(sn.DetectorState.Resolution)},,,,,,,",
|
||||
|
||||
SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.XM.ToString(inv)},{sp.MotionState.YM.ToString(inv)},{sp.MotionState.ZT.ToString(inv)},{sp.MotionState.ZD.ToString(inv)},{sp.MotionState.TiltD.ToString(inv)},{sp.MotionState.Dist.ToString(inv)},,,,,,,,,,,,,,",
|
||||
|
||||
InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}",
|
||||
|
||||
InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,",
|
||||
|
||||
PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,",
|
||||
|
||||
WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},",
|
||||
|
||||
CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,",
|
||||
|
||||
_ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,"
|
||||
};
|
||||
|
||||
sb.AppendLine(row);
|
||||
}
|
||||
|
||||
File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8);
|
||||
_logger.Info("CSV 已导出 | CSV exported: {FilePath}, 节点数={Count}", dlg.FileName, _currentProgram.Nodes.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "导出 CSV 失败 | Failed to export CSV");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CSV 字段转义:含逗号、引号或换行时用双引号包裹
|
||||
/// Escape CSV field: wrap with double quotes if it contains comma, quote, or newline
|
||||
/// </summary>
|
||||
private static string Esc(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
|
||||
// ── 辅助方法 | Helper methods ───────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace XplorePlane.Views.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// 通过 Win32 API 移除 WPF 窗口标题栏图标。
|
||||
/// </summary>
|
||||
internal static class WindowIconHelper
|
||||
{
|
||||
private const int GWL_EXSTYLE = -20;
|
||||
private const int WS_EX_DLGMODALFRAME = 0x0001;
|
||||
private const int SWP_NOSIZE = 0x0001;
|
||||
private const int SWP_NOMOVE = 0x0002;
|
||||
private const int SWP_NOZORDER = 0x0004;
|
||||
private const int SWP_FRAMECHANGED = 0x0020;
|
||||
private const uint WM_SETICON = 0x0080;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowLong(IntPtr hwnd, int index);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter,
|
||||
int x, int y, int cx, int cy, uint flags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
public static void RemoveIcon(Window window)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(window).Handle;
|
||||
|
||||
// 添加 WS_EX_DLGMODALFRAME 扩展样式以移除图标
|
||||
int extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
|
||||
SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_DLGMODALFRAME);
|
||||
|
||||
// 清除大小图标
|
||||
SendMessage(hwnd, WM_SETICON, IntPtr.Zero, IntPtr.Zero);
|
||||
SendMessage(hwnd, WM_SETICON, new IntPtr(1), IntPtr.Zero);
|
||||
|
||||
// 刷新窗口非客户区
|
||||
SetWindowPos(hwnd, IntPtr.Zero, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user