346 lines
10 KiB
C#
346 lines
10 KiB
C#
using System;
|
||
using System.Drawing;
|
||
using System.Drawing.Drawing2D;
|
||
using System.Windows.Forms;
|
||
|
||
namespace DHSoftware.Views
|
||
{
|
||
public partial class ImageViewerControl : UserControl
|
||
{
|
||
#region 内部控件
|
||
private PictureBox pictureBox;
|
||
private Label statusLabel;
|
||
#endregion
|
||
|
||
#region 核心字段
|
||
private Bitmap _currentImage;
|
||
private float _scale = 1.0f;
|
||
private PointF _offset = PointF.Empty;
|
||
private Point _dragStart;
|
||
private bool _isDragging;
|
||
private readonly object _imageLock = new object();
|
||
#endregion
|
||
|
||
#region 公开属性
|
||
public Bitmap Image
|
||
{
|
||
get
|
||
{
|
||
lock (_imageLock)
|
||
{
|
||
return _currentImage?.Clone() as Bitmap;
|
||
}
|
||
}
|
||
set
|
||
{
|
||
Bitmap newImage = value?.Clone() as Bitmap;
|
||
Bitmap oldImageToDispose = null;
|
||
|
||
lock (_imageLock)
|
||
{
|
||
// 交换图像引用
|
||
oldImageToDispose = _currentImage;
|
||
_currentImage = newImage;
|
||
|
||
if (_currentImage != null)
|
||
{
|
||
if (oldImageToDispose?.Size != _currentImage.Size)
|
||
{
|
||
AutoFit();
|
||
}
|
||
else
|
||
{
|
||
ClampOffset();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 在锁外安全释放旧图像
|
||
if (oldImageToDispose != null)
|
||
{
|
||
// 使用BeginInvoke确保在UI线程释放资源
|
||
BeginInvoke(new Action(() =>
|
||
{
|
||
oldImageToDispose.Dispose();
|
||
oldImageToDispose = null;
|
||
}));
|
||
}
|
||
|
||
SafeInvalidate();
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
public ImageViewerControl()
|
||
{
|
||
InitializeComponents();
|
||
SetupDoubleBuffering();
|
||
}
|
||
|
||
#region 初始化
|
||
private void InitializeComponents()
|
||
{
|
||
pictureBox = new PictureBox
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
BackColor = Color.DarkGray,
|
||
Cursor = Cursors.Hand
|
||
};
|
||
|
||
statusLabel = new Label
|
||
{
|
||
Dock = DockStyle.Bottom,
|
||
Height = 20,
|
||
Text = "就绪",
|
||
BorderStyle = BorderStyle.FixedSingle,
|
||
Font = new Font("Consolas", 10)
|
||
};
|
||
|
||
pictureBox.MouseDown += PictureBox_MouseDown;
|
||
pictureBox.MouseMove += PictureBox_MouseMove;
|
||
pictureBox.MouseUp += PictureBox_MouseUp;
|
||
pictureBox.MouseWheel += PictureBox_MouseWheel;
|
||
pictureBox.Paint += PictureBox_Paint;
|
||
|
||
Controls.Add(pictureBox);
|
||
Controls.Add(statusLabel);
|
||
}
|
||
|
||
private void SetupDoubleBuffering()
|
||
{
|
||
typeof(PictureBox).GetMethod("SetStyle",
|
||
System.Reflection.BindingFlags.NonPublic |
|
||
System.Reflection.BindingFlags.Instance)?
|
||
.Invoke(pictureBox, new object[] {
|
||
ControlStyles.OptimizedDoubleBuffer |
|
||
ControlStyles.AllPaintingInWmPaint,
|
||
true
|
||
});
|
||
}
|
||
#endregion
|
||
|
||
#region 核心功能
|
||
private void AutoFit()
|
||
{
|
||
lock (_imageLock)
|
||
{
|
||
if (_currentImage == null) return;
|
||
|
||
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
|
||
);
|
||
|
||
_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)
|
||
{
|
||
Bitmap drawImage = null;
|
||
RectangleF destRect;
|
||
float scale;
|
||
PointF offset;
|
||
|
||
// 创建临时绘图参数
|
||
lock (_imageLock)
|
||
{
|
||
if (_currentImage == null) return;
|
||
|
||
// 创建绘图副本
|
||
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 鼠标交互(左键拖动)
|
||
private void PictureBox_MouseDown(object sender, MouseEventArgs e)
|
||
{
|
||
if (InvokeRequired) return;
|
||
|
||
lock (_imageLock)
|
||
{
|
||
if (_currentImage == null) return;
|
||
|
||
if (e.Button == MouseButtons.Left)
|
||
{
|
||
_dragStart = e.Location;
|
||
_isDragging = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void PictureBox_MouseMove(object sender, MouseEventArgs e)
|
||
{
|
||
if (InvokeRequired) return;
|
||
|
||
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)
|
||
{
|
||
lock (_imageLock)
|
||
{
|
||
_isDragging = false;
|
||
}
|
||
}
|
||
|
||
private void PictureBox_MouseWheel(object sender, MouseEventArgs e)
|
||
{
|
||
if (InvokeRequired) return;
|
||
|
||
lock (_imageLock)
|
||
{
|
||
if (_currentImage == null) 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);
|
||
|
||
_offset.X = e.Location.X - imgPos.X * newScale;
|
||
_offset.Y = e.Location.Y - imgPos.Y * newScale;
|
||
|
||
_scale = newScale;
|
||
ClampOffset();
|
||
SafeInvalidate();
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region 辅助方法
|
||
private PointF ConvertToImageCoords(Point mousePos)
|
||
{
|
||
lock (_imageLock)
|
||
{
|
||
if (_currentImage == null) return PointF.Empty;
|
||
return new PointF(
|
||
(mousePos.X - _offset.X) / _scale,
|
||
(mousePos.Y - _offset.Y) / _scale);
|
||
}
|
||
}
|
||
|
||
private void ClampOffset()
|
||
{
|
||
lock (_imageLock)
|
||
{
|
||
if (_currentImage == null) return;
|
||
|
||
float imgWidth = _currentImage.Width * _scale;
|
||
float imgHeight = _currentImage.Height * _scale;
|
||
|
||
_offset.X = Math.Clamp(_offset.X,
|
||
imgWidth > Width ? Width - imgWidth : 0,
|
||
imgWidth > Width ? 0 : Width - imgWidth);
|
||
|
||
_offset.Y = Math.Clamp(_offset.Y,
|
||
imgHeight > Height ? Height - imgHeight : 0,
|
||
imgHeight > Height ? 0 : Height - imgHeight);
|
||
}
|
||
}
|
||
|
||
private void UpdateStatus(Point mousePos)
|
||
{
|
||
if (InvokeRequired)
|
||
{
|
||
BeginInvoke(new Action<Point>(UpdateStatus), mousePos);
|
||
return;
|
||
}
|
||
|
||
lock (_imageLock)
|
||
{
|
||
if (_currentImage == null)
|
||
{
|
||
statusLabel.Text = "无有效图像";
|
||
return;
|
||
}
|
||
|
||
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:F1}, {imgPos.Y:F1}) | 缩放: {_scale * 100:0}%" :
|
||
$"图像尺寸: {_currentImage.Width}x{_currentImage.Height} | 缩放: {_scale * 100:0}%";
|
||
}
|
||
}
|
||
|
||
private void SafeInvalidate()
|
||
{
|
||
if (InvokeRequired)
|
||
{
|
||
BeginInvoke(new Action(pictureBox.Invalidate));
|
||
}
|
||
else
|
||
{
|
||
pictureBox.Invalidate();
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
public Bitmap GetCurrentSnapshot()
|
||
{
|
||
lock (_imageLock)
|
||
{
|
||
// 返回深拷贝防止原始图像被修改
|
||
return _currentImage?.Clone() as Bitmap;
|
||
}
|
||
}
|
||
|
||
protected override void Dispose(bool disposing)
|
||
{
|
||
if (disposing && (components != null))
|
||
{
|
||
components.Dispose();
|
||
}
|
||
lock (_imageLock)
|
||
{
|
||
_currentImage?.Dispose();
|
||
_currentImage = null;
|
||
}
|
||
base.Dispose(disposing);
|
||
}
|
||
}
|
||
} |