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 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); } } } /// /// 一个线程安全的、用于更新显示图像的公共方法。 /// 这个方法将替换掉直接的 pictureBox.Image = ... 赋值。 /// /// 要显示的新图像。此方法会接管该对象的所有权。 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(); } } }