WPF学习 - 用鼠标移动、缩放、旋转图片(2)- 使用MatrixTransform
在上一篇文章中,提到了以鼠标控制变换图片的方法。
这种方法在某种情况下可以,例如,直接在windows窗体上。但我发现,当把它封装到一个控件中的时候,它就不行了。
经过不断的尝试,我找到了一种更简单的方法,而且,封装到控件中也工作正常。
这里就要介绍WPF中另外一种变换:矩阵变换(MatrixTransform)。它就是通过对矩阵进行计算以改变图片的坐标系。
实际上,WPF中的所有变换,都是矩阵变换的特例。例如ScaleTransform、TranslateTransform等,都是通过MatrixTransform来实现的。而TransformGroup就是将ScaleTransform、TranslateTransform、
RotateTransform、SkewTransform等四种变换融合到MatrixTransform中进行计算。
因此,如果要控制图片的变形,可以直接使用MatrixTransform。而且,对于多种组合变换,它更简单。
这里还需要强调一个重点:
// 这条代码在Mouse事件中
// img为Iamge控件。而实际上可以是任何UIElement的实例
Point start = e.GetPosition(img);
这条语句,获取到的点,其实是相对于img控件的原始坐标系的。不管img元素如何变换,获取到的点,只要鼠标的点不变,获取到的值永远不变。因此,如果想要以鼠标为中心点旋转、缩放,就需要将start点变换到控件现状的坐标系下(也就是鼠标在屏幕中的实际位置)。
直接上代码吧,其他的不解释了。
Xaml代码:
<Style TargetType="{x:Type rayCtl:ImgViewer}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type rayCtl:ImgViewer}"> <Grid ClipToBounds="{TemplateBinding ClipToBounds}"> <Image Name="Part_Image" RenderOptions.BitmapScalingMode="NearestNeighbor" Source="{TemplateBinding Source}"/>
<!-- 以下是控件按钮,以执行某些命令。如果不需要,可以删除 --> <StackPanel Name="toolBar" Orientation="Horizontal" Opacity="0" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,15,0" > <StackPanel.Background> <LinearGradientBrush> <GradientStop Color="#EAE5C9" Offset="0"/> <GradientStop Color="#6CC6CB" Offset="1"/> </LinearGradientBrush> </StackPanel.Background> <StackPanel.Resources> <ControlTemplate TargetType="Separator" x:Key="simpleSeparator"> <Border SnapsToDevicePixels="True" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" BorderThickness="{TemplateBinding Border.BorderThickness}" BorderBrush="{TemplateBinding Border.BorderBrush}" Background="{TemplateBinding Panel.Background}" > </Border> </ControlTemplate> <Style TargetType="Button"> <Setter Property="Background" Value="Transparent"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="Padding" Value="2"/> <Setter Property="Margin" Value="3"/> </Style> <Style TargetType="Path"> <Setter Property="Fill" Value="White"/> <Setter Property="Width" Value="16"/> <Setter Property="Stretch" Value="Uniform"/> </Style> </StackPanel.Resources> <Button Command="rayCtl:TransformCommands.MoveLeft"> <Path Data="M896 544H250.4l242.4 242.4L448 832 173.6 557.6 128 512l45.6-45.6L448 192l45.6 45.6L250.4 480H896v64z" /> </Button> <Button Command="rayCtl:TransformCommands.MoveRight"> <Path Data="M128 480h645.6L530.4 237.6 576 192l274.4 274.4L896 512l-45.6 45.6L576 832l-45.6-45.6L773.6 544H128v-64z"/> </Button> <Button Command="rayCtl:TransformCommands.MoveUp"> <Path Data="M480 896V250.4L237.6 493.6 192 448l274.4-274.4L512 128l45.6 45.6L832 448l-45.6 45.6L544 250.4V896h-64z"/> </Button> <Button Command="rayCtl:TransformCommands.MoveDown"> <Path Data="M544 128v645.6l242.4-242.4L832 576l-274.4 274.4L512 896l-45.6-45.6L192 576l45.6-45.6L480 773.6V128h64z" /> </Button> <Separator Width="1" Template="{StaticResource simpleSeparator}"/> <Button Command="rayCtl:TransformCommands.RotateLeft"> <Path Data="M64 536.6c0.6-19.4 2.5-38.8 5.5-58l143 17.6c-1 13.5-1.2 27-0.5 40.4H64zM81.7 423.2c10.3-36.3 25.1-71.3 43.7-103.8l117.3 62.1c-11.4 24.2-19.7 49.6-24.9 75.5L81.7 423.2zM156.1 272.6c22.3-29.9 48.1-56.9 76.6-80.4l80.4 91c-19.6 19-36.7 40.2-51 63l-106-73.6zM277.4 159.7c31-19.8 64.3-35.8 98.9-47.6L415.2 215a336.57 336.57 0 0 0-71.1 41.5l-66.7-96.8zM429.1 97.8c35.6-7.4 71.9-10.4 107.8-9.1v98.6c-28.1 1.3-55.9 5.8-82.5 13.6L429.1 97.8zM590.5 93.8c35.5 5.5 70.2 15.4 103 29.2l-31 81.6c-27.1-8.9-55.2-14.5-83.4-16.6l11.4-94.2zM741.3 146.6c30.8 17.6 59.3 38.9 84.7 63.1l-50.3 56.8c-22.6-18.3-47.2-33.8-73.2-46l38.8-73.9zM861.8 248.2c22.3 27 41.2 56.5 56.2 87.8l-57 30c-14.9-25.5-32.8-49-53.1-70l53.9-47.8zM937.7 384c11.1 32.7 18.2 66.7 21 100.8l-52.8 6.4c-4.9-29.6-13.3-58.4-25-85.6l56.8-21.6zM960 535.9c-1.2 34-6.5 67.8-15.7 100.3l-40.6-9.9c6-29.8 8.2-60.2 6.9-90.3l49.4-0.1zM927.6 684c-13 31-29.8 60.3-49.7 87l-25.1-17.2c16.4-26.1 29.6-54.1 39.2-83.1l35.6 13.3zM845.7 809c-22.9 24-48.5 45.2-76.2 62.9l-10.9-15.6c25-18.9 47.5-40.8 67.1-64.9l20 17.6zM726.6 895.9c-29.5 14.2-60.5 24.7-92.3 31.3l-1.9-7.4c30.5-8.9 59.8-21.5 87-37.4l7.2 13.5zM586.1 934.2c-16.5 1.4-33 1.7-49.5 1.1 16.4-0.6 32.8-2.3 49-4.9l0.5 3.8z"/> </Button> <TextBlock /> <Button Command="rayCtl:TransformCommands.RotateRight"> <Path Data="M158.6,104.8c0.1-2.6,0.1-5.3-0.1-7.9l27.9-3.4c0.6,3.8,1,7.5,1.1,11.3H158.6z M157.5,89.3c-1-5.1-2.6-10-4.9-14.7 l22.9-12.1c3.6,6.3,6.5,13.2,8.5,20.3L157.5,89.3z M148.8,67.6c-2.8-4.5-6.1-8.6-10-12.3l15.7-17.8c5.6,4.6,10.6,9.9,15,15.7 L148.8,67.6z M132.8,50.1c-4.3-3.3-8.9-6-13.9-8.1l7.6-20.1c6.8,2.3,13.3,5.4,19.3,9.3C145.8,31.2,132.8,50.1,132.8,50.1z M111.3,39.2c-5.2-1.5-10.6-2.4-16.1-2.7V17.3c7-0.3,14.1,0.3,21.1,1.8L111.3,39.2z M86.9,36.7c-5.5,0.4-11,1.5-16.3,3.2L64.6,24 c6.4-2.7,13.2-4.6,20.1-5.7L86.9,36.7z M62.8,43.1c-5.1,2.4-9.9,5.4-14.3,9L38.7,41c5-4.7,10.5-8.9,16.5-12.3L62.8,43.1z M31.7,48.5 l10.5,9.3c-4,4.1-7.5,8.7-10.4,13.7l-11.1-5.9C23.6,59.5,27.3,53.8,31.7,48.5L31.7,48.5z M27.9,79.2c-2.3,5.3-3.9,10.9-4.9,16.7 l-10.3-1.3c0.5-6.7,1.9-13.3,4.1-19.7C16.9,75,27.9,79.2,27.9,79.2z M12.5,104.7l9.6,0c-0.3,5.9,0.2,11.8,1.3,17.6l-7.9,1.9 C13.8,117.9,12.7,111.3,12.5,104.7L12.5,104.7z M25.8,131c1.9,5.7,4.5,11.1,7.7,16.2l-4.9,3.4c-3.9-5.2-7.2-10.9-9.7-17L25.8,131z M34.8,158l3.9-3.4c3.8,4.7,8.2,9,13.1,12.7l-2.1,3C44.3,166.8,39.3,162.7,34.8,158L34.8,158z M58.1,175l1.4-2.6 c5.3,3.1,11,5.6,17,7.3l-0.4,1.4C69.9,179.8,63.8,177.8,58.1,175L58.1,175z M85.5,182.5l0.1-0.7c3.2,0.5,6.4,0.8,9.6,1 C92,182.8,88.8,182.7,85.5,182.5L85.5,182.5z"/> </Button> <Separator Width="1" Template="{StaticResource simpleSeparator}"/> <Button Command="rayCtl:TransformCommands.Enlarge"> <Path> <Path.Data> <GeometryGroup> <PathGeometry Figures="M763.221333 702.848L981.333333 921.002667 921.002667 981.333333l-218.154667-218.112A403.626667 403.626667 0 0 1 448 853.333333a405.333333 405.333333 0 1 1 405.333333-405.333333 403.626667 403.626667 0 0 1-90.112 254.848zM448 768a320 320 0 1 0 0-640 320 320 0 0 0 0 640z"/> <PathGeometry Figures="M490.666667 405.333333h128v85.333334h-128v128h-85.333334v-128h-128v-85.333334h128v-128h85.333334z"/> </GeometryGroup> </Path.Data> </Path> </Button> <TextBlock /> <Button Command="rayCtl:TransformCommands.Reduce"> <Path> <Path.Data> <GeometryGroup> <PathGeometry Figures="M763.221333 702.848L981.333333 921.002667 921.002667 981.333333l-218.154667-218.112A403.626667 403.626667 0 0 1 448 853.333333a405.333333 405.333333 0 1 1 405.333333-405.333333 403.626667 403.626667 0 0 1-90.112 254.848zM448 768a320 320 0 1 0 0-640 320 320 0 0 0 0 640z"/> <PathGeometry Figures="M277.333333 512v-85.333333h341.333334v85.333333z"/> </GeometryGroup> </Path.Data> </Path> </Button> <Separator Width="1" Template="{StaticResource simpleSeparator}"/> <Button Command="rayCtl:TransformCommands.FlipX"> <Path Data="M507.164444 913.92h398.222223l-397.653334-796.444444v796.444444M450.275556 913.92h-398.222223l398.222223-796.444444v796.444444m-42.382223-56.888889v-568.888889l-284.444444 568.888889h284.444444"/> </Button> <Button Command="rayCtl:TransformCommands.FlipY"> <Path Data="M724.48 202.808889l-224.142222 206.791111L275.911111 202.808889h448.568889m145.635556-56.888889h-739.555556l369.777778 341.333333 369.777778-341.333333zM500.337778 544.142222l-369.777778 341.333334h739.555556l-369.777778-341.333334z" /> </Button> <Separator Width="1" Template="{StaticResource simpleSeparator}"/> <Button Command="rayCtl:TransformCommands.Reset"> <Path Data="M750.72 170.666667h-111.061333V85.333333h256.170666v256.256h-85.290666V231.424l-196.010667 195.157333-60.16-60.458666 196.352-195.498667z m-0.64 640l-195.712-195.84 60.330667-60.288L810.666667 750.677333v-110.848H896V896h-256.341333v-85.333333h110.421333zM170.666667 750.677333l195.413333-196.096 60.416 60.245334L231.168 810.666667h110.506667V896H85.333333v-256.170667h85.290667v110.805334zM170.666667 230.613333v111.061334H85.333333V85.333333h254.506667v85.333334H231.424L426.666667 366.421333 366.250667 426.666667z"/> </Button> </StackPanel> </Grid>
<!-- 触发器只是为了让按钮显示,如果没有按钮,可以删除 --> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="toolBar" Property="Opacity" Value="1" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
后台代码:
namespace RaymondLib.Controls { [TemplatePart(Name = elementName, Type =typeof(Image))] public class ImgViewer : Control { const string elementName = "Part_Image"; Image? img; MatrixTransform tranformer; // 变形器 Point geometryCenter; // img的几何中心 Point moveStart; Point rotateStart; bool mouseDown; bool executable; // 命令是否能执 #region 依赖属性 internal static readonly DependencyProperty SourceProperty = DependencyProperty.Register( "Source", typeof(BitmapSource), typeof(ImgViewer), new PropertyMetadata(null)); /// <summary> /// 图片的源 /// </summary> public BitmapSource Source { get => (BitmapSource)GetValue(SourceProperty); set => SetValue(SourceProperty, value); } #endregion static ImgViewer() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ImgViewer), new FrameworkPropertyMetadata(typeof(ImgViewer))); } public ImgViewer() { // 注册命令 CommandBindings.Add(new CommandBinding(TransformCommands.MoveUp, OnMoveUp)); CommandBindings.Add(new CommandBinding(TransformCommands.MoveDown, OnMoveDown)); CommandBindings.Add(new CommandBinding(TransformCommands.MoveLeft, OnMoveLeft)); CommandBindings.Add(new CommandBinding(TransformCommands.MoveRight, OnMoveRight)); CommandBindings.Add(new CommandBinding(TransformCommands.RotateLeft, OnRotateLeft)); CommandBindings.Add(new CommandBinding(TransformCommands.RotateRight, OnRotateRight)); CommandBindings.Add(new CommandBinding(TransformCommands.Enlarge, OnEnlarge)); CommandBindings.Add(new CommandBinding(TransformCommands.Reduce, OnReduce)); CommandBindings.Add(new CommandBinding(TransformCommands.FlipX, OnFlipX)); CommandBindings.Add(new CommandBinding(TransformCommands.FlipY, OnFlipY)); CommandBindings.Add(new CommandBinding(TransformCommands.Reset, OnReset)); } #region 命令:对基础方法的调用 // 向上移动20 private void OnMoveUp(object sender, ExecutedRoutedEventArgs e) { if (executable) DoMove(0, -20); } // 向下移动20 private void OnMoveDown(object sender, ExecutedRoutedEventArgs e) { if (executable) DoMove(0, 20); } // 向左移动20 private void OnMoveLeft(object sender, ExecutedRoutedEventArgs e) { if (executable) DoMove(-20, 0); } // 向右移动20 private void OnMoveRight(object sender, ExecutedRoutedEventArgs e) { if (executable) DoMove(20, 0); } // 向左旋转90° private void OnRotateLeft(object sender, ExecutedRoutedEventArgs e) { if (executable) DoRotate(-90); } // 向右旋转90° private void OnRotateRight(object sender, ExecutedRoutedEventArgs e) { if (executable) DoRotate(90); } // 放大0.2倍 private void OnEnlarge(object sender, ExecutedRoutedEventArgs e) { if (executable) DoScale(true); } // 缩小0.2倍 private void OnReduce(object sender, ExecutedRoutedEventArgs e) { if (executable) DoScale(false); } // 垂直翻转命令 private void OnFlipY(object sender, ExecutedRoutedEventArgs e) { if (executable) DoVerFlip(); } // 水平翻转命令 private void OnFlipX(object sender, ExecutedRoutedEventArgs e) { if (executable) DoHorFlip(); } // 重置 private void OnReset(object sender, ExecutedRoutedEventArgs e) { if (executable) DoReset(); } #endregion #region 基础方法:平移、缩放、旋转 /// <summary> /// 移动图片 /// </summary> /// <param name="deltaX">水平方向增量</param> /// <param name="deltaY">垂直方向增量</param> private void DoMove(double deltaX,double deltaY) { Vector v = new Vector(deltaX, deltaY); DoMove(v); } /// <summary> /// 移动图片 /// </summary> /// <param name="vector">移动的向量</param> private void DoMove(Vector vector) { Matrix temp = tranformer.Matrix; Vector moveReal = temp.Transform(vector); // 将相对于图片原始坐标的向量,变换为经过变换的向量,其实就是考虑到了旋转、翻转的情况 temp.Translate(moveReal.X, moveReal.Y); tranformer.Matrix = temp; } /// <summary> /// 缩放元素:以几何中心点为缩放中心点。 /// 最小缩放0.2倍,最大30倍 /// </summary> /// <param name="delta">缩放倍率的增量</param> private void DoScale(bool enlarge) { DoScale(enlarge, geometryCenter); } /// <summary> /// 缩放元素 /// </summary> /// <param name="delta">缩放倍率</param> /// <param name="center">缩放的中心点。这个中心点就是e.GetPosition(img)获取到的点</param> private void DoScale(bool enlarge,Point center) { Matrix temp = tranformer.Matrix; Point centerReal = temp.Transform(center); // 将相对于img原始坐标的点,变换成现状坐标系的点(鼠标在屏幕中的位置) // 缩放倍率分为三段: // 小于0.2时,只允许放大 // 0.2-50之间,可以放大、缩小 // 大于50,只允许缩小 if(Math.Abs(temp.M11) < 0.2) { if (enlarge) {
// 这里需要注意:ScaleAt()方法内部将创建一个缩放倍数的矩阵与现有矩阵相乘。因此但给定一个小于1的缩放倍数,则实际将缩小图片 temp.ScaleAt(1.1, 1.1, centerReal.X, centerReal.Y); } } else if (Math.Abs(temp.M11) > 50) { if(!enlarge) { temp.ScaleAt(0.9, 0.9, centerReal.X, centerReal.Y); }0 } else { if (enlarge) { temp.ScaleAt(1.1, 1.1, centerReal.X, centerReal.Y); } else { temp.ScaleAt(0.9, 0.9, centerReal.X, centerReal.Y); } } tranformer.Matrix = temp; } /// <summary> /// 旋转元素:以几何中心点为旋转中心点 /// </summary> /// <param name="angle">旋转的角度增量</param> private void DoRotate(double angle) { DoRotate(angle, geometryCenter); } /// <summary> /// 旋转元素 /// </summary> /// <param name="angle">旋转的角度增量</param> /// <param name="center">旋转的中心点</param> private void DoRotate(double angle,Point center) { Matrix temp = tranformer.Matrix; Point centerReal = temp.Transform(center); temp.RotateAt(angle, centerReal.X, centerReal.Y); tranformer.Matrix = temp; } /// <summary> /// 水平翻转:以元素的几何中心为中心点水平翻转 /// </summary> private void DoHorFlip() { Matrix temp = tranformer.Matrix; temp.M11 *= -1; temp.M21 *= -1; temp.OffsetX = img.ActualWidth - temp.OffsetX; // 以几何中心为原点,沿Y轴翻转 tranformer.Matrix = temp; } /// <summary> /// 垂直翻转:以元素的几何中心为中心点垂直翻转 /// </summary> private void DoVerFlip() { Matrix temp = tranformer.Matrix; temp.M12 *= -1; temp.M22 *= -1; temp.OffsetY = img.ActualHeight - temp.OffsetY; // 以几何中心为原点,沿X轴翻转 tranformer.Matrix = temp; } /// <summary> /// 重置 /// </summary> private void DoReset() { tranformer.Matrix = new Matrix(); } #endregion // 设置模板 public override void OnApplyTemplate() { executable = false; if (img != null) { tranformer = null; } img = GetTemplateChild(elementName) as Image; if (img != null) { tranformer = new MatrixTransform(); img.RenderTransform = tranformer; executable = true; // 命令是否能执行 // 鼠标事件 img.MouseLeftButtonDown += Element_MouseLeftButtonDown; img.MouseRightButtonDown += Element_MouseRightButtonDown; img.MouseMove += Element_MouseMove; img.MouseUp += Element_MouseUp; img.MouseWheel += Element_MouseWheel; img.SizeChanged += Element_SizeChanged; } } // 尺寸发生改变时:中心发生变化 private void Element_SizeChanged(object sender, SizeChangedEventArgs e) {
// 这里应该使用img的ActualXX属性,因为img有可能控件在布局的时候,有可能缩放。 this.geometryCenter = new Point(img.ActualWidth / 2, img.ActualHeight / 2); } // 按下鼠标左键 private void Element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { img.CaptureMouse(); mouseDown = true; moveStart = e.GetPosition(img); } private void Element_MouseRightButtonDown(object sender, MouseButtonEventArgs e) { img.CaptureMouse(); mouseDown = true; rotateStart = e.GetPosition(img); } // 鼠标移动 private void Element_MouseMove(object sender, MouseEventArgs e) { if (!mouseDown) return; Point mouseEnd = e.GetPosition(img); if (e.LeftButton == MouseButtonState.Pressed) { var delta = mouseEnd - moveStart; DoMove(delta.X, delta.Y); //moveStart = mouseEnd; } else if(e.RightButton == MouseButtonState.Pressed) { // 按下鼠标右键,旋转图片 double angle = (mouseEnd.Y - rotateStart.Y) * 0.2; DoRotate(angle,rotateStart); resetRotater = true; } } // 释放鼠标 private void Element_MouseUp(object sender, MouseButtonEventArgs e) { img.ReleaseMouseCapture(); mouseDown = false; } // 滚动鼠标滚轮 private void Element_MouseWheel(object sender, MouseWheelEventArgs e) { Point scaleCenter = e.GetPosition(img); bool enlarge = e.Delta > 0; DoScale(enlarge, scaleCenter); } }
// 以下为该控件支持的命令 public static class TransformCommands { /// <summary> /// 向上移动 /// </summary> public static RoutedCommand MoveUp { get; } = new(nameof(MoveUp), typeof(TransformCommands)); /// <summary> /// 向下移动 /// </summary> public static RoutedCommand MoveDown { get; } = new(nameof(MoveDown), typeof(TransformCommands)); /// <summary> /// 向左移动 /// </summary> public static RoutedCommand MoveLeft { get; } = new(nameof(MoveLeft), typeof(TransformCommands)); /// <summary> /// 向右移动 /// </summary> public static RoutedCommand MoveRight { get; } = new(nameof(MoveRight), typeof(TransformCommands)); /// <summary> /// 向左旋转 /// </summary> public static RoutedCommand RotateLeft { get; } = new(nameof(RotateLeft), typeof(TransformCommands)); /// <summary> /// 向右旋转 /// </summary> public static RoutedCommand RotateRight { get; } = new(nameof(RotateRight), typeof(TransformCommands)); /// <summary> /// 放大 /// </summary> public static RoutedCommand Enlarge { get; } = new(nameof(Enlarge), typeof(TransformCommands)); /// <summary> /// 缩小 /// </summary> public static RoutedCommand Reduce { get; } = new(nameof(Reduce), typeof(TransformCommands)); /// <summary> /// 水平翻转 /// </summary> public static RoutedCommand FlipX { get; } = new(nameof(FlipX), typeof(TransformCommands)); /// <summary> /// 垂直翻转 /// </summary> public static RoutedCommand FlipY { get; } = new(nameof(FlipY), typeof(TransformCommands)); /// <summary> /// 重置 /// </summary> public static RoutedCommand Reset { get; } = new(nameof(Reset), typeof(TransformCommands)); } }
以上为个人自己摸索所得。如有错误,还请高手指点。