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;
///
/// 图片查看弹窗,直接静态调用:
/// ImageViewerWindow.Open(@"C:\a.jpg");
/// ImageViewerWindow.OpenDialog("https://example.com/a.png", "预览");
/// source 支持 string(路径/URL) / Uri / ImageSource / byte[] / Stream
///
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;
}
/// 如果 Img 的父级不是 Canvas,就在原位置包一层 Canvas。
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 静态调用入口
/// 非模态弹出(不阻塞调用方)
public static void Open(object source, string title = null, Window owner = null)
{
RunOnUi(() => Create(source, title, owner).Show());
}
/// 模态弹出(关闭前阻塞调用方)
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 依赖属性
/// 图片来源:string(路径或URL) / Uri / ImageSource / byte[] / Stream
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;
/// 当前缩放比例(1 = 100%)
public double Zoom
{
get => _scale;
set => ZoomAt(ViewCenter, value);
}
public event EventHandler ZoomChanged;
public event EventHandler 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 缩放 / 平移
/// 适应窗口(只缩小,不放大小图)
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)));
}
/// 按原始大小显示,并将图片几何中心放到视口中心。
public void ActualSize()
{
CenterAtScale(1.0);
_fit = false;
}
public void ZoomIn() => ZoomAt(ViewCenter, _scale * 1.25);
public void ZoomOut() => ZoomAt(ViewCenter, _scale / 1.25);
/// 以 p(控件坐标)为不动点缩放到 newScale
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();
}
/// 按指定比例显示,并将图片几何中心与视口中心对齐。
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);
}
/// 图片小于视口时居中;大于视口时不允许拖出空白
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
}