using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using DCIT.App.Presentation; using DCIT.Core.Models; using DCIT.Core.Services; using DCIT.Core.Utilities; namespace DCIT.App.Forms { public sealed class MainForm : Form { private readonly ConfigurationService _configurationService; private readonly LogService _logService; private readonly FileScanService _fileScanService; private readonly RuleService _ruleService; private readonly AutoExtractService _autoExtractService; private readonly IDocumentProcessingService _documentProcessingService; private readonly IUserDialogService _dialogService; private readonly BatchExecutionPlanner _batchExecutionPlanner = new BatchExecutionPlanner(); private readonly BindingList _replaceItems = new BindingList(); private readonly BindingList _restoreItems = new BindingList(); private readonly BindingList _rules = new BindingList(); private readonly TabControl _tabControl = new TabControl(); private readonly TabPage _replaceTab = new TabPage("替换"); private readonly TabPage _restoreTab = new TabPage("恢复"); private readonly TabPage _settingsTab = new TabPage("设置"); private readonly TextBox _txtReplaceSource = new TextBox(); private readonly TextBox _txtReplaceOutput = new TextBox(); private readonly TextBox _txtRuleFile = new TextBox(); private readonly TextBox _txtRestoreSource = new TextBox(); private readonly TextBox _txtRestoreOutput = new TextBox(); private readonly ComboBox _cmbRestoreDiffInput = new ComboBox(); private readonly DataGridView _gridReplace = new DataGridView(); private readonly DataGridView _gridRestore = new DataGridView(); private readonly DataGridView _gridRules = new DataGridView(); private SplitContainer _settingsRuleListSplitContainer; private readonly RichTextBox _logBox = new RichTextBox(); private readonly ContextMenuStrip _logContextMenu = new ContextMenuStrip(); private readonly ContextMenuStrip _replaceGridContextMenu = new ContextMenuStrip(); private readonly ContextMenuStrip _restoreGridContextMenu = new ContextMenuStrip(); private readonly ContextMenuStrip _ruleGridContextMenu = new ContextMenuStrip(); private readonly Button _btnAutoExtract = new Button(); private readonly Button _btnStartReplace = new Button(); private readonly Button _btnStopReplace = new Button(); private readonly Button _btnStartRestore = new Button(); private readonly Button _btnStopRestore = new Button(); private readonly Button _btnBrowseReplaceSource = new Button(); private readonly Button _btnBrowseReplaceOutput = new Button(); private readonly Button _btnSelectAllReplace = new Button(); private readonly Button _btnClearReplace = new Button(); private readonly Button _btnBrowseRestoreSource = new Button(); private readonly Button _btnBrowseRestoreOutput = new Button(); private readonly Button _btnSelectAllRestore = new Button(); private readonly Button _btnClearRestore = new Button(); private readonly Button _btnUndoAutoExtract = new Button(); private readonly Button _btnSaveRules = new Button(); private readonly Button _btnOpenRules = new Button(); private readonly ProgressBar _replaceTotalProgress = new ProgressBar(); private readonly ProgressBar _replaceFileProgress = new ProgressBar(); private readonly ProgressBar _restoreTotalProgress = new ProgressBar(); private readonly ProgressBar _restoreFileProgress = new ProgressBar(); private readonly Label _lblReplaceRuntime = new Label(); private readonly Label _lblRestoreRuntime = new Label(); private readonly CheckBox _chkJsonDiffEncryption = new CheckBox(); private readonly CheckBox _chkEnableImageReplacement = new CheckBox(); private readonly CheckBox _chkUseFixedImageSeed = new CheckBox(); private readonly CheckBox _chkExtractKeywords = new CheckBox(); private readonly CheckBox _chkExtractWords = new CheckBox(); private readonly CheckBox _chkExtractSentences = new CheckBox(); private readonly CheckBox _chkKeywordFrequency = new CheckBox(); private readonly CheckBox _chkKeywordTfIdf = new CheckBox(); private readonly CheckBox _chkKeywordTextRank = new CheckBox(); private readonly CheckBox _chkChineseWords = new CheckBox(); private readonly CheckBox _chkEnglishWords = new CheckBox(); private readonly CheckBox _chkChineseSentences = new CheckBox(); private readonly CheckBox _chkEnglishSentences = new CheckBox(); private readonly CheckBox _chkExcludeEmails = new CheckBox(); private readonly NumericUpDown _numKeywordCount = new NumericUpDown(); private readonly NumericUpDown _numWordCount = new NumericUpDown(); private readonly NumericUpDown _numSentenceCount = new NumericUpDown(); private readonly NumericUpDown _numFixedImageSeed = new NumericUpDown(); private readonly NumericUpDown _numMinChineseLength = new NumericUpDown(); private readonly NumericUpDown _numMinEnglishLength = new NumericUpDown(); private readonly TextBox _txtSentenceDelimiters = new TextBox(); private AppConfig _config; private ConfigurationLoadResult _configurationLoadResult; private RuleFile _ruleFile = new RuleFile(); private List _lastAutoExtractRuleIds = new List(); private CancellationTokenSource _replaceScanCancellation; private CancellationTokenSource _restoreScanCancellation; private CancellationTokenSource _autoExtractCancellation; private CancellationTokenSource _replaceExecutionCancellation; private CancellationTokenSource _restoreExecutionCancellation; private bool _isAutoExtractRunning; private bool _isReplaceRunning; private bool _isRestoreRunning; private bool _skipConfigSaveOnClose; private bool _settingsRuleListInitialWidthApplied; private int _lastReplaceRuntimeFlushTick; private int _lastRestoreRuntimeFlushTick; private const int RuntimeUiRefreshIntervalMilliseconds = 100; public MainForm( ConfigurationService configurationService, LogService logService, FileScanService fileScanService, RuleService ruleService, AutoExtractService autoExtractService, IDocumentProcessingService documentProcessingService, IUserDialogService dialogService) { _configurationService = configurationService; _logService = logService; _fileScanService = fileScanService; _ruleService = ruleService; _autoExtractService = autoExtractService; _documentProcessingService = documentProcessingService; _dialogService = dialogService; _configurationLoadResult = _configurationService.Load(); _config = _configurationLoadResult.Config; Text = "WORD 2007 格式调整软件"; StartPosition = FormStartPosition.CenterScreen; MinimumSize = new Size(1400, 900); Width = 1600; Height = 980; Font = new Font("Microsoft YaHei UI", 9F); InitializeLayout(); BindData(); ApplyConfigToUi(); _logService.EntryWritten += LogService_EntryWritten; _logService.WriteFailed += LogService_WriteFailed; FormClosing += MainForm_FormClosing; Shown += MainForm_Shown; } private void InitializeLayout() { var splitContainer = new SplitContainer { Dock = DockStyle.Fill, Orientation = Orientation.Horizontal, SplitterDistance = 720, }; _tabControl.Dock = DockStyle.Fill; _tabControl.TabPages.Add(_replaceTab); _tabControl.TabPages.Add(_restoreTab); _tabControl.TabPages.Add(_settingsTab); splitContainer.Panel1.Controls.Add(_tabControl); _logBox.Dock = DockStyle.Fill; _logBox.ReadOnly = true; _logBox.BackColor = Color.White; _logBox.Font = new Font("Consolas", 10F); splitContainer.Panel2.Controls.Add(_logBox); Controls.Add(splitContainer); BuildReplaceTab(); BuildRestoreTabV2(); BuildSettingsTab(); ConfigureContextMenus(); } private void BuildReplaceTab() { var root = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3, }; root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); var pathPanel = new TableLayoutPanel { Dock = DockStyle.Top, ColumnCount = 6, AutoSize = true, Padding = new Padding(8), }; pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F)); _btnBrowseReplaceSource.Text = "选择原始文件夹"; _btnBrowseReplaceSource.Dock = DockStyle.Fill; _btnBrowseReplaceOutput.Text = "选择替换文件夹"; _btnBrowseReplaceOutput.Dock = DockStyle.Fill; _btnSelectAllReplace.Text = "全选"; _btnSelectAllReplace.AutoSize = true; _btnClearReplace.Text = "全不选"; _btnClearReplace.AutoSize = true; _btnAutoExtract.Text = "自动提取"; _btnStartReplace.Text = "开始替换"; _btnStopReplace.Text = "停止"; _btnStopReplace.Enabled = false; _lblReplaceRuntime.AutoSize = true; _lblReplaceRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0"; AddLabeledRow(pathPanel, 0, "原始文件夹", _txtReplaceSource, _btnBrowseReplaceSource, "替换文件夹", _txtReplaceOutput, _btnBrowseReplaceOutput); _btnBrowseReplaceSource.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtReplaceSource, RefreshReplaceScanAsync); }; _btnBrowseReplaceOutput.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtReplaceOutput, RefreshReplaceScanAsync); }; _txtReplaceSource.Leave += async delegate { await RefreshReplaceScanAsync(); }; _txtReplaceOutput.Leave += async delegate { await RefreshReplaceScanAsync(); }; _btnSelectAllReplace.Click += delegate { SetSelection(_replaceItems, true); }; _btnClearReplace.Click += delegate { SetSelection(_replaceItems, false); }; _btnAutoExtract.Click += async delegate { await RunAutoExtractAsync(); }; _btnStartReplace.Click += StartReplaceExecution_Click; _btnStopReplace.Click += StopReplaceOrAutoExtract_Click; ConfigureFileGrid(_gridReplace, isRestoreGrid: false); var gridPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8, 0, 8, 0) }; _gridReplace.Dock = DockStyle.Fill; gridPanel.Controls.Add(_gridReplace); var bottomPanel = new TableLayoutPanel { Dock = DockStyle.Top, ColumnCount = 1, AutoSize = true, Padding = new Padding(8), }; var buttonPanel = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true, FlowDirection = FlowDirection.LeftToRight, }; buttonPanel.Controls.Add(_btnSelectAllReplace); buttonPanel.Controls.Add(_btnClearReplace); buttonPanel.Controls.Add(_btnAutoExtract); buttonPanel.Controls.Add(_btnStartReplace); buttonPanel.Controls.Add(_btnStopReplace); bottomPanel.Controls.Add(buttonPanel); bottomPanel.Controls.Add(CreateProgressLine("总进度", _replaceTotalProgress)); bottomPanel.Controls.Add(CreateProgressLine("单文件进度", _replaceFileProgress)); bottomPanel.Controls.Add(_lblReplaceRuntime); root.Controls.Add(pathPanel, 0, 0); root.Controls.Add(gridPanel, 0, 1); root.Controls.Add(bottomPanel, 0, 2); _replaceTab.Controls.Add(root); } private void BuildRestoreTab() { var root = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3, }; root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); var pathPanel = new TableLayoutPanel { Dock = DockStyle.Top, ColumnCount = 6, AutoSize = true, Padding = new Padding(8), }; pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F)); var btnBrowseRestoreSource = new Button { Text = "选择替换文件夹", Dock = DockStyle.Fill }; var btnBrowseRestoreOutput = new Button { Text = "选择恢复文件夹", Dock = DockStyle.Fill }; var btnSelectAllRestore = new Button { Text = "全选", AutoSize = true }; var btnClearRestore = new Button { Text = "全不选", AutoSize = true }; _btnStartRestore.Text = "开始恢复"; _btnStopRestore.Text = "停止"; _btnStopRestore.Enabled = false; _lblRestoreRuntime.AutoSize = true; _lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0"; AddLabeledRow(pathPanel, 0, "替换文件夹", _txtRestoreSource, btnBrowseRestoreSource, "恢复文件夹", _txtRestoreOutput, btnBrowseRestoreOutput); btnBrowseRestoreSource.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreSource, RefreshRestoreScanAsync); }; btnBrowseRestoreOutput.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreOutput, RefreshRestoreScanAsync); }; _txtRestoreSource.Leave += async delegate { await RefreshRestoreScanAsync(); }; _txtRestoreOutput.Leave += async delegate { await RefreshRestoreScanAsync(); }; btnSelectAllRestore.Click += delegate { SetSelection(_restoreItems, true); }; btnClearRestore.Click += delegate { SetSelection(_restoreItems, false); }; _btnStartRestore.Click += StartRestoreExecution_Click; ConfigureFileGrid(_gridRestore, isRestoreGrid: true); var gridPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8, 0, 8, 0) }; _gridRestore.Dock = DockStyle.Fill; gridPanel.Controls.Add(_gridRestore); var bottomPanel = new TableLayoutPanel { Dock = DockStyle.Top, ColumnCount = 1, AutoSize = true, Padding = new Padding(8), }; var buttonPanel = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true, FlowDirection = FlowDirection.LeftToRight, }; buttonPanel.Controls.Add(btnSelectAllRestore); buttonPanel.Controls.Add(btnClearRestore); buttonPanel.Controls.Add(_btnStartRestore); buttonPanel.Controls.Add(_btnStopRestore); bottomPanel.Controls.Add(buttonPanel); bottomPanel.Controls.Add(CreateProgressLine("总进度", _restoreTotalProgress)); bottomPanel.Controls.Add(CreateProgressLine("单文件进度", _restoreFileProgress)); bottomPanel.Controls.Add(_lblRestoreRuntime); root.Controls.Add(pathPanel, 0, 0); root.Controls.Add(gridPanel, 0, 1); root.Controls.Add(bottomPanel, 0, 2); _restoreTab.Controls.Add(root); } private void BuildRestoreTabV2() { var root = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3, }; root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); var pathPanel = new TableLayoutPanel { Dock = DockStyle.Top, ColumnCount = 6, AutoSize = true, Padding = new Padding(8), }; pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); pathPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F)); _btnBrowseRestoreSource.Text = "选择替换文件夹"; _btnBrowseRestoreSource.Dock = DockStyle.Fill; _btnBrowseRestoreOutput.Text = "选择恢复文件夹"; _btnBrowseRestoreOutput.Dock = DockStyle.Fill; _btnSelectAllRestore.Text = "全选"; _btnSelectAllRestore.AutoSize = true; _btnClearRestore.Text = "全不选"; _btnClearRestore.AutoSize = true; _btnStartRestore.Text = "开始恢复"; _btnStopRestore.Text = "停止"; _btnStopRestore.Enabled = false; _lblRestoreRuntime.AutoSize = true; _lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0"; _cmbRestoreDiffInput.DropDownStyle = ComboBoxStyle.DropDownList; _cmbRestoreDiffInput.Items.Clear(); _cmbRestoreDiffInput.Items.Add(".diff"); _cmbRestoreDiffInput.Items.Add(".bmp"); _cmbRestoreDiffInput.SelectedIndex = 0; AddLabeledRow(pathPanel, 0, "替换文件夹", _txtRestoreSource, _btnBrowseRestoreSource, "恢复文件夹", _txtRestoreOutput, _btnBrowseRestoreOutput); pathPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); pathPanel.Controls.Add(new Label { Text = "差异文件格式", Anchor = AnchorStyles.Left, AutoSize = true }, 0, 1); pathPanel.Controls.Add(_cmbRestoreDiffInput, 1, 1); _btnBrowseRestoreSource.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreSource, RefreshRestoreScanAsync); }; _btnBrowseRestoreOutput.Click += async delegate { await BrowseFolderIntoTextBoxAsync(_txtRestoreOutput, RefreshRestoreScanAsync); }; _txtRestoreSource.Leave += async delegate { await RefreshRestoreScanAsync(); }; _txtRestoreOutput.Leave += async delegate { await RefreshRestoreScanAsync(); }; _cmbRestoreDiffInput.SelectedIndexChanged += async delegate { await RefreshRestoreScanAsync(); }; _btnSelectAllRestore.Click += delegate { SetSelection(_restoreItems, true); }; _btnClearRestore.Click += delegate { SetSelection(_restoreItems, false); }; _btnStartRestore.Click += StartRestoreExecution_Click; _btnStopRestore.Click += StopRestore_Click; ConfigureFileGrid(_gridRestore, isRestoreGrid: true); var gridPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8, 0, 8, 0) }; _gridRestore.Dock = DockStyle.Fill; gridPanel.Controls.Add(_gridRestore); var bottomPanel = new TableLayoutPanel { Dock = DockStyle.Top, ColumnCount = 1, AutoSize = true, Padding = new Padding(8), }; var buttonPanel = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true, FlowDirection = FlowDirection.LeftToRight, }; buttonPanel.Controls.Add(_btnSelectAllRestore); buttonPanel.Controls.Add(_btnClearRestore); buttonPanel.Controls.Add(_btnStartRestore); buttonPanel.Controls.Add(_btnStopRestore); bottomPanel.Controls.Add(buttonPanel); bottomPanel.Controls.Add(CreateProgressLine("总进度", _restoreTotalProgress)); bottomPanel.Controls.Add(CreateProgressLine("单文件进度", _restoreFileProgress)); bottomPanel.Controls.Add(_lblRestoreRuntime); root.Controls.Add(pathPanel, 0, 0); root.Controls.Add(gridPanel, 0, 1); root.Controls.Add(bottomPanel, 0, 2); _restoreTab.Controls.Add(root); } private void BuildSettingsTab() { var root = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3, }; root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); var topPanel = new TableLayoutPanel { Dock = DockStyle.Top, ColumnCount = 5, AutoSize = true, Padding = new Padding(8), }; topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F)); topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F)); topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110F)); topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140F)); _btnOpenRules.Text = "打开规则"; _btnSaveRules.Text = "保存规则"; _btnUndoAutoExtract.Text = "撤销自动提取"; _btnUndoAutoExtract.Enabled = false; topPanel.Controls.Add(new Label { Text = "规则文件", Anchor = AnchorStyles.Left, AutoSize = true }, 0, 0); _txtRuleFile.Dock = DockStyle.Fill; topPanel.Controls.Add(_txtRuleFile, 1, 0); topPanel.Controls.Add(_btnOpenRules, 2, 0); topPanel.Controls.Add(_btnSaveRules, 3, 0); topPanel.Controls.Add(_btnUndoAutoExtract, 4, 0); _btnOpenRules.Click += OpenRules_Click; _btnSaveRules.Click += SaveRules_Click; _btnUndoAutoExtract.Click += UndoAutoExtract_Click; var actionPanel = new FlowLayoutPanel { Dock = DockStyle.Top, AutoSize = true, Padding = new Padding(8, 0, 8, 8), }; var btnAddRule = new Button { Text = "新增规则", AutoSize = true }; var btnDeleteRule = new Button { Text = "删除规则", AutoSize = true }; var btnMoveUp = new Button { Text = "上移", AutoSize = true }; var btnMoveDown = new Button { Text = "下移", AutoSize = true }; btnAddRule.Click += AddRule_Click; btnDeleteRule.Click += DeleteRule_Click; btnMoveUp.Click += MoveRuleUp_Click; btnMoveDown.Click += MoveRuleDown_Click; actionPanel.Controls.Add(btnAddRule); actionPanel.Controls.Add(btnDeleteRule); actionPanel.Controls.Add(btnMoveUp); actionPanel.Controls.Add(btnMoveDown); var settingsAndGrid = new SplitContainer { Dock = DockStyle.Fill, Orientation = Orientation.Vertical, SplitterDistance = 350, }; _settingsRuleListSplitContainer = settingsAndGrid; settingsAndGrid.HandleCreated += delegate { ApplySettingsRuleListInitialWidth(settingsAndGrid); }; settingsAndGrid.SizeChanged += delegate { ApplySettingsRuleListInitialWidth(settingsAndGrid); }; settingsAndGrid.VisibleChanged += delegate { ApplySettingsRuleListInitialWidth(settingsAndGrid); }; _tabControl.SelectedIndexChanged += async delegate { ApplySettingsRuleListInitialWidth(settingsAndGrid); await RefreshScanOnTabSwitchAsync(); }; settingsAndGrid.Panel1.Controls.Add(BuildAutoExtractSettingsGroup()); settingsAndGrid.Panel2.Controls.Add(BuildRulesGridPanel()); root.Controls.Add(topPanel, 0, 0); root.Controls.Add(actionPanel, 0, 1); root.Controls.Add(settingsAndGrid, 0, 2); _settingsTab.Controls.Add(root); } private void ApplySettingsRuleListInitialWidth(SplitContainer splitContainer) { if (_settingsRuleListInitialWidthApplied || splitContainer == null || splitContainer.Orientation != Orientation.Vertical) { return; } if (splitContainer.ClientSize.Width < 600) { return; } var availableWidth = splitContainer.ClientSize.Width - splitContainer.SplitterWidth; if (availableWidth <= 0 || availableWidth < splitContainer.Panel1MinSize + splitContainer.Panel2MinSize) { return; } var leftWidth = availableWidth / 3; var maxLeftWidth = splitContainer.ClientSize.Width - splitContainer.SplitterWidth - splitContainer.Panel2MinSize; if (maxLeftWidth < splitContainer.Panel1MinSize) { return; } leftWidth = Math.Max(splitContainer.Panel1MinSize, Math.Min(maxLeftWidth, leftWidth)); splitContainer.SplitterDistance = leftWidth; _settingsRuleListInitialWidthApplied = true; } private void ConfigureContextMenus() { ConfigureLogContextMenu(); ConfigureFileGridContextMenu(_gridReplace, _replaceGridContextMenu); ConfigureFileGridContextMenu(_gridRestore, _restoreGridContextMenu); ConfigureRuleGridContextMenu(); } private void ConfigureLogContextMenu() { _logContextMenu.Items.Clear(); _logContextMenu.Items.Add("复制全部内容", null, delegate { CopyAllLogs(); }); _logContextMenu.Items.Add("清空日志", null, delegate { ClearLogBox(); }); _logBox.ContextMenuStrip = _logContextMenu; } private void ConfigureFileGridContextMenu(DataGridView grid, ContextMenuStrip menu) { menu.Items.Clear(); menu.Items.Add("删除", null, delegate { if (ReferenceEquals(grid, _gridReplace)) { DeleteSelectedFileItems(_replaceItems, grid); } else { DeleteSelectedFileItems(_restoreItems, grid); } }); menu.Items.Add("启用", null, delegate { SetSelectedFileItems(grid, true); }); menu.Items.Add("不启用", null, delegate { SetSelectedFileItems(grid, false); }); grid.ContextMenuStrip = menu; } private void ConfigureRuleGridContextMenu() { _ruleGridContextMenu.Items.Clear(); _ruleGridContextMenu.Items.Add("删除", null, delegate { DeleteSelectedRules(); }); _ruleGridContextMenu.Items.Add("启用", null, delegate { SetSelectedRulesEnabled(true); }); _ruleGridContextMenu.Items.Add("不启用", null, delegate { SetSelectedRulesEnabled(false); }); _gridRules.ContextMenuStrip = _ruleGridContextMenu; } private Control BuildAutoExtractSettingsGroup() { var group = new GroupBox { Text = "自动提取设置", Dock = DockStyle.Fill, Padding = new Padding(12), }; var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, AutoScroll = true, }; layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 55F)); layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 45F)); _chkJsonDiffEncryption.Text = "JSON 差异文件加密"; _chkEnableImageReplacement.Text = "启用图片替换"; _chkUseFixedImageSeed.Text = "使用固定图像随机种子"; _chkExtractKeywords.Text = "提取关键字"; _chkExtractWords.Text = "提取高频词"; _chkExtractSentences.Text = "提取高频句"; _chkKeywordFrequency.Text = "关键字算法: 词频"; _chkKeywordTfIdf.Text = "关键字算法: TF-IDF"; _chkKeywordTextRank.Text = "关键字算法: TextRank"; _chkChineseWords.Text = "统计中文词"; _chkEnglishWords.Text = "统计英文词"; _chkChineseSentences.Text = "统计中文句"; _chkEnglishSentences.Text = "统计英文句"; _chkExcludeEmails.Text = "排除邮箱地址"; ConfigureNumeric(_numKeywordCount, 1, 1000); ConfigureNumeric(_numWordCount, 1, 1000); ConfigureNumeric(_numSentenceCount, 1, 1000); ConfigureNumeric(_numFixedImageSeed, 0, int.MaxValue); ConfigureNumeric(_numMinChineseLength, 1, 50); ConfigureNumeric(_numMinEnglishLength, 1, 50); AddSettingRow(layout, 0, _chkJsonDiffEncryption, new Label()); AddSettingRow(layout, 1, _chkEnableImageReplacement, new Label()); AddSettingRow(layout, 2, _chkUseFixedImageSeed, _numFixedImageSeed); AddSettingRow(layout, 3, _chkExtractKeywords, _chkKeywordFrequency); AddSettingRow(layout, 4, _chkExtractWords, _chkKeywordTfIdf); AddSettingRow(layout, 5, _chkExtractSentences, _chkKeywordTextRank); AddSettingRow(layout, 6, new Label { Text = "关键字最大数量", Anchor = AnchorStyles.Left, AutoSize = true }, _numKeywordCount); AddSettingRow(layout, 7, new Label { Text = "高频词最大数量", Anchor = AnchorStyles.Left, AutoSize = true }, _numWordCount); AddSettingRow(layout, 8, new Label { Text = "高频句最大数量", Anchor = AnchorStyles.Left, AutoSize = true }, _numSentenceCount); AddSettingRow(layout, 9, _chkChineseWords, _chkEnglishWords); AddSettingRow(layout, 10, _chkChineseSentences, _chkEnglishSentences); AddSettingRow(layout, 11, new Label { Text = "中文最小长度", Anchor = AnchorStyles.Left, AutoSize = true }, _numMinChineseLength); AddSettingRow(layout, 12, new Label { Text = "英文最小长度", Anchor = AnchorStyles.Left, AutoSize = true }, _numMinEnglishLength); AddSettingRow(layout, 13, new Label { Text = "句子分隔符", Anchor = AnchorStyles.Left, AutoSize = true }, _txtSentenceDelimiters); AddSettingRow(layout, 14, _chkExcludeEmails, new Label()); _chkUseFixedImageSeed.CheckedChanged += delegate { _numFixedImageSeed.Enabled = _chkUseFixedImageSeed.Checked; }; group.Controls.Add(layout); return group; } private Control BuildRulesGridPanel() { ConfigureRuleGrid(_gridRules); var panel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8), }; panel.Controls.Add(_gridRules); _gridRules.Dock = DockStyle.Fill; return panel; } private void BindData() { _gridReplace.DataSource = _replaceItems; _gridRestore.DataSource = _restoreItems; _gridRules.DataSource = _rules; } private void ApplyConfigToUi() { _txtReplaceSource.Text = _config.ReplacePage.SourceDirectory ?? string.Empty; _txtReplaceOutput.Text = _config.ReplacePage.OutputDirectory ?? string.Empty; _txtRuleFile.Text = _config.ReplacePage.RuleFilePath ?? string.Empty; _txtRestoreSource.Text = _config.RestorePage.ReplaceDirectory ?? string.Empty; _txtRestoreOutput.Text = _config.RestorePage.RestoreDirectory ?? string.Empty; _cmbRestoreDiffInput.SelectedItem = string.Equals(_config.RestorePage.DiffInputFormat, ".bmp", StringComparison.OrdinalIgnoreCase) ? ".bmp" : ".diff"; var auto = _config.Settings.AutoExtract; _chkJsonDiffEncryption.Checked = _config.Settings.JsonDiffEncryption; _chkEnableImageReplacement.Checked = _config.Settings.EnableImageReplacement; _chkUseFixedImageSeed.Checked = _config.Settings.UseFixedImageSeed; _numFixedImageSeed.Value = Math.Max(_numFixedImageSeed.Minimum, Math.Min(_numFixedImageSeed.Maximum, _config.Settings.FixedImageSeed)); _numFixedImageSeed.Enabled = _chkUseFixedImageSeed.Checked; _chkExtractKeywords.Checked = auto.ExtractKeywords; _chkExtractWords.Checked = auto.ExtractHighFrequencyWords; _chkExtractSentences.Checked = auto.ExtractHighFrequencySentences; _chkKeywordFrequency.Checked = auto.UseKeywordFrequency; _chkKeywordTfIdf.Checked = auto.UseKeywordTfIdf; _chkKeywordTextRank.Checked = auto.UseKeywordTextRank; _chkChineseWords.Checked = auto.CountChineseWords; _chkEnglishWords.Checked = auto.CountEnglishWords; _chkChineseSentences.Checked = auto.CountChineseSentences; _chkEnglishSentences.Checked = auto.CountEnglishSentences; _chkExcludeEmails.Checked = auto.ExcludeEmailAddresses; _numKeywordCount.Value = auto.MaxKeywordCount; _numWordCount.Value = auto.MaxWordCount; _numSentenceCount.Value = auto.MaxSentenceCount; _numMinChineseLength.Value = auto.MinChineseWordLength; _numMinEnglishLength.Value = auto.MinEnglishWordLength; _txtSentenceDelimiters.Text = auto.SentenceDelimiters ?? string.Empty; if (!string.IsNullOrWhiteSpace(_txtRuleFile.Text) && File.Exists(_txtRuleFile.Text)) { LoadRuleFile(_txtRuleFile.Text); } } private void CaptureUiToConfig() { _config.ReplacePage.SourceDirectory = _txtReplaceSource.Text.Trim(); _config.ReplacePage.OutputDirectory = _txtReplaceOutput.Text.Trim(); _config.ReplacePage.RuleFilePath = _txtRuleFile.Text.Trim(); _config.RestorePage.ReplaceDirectory = _txtRestoreSource.Text.Trim(); _config.RestorePage.RestoreDirectory = _txtRestoreOutput.Text.Trim(); _config.RestorePage.DiffInputFormat = GetSelectedRestoreDiffInputFormat(); var auto = _config.Settings.AutoExtract; _config.Settings.JsonDiffEncryption = _chkJsonDiffEncryption.Checked; _config.Settings.EnableImageReplacement = _chkEnableImageReplacement.Checked; _config.Settings.UseFixedImageSeed = _chkUseFixedImageSeed.Checked; _config.Settings.FixedImageSeed = Decimal.ToInt32(_numFixedImageSeed.Value); auto.ExtractKeywords = _chkExtractKeywords.Checked; auto.ExtractHighFrequencyWords = _chkExtractWords.Checked; auto.ExtractHighFrequencySentences = _chkExtractSentences.Checked; auto.UseKeywordFrequency = _chkKeywordFrequency.Checked; auto.UseKeywordTfIdf = _chkKeywordTfIdf.Checked; auto.UseKeywordTextRank = _chkKeywordTextRank.Checked; auto.CountChineseWords = _chkChineseWords.Checked; auto.CountEnglishWords = _chkEnglishWords.Checked; auto.CountChineseSentences = _chkChineseSentences.Checked; auto.CountEnglishSentences = _chkEnglishSentences.Checked; auto.ExcludeEmailAddresses = _chkExcludeEmails.Checked; auto.MaxKeywordCount = Decimal.ToInt32(_numKeywordCount.Value); auto.MaxWordCount = Decimal.ToInt32(_numWordCount.Value); auto.MaxSentenceCount = Decimal.ToInt32(_numSentenceCount.Value); auto.MinChineseWordLength = Decimal.ToInt32(_numMinChineseLength.Value); auto.MinEnglishWordLength = Decimal.ToInt32(_numMinEnglishLength.Value); auto.SentenceDelimiters = _txtSentenceDelimiters.Text; } private async void MainForm_Shown(object sender, EventArgs e) { _logService.Info("APP_START", "程序启动。"); if (_configurationLoadResult.IsCorrupted) { var message = "config.json 已损坏或无法解析,当前禁止执行替换、恢复和自动提取。是否重建默认配置?" + Environment.NewLine + _configurationLoadResult.ErrorMessage; var result = ShowMessage(message, "配置文件损坏", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (result == DialogResult.Yes) { _config = _configurationService.CreateDefault(); ApplyConfigToUi(); _configurationService.Save(_config); _logService.Warning("CONFIG_RESET", "配置文件已重建为默认值。", _configurationLoadResult.ErrorMessage); } else { _logService.Error("CONFIG_BLOCK", "配置文件损坏且用户拒绝重建默认配置。", _configurationLoadResult.ErrorMessage); _skipConfigSaveOnClose = true; Close(); return; } } else if (_configurationLoadResult.IsMissing) { _logService.Info("CONFIG_DEFAULT", "config.json 不存在,当前使用默认配置。"); } await RefreshReplaceScanAsync(); await RefreshRestoreScanAsync(); UpdateActionStates(); } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (_isAutoExtractRunning || _isReplaceRunning || _isRestoreRunning) { e.Cancel = true; ShowMessage("当前仍有任务在执行,请先停止任务并等待当前文件处理完成。", "WORD 2007 格式调整软件", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } try { if (!_skipConfigSaveOnClose) { CaptureUiToConfig(); _configurationService.Save(_config); } } catch (Exception ex) { ShowMessage("保存配置失败: " + ex.Message, "WORD 2007 格式调整软件", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private void LogService_EntryWritten(object sender, LogEntry entry) { if (InvokeRequired) { BeginInvoke(new Action(LogService_EntryWritten), sender, entry); return; } var color = Color.Black; switch (entry.Level) { case LogLevel.Info: color = Color.Green; break; case LogLevel.Warning: color = Color.DarkOrange; break; case LogLevel.Error: color = Color.Red; break; } _logBox.SelectionStart = _logBox.TextLength; _logBox.SelectionColor = color; _logBox.AppendText(string.Format("{0:HH:mm:ss} [{1}] {2} {3}", entry.Timestamp, entry.Level.ToString().ToUpperInvariant(), entry.EventCode, entry.Message) + Environment.NewLine); if (!string.IsNullOrWhiteSpace(entry.Detail)) { _logBox.SelectionColor = Color.Black; _logBox.AppendText(IndentBlock(entry.Detail) + Environment.NewLine); } _logBox.SelectionStart = _logBox.TextLength; _logBox.ScrollToCaret(); FlushLogBox(); } private void LogService_WriteFailed(object sender, LogWriteFailureEventArgs e) { if (InvokeRequired) { BeginInvoke(new Action(LogService_WriteFailed), sender, e); return; } var detail = string.Format( "日志文件写入失败,当前日志路径: {0}{1}{2}", e.LogFilePath, Environment.NewLine, e.Exception.Message); _logBox.SelectionStart = _logBox.TextLength; _logBox.SelectionColor = Color.Red; _logBox.AppendText(string.Format("{0:HH:mm:ss} [ERROR] LOG_WRITE_FAIL 日志文件写入失败", DateTime.Now) + Environment.NewLine); _logBox.SelectionColor = Color.Black; _logBox.AppendText(IndentBlock(detail) + Environment.NewLine); _logBox.SelectionStart = _logBox.TextLength; _logBox.ScrollToCaret(); FlushLogBox(); if (_isAutoExtractRunning && _autoExtractCancellation != null && !_autoExtractCancellation.IsCancellationRequested) { _autoExtractCancellation.Cancel(); } if (_isReplaceRunning && _replaceExecutionCancellation != null && !_replaceExecutionCancellation.IsCancellationRequested) { _replaceExecutionCancellation.Cancel(); } if (_isRestoreRunning && _restoreExecutionCancellation != null && !_restoreExecutionCancellation.IsCancellationRequested) { _restoreExecutionCancellation.Cancel(); } ShowMessage( "日志文件当前无法继续写入。当前正在处理的文件会在可控状态结束后停止,后续文件不会继续处理。" + Environment.NewLine + detail, "日志异常", MessageBoxButtons.OK, MessageBoxIcon.Warning); } private void FlushLogBox() { FlushControl(_logBox, forceRefresh: false); } private string GetAllLogText() { return _logBox.Text; } private static string IndentBlock(string value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } var normalized = value.Replace("\r\n", "\n").Replace('\r', '\n'); return " " + normalized.Replace("\n", Environment.NewLine + " "); } private string BuildReplaceScanStartDetail(string sourceDirectory, string configuredOutputDirectory, string effectiveOutputDirectory) { var builder = new StringBuilder(); AppendKeyValue(builder, "原始文件夹", sourceDirectory); AppendKeyValue(builder, "替换文件夹(输入)", configuredOutputDirectory); AppendKeyValue(builder, "替换文件夹(生效)", effectiveOutputDirectory); return builder.ToString().TrimEnd(); } private string BuildReplaceScanDoneDetail(IList items) { var builder = new StringBuilder(); AppendKeyValue(builder, "候选数量", (items == null ? 0 : items.Count).ToString()); AppendScanItems(builder, items); return builder.ToString().TrimEnd(); } private string BuildRestoreScanStartDetail(string replaceDirectory, string configuredOutputDirectory, string effectiveOutputDirectory, string diffInputFormat) { var builder = new StringBuilder(); AppendKeyValue(builder, "替换文件夹", replaceDirectory); AppendKeyValue(builder, "恢复文件夹(输入)", configuredOutputDirectory); AppendKeyValue(builder, "恢复文件夹(生效)", effectiveOutputDirectory); AppendKeyValue(builder, "差异文件格式", diffInputFormat); return builder.ToString().TrimEnd(); } private string BuildRestoreScanDoneDetail(IList items, string diffInputFormat) { var builder = new StringBuilder(); AppendKeyValue(builder, "候选数量", (items == null ? 0 : items.Count).ToString()); AppendKeyValue(builder, "差异文件格式", diffInputFormat); AppendScanItems(builder, items); return builder.ToString().TrimEnd(); } private string BuildReplaceBatchPlanDetail(IList selectedItems, BatchExecutionPlan plan, RuleFile executionRuleFile, bool encryptDiff, int? fixedImageSeed) { var builder = new StringBuilder(); AppendKeyValue(builder, "原始文件夹", _txtReplaceSource.Text.Trim()); AppendKeyValue(builder, "替换文件夹", _txtReplaceOutput.Text.Trim()); AppendKeyValue(builder, "规则文件", _txtRuleFile.Text.Trim()); AppendKeyValue(builder, "差异 JSON 加密", encryptDiff ? "是" : "否"); AppendKeyValue(builder, "图片替换", _chkEnableImageReplacement.Checked ? "启用" : "禁用"); AppendKeyValue(builder, "固定图片种子", fixedImageSeed.HasValue ? fixedImageSeed.Value.ToString() : "否"); AppendKeyValue(builder, "规则总数", executionRuleFile == null || executionRuleFile.Rules == null ? "0" : executionRuleFile.Rules.Count.ToString()); AppendKeyValue(builder, "勾选文件数", selectedItems == null ? "0" : selectedItems.Count.ToString()); AppendKeyValue(builder, "实际执行数", plan == null ? "0" : plan.ExecutionItems.Count.ToString()); AppendKeyValue(builder, "预跳过数", plan == null ? "0" : plan.SkippedItems.Count.ToString()); AppendKeyValue(builder, "并发度", plan == null ? "0" : plan.DegreeOfParallelism.ToString()); if (plan != null && plan.SkippedItems.Count > 0) { builder.AppendLine("预跳过项目:"); foreach (var skipped in plan.SkippedItems) { builder.AppendLine("- " + NormalizeDetailValue(skipped.Item == null ? string.Empty : skipped.Item.SourcePath)); AppendKeyValue(builder, " 原因", skipped.Message); AppendKeyValue(builder, " 冲突路径", skipped.ConflictingPath); } } builder.AppendLine("执行项目:"); AppendScanItems(builder, plan == null ? null : plan.ExecutionItems.ToList()); return builder.ToString().TrimEnd(); } private string BuildRestoreBatchPlanDetail(IList selectedItems, BatchExecutionPlan plan) { var builder = new StringBuilder(); AppendKeyValue(builder, "替换文件夹", _txtRestoreSource.Text.Trim()); AppendKeyValue(builder, "恢复文件夹", _txtRestoreOutput.Text.Trim()); AppendKeyValue(builder, "差异文件格式", GetSelectedRestoreDiffInputFormat()); AppendKeyValue(builder, "规则文件", _txtRuleFile.Text.Trim()); AppendKeyValue(builder, "勾选文件数", selectedItems == null ? "0" : selectedItems.Count.ToString()); AppendKeyValue(builder, "实际执行数", plan == null ? "0" : plan.ExecutionItems.Count.ToString()); AppendKeyValue(builder, "预跳过数", plan == null ? "0" : plan.SkippedItems.Count.ToString()); AppendKeyValue(builder, "并发度", plan == null ? "0" : plan.DegreeOfParallelism.ToString()); if (plan != null && plan.SkippedItems.Count > 0) { builder.AppendLine("预跳过项目:"); foreach (var skipped in plan.SkippedItems) { builder.AppendLine("- " + NormalizeDetailValue(skipped.Item == null ? string.Empty : skipped.Item.SourcePath)); AppendKeyValue(builder, " 原因", skipped.Message); AppendKeyValue(builder, " 冲突路径", skipped.ConflictingPath); } } builder.AppendLine("执行项目:"); AppendScanItems(builder, plan == null ? null : plan.ExecutionItems.ToList()); return builder.ToString().TrimEnd(); } private string BuildReplaceFileStartDetail(FileScanItem item, int displayIndex, int totalCount) { return BuildFileExecutionDetail("替换", item, displayIndex, totalCount, includeResultFiles: false, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: null); } private string BuildReplaceFileDoneDetail(FileScanItem item, FileProcessResult result, int displayIndex, int totalCount) { return BuildFileExecutionDetail( "替换", item, displayIndex, totalCount, includeResultFiles: true, textOperationCount: result == null ? null : (int?)result.TextOperationCount, imageOperationCount: result == null ? null : (int?)result.ImageOperationCount, bmpPath: result == null ? null : result.BmpPath, exception: null); } private string BuildReplaceFileFailDetail(FileScanItem item, Exception exception, int displayIndex, int totalCount) { return BuildFileExecutionDetail("替换", item, displayIndex, totalCount, includeResultFiles: true, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: exception); } private string BuildRestoreFileStartDetail(FileScanItem item, int displayIndex, int totalCount) { return BuildFileExecutionDetail("恢复", item, displayIndex, totalCount, includeResultFiles: false, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: null); } private string BuildRestoreFileDoneDetail(FileScanItem item, FileProcessResult result, int displayIndex, int totalCount) { return BuildFileExecutionDetail( "恢复", item, displayIndex, totalCount, includeResultFiles: true, textOperationCount: result == null ? null : (int?)result.TextOperationCount, imageOperationCount: result == null ? null : (int?)result.ImageOperationCount, bmpPath: result == null ? null : result.BmpPath, exception: null); } private string BuildRestoreFileFailDetail(FileScanItem item, Exception exception, int displayIndex, int totalCount) { return BuildFileExecutionDetail("恢复", item, displayIndex, totalCount, includeResultFiles: true, textOperationCount: null, imageOperationCount: null, bmpPath: null, exception: exception); } private string BuildFileExecutionDetail( string operationName, FileScanItem item, int displayIndex, int totalCount, bool includeResultFiles, int? textOperationCount, int? imageOperationCount, string bmpPath, Exception exception) { var builder = new StringBuilder(); AppendKeyValue(builder, "操作", operationName); AppendKeyValue(builder, "序号", string.Format("{0}/{1}", displayIndex, totalCount)); AppendFileSnapshot(builder, "源文件", item == null ? null : item.SourcePath); AppendKeyValue(builder, "目标输出", item == null ? null : item.OutputPath); AppendKeyValue(builder, "差异文件", item == null ? null : item.DiffPath); if (includeResultFiles) { AppendFileSnapshot(builder, "目标输出", item == null ? null : item.OutputPath); AppendFileSnapshot(builder, "差异文件", item == null ? null : item.DiffPath); AppendFileSnapshot(builder, "BMP 差异文件", bmpPath); } if (textOperationCount.HasValue) { AppendKeyValue(builder, "文本命中/恢复数", textOperationCount.Value.ToString()); } if (imageOperationCount.HasValue) { AppendKeyValue(builder, "图片命中/恢复数", imageOperationCount.Value.ToString()); } AppendExceptionDetail(builder, exception); return builder.ToString().TrimEnd(); } private static void AppendScanItems(StringBuilder builder, IList items) { if (builder == null) { return; } if (items == null || items.Count == 0) { builder.AppendLine("候选项: "); return; } var maxCount = Math.Min(items.Count, 20); for (var index = 0; index < maxCount; index++) { var item = items[index]; builder.AppendLine(string.Format("[{0}]", index + 1)); AppendFileSnapshot(builder, " 源文件", item == null ? null : item.SourcePath); AppendKeyValue(builder, " 目标输出", item == null ? null : item.OutputPath); AppendKeyValue(builder, " 差异文件", item == null ? null : item.DiffPath); AppendKeyValue(builder, " 勾选", item != null && item.IsSelected ? "是" : "否"); AppendKeyValue(builder, " 状态", item == null ? null : item.Status.ToString()); } if (items.Count > maxCount) { builder.AppendLine(string.Format("... 其余 {0} 项省略", items.Count - maxCount)); } } private static void AppendFileSnapshot(StringBuilder builder, string label, string path) { if (builder == null) { return; } AppendKeyValue(builder, label + "路径", path); if (string.IsNullOrWhiteSpace(path)) { return; } try { if (!File.Exists(path)) { AppendKeyValue(builder, label + "存在", "否"); return; } var info = new FileInfo(path); AppendKeyValue(builder, label + "存在", "是"); AppendKeyValue(builder, label + "大小(bytes)", info.Length.ToString()); AppendKeyValue(builder, label + "属性", info.Attributes.ToString()); AppendKeyValue(builder, label + "只读", info.IsReadOnly ? "是" : "否"); AppendKeyValue(builder, label + "最后写入时间", info.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss.fff")); AppendKeyValue(builder, label + "最后写入时间(UTC)", info.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss.fff")); AppendKeyValue(builder, label + "SHA256", SafeComputeFileFingerprint(path)); } catch (Exception ex) { AppendKeyValue(builder, label + "快照错误", ex.GetType().Name + ": " + ex.Message); } } private static void AppendExceptionDetail(StringBuilder builder, Exception exception) { if (builder == null || exception == null) { return; } AppendKeyValue(builder, "异常类型", exception.GetType().FullName); AppendKeyValue(builder, "异常消息", exception.Message); if (exception.InnerException != null) { AppendKeyValue(builder, "内部异常", exception.InnerException.GetType().FullName + ": " + exception.InnerException.Message); } AppendKeyValue(builder, "异常堆栈", exception.ToString()); } private static void AppendKeyValue(StringBuilder builder, string key, string value) { if (builder == null) { return; } builder.Append(key ?? string.Empty); builder.Append(": "); builder.AppendLine(NormalizeDetailValue(value)); } private static string SafeComputeFileFingerprint(string path) { if (string.IsNullOrWhiteSpace(path)) { return ""; } try { return HashUtility.ComputeFileSha256Base64(path); } catch (Exception ex) { return ""; } } private static string NormalizeDetailValue(string value) { return string.IsNullOrWhiteSpace(value) ? "" : value; } private void CopyAllLogs() { var text = GetAllLogText(); if (string.IsNullOrEmpty(text)) { return; } try { Clipboard.SetText(text); } catch { } } private void ClearLogBox() { _logBox.Clear(); } private async Task RefreshScanOnTabSwitchAsync() { try { if (_tabControl.SelectedTab == _replaceTab) { await RefreshReplaceScanAsync(); } else if (_tabControl.SelectedTab == _restoreTab) { await RefreshRestoreScanAsync(); } } catch (OperationCanceledException) { } } private async Task RefreshReplaceScanAsync() { _replaceScanCancellation?.Cancel(); _replaceScanCancellation = new CancellationTokenSource(); var token = _replaceScanCancellation.Token; try { var source = _txtReplaceSource.Text.Trim(); if (string.IsNullOrWhiteSpace(source) || !Directory.Exists(source)) { _replaceItems.Clear(); _lblReplaceRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0"; FlushReplaceRuntimeUi(forceRefresh: true); UpdateActionStates(); return; } var output = GetReplacePreviewOutputDirectory(source); if (!string.IsNullOrWhiteSpace(_txtReplaceOutput.Text.Trim()) && string.IsNullOrWhiteSpace(output)) { foreach (var message in _fileScanService.ValidateReplaceDirectories(source, _txtReplaceOutput.Text.Trim())) { _logService.Error("SCAN_BLOCK", message); } } _logService.Info("SCAN_START", "开始扫描替换候选文件。", BuildReplaceScanStartDetail(source, _txtReplaceOutput.Text.Trim(), output)); var ruleFile = CreateExecutionRuleFile(); var items = await Task.Run(() => _fileScanService.ScanReplaceCandidates(source, output, ruleFile), token); if (token.IsCancellationRequested) { return; } ReplaceBinding(_replaceItems, items); _lblReplaceRuntime.Text = string.Format( "当前文件 0/{0} | 当前规则 - | 当前文件命中 0 | 累计命中 0", items.Count); FlushReplaceRuntimeUi(forceRefresh: true); _logService.Info("SCAN_DONE", string.Format("替换候选文件扫描完成,共 {0} 项。", items.Count), BuildReplaceScanDoneDetail(items)); UpdateActionStates(); } catch (OperationCanceledException) { } catch (Exception ex) { _logService.Error("SCAN_FAIL", "替换候选文件扫描失败。", ex.Message); } } private async Task RefreshRestoreScanAsync() { _restoreScanCancellation?.Cancel(); _restoreScanCancellation = new CancellationTokenSource(); var token = _restoreScanCancellation.Token; try { var source = _txtRestoreSource.Text.Trim(); if (string.IsNullOrWhiteSpace(source) || !Directory.Exists(source)) { _restoreItems.Clear(); _lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0"; FlushRestoreRuntimeUi(forceRefresh: true); UpdateActionStates(); return; } var output = GetRestorePreviewOutputDirectory(source); var diffInputFormat = GetSelectedRestoreDiffInputFormat(); if (!string.IsNullOrWhiteSpace(_txtRestoreOutput.Text.Trim()) && string.IsNullOrWhiteSpace(output)) { foreach (var message in _fileScanService.ValidateRestoreDirectories(source, _txtRestoreOutput.Text.Trim())) { _logService.Error("RESTORE_SCAN_BLOCK", message); } } _logService.Info("RESTORE_SCAN_START", "开始扫描恢复候选文件。", BuildRestoreScanStartDetail(source, _txtRestoreOutput.Text.Trim(), output, diffInputFormat)); var items = await Task.Run(() => _fileScanService.ScanRestoreCandidates(source, output, diffInputFormat), token); if (token.IsCancellationRequested) { return; } ReplaceBinding(_restoreItems, items); _lblRestoreRuntime.Text = string.Format( "当前文件 0/{0} | 当前规则 - | 当前文件命中 0 | 累计命中 0", items.Count); FlushRestoreRuntimeUi(forceRefresh: true); _logService.Info("RESTORE_SCAN_DONE", string.Format("恢复候选文件扫描完成,共 {0} 项。", items.Count), BuildRestoreScanDoneDetail(items, diffInputFormat)); UpdateActionStates(); } catch (OperationCanceledException) { } catch (Exception ex) { _logService.Error("RESTORE_SCAN_FAIL", "恢复候选文件扫描失败。", ex.Message); } } private async Task RunAutoExtractAsync() { var selectedFiles = _replaceItems.Where(item => item.IsSelected).Select(item => item.SourcePath).ToList(); if (selectedFiles.Count == 0) { ShowMessage("未勾选任何文件,无法执行自动提取。", "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } CaptureUiToConfig(); _autoExtractCancellation = new CancellationTokenSource(); _isAutoExtractRunning = true; SetRuleEditingEnabled(false); _btnStopReplace.Enabled = true; UpdateActionStates(); _replaceTotalProgress.Maximum = Math.Max(selectedFiles.Count, 1); _replaceTotalProgress.Value = 0; _replaceFileProgress.Value = 0; _logService.Info("EXTRACT_START", string.Format("开始自动提取,勾选文件数={0}。", selectedFiles.Count)); try { var progress = new Progress(value => { _replaceTotalProgress.Maximum = Math.Max(value.Total, 1); _replaceTotalProgress.Value = Math.Min(value.Current, _replaceTotalProgress.Maximum); _replaceFileProgress.Style = ProgressBarStyle.Marquee; _lblReplaceRuntime.Text = value.Message; }); var result = await _autoExtractService.ExtractAsync( selectedFiles, _config.Settings.AutoExtract, _rules.ToList(), progress, _autoExtractCancellation.Token); _replaceFileProgress.Style = ProgressBarStyle.Blocks; _replaceFileProgress.Value = 100; foreach (var warning in result.Warnings) { _logService.Warning("EXTRACT_SKIP", warning); } if (result.Candidates.Count == 0) { ShowMessage("未提取到可用项。", "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Information); _logService.Info("EXTRACT_DONE", "自动提取完成,但未提取到可用项。"); return; } var preview = _dialogService.ShowAutoExtractPreview(this, result.Candidates); if (preview.DialogResult != DialogResult.OK) { _logService.Info("EXTRACT_DONE", "自动提取预览已取消,未追加规则。"); return; } var selectedCandidates = preview.SelectedCandidates ?? Array.Empty(); AppendAutoExtractRules(selectedCandidates); var skippedCount = result.Candidates.Count - selectedCandidates.Count; var summary = string.Format("自动提取完成。提取={0},追加={1},跳过={2}。", result.Candidates.Count, selectedCandidates.Count, skippedCount); ShowMessage(summary, "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Information); _logService.Info("EXTRACT_DONE", summary); } catch (OperationCanceledException) { _logService.Warning("EXTRACT_CANCEL", "自动提取已停止。"); } catch (Exception ex) { _logService.Error("EXTRACT_FAIL", "自动提取失败。", ex.ToString()); ShowMessage("自动提取失败: " + ex.Message, "自动提取", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _isAutoExtractRunning = false; SetRuleEditingEnabled(true); _btnStopReplace.Enabled = false; _replaceFileProgress.Style = ProgressBarStyle.Blocks; UpdateActionStates(); } } private void AppendAutoExtractRules(IList candidates) { if (candidates.Count == 0) { return; } var newRuleIds = new List(); foreach (var candidate in candidates) { var rule = _ruleService.CreateAutoExtractRule(candidate, _rules.ToList()); _rules.Add(rule); newRuleIds.Add(rule.Id); } _lastAutoExtractRuleIds = newRuleIds; _btnUndoAutoExtract.Enabled = true; var report = _ruleService.Validate(new RuleFile { Rules = _rules.ToList() }); if (report.Issues.Count > 0) { var text = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue)); ShowMessage(text, "规则检查", MessageBoxButtons.OK, report.HasErrors ? MessageBoxIcon.Warning : MessageBoxIcon.Information); foreach (var issue in report.Issues) { LogRuleIssue(issue); } } _tabControl.SelectedTab = _settingsTab; if (_rules.Count > 0) { var firstIndex = _rules.ToList().FindIndex(rule => newRuleIds.Contains(rule.Id)); if (firstIndex >= 0) { _gridRules.ClearSelection(); if (_gridRules.Rows[firstIndex].Cells.Count > 0) { _gridRules.CurrentCell = _gridRules.Rows[firstIndex].Cells[0]; } _gridRules.Rows[firstIndex].Selected = true; try { _gridRules.FirstDisplayedScrollingRowIndex = firstIndex; } catch (InvalidOperationException) { } } } } private void OpenRules_Click(object sender, EventArgs e) { using (var dialog = new OpenFileDialog()) { dialog.Filter = "规则文件 (*.rule)|*.rule|所有文件 (*.*)|*.*"; if (dialog.ShowDialog(this) != DialogResult.OK) { return; } LoadRuleFile(dialog.FileName); _txtRuleFile.Text = dialog.FileName; } } private void SaveRules_Click(object sender, EventArgs e) { try { var path = _txtRuleFile.Text.Trim(); if (string.IsNullOrWhiteSpace(path)) { using (var dialog = new SaveFileDialog()) { dialog.Filter = "规则文件 (*.rule)|*.rule"; dialog.DefaultExt = "rule"; if (dialog.ShowDialog(this) != DialogResult.OK) { return; } path = dialog.FileName; _txtRuleFile.Text = path; } } _ruleFile.Rules = _rules.ToList(); var report = _ruleService.Validate(_ruleFile); foreach (var issue in report.Issues) { LogRuleIssue(issue); } if (report.Issues.Count > 0) { var summary = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue)); ShowMessage(summary, "规则检查", MessageBoxButtons.OK, report.HasErrors ? MessageBoxIcon.Warning : MessageBoxIcon.Information); if (report.HasErrors) { return; } } _ruleService.Save(path, _ruleFile); _logService.Info("RULE_SAVE", "规则文件已保存。", path); } catch (Exception ex) { _logService.Error("RULE_SAVE_FAIL", "保存规则文件失败。", ex.Message); ShowMessage("保存规则文件失败: " + ex.Message, "规则文件", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void UndoAutoExtract_Click(object sender, EventArgs e) { if (_lastAutoExtractRuleIds.Count == 0) { return; } for (var index = _rules.Count - 1; index >= 0; index--) { if (_lastAutoExtractRuleIds.Contains(_rules[index].Id)) { _rules.RemoveAt(index); } } _logService.Info("EXTRACT_UNDO", string.Format("已撤销最近一次自动提取追加的 {0} 条规则。", _lastAutoExtractRuleIds.Count)); _lastAutoExtractRuleIds.Clear(); _btnUndoAutoExtract.Enabled = false; } private void AddRule_Click(object sender, EventArgs e) { var nextIndex = _rules.Count + 1; _rules.Add(new RuleDefinition { Id = "MR" + nextIndex.ToString("0000"), Name = "MANUAL-" + nextIndex.ToString("0000"), IsEnabled = true, MatchMode = RuleMatchMode.PlainText, Target = RuleTarget.DocumentContent, Note = string.Empty, }); } private void DeleteRule_Click(object sender, EventArgs e) { DeleteSelectedRules(); } private void MoveRuleUp_Click(object sender, EventArgs e) { MoveCurrentRule(-1); } private void MoveRuleDown_Click(object sender, EventArgs e) { MoveCurrentRule(1); } private void MoveCurrentRule(int delta) { if (_gridRules.CurrentRow == null) { return; } var currentIndex = _gridRules.CurrentRow.Index; var nextIndex = currentIndex + delta; if (currentIndex < 0 || nextIndex < 0 || nextIndex >= _rules.Count) { return; } var item = _rules[currentIndex]; _rules.RemoveAt(currentIndex); _rules.Insert(nextIndex, item); _gridRules.ClearSelection(); _gridRules.Rows[nextIndex].Selected = true; } private async void StartReplaceExecution_Click(object sender, EventArgs e) { if (_isReplaceRunning || _isRestoreRunning) { return; } if (!TryPersistConfigFromUi()) { return; } var selectedItems = _replaceItems.Where(item => item.IsSelected).ToList(); if (selectedItems.Count == 0) { ShowMessage("未勾选任何替换文件。", "开始替换", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var validationMessages = GetReplaceExecutionValidationMessages(); if (validationMessages.Count > 0) { foreach (var message in validationMessages) { _logService.Error("REPLACE_BLOCK", message); } ShowMessage(string.Join(Environment.NewLine, validationMessages), "开始替换", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } RuleFile executionRuleFile; if (!PrepareRuleExecutionWithBlockingErrors(out executionRuleFile)) { return; } var emptyReplacementRules = _rules.Where(rule => rule.IsEnabled && string.IsNullOrEmpty(rule.ReplacementText)).ToList(); if (emptyReplacementRules.Count > 0) { var warning = string.Format("当前存在 {0} 条启用规则的替换文本为空,执行后会删除命中的原始内容。是否继续?", emptyReplacementRules.Count); if (ShowMessage(warning, "强提示", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) { return; } } await ExecuteReplaceBatchAsync(selectedItems, executionRuleFile); } private async void StartRestoreExecution_Click(object sender, EventArgs e) { if (_isReplaceRunning || _isRestoreRunning) { return; } if (!TryPersistConfigFromUi()) { return; } var selectedItems = _restoreItems.Where(item => item.IsSelected).ToList(); if (selectedItems.Count == 0) { ShowMessage("未勾选任何恢复文件。", "开始恢复", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var validationMessages = GetRestoreExecutionValidationMessages(); if (validationMessages.Count > 0) { foreach (var message in validationMessages) { _logService.Error("RESTORE_BLOCK", message); } ShowMessage(string.Join(Environment.NewLine, validationMessages), "开始恢复", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } await ExecuteRestoreBatchAsync(selectedItems); } private void StopReplaceOrAutoExtract_Click(object sender, EventArgs e) { if (_isAutoExtractRunning) { _autoExtractCancellation?.Cancel(); return; } if (_isReplaceRunning && _replaceExecutionCancellation != null && !_replaceExecutionCancellation.IsCancellationRequested) { _replaceExecutionCancellation.Cancel(); _logService.Warning("REPLACE_STOP", "已请求停止替换任务,当前文件完成后停止。"); } } private void StopRestore_Click(object sender, EventArgs e) { if (_isRestoreRunning && _restoreExecutionCancellation != null && !_restoreExecutionCancellation.IsCancellationRequested) { _restoreExecutionCancellation.Cancel(); _logService.Warning("RESTORE_STOP", "已请求停止恢复任务,当前文件完成后停止。"); } } private async Task ExecuteReplaceBatchAsync(IList selectedItems, RuleFile executionRuleFile) { _replaceExecutionCancellation?.Dispose(); _replaceExecutionCancellation = new CancellationTokenSource(); _isReplaceRunning = true; SetRuleEditingEnabled(false); var encryptDiff = _chkJsonDiffEncryption.Checked; var fixedImageSeed = _chkUseFixedImageSeed.Checked ? Decimal.ToInt32(_numFixedImageSeed.Value) : (int?)null; foreach (var item in selectedItems) { item.Status = ProcessingStatus.Pending; item.TextOperationCount = 0; item.ImageOperationCount = 0; } ResetReplaceProgress(selectedItems.Count); UpdateActionStates(); var plan = _batchExecutionPlanner.CreatePlan(selectedItems, Environment.ProcessorCount); var executionItems = plan.ExecutionItems.ToList(); var displayOrder = executionItems .Select((item, index) => new { item, index = index + 1 }) .ToDictionary(entry => entry.item, entry => entry.index); var successCount = 0; var failedCount = 0; var stoppedCount = 0; var totalHits = 0; var completedCount = 0; var nextExecutionIndex = -1; foreach (var skipped in plan.SkippedItems) { skipped.Item.Status = ProcessingStatus.Failed; RefreshReplaceItemRow(skipped.Item, forceRefresh: true); failedCount++; completedCount++; _replaceTotalProgress.Value = Math.Min(completedCount, _replaceTotalProgress.Maximum); FlushReplaceRuntimeUi(forceRefresh: true); _logService.Warning( "REPLACE_BATCH_SKIP", skipped.Message, string.Format("源文件: {0}{1}冲突路径: {2}", skipped.Item.SourcePath, Environment.NewLine, skipped.ConflictingPath)); } try { _logService.Info( "REPLACE_BATCH_PLAN", string.Format( "替换批处理开始。勾选={0},执行={1},预跳过={2},并发度={3}{4}。", selectedItems.Count, executionItems.Count, plan.SkippedItems.Count, plan.DegreeOfParallelism, plan.RequiresSerialExecution ? "(包含 .doc,已退回串行)" : string.Empty), BuildReplaceBatchPlanDetail(selectedItems, plan, executionRuleFile, encryptDiff, fixedImageSeed)); async Task RunWorkerAsync() { while (true) { if (_replaceExecutionCancellation.IsCancellationRequested) { return; } var executionIndex = Interlocked.Increment(ref nextExecutionIndex); if (executionIndex >= executionItems.Count) { return; } var item = executionItems[executionIndex]; var displayIndex = displayOrder[item]; item.Status = ProcessingStatus.InProgress; RefreshReplaceItemRow(item, forceRefresh: true); UpdateReplaceStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true); _logService.Info("DEBUG_RULES", "执行规则列表。", string.Format("总规则数={0}; 规则详情=[{1}]", executionRuleFile.Rules.Count, string.Join("; ", executionRuleFile.Rules.Select(r => string.Format("Id={0}, Name={1}, Enabled={2}, Target={3}, SearchText='{4}', ReplacementText='{5}'", r.Id, r.Name, r.IsEnabled, r.Target, r.SearchText, r.ReplacementText))))); var fileNameRules = executionRuleFile.Rules .Where(r => r != null && r.IsEnabled && !string.IsNullOrEmpty(r.SearchText) && r.Target == RuleTarget.FileName) .ToList(); if (fileNameRules.Count > 0) { var sourceFileName = Path.GetFileNameWithoutExtension(item.SourcePath); var extension = Path.GetExtension(item.SourcePath); _logService.Info("FILENAME_RULES", "应用文件名替换规则。", string.Format("源文件名={0}; 规则数={1}; 规则详情=[{2}]", sourceFileName, fileNameRules.Count, string.Join("; ", fileNameRules.Select(r => string.Format("Id={0}, SearchText='{1}', ReplacementText='{2}', MatchMode={3}, CaseSensitive={4}, WholeWord={5}", r.Id, r.SearchText, r.ReplacementText, r.MatchMode, r.IsCaseSensitive, r.IsWholeWord))))); var replacedName = FileScanService.ApplyRulesToFileName(sourceFileName, fileNameRules); _logService.Info("FILENAME_REPLACED", "文件名替换结果。", string.Format("原文件名={0}; 替换后={1}", sourceFileName, replacedName)); var outputDir = Path.GetDirectoryName(item.OutputPath) ?? string.Empty; var diffDir = Path.GetDirectoryName(item.DiffPath) ?? string.Empty; var datedSuffix = "-" + DateTime.Today.ToString("yyyyMMdd"); item.OutputPath = Path.Combine(outputDir, replacedName + datedSuffix + extension); item.DiffPath = Path.Combine(diffDir, replacedName + datedSuffix + ".diff"); } var startDetail = await Task.Run(() => BuildReplaceFileStartDetail(item, displayIndex, selectedItems.Count)); _logService.Info("REPLACE_FILE_START", "开始处理替换文件。", startDetail); try { var progressThrottle = new RuntimeProgressThrottle(); var progress = new Progress(value => { LogProcessProgressWarning("REPLACE_PROGRESS_WARN", value); var forceRefresh = value.FilePercent >= 100; if (!progressThrottle.ShouldApply(forceRefresh)) { return; } UpdateReplaceStatus( displayIndex, selectedItems.Count, value.CurrentRuleName ?? value.CurrentRuleId ?? "-", value.CurrentFileTextCount, totalHits + value.CurrentFileTextCount, value.FilePercent, forceRefresh); }); var request = new ReplaceFileRequest { SourcePath = item.SourcePath, OutputPath = item.OutputPath, DiffPath = item.DiffPath, SourceRootDirectory = _txtReplaceSource.Text.Trim(), RuleFilePath = _txtRuleFile.Text.Trim(), RuleFile = executionRuleFile, EncryptDiff = encryptDiff, EnableImageReplacement = _chkEnableImageReplacement.Checked, FixedImageSeed = fixedImageSeed, }; var result = await Task.Run(() => _documentProcessingService.Replace(request, progress, CancellationToken.None)); item.OutputPath = result.OutputPath; item.DiffPath = result.DiffPath; item.TextOperationCount = result.TextOperationCount; item.ImageOperationCount = result.ImageOperationCount; item.Status = ProcessingStatus.Success; RefreshReplaceItemRow(item, forceRefresh: true); successCount++; totalHits += result.TextOperationCount + result.ImageOperationCount; UpdateReplaceStatus(displayIndex, selectedItems.Count, "-", result.TextOperationCount + result.ImageOperationCount, totalHits, 100, forceRefresh: true); var doneDetail = await Task.Run(() => BuildReplaceFileDoneDetail(item, result, displayIndex, selectedItems.Count)); _logService.Info( "REPLACE_FILE_DONE", string.Format("文件替换完成。文本 {0} 次,图片 {1} 次。", result.TextOperationCount, result.ImageOperationCount), doneDetail); } catch (Exception ex) { item.Status = ProcessingStatus.Failed; RefreshReplaceItemRow(item, forceRefresh: true); failedCount++; _replaceFileProgress.Value = 0; UpdateReplaceStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true); var failDetail = await Task.Run(() => BuildReplaceFileFailDetail(item, ex, displayIndex, selectedItems.Count)); _logService.Error("REPLACE_FILE_FAIL", "替换文件处理失败。", failDetail); } completedCount++; _replaceTotalProgress.Value = Math.Min(completedCount, _replaceTotalProgress.Maximum); FlushReplaceRuntimeUi(forceRefresh: true); } } var workers = Enumerable.Range(0, Math.Max(plan.DegreeOfParallelism, 1)) .Select(_ => RunWorkerAsync()) .ToArray(); await Task.WhenAll(workers); if (_replaceExecutionCancellation.IsCancellationRequested) { stoppedCount += MarkRemainingItemsStopped(selectedItems.Where(item => item.Status == ProcessingStatus.Pending || item.Status == ProcessingStatus.InProgress)); completedCount = Math.Min(selectedItems.Count, successCount + failedCount + stoppedCount); _replaceTotalProgress.Value = Math.Min(completedCount, _replaceTotalProgress.Maximum); FlushControl(_gridReplace, forceRefresh: true); FlushReplaceRuntimeUi(forceRefresh: true); } } finally { _isReplaceRunning = false; _replaceFileProgress.Style = ProgressBarStyle.Blocks; _btnStopReplace.Enabled = false; SetRuleEditingEnabled(true); UpdateActionStates(); } // 批次里每个文档的 DiffPayload(含所有图片 base64)是大块瞬态分配,GC 不保证即时回收。 // 在弹完成对话框、触发自动恢复扫描之前主动回收一次,把 ~4GB 还给系统,避免残留内存压力拖慢后续 UI。 await Task.Run(() => { GC.Collect(); GC.WaitForPendingFinalizers(); }); CompleteReplaceRuntimeStatus(successCount + failedCount + stoppedCount, selectedItems.Count, totalHits); var summary = string.Format("替换完成。成功 {0},失败 {1},停止 {2},累计命中 {3}。", successCount, failedCount, stoppedCount, totalHits); _logService.Info("REPLACE_DONE", summary); ShowMessage(summary, "开始替换", MessageBoxButtons.OK, failedCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information); await RefreshRestoreScanAsync(); } private async Task ExecuteRestoreBatchAsync(IList selectedItems) { _restoreExecutionCancellation?.Dispose(); _restoreExecutionCancellation = new CancellationTokenSource(); _isRestoreRunning = true; SetRuleEditingEnabled(false); foreach (var item in selectedItems) { item.Status = ProcessingStatus.Pending; item.TextOperationCount = 0; item.ImageOperationCount = 0; } ResetRestoreProgress(selectedItems.Count); UpdateActionStates(); var plan = _batchExecutionPlanner.CreatePlan(selectedItems, Environment.ProcessorCount); var executionItems = plan.ExecutionItems.ToList(); var displayOrder = executionItems .Select((item, index) => new { item, index = index + 1 }) .ToDictionary(entry => entry.item, entry => entry.index); var successCount = 0; var failedCount = 0; var stoppedCount = 0; var totalHits = 0; var completedCount = 0; var nextExecutionIndex = -1; foreach (var skipped in plan.SkippedItems) { skipped.Item.Status = ProcessingStatus.Failed; RefreshRestoreItemRow(skipped.Item, forceRefresh: true); failedCount++; completedCount++; _restoreTotalProgress.Value = Math.Min(completedCount, _restoreTotalProgress.Maximum); FlushRestoreRuntimeUi(forceRefresh: true); _logService.Warning( "RESTORE_BATCH_SKIP", skipped.Message, string.Format("源文件: {0}{1}冲突路径: {2}", skipped.Item.SourcePath, Environment.NewLine, skipped.ConflictingPath)); } try { _logService.Info( "RESTORE_BATCH_PLAN", string.Format( "恢复批处理开始。勾选={0},执行={1},预跳过={2},并发度={3}{4}。", selectedItems.Count, executionItems.Count, plan.SkippedItems.Count, plan.DegreeOfParallelism, plan.RequiresSerialExecution ? "(包含 .doc,已退回串行)" : string.Empty), BuildRestoreBatchPlanDetail(selectedItems, plan)); async Task RunWorkerAsync() { while (true) { if (_restoreExecutionCancellation.IsCancellationRequested) { return; } var executionIndex = Interlocked.Increment(ref nextExecutionIndex); if (executionIndex >= executionItems.Count) { return; } var item = executionItems[executionIndex]; var displayIndex = displayOrder[item]; item.Status = ProcessingStatus.InProgress; RefreshRestoreItemRow(item, forceRefresh: true); UpdateRestoreStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true); var startDetail = await Task.Run(() => BuildRestoreFileStartDetail(item, displayIndex, selectedItems.Count)); _logService.Info("RESTORE_FILE_START", "开始处理恢复文件。", startDetail); try { var progressThrottle = new RuntimeProgressThrottle(); var progress = new Progress(value => { LogProcessProgressWarning("RESTORE_PROGRESS_WARN", value); var forceRefresh = value.FilePercent >= 100; if (!progressThrottle.ShouldApply(forceRefresh)) { return; } UpdateRestoreStatus( displayIndex, selectedItems.Count, value.CurrentRuleName ?? value.CurrentRuleId ?? "-", value.CurrentFileTextCount, totalHits + value.CurrentFileTextCount, value.FilePercent, forceRefresh); }); var request = new RestoreFileRequest { ReplacedPath = item.SourcePath, DiffPath = item.DiffPath, OutputPath = item.OutputPath, ReplaceRootDirectory = _txtRestoreSource.Text.Trim(), CurrentRuleFilePath = _txtRuleFile.Text.Trim(), }; var result = await Task.Run(() => _documentProcessingService.Restore(request, progress, CancellationToken.None)); item.OutputPath = result.OutputPath; item.TextOperationCount = result.TextOperationCount; item.ImageOperationCount = result.ImageOperationCount; item.Status = ProcessingStatus.Success; RefreshRestoreItemRow(item, forceRefresh: true); successCount++; totalHits += result.TextOperationCount + result.ImageOperationCount; UpdateRestoreStatus(displayIndex, selectedItems.Count, "-", result.TextOperationCount + result.ImageOperationCount, totalHits, 100, forceRefresh: true); var doneDetail = await Task.Run(() => BuildRestoreFileDoneDetail(item, result, displayIndex, selectedItems.Count)); _logService.Info( "RESTORE_FILE_DONE", string.Format("文件恢复完成。文本 {0} 次,图片 {1} 次。", result.TextOperationCount, result.ImageOperationCount), doneDetail); } catch (Exception ex) { item.Status = ProcessingStatus.Failed; RefreshRestoreItemRow(item, forceRefresh: true); failedCount++; _restoreFileProgress.Value = 0; UpdateRestoreStatus(displayIndex, selectedItems.Count, "-", 0, totalHits, 0, forceRefresh: true); var failDetail = await Task.Run(() => BuildRestoreFileFailDetail(item, ex, displayIndex, selectedItems.Count)); _logService.Error("RESTORE_FILE_FAIL", "恢复文件处理失败。", failDetail); } completedCount++; _restoreTotalProgress.Value = Math.Min(completedCount, _restoreTotalProgress.Maximum); FlushRestoreRuntimeUi(forceRefresh: true); } } var workers = Enumerable.Range(0, Math.Max(plan.DegreeOfParallelism, 1)) .Select(_ => RunWorkerAsync()) .ToArray(); await Task.WhenAll(workers); if (_restoreExecutionCancellation.IsCancellationRequested) { stoppedCount += MarkRemainingItemsStopped(selectedItems.Where(item => item.Status == ProcessingStatus.Pending || item.Status == ProcessingStatus.InProgress)); completedCount = Math.Min(selectedItems.Count, successCount + failedCount + stoppedCount); _restoreTotalProgress.Value = Math.Min(completedCount, _restoreTotalProgress.Maximum); FlushControl(_gridRestore, forceRefresh: true); FlushRestoreRuntimeUi(forceRefresh: true); } } finally { _isRestoreRunning = false; _restoreFileProgress.Style = ProgressBarStyle.Blocks; _btnStopRestore.Enabled = false; SetRuleEditingEnabled(true); UpdateActionStates(); } // 恢复批次同样释放掉 DiffPayload 载荷,避免残留内存影响后续交互。 await Task.Run(() => { GC.Collect(); GC.WaitForPendingFinalizers(); }); CompleteRestoreRuntimeStatus(successCount + failedCount + stoppedCount, selectedItems.Count, totalHits); var summary = string.Format("恢复完成。成功 {0},失败 {1},停止 {2},累计命中 {3}。", successCount, failedCount, stoppedCount, totalHits); _logService.Info("RESTORE_DONE", summary); ShowMessage(summary, "开始恢复", MessageBoxButtons.OK, failedCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information); } private bool PrepareRuleExecution(out RuleFile executionRuleFile) { executionRuleFile = CreateExecutionRuleFile(); var report = _ruleService.Validate(executionRuleFile); foreach (var issue in report.Issues) { LogRuleIssue(issue); } if (report.Issues.Count == 0) { return true; } var summary = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue)); var prompt = summary + Environment.NewLine + Environment.NewLine + "是否继续执行?"; return ShowMessage( prompt, "规则执行检查", MessageBoxButtons.YesNo, report.HasErrors ? MessageBoxIcon.Warning : MessageBoxIcon.Information) == DialogResult.Yes; } private bool PrepareRuleExecutionWithBlockingErrors(out RuleFile executionRuleFile) { executionRuleFile = CreateExecutionRuleFile(); var report = _ruleService.Validate(executionRuleFile); foreach (var issue in report.Issues) { LogRuleIssue(issue); } if (report.Issues.Count == 0) { return true; } var summary = string.Join(Environment.NewLine, report.Issues.Select(_ruleService.FormatIssue)); if (report.HasErrors) { ShowMessage( summary + Environment.NewLine + Environment.NewLine + "存在错误,必须先修复后才能执行。", "规则执行检查", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } var prompt = summary + Environment.NewLine + Environment.NewLine + "是否继续执行?"; return ShowMessage( prompt, "规则执行检查", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes; } private RuleFile CreateExecutionRuleFile() { if (_gridRules.IsHandleCreated) { _gridRules.EndEdit(); } var result = new RuleFile { Format = _ruleFile.Format, Version = _ruleFile.Version, }; foreach (var rule in _rules) { result.Rules.Add(new RuleDefinition { Id = rule.Id, Name = rule.Name, IsEnabled = rule.IsEnabled, MatchMode = rule.MatchMode, Target = rule.Target, IsCaseSensitive = rule.IsCaseSensitive, IsWholeWord = rule.IsWholeWord, SearchText = rule.SearchText, ReplacementText = rule.ReplacementText, Note = rule.Note, }); } _ruleFile = result; return result; } private bool TryPersistConfigFromUi() { try { CaptureUiToConfig(); _configurationService.Save(_config); return true; } catch (Exception ex) { _logService.Error("CONFIG_SAVE_FAIL", "保存当前配置失败。", ex.Message); ShowMessage("保存当前配置失败: " + ex.Message, "WORD 2007 格式调整软件", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } } private void ResetReplaceProgress(int totalCount) { _replaceTotalProgress.Style = ProgressBarStyle.Blocks; _replaceTotalProgress.Maximum = Math.Max(totalCount, 1); _replaceTotalProgress.Value = 0; _replaceFileProgress.Style = ProgressBarStyle.Blocks; _replaceFileProgress.Value = 0; _lblReplaceRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0"; FlushReplaceRuntimeUi(forceRefresh: true); } private void ResetRestoreProgress(int totalCount) { _restoreTotalProgress.Style = ProgressBarStyle.Blocks; _restoreTotalProgress.Maximum = Math.Max(totalCount, 1); _restoreTotalProgress.Value = 0; _restoreFileProgress.Style = ProgressBarStyle.Blocks; _restoreFileProgress.Value = 0; _lblRestoreRuntime.Text = "当前文件 0/0 | 当前规则 - | 当前文件命中 0 | 累计命中 0"; FlushRestoreRuntimeUi(forceRefresh: true); } private void LogProcessProgressWarning(string eventCode, FileProcessProgress value) { if (value == null || !value.IsWarning || string.IsNullOrWhiteSpace(value.Message)) { return; } var detail = string.IsNullOrWhiteSpace(value.FilePath) ? null : "文件: " + value.FilePath; _logService.Warning(eventCode, value.Message, detail); } private void UpdateReplaceStatus(int currentFile, int totalFiles, string currentRule, int currentFileHits, int totalHits, int filePercent, bool forceRefresh = false) { _replaceFileProgress.Value = Math.Max(0, Math.Min(filePercent, 100)); _lblReplaceRuntime.Text = string.Format( "当前文件 {0}/{1} | 当前规则 {2} | 当前文件命中 {3} | 累计命中 {4}", currentFile, totalFiles, string.IsNullOrWhiteSpace(currentRule) ? "-" : currentRule, currentFileHits, totalHits); FlushReplaceRuntimeUi(forceRefresh); } private void CompleteReplaceRuntimeStatus(int completedFiles, int totalFiles, int totalHits) { var safeTotal = Math.Max(0, totalFiles); var safeCompleted = Math.Max(0, Math.Min(completedFiles, safeTotal)); _replaceTotalProgress.Value = Math.Min(safeCompleted, _replaceTotalProgress.Maximum); UpdateReplaceStatus( safeCompleted, safeTotal, "-", 0, totalHits, safeTotal > 0 && safeCompleted >= safeTotal ? 100 : 0, forceRefresh: true); } private void UpdateRestoreStatus(int currentFile, int totalFiles, string currentRule, int currentFileHits, int totalHits, int filePercent, bool forceRefresh = false) { _restoreFileProgress.Value = Math.Max(0, Math.Min(filePercent, 100)); _lblRestoreRuntime.Text = string.Format( "当前文件 {0}/{1} | 当前规则 {2} | 当前文件命中 {3} | 累计命中 {4}", currentFile, totalFiles, string.IsNullOrWhiteSpace(currentRule) ? "-" : currentRule, currentFileHits, totalHits); FlushRestoreRuntimeUi(forceRefresh); } private void CompleteRestoreRuntimeStatus(int completedFiles, int totalFiles, int totalHits) { var safeTotal = Math.Max(0, totalFiles); var safeCompleted = Math.Max(0, Math.Min(completedFiles, safeTotal)); _restoreTotalProgress.Value = Math.Min(safeCompleted, _restoreTotalProgress.Maximum); UpdateRestoreStatus( safeCompleted, safeTotal, "-", 0, totalHits, safeTotal > 0 && safeCompleted >= safeTotal ? 100 : 0, forceRefresh: true); } private void FlushReplaceRuntimeUi(bool forceRefresh) { if (!forceRefresh && !ShouldRefreshRuntimeUi(ref _lastReplaceRuntimeFlushTick)) { return; } FlushControl(_replaceFileProgress, forceRefresh); FlushControl(_replaceTotalProgress, forceRefresh); FlushControl(_lblReplaceRuntime, forceRefresh); } private void FlushRestoreRuntimeUi(bool forceRefresh) { if (!forceRefresh && !ShouldRefreshRuntimeUi(ref _lastRestoreRuntimeFlushTick)) { return; } FlushControl(_restoreFileProgress, forceRefresh); FlushControl(_restoreTotalProgress, forceRefresh); FlushControl(_lblRestoreRuntime, forceRefresh); } private static bool ShouldRefreshRuntimeUi(ref int lastFlushTick) { var now = Environment.TickCount & int.MaxValue; if (lastFlushTick != 0 && now >= lastFlushTick && now - lastFlushTick < RuntimeUiRefreshIntervalMilliseconds) { return false; } lastFlushTick = now; return true; } private static bool ShouldApplyProgressUpdate(ref int lastProgressTick, bool forceRefresh) { if (forceRefresh) { lastProgressTick = Environment.TickCount & int.MaxValue; return true; } return ShouldRefreshRuntimeUi(ref lastProgressTick); } private static void FlushControl(Control control, bool forceRefresh) { if (control == null || control.IsDisposed || !control.IsHandleCreated) { return; } if (forceRefresh) { control.Refresh(); return; } control.Update(); } private void RefreshReplaceItemRow(FileScanItem item, bool forceRefresh = false) { RefreshFileItemRow(_gridReplace, _replaceItems, item, forceRefresh); } private void RefreshRestoreItemRow(FileScanItem item, bool forceRefresh = false) { RefreshFileItemRow(_gridRestore, _restoreItems, item, forceRefresh); } private static void RefreshFileItemRow(DataGridView grid, BindingList items, FileScanItem item, bool forceRefresh) { if (grid == null || items == null || item == null || grid.IsDisposed || !grid.IsHandleCreated) { return; } var rowIndex = items.IndexOf(item); if (rowIndex >= 0 && rowIndex < grid.Rows.Count) { grid.InvalidateRow(rowIndex); } FlushControl(grid, forceRefresh); } private sealed class RuntimeProgressThrottle { private int _lastProgressTick; public bool ShouldApply(bool forceRefresh) { return ShouldApplyProgressUpdate(ref _lastProgressTick, forceRefresh); } } private static int MarkRemainingItemsStopped(IEnumerable items) { var count = 0; foreach (var item in items) { if (item.Status == ProcessingStatus.Pending || item.Status == ProcessingStatus.InProgress) { item.Status = ProcessingStatus.Stopped; count++; } } return count; } private string GetSelectedRestoreDiffInputFormat() { return string.Equals(_cmbRestoreDiffInput.SelectedItem as string, ".bmp", StringComparison.OrdinalIgnoreCase) ? ".bmp" : ".diff"; } private void SetRestoreDiffInputFormat(string diffInputFormat) { var normalized = string.Equals(diffInputFormat, ".bmp", StringComparison.OrdinalIgnoreCase) ? ".bmp" : ".diff"; _cmbRestoreDiffInput.SelectedItem = normalized; } private void LoadRuleFile(string filePath) { try { _ruleFile = _ruleService.Load(filePath); ReplaceBinding(_rules, _ruleFile.Rules); _logService.Info("RULE_LOAD", "规则文件已加载。", filePath); UpdateActionStates(); } catch (Exception ex) { _logService.Error("RULE_LOAD_FAIL", "加载规则文件失败。", ex.Message); ShowMessage("加载规则文件失败: " + ex.Message, "规则文件", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private async Task BrowseFolderIntoTextBoxAsync(TextBox textBox, Func refreshAsync) { string selectedPath; if (!ModernFolderPicker.TrySelectFolder(this, textBox.Text, out selectedPath)) { return; } await ApplySelectedFolderPathAsync(textBox, selectedPath, refreshAsync); } private async Task ApplySelectedFolderPathAsync(TextBox textBox, string selectedPath, Func refreshAsync) { textBox.Text = selectedPath ?? string.Empty; if (refreshAsync != null) { await refreshAsync(); } } private string GetReplacePreviewOutputDirectory(string sourceDirectory) { var output = _txtReplaceOutput.Text.Trim(); if (string.IsNullOrWhiteSpace(output)) { return string.Empty; } var validationMessages = _fileScanService.ValidateReplaceDirectories(sourceDirectory, output); return validationMessages.Count == 0 ? output : string.Empty; } private string GetRestorePreviewOutputDirectory(string replaceDirectory) { var output = _txtRestoreOutput.Text.Trim(); if (string.IsNullOrWhiteSpace(output)) { return string.Empty; } var validationMessages = _fileScanService.ValidateRestoreDirectories(replaceDirectory, output); return validationMessages.Count == 0 ? output : string.Empty; } private IList GetReplaceExecutionValidationMessages() { var source = _txtReplaceSource.Text.Trim(); var output = _txtReplaceOutput.Text.Trim(); var messages = new List(); if (string.IsNullOrWhiteSpace(source)) { messages.Add("未选择原始文件夹。"); } else if (!Directory.Exists(source)) { messages.Add("原始文件夹不存在。"); } if (string.IsNullOrWhiteSpace(output)) { messages.Add("未选择替换文件夹。"); } messages.AddRange(_fileScanService.ValidateReplaceDirectories(source, output)); return messages; } private IList GetRestoreExecutionValidationMessages() { var source = _txtRestoreSource.Text.Trim(); var output = _txtRestoreOutput.Text.Trim(); var messages = new List(); if (string.IsNullOrWhiteSpace(source)) { messages.Add("未选择替换文件夹。"); } else if (!Directory.Exists(source)) { messages.Add("替换文件夹不存在。"); } if (string.IsNullOrWhiteSpace(output)) { messages.Add("未选择恢复文件夹。"); } messages.AddRange(_fileScanService.ValidateRestoreDirectories(source, output)); return messages; } private void UpdateActionStates() { var hasReplaceSelection = _replaceItems.Any(item => item.IsSelected); var hasRestoreSelection = _restoreItems.Any(item => item.IsSelected); var busy = _isAutoExtractRunning || _isReplaceRunning || _isRestoreRunning; var canEditReplacePage = !busy; var canEditRestorePage = !busy; var canEditRules = !busy && _gridRules.Enabled; var canExecuteReplace = !busy && hasReplaceSelection && GetReplaceExecutionValidationMessages().Count == 0; var canExecuteRestore = !busy && hasRestoreSelection && GetRestoreExecutionValidationMessages().Count == 0; _btnAutoExtract.Enabled = !_isAutoExtractRunning && !_isReplaceRunning && !_isRestoreRunning && hasReplaceSelection; _btnStartReplace.Enabled = canExecuteReplace; _btnStopReplace.Enabled = _isAutoExtractRunning || _isReplaceRunning; _btnStartRestore.Enabled = canExecuteRestore; _btnStopRestore.Enabled = _isRestoreRunning; _txtReplaceSource.Enabled = canEditReplacePage; _txtReplaceOutput.Enabled = canEditReplacePage; _btnBrowseReplaceSource.Enabled = canEditReplacePage; _btnBrowseReplaceOutput.Enabled = canEditReplacePage; _btnSelectAllReplace.Enabled = canEditReplacePage && _replaceItems.Count > 0; _btnClearReplace.Enabled = canEditReplacePage && _replaceItems.Count > 0; _gridReplace.ReadOnly = busy; _txtRestoreSource.Enabled = canEditRestorePage; _txtRestoreOutput.Enabled = canEditRestorePage; _cmbRestoreDiffInput.Enabled = canEditRestorePage; _btnBrowseRestoreSource.Enabled = canEditRestorePage; _btnBrowseRestoreOutput.Enabled = canEditRestorePage; _btnSelectAllRestore.Enabled = canEditRestorePage && _restoreItems.Count > 0; _btnClearRestore.Enabled = canEditRestorePage && _restoreItems.Count > 0; _gridRestore.ReadOnly = busy; _gridRules.Enabled = canEditRules; _txtRuleFile.Enabled = canEditRules; _btnOpenRules.Enabled = canEditRules; _btnSaveRules.Enabled = canEditRules; _btnUndoAutoExtract.Enabled = canEditRules && _lastAutoExtractRuleIds.Count > 0; } private void SetRuleEditingEnabled(bool enabled) { _gridRules.Enabled = enabled; _txtRuleFile.Enabled = enabled; UpdateActionStates(); } private void LogRuleIssue(RuleValidationIssue issue) { var formatted = _ruleService.FormatIssue(issue); if (issue.Severity == RuleValidationSeverity.Error) { _logService.Error("RULE_CHECK", formatted); } else if (issue.Severity == RuleValidationSeverity.Warning) { _logService.Warning("RULE_CHECK", formatted); } else { _logService.Info("RULE_CHECK", formatted); } } private void SetSelection(IList items, bool isSelected) { foreach (var item in items) { item.IsSelected = isSelected; } UpdateActionStates(); } private void SetSelectedFileItems(DataGridView grid, bool isSelected) { foreach (var item in GetSelectedDataBoundItems(grid)) { item.IsSelected = isSelected; } grid.Refresh(); UpdateActionStates(); } private void DeleteSelectedFileItems(BindingList items, DataGridView grid) { foreach (var item in GetSelectedDataBoundItems(grid)) { items.Remove(item); } UpdateActionStates(); } private void SetSelectedRulesEnabled(bool isEnabled) { foreach (var rule in GetSelectedDataBoundItems(_gridRules)) { rule.IsEnabled = isEnabled; } RefreshRuleState(); } private void DeleteSelectedRules() { foreach (var rule in GetSelectedDataBoundItems(_gridRules)) { _rules.Remove(rule); } RefreshRuleState(); } private void RefreshRuleState() { _ruleFile.Rules = _rules.ToList(); var existingRuleIds = new HashSet( _rules.Where(item => !string.IsNullOrWhiteSpace(item.Id)).Select(item => item.Id), StringComparer.OrdinalIgnoreCase); _lastAutoExtractRuleIds = _lastAutoExtractRuleIds.Where(existingRuleIds.Contains).ToList(); _gridRules.Refresh(); UpdateActionStates(); } private static List GetSelectedDataBoundItems(DataGridView grid) where T : class { return grid.SelectedRows .Cast() .OrderBy(row => row.Index) .Select(row => row.DataBoundItem as T) .Where(item => item != null) .ToList(); } private static List NormalizeIndexes(IEnumerable rowIndexes, int count) { return (rowIndexes ?? Array.Empty()) .Where(index => index >= 0 && index < count) .Distinct() .OrderBy(index => index) .ToList(); } private static void ReplaceBinding(BindingList target, IEnumerable source) { target.Clear(); foreach (var item in source) { target.Add(item); } } private static void ConfigureNumeric(NumericUpDown numeric, int minimum, int maximum) { numeric.Minimum = minimum; numeric.Maximum = maximum; numeric.Width = 120; } private static Control CreateProgressLine(string labelText, ProgressBar progressBar) { var panel = new TableLayoutPanel { Dock = DockStyle.Top, ColumnCount = 2, AutoSize = true, }; panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90F)); panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); panel.Controls.Add(new Label { Text = labelText, Anchor = AnchorStyles.Left, AutoSize = true }, 0, 0); progressBar.Dock = DockStyle.Fill; progressBar.Maximum = 100; panel.Controls.Add(progressBar, 1, 0); return panel; } private static void AddLabeledRow( TableLayoutPanel layout, int rowIndex, string leftLabel, Control leftTextBox, Control leftButton, string rightLabel, Control rightTextBox, Control rightButton) { layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); layout.Controls.Add(new Label { Text = leftLabel, Anchor = AnchorStyles.Left, AutoSize = true }, 0, rowIndex); leftTextBox.Dock = DockStyle.Fill; layout.Controls.Add(leftTextBox, 1, rowIndex); layout.Controls.Add(leftButton, 2, rowIndex); layout.Controls.Add(new Label { Text = rightLabel, Anchor = AnchorStyles.Left, AutoSize = true }, 3, rowIndex); rightTextBox.Dock = DockStyle.Fill; layout.Controls.Add(rightTextBox, 4, rowIndex); layout.Controls.Add(rightButton, 5, rowIndex); } private static void AddSettingRow(TableLayoutPanel layout, int rowIndex, Control leftControl, Control rightControl) { layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); leftControl.Dock = DockStyle.Fill; rightControl.Dock = DockStyle.Fill; layout.Controls.Add(leftControl, 0, rowIndex); layout.Controls.Add(rightControl, 1, rowIndex); } private void ConfigureFileGrid(DataGridView grid, bool isRestoreGrid) { grid.AllowUserToAddRows = false; grid.AllowUserToDeleteRows = false; grid.AutoGenerateColumns = false; grid.MultiSelect = true; grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect; grid.EditMode = DataGridViewEditMode.EditOnEnter; grid.Columns.Clear(); var checkColumn = new DataGridViewCheckBoxColumn { DataPropertyName = nameof(FileScanItem.IsSelected), HeaderText = "勾选", Width = 60, }; grid.Columns.Add(checkColumn); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(FileScanItem.SourcePath), HeaderText = isRestoreGrid ? "替换后文件路径" : "原始文件路径", Width = 360, ReadOnly = true, }); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(FileScanItem.OutputPath), HeaderText = isRestoreGrid ? "恢复文件路径" : "替换后文件路径", Width = 360, ReadOnly = true, }); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(FileScanItem.DiffPath), HeaderText = "差异文件路径", Width = 320, ReadOnly = true, }); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(FileScanItem.TextOperationCount), HeaderText = isRestoreGrid ? "文本恢复次数" : "文本替换次数", Width = 110, }); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(FileScanItem.ImageOperationCount), HeaderText = isRestoreGrid ? "图片恢复次数" : "图片替换次数", Width = 110, }); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(FileScanItem.Status), HeaderText = "当前状态", Width = 90, ReadOnly = true, }); grid.CurrentCellDirtyStateChanged += Grid_CurrentCellDirtyStateChanged; grid.CellValueChanged += GridSelection_CellValueChanged; grid.CellMouseDown += Grid_CellMouseDownSelectRow; grid.RowPrePaint += Grid_RowPrePaint; } private void ConfigureRuleGrid(DataGridView grid) { grid.AllowUserToAddRows = false; grid.AllowUserToDeleteRows = false; grid.AutoGenerateColumns = false; grid.MultiSelect = true; grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect; grid.EditMode = DataGridViewEditMode.EditOnEnter; grid.Columns.Clear(); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.Id), HeaderText = "规则 ID", Width = 110 }); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.Name), HeaderText = "规则名称", Width = 140 }); grid.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(RuleDefinition.IsEnabled), HeaderText = "启用", Width = 60 }); grid.Columns.Add(new DataGridViewComboBoxColumn { DataPropertyName = nameof(RuleDefinition.MatchMode), HeaderText = "匹配模式", Width = 110, DataSource = Enum.GetValues(typeof(RuleMatchMode)), }); grid.Columns.Add(new DataGridViewComboBoxColumn { DataPropertyName = nameof(RuleDefinition.Target), HeaderText = "目标类型", Width = 100, DataSource = Enum.GetValues(typeof(RuleTarget)), ValueType = typeof(RuleTarget), }); grid.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(RuleDefinition.IsCaseSensitive), HeaderText = "区分大小写", Width = 90 }); grid.Columns.Add(new DataGridViewCheckBoxColumn { DataPropertyName = nameof(RuleDefinition.IsWholeWord), HeaderText = "全字匹配", Width = 90 }); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.SearchText), HeaderText = "原文本/表达式", Width = 200 }); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.ReplacementText), HeaderText = "新文本/模板", Width = 180 }); grid.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName = nameof(RuleDefinition.Note), HeaderText = "备注", Width = 320 }); grid.CurrentCellDirtyStateChanged += Grid_CurrentCellDirtyStateChanged; grid.CellValueChanged += GridSelection_CellValueChanged; grid.CellMouseDown += Grid_CellMouseDownSelectRow; grid.DataError += delegate { }; } private void Grid_CurrentCellDirtyStateChanged(object sender, EventArgs e) { var grid = sender as DataGridView; if (grid != null && grid.IsCurrentCellDirty && grid.CurrentCell is DataGridViewCheckBoxCell) { grid.CommitEdit(DataGridViewDataErrorContexts.Commit); } } private void GridSelection_CellValueChanged(object sender, DataGridViewCellEventArgs e) { UpdateActionStates(); } private void Grid_CellMouseDownSelectRow(object sender, DataGridViewCellMouseEventArgs e) { if (e.Button != MouseButtons.Right || e.RowIndex < 0) { return; } var grid = sender as DataGridView; if (grid == null || e.RowIndex >= grid.Rows.Count) { return; } var row = grid.Rows[e.RowIndex]; if (!row.Selected) { grid.ClearSelection(); row.Selected = true; } if (e.ColumnIndex >= 0 && e.ColumnIndex < row.Cells.Count) { grid.CurrentCell = row.Cells[e.ColumnIndex]; } } private void Grid_RowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e) { var grid = sender as DataGridView; if (grid == null || e.RowIndex < 0 || e.RowIndex >= grid.Rows.Count) { return; } var item = grid.Rows[e.RowIndex].DataBoundItem as FileScanItem; if (item == null) { return; } var color = Color.Black; switch (item.Status) { case ProcessingStatus.Success: color = Color.Green; break; case ProcessingStatus.Failed: color = Color.Red; break; case ProcessingStatus.InProgress: color = Color.Blue; break; case ProcessingStatus.Stopped: color = Color.Gray; break; } grid.Rows[e.RowIndex].DefaultCellStyle.ForeColor = color; } private DialogResult ShowMessage(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) { return _dialogService.ShowMessage(this, text, caption, buttons, icon); } internal Task ExecuteReplaceBatchForTestAsync(IList selectedItems, RuleFile executionRuleFile, string sourceRootDirectory, string ruleFilePath, bool encryptDiff, int? fixedImageSeed) { _txtReplaceSource.Text = sourceRootDirectory ?? string.Empty; _txtRuleFile.Text = ruleFilePath ?? string.Empty; _chkJsonDiffEncryption.Checked = encryptDiff; _chkUseFixedImageSeed.Checked = fixedImageSeed.HasValue; _numFixedImageSeed.Value = Math.Max(_numFixedImageSeed.Minimum, Math.Min(_numFixedImageSeed.Maximum, fixedImageSeed ?? 0)); return ExecuteReplaceBatchAsync(selectedItems, executionRuleFile); } internal Task ExecuteRestoreBatchForTestAsync(IList selectedItems, string replaceRootDirectory, string ruleFilePath) { _txtRestoreSource.Text = replaceRootDirectory ?? string.Empty; _txtRuleFile.Text = ruleFilePath ?? string.Empty; return ExecuteRestoreBatchAsync(selectedItems); } internal Task RefreshReplaceScanForTestAsync(string sourceDirectory, string outputDirectory) { _txtReplaceSource.Text = sourceDirectory ?? string.Empty; _txtReplaceOutput.Text = outputDirectory ?? string.Empty; return RefreshReplaceScanAsync(); } internal Task ApplyReplaceSourceSelectionForTestAsync(string selectedPath) { return ApplySelectedFolderPathAsync(_txtReplaceSource, selectedPath, RefreshReplaceScanAsync); } internal Task ApplyReplaceOutputSelectionForTestAsync(string selectedPath) { return ApplySelectedFolderPathAsync(_txtReplaceOutput, selectedPath, RefreshReplaceScanAsync); } internal Task RefreshRestoreScanForTestAsync(string sourceDirectory, string outputDirectory, string diffInputFormat) { _txtRestoreSource.Text = sourceDirectory ?? string.Empty; _txtRestoreOutput.Text = outputDirectory ?? string.Empty; SetRestoreDiffInputFormat(diffInputFormat); return RefreshRestoreScanAsync(); } internal Task ApplyRestoreSourceSelectionForTestAsync(string selectedPath) { return ApplySelectedFolderPathAsync(_txtRestoreSource, selectedPath, RefreshRestoreScanAsync); } internal Task ApplyRestoreOutputSelectionForTestAsync(string selectedPath) { return ApplySelectedFolderPathAsync(_txtRestoreOutput, selectedPath, RefreshRestoreScanAsync); } internal Task SetRestoreDiffInputForTestAsync(string diffInputFormat) { SetRestoreDiffInputFormat(diffInputFormat); return RefreshRestoreScanAsync(); } internal Task RunAutoExtractForTestAsync(IList replaceItems, IList existingRules, AutoExtractSettings settings) { ReplaceBinding(_replaceItems, replaceItems ?? Array.Empty()); ReplaceBinding(_rules, existingRules ?? Array.Empty()); _ruleFile.Rules = _rules.ToList(); if (settings != null) { _config.Settings.AutoExtract = settings; ApplyConfigToUi(); } UpdateActionStates(); return RunAutoExtractAsync(); } internal void RequestStopReplaceForTest() { if (_isReplaceRunning && _replaceExecutionCancellation != null && !_replaceExecutionCancellation.IsCancellationRequested) { _replaceExecutionCancellation.Cancel(); } } internal void RequestStopRestoreForTest() { if (_isRestoreRunning && _restoreExecutionCancellation != null && !_restoreExecutionCancellation.IsCancellationRequested) { _restoreExecutionCancellation.Cancel(); } } internal void RaiseLogWriteFailureForTest(LogWriteFailureEventArgs e) { LogService_WriteFailed(this, e); } internal void UndoAutoExtractForTest() { UndoAutoExtract_Click(this, EventArgs.Empty); } internal void LoadReplaceItemsForTest(IList items) { ReplaceBinding(_replaceItems, items ?? Array.Empty()); UpdateActionStates(); } internal void LoadRestoreItemsForTest(IList items) { ReplaceBinding(_restoreItems, items ?? Array.Empty()); UpdateActionStates(); } internal void LoadRulesForTest(IList rules) { ReplaceBinding(_rules, rules ?? Array.Empty()); RefreshRuleState(); } internal IList LogContextMenuItemsForTest { get { return GetContextMenuTexts(_logBox.ContextMenuStrip); } } internal IList ReplaceContextMenuItemsForTest { get { return GetContextMenuTexts(_gridReplace.ContextMenuStrip); } } internal IList RestoreContextMenuItemsForTest { get { return GetContextMenuTexts(_gridRestore.ContextMenuStrip); } } internal IList RuleContextMenuItemsForTest { get { return GetContextMenuTexts(_gridRules.ContextMenuStrip); } } internal bool IsRuleEditingEnabledForTest { get { return _gridRules.Enabled; } } internal bool IsUndoAutoExtractEnabledForTest { get { return _btnUndoAutoExtract.Enabled; } } internal bool IsAutoExtractEnabledForTest { get { return _btnAutoExtract.Enabled; } } internal bool IsStartReplaceEnabledForTest { get { return _btnStartReplace.Enabled; } } internal bool IsStartRestoreEnabledForTest { get { return _btnStartRestore.Enabled; } } internal bool IsStopReplaceEnabledForTest { get { return _btnStopReplace.Enabled; } } internal bool IsStopRestoreEnabledForTest { get { return _btnStopRestore.Enabled; } } internal string ReplaceRuntimeTextForTest { get { return _lblReplaceRuntime.Text; } } internal string RestoreRuntimeTextForTest { get { return _lblRestoreRuntime.Text; } } internal string RestoreBrowseSourceButtonTextForTest { get { return _btnBrowseRestoreSource.Text; } } internal string RestoreBrowseOutputButtonTextForTest { get { return _btnBrowseRestoreOutput.Text; } } internal string RestoreSelectAllButtonTextForTest { get { return _btnSelectAllRestore.Text; } } internal string RestoreClearButtonTextForTest { get { return _btnClearRestore.Text; } } internal int SettingsRuleListWidthForTest { get { return _settingsRuleListSplitContainer == null ? 0 : _settingsRuleListSplitContainer.ClientSize.Width - _settingsRuleListSplitContainer.SplitterWidth - _settingsRuleListSplitContainer.SplitterDistance; } } internal void ApplySettingsRuleListInitialWidthForTest(int width) { if (_settingsRuleListSplitContainer == null) { throw new InvalidOperationException("设置页规则列表分隔容器尚未初始化。"); } _settingsRuleListInitialWidthApplied = false; _settingsRuleListSplitContainer.Width = width; ApplySettingsRuleListInitialWidth(_settingsRuleListSplitContainer); } internal string SelectedTabTextForTest { get { return _tabControl.SelectedTab == null ? string.Empty : _tabControl.SelectedTab.Text; } } internal int SelectedRuleIndexForTest { get { if (_gridRules.SelectedRows.Count > 0) { return _gridRules.SelectedRows[0].Index; } if (_gridRules.CurrentCell != null) { return _gridRules.CurrentCell.RowIndex; } return -1; } } internal IList RulesForTest { get { return _rules.Select(CloneRule).ToList(); } } internal IList ReplaceItemsForTest { get { return _replaceItems.Select(CloneFileScanItem).ToList(); } } internal IList RestoreItemsForTest { get { return _restoreItems.Select(CloneFileScanItem).ToList(); } } internal string LogTextForTest { get { return _logBox.Text; } } internal string LogCopyTextForTest { get { return GetAllLogText(); } } internal void AppendLogTextForTest(string text) { if (!string.IsNullOrEmpty(text)) { _logBox.AppendText(text); } } internal void ClearLogsForTest() { ClearLogBox(); } internal void SetReplaceItemSelectionForTest(IEnumerable rowIndexes, bool isSelected) { foreach (var index in NormalizeIndexes(rowIndexes, _replaceItems.Count)) { _replaceItems[index].IsSelected = isSelected; } UpdateActionStates(); } internal void DeleteReplaceItemsForTest(IEnumerable rowIndexes) { foreach (var index in NormalizeIndexes(rowIndexes, _replaceItems.Count).OrderByDescending(item => item)) { _replaceItems.RemoveAt(index); } UpdateActionStates(); } internal void SetRestoreItemSelectionForTest(IEnumerable rowIndexes, bool isSelected) { foreach (var index in NormalizeIndexes(rowIndexes, _restoreItems.Count)) { _restoreItems[index].IsSelected = isSelected; } UpdateActionStates(); } internal void DeleteRestoreItemsForTest(IEnumerable rowIndexes) { foreach (var index in NormalizeIndexes(rowIndexes, _restoreItems.Count).OrderByDescending(item => item)) { _restoreItems.RemoveAt(index); } UpdateActionStates(); } internal void SetRuleEnabledForTest(IEnumerable rowIndexes, bool isEnabled) { foreach (var index in NormalizeIndexes(rowIndexes, _rules.Count)) { _rules[index].IsEnabled = isEnabled; } RefreshRuleState(); } internal void DeleteRulesForTest(IEnumerable rowIndexes) { foreach (var index in NormalizeIndexes(rowIndexes, _rules.Count).OrderByDescending(item => item)) { _rules.RemoveAt(index); } RefreshRuleState(); } private static FileScanItem CloneFileScanItem(FileScanItem source) { if (source == null) { return new FileScanItem(); } return new FileScanItem { IsSelected = source.IsSelected, SourcePath = source.SourcePath, OutputPath = source.OutputPath, DiffPath = source.DiffPath, TextOperationCount = source.TextOperationCount, ImageOperationCount = source.ImageOperationCount, Status = source.Status, }; } private static RuleDefinition CloneRule(RuleDefinition source) { if (source == null) { return new RuleDefinition(); } return new RuleDefinition { Id = source.Id, Name = source.Name, IsEnabled = source.IsEnabled, MatchMode = source.MatchMode, IsCaseSensitive = source.IsCaseSensitive, IsWholeWord = source.IsWholeWord, SearchText = source.SearchText, ReplacementText = source.ReplacementText, Note = source.Note, }; } private static List GetContextMenuTexts(ContextMenuStrip contextMenu) { if (contextMenu == null) { return new List(); } return contextMenu.Items.Cast().Select(item => item.Text).ToList(); } } }