|
|
@@ -6,6 +6,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;
|
|
|
@@ -35,6 +36,10 @@ namespace TeamAAS_VP.Views.Home
|
|
|
private readonly ISystemDatabaseService _systemDatabaseService;
|
|
|
private readonly IProductService _productService;
|
|
|
|
|
|
+ // ======================= 【新增】保存任务控制:防闪退/防资源泄露 =======================
|
|
|
+ private CancellationTokenSource _saveImageCts = new CancellationTokenSource();
|
|
|
+ private readonly SemaphoreSlim _saveSemaphore = new SemaphoreSlim(2, 2);
|
|
|
+
|
|
|
public ShowVisionRender()
|
|
|
{
|
|
|
InitializeComponent();
|
|
|
@@ -51,6 +56,19 @@ namespace TeamAAS_VP.Views.Home
|
|
|
viewModel.UpdateLayout += ViewModel_UpdateLayout;
|
|
|
_eventAggregator.GetEvent<RenderUpdateNotification>().Subscribe(UpdateRenderModuleSource);
|
|
|
_eventAggregator.GetEvent<ProductChangedNotification>().Subscribe(ProductChanged);
|
|
|
+
|
|
|
+ // 【可选】控件卸载时取消所有保存任务,避免页面关闭后仍在截图/保存导致闪退
|
|
|
+ this.Unloaded += ShowVisionRender_Unloaded;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ShowVisionRender_Unloaded(object sender, RoutedEventArgs e)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ _saveImageCts.Cancel();
|
|
|
+ _saveImageCts.Dispose();
|
|
|
+ }
|
|
|
+ catch { }
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -77,25 +95,9 @@ namespace TeamAAS_VP.Views.Home
|
|
|
if (render.Record != null)
|
|
|
disp.Record = render.Record;
|
|
|
|
|
|
- if (render.IsSaveImage)
|
|
|
- {
|
|
|
- // 注意:CreateContentBitmap 可能比较耗时,但你这里已经在 UI 线程里调用了;
|
|
|
- // 如果卡顿明显,建议把 bitmap 生成放到后台线程(需谨慎处理线程访问)
|
|
|
- App.Current.Dispatcher.BeginInvoke(new Action(() =>
|
|
|
- {
|
|
|
- var bmp = (Bitmap)disp.CreateContentBitmap(Cognex.VisionPro.Display.CogDisplayContentBitmapConstants.Custom);
|
|
|
-
|
|
|
- SaveImage(
|
|
|
- render.SaveImageModel,
|
|
|
- render.SaveImagePathModel,
|
|
|
- render.SavePath,
|
|
|
- render.Image,
|
|
|
- bmp,
|
|
|
- render.Result,
|
|
|
- render.IsCompress
|
|
|
- );
|
|
|
- }));
|
|
|
- }
|
|
|
+ // ======================= 【关键优化】保存图像走安全入口 =======================
|
|
|
+ RequestSaveImage(render, disp);
|
|
|
+
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
@@ -108,6 +110,15 @@ namespace TeamAAS_VP.Views.Home
|
|
|
/// </summary>
|
|
|
private async 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;
|
|
|
|
|
|
// 1) 清理旧控件
|
|
|
@@ -132,15 +143,7 @@ namespace TeamAAS_VP.Views.Home
|
|
|
// 3) 限制最大 16(防止越界)
|
|
|
int count = Math.Min(obj.Count, 16);
|
|
|
|
|
|
- // 4) 计算行列(1~16自动铺满:尽量接近正方形;最大 4*4)
|
|
|
- // 1 -> 1*1
|
|
|
- // 2 -> 1*2
|
|
|
- // 3 -> 2*2
|
|
|
- // 4 -> 2*2
|
|
|
- // 5~6 -> 2*3
|
|
|
- // 7~9 -> 3*3(9满)
|
|
|
- // 10~12 -> 3*4(12满)
|
|
|
- // 13~16 -> 4*4(16满)
|
|
|
+ // 4) 计算行列(最大 4*4)
|
|
|
GetGridSize(count, out int rows, out int cols);
|
|
|
|
|
|
// 5) 创建主Grid的行列
|
|
|
@@ -158,13 +161,39 @@ namespace TeamAAS_VP.Views.Home
|
|
|
var sr = obj[i];
|
|
|
|
|
|
// (1) 创建显示控件(WindowsFormsHost + CogRecordDisplay)
|
|
|
- var host = new WindowsFormsHost()
|
|
|
+ var disp = new CogRecordDisplay()
|
|
|
{
|
|
|
Tag = sr.ProceductName,
|
|
|
- Child = new CogRecordDisplay()
|
|
|
+ };
|
|
|
+
|
|
|
+ disp.HandleCreated += (s, e) =>
|
|
|
+ {
|
|
|
+ try
|
|
|
{
|
|
|
- Tag = sr.ProceductName,
|
|
|
+ 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);
|
|
|
|
|
|
@@ -209,7 +238,6 @@ namespace TeamAAS_VP.Views.Home
|
|
|
/// </summary>
|
|
|
private void GetGridSize(int count, out int rows, out int cols)
|
|
|
{
|
|
|
- // 默认值
|
|
|
rows = 1;
|
|
|
cols = 1;
|
|
|
|
|
|
@@ -220,7 +248,6 @@ namespace TeamAAS_VP.Views.Home
|
|
|
if (count <= 9) { rows = 3; cols = 3; return; }
|
|
|
if (count <= 12) { rows = 3; cols = 4; return; }
|
|
|
|
|
|
- // 13~16
|
|
|
rows = 4;
|
|
|
cols = 4;
|
|
|
}
|
|
|
@@ -233,11 +260,94 @@ namespace TeamAAS_VP.Views.Home
|
|
|
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();
|
|
|
+
|
|
|
+ SaveImage(
|
|
|
+ render.SaveImageModel,
|
|
|
+ render.SaveImagePathModel,
|
|
|
+ render.SavePath,
|
|
|
+ render.Image,
|
|
|
+ clonedBitmap,
|
|
|
+ render.Result,
|
|
|
+ render.IsCompress
|
|
|
+ );
|
|
|
+ }
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
- /// 保存图像(保持你原有逻辑不变)
|
|
|
+ /// 保存图像(保持你原有逻辑不变,补充 Dispose 兜底,防止压缩分支不释放导致闪退)
|
|
|
/// </summary>
|
|
|
private void SaveImage(ProcedureSaveImageModel saveImageModel,
|
|
|
ProcedureSaveImagePathModel saveImagePathModel,
|
|
|
@@ -358,7 +468,6 @@ namespace TeamAAS_VP.Views.Home
|
|
|
else
|
|
|
{
|
|
|
reimage.Save(Recordedpath);
|
|
|
- reimage.Dispose();
|
|
|
}
|
|
|
}
|
|
|
else if (saveImageModel == ProcedureSaveImageModel.OriginalAndRecorded)
|
|
|
@@ -438,7 +547,6 @@ namespace TeamAAS_VP.Views.Home
|
|
|
else
|
|
|
{
|
|
|
reimage.Save(Recordedpath);
|
|
|
- reimage.Dispose();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -446,6 +554,11 @@ namespace TeamAAS_VP.Views.Home
|
|
|
{
|
|
|
LogHelper.WriteLogError("保存流程运行结果的图片时出错!", ex);
|
|
|
}
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ // ======================= 【关键修复】无论是否压缩都释放Bitmap =======================
|
|
|
+ try { reimage?.Dispose(); } catch { }
|
|
|
+ }
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -478,5 +591,97 @@ namespace TeamAAS_VP.Views.Home
|
|
|
}
|
|
|
}));
|
|
|
}
|
|
|
+ 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,因为传给窗口了
|
|
|
+ // 窗口关闭时会释放
|
|
|
+ }
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 将 VisionPro 的 ICogImage 尝试转换成 Bitmap(作为 CreateContentBitmap 失败时的兜底)
|
|
|
+ /// </summary>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
}
|