平移和缩放图像
Posted
技术标签:
【中文标题】平移和缩放图像【英文标题】:Pan & Zoom Image 【发布时间】:2010-10-19 00:33:03 【问题描述】:我想在 WPF 中创建一个简单的图像查看器,使用户能够:
平移(通过鼠标拖动图像)。 缩放(使用滑块)。 显示叠加层(例如矩形选择)。 显示原始图像(如果需要,可以使用滚动条)。你能解释一下怎么做吗?
我没有在网上找到好的样本。 我应该使用 ViewBox 吗?还是图像刷? 我需要 ScrollViewer 吗?
【问题讨论】:
要获得专业的 WPF 缩放控制,请查看ZoomPanel。它不是免费的,但非常易于使用并具有许多功能 - 动画缩放和平移,支持 ScrollViewer,支持鼠标滚轮,包括 ZoomController(带有移动、放大、缩小、矩形缩放、重置按钮)。它还附带许多代码示例。 我在 codeproject.com 上写了一篇关于 WPF 缩放和平移控件实现的文章。 codeproject.com/KB/WPF/zoomandpancontrol.aspx 很好的发现。免费试用,如果您打算使用它构建软件,他们需要 69 美元/计算机的许可证。这是一个要使用的 DLL,因此他们无法阻止您,但如果您为客户进行商业构建,尤其是需要声明和单独许可任何第三方实用程序的客户,您将不得不付费开发费。但是,在 EULA 中,它并没有说它是基于“每个应用程序”的,因此,一旦您注册了购买,那么您创建的所有应用程序都将是“免费”的,并且可以将您的付费许可证文件复制到用它来代表购买。 【参考方案1】: 平移:将图像放入画布中。实现 Mouse Up、Down 和 Move 事件以移动 Canvas.Top、Canvas.Left 属性。向下时,将 isDraggingFlag 标记为 true,向上时将标志设置为 false。在移动时,您检查是否设置了标志,如果是,则在画布内偏移图像上的 Canvas.Top 和 Canvas.Left 属性。 缩放:将滑块绑定到画布的缩放变换 显示叠加层:在包含图像的画布上添加其他没有背景的画布。 显示原始图像:ViewBox 内的图像控件【讨论】:
【参考方案2】:我解决此问题的方法是将图像放置在边框内,并将其 ClipToBounds 属性设置为 True。然后将图像上的 RenderTransformOrigin 设置为 0.5,0.5,因此图像将开始在图像的中心进行缩放。 RenderTransform 也设置为包含 ScaleTransform 和 TranslateTransform 的 TransformGroup。
然后我处理图像上的 MouseWheel 事件以实现缩放
private void image_MouseWheel(object sender, MouseWheelEventArgs e)
var st = (ScaleTransform)image.RenderTransform;
double zoom = e.Delta > 0 ? .2 : -.2;
st.ScaleX += zoom;
st.ScaleY += zoom;
为了处理平移,我做的第一件事是处理图像上的 MouseLeftButtonDown 事件,捕捉鼠标并记录它的位置,我还存储了 TranslateTransform 的当前值,这是为了实现平移而更新的内容。
Point start;
Point origin;
private void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
image.CaptureMouse();
var tt = (TranslateTransform)((TransformGroup)image.RenderTransform)
.Children.First(tr => tr is TranslateTransform);
start = e.GetPosition(border);
origin = new Point(tt.X, tt.Y);
然后我处理了 MouseMove 事件来更新 TranslateTransform。
private void image_MouseMove(object sender, MouseEventArgs e)
if (image.IsMouseCaptured)
var tt = (TranslateTransform)((TransformGroup)image.RenderTransform)
.Children.First(tr => tr is TranslateTransform);
Vector v = start - e.GetPosition(border);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
最后别忘了释放鼠标捕捉。
private void image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
image.ReleaseMouseCapture();
至于调整大小的选择句柄可以使用装饰器完成,请查看this article 了解更多信息。
【讨论】:
虽然有一个观察结果,在 image_MouseLeftButtonDown 中调用 CaptureMouse 将导致调用 image_MouseMove ,其中原点尚未初始化 - 在上面的代码中,纯机会为零,但如果原点不是(0,0),图像将经历短暂的跳跃。因此,我认为最好在 image_MouseLeftButtonDown 的末尾调用 image.CaptureMouse() 来解决这个问题。 两件事。 1) image_MouseWheel 有一个错误,您必须以与获得 TranslateTransform 类似的方式获得 ScaleTransform。也就是说,将其转换为 TransformGroup,然后选择并转换适当的 Child。 2)如果你的动作是紧张的,记住你不能使用图像来获得你的鼠标位置(因为它是动态的),你必须使用静态的东西。在此示例中,使用了边框。【参考方案3】:试试这个缩放控件:http://wpfextensions.codeplex.com
控件的用法很简单,参考wpfextensions组件比:
<wpfext:ZoomControl>
<Image Source="..."/>
</wpfext:ZoomControl>
目前不支持滚动条。 (它将在下一个版本中发布,一到两周后可用)。
【讨论】:
是的,很享受。不过,您图书馆的其余部分非常琐碎。 虽然似乎没有直接支持“显示叠加层(例如矩形选择)”,但对于缩放/平移行为,这是一个很好的控制。【参考方案4】:答案已在上面发布,但不完整。这是完整的版本:
XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MapTest.Window1"
x:Name="Window"
Title="Window1"
Width="1950" Height="1546" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Controls="clr-namespace:WPFExtensions.Controls;assembly=WPFExtensions" mc:Ignorable="d" Background="#FF000000">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="52.92"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="1" Name="border">
<Image Name="image" Source="map3-2.png" Opacity="1" RenderTransformOrigin="0.5,0.5" />
</Border>
</Grid>
代码背后
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace MapTest
public partial class Window1 : Window
private Point origin;
private Point start;
public Window1()
InitializeComponent();
TransformGroup group = new TransformGroup();
ScaleTransform xform = new ScaleTransform();
group.Children.Add(xform);
TranslateTransform tt = new TranslateTransform();
group.Children.Add(tt);
image.RenderTransform = group;
image.MouseWheel += image_MouseWheel;
image.MouseLeftButtonDown += image_MouseLeftButtonDown;
image.MouseLeftButtonUp += image_MouseLeftButtonUp;
image.MouseMove += image_MouseMove;
private void image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
image.ReleaseMouseCapture();
private void image_MouseMove(object sender, MouseEventArgs e)
if (!image.IsMouseCaptured) return;
var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform);
Vector v = start - e.GetPosition(border);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
private void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
image.CaptureMouse();
var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform);
start = e.GetPosition(border);
origin = new Point(tt.X, tt.Y);
private void image_MouseWheel(object sender, MouseWheelEventArgs e)
TransformGroup transformGroup = (TransformGroup) image.RenderTransform;
ScaleTransform transform = (ScaleTransform) transformGroup.Children[0];
double zoom = e.Delta > 0 ? .2 : -.2;
transform.ScaleX += zoom;
transform.ScaleY += zoom;
我的网站上有一个使用此代码的完整 wpf 项目示例:Jot the sticky note app。
【讨论】:
关于如何使它在 Silverlight 3 中可用的任何建议?我在使用 Vector 时遇到问题,并从另一个点中减去一个点...谢谢。 @Number8 在下面为您发布了适用于 Silverlight 3 的实现:) 一个小缺点 - 图像与边界一起增长,而不是在边界内 你们能否提出一些建议,如何在 Windows 8 Metro 风格应用程序中实现相同的东西。我正在使用 c#,Windows8 上的 xaml 在 image_MouseWheel 中,您可以测试 transform.ScaleX 和 ScaleY 值,如果这些值 + zoom > 您的限制,则不要应用 += 缩放线。【参考方案5】:要相对于鼠标位置进行缩放,您只需要:
var position = e.GetPosition(image1);
image1.RenderTransformOrigin = new Point(position.X / image1.ActualWidth, position.Y / image1.ActualHeight);
【讨论】:
我用的是 PictureBox,RenderTransformOrigin 已经不存在了。 @Switch RenderTransformOrigin 用于 WPF 控件。【参考方案6】:@Anothen 和 @Number8 - Vector 类在 Silverlight 中不可用,因此为了使其正常工作,我们只需要记录上次调用 MouseMove 事件时看到的最后位置,并将这两个点与找出差异;然后调整变换。
XAML:
<Border Name="viewboxBackground" Background="Black">
<Viewbox Name="viewboxMain">
<!--contents go here-->
</Viewbox>
</Border>
代码隐藏:
public Point _mouseClickPos;
public bool bMoving;
public MainPage()
InitializeComponent();
viewboxMain.RenderTransform = new CompositeTransform();
void MouseMoveHandler(object sender, MouseEventArgs e)
if (bMoving)
//get current transform
CompositeTransform transform = viewboxMain.RenderTransform as CompositeTransform;
Point currentPos = e.GetPosition(viewboxBackground);
transform.TranslateX += (currentPos.X - _mouseClickPos.X) ;
transform.TranslateY += (currentPos.Y - _mouseClickPos.Y) ;
viewboxMain.RenderTransform = transform;
_mouseClickPos = currentPos;
void MouseClickHandler(object sender, MouseButtonEventArgs e)
_mouseClickPos = e.GetPosition(viewboxBackground);
bMoving = true;
void MouseReleaseHandler(object sender, MouseButtonEventArgs e)
bMoving = false;
另外请注意,您不需要 TransformGroup 或集合来实现平移和缩放;相反,CompositeTransform 可以轻松解决问题。
我很确定这在资源使用方面确实效率低下,但至少它有效:)
【讨论】:
【参考方案7】:@默克
对于您的 lambda 表达式解决方案,您可以使用以下代码:
//var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform);
TranslateTransform tt = null;
TransformGroup transformGroup = (TransformGroup)grid.RenderTransform;
for (int i = 0; i < transformGroup.Children.Count; i++)
if (transformGroup.Children[i] is TranslateTransform)
tt = (TranslateTransform)transformGroup.Children[i];
此代码可按原样用于 .Net Frame work 3.0 或 2.0
希望对你有帮助:-)
【讨论】:
【参考方案8】:使用此问题中的示例后,我制作了完整版本的平移和缩放应用程序,并相对于鼠标指针进行了适当的缩放。所有平移和缩放代码已移至名为 ZoomBorder 的单独类。
ZoomBorder.cs
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace PanAndZoom
public class ZoomBorder : Border
private UIElement child = null;
private Point origin;
private Point start;
private TranslateTransform GetTranslateTransform(UIElement element)
return (TranslateTransform)((TransformGroup)element.RenderTransform)
.Children.First(tr => tr is TranslateTransform);
private ScaleTransform GetScaleTransform(UIElement element)
return (ScaleTransform)((TransformGroup)element.RenderTransform)
.Children.First(tr => tr is ScaleTransform);
public override UIElement Child
get return base.Child;
set
if (value != null && value != this.Child)
this.Initialize(value);
base.Child = value;
public void Initialize(UIElement element)
this.child = element;
if (child != null)
TransformGroup group = new TransformGroup();
ScaleTransform st = new ScaleTransform();
group.Children.Add(st);
TranslateTransform tt = new TranslateTransform();
group.Children.Add(tt);
child.RenderTransform = group;
child.RenderTransformOrigin = new Point(0.0, 0.0);
this.MouseWheel += child_MouseWheel;
this.MouseLeftButtonDown += child_MouseLeftButtonDown;
this.MouseLeftButtonUp += child_MouseLeftButtonUp;
this.MouseMove += child_MouseMove;
this.PreviewMouseRightButtonDown += new MouseButtonEventHandler(
child_PreviewMouseRightButtonDown);
public void Reset()
if (child != null)
// reset zoom
var st = GetScaleTransform(child);
st.ScaleX = 1.0;
st.ScaleY = 1.0;
// reset pan
var tt = GetTranslateTransform(child);
tt.X = 0.0;
tt.Y = 0.0;
#region Child Events
private void child_MouseWheel(object sender, MouseWheelEventArgs e)
if (child != null)
var st = GetScaleTransform(child);
var tt = GetTranslateTransform(child);
double zoom = e.Delta > 0 ? .2 : -.2;
if (!(e.Delta > 0) && (st.ScaleX < .4 || st.ScaleY < .4))
return;
Point relative = e.GetPosition(child);
double absoluteX;
double absoluteY;
absoluteX = relative.X * st.ScaleX + tt.X;
absoluteY = relative.Y * st.ScaleY + tt.Y;
st.ScaleX += zoom;
st.ScaleY += zoom;
tt.X = absoluteX - relative.X * st.ScaleX;
tt.Y = absoluteY - relative.Y * st.ScaleY;
private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
if (child != null)
var tt = GetTranslateTransform(child);
start = e.GetPosition(this);
origin = new Point(tt.X, tt.Y);
this.Cursor = Cursors.Hand;
child.CaptureMouse();
private void child_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
if (child != null)
child.ReleaseMouseCapture();
this.Cursor = Cursors.Arrow;
void child_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
this.Reset();
private void child_MouseMove(object sender, MouseEventArgs e)
if (child != null)
if (child.IsMouseCaptured)
var tt = GetTranslateTransform(child);
Vector v = start - e.GetPosition(this);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
#endregion
MainWindow.xaml
<Window x:Class="PanAndZoom.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PanAndZoom"
Title="PanAndZoom" Height="600" Width="900" WindowStartupLocation="CenterScreen">
<Grid>
<local:ZoomBorder x:Name="border" ClipToBounds="True" Background="Gray">
<Image Source="image.jpg"/>
</local:ZoomBorder>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace PanAndZoom
public partial class MainWindow : Window
public MainWindow()
InitializeComponent();
【讨论】:
很遗憾,我不能给你更多的点。这真的很棒。 在 cmets 因“干得好!”而被阻止之前或“伟大的工作”我只想说好工作和伟大的工作。这是一个 WPF 宝石。它将 wpf ext zoombox 吹出水面。 站出来。我今晚也许还能回家... +1000 太棒了。我没有想过这样的实现,但它真的很好!非常感谢! 很好的答案!我对缩放系数进行了轻微修正,因此它不会“变慢”double zoomCorrected = zoom*st.ScaleX; st.ScaleX += zoomCorrected; st.ScaleY += zoomCorrected;
【参考方案9】:
同类控件的另一个版本。它具有与其他类似的功能,但它增加了:
-
触摸支持(拖动/捏合)
可以删除图像(通常,图像控件会将图像锁定在磁盘上,因此您无法删除它)。
内边框子项,因此平移后的图像不会与边框重叠。如果边框带有圆角矩形,请查找 ClippedBorder 类。
用法很简单:
<Controls:ImageViewControl ImagePath="Binding ..." />
还有代码:
public class ImageViewControl : Border
private Point origin;
private Point start;
private Image image;
public ImageViewControl()
ClipToBounds = true;
Loaded += OnLoaded;
#region ImagePath
/// <summary>
/// ImagePath Dependency Property
/// </summary>
public static readonly DependencyProperty ImagePathProperty = DependencyProperty.Register("ImagePath", typeof (string), typeof (ImageViewControl), new FrameworkPropertyMetadata(string.Empty, OnImagePathChanged));
/// <summary>
/// Gets or sets the ImagePath property. This dependency property
/// indicates the path to the image file.
/// </summary>
public string ImagePath
get return (string) GetValue(ImagePathProperty);
set SetValue(ImagePathProperty, value);
/// <summary>
/// Handles changes to the ImagePath property.
/// </summary>
private static void OnImagePathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
var target = (ImageViewControl) d;
var oldImagePath = (string) e.OldValue;
var newImagePath = target.ImagePath;
target.ReloadImage(newImagePath);
target.OnImagePathChanged(oldImagePath, newImagePath);
/// <summary>
/// Provides derived classes an opportunity to handle changes to the ImagePath property.
/// </summary>
protected virtual void OnImagePathChanged(string oldImagePath, string newImagePath)
#endregion
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
image = new Image
//IsManipulationEnabled = true,
RenderTransformOrigin = new Point(0.5, 0.5),
RenderTransform = new TransformGroup
Children = new TransformCollection
new ScaleTransform(),
new TranslateTransform()
;
// NOTE I use a border as the first child, to which I add the image. I do this so the panned image doesn't partly obscure the control's border.
// In case you are going to use rounder corner's on this control, you may to update your clipping, as in this example:
// http://wpfspark.wordpress.com/2011/06/08/clipborder-a-wpf-border-that-clips/
var border = new Border
IsManipulationEnabled = true,
ClipToBounds = true,
Child = image
;
Child = border;
image.MouseWheel += (s, e) =>
var zoom = e.Delta > 0
? .2
: -.2;
var position = e.GetPosition(image);
image.RenderTransformOrigin = new Point(position.X / image.ActualWidth, position.Y / image.ActualHeight);
var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform);
st.ScaleX += zoom;
st.ScaleY += zoom;
e.Handled = true;
;
image.MouseLeftButtonDown += (s, e) =>
if (e.ClickCount == 2)
ResetPanZoom();
else
image.CaptureMouse();
var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform);
start = e.GetPosition(this);
origin = new Point(tt.X, tt.Y);
e.Handled = true;
;
image.MouseMove += (s, e) =>
if (!image.IsMouseCaptured) return;
var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform);
var v = start - e.GetPosition(this);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
e.Handled = true;
;
image.MouseLeftButtonUp += (s, e) => image.ReleaseMouseCapture();
//NOTE I apply the manipulation to the border, and not to the image itself (which caused stability issues when translating)!
border.ManipulationDelta += (o, e) =>
var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform);
var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform);
st.ScaleX *= e.DeltaManipulation.Scale.X;
st.ScaleY *= e.DeltaManipulation.Scale.X;
tt.X += e.DeltaManipulation.Translation.X;
tt.Y += e.DeltaManipulation.Translation.Y;
e.Handled = true;
;
private void ResetPanZoom()
var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform);
var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform);
st.ScaleX = st.ScaleY = 1;
tt.X = tt.Y = 0;
image.RenderTransformOrigin = new Point(0.5, 0.5);
/// <summary>
/// Load the image (and do not keep a hold on it, so we can delete the image without problems)
/// </summary>
/// <see cref="http://blogs.vertigo.com/personal/ralph/Blog/Lists/Posts/Post.aspx?ID=18"/>
/// <param name="path"></param>
private void ReloadImage(string path)
try
ResetPanZoom();
// load the image, specify CacheOption so the file is not locked
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.UriSource = new Uri(path, UriKind.RelativeOrAbsolute);
bitmapImage.EndInit();
image.Source = bitmapImage;
catch (SystemException e)
Console.WriteLine(e.Message);
【讨论】:
我发现的唯一问题是,如果在 XAML 中指定了图像的路径,它会尝试在构造图像对象之前(即在调用 OnLoaded 之前)呈现它。为了解决这个问题,我将“image = new Image ...”代码从 onLoaded 方法移到了构造函数中。谢谢。 其他问题是图像可以缩小到很小,直到我们什么也看不见。我添加了一点限制:if (image.ActualWidth*(st.ScaleX + zoom) < 200 || image.ActualHeight*(st.ScaleY + zoom) < 200) //don't zoom out too small. return;
in image.MouseWheel【参考方案10】:
这将放大和缩小以及平移,但将图像保持在容器的范围内。编写为控件,因此直接或通过Themes/Viewport.xaml
将样式添加到App.xaml
。
为了便于阅读,我还在 gist 和 github 上上传了此内容
我也在nuget上打包了这个
PM > Install-Package Han.Wpf.ViewportControl
./Controls/Viewport.cs:
public class Viewport : ContentControl
private bool _capture;
private FrameworkElement _content;
private Matrix _matrix;
private Point _origin;
public static readonly DependencyProperty MaxZoomProperty =
DependencyProperty.Register(
nameof(MaxZoom),
typeof(double),
typeof(Viewport),
new PropertyMetadata(0d));
public static readonly DependencyProperty MinZoomProperty =
DependencyProperty.Register(
nameof(MinZoom),
typeof(double),
typeof(Viewport),
new PropertyMetadata(0d));
public static readonly DependencyProperty ZoomSpeedProperty =
DependencyProperty.Register(
nameof(ZoomSpeed),
typeof(float),
typeof(Viewport),
new PropertyMetadata(0f));
public static readonly DependencyProperty ZoomXProperty =
DependencyProperty.Register(
nameof(ZoomX),
typeof(double),
typeof(Viewport),
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty ZoomYProperty =
DependencyProperty.Register(
nameof(ZoomY),
typeof(double),
typeof(Viewport),
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty OffsetXProperty =
DependencyProperty.Register(
nameof(OffsetX),
typeof(double),
typeof(Viewport),
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty OffsetYProperty =
DependencyProperty.Register(
nameof(OffsetY),
typeof(double),
typeof(Viewport),
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty BoundsProperty =
DependencyProperty.Register(
nameof(Bounds),
typeof(Rect),
typeof(Viewport),
new FrameworkPropertyMetadata(default(Rect), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public Rect Bounds
get => (Rect) GetValue(BoundsProperty);
set => SetValue(BoundsProperty, value);
public double MaxZoom
get => (double) GetValue(MaxZoomProperty);
set => SetValue(MaxZoomProperty, value);
public double MinZoom
get => (double) GetValue(MinZoomProperty);
set => SetValue(MinZoomProperty, value);
public double OffsetX
get => (double) GetValue(OffsetXProperty);
set => SetValue(OffsetXProperty, value);
public double OffsetY
get => (double) GetValue(OffsetYProperty);
set => SetValue(OffsetYProperty, value);
public float ZoomSpeed
get => (float) GetValue(ZoomSpeedProperty);
set => SetValue(ZoomSpeedProperty, value);
public double ZoomX
get => (double) GetValue(ZoomXProperty);
set => SetValue(ZoomXProperty, value);
public double ZoomY
get => (double) GetValue(ZoomYProperty);
set => SetValue(ZoomYProperty, value);
public Viewport()
DefaultStyleKey = typeof(Viewport);
Loaded += OnLoaded;
Unloaded += OnUnloaded;
private void Arrange(Size desired, Size render)
_matrix = Matrix.Identity;
var zx = desired.Width / render.Width;
var zy = desired.Height / render.Height;
var cx = render.Width < desired.Width ? render.Width / 2.0 : 0.0;
var cy = render.Height < desired.Height ? render.Height / 2.0 : 0.0;
var zoom = Math.Min(zx, zy);
if (render.Width > desired.Width &&
render.Height > desired.Height)
cx = (desired.Width - (render.Width * zoom)) / 2.0;
cy = (desired.Height - (render.Height * zoom)) / 2.0;
_matrix = new Matrix(zoom, 0d, 0d, zoom, cx, cy);
else
_matrix.ScaleAt(zoom, zoom, cx, cy);
private void Attach(FrameworkElement content)
content.MouseMove += OnMouseMove;
content.MouseLeave += OnMouseLeave;
content.MouseWheel += OnMouseWheel;
content.MouseLeftButtonDown += OnMouseLeftButtonDown;
content.MouseLeftButtonUp += OnMouseLeftButtonUp;
content.SizeChanged += OnSizeChanged;
content.MouseRightButtonDown += OnMouseRightButtonDown;
private void ChangeContent(FrameworkElement content)
if (content != null && !Equals(content, _content))
if (_content != null)
Detatch();
Attach(content);
_content = content;
private double Constrain(double value, double min, double max)
if (min > max)
min = max;
if (value <= min)
return min;
if (value >= max)
return max;
return value;
private void Constrain()
var x = Constrain(_matrix.OffsetX, _content.ActualWidth - _content.ActualWidth * _matrix.M11, 0);
var y = Constrain(_matrix.OffsetY, _content.ActualHeight - _content.ActualHeight * _matrix.M22, 0);
_matrix = new Matrix(_matrix.M11, 0d, 0d, _matrix.M22, x, y);
private void Detatch()
_content.MouseMove -= OnMouseMove;
_content.MouseLeave -= OnMouseLeave;
_content.MouseWheel -= OnMouseWheel;
_content.MouseLeftButtonDown -= OnMouseLeftButtonDown;
_content.MouseLeftButtonUp -= OnMouseLeftButtonUp;
_content.SizeChanged -= OnSizeChanged;
_content.MouseRightButtonDown -= OnMouseRightButtonDown;
private void Invalidate()
if (_content != null)
Constrain();
_content.RenderTransformOrigin = new Point(0, 0);
_content.RenderTransform = new MatrixTransform(_matrix);
_content.InvalidateVisual();
ZoomX = _matrix.M11;
ZoomY = _matrix.M22;
OffsetX = _matrix.OffsetX;
OffsetY = _matrix.OffsetY;
var rect = new Rect
X = OffsetX * -1,
Y = OffsetY * -1,
Width = ActualWidth,
Height = ActualHeight
;
Bounds = rect;
public override void OnApplyTemplate()
base.OnApplyTemplate();
_matrix = Matrix.Identity;
protected override void OnContentChanged(object oldContent, object newContent)
base.OnContentChanged(oldContent, newContent);
if (Content is FrameworkElement element)
ChangeContent(element);
private void OnLoaded(object sender, RoutedEventArgs e)
if (Content is FrameworkElement element)
ChangeContent(element);
SizeChanged += OnSizeChanged;
Loaded -= OnLoaded;
private void OnMouseLeave(object sender, MouseEventArgs e)
if (_capture)
Released();
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
if (IsEnabled && !_capture)
Pressed(e.GetPosition(this));
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
if (IsEnabled && _capture)
Released();
private void OnMouseMove(object sender, MouseEventArgs e)
if (IsEnabled && _capture)
var position = e.GetPosition(this);
var point = new Point
X = position.X - _origin.X,
Y = position.Y - _origin.Y
;
var delta = point;
_origin = position;
_matrix.Translate(delta.X, delta.Y);
Invalidate();
private void OnMouseRightButtonDown(object sender, MouseButtonEventArgs e)
if (IsEnabled)
Reset();
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
if (IsEnabled)
var scale = e.Delta > 0 ? ZoomSpeed : 1 / ZoomSpeed;
var position = e.GetPosition(_content);
var x = Constrain(scale, MinZoom / _matrix.M11, MaxZoom / _matrix.M11);
var y = Constrain(scale, MinZoom / _matrix.M22, MaxZoom / _matrix.M22);
_matrix.ScaleAtPrepend(x, y, position.X, position.Y);
ZoomX = _matrix.M11;
ZoomY = _matrix.M22;
Invalidate();
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
if (_content?.IsMeasureValid ?? false)
Arrange(_content.DesiredSize, _content.RenderSize);
Invalidate();
private void OnUnloaded(object sender, RoutedEventArgs e)
Detatch();
SizeChanged -= OnSizeChanged;
Unloaded -= OnUnloaded;
private void Pressed(Point position)
if (IsEnabled)
_content.Cursor = Cursors.Hand;
_origin = position;
_capture = true;
private void Released()
if (IsEnabled)
_content.Cursor = null;
_capture = false;
private void Reset()
_matrix = Matrix.Identity;
if (_content != null)
Arrange(_content.DesiredSize, _content.RenderSize);
Invalidate();
./Themes/Viewport.xaml:
<ResourceDictionary ... >
<Style TargetType="x:Type controls:Viewport"
BasedOn="StaticResource x:Type ContentControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="x:Type controls:Viewport">
<Border BorderBrush="TemplateBinding BorderBrush"
BorderThickness="TemplateBinding BorderThickness"
Background="TemplateBinding Background">
<Grid ClipToBounds="True"
Width="TemplateBinding Width"
Height="TemplateBinding Height">
<Grid x:Name="PART_Container">
<ContentPresenter x:Name="PART_Presenter" />
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
./App.xaml
<Application ... >
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="./Themes/Viewport.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
用法:
<viewers:Viewport>
<Image Source="Binding"/>
</viewers:Viewport>
有任何问题,请给我留言。
编码愉快:)
【讨论】:
太好了,我喜欢这个版本。有什么办法可以添加滚动条吗? 顺便说一句,您使用依赖属性的方式错误。对于缩放和翻译,您不能将代码放在属性设置器中,因为它在绑定时根本不会被调用。您需要在依赖属性本身上注册 Change 和 Coerce 处理程序并在那里完成工作。 自从写了这个答案后,我已经大大改变了它,我会更新它,修复我以后在生产中使用它时遇到的一些问题 这个解决方案很棒,但我不太明白为什么鼠标滚轮滚动功能在放大和缩小图像时似乎有一个奇怪的方向拉动,而不是使用鼠标指针位置作为缩放原点。我疯了还是对此有一些合乎逻辑的解释? 我正在努力让它在 ScrollViewer 控件中始终如一地工作。我对其进行了一些修改,以使用光标位置作为比例原点(使用鼠标位置放大和缩小),但实际上可以使用一些输入来让它在 ScrollViewer 内工作。谢谢!【参考方案11】:@Wiesław Šoltés 回答 above 提供的出色解决方案的一个补充
现有代码使用右键单击重置图像位置,但我更习惯通过双击来执行此操作。只需替换现有的 child_MouseLeftButtonDown 处理程序:
private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
if (child != null)
var tt = GetTranslateTransform(child);
start = e.GetPosition(this);
origin = new Point(tt.X, tt.Y);
this.Cursor = Cursors.Hand;
child.CaptureMouse();
有了这个:
private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
if ((e.ChangedButton == MouseButton.Left && e.ClickCount == 1))
if (child != null)
var tt = GetTranslateTransform(child);
start = e.GetPosition(this);
origin = new Point(tt.X, tt.Y);
this.Cursor = Cursors.Hand;
child.CaptureMouse();
if ((e.ChangedButton == MouseButton.Left && e.ClickCount == 2))
this.Reset();
【讨论】:
【参考方案12】:我也尝试了this answer,但对结果并不完全满意。我一直在谷歌搜索,终于找到了一个 Nuget 包,它可以帮助我管理我想要的结果,anno 2021。我想与 Stack Overflow 的前开发人员分享。
我使用了通过this Github Repository 找到的this Nuget Package Gu.WPF.Geometry。开发的所有功劳应归 此软件包的所有者 Johan Larsson。
我是如何使用它的?我希望将命令作为缩放框下方的按钮,如MachineLayoutControl.xaml
所示。
<UserControl
x:Class="MyLib.MachineLayoutControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:csmachinelayoutdrawlib="clr-namespace:CSMachineLayoutDrawLib"
xmlns:effects="http://gu.se/Geometry">
<UserControl.Resources>
<ResourceDictionary Source="Resources/ResourceDictionaries/AllResourceDictionariesCombined.xaml" />
</UserControl.Resources>
<Grid Margin="0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
Grid.Row="0"
Margin="0,0"
Padding="0"
BorderThickness="1"
Style="StaticResource Border_Head"
Visibility="Visible">
<effects:Zoombox
x:Name="ImageBox"
IsManipulationEnabled="True"
MaxZoom="10"
MinZoom="0.1"
Visibility="Binding Zoombox_Visibility">
<ContentControl Content="Binding Viewing_Canvas" />
</effects:Zoombox>
</Border>
<StackPanel
Grid.Column="1"
Margin="10"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Command="effects:ZoomCommands.Increase"
CommandParameter="2.0"
CommandTarget="Binding ElementName=ImageBox"
Content="Zoom In"
Style="StaticResource StyleForResizeButtons" />
<Button
Command="effects:ZoomCommands.Decrease"
CommandParameter="2.0"
CommandTarget="Binding ElementName=ImageBox"
Content="Zoom Out"
Style="StaticResource StyleForResizeButtons" />
<Button
Command="effects:ZoomCommands.Uniform"
CommandTarget="Binding ElementName=ImageBox"
Content="See Full Machine"
Style="StaticResource StyleForResizeButtons" />
<Button
Command="effects:ZoomCommands.UniformToFill"
CommandTarget="Binding ElementName=ImageBox"
Content="Zoom To Machine Width"
Style="StaticResource StyleForResizeButtons" />
</StackPanel>
</Grid>
</UserControl>
在底层 Viewmodel 中,我有以下相关代码:
public Visibility Zoombox_Visibility get => movZoombox_Visibility; set movZoombox_Visibility = value; OnPropertyChanged(nameof(Zoombox_Visibility));
public Canvas Viewing_Canvas get => mdvViewing_Canvas; private set => mdvViewing_Canvas = value;
另外,我希望 在加载时立即执行统一填充命令,这是我设法在 代码隐藏 MachineLayoutControl.xaml.cs
.您会看到,我仅在执行命令时才将 Zoombox 设置为可见,以避免在加载用户控件时“闪烁”。
public partial class MachineLayoutControl : UserControl
#region Constructors
public MachineLayoutControl()
InitializeComponent();
Loaded += MyWindow_Loaded;
#endregion Constructors
#region EventHandlers
private void MyWindow_Loaded(object sender, RoutedEventArgs e)
Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.ApplicationIdle,
new Action(() =>
ZoomCommands.Uniform.Execute(null, ImageBox);
((MachineLayoutControlViewModel)DataContext).Zoombox_Visibility = Visibility.Visible;
));
#endregion EventHandlers
【讨论】:
以上是关于平移和缩放图像的主要内容,如果未能解决你的问题,请参考以下文章