using Cognex.VisionPro; using Cognex.VisionPro.ImageFile; using Prism.Events; using Prism.Ioc; 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; using System.Windows.Forms.Integration; using TeamAAS_VP.Core; using TeamAAS_VP.Enums; using TeamAAS_VP.Events; using TeamAAS_VP.Interfaces; using TeamAAS_VP.Models; using TeamAAS_VP.Resources.Languages; using TeamAAS_VP.ViewModels.Home; namespace TeamAAS_VP.Views.Home { /// /// Interaction logic for ShowVisionRender /// public partial class ShowVisionRender : UserControl { private ShowVisionRenderViewModel viewModel; // key=ShowRender.Id private readonly Dictionary vmRenders; private readonly Dictionary Titles; private readonly IEventAggregator _eventAggregator; private readonly ISystemDatabaseService _systemDatabaseService; private readonly IProductService _productService; private List delaySaveImages = new List(); // ======================= 【新增】保存任务控制:防闪退/防资源泄露 ======================= private CancellationTokenSource _saveImageCts = new CancellationTokenSource(); private readonly SemaphoreSlim _saveSemaphore = new SemaphoreSlim(2, 2); public ShowVisionRender() { InitializeComponent(); vmRenders = new Dictionary(); Titles = new Dictionary(); viewModel = DataContext as ShowVisionRenderViewModel; _eventAggregator = viewModel._eventAggregator; _systemDatabaseService = ((Prism.PrismApplicationBase)App.Current).Container.Resolve(); _productService = ((Prism.PrismApplicationBase)App.Current).Container.Resolve(); viewModel.UpdateLayout += ViewModel_UpdateLayout; _eventAggregator.GetEvent().Subscribe(UpdateRenderModuleSource); _eventAggregator.GetEvent().Subscribe(ProductChanged); _eventAggregator.GetEvent().Subscribe(UpdateDelaySaveImage); // 【可选】控件卸载时取消所有保存任务,避免页面关闭后仍在截图/保存导致闪退 this.Unloaded += ShowVisionRender_Unloaded; } private void ShowVisionRender_Unloaded(object sender, RoutedEventArgs e) { try { _saveImageCts.Cancel(); _saveImageCts.Dispose(); } catch { } } /// /// 更新某个工位/模块的显示内容(图像/图形/Record/保存) /// private void UpdateRenderModuleSource(ShowRender render) { this.gridRender.Dispatcher.BeginInvoke(new Action(() => { foreach (var item in vmRenders) { if (item.Value?.Child is CogRecordDisplay disp) { // 通过 Tag(产品名) 匹配当前需要更新的窗口 if (disp.Tag != null && disp.Tag.ToString() == render.ProceductName) { disp.Record = null; disp.Image = render.Image; disp.StaticGraphics.Clear(); if (render.Graphic != null) disp.StaticGraphics.AddList(render.Graphic, ""); if (render.Record != null) disp.Record = render.Record; // ======================= 【关键优化】保存图像走安全入口 ======================= RequestSaveImage(render, disp); break; } } } })); } /// /// 【核心】根据 obj.Count 自动生成 1~16 个显示格子(标题+CogRecordDisplay) /// private async void ViewModel_UpdateLayout(System.Collections.ObjectModel.ObservableCollection obj) { // ======================= 【关键优化】布局刷新前先取消旧保存任务 ======================= try { _saveImageCts.Cancel(); _saveImageCts.Dispose(); } catch { } _saveImageCts = new CancellationTokenSource(); System.Windows.Media.FontFamily font = Application.Current.Resources["DefaultFont"] as System.Windows.Media.FontFamily; // 1) 清理旧控件 if (vmRenders.Count > 0) { foreach (var render in vmRenders) { // WindowsFormsHost.Dispose() 会销毁 WinForms 句柄,防止资源泄漏 render.Value?.Dispose(); } } vmRenders.Clear(); Titles.Clear(); gridRender.Children.Clear(); gridRender.RowDefinitions.Clear(); gridRender.ColumnDefinitions.Clear(); // 2) 判空 if (obj == null || obj.Count == 0) return; // 3) 限制最大 16(防止越界) int count = Math.Min(obj.Count, 16); // 4) 计算行列(最大 4*4) GetGridSize(count, out int rows, out int cols); // 5) 创建主Grid的行列 for (int r = 0; r < rows; r++) gridRender.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) }); for (int c = 0; c < cols; c++) gridRender.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) }); gridRender.ShowGridLines = true; // 6) 逐个创建子格子(每个子格子两行:标题/画面) for (int i = 0; i < count; i++) { var sr = obj[i]; // (1) 创建显示控件(WindowsFormsHost + CogRecordDisplay) var disp = new CogRecordDisplay() { Tag = sr.ProceductName, }; disp.HandleCreated += (s, e) => { try { ConfigureCogDisplay(disp); // 句柄创建后再配置,最稳 // 绑定 WinForms 双击事件 //disp.MouseDoubleClick += (r, w) => //{ // try // { // OnDisplayDoubleClick(disp); // } // catch (Exception ex) // { // LogHelper.WriteLogError("双击预览弹窗出错", ex); // } //}; } catch (Exception ex) { LogHelper.WriteLogError("ConfigureCogDisplay 失败", ex); } }; var host = new WindowsFormsHost() { Tag = sr.ProceductName, Child = disp }; vmRenders.Add(sr.Id, host); // (2) 创建标题 var title = new TextBlock() { Text = sr.ProceductName, HorizontalAlignment = System.Windows.HorizontalAlignment.Center, FontWeight = FontWeights.Bold, FontFamily = font, FontSize = 12, Foreground = System.Windows.Media.Brushes.Black }; Titles.Add(sr.Id, title); // (3) 子Grid(标题 + 显示) var cell = new Grid(); cell.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) }); // 标题 cell.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) }); // 图像 cell.Children.Add(title); cell.Children.Add(host); Grid.SetRow(title, 0); Grid.SetColumn(title, 0); Grid.SetRow(host, 1); Grid.SetColumn(host, 0); // (4) 放到主 gridRender 中 int row = i / cols; int col = i % cols; gridRender.Children.Add(cell); Grid.SetRow(cell, row); Grid.SetColumn(cell, col); } } /// /// 根据数量返回最合适的 rows/cols(最大 4*4) /// private void GetGridSize(int count, out int rows, out int cols) { rows = 1; cols = 1; if (count <= 1) { rows = 1; cols = 1; return; } if (count <= 2) { rows = 1; cols = 2; return; } if (count <= 4) { rows = 2; cols = 2; return; } if (count <= 6) { rows = 2; cols = 3; return; } if (count <= 9) { rows = 3; cols = 3; return; } if (count <= 12) { rows = 3; cols = 4; return; } rows = 4; cols = 4; } /// /// 统一配置 VisionPro 显示属性 /// private void ConfigureCogDisplay(CogRecordDisplay disp) { disp.HorizontalScrollBar = false; disp.VerticalScrollBar = false; disp.AutoFit = true; disp.AutoFitWithGraphics = true; disp.BackColor = System.Drawing.SystemColors.ActiveCaption; } // ======================= 【新增】安全保存入口:解决闪退 ======================= private void RequestSaveImage(ShowRender render, CogRecordDisplay disp) { if (render == null) return; if (disp == null) return; if (!render.IsSaveImage) return; // Original 模式不需要 CreateContentBitmap(你的 SaveImage 里 Original 只写 ICogImage) bool needRecordedBitmap = render.SaveImageModel == ProcedureSaveImageModel.Recorded || render.SaveImageModel == ProcedureSaveImageModel.OriginalAndRecorded; _saveImageCts = new CancellationTokenSource(); var token = _saveImageCts.Token; // 必须在控件的 UI 线程上执行 CreateContentBitmap disp.BeginInvoke(new Action(() => { Bitmap clonedBitmap = null; try { token.ThrowIfCancellationRequested(); if (needRecordedBitmap) { // UI线程创建 Bitmap uiBitmap = (Bitmap)disp.CreateContentBitmap( Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom); // 立刻 Clone,后台线程只用 Clone(防止控件刷新/释放导致崩溃) clonedBitmap = (Bitmap)uiBitmap.Clone(); // UI线程立刻释放 uiBitmap.Dispose(); } // 后台保存(限并发,防止GDI/内存瞬时飙升) Task.Run(async () => { await _saveSemaphore.WaitAsync(token); try { token.ThrowIfCancellationRequested(); if (render.IsDelaySaveImage)// 延迟保存(适合你那边后续有处理的场景) { // 创建延迟保存项,转移资源所有权 var delayItem = new DelaySaveImage() { SaveImageModel = render.SaveImageModel, SaveImagePathModel = render.SaveImagePathModel, SavePath = render.SavePath, Image = render.Image?.CopyBase(CogImageCopyModeConstants.CopyPixels), // 深拷贝ICogImage clonedBitmap = (Bitmap)clonedBitmap.Clone(), // 转移所有权 Result = render.Result, IsCompress = render.IsCompress, ImageFileName = render.ImageFileName }; delaySaveImages.Add(delayItem); } else { if (string.IsNullOrEmpty(_productService.CurrentProductCode)) { _eventAggregator.GetEvent() .Publish(new Models.MessageStruct() { Message = $"当前产品SN不存在,无法保存图片!", level = MessageLevel.Alarm }); _saveSemaphore.Release(); return; } SaveImage( render.SaveImageModel, render.SaveImagePathModel, render.SavePath, render.Image, clonedBitmap, render.Result, render.IsCompress, render.ImageFileName ); } } catch (OperationCanceledException) { // 取消属于正常情况,不记录 } catch (Exception ex) { LogHelper.WriteLogError("后台保存图像任务出错!", ex); } finally { // 确保释放 Clone 的 Bitmap,避免 GDI 泄漏导致闪退 //try { clonedBitmap?.Dispose(); } catch { } _saveSemaphore.Release(); } }, token); } catch (OperationCanceledException) { //try { clonedBitmap?.Dispose(); } catch { } } catch (Exception ex) { //try { clonedBitmap?.Dispose(); } catch { } LogHelper.WriteLogError("RequestSaveImage(UI截图阶段) 出错!", ex); } })); } private void UpdateDelaySaveImage(bool isSave) { if (!isSave) { delaySaveImages.Clear(); return; } // 这里简单遍历保存,实际可以根据需要优化(如批量处理/分队列等) foreach (var item in delaySaveImages) { try { if (string.IsNullOrEmpty(_productService.CurrentProductCode)) { _eventAggregator.GetEvent() .Publish(new Models.MessageStruct() { Message = $"当前产品SN不存在,无法保存图片!", level = MessageLevel.Alarm }); try { item.clonedBitmap?.Dispose(); } catch { } break; } SaveImage( item.SaveImageModel, item.SaveImagePathModel, item.SavePath, item.Image, item.clonedBitmap, item.Result, item.IsCompress, item.ImageFileName ); } catch (Exception ex) { LogHelper.WriteLogError("UpdateDelaySaveImage 保存图像时出错!", ex); } finally { // 保存后���放资源 try { item.clonedBitmap?.Dispose(); } catch { } } } delaySaveImages.Clear(); } /// /// 保存图像(保持你原有逻辑不变,补充 Dispose 兜底,防止压缩分支不释放导致闪退) /// private void SaveImage(ProcedureSaveImageModel saveImageModel, ProcedureSaveImagePathModel saveImagePathModel, string path, ICogImage image, Bitmap reimage, bool result, bool isCompress, string imageFileName) { if (string.IsNullOrEmpty(path) || string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path)) { _eventAggregator.GetEvent() .Publish(new Models.MessageStruct() { Message = "保存图像路径为无效的路径!", level = MessageLevel.Alarm }); return; } Task.Run(() => { try { DateTime now = DateTime.Now; //图片名 string imgName = now.ToString("yyyy-MM-dd HH_mm_ss"); if (!string.IsNullOrEmpty(imageFileName)) { imgName = imageFileName; } if (saveImageModel == ProcedureSaveImageModel.Original) { string Originalpath = $"{path}\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp"; if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_OkNG_Time) { Originalpath = result ? $"{path}\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp" : $"{path}\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp"; } else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ok_Time) { if (result) Originalpath = $"{path}\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp"; else return; } else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ng_Time) { if (!result) Originalpath = $"{path}\\{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, '_'); // 产品SN(过滤非法字符) string productSN = _productService.CurrentProductCode; if (!string.IsNullOrEmpty(productSN)) { foreach (char c in Path.GetInvalidFileNameChars()) productSN = productSN.Replace(c, '_'); } string resultStr = result ? "OK" : "NG"; Originalpath = $"{path}\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp"; } 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(); } } else if (saveImageModel == ProcedureSaveImageModel.Recorded) { string Recordedpath = $"{path}\\{now:yyyy-MM}\\{now:MM-dd}\\{imgName}.bmp"; if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_OkNG_Time) { Recordedpath = result ? $"{path}\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp" : $"{path}\\{now:yyyy-MM}\\{now:MM-dd}\\NG\\{imgName}.bmp"; } else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ok_Time) { if (result) Recordedpath = $"{path}\\{now:yyyy-MM}\\{now:MM-dd}\\OK\\{imgName}.bmp"; else return; } else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ng_Time) { if (!result) Recordedpath = $"{path}\\{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 resultStr = result ? "OK" : "NG"; Recordedpath = $"{path}\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp"; } if (!Directory.Exists(Path.GetDirectoryName(Recordedpath))) Directory.CreateDirectory(Path.GetDirectoryName(Recordedpath)); if (isCompress) { ImageHelper.CompressImage(reimage, Recordedpath, 100); } else { reimage.Save(Recordedpath); } } else if (saveImageModel == ProcedureSaveImageModel.OriginalAndRecorded) { string 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"; } } else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ok_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 { return; } } else if (saveImagePathModel == ProcedureSaveImagePathModel.Month_Day_Ng_Time) { 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; } } 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 (string.IsNullOrEmpty(productSN)) { 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"; Recordedpath = $"{path}\\Recored\\{now:yyyy-MM}\\{now:MM-dd}\\{formulaName}\\{productSN}\\{resultStr}\\{imgName}.bmp"; } 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(); } if (!Directory.Exists(Path.GetDirectoryName(Recordedpath))) Directory.CreateDirectory(Path.GetDirectoryName(Recordedpath)); if (isCompress) { ImageHelper.CompressImage(reimage, Recordedpath, 100); } else { reimage.Save(Recordedpath); } } } catch (Exception ex) { LogHelper.WriteLogError("保存流程运行结果的图片时出错!", ex); } finally { // ======================= 【关键修复】无论是否压缩都释放Bitmap ======================= try { reimage?.Dispose(); } catch { } } }); } /// /// 切换产品时清空主页显示(保持你原有逻辑) /// private void ProductChanged(ProductModel product) { this.gridRender.Dispatcher.BeginInvoke(new Action(() => { foreach (var item in vmRenders) { try { if (item.Value?.Child is CogRecordDisplay disp) { if (disp.Tag != null) { disp.Image = null; disp.StaticGraphics.Clear(); // 你原来 break 只清一次;这里保留你的行为 break; } } } catch (Exception ex) { LogHelper.WriteLogError(Lang.切换产品清除主页图像时出错, ex); } } })); } private void OnDisplayDoubleClick(CogRecordDisplay disp) { if (disp == null) return; // 必须在 WinForms/ActiveX 线程截图 disp.BeginInvoke(new Action(() => { Bitmap bmp = null; try { if (disp.IsDisposed || !disp.IsHandleCreated) return; // 优先使用显示内容截图(带图形/record叠加) try { var uiBmp = (Bitmap)disp.CreateContentBitmap( Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom); bmp = (Bitmap)uiBmp.Clone(); uiBmp.Dispose(); } catch { // 如果 ActiveX 状态不允许截图,退化:尝试从 Image 转 Bitmap bmp = TryConvertCogImageToBitmap(disp.Image); } if (bmp == null) return; // 回到 WPF UI 线程弹窗 this.Dispatcher.BeginInvoke(new Action(() => { //var win = new ImagePreviewWindow(bmp, $"预览:{disp.Tag ?? "Display"}"); //win.Owner = Window.GetWindow(this); // 让弹窗归属当前窗口 //win.Show(); })); } catch (Exception ex) { LogHelper.WriteLogError("OnDisplayDoubleClick 截图/弹窗出错", ex); } finally { // 这里 bmp 不要 Dispose,因为传给窗口了 // 窗口关闭时会释放 } })); } /// /// 将 VisionPro 的 ICogImage 尝试转换成 Bitmap(作为 CreateContentBitmap 失败时的兜底) /// private Bitmap TryConvertCogImageToBitmap(ICogImage cogImage) { try { if (cogImage == null) return null; // VisionPro 常用转换:CogImage8Grey / CogImage24PlanarColor / CogImage24PackedColor 等 // 这里给通用兜底:用 CogImageFileBMP 落地到内存流,再读回 Bitmap using (var ms = new MemoryStream()) { // 需要引用 Cognex.VisionPro.ImageFile using (var bmpFile = new CogImageFileBMP()) { // CogImageFileBMP 只能写文件路径,不直接写流 // 所以用临时文件方式最稳(如你不想落盘,可以另写转换器) var tmp = Path.Combine(Path.GetTempPath(), $"vp_preview_{Guid.NewGuid():N}.bmp"); try { bmpFile.Open(tmp, CogImageFileModeConstants.Write); bmpFile.Append(cogImage); bmpFile.Close(); var bitmap = (Bitmap)Bitmap.FromFile(tmp); return (Bitmap)bitmap.Clone(); } finally { try { if (File.Exists(tmp)) File.Delete(tmp); } catch { } } } } } catch { return null; } } } }