Przeglądaj źródła

perf(ShowVisionRender): 图像更新与保存完全异步化,消除 UI 卡顿

- Subscribe 改为 ThreadOption.BackgroundThread,Publish() 立即返回
- UpdateRenderModuleSource 在后台线程深拷贝 ICogImage,防止像素数据被覆盖
- 提取 RequestSaveImage 方法,CreateContentBitmap + 文件 I/O 全部 fire-and-forget
- SaveImage → SaveImageAsync:移除嵌套 Task.Run,传播 CancellationToken
- 添加 _saveImageCts / _saveSemaphore / Unloaded 资源清理
- ViewModel_UpdateLayout 重置 CancellationTokenSource
赵留洋 2 miesięcy temu
rodzic
commit
cf5f704eb3
1 zmienionych plików z 346 dodań i 256 usunięć
  1. 346 256
      TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs

+ 346 - 256
TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs

@@ -9,6 +9,7 @@ using System;
 using System.Collections.Generic;
 using System.Drawing;
 using System.IO;
+using System.Threading;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
@@ -44,6 +45,10 @@ namespace TeamAAS_VP.Views.Home
         private readonly IProductService _productService;
         private readonly Prism.Services.Dialogs.IDialogService _dialogService;
 
+        // ====== 异步保存控制 ======
+        private CancellationTokenSource _saveImageCts = new CancellationTokenSource();
+        private readonly SemaphoreSlim _saveSemaphore = new SemaphoreSlim(5, 5);
+
         public ShowVisionRender()
         {
             InitializeComponent();
@@ -57,8 +62,22 @@ namespace TeamAAS_VP.Views.Home
             _dialogService = ((Prism.PrismApplicationBase)App.Current).Container.Resolve<Prism.Services.Dialogs.IDialogService>();
 
             viewModel.UpdateLayout += ViewModel_UpdateLayout;
-            _eventAggregator.GetEvent<RenderUpdateNotification>().Subscribe(UpdateRenderModuleSource);
+            // ThreadOption.BackgroundThread:Publish() 立即返回,图像处理不等待 UI 更新和文件 I/O
+            _eventAggregator.GetEvent<RenderUpdateNotification>().Subscribe(UpdateRenderModuleSource, ThreadOption.BackgroundThread);
             _eventAggregator.GetEvent<ProductChangedNotification>().Subscribe(ProductChanged);
+
+            // 控件卸载时取消所有保存任务,防止页面关闭后仍在写文件导致闪退
+            this.Unloaded += ShowVisionRender_Unloaded;
+        }
+
+        private void ShowVisionRender_Unloaded(object sender, RoutedEventArgs e)
+        {
+            try
+            {
+                _saveImageCts.Cancel();
+                _saveImageCts.Dispose();
+            }
+            catch { }
         }
 
         private bool GetView3DUse()
@@ -82,103 +101,158 @@ namespace TeamAAS_VP.Views.Home
             }
         }
 
