Files
AohDrllTools/App.xaml.cs
Mr.Xia b75439cff4 fix: 修复含特殊字符路径的钻带文件读取失败 — 移除 cmd /c type,改为 AnsiFileReader 直接文件IO
问题:路径含 ( ) & % ! ^ @ 等字符时报"文件路径包含不允许的命令行特殊字符",
(D2-f)、(1) 这类正常业务文件名直接命中。

根因:读取统一入口把路径拼进命令行(cmd.exe /d /c type "..."),为"防炸"在 03d9b8b
引入 ForbiddenPathChars 黑名单——并非业务需求,是实现的自我约束。
实测:真正会坏的只有 %VAR%(必坏) 与延迟展开开启时的 !VAR!,而 ( ) & ^ @ 空格在
双引号内均正常;且 type 是字节透传,这层 cmd 对当初的编码问题毫无帮助。

改动:
- 新增 AnsiFileReader.cs:FileStream + StreamReader(默认 936 解码 + BOM 探测,
  与旧 StreamReader 解码路径逐字符等价),不经命令行
- 删除 CommandTypeFileReader.cs 及其 ForbiddenPathChars 字符校验
- 路径只保留"非空"与"文件存在"校验;IO 异常包装为
  InvalidOperationException("读取文件失败:{路径}({原因})")并保留 InnerException
- 15 处调用点同步改名;更新 CLAUDE.md 与 Docs 说明

验证:14 个 demo_drl 样例新旧实现字符串/行数逐字符一致;含特殊字符路径(含 %TEMP% 字面
路径,旧实现 rc=1)可读可解析;UTF-8/UTF-16 BOM 结果与旧实现一致;
dotnet build -p:Platform=x86 0 错误且警告集与基线逐条相同。
2026-09-20 11:30:04 +08:00

