Files
CheckDevice/Check.Main/UI/ZoomPictureBox.cs
17860779768 2e46747ba9 视觉修改
2025-08-25 16:33:58 +08:00

382 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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