+        /// <summary>
+        /// 更新 2D/3D 显示并触发异步保存。
+        /// 运行在后台线程(ThreadOption.BackgroundThread),不阻塞图像处理主流程。
+        /// </summary>
         private void UpdateRenderModuleSource(ShowRender render)
         {
-            //foreach (var item in vmRenders)
-            //{
-            //    if (item.Key== render.Id)
-            //    {
-            //        App.Current.Dispatcher.Invoke(new Action(() =>
-            //        {
-            //            item.Value.ModuleSource = render.ModuleSource;
-            //        }));
-            //        break;
-            //    }
-            //}
-            this.gridRender.Dispatcher.BeginInvoke(new Action(() =>
-            {
-                if (render == null)
-                    return;
-
-                if (render.Is3DModel == true && GetView3DUse()==true)
-                {
-                    //if (!GetView3DUse())
-                    //    return;
+            if (render == null) return;
 
-                    if (!vmRenders.TryGetValue(Get3DKey(render.Id), out var host))
-                        return;
-
-                    var display = host.Child as Cognex.VisionPro3D.Cog3DDisplayV2WF;
-                    if (display == null)
-                        return;
+            // 【关键】在后台线程深拷贝 ICogImage,防止下一次 ToolBlock.Run() 覆盖像素数据
+            ICogImage imageCopy = null;
+            try
+            {
+                imageCopy = render.Image?.CopyBase(CogImageCopyModeConstants.CopyPixels);
+            }
+            catch (Exception ex)
+            {
+                LogHelper.WriteLogError("深拷贝 ICogImage 失败", ex);
+            }
 
-                    try
-                    {
-                        display.Clear();
+            var token = _saveImageCts.Token;
 
-                        if (render.RangeImage == null || render.GreyImage == null)
-                            return;
+            // 派发 UI 更新到 WPF UI 线程(fire-and-forget)
+            this.gridRender.Dispatcher.BeginInvoke(new Action(() =>
+            {
+                try
+                {
+                    if (token.IsCancellationRequested) return;
 
-                        var rImgG = new Cog3DRangeImageGraphic(render.RangeImage, render.GreyImage);
-                        display.Add(rImgG, "__default");
-                        display.FitView();
-                    }
-                    catch (System.Windows.Forms.AxHost.InvalidActiveXStateException)
+                    // ==== 3D 显示 ====
+                    bool is3D = render.Is3DModel && GetView3DUse();
+                    if (is3D)
                     {
+                        if (vmRenders.TryGetValue(Get3DKey(render.Id), out var host3D))
+                        {
+                            if (host3D.Child is Cognex.VisionPro3D.Cog3DDisplayV2WF display3D)
+                            {
+                                try
+                                {
+                                    display3D.Clear();
+                                    if (render.RangeImage != null && render.GreyImage != null)
+                                    {
+                                        var rImgG = new Cog3DRangeImageGraphic(render.RangeImage, render.GreyImage);
+                                        display3D.Add(rImgG, "__default");
+                                        display3D.FitView();
+                                    }
+                                }
+                                catch (System.Windows.Forms.AxHost.InvalidActiveXStateException) { }
+                            }
+                        }
                     }
 
-                    //return;
-                }
-
-                
-                if (!vmRenders.TryGetValue(Get2DKey(render.Id), out var host2D))
-                    return;
+                    // ==== 2D 显示 ====
+                    if (!vmRenders.TryGetValue(Get2DKey(render.Id), out var host2D))
+                        return;
 
-                var recordDisplay = host2D.Child as Cognex.VisionPro.CogRecordDisplay;
-                if (recordDisplay == null)
-                    return;
+                    if (!(host2D.Child is CogRecordDisplay recordDisplay))
+                        return;
 
-                try
-                {
                     recordDisplay.Record = null;
-                    recordDisplay.Image = render.Image;
-                    recordDisplay.StaticGraphics.Clear();
+                    recordDisplay.Image = imageCopy;            // 深拷贝后的图像
 
+                    recordDisplay.StaticGraphics.Clear();
                     if (render.Graphic != null)
                         recordDisplay.StaticGraphics.AddList(render.Graphic, "");
 
                     if (render.Record != null)
                         recordDisplay.Record = render.Record;
+
+                    // ==== 异步保存(fire-and-forget,不阻塞 UI 线程) ====
                     if (render.IsSaveImage)
                     {
-                        Bitmap bitmap = null;
+                        RequestSaveImage(recordDisplay, imageCopy, render, token);
+                    }
+                }
+                catch (System.Windows.Forms.AxHost.InvalidActiveXStateException) { }
+            }));
+        }
+
+        /// <summary>
+        /// 在 WinForms 控件线程上截图,然后后台异步保存。
+        /// </summary>
+        private void RequestSaveImage(
+            CogRecordDisplay recordDisplay,
+            ICogImage imageCopy,
+            ShowRender render,
+            CancellationToken token)
+        {
+            recordDisplay.BeginInvoke(new Action(() =>
+            {
+                Bitmap clonedBitmap = null;
+                try
+                {
+                    if (token.IsCancellationRequested) return;
+                    if (recordDisplay.IsDisposed || !recordDisplay.IsHandleCreated) return;
+
+                    Bitmap uiBitmap = (Bitmap)recordDisplay.CreateContentBitmap(
+                        Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
+                    clonedBitmap = (Bitmap)uiBitmap.Clone();
+                    uiBitmap.Dispose();
+
+                    var capturedBitmap = clonedBitmap;
+                    var capturedRender = render;
+                    var capturedImage = imageCopy;
 
+                    Task.Run(async () =>
+                    {
+                        await _saveSemaphore.WaitAsync(token);
                         try
                         {
-                            bitmap = (Bitmap)recordDisplay.CreateContentBitmap(Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
-
-                            SaveImage(
-                                render.SaveImageModel,
-                                render.SaveImagePathModel,
-                                render.SavePath,
-                                render.Image,
-                                bitmap,
-                                render.Result,
-                                render.IsCompress,
-                                render.ImageFileName,
-                                render.Is3DModel
+                            if (token.IsCancellationRequested) return;
+
+                            if (string.IsNullOrEmpty(_productService.CurrentProductCode))
+                            {
+                                _eventAggregator.GetEvent<TaskMessageNotification>()
+                                    .Publish(new Models.MessageStruct()
+                                    {
+                                        Message = "当前产品SN不存在,无法保存图片!",
+                                        level = MessageLevel.Alarm
+                                    });
+                                return;
+                            }
+
+                            LogHelper.WriteLogInfo("开始异步保存图像");
+                            await SaveImageAsync(
+                                capturedRender.SaveImageModel,
+                                capturedRender.SaveImagePathModel,
+                                capturedRender.SavePath,
+                                capturedImage,
+                                capturedBitmap,
+                                capturedRender.Result,
+                                capturedRender.IsCompress,
+                                capturedRender.ImageFileName,
+                                capturedRender.Is3DModel,
+                                token
                             );
                         }
-                        catch
+                        catch (OperationCanceledException) { }
+                        catch (Exception ex)
                         {
-                            try { bitmap?.Dispose(); } catch { }
-                            throw;
+                            LogHelper.WriteLogError("后台保存图像任务出错!", ex);
                         }
-                    }
-
+                        finally
+                        {
+                            try { capturedBitmap?.Dispose(); } catch { }
+                            _saveSemaphore.Release();
+                        }
+                    }, token);
                 }
-                catch (System.Windows.Forms.AxHost.InvalidActiveXStateException)
+                catch (Exception ex)
                 {
+                    LogHelper.WriteLogError("RequestSaveImage(UI截图阶段) 出错!", ex);
+                    try { clonedBitmap?.Dispose(); } catch { }
                 }
             }));
         }
@@ -296,6 +370,15 @@ namespace TeamAAS_VP.Views.Home
         Grid grid20 = new Grid();
         private void ViewModel_UpdateLayout(System.Collections.ObjectModel.ObservableCollection<Models.ShowRender> obj)
         {
+            // 布局刷新前取消旧保存任务
+            try
+            {
+                _saveImageCts.Cancel();
+                _saveImageCts.Dispose();
+            }
+            catch { }
+            _saveImageCts = new CancellationTokenSource();
+
             System.Windows.Media.FontFamily font = Application.Current.Resources["DefaultFont"] as System.Windows.Media.FontFamily;
             
             if (vmRenders.Count > 0)
@@ -636,7 +719,20 @@ namespace TeamAAS_VP.Views.Home
         /// <summary>
         /// 保存图像(保持你原有逻辑不变,补充 Dispose 兜底,防止压缩分支不释放导致闪退)
         /// </summary>
-        private void SaveImage(ProcedureSaveImageModel saveImageModel, ProcedureSaveImagePathModel saveImagePathModel, string path, ICogImage image, Bitmap reimage, bool result, bool isCompress, string imageFileName,bool is3Dmodel)
+        /// <summary>
+        /// 异步保存图像(调用者已在后台线程,不再嵌套 Task.Run)
+        /// </summary>
+        private async Task SaveImageAsync(
+            ProcedureSaveImageModel saveImageModel,
+            ProcedureSaveImagePathModel saveImagePathModel,
+            string path,
+            ICogImage image,
+            Bitmap reimage,
+            bool result,
+            bool isCompress,
+            string imageFileName,
+            bool is3Dmodel,
+            CancellationToken cancellationToken)
         {
             if (string.IsNullOrEmpty(path) || string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path))
             {
@@ -645,236 +741,230 @@ namespace TeamAAS_VP.Views.Home
                 return;
             }
 
-            Task.Run(() =>
+            try
             {
-                try
+                cancellationToken.ThrowIfCancellationRequested();
+
+                DateTime now = DateTime.Now;
+                string imgName = now.ToString("yyyy-MM-dd HH_mm_ss");
+                if (!string.IsNullOrEmpty(imageFileName))
+                {
+                    imgName = imageFileName;
+                }
+
+                if (saveImageModel == ProcedureSaveImageModel.Original)
                 {
-                    DateTime now = DateTime.Now;
-                    //图片名
-                    string imgName = now.ToString("yyyy-MM-dd HH_mm_ss");
-                    if (!string.IsNullOrEmpty(imageFileName))
+                    string Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
+
+                    if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_OkNG_Time)
                     {
-                        imgName = imageFileName;
+                        Originalpath = result
+                            ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp"
+                            : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
                     }
-
-                    if (saveImageModel == ProcedureSaveImageModel.Original)
+                    else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ok_Time)
+                    {
+                        if (result)
+                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
+                        else
+                            return;
+                    }
+                    else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ng_Time)
                     {
-                        string Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
+                        if (!result)
+                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
+                        else
+                            return;
+                    }
+                    else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Formula_ProductSN_Time)
+                    {
+                        string formulaName = _productService.GetCurrentProduct().Name;
+                        foreach (char c in Path.GetInvalidFileNameChars())
+                            formulaName = formulaName.Replace(c, '_');
 
-                        if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_OkNG_Time)
-                        {
-                            Originalpath = result
-                                ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp"
-                                : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
-                        }
-                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ok_Time)
-                        {
-                            if (result)
-                                Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
-                            else
-                                return;
-                        }
-                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ng_Time)
-                        {
-                            if (!result)
-                                Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
-                            else
-                                return;
-                        }
-                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Formula_ProductSN_Time)
-                        {
-                            // 当前产品配方名(过滤非法字符)
-                            string formulaName = _productService.GetCurrentProduct().Name;
-                            foreach (char c in Path.GetInvalidFileNameChars())
-                                formulaName = formulaName.Replace(c, '_');
+                        string productSN = _productService.CurrentProductCode;
+                        foreach (char c in Path.GetInvalidFileNameChars())
+                            productSN = productSN.Replace(c, '_');
 
-                            // 产品SN(过滤非法字符)
-                            string productSN = _productService.CurrentProductCode;
-                            foreach (char c in Path.GetInvalidFileNameChars())
-                                productSN = productSN.Replace(c, '_');
+                        string resultStr = result ? "OK" : "NG";
+                        Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp";
+                    }
 
-                            string resultStr = result ? "OK" : "NG";
-                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp";
-                        }
+                    cancellationToken.ThrowIfCancellationRequested();
 
-                        if (!Directory.Exists(Path.GetDirectoryName(Originalpath)))
-                            Directory.CreateDirectory(Path.GetDirectoryName(Originalpath));
+                    if (!Directory.Exists(Path.GetDirectoryName(Originalpath)))
+                        Directory.CreateDirectory(Path.GetDirectoryName(Originalpath));
 
-                        using (CogImageFileBMP _cogImageFileTool = new CogImageFileBMP())
-                        {
-                            _cogImageFileTool.Open(Originalpath, CogImageFileModeConstants.Write);
-                            _cogImageFileTool.Append(image);
-                            _cogImageFileTool.Close();
-                        }
+                    using (CogImageFileBMP _cogImageFileTool = new CogImageFileBMP())
+                    {
+                        _cogImageFileTool.Open(Originalpath, CogImageFileModeConstants.Write);
+                        _cogImageFileTool.Append(image);
+                        _cogImageFileTool.Close();
+                    }
+                }
+                else if (saveImageModel == ProcedureSaveImageModel.Recorded)
+                {
+                    string Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
+
+                    if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_OkNG_Time)
+                    {
+                        Recordedpath = result
+                            ? $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp"
+                            : $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
                     }
-                    else if (saveImageModel == ProcedureSaveImageModel.Recorded)
+                    else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ok_Time)
                     {
-                        string Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
+                        if (result)
+                            Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
+                        else
+                            return;
+                    }
+                    else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ng_Time)
+                    {
+                        if (!result)
+                            Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
+                        else
+                            return;
+                    }
+                    else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Formula_ProductSN_Time)
+                    {
+                        string formulaName = _productService.GetCurrentProduct().Name;
+                        foreach (char c in Path.GetInvalidFileNameChars())
+                            formulaName = formulaName.Replace(c, '_');
 
-                        if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_OkNG_Time)
-                        {
-                            Recordedpath = result
-                                ? $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp"
-                                : $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
-                        }
-                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ok_Time)
-                        {
-                            if (result)
-                                Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
-                            else
-                                return;
-                        }
-                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ng_Time)
-                        {
-                            if (!result)
-                                Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
-                            else
-                                return;
-                        }
-                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Formula_ProductSN_Time)
-                        {
-                            string formulaName = _productService.GetCurrentProduct().Name;
-                            foreach (char c in Path.GetInvalidFileNameChars())
-                                formulaName = formulaName.Replace(c, '_');
+                        string productSN = _productService.CurrentProductCode;
+                        foreach (char c in Path.GetInvalidFileNameChars())
+                            productSN = productSN.Replace(c, '_');
 
-                            string productSN = _productService.CurrentProductCode;
-                            foreach (char c in Path.GetInvalidFileNameChars())
-                                productSN = productSN.Replace(c, '_');
+                        string resultStr = result ? "OK" : "NG";
+                        Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp";
+                    }
 
