555 lines
16 KiB
C#
555 lines
16 KiB
C#
using System.Drawing.Drawing2D;
|
|
using WorkCameraExport.Services;
|
|
|
|
namespace WorkCameraExport.Forms
|
|
{
|
|
/// <summary>
|
|
/// 图片查看器窗体 - 支持缩放、拖拽、轮播
|
|
/// Requirements: 3.3, 3.4, 3.5, 3.6
|
|
/// </summary>
|
|
public partial class ImageViewerForm : Form
|
|
{
|
|
private readonly List<string> _imageUrls;
|
|
private readonly Dictionary<string, Image?> _imageCache;
|
|
private readonly ApiService? _apiService;
|
|
|
|
private int _currentIndex;
|
|
private float _zoomFactor = 1.0f;
|
|
private PointF _imageOffset;
|
|
private Point _lastMousePosition;
|
|
private bool _isDragging;
|
|
private Image? _currentImage;
|
|
|
|
// 缩放限制
|
|
private const float MinZoom = 0.1f;
|
|
private const float MaxZoom = 10.0f;
|
|
private const float ZoomStep = 0.1f;
|
|
|
|
/// <summary>
|
|
/// 当前图片索引
|
|
/// </summary>
|
|
public int CurrentIndex
|
|
{
|
|
get => _currentIndex;
|
|
private set
|
|
{
|
|
if (_imageUrls.Count == 0) return;
|
|
|
|
// 循环索引
|
|
_currentIndex = WrapIndex(value, _imageUrls.Count);
|
|
UpdateDisplay();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 图片总数
|
|
/// </summary>
|
|
public int ImageCount => _imageUrls.Count;
|
|
|
|
/// <summary>
|
|
/// 构造函数
|
|
/// </summary>
|
|
/// <param name="imageUrls">图片 URL 列表</param>
|
|
/// <param name="startIndex">起始索引</param>
|
|
/// <param name="apiService">API 服务(用于下载图片)</param>
|
|
public ImageViewerForm(List<string> imageUrls, int startIndex = 0, ApiService? apiService = null)
|
|
{
|
|
InitializeComponent();
|
|
|
|
_imageUrls = imageUrls ?? new List<string>();
|
|
_imageCache = new Dictionary<string, Image?>();
|
|
_apiService = apiService;
|
|
_currentIndex = WrapIndex(startIndex, Math.Max(1, _imageUrls.Count));
|
|
_imageOffset = PointF.Empty;
|
|
|
|
SetupEventHandlers();
|
|
|
|
if (_imageUrls.Count > 0)
|
|
{
|
|
_ = LoadCurrentImageAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置事件处理器
|
|
/// </summary>
|
|
private void SetupEventHandlers()
|
|
{
|
|
// 图片面板事件
|
|
panelImage.Paint += PanelImage_Paint;
|
|
panelImage.MouseWheel += PanelImage_MouseWheel;
|
|
panelImage.MouseDown += PanelImage_MouseDown;
|
|
panelImage.MouseMove += PanelImage_MouseMove;
|
|
panelImage.MouseUp += PanelImage_MouseUp;
|
|
panelImage.Resize += PanelImage_Resize;
|
|
|
|
// 键盘事件
|
|
this.KeyPreview = true;
|
|
this.KeyDown += ImageViewerForm_KeyDown;
|
|
}
|
|
|
|
#region 图片导航
|
|
|
|
/// <summary>
|
|
/// 显示下一张图片
|
|
/// </summary>
|
|
public void NextImage()
|
|
{
|
|
if (_imageUrls.Count <= 1) return;
|
|
CurrentIndex = _currentIndex + 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 显示上一张图片
|
|
/// </summary>
|
|
public void PreviousImage()
|
|
{
|
|
if (_imageUrls.Count <= 1) return;
|
|
CurrentIndex = _currentIndex - 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 显示指定索引的图片
|
|
/// </summary>
|
|
public void ShowImage(int index)
|
|
{
|
|
CurrentIndex = index;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 循环索引计算
|
|
/// 确保索引始终在 [0, count-1] 范围内
|
|
/// </summary>
|
|
public static int WrapIndex(int index, int count)
|
|
{
|
|
if (count <= 0) return 0;
|
|
|
|
// 处理负数索引
|
|
index = index % count;
|
|
if (index < 0) index += count;
|
|
|
|
return index;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 图片加载
|
|
|
|
/// <summary>
|
|
/// 加载当前图片
|
|
/// </summary>
|
|
private async Task LoadCurrentImageAsync()
|
|
{
|
|
if (_imageUrls.Count == 0) return;
|
|
|
|
var url = _imageUrls[_currentIndex];
|
|
lblStatus.Text = $"正在加载图片 {_currentIndex + 1}/{_imageUrls.Count}...";
|
|
lblStatus.ForeColor = Color.Blue;
|
|
|
|
try
|
|
{
|
|
// 检查缓存
|
|
if (_imageCache.TryGetValue(url, out var cachedImage) && cachedImage != null)
|
|
{
|
|
_currentImage = cachedImage;
|
|
ResetView();
|
|
UpdateStatusLabel();
|
|
panelImage.Invalidate();
|
|
return;
|
|
}
|
|
|
|
// 下载图片
|
|
byte[]? imageData = null;
|
|
|
|
if (_apiService != null)
|
|
{
|
|
var (success, data, _) = await _apiService.DownloadImageAsync(url, CancellationToken.None);
|
|
if (success) imageData = data;
|
|
}
|
|
else
|
|
{
|
|
// 直接使用 HttpClient 下载
|
|
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
|
|
imageData = await httpClient.GetByteArrayAsync(url);
|
|
}
|
|
|
|
if (imageData != null && imageData.Length > 0)
|
|
{
|
|
using var ms = new MemoryStream(imageData);
|
|
var image = Image.FromStream(ms);
|
|
_imageCache[url] = image;
|
|
_currentImage = image;
|
|
ResetView();
|
|
UpdateStatusLabel();
|
|
}
|
|
else
|
|
{
|
|
_currentImage = null;
|
|
lblStatus.Text = "图片加载失败";
|
|
lblStatus.ForeColor = Color.Red;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_currentImage = null;
|
|
lblStatus.Text = $"加载失败: {ex.Message}";
|
|
lblStatus.ForeColor = Color.Red;
|
|
}
|
|
|
|
panelImage.Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 更新显示
|
|
/// </summary>
|
|
private void UpdateDisplay()
|
|
{
|
|
_ = LoadCurrentImageAsync();
|
|
UpdateNavigationButtons();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 更新状态标签
|
|
/// </summary>
|
|
private void UpdateStatusLabel()
|
|
{
|
|
if (_currentImage != null)
|
|
{
|
|
lblStatus.Text = $"图片 {_currentIndex + 1}/{_imageUrls.Count} | " +
|
|
$"尺寸: {_currentImage.Width}x{_currentImage.Height} | " +
|
|
$"缩放: {_zoomFactor:P0}";
|
|
lblStatus.ForeColor = Color.FromArgb(51, 51, 51);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 更新导航按钮状态
|
|
/// </summary>
|
|
private void UpdateNavigationButtons()
|
|
{
|
|
btnPrevious.Enabled = _imageUrls.Count > 1;
|
|
btnNext.Enabled = _imageUrls.Count > 1;
|
|
lblImageIndex.Text = _imageUrls.Count > 0
|
|
? $"{_currentIndex + 1} / {_imageUrls.Count}"
|
|
: "0 / 0";
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 缩放功能
|
|
|
|
/// <summary>
|
|
/// 放大图片
|
|
/// </summary>
|
|
public void ZoomIn()
|
|
{
|
|
SetZoom(_zoomFactor + ZoomStep);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 缩小图片
|
|
/// </summary>
|
|
public void ZoomOut()
|
|
{
|
|
SetZoom(_zoomFactor - ZoomStep);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重置缩放
|
|
/// </summary>
|
|
public void ResetZoom()
|
|
{
|
|
_zoomFactor = 1.0f;
|
|
_imageOffset = PointF.Empty;
|
|
UpdateStatusLabel();
|
|
panelImage.Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 适应窗口大小
|
|
/// </summary>
|
|
public void FitToWindow()
|
|
{
|
|
if (_currentImage == null) return;
|
|
|
|
var panelWidth = panelImage.ClientSize.Width;
|
|
var panelHeight = panelImage.ClientSize.Height;
|
|
var imageWidth = _currentImage.Width;
|
|
var imageHeight = _currentImage.Height;
|
|
|
|
var scaleX = (float)panelWidth / imageWidth;
|
|
var scaleY = (float)panelHeight / imageHeight;
|
|
|
|
_zoomFactor = Math.Min(scaleX, scaleY);
|
|
_zoomFactor = Math.Clamp(_zoomFactor, MinZoom, MaxZoom);
|
|
_imageOffset = PointF.Empty;
|
|
|
|
UpdateStatusLabel();
|
|
panelImage.Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置缩放比例
|
|
/// </summary>
|
|
private void SetZoom(float zoom)
|
|
{
|
|
_zoomFactor = Math.Clamp(zoom, MinZoom, MaxZoom);
|
|
UpdateStatusLabel();
|
|
panelImage.Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重置视图
|
|
/// </summary>
|
|
private void ResetView()
|
|
{
|
|
_zoomFactor = 1.0f;
|
|
_imageOffset = PointF.Empty;
|
|
FitToWindow();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 绘制
|
|
|
|
/// <summary>
|
|
/// 绘制图片
|
|
/// </summary>
|
|
private void PanelImage_Paint(object? sender, PaintEventArgs e)
|
|
{
|
|
var g = e.Graphics;
|
|
g.Clear(Color.FromArgb(30, 30, 30));
|
|
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
|
g.SmoothingMode = SmoothingMode.HighQuality;
|
|
|
|
if (_currentImage == null)
|
|
{
|
|
// 显示无图片提示
|
|
using var font = new Font("Microsoft YaHei UI", 14f);
|
|
using var brush = new SolidBrush(Color.Gray);
|
|
var text = _imageUrls.Count == 0 ? "没有图片" : "图片加载中...";
|
|
var size = g.MeasureString(text, font);
|
|
var x = (panelImage.Width - size.Width) / 2;
|
|
var y = (panelImage.Height - size.Height) / 2;
|
|
g.DrawString(text, font, brush, x, y);
|
|
return;
|
|
}
|
|
|
|
// 计算图片绘制位置和大小
|
|
var scaledWidth = _currentImage.Width * _zoomFactor;
|
|
var scaledHeight = _currentImage.Height * _zoomFactor;
|
|
|
|
// 居中显示
|
|
var centerX = (panelImage.Width - scaledWidth) / 2 + _imageOffset.X;
|
|
var centerY = (panelImage.Height - scaledHeight) / 2 + _imageOffset.Y;
|
|
|
|
var destRect = new RectangleF(centerX, centerY, scaledWidth, scaledHeight);
|
|
var srcRect = new RectangleF(0, 0, _currentImage.Width, _currentImage.Height);
|
|
|
|
g.DrawImage(_currentImage, destRect, srcRect, GraphicsUnit.Pixel);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 鼠标事件
|
|
|
|
/// <summary>
|
|
/// 鼠标滚轮缩放
|
|
/// </summary>
|
|
private void PanelImage_MouseWheel(object? sender, MouseEventArgs e)
|
|
{
|
|
if (_currentImage == null) return;
|
|
|
|
var delta = e.Delta > 0 ? ZoomStep : -ZoomStep;
|
|
SetZoom(_zoomFactor + delta);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 鼠标按下 - 开始拖拽
|
|
/// </summary>
|
|
private void PanelImage_MouseDown(object? sender, MouseEventArgs e)
|
|
{
|
|
if (e.Button == MouseButtons.Left && _currentImage != null)
|
|
{
|
|
_isDragging = true;
|
|
_lastMousePosition = e.Location;
|
|
panelImage.Cursor = Cursors.Hand;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 鼠标移动 - 拖拽图片
|
|
/// </summary>
|
|
private void PanelImage_MouseMove(object? sender, MouseEventArgs e)
|
|
{
|
|
if (_isDragging && _currentImage != null)
|
|
{
|
|
var deltaX = e.X - _lastMousePosition.X;
|
|
var deltaY = e.Y - _lastMousePosition.Y;
|
|
|
|
_imageOffset = new PointF(
|
|
_imageOffset.X + deltaX,
|
|
_imageOffset.Y + deltaY);
|
|
|
|
_lastMousePosition = e.Location;
|
|
panelImage.Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 鼠标释放 - 结束拖拽
|
|
/// </summary>
|
|
private void PanelImage_MouseUp(object? sender, MouseEventArgs e)
|
|
{
|
|
if (e.Button == MouseButtons.Left)
|
|
{
|
|
_isDragging = false;
|
|
panelImage.Cursor = Cursors.Default;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 面板大小改变
|
|
/// </summary>
|
|
private void PanelImage_Resize(object? sender, EventArgs e)
|
|
{
|
|
panelImage.Invalidate();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 键盘事件
|
|
|
|
/// <summary>
|
|
/// 键盘按键处理
|
|
/// </summary>
|
|
private void ImageViewerForm_KeyDown(object? sender, KeyEventArgs e)
|
|
{
|
|
switch (e.KeyCode)
|
|
{
|
|
case Keys.Left:
|
|
case Keys.A:
|
|
PreviousImage();
|
|
e.Handled = true;
|
|
break;
|
|
|
|
case Keys.Right:
|
|
case Keys.D:
|
|
NextImage();
|
|
e.Handled = true;
|
|
break;
|
|
|
|
case Keys.Add:
|
|
case Keys.Oemplus:
|
|
ZoomIn();
|
|
e.Handled = true;
|
|
break;
|
|
|
|
case Keys.Subtract:
|
|
case Keys.OemMinus:
|
|
ZoomOut();
|
|
e.Handled = true;
|
|
break;
|
|
|
|
case Keys.D0:
|
|
case Keys.NumPad0:
|
|
ResetZoom();
|
|
e.Handled = true;
|
|
break;
|
|
|
|
case Keys.F:
|
|
FitToWindow();
|
|
e.Handled = true;
|
|
break;
|
|
|
|
case Keys.Escape:
|
|
this.Close();
|
|
e.Handled = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 按钮事件
|
|
|
|
/// <summary>
|
|
/// 上一张按钮点击
|
|
/// </summary>
|
|
private void btnPrevious_Click(object sender, EventArgs e)
|
|
{
|
|
PreviousImage();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 下一张按钮点击
|
|
/// </summary>
|
|
private void btnNext_Click(object sender, EventArgs e)
|
|
{
|
|
NextImage();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 放大按钮点击
|
|
/// </summary>
|
|
private void btnZoomIn_Click(object sender, EventArgs e)
|
|
{
|
|
ZoomIn();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 缩小按钮点击
|
|
/// </summary>
|
|
private void btnZoomOut_Click(object sender, EventArgs e)
|
|
{
|
|
ZoomOut();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重置按钮点击
|
|
/// </summary>
|
|
private void btnReset_Click(object sender, EventArgs e)
|
|
{
|
|
ResetZoom();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 适应窗口按钮点击
|
|
/// </summary>
|
|
private void btnFit_Click(object sender, EventArgs e)
|
|
{
|
|
FitToWindow();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 关闭按钮点击
|
|
/// </summary>
|
|
private void btnClose_Click(object sender, EventArgs e)
|
|
{
|
|
this.Close();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 清理
|
|
|
|
/// <summary>
|
|
/// 窗体关闭时清理资源
|
|
/// </summary>
|
|
protected override void OnFormClosed(FormClosedEventArgs e)
|
|
{
|
|
base.OnFormClosed(e);
|
|
|
|
// 清理图片缓存
|
|
foreach (var image in _imageCache.Values)
|
|
{
|
|
image?.Dispose();
|
|
}
|
|
_imageCache.Clear();
|
|
_currentImage = null;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|