diff --git a/Doc/RAYSOURCE_VIEW_INTEGRATION_TECHNICAL.md b/XplorePlane/Doc/硬件层及UI集成技术路线.md similarity index 100% rename from Doc/RAYSOURCE_VIEW_INTEGRATION_TECHNICAL.md rename to XplorePlane/Doc/硬件层及UI集成技术路线.md diff --git a/XplorePlane/Libs/Hardware/XP.Hardware.Detector.dll b/XplorePlane/Libs/Hardware/XP.Hardware.Detector.dll new file mode 100644 index 0000000..a9c88df Binary files /dev/null and b/XplorePlane/Libs/Hardware/XP.Hardware.Detector.dll differ diff --git a/XplorePlane/Libs/Hardware/XP.Hardware.PLC.dll b/XplorePlane/Libs/Hardware/XP.Hardware.PLC.dll new file mode 100644 index 0000000..dbef651 Binary files /dev/null and b/XplorePlane/Libs/Hardware/XP.Hardware.PLC.dll differ diff --git a/XplorePlane/Libs/Hardware/XP.Hardware.RaySource.Comet.Messages.dll b/XplorePlane/Libs/Hardware/XP.Hardware.RaySource.Comet.Messages.dll new file mode 100644 index 0000000..6adcfb0 Binary files /dev/null and b/XplorePlane/Libs/Hardware/XP.Hardware.RaySource.Comet.Messages.dll differ diff --git a/XplorePlane/Libs/Hardware/XP.Hardware.RaySource_Framework.dll b/XplorePlane/Libs/Hardware/XP.Hardware.RaySource_Framework.dll new file mode 100644 index 0000000..eb1c1c6 Binary files /dev/null and b/XplorePlane/Libs/Hardware/XP.Hardware.RaySource_Framework.dll differ diff --git a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs index ae03585..ffc9073 100644 --- a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs @@ -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 } /// - /// 导出 CSV(占位实现)| Export CSV (placeholder) + /// 导出当前程序为 CSV 文件 | Export current program to CSV file /// 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"); + } + } + + /// + /// CSV 字段转义:含逗号、引号或换行时用双引号包裹 + /// Escape CSV field: wrap with double quotes if it contains comma, quote, or newline + /// + 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 ─────────────────────────────────── diff --git a/XplorePlane/Views/Cnc/WindowIconHelper.cs b/XplorePlane/Views/Cnc/WindowIconHelper.cs new file mode 100644 index 0000000..f5dbc15 --- /dev/null +++ b/XplorePlane/Views/Cnc/WindowIconHelper.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; + +namespace XplorePlane.Views.Cnc +{ + /// + /// 通过 Win32 API 移除 WPF 窗口标题栏图标。 + /// + 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); + } + } +}