-                            string resultStr = result ? "OK" : "NG";
-                            Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp";
-                        }
+                    cancellationToken.ThrowIfCancellationRequested();
+
+                    if (!Directory.Exists(Path.GetDirectoryName(Recordedpath)))
+                        Directory.CreateDirectory(Path.GetDirectoryName(Recordedpath));
+
+                    if (isCompress)
+                    {
+                        ImageHelper.CompressImage(reimage, Recordedpath, 20);
+                    }
+                    else
+                    {
+                        reimage.Save(Recordedpath);
+                    }
+                }
+                else if (saveImageModel == ProcedureSaveImageModel.OriginalAndRecorded)
+                {
+                    string Originalpath = is3Dmodel
+                        ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.idb"
+                        : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
 
-                        if (!Directory.Exists(Path.GetDirectoryName(Recordedpath)))
-                            Directory.CreateDirectory(Path.GetDirectoryName(Recordedpath));
+                    string Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
 
-                        if (isCompress)
+                    if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_OkNG_Time)
+                    {
+                        if (result)
                         {
-                            ImageHelper.CompressImage(reimage, Recordedpath, 20);
+                            Originalpath = is3Dmodel
+                                ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.idb"
+                                : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
+                            Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
                         }
                         else
                         {
-                            reimage.Save(Recordedpath);
+                            Originalpath = is3Dmodel
+                                ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.idb"
+                                : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
+                            Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
                         }
                     }