442 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Collections.ObjectModel;
namespace DrillTools
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : System.Windows.Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
RegisterShellContextMenu();
string? filePath = null;
if (e.Args.Length > 0)
{
filePath = e.Args[0];
if (!IsValidDrillTapeFile(filePath))
filePath = null;
}
if (filePath == null)
{
ShowMainWindow(null);
return;
}
// 防止选择窗口关闭后触发 OnLastWindowClose 导致应用退出
ShutdownMode = ShutdownMode.OnExplicitShutdown;
// 只读取和解析文件一次,提取所有需要的信息
string content = AnsiFileReader.ReadAllText(filePath);
var viewModel = new MainWindowViewModel
{
IsStartupDrillTapeFile = true,
OriginalFilePath = filePath,
ShouldCheckSortFileOnLoad = false
};
viewModel.LoadToolsFromDrillTape(content);
bool canClearParameters = DrillTapeParameterCleaner.CanClearParameters(content);
bool canGeneratePpDrillTape = viewModel.CanGeneratePpDrillTape;
string extension = Path.GetExtension(filePath).ToLowerInvariant();
bool canScaleDrillTape = extension == ".dr2" || extension == ".dpin";
bool isRouFile = extension == ".rou" || extension == ".rou1" || extension == ".rou2" || extension == ".roux";
bool isDrlFile = extension == ".drl";
double minDrill = viewModel.MinDrillDiameter;
double minSlot = viewModel.MinSlotDiameter;
double minEA = viewModel.MinEADiameter;
bool isPpDrillTape = viewModel.IsPpDrillTape;
double ppXSpacing = viewModel.PpXSpacing;
double ppYSpacing = viewModel.PpYSpacing;
bool hasOuter3175Spacing = viewModel.HasOuter3175Spacing;
double outer3175XSpacing = viewModel.Outer3175XSpacing;
double outer3175YSpacing = viewModel.Outer3175YSpacing;
var selectionViewModel = new StartupSelectionViewModel
{
FileName = Path.GetFileNameWithoutExtension(filePath),
MinDrillDiameter = minDrill,
MinSlotDiameter = minSlot,
MinEADiameter = minEA,
IsPpDrillTape = isPpDrillTape,
PpXSpacing = ppXSpacing,
PpYSpacing = ppYSpacing,
HasOuter3175Spacing = hasOuter3175Spacing,
Outer3175XSpacing = outer3175XSpacing,
Outer3175YSpacing = outer3175YSpacing
};
// 叠板计算用的最小刀径独立遍历 Tools 计算,包含 0.749mm
CalculateStackMinDiameters(viewModel.Tools, selectionViewModel);
var selectionWindow = new StartupSelectionWindow(
selectionViewModel,
canClearParameters,
canGeneratePpDrillTape,
canScaleDrillTape,
canReplaceRouParameters: isRouFile,
canReplaceDrlParameters: isDrlFile);
selectionWindow.ShowDialog();
switch (selectionWindow.SelectedAction)
{
case StartupAction.AdjustToolOrder:
ShutdownMode = ShutdownMode.OnLastWindowClose;
ShowMainWindow(filePath);
break;
case StartupAction.ExportHoleCount:
PerformHeadlessExport(filePath);
Shutdown();
break;
case StartupAction.ClearParameters:
PerformParameterCleanup(filePath);
Shutdown();
break;
case StartupAction.GeneratePpDrillTape:
PerformPpDrillTapeGeneration(filePath);
Shutdown();
break;
case StartupAction.ScaleDrillTape:
PerformDrillScaling(filePath);
Shutdown();
break;
case StartupAction.ReplaceRouParameters:
PerformRouParameterReplace(filePath);
Shutdown();
break;
case StartupAction.ReplaceDrlParameters:
PerformDrlParameterReplace(filePath);
Shutdown();
break;
default:
Shutdown();
break;
}
}
private static void ShowMainWindow(string? filePath)
{
try
{
MainWindow mainWindow = new MainWindow(filePath);
mainWindow.Show();
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"创建MainWindow时发生异常:\n{ex.GetType().Name} - {ex.Message}\n\n{ex.StackTrace}",
"启动失败", MessageBoxButton.OK, MessageBoxImage.Error);
throw;
}
}
private static void PerformHeadlessExport(string filePath)
{
try
{
var viewModel = new MainWindowViewModel
{
ShouldCheckSortFileOnLoad = false
};
string content = AnsiFileReader.ReadAllText(filePath);
viewModel.OriginalFilePath = filePath;
viewModel.LoadToolsFromDrillTape(content);
if (viewModel.Tools.Count == 0)
{
System.Windows.MessageBox.Show("钻带文件中未找到有效的刀具数据,无法导出孔数报表。",
"提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
viewModel.ExportDrillUsageReport();
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"导出孔数报表失败:\n{ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private static void PerformPpDrillTapeGeneration(string filePath)
{
try
{
var viewModel = new MainWindowViewModel
{
IsStartupDrillTapeFile = true,
OriginalFilePath = filePath,
ShouldCheckSortFileOnLoad = false
};
string content = AnsiFileReader.ReadAllText(filePath);
viewModel.LoadToolsFromDrillTape(content);
string outputFilePath = viewModel.GeneratePpDrillTape();
System.Windows.MessageBox.Show(
$"PP钻带已生成:\n{outputFilePath}",
"生成PP钻带完成",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"生成PP钻带失败:\n{ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private static void PerformParameterCleanup(string filePath)
{
try
{
DrillTapeParameterCleaner.ClearParametersAndSave(filePath);
System.Windows.MessageBox.Show(
$"参数已清空,原文件已备份为:\n{Path.GetFileName(filePath)}.bak",
"清空参数完成",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"清空参数失败:\n{ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private static void PerformDrillScaling(string filePath)
{
try
{
var scalingWindow = new DrillScalingWindow();
scalingWindow.ShowDialog();
if (scalingWindow.ScalingResult == null)
return;
var result = scalingWindow.ScalingResult.Value;
string content = AnsiFileReader.ReadAllText(filePath);
string scaledContent = Integration.ExcellonScaler.Scale(content, result.Kx, result.Ky);
// 在源文件所在目录下新建子文件夹 dr2 x{InputX} y{InputY}
string sourceDir = Path.GetDirectoryName(filePath)!;
string outputDir = Path.Combine(sourceDir, $"dr2 x{result.InputX} y{result.InputY}");
Directory.CreateDirectory(outputDir);
// 将涨缩后的钻带写入子文件夹,文件名不变
string outputFilePath = Path.Combine(outputDir, Path.GetFileName(filePath));
using var writer = new StreamWriter(outputFilePath, false, Encoding.GetEncoding(936));
writer.Write(scaledContent);
// 系数记忆:紧跟钻带写回立即落盘,不滞后到完成提示之后。
// 属辅助功能,失败不回滚已完成的涨缩,失败原因随完成提示一并告知。
string? memoryWarning = null;
try
{
ScalingProfileStore.Save(result.Mode, result.InputX, result.InputY, Path.GetFileName(filePath));
}
catch (Exception ex)
{
memoryWarning = $"\n(系数记忆保存失败:{ex.Message})";
}
string completionMessage = $"涨缩完成,输出至:\n{outputFilePath}";
if (memoryWarning != null)
completionMessage += memoryWarning;
System.Windows.MessageBox.Show(
completionMessage,
"涨缩完成",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"涨缩失败:\n{ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private static void RegisterShellContextMenu()
{
try
{
string exePath = Process.GetCurrentProcess().MainModule!.FileName!;
string[] extensions = { ".drl", ".dr2", ".dr3", ".trg", ".dpin", ".txt", ".rou", ".rou1", ".rou2", ".roux" };
string menuText = "用DrillTools打开";
foreach (string ext in extensions)
{
string keyPath = $@"SystemFileAssociations\{ext}\shell\{menuText}\command";
using var key = Microsoft.Win32.Registry.ClassesRoot.CreateSubKey(keyPath);
key.SetValue("", $"\"{exePath}\" \"%1\"");
}
}
catch
{
// 非管理员权限时注册可能失败,静默忽略不影响主功能
}
}
/// <summary>
/// 验证是否为有效的钻带文件
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns>是否为有效钻带文件</returns>
private static bool IsValidDrillTapeFile(string filePath)
{
if (string.IsNullOrEmpty(filePath))
return false;
if (!File.Exists(filePath))
return false;
// 检查文件扩展名是否为支持的钻带文件格式
string extension = Path.GetExtension(filePath).ToLowerInvariant();
string[] supportedExtensions = { ".txt", ".drl", ".dr2", ".dr3", ".trg", ".dpin", ".rou", ".rou1", ".rou2", ".roux" };
return supportedExtensions.Contains(extension);
}
private static void PerformRouParameterReplace(string filePath)
{
try
{
#if DEBUG
string paramDir = @"C:\Users\14244\source\repos\Aoh\DrillTools\ROU-canshu\";
#else
string paramDir = @"\\aht-file-1\AHT-workdatatemp\泰国澳弘的相关说明资料及作业指引\钻锣带\成型\";
#endif
if (!Directory.Exists(paramDir))
{
System.Windows.MessageBox.Show(
$"参数文件目录不存在:\n{paramDir}",
"错误",
MessageBoxButton.OK,
MessageBoxImage.Error);
return;
}
var selectionWindow = new ParameterFileSelectionWindow(paramDir);
selectionWindow.ShowDialog();
if (selectionWindow.SelectedFilePath == null)
return;
RouParameterReplacer.ReplaceParameters(filePath, selectionWindow.SelectedFilePath, out var unmatchedDiameters);
string message = $"参数替换完成:\n{filePath}\n原文件已备份为:\n{Path.GetFileName(filePath)}.bak";
if (unmatchedDiameters.Count > 0)
{
var diaStrings = unmatchedDiameters.Select(d => d.ToString("F3", CultureInfo.InvariantCulture)).ToList();
message += $"\n\n⚠ 以下直径未找到匹配参数,已保留原行:\n{string.Join(", ", diaStrings)}";
}
System.Windows.MessageBox.Show(
message,
"参数替换完成",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
System.Windows.MessageBox.Show(
$"参数替换失败:\n{ex.Message}",
"错误",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
private static void PerformDrlParameterReplace(string filePath)
{
try
{
#if DEBUG
string paramDir = @"C:\Users\14244\source\repos\Aoh\DrillTools\DRL-canshu";
#else
string paramDir = @"\\ah-file-2\workdata\Drill\DRILLING\etc";
#endif
if (!Directory.Exists(paramDir))
{
System.Windows.MessageBox.Show(
$"参数文件目录不存在:\n{paramDir}",
"错误",
MessageBoxButton.OK,
MessageBoxImage.Error);
return;
}
var selectionWindow = new DrlParameterSelectionWindow(paramDir);
selectionWindow.ShowDialog();
if (selectionWindow.SelectedFilePath == null)
return;
DrlParameterReplacer.ReplaceParameters(filePath, selectionWindow.SelectedFilePath, out var unmatchedDiameters);
string message = $"参数替换完成:\n{filePath}\n原文件已备份为:\n{Path.GetFileName(filePath)}.bak";
if (unmatchedDiameters.Count > 0)
{
var diaStrings = unmatchedDiameters.Select(d => d.ToString("F3", CultureInfo.InvariantCulture)).ToList();
message += $"\n\n⚠ 以下直径未在参数文件中找到匹配,已保留原行:\n{string.Join(", ", diaStrings)}";
}
System.Windows.MessageBox.Show(
message,
"参数替换完成",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
System.Windows.MessageBox.Show(
$"参数替换失败:\n{ex.Message}",
"错误",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
/// <summary>
/// 从 MainWindowViewModel.Tools 独立遍历计算叠板用最小刀径。
/// 分类逻辑与 UpdateMinDiameterInfo 一致(按尾号使用标准),不排除 0.749mm;
/// 特殊固定孔径(3.203 等)不参与叠板计算。
/// </summary>
private static void CalculateStackMinDiameters(
ObservableCollection<ToolItem> tools,
StartupSelectionViewModel selectionViewModel)
{
var (minDrill, minSlot, minEA) = ToolItem.CalculateMinDiameters(tools, exclude749ForDisplay: false);
selectionViewModel.StackCalcMinDrill = minDrill;
selectionViewModel.StackCalcMinSlot = minSlot;
selectionViewModel.StackCalcMinEA = minEA;
}
}
}