Files
2026-10-20 17:03:23 +08:00

452 lines
12 KiB
C#
Raw Permalink 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 System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace WxOrder.Framework.Controls;
/// <summary>
/// 图片查看弹窗,直接静态调用:
/// ImageViewerWindow.Open(@"C:\a.jpg");
/// ImageViewerWindow.OpenDialog("https://example.com/a.png", "预览");
/// source 支持 string(路径/URL) / Uri / ImageSource / byte[] / Stream
/// </summary>
public partial class ImageViewerWindow : Window
{
private readonly object _source;
private ImageViewerWindow(object source, string title)
{
InitializeComponent();
// 保证 Img 位于 Canvas 中:
// 如果 Img 直接放在 Grid 里,图片比窗口大时会被 WPF 按窗口大小做布局裁剪,
// 缩放后只剩左上角一块,表现为“图片没居中”。
EnsureImageInCanvas();
var group = new TransformGroup();
group.Children.Add(_scaleT);
group.Children.Add(_translateT);
Img.RenderTransform = group;
Img.RenderTransformOrigin = new Point(0, 0);
_source = source;
Title = title ?? GuessTitle(source);
// 默认大小:屏幕工作区的 70%
Width = Math.Max(MinWidth, SystemParameters.WorkArea.Width * 0.7);
Height = Math.Max(MinHeight, SystemParameters.WorkArea.Height * 0.7);
ImageFailed += (s, ex) =>
{
MessageBox.Show(this, "图片加载失败:" + ex.Message, "提示",
MessageBoxButton.OK, MessageBoxImage.Warning);
Close();
};
// 放到 Loaded 里设置,这样加载失败时可以安全地关闭窗口
Loaded += (s, e) => Source = _source;
}
/// <summary>如果 Img 的父级不是 Canvas,就在原位置包一层 Canvas。</summary>
private void EnsureImageInCanvas()
{
if (Img.Parent is Canvas) return;
if (Img.Parent is not Panel parent) return;
int index = parent.Children.IndexOf(Img);
parent.Children.RemoveAt(index);
Img.Stretch = Stretch.None;
Img.HorizontalAlignment = HorizontalAlignment.Left;
Img.VerticalAlignment = VerticalAlignment.Top;
Canvas.SetLeft(Img, 0);
Canvas.SetTop(Img, 0);
var canvas = new Canvas { IsHitTestVisible = false };
canvas.Children.Add(Img);
parent.Children.Insert(index, canvas);
}
#region 静态调用入口
/// <summary>非模态弹出(不阻塞调用方)</summary>
public static void Open(object source, string title = null, Window owner = null)
{
RunOnUi(() => Create(source, title, owner).Show());
}
/// <summary>模态弹出(关闭前阻塞调用方)</summary>
public static void OpenDialog(object source, string title = null, Window owner = null)
{
RunOnUi(() => Create(source, title, owner).ShowDialog());
}
#endregion
private static ImageViewerWindow Create(object source, string title, Window owner)
{
var win = new ImageViewerWindow(source, title);
owner = owner ?? Application.Current?.MainWindow;
if (owner != null && owner.IsLoaded && owner != win)
{
win.Owner = owner;
win.WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
else
{
win.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
return win;
}
private static void RunOnUi(Action action)
{
var dispatcher = Application.Current?.Dispatcher
?? throw new InvalidOperationException("没有可用的 WPF Application。");
if (dispatcher.CheckAccess()) action();
else dispatcher.Invoke(action); // 支持从后台线程调用
}
private static string GuessTitle(object source)
{
try
{
if (source is string s && !string.IsNullOrWhiteSpace(s))
{
if (Uri.TryCreate(s, UriKind.Absolute, out var u))
return Path.GetFileName(u.IsFile ? u.LocalPath : u.AbsolutePath);
return Path.GetFileName(s);
}
if (source is Uri uri)
return Path.GetFileName(uri.IsFile ? uri.LocalPath : uri.AbsolutePath);
}
catch { /* 忽略,使用默认标题 */ }
return "图片查看";
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Escape: Close(); break;
case Key.Add:
case Key.OemPlus: ZoomIn(); break;
case Key.Subtract:
case Key.OemMinus: ZoomOut(); break;
case Key.D0:
case Key.NumPad0: FitToView(); break;
case Key.D1:
case Key.NumPad1: ActualSize(); break;
}
}
#region 依赖属性
/// <summary>图片来源:string(路径或URL) / Uri / ImageSource / byte[] / Stream</summary>
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageViewerWindow),
new PropertyMetadata(null, (d, e) => ((ImageViewerWindow)d).LoadSource(e.NewValue)));
public static readonly DependencyProperty ShowToolbarProperty =
DependencyProperty.Register(nameof(ShowToolbar), typeof(bool), typeof(ImageViewerWindow),
new PropertyMetadata(true, (d, e) =>
((ImageViewerWindow)d).ToolBar.Visibility = (bool)e.NewValue ? Visibility.Visible : Visibility.Collapsed));
public object Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
public bool ShowToolbar
{
get => (bool)GetValue(ShowToolbarProperty);
set => SetValue(ShowToolbarProperty, value);
}
#endregion
#region 公共属性 / 事件
public double MinZoom { get; set; } = 0.05;
public double MaxZoom { get; set; } = 32;
/// <summary>当前缩放比例(1 = 100%)</summary>
public double Zoom
{
get => _scale;
set => ZoomAt(ViewCenter, value);
}
public event EventHandler ZoomChanged;
public event EventHandler<Exception> ImageFailed;
#endregion
private readonly ScaleTransform _scaleT = new ScaleTransform();
private readonly TranslateTransform _translateT = new TranslateTransform();
private ImageSource _image;
private BitmapSource _pending; // 正在下载的网络图片
private double _scale = 1, _tx, _ty;
private bool _fit = true; // 是否处于"适应窗口"状态(窗口大小变化时自动重新适应)
private bool _dragging;
private Point _lastPos;
private Point ViewCenter => new Point(Root.ActualWidth / 2, Root.ActualHeight / 2);
#region 加载图片
private void LoadSource(object src)
{
DetachPending();
try
{
var img = CreateImage(src);
if (img is BitmapSource bs && bs.IsDownloading)
{
_pending = bs;
bs.DownloadCompleted += OnDownloadCompleted;
bs.DownloadFailed += OnDownloadFailed;
SetImage(null);
return;
}
SetImage(img);
}
catch (Exception ex)
{
SetImage(null);
ImageFailed?.Invoke(this, ex);
}
}
private void DetachPending()
{
if (_pending == null) return;
_pending.DownloadCompleted -= OnDownloadCompleted;
_pending.DownloadFailed -= OnDownloadFailed;
_pending = null;
}
private void OnDownloadCompleted(object sender, EventArgs e)
{
var bs = (BitmapSource)sender;
if (bs != _pending) return;
DetachPending();
if (bs.CanFreeze) bs.Freeze();
SetImage(bs);
}
private void OnDownloadFailed(object sender, ExceptionEventArgs e)
{
DetachPending();
SetImage(null);
ImageFailed?.Invoke(this, e.ErrorException);
}
private static ImageSource CreateImage(object src)
{
switch (src)
{
case null: return null;
case ImageSource s: return s;
case Uri u: return FromUri(u);
case string str:
var uri = ToUri(str);
return uri == null ? null : FromUri(uri);
case byte[] bytes:
using (var ms = new MemoryStream(bytes)) return FromStream(ms);
case Stream stream: return FromStream(stream);
default:
throw new NotSupportedException("不支持的图片来源类型:" + src.GetType().FullName);
}
}
private static Uri ToUri(string s)
{
if (string.IsNullOrWhiteSpace(s)) return null;
if (Uri.TryCreate(s, UriKind.Absolute, out var u)) return u; // C:\a.png / http://... / pack://...
return new Uri(Path.GetFullPath(s)); // 相对路径
}
private static BitmapImage FromUri(Uri uri)
{
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.UriSource = uri;
bmp.CacheOption = BitmapCacheOption.OnLoad; // 加载后不占用文件
bmp.CreateOptions = BitmapCreateOptions.IgnoreImageCache; // 文件被替换后能刷新
bmp.EndInit();
if (!bmp.IsDownloading && bmp.CanFreeze) bmp.Freeze();
return bmp;
}
private static BitmapImage FromStream(Stream stream)
{
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.StreamSource = stream;
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit();
bmp.Freeze();
return bmp;
}
private void SetImage(ImageSource img)
{
_image = img;
Img.Source = img;
_fit = true;
FitToView();
}
#endregion
#region 缩放 / 平移
/// <summary>适应窗口(只缩小,不放大小图)</summary>
public void FitToView()
{
_fit = true;
if (_image == null) return;
double cw = Root.ActualWidth, ch = Root.ActualHeight;
if (cw <= 0 || ch <= 0 || _image.Width <= 0 || _image.Height <= 0) return;
CenterAtScale(Math.Min(1.0, Math.Min(cw / _image.Width, ch / _image.Height)));
}
/// <summary>按原始大小显示,并将图片几何中心放到视口中心。</summary>
public void ActualSize()
{
CenterAtScale(1.0);
_fit = false;
}
public void ZoomIn() => ZoomAt(ViewCenter, _scale * 1.25);
public void ZoomOut() => ZoomAt(ViewCenter, _scale / 1.25);
/// <summary>以 p(控件坐标)为不动点缩放到 newScale</summary>
private void ZoomAt(Point p, double newScale)
{
if (_image == null) return;
newScale = Math.Max(MinZoom, Math.Min(MaxZoom, newScale));
double ratio = newScale / _scale;
_tx = p.X - (p.X - _tx) * ratio;
_ty = p.Y - (p.Y - _ty) * ratio;
_scale = newScale;
_fit = false;
ApplyTransform();
}
/// <summary>按指定比例显示,并将图片几何中心与视口中心对齐。</summary>
private void CenterAtScale(double newScale)
{
if (_image == null) return;
_scale = Math.Max(MinZoom, Math.Min(MaxZoom, newScale));
_tx = (Root.ActualWidth - _image.Width * _scale) / 2;
_ty = (Root.ActualHeight - _image.Height * _scale) / 2;
ApplyTransform();
}
private void ApplyTransform()
{
if (_image == null)
{
ZoomText.Text = "--";
return;
}
ClampOffset();
_scaleT.ScaleX = _scaleT.ScaleY = _scale;
_translateT.X = _tx;
_translateT.Y = _ty;
// 缩小用高质量,放大到像素级时用最近邻(能看清像素)
RenderOptions.SetBitmapScalingMode(Img,
_scale >= 3 ? BitmapScalingMode.NearestNeighbor :
_scale < 1 ? BitmapScalingMode.Fant : BitmapScalingMode.Linear);
ZoomText.Text = $"{_scale * 100:0}%";
ZoomChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>图片小于视口时居中;大于视口时不允许拖出空白</summary>
private void ClampOffset()
{
double cw = Root.ActualWidth, ch = Root.ActualHeight;
double w = _image.Width * _scale, h = _image.Height * _scale;
_tx = w <= cw ? (cw - w) / 2 : Math.Min(0, Math.Max(cw - w, _tx));
_ty = h <= ch ? (ch - h) / 2 : Math.Min(0, Math.Max(ch - h, _ty));
}
#endregion
#region 事件处理
private void Root_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (_image == null) return;
if (_fit) FitToView();
else ApplyTransform();
}
private void Root_MouseWheel(object sender, MouseWheelEventArgs e)
{
if (_image == null) return;
double factor = Math.Pow(1.001, e.Delta); // 一格滚轮约 ×1.13,触摸板更平滑
ZoomAt(e.GetPosition(Root), _scale * factor);
e.Handled = true;
}
private void Root_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (_image == null) return;
if (e.ClickCount == 2) // 双击:适应窗口 <-> 100%
{
if (_fit) ZoomAt(e.GetPosition(Root), 1.0);
else FitToView();
return;
}
_dragging = true;
_lastPos = e.GetPosition(Root);
Root.CaptureMouse();
Root.Cursor = Cursors.SizeAll;
}
private void Root_MouseMove(object sender, MouseEventArgs e)
{
if (!_dragging) return;
var pos = e.GetPosition(Root);
_tx += pos.X - _lastPos.X;
_ty += pos.Y - _lastPos.Y;
_lastPos = pos;
_fit = false;
ApplyTransform();
}
private void Root_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => EndDrag();
private void Root_LostMouseCapture(object sender, MouseEventArgs e) => EndDrag();
private void EndDrag()
{
if (!_dragging) return;
_dragging = false;
Root.ReleaseMouseCapture();
Root.Cursor = null;
}
private void ZoomIn_Click(object sender, RoutedEventArgs e) => ZoomIn();
private void ZoomOut_Click(object sender, RoutedEventArgs e) => ZoomOut();
private void Fit_Click(object sender, RoutedEventArgs e) => FitToView();
private void Actual_Click(object sender, RoutedEventArgs e) => ActualSize();
#endregion
}