-                    else if (saveImageModel == ProcedureSaveImageModel.OriginalAndRecorded)
+                    else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ok_Time)
                     {
-                        string Originalpath = "";
-                        if (is3Dmodel==true)
+                        if (result)
                         {
-                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.idb";
+                            Originalpath = is3Dmodel
+                                ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.idb"
+                                : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
+                            Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
                         }
                         else
                         {
-                            Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
-                        }
-                        string Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
-
-                        if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_OkNG_Time)
-                        {
-                            if (result)
-                            {
-                                Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
-                                Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
-                            }
-                            else
-                            {
-                                Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
-                                Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
-                            }
+                            return;
                         }
-                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ok_Time)
+                    }
+                    else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ng_Time)
+                    {
+                        if (!result)
                         {
-                            if (result)
-                            {
-                                Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
-                                Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp";
-                            }
-                            else
-                            {
-                                return;
-                            }
+                            Originalpath = is3Dmodel
+                                ? $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.idb"
+                                : $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
+                            Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
                         }
-                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ng_Time)
+                        else
                         {
-                            if (!result)
-                            {
-                                Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
-                                Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp";
-                            }
-                            else
-                            {
-                                return;
-                            }
+                            return;
                         }
-                        else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Formula_ProductSN_Time)
+                    }
+                    else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Formula_ProductSN_Time)
+                    {
+                        string formulaName = _productService.GetCurrentProduct().Name;
+                        foreach (char c in Path.GetInvalidFileNameChars())
+                            formulaName = formulaName.Replace(c, '_');
+
+                        string productSN = _productService.CurrentProductCode;
+                        if (productSN != null)
                         {
-                            string formulaName = _productService.GetCurrentProduct().Name;
                             foreach (char c in Path.GetInvalidFileNameChars())
-                                formulaName = formulaName.Replace(c, '_');
+                                productSN = productSN.Replace(c, '_');
+                        }
 
-                            string productSN = _productService.CurrentProductCode;
-                            if(productSN!=null )
-                            {
-                                foreach (char c in Path.GetInvalidFileNameChars())
-                                    productSN = productSN.Replace(c, '_');
-                            }
-                            
+                        string resultStr = result ? "OK" : "NG";
+                        string ext = is3Dmodel ? ".idb" : ".bmp";
+                        Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}{ext}";
+                        Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp";
+                    }
 
