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();
|
||
}
|
||
}
|
||
}
|