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 }