-                            string resultStr = result ? "OK" : "NG";
-                            if (is3Dmodel == true)
-                            {
-                                Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.idb";
-                                //Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.idb";
-                            }
-                            else
-                            {
-                                Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp";
-                                //Originalpath = $"{path}\\Original\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp";
-                            }
-                            Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp";
-                        }
+                    cancellationToken.ThrowIfCancellationRequested();
 
-                        if (!Directory.Exists(Path.GetDirectoryName(Originalpath)))
-                            Directory.CreateDirectory(Path.GetDirectoryName(Originalpath));
+                    if (!Directory.Exists(Path.GetDirectoryName(Originalpath)))
+                        Directory.CreateDirectory(Path.GetDirectoryName(Originalpath));
 
-                        //using (CogImageFileBMP _cogImageFileTool = new CogImageFileBMP())
-                        //{
-                        //    _cogImageFileTool.Open(Originalpath, CogImageFileModeConstants.Write);
-                        //    _cogImageFileTool.Append(image);
-                        //    _cogImageFileTool.Close();
-                        //}
+                    using (CogImageFile _cogImageFileTool = new CogImageFile())
+                    {
+                        _cogImageFileTool.Open(Originalpath, CogImageFileModeConstants.Write);
+                        _cogImageFileTool.Append(image);
+                        _cogImageFileTool.Close();
+                    }
 
