382 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using OpenCvSharp;
 | ||
| using OpenCvSharp.Extensions;
 | ||
| using System;
 | ||
| using System.Collections.Generic;
 | ||
| using System.ComponentModel;
 | ||
| using System.Drawing;
 | ||
| using System.Linq;
 | ||
| using System.Text;
 | ||
| using System.Threading.Tasks;
 | ||
| using System.Windows.Forms;
 | ||
| 
 | ||
| namespace Check.Main.UI
 | ||
| {
 | ||
|     public class ZoomPictureBox : PictureBox, IDisposable
 | ||
|     {
 | ||
|         private float zoom = 1.0f;
 | ||
|         private System.Drawing.Point imageLocation;
 | ||
|         private Image image;
 | ||
|         private bool dragging = false;
 | ||
|         private System.Drawing.Point mouseDownPos;
 | ||
|         private System.Drawing.Point imageLocationOnMouseDown;
 | ||
| 
 | ||
|         // 矩形绘制相关
 | ||
|         private bool isDrawingRect = false;      // 绘制状态
 | ||
|         private Rectangle currentRect;           // 当前绘制矩形
 | ||
|         private Rectangle storedRect;            // 上次绘制并固定的矩形
 | ||
|         private bool hasStoredRect = false;      // 是否存在固定矩形
 | ||
|         private System.Drawing.Point rectStartPoint;
 | ||
| 
 | ||
|         // 一个私有的锁对象,专门用于保护对 'image' 字段的访问。
 | ||
|         private readonly object imageLock = new object();
 | ||
| 
 | ||
|         private bool croppingEnabled = true;
 | ||
|         //private bool croppingEnabled = true;
 | ||
|         [Category("Behavior")]
 | ||
|         [Description("是否允许通过右键绘制矩形裁剪区域。设置为 false 则不再响应右键并清除已有矩形。")]
 | ||
|         public bool CroppingEnabled
 | ||
|         {
 | ||
|             get => croppingEnabled;
 | ||
|             set
 | ||
|             {
 | ||
|                 croppingEnabled = value;
 | ||
|                 // 禁用裁剪时清除所有矩形和裁剪结果
 | ||
|                 if (!croppingEnabled)
 | ||
|                 {
 | ||
|                     isDrawingRect = false;
 | ||
|                     hasStoredRect = false;
 | ||
|                     LastCroppedMat?.Dispose();
 | ||
|                     LastCroppedMat = null;
 | ||
|                 }
 | ||
|                 Invalidate();
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         // 裁剪完成事件,外部可订阅以接收 Mat
 | ||
|         public event Action<Mat> Cropped;
 | ||
| 
 | ||
|         public Mat LastCroppedMat { get; private set; }
 | ||
| 
 | ||
|         private Color backgroundFillColor = Color.LightSteelBlue;
 | ||
|         private Color rectangleColor = Color.Red;
 | ||
|         private int rectangleThickness = 2;
 | ||
| 
 | ||
|         [Category("Appearance")]
 | ||
|         [Description("指定控件背景的填充颜色。使用 BackColor 属性同步更新。")]
 | ||
|         public Color BackgroundFillColor
 | ||
|         {
 | ||
|             get => backgroundFillColor;
 | ||
|             set
 | ||
|             {
 | ||
|                 backgroundFillColor = value;
 | ||
|                 base.BackColor = value;  // 同步 WinForms BackColor
 | ||
|                 Invalidate();
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         [Category("Appearance")]
 | ||
|         [Description("绘制矩形框的颜色。")]
 | ||
|         public Color RectangleColor { get => rectangleColor; set { rectangleColor = value; Invalidate(); } }
 | ||
| 
 | ||
|         [Category("Appearance")]
 | ||
|         [Description("绘制矩形框的线宽。")]
 | ||
|         public int RectangleThickness { get => rectangleThickness; set { rectangleThickness = Math.Max(1, value); Invalidate(); } }
 | ||
| 
 | ||
|         public ZoomPictureBox()
 | ||
|         {
 | ||
|             DoubleBuffered = true;
 | ||
|             ResizeRedraw = true;
 | ||
|             base.BackColor = backgroundFillColor;
 | ||
|             SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true);
 | ||
|             MouseWheel += ZoomPictureBox_MouseWheel;
 | ||
|             MouseDown += ZoomPictureBox_MouseDown;
 | ||
|             MouseMove += ZoomPictureBox_MouseMove;
 | ||
|             MouseUp += ZoomPictureBox_MouseUp;
 | ||
|             DoubleClick += ZoomPictureBox_DoubleClick;
 | ||
|         }
 | ||
|         // 重写背景绘制,使用自定义背景色
 | ||
|         protected override void OnPaintBackground(PaintEventArgs e)
 | ||
|         {
 | ||
|             e.Graphics.Clear(backgroundFillColor);
 | ||
|         }
 | ||
|         //[Browsable(false)]
 | ||
|         //public new Image Image
 | ||
|         //{
 | ||
|         //    get => image;
 | ||
|         //    set
 | ||
|         //    {
 | ||
|         //        image = value;
 | ||
|         //        zoom = 1.0f;
 | ||
|         //        LastCroppedMat?.Dispose();
 | ||
|         //        LastCroppedMat = null;
 | ||
|         //        isDrawingRect = false;
 | ||
|         //        hasStoredRect = false;
 | ||
|         //        FitImage();
 | ||
|         //        Invalidate();
 | ||
|         //    }
 | ||
|         //}
 | ||
|         // 【关键优化】
 | ||
|         // 2. 重写 Control.Dispose 方法来释放我们自己的资源
 | ||
|         protected override void Dispose(bool disposing)
 | ||
|         {
 | ||
|             if (disposing)
 | ||
|             {
 | ||
|                 // 在这里释放托管和非托管资源
 | ||
|                 // 清理事件订阅 (虽然在这个类里没有,但这是个好习惯)
 | ||
| 
 | ||
|                 // 释放我们持有的最后一个图像资源
 | ||
|                 lock (imageLock)
 | ||
|                 {
 | ||
|                     this.image?.Dispose();
 | ||
|                     this.image = null;
 | ||
|                 }
 | ||
| 
 | ||
|                 // 释放我们持有的最后一个裁剪结果资源
 | ||
|                 this.LastCroppedMat?.Dispose();
 | ||
|                 this.LastCroppedMat = null;
 | ||
|             }
 | ||
| 
 | ||
|             // 调用基类的 Dispose 方法来完成标准清理
 | ||
|             base.Dispose(disposing);
 | ||
|         }
 | ||
| 
 | ||
|         // 重写 Image 属性,但不做任何额外操作,因为我们将通过一个新方法来更新它。
 | ||
|         [Browsable(false)]
 | ||
|         public new Image Image
 | ||
|         {
 | ||
|             get
 | ||
|             {
 | ||
|                 // 在访问时也加锁,确保读取的是一个完整的对象
 | ||
|                 lock (imageLock)
 | ||
|                 {
 | ||
|                     return image;
 | ||
|                 }
 | ||
|             }
 | ||
|             // set 访问器可以保持原样,但我们不再直接使用它来更新图像
 | ||
|             private set
 | ||
|             {
 | ||
|                 lock (imageLock)
 | ||
|                 {
 | ||
|                     image = value;
 | ||
|                 }
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         protected override void OnPaint(PaintEventArgs e)
 | ||
|         {
 | ||
|             base.OnPaint(e);
 | ||
|             var g = e.Graphics;
 | ||
|             //在绘制前获取锁,确保在绘制期间,image对象不会被其他线程替换或释放
 | ||
|             lock (imageLock)
 | ||
|             {
 | ||
|                 if (image != null)
 | ||
|                 {
 | ||
|                     try
 | ||
|                     {
 | ||
|                         int w = (int)(image.Width * zoom);
 | ||
|                         int h = (int)(image.Height * zoom);
 | ||
|                         g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
 | ||
|                         g.DrawImage(image, new Rectangle(imageLocation, new System.Drawing.Size(w, h)));
 | ||
|                     }
 | ||
|                     catch (Exception)
 | ||
|                     {
 | ||
| 
 | ||
|                         // 如果在绘制时仍然发生罕见的GDI+错误,静默忽略,避免程序崩溃。
 | ||
|                         // 这通常意味着图像状态仍然存在问题,但UI不会因此卡死。
 | ||
|                     }
 | ||
|                 }
 | ||
|             }
 | ||
|             //if (image != null)
 | ||
|             //{
 | ||
|             //    int w = (int)(image.Width * zoom);
 | ||
|             //    int h = (int)(image.Height * zoom);
 | ||
|             //    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
 | ||
|             //    g.DrawImage(image, new Rectangle(imageLocation, new System.Drawing.Size(w, h)));
 | ||
|             //}
 | ||
|             // 仅当允许裁剪时才绘制矩形
 | ||
|             if (croppingEnabled)
 | ||
|             {
 | ||
|                 using (var pen = new Pen(rectangleColor, rectangleThickness))
 | ||
|                 {
 | ||
|                     if (hasStoredRect) g.DrawRectangle(pen, storedRect);
 | ||
|                     else if (isDrawingRect) g.DrawRectangle(pen, currentRect);
 | ||
|                 }
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 一个线程安全的、用于更新显示图像的公共方法。
 | ||
|         /// 这个方法将替换掉直接的 pictureBox.Image = ... 赋值。
 | ||
|         /// </summary>
 | ||
|         /// <param name="newImage">要显示的新图像。此方法会接管该对象的所有权。</param>
 | ||
|         public void SetImageThreadSafe(Image newImage)
 | ||
|         {
 | ||
|             Image oldImage = null;
 | ||
| 
 | ||
|             lock (imageLock)
 | ||
|             {
 | ||
|                 // 1. 保存旧图像的引用,以便在锁外部释放它
 | ||
|                 oldImage = this.image;
 | ||
|                 // 2. 将新图像赋值给成员字段
 | ||
|                 this.image = newImage;
 | ||
|             }
 | ||
|             // 3. 【关键】在锁的外部释放旧图像,避免长时间持有锁
 | ||
|             oldImage?.Dispose();
 | ||
| 
 | ||
|             // 4. 计算自适应并触发重绘
 | ||
|             FitImage();
 | ||
|             Invalidate();
 | ||
|         }
 | ||
| 
 | ||
|         private void FitImage()
 | ||
|         {
 | ||
|             //if (image == null) return;
 | ||
|             //float sx = (float)ClientSize.Width / image.Width;
 | ||
|             //float sy = (float)ClientSize.Height / image.Height;
 | ||
|             //zoom = Math.Min(sx, sy);
 | ||
|             //CenterImage();
 | ||
|             //在访问image属性前获取锁
 | ||
|             lock (imageLock)
 | ||
|             {
 | ||
|                 if (image == null) return;
 | ||
| 
 | ||
|                 try
 | ||
|                 {
 | ||
|                     float sx = (float)ClientSize.Width / image.Width;
 | ||
|                     float sy = (float)ClientSize.Height / image.Height;
 | ||
|                     zoom = Math.Min(sx, sy);
 | ||
|                     CenterImage();
 | ||
|                 }
 | ||
|                 catch (Exception)
 | ||
|                 {
 | ||
| 
 | ||
|                     // 忽略错误,同 OnPaint
 | ||
|                 }
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         private void CenterImage()
 | ||
|         {
 | ||
|             if (image == null) return;
 | ||
|             int w = (int)(image.Width * zoom);
 | ||
|             int h = (int)(image.Height * zoom);
 | ||
|             imageLocation = new System.Drawing.Point((ClientSize.Width - w) / 2, (ClientSize.Height - h) / 2);
 | ||
|         }
 | ||
| 
 | ||
|         private void ZoomPictureBox_MouseWheel(object sender, MouseEventArgs e)
 | ||
|         {
 | ||
|             if (image == null) return;
 | ||
|             float oldZoom = zoom;
 | ||
|             zoom *= e.Delta > 0 ? 1.1f : 1 / 1.1f;
 | ||
|             zoom = Math.Max(0.1f, Math.Min(zoom, 100f));
 | ||
|             var m = e.Location;
 | ||
|             float ix = (m.X - imageLocation.X) / oldZoom;
 | ||
|             float iy = (m.Y - imageLocation.Y) / oldZoom;
 | ||
|             imageLocation = new System.Drawing.Point((int)(m.X - ix * zoom), (int)(m.Y - iy * zoom));
 | ||
|             Invalidate();
 | ||
|         }
 | ||
| 
 | ||
|         private void ZoomPictureBox_MouseDown(object sender, MouseEventArgs e)
 | ||
|         {
 | ||
|             if (image == null) return;
 | ||
|             if (e.Button == MouseButtons.Left)
 | ||
|             {
 | ||
|                 hasStoredRect = false;
 | ||
|                 LastCroppedMat?.Dispose();
 | ||
|                 LastCroppedMat = null;
 | ||
| 
 | ||
|                 dragging = true;
 | ||
|                 mouseDownPos = e.Location;
 | ||
|                 imageLocationOnMouseDown = imageLocation;
 | ||
|                 Cursor = Cursors.Hand;
 | ||
|             }
 | ||
|             else if (e.Button == MouseButtons.Right && croppingEnabled)
 | ||
|             {
 | ||
|                 // 清除上次固定矩形和裁剪
 | ||
|                 hasStoredRect = false;
 | ||
|                 LastCroppedMat?.Dispose();
 | ||
|                 LastCroppedMat = null;
 | ||
| 
 | ||
|                 // 开始新绘制
 | ||
|                 isDrawingRect = true;
 | ||
|                 rectStartPoint = e.Location;
 | ||
|                 currentRect = new Rectangle(e.Location, System.Drawing.Size.Empty);
 | ||
|                 Invalidate();
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         private void ZoomPictureBox_MouseMove(object sender, MouseEventArgs e)
 | ||
|         {
 | ||
|             if (dragging)
 | ||
|             {
 | ||
|                 var delta = new System.Drawing.Point(e.X - mouseDownPos.X, e.Y - mouseDownPos.Y);
 | ||
|                 imageLocation = new System.Drawing.Point(imageLocationOnMouseDown.X + delta.X, imageLocationOnMouseDown.Y + delta.Y);
 | ||
|                 Invalidate();
 | ||
|             }
 | ||
|             else if (isDrawingRect)
 | ||
|             {
 | ||
|                 int x = Math.Min(rectStartPoint.X, e.X);
 | ||
|                 int y = Math.Min(rectStartPoint.Y, e.Y);
 | ||
|                 int w = Math.Abs(e.X - rectStartPoint.X);
 | ||
|                 int h = Math.Abs(e.Y - rectStartPoint.Y);
 | ||
|                 currentRect = new Rectangle(x, y, w, h);
 | ||
|                 Invalidate();
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         private void ZoomPictureBox_MouseUp(object sender, MouseEventArgs e)
 | ||
|         {
 | ||
|             if (e.Button == MouseButtons.Left && dragging)
 | ||
|             {
 | ||
|                 dragging = false;
 | ||
|                 Cursor = Cursors.Default;
 | ||
|             }
 | ||
|             else if (e.Button == MouseButtons.Right && isDrawingRect)
 | ||
|             {
 | ||
|                 isDrawingRect = false;
 | ||
|                 // 固定当前矩形框
 | ||
|                 storedRect = currentRect;
 | ||
|                 hasStoredRect = true;
 | ||
|                 if (croppingEnabled && image is Bitmap bmp)
 | ||
|                 {
 | ||
|                     this.LastCroppedMat?.Dispose();
 | ||
|                     this.LastCroppedMat = null;
 | ||
|                     Mat srcMat = null;
 | ||
|                     try
 | ||
|                     {
 | ||
|                         int ix = (int)((storedRect.X - imageLocation.X) / zoom);
 | ||
|                         int iy = (int)((storedRect.Y - imageLocation.Y) / zoom);
 | ||
|                         int iw = (int)(storedRect.Width / zoom);
 | ||
|                         int ih = (int)(storedRect.Height / zoom);
 | ||
|                         ix = Math.Max(0, ix);
 | ||
|                         iy = Math.Max(0, iy);
 | ||
|                         if (ix + iw > bmp.Width) iw = bmp.Width - ix;
 | ||
|                         if (iy + ih > bmp.Height) ih = bmp.Height - iy;
 | ||
|                         if (iw > 0 && ih > 0)
 | ||
|                         {
 | ||
|                             srcMat = BitmapConverter.ToMat(bmp);
 | ||
|                             var roi = new Rect(ix, iy, iw, ih);
 | ||
|                             LastCroppedMat = new Mat(srcMat, roi);
 | ||
|                             Cropped?.Invoke(LastCroppedMat);
 | ||
|                             bmp.Dispose();
 | ||
|                         }
 | ||
|                     }
 | ||
|                     finally
 | ||
|                     {
 | ||
|                         // 5.确保从Bitmap转换来的 srcMat 被释放
 | ||
|                         srcMat?.Dispose();
 | ||
|                     }
 | ||
|                 }
 | ||
|                 Invalidate();
 | ||
|             }
 | ||
|         }
 | ||
|         private void ZoomPictureBox_DoubleClick(object sender, EventArgs e)
 | ||
|         {
 | ||
|             if (image == null) return;
 | ||
|             FitImage();
 | ||
|             Invalidate();
 | ||
|         }
 | ||
|     }
 | ||
| }
 |