1111
This commit is contained in:
@ -16,54 +16,61 @@ namespace DHSoftware.Views
|
||||
private Bitmap _currentImage;
|
||||
private float _scale = 1.0f;
|
||||
private PointF _offset = PointF.Empty;
|
||||
private RectangleF _roiRect;
|
||||
private PointF _roiStart;
|
||||
private bool _isDrawing;
|
||||
private Point _dragStart;
|
||||
private bool _isDragging;
|
||||
private Pen _roiPen = new Pen(Color.Red, 2);
|
||||
private readonly object _imageLock = new object();
|
||||
#endregion
|
||||
|
||||
#region 公开属性
|
||||
#region 公开属性
|
||||
public Bitmap Image
|
||||
{
|
||||
get => _currentImage;
|
||||
get
|
||||
{
|
||||
lock (_imageLock)
|
||||
{
|
||||
return _currentImage?.Clone() as Bitmap;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
// 记录旧状态
|
||||
var oldSize = _currentImage?.Size ?? Size.Empty;
|
||||
var oldScale = _scale;
|
||||
var oldOffset = _offset;
|
||||
Bitmap newImage = value?.Clone() as Bitmap;
|
||||
Bitmap oldImageToDispose = null;
|
||||
|
||||
_currentImage?.Dispose();
|
||||
_currentImage = value;
|
||||
|
||||
if (_currentImage != null)
|
||||
lock (_imageLock)
|
||||
{
|
||||
if (_currentImage.Size != oldSize)
|
||||
// 交换图像引用
|
||||
oldImageToDispose = _currentImage;
|
||||
_currentImage = newImage;
|
||||
|
||||
if (_currentImage != null)
|
||||
{
|
||||
// 尺寸不同时:重置ROI、自动适配
|
||||
_roiRect = RectangleF.Empty;
|
||||
AutoFit();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 尺寸相同时:保留缩放和偏移
|
||||
_scale = oldScale;
|
||||
_offset = oldOffset;
|
||||
ClampOffset();
|
||||
if (oldImageToDispose?.Size != _currentImage.Size)
|
||||
{
|
||||
AutoFit();
|
||||
}
|
||||
else
|
||||
{
|
||||
ClampOffset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pictureBox.Invalidate();
|
||||
// 在锁外安全释放旧图像
|
||||
if (oldImageToDispose != null)
|
||||
{
|
||||
// 使用BeginInvoke确保在UI线程释放资源
|
||||
BeginInvoke(new Action(() =>
|
||||
{
|
||||
oldImageToDispose.Dispose();
|
||||
oldImageToDispose = null;
|
||||
}));
|
||||
}
|
||||
|
||||
SafeInvalidate();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public RectangleF CurrentROI => _roiRect;
|
||||
#endregion
|
||||
|
||||
public ImageViewerControl()
|
||||
{
|
||||
InitializeComponents();
|
||||
@ -73,15 +80,13 @@ namespace DHSoftware.Views
|
||||
#region 初始化
|
||||
private void InitializeComponents()
|
||||
{
|
||||
// 主显示区域
|
||||
pictureBox = new PictureBox
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
BackColor = Color.DarkGray,
|
||||
Cursor = Cursors.Cross
|
||||
Cursor = Cursors.Hand
|
||||
};
|
||||
|
||||
// 状态栏
|
||||
statusLabel = new Label
|
||||
{
|
||||
Dock = DockStyle.Bottom,
|
||||
@ -91,7 +96,6 @@ namespace DHSoftware.Views
|
||||
Font = new Font("Consolas", 10)
|
||||
};
|
||||
|
||||
// 事件绑定
|
||||
pictureBox.MouseDown += PictureBox_MouseDown;
|
||||
pictureBox.MouseMove += PictureBox_MouseMove;
|
||||
pictureBox.MouseUp += PictureBox_MouseUp;
|
||||
@ -106,8 +110,8 @@ namespace DHSoftware.Views
|
||||
{
|
||||
typeof(PictureBox).GetMethod("SetStyle",
|
||||
System.Reflection.BindingFlags.NonPublic |
|
||||
System.Reflection.BindingFlags.Instance)
|
||||
?.Invoke(pictureBox, new object[] {
|
||||
System.Reflection.BindingFlags.Instance)?
|
||||
.Invoke(pictureBox, new object[] {
|
||||
ControlStyles.OptimizedDoubleBuffer |
|
||||
ControlStyles.AllPaintingInWmPaint,
|
||||
true
|
||||
@ -118,189 +122,225 @@ namespace DHSoftware.Views
|
||||
#region 核心功能
|
||||
private void AutoFit()
|
||||
{
|
||||
if (_currentImage == null) return;
|
||||
lock (_imageLock)
|
||||
{
|
||||
if (_currentImage == null) return;
|
||||
|
||||
const float marginRatio = 0.1f;
|
||||
float marginWidth = Width * marginRatio;
|
||||
float marginHeight = Height * marginRatio;
|
||||
const float marginRatio = 0.1f;
|
||||
float marginWidth = Width * marginRatio;
|
||||
float marginHeight = Height * marginRatio;
|
||||
|
||||
_scale = Math.Min(
|
||||
(Width - marginWidth * 2) / _currentImage.Width,
|
||||
(Height - marginHeight * 2) / _currentImage.Height
|
||||
);
|
||||
_scale = Math.Min(
|
||||
(Width - marginWidth * 2) / _currentImage.Width,
|
||||
(Height - marginHeight * 2) / _currentImage.Height
|
||||
);
|
||||
|
||||
_offset.X = marginWidth + (Width - marginWidth * 2 - _currentImage.Width * _scale) / 2;
|
||||
_offset.Y = marginHeight + (Height - marginHeight * 2 - _currentImage.Height * _scale) / 2;
|
||||
_offset.X = marginWidth + (Width - marginWidth * 2 - _currentImage.Width * _scale) / 2;
|
||||
_offset.Y = marginHeight + (Height - marginHeight * 2 - _currentImage.Height * _scale) / 2;
|
||||
|
||||
ClampOffset();
|
||||
}
|
||||
}
|
||||
|
||||
private void PictureBox_Paint(object sender, PaintEventArgs e)
|
||||
{
|
||||
if (_currentImage == null) return;
|
||||
Bitmap drawImage = null;
|
||||
RectangleF destRect;
|
||||
float scale;
|
||||
PointF offset;
|
||||
|
||||
// 绘制图像
|
||||
var destRect = new RectangleF(
|
||||
_offset.X,
|
||||
_offset.Y,
|
||||
_currentImage.Width * _scale,
|
||||
_currentImage.Height * _scale);
|
||||
|
||||
e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
e.Graphics.DrawImage(_currentImage, destRect);
|
||||
|
||||
// 绘制ROI
|
||||
if (!_roiRect.IsEmpty)
|
||||
// 创建临时绘图参数
|
||||
lock (_imageLock)
|
||||
{
|
||||
var displayRect = new RectangleF(
|
||||
_roiRect.X * _scale + _offset.X,
|
||||
_roiRect.Y * _scale + _offset.Y,
|
||||
_roiRect.Width * _scale,
|
||||
_roiRect.Height * _scale);
|
||||
if (_currentImage == null) return;
|
||||
|
||||
using (var pen = new Pen(_roiPen.Color, _roiPen.Width / _scale))
|
||||
{
|
||||
e.Graphics.DrawRectangle(pen,
|
||||
displayRect.X,
|
||||
displayRect.Y,
|
||||
displayRect.Width,
|
||||
displayRect.Height);
|
||||
}
|
||||
// 创建绘图副本
|
||||
drawImage = _currentImage.Clone() as Bitmap;
|
||||
scale = _scale;
|
||||
offset = _offset;
|
||||
|
||||
destRect = new RectangleF(
|
||||
offset.X,
|
||||
offset.Y,
|
||||
drawImage.Width * scale,
|
||||
drawImage.Height * scale);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
e.Graphics.DrawImage(drawImage, destRect);
|
||||
}
|
||||
finally
|
||||
{
|
||||
drawImage?.Dispose();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 鼠标事件
|
||||
#region 鼠标交互(左键拖动)
|
||||
private void PictureBox_MouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Left && _currentImage != null)
|
||||
if (InvokeRequired) return;
|
||||
|
||||
lock (_imageLock)
|
||||
{
|
||||
_roiStart = ClampCoordinates(ConvertToImageCoords(e.Location));
|
||||
_isDrawing = true;
|
||||
}
|
||||
else if (e.Button == MouseButtons.Right)
|
||||
{
|
||||
_dragStart = e.Location;
|
||||
_isDragging = true;
|
||||
if (_currentImage == null) return;
|
||||
|
||||
if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
_dragStart = e.Location;
|
||||
_isDragging = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PictureBox_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (_isDragging)
|
||||
{
|
||||
_offset.X += e.X - _dragStart.X;
|
||||
_offset.Y += e.Y - _dragStart.Y;
|
||||
_dragStart = e.Location;
|
||||
ClampOffset();
|
||||
pictureBox.Invalidate();
|
||||
}
|
||||
else if (_isDrawing && _currentImage != null)
|
||||
{
|
||||
PointF current = ClampCoordinates(ConvertToImageCoords(e.Location));
|
||||
_roiRect = new RectangleF(
|
||||
Math.Min(_roiStart.X, current.X),
|
||||
Math.Min(_roiStart.Y, current.Y),
|
||||
Math.Abs(_roiStart.X - current.X),
|
||||
Math.Abs(_roiStart.Y - current.Y)
|
||||
);
|
||||
pictureBox.Invalidate();
|
||||
}
|
||||
if (InvokeRequired) return;
|
||||
|
||||
UpdateStatus(e.Location);
|
||||
lock (_imageLock)
|
||||
{
|
||||
if (_currentImage == null) return;
|
||||
|
||||
if (_isDragging)
|
||||
{
|
||||
_offset.X += e.X - _dragStart.X;
|
||||
_offset.Y += e.Y - _dragStart.Y;
|
||||
_dragStart = e.Location;
|
||||
ClampOffset();
|
||||
SafeInvalidate();
|
||||
}
|
||||
|
||||
UpdateStatus(e.Location);
|
||||
}
|
||||
}
|
||||
|
||||
private void PictureBox_MouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
_isDragging = false;
|
||||
_isDrawing = false;
|
||||
lock (_imageLock)
|
||||
{
|
||||
_isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void PictureBox_MouseWheel(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (_currentImage == null) return;
|
||||
if (InvokeRequired) return;
|
||||
|
||||
PointF mousePos = e.Location;
|
||||
PointF imgPosBefore = ConvertToImageCoords(mousePos);
|
||||
lock (_imageLock)
|
||||
{
|
||||
if (_currentImage == null) return;
|
||||
|
||||
if (imgPosBefore.X < 0 || imgPosBefore.X > _currentImage.Width ||
|
||||
imgPosBefore.Y < 0 || imgPosBefore.Y > _currentImage.Height) return;
|
||||
PointF imgPos = ConvertToImageCoords(e.Location);
|
||||
if (imgPos.X < 0 || imgPos.X > _currentImage.Width ||
|
||||
imgPos.Y < 0 || imgPos.Y > _currentImage.Height)
|
||||
return;
|
||||
|
||||
float zoom = e.Delta > 0 ? 1.1f : 0.9f;
|
||||
float newScale = Math.Clamp(_scale * zoom, 0.1f, 10f);
|
||||
float zoom = e.Delta > 0 ? 1.1f : 0.9f;
|
||||
float newScale = Math.Clamp(_scale * zoom, 0.1f, 10f);
|
||||
|
||||
_offset.X = mousePos.X - imgPosBefore.X * newScale;
|
||||
_offset.Y = mousePos.Y - imgPosBefore.Y * newScale;
|
||||
_offset.X = e.Location.X - imgPos.X * newScale;
|
||||
_offset.Y = e.Location.Y - imgPos.Y * newScale;
|
||||
|
||||
_scale = newScale;
|
||||
ClampOffset();
|
||||
pictureBox.Invalidate();
|
||||
_scale = newScale;
|
||||
ClampOffset();
|
||||
SafeInvalidate();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 辅助方法
|
||||
private PointF ConvertToImageCoords(PointF mousePos)
|
||||
private PointF ConvertToImageCoords(Point mousePos)
|
||||
{
|
||||
return new PointF(
|
||||
(mousePos.X - _offset.X) / _scale,
|
||||
(mousePos.Y - _offset.Y) / _scale);
|
||||
}
|
||||
|
||||
private PointF ClampCoordinates(PointF point)
|
||||
{
|
||||
return new PointF(
|
||||
Math.Max(0, Math.Min(_currentImage.Width, point.X)),
|
||||
Math.Max(0, Math.Min(_currentImage.Height, point.Y)));
|
||||
lock (_imageLock)
|
||||
{
|
||||
if (_currentImage == null) return PointF.Empty;
|
||||
return new PointF(
|
||||
(mousePos.X - _offset.X) / _scale,
|
||||
(mousePos.Y - _offset.Y) / _scale);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClampOffset()
|
||||
{
|
||||
if (_currentImage == null) return;
|
||||
lock (_imageLock)
|
||||
{
|
||||
if (_currentImage == null) return;
|
||||
|
||||
float imgWidth = _currentImage.Width * _scale;
|
||||
float imgHeight = _currentImage.Height * _scale;
|
||||
float imgWidth = _currentImage.Width * _scale;
|
||||
float imgHeight = _currentImage.Height * _scale;
|
||||
|
||||
if (imgWidth <= Width)
|
||||
{
|
||||
_offset.X = Math.Clamp(_offset.X, 0, Width - imgWidth);
|
||||
}
|
||||
else
|
||||
{
|
||||
_offset.X = Math.Clamp(_offset.X, Width - imgWidth, 0);
|
||||
}
|
||||
_offset.X = Math.Clamp(_offset.X,
|
||||
imgWidth > Width ? Width - imgWidth : 0,
|
||||
imgWidth > Width ? 0 : Width - imgWidth);
|
||||
|
||||
if (imgHeight <= Height)
|
||||
{
|
||||
_offset.Y = Math.Clamp(_offset.Y, 0, Height - imgHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
_offset.Y = Math.Clamp(_offset.Y, Height - imgHeight, 0);
|
||||
_offset.Y = Math.Clamp(_offset.Y,
|
||||
imgHeight > Height ? Height - imgHeight : 0,
|
||||
imgHeight > Height ? 0 : Height - imgHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatus(Point mousePos)
|
||||
{
|
||||
if (_currentImage == null) return;
|
||||
if (InvokeRequired)
|
||||
{
|
||||
BeginInvoke(new Action<Point>(UpdateStatus), mousePos);
|
||||
return;
|
||||
}
|
||||
|
||||
PointF imgPos = ConvertToImageCoords(mousePos);
|
||||
bool inImage = imgPos.X >= 0 && imgPos.X <= _currentImage.Width &&
|
||||
imgPos.Y >= 0 && imgPos.Y <= _currentImage.Height;
|
||||
lock (_imageLock)
|
||||
{
|
||||
if (_currentImage == null)
|
||||
{
|
||||
statusLabel.Text = "无有效图像";
|
||||
return;
|
||||
}
|
||||
|
||||
string roiInfo = _roiRect.IsEmpty ?
|
||||
"未选择区域" :
|
||||
$"选区: X={_roiRect.X:0} Y={_roiRect.Y:0} {_roiRect.Width:0}x{_roiRect.Height:0}";
|
||||
PointF imgPos = ConvertToImageCoords(mousePos);
|
||||
bool inImage = imgPos.X >= 0 && imgPos.X <= _currentImage.Width &&
|
||||
imgPos.Y >= 0 && imgPos.Y <= _currentImage.Height;
|
||||
|
||||
statusLabel.Text = inImage ?
|
||||
$"坐标: ({imgPos.X:0}, {imgPos.Y:0}) | 缩放: {_scale * 100:0}% | {roiInfo}" :
|
||||
$"图像尺寸: {_currentImage.Width}x{_currentImage.Height} | 缩放: {_scale * 100:0}% | {roiInfo}";
|
||||
statusLabel.Text = inImage ?
|
||||
$"坐标: ({imgPos.X:F1}, {imgPos.Y:F1}) | 缩放: {_scale * 100:0}%" :
|
||||
$"图像尺寸: {_currentImage.Width}x{_currentImage.Height} | 缩放: {_scale * 100:0}%";
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearROI()
|
||||
private void SafeInvalidate()
|
||||
{
|
||||
_roiRect = RectangleF.Empty;
|
||||
pictureBox.Invalidate(); // 触发重绘
|
||||
UpdateStatus(Point.Empty); // 更新状态栏
|
||||
if (InvokeRequired)
|
||||
{
|
||||
BeginInvoke(new Action(pictureBox.Invalidate));
|
||||
}
|
||||
else
|
||||
{
|
||||
pictureBox.Invalidate();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public Bitmap GetCurrentSnapshot()
|
||||
{
|
||||
lock (_imageLock)
|
||||
{
|
||||
// 返回深拷贝防止原始图像被修改
|
||||
return _currentImage?.Clone() as Bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
lock (_imageLock)
|
||||
{
|
||||
_currentImage?.Dispose();
|
||||
_currentImage = null;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user