-                        using (CogImageFile _cogImageFileTool = new CogImageFile())
-                        {
-                            _cogImageFileTool.Open(Originalpath, CogImageFileModeConstants.Write);
-                            _cogImageFileTool.Append(image);
-                            _cogImageFileTool.Close();
-                        }
+                    cancellationToken.ThrowIfCancellationRequested();
 
-                        if (!Directory.Exists(Path.GetDirectoryName(Recordedpath)))
-                            Directory.CreateDirectory(Path.GetDirectoryName(Recordedpath));
+                    if (!Directory.Exists(Path.GetDirectoryName(Recordedpath)))
+                        Directory.CreateDirectory(Path.GetDirectoryName(Recordedpath));
 
-                        if (isCompress)
-                        {
-                            ImageHelper.CompressImage(reimage, Recordedpath, 20);
-                        }
-                        else
-                        {
-                            reimage.Save(Recordedpath);
-                        }
+                    if (isCompress)
+                    {
+                        ImageHelper.CompressImage(reimage, Recordedpath, 20);
+                    }
+                    else
+                    {
+                        reimage.Save(Recordedpath);
                     }
                 }
-                catch (Exception ex)
-                {
-                    LogHelper.WriteLogError("保存流程运行结果的图片时出错!", ex);
-                }
-                finally
-                {
-                    // ======================= 【关键修复】无论是否压缩都释放Bitmap =======================
-                    try { reimage?.Dispose(); } catch { }
-                }
-            });
+            }
+            catch (OperationCanceledException)
+            {
+                // 正常取消,不记录日志
+            }
+            catch (Exception ex)
+            {
+                LogHelper.WriteLogError("保存流程运行结果的图片时出错!", ex);
+            }
+            finally
+            {
+                try { reimage?.Dispose(); } catch { }
+            }
         }
 
         private void ProductChanged(ProductModel product)