WorkCamera/client/WorkCameraExport/Forms/ImageViewerForm.cs
2026-01-05 23:58:56 +08:00

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
}
}