WPF学习 - 用鼠标移动、缩放、旋转图片(1)
1. 需求
其实我的需求很简单。就是想做一个图片查看器,可以通过鼠标来平移、缩放、旋转图片。
2. 解决思路:
WPF中的UIElement提供了RenderTransform属性,用于承载各种Transform,例如TranslateTransform(平移转换)、ScaleTransform(缩放转换)、RotateTransform(旋转转换)、SkewTransform(倾斜转换)MatrixTransform(矩阵转换)和TransformGroup(组合转换)。因此我们可以用它们来实现需要的功能。
题外话:
实际上,TranslateTransform、ScaleTransform、RotateTransform、SkewTransform等都是预定义好的MatrixTransform。他们的底层逻辑也就是MatrixTransform。而TransformGroup,可以将各种转换打包到一起,同时起作用。
本文的着重点不在介绍这些转换的基本用法,有需要的朋友,可以自行搜索,有非常多的文章介绍基础用法。本文的重点在于,如何用鼠标控制他们。
3. 解决方法
首先,上xaml代码
1 <ContentControl Name="Part_ImageContainer" ClipToBounds="True" <!-- ClipToBounds用于指示当子内容超出边界的时候,是否切除超出部分 -->
2 Cursor="SizeAll"
3 Grid.Row="1" Grid.Column="1"
4 MouseLeftButtonDown="ImgMouseLeftButtonDown" <!-- 鼠标左键用于控制平移 -->
5 MouseRightButtonDown="ImgMouseRightButtonDown" <!-- 鼠标右键用于控制旋转 -->
6 MouseUp="ImgMouseUp"
7 MouseMove="ImgMouseMove"
8 MouseWheel="ImgMouseWheel">
9
10 <Image Name="Part_Image"
11 RenderOptions.BitmapScalingMode="NearestNeighbor"
12 Source="/ControlViewer/2020_11_26_11_00_29_0.jpg">
13 <Image.RenderTransform>
14 <TransformGroup x:Name="group"> <!-- 将下面的三种转换组合到一起 -->
15 <ScaleTransform x:Name="scaler"/>
16 <TranslateTransform x:Name="transer"/>
17 <RotateTransform x:Name="rotater"/>
18 </TransformGroup>
19 </Image.RenderTransform>
20 </Image>
21</ContentControl>
3.1 平移:
平移的思路是,当鼠标点按下的时候,记录按下的位置(moveStart),然后鼠标移动。在移动的过程中,记录鼠标点的位置(moveEnd),moveEnd - moveStart的增量,就是平移的量。因此有如下代码:
/// <summary>
/// 鼠标是否按下
/// </summary>
private bool mouseDown; /// <summary>
/// 移动图片前,按下鼠标左键时,鼠标相对于Part_ImageContainer的点
/// </summary>
Point moveStart; // 鼠标左键按下
private void ImgMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Part_ImageContainer.CaptureMouse();
mouseDown = true;
moveStart = e.GetPosition(Part_ImageContainer);
} // 鼠标移动:有可能是移动图片,也有可能是旋转图片
private void ImgMouseMove(object sender, MouseEventArgs e)
{
var mouseEnd = e.GetPosition(Part_ImageContainer); // 鼠标移动时,获取鼠标相对Part_ImageContainer的点
if(mouseDown)
{
if (e.LeftButton == MouseButtonState.Pressed) // 按下鼠标左键,移动图片
{
DoMove(mouseEnd);
}
}
} /// <summary>
/// 移动图片
/// </summary>
/// <param name="moveEndPoint">移动图片的终点(相对于Part_ImageContainer)</param>
private void DoMove(Point moveEndPoint)
{
// 考虑到旋转的影响,因此将两个点转换到Part_Image坐标系,计算x、y的增量
Point start = Part_ImageContainer.TranslatePoint(moveStart, Part_Image);
Point end = Part_ImageContainer.TranslatePoint(moveEndPoint, Part_Image); // 判断一下,如果scale很大的时候,移动会很迟缓。此时应该将移动放大
if(scaler.ScaleX > 7)
{
transer.X += (end.X - start.X) * 4;
transer.Y += (end.Y - start.Y) * 4;
}
else if(scaler.ScaleX > 5)
{
transer.X += (end.X - start.X) * 3;
transer.Y += (end.Y - start.Y) * 3;
}
else if (scaler.ScaleX > 3)
{
transer.X += (end.X - start.X) * 2;
transer.Y += (end.Y - start.Y) * 2;
}
else if (scaler.ScaleX < 0.5)
{
transer.X += (end.X - start.X) * 0.5;
transer.Y += (end.Y - start.Y) * 0.5;
}
else
{
transer.X += (end.X - start.X);
transer.Y += (end.Y - start.Y);
}
moveStart = moveEndPoint;
// 以下代码,抄的https://blog.csdn.net/weixin_42975610/article/details/113741534
// W+w > 2*move_x > -((2*scale-1)*w + W) 水平平移限制条件
// H+h > 2*move_y > -((2*scale-1)*h + H) 垂直平移限制条件 if (transer.X * 2 > Part_Image.ActualWidth + Part_ImageContainer.ActualWidth - 20)
transer.X = (Part_Image.ActualWidth + Part_ImageContainer.ActualWidth - 20) / 2; if (-transer.X * 2 > (2 * scaler.ScaleX - 1) * Part_Image.ActualWidth + Part_ImageContainer.ActualWidth - 20)
transer.X = -((scaler.ScaleX - 0.5) * Part_Image.ActualWidth + Part_ImageContainer.ActualWidth / 2 - 10); if (transer.Y * 2 > Part_Image.ActualHeight + Part_ImageContainer.ActualHeight - 20)
transer.Y = (Part_Image.ActualHeight + Part_ImageContainer.ActualHeight - 20) / 2; if (-transer.Y * 2 > (2 * scaler.ScaleY - 1) * Part_Image.ActualHeight + Part_ImageContainer.ActualHeight - 20)
transer.Y = -((scaler.ScaleY - 0.5) * Part_Image.ActualHeight + Part_ImageContainer.ActualHeight / 2 - 10);
}
重要知识点:UIElement.TranslatePoint(Point point, UIElement relativeTo)
这个方法的用途是,将相对于UIElement的点(参数point),转换为相对于relativeTo元素的点。
本例中,moveStart、moveEndPoint都是相对于Part_ImageContainer的点。但是我需要相对于Part_Image的点,因此,就用了Part_ImageContainer.TranslatePoint(moveStart,Part_Image)方法,将点moveStart、moveEndPoint转换为相对于Part_Image的点。
3.2 旋转
我希望,在图片上点击鼠标右键并按住,然后移动鼠标时,就开始旋转图片。那么就需要设置旋转的中心点,已经旋转的角度。我搜遍了很多文章,并没有合适的方法。只能自己摸索了。看如下代码:
Point rotateStart; // 旋转图片前,按下鼠标右键时,鼠标相对于Part_ImageContainer的点 // 鼠标右键按下
private void ImgMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
Part_ImageContainer.CaptureMouse();
mouseDown = true;
rotateStart = e.GetPosition(Part_ImageContainer); // 需要注意的是:RotateTransfrom的CenterX和CenterY,始终是相对于原始坐标系(未经过变换的坐标系)的。
// 因此,设置CenterX和CenterY之后,需要点拉回到坐标原点
Point toImage = Part_ImageContainer.TranslatePoint(rotateStart, Part_Image);
Point center = group.Transform(toImage); // 将中心点转换到Part_Image现状的坐标系下
transer.X = (center.X - toImage.X * scaler.ScaleX);
transer.Y = (center.Y - toImage.Y * scaler.ScaleY);
rotater.CenterX = center.X;
rotater.CenterY = center.Y;
} // 鼠标移动:有可能是移动图片,也有可能是旋转图片
private void ImgMouseMove(object sender, MouseEventArgs e)
{
var mouseEnd = e.GetPosition(Part_ImageContainer); // 鼠标移动时,获取鼠标相对图片容器的点
if (e.RightButton == MouseButtonState.Pressed && mouseDown)
{
double angle = (mouseEnd.Y - rotateStart.Y) * 0.5; // 以y轴的增量作为角度值。并缩小倍数,实现慢一点的旋转
rotate.Angle += angle;
rotateStart = mouseEnd; // 将鼠标终点赋值给旋转起点,以实现小角度连续旋转,
}
}
重要知识点:
3.2.1 Transform.Transform(Point point)方法
这个方法的作用是,将点point使用Transform转换。例如上面的代码中:group.Transform(toImage),就是将toImage点,用group去进行转换。
3.2.2 Transform.Inverse属性:逆变换,反变换
对于任何Transform对象,一旦使用它去变换某个元素后,它的Inverse属性就会有值。它是GeneralTransform类型,也就是一个变换器,用于将元素变换到原始状态,相当于是Transform的反向操作。
需要注意的是,Inverse是用于返回到原始状态的,而不上一次转换的状态。
难点:
这里最难理解就是旋转中心点。一开始我直接将鼠标点(相对于Part_Image)赋值给CenterX和CenterY,发现图片会“跳”走。后来经过不断的摸索,才发现CenterX和CenterY的值,是相对于元素的原始坐标系的。也就是说,不论这个元素如何变换,CenterX和CenterY都是相对于未经过任何变换时的坐标系的。如下图所示:
(黑色矩形为原始状态,其上有一个点a(2,1)。当以右下角为中心旋转一个角度后,会得到红色的矩形,a点也会跟着旋转到a'。但是a'的坐标还是(2,1)。这是因为,旋转的是矩形的坐标系,而a点相对于坐标系的位置是没有变化的。)
因此,我们就需要将CenterX和CenterY所代表的点,平移到鼠标点上来:
首先:获取到rotateStart的值,这个点是相对于Part_ImageContainer的。
然后:将这个点转换到Part_Image的坐标系中:Point toImage = Part_ImageContainer.TranslatePoint(rotateStart, Part_Image);
此时,toImage点(假设为图中a'点)有两层含义:
1. 它是相对于Part_Image现状坐标系(经过各种变化后的坐标系)的点。也就是上图中a'点。
2. 因为鼠标点击的是这个位置,也就是希望以这个点为中心。如果它是CenterX,CenterY的值,那么它就代表a点(在原始坐标系下)。
这里,我把它当作原始坐标系下。因此使用Transform()方法,将toImage点转换到现状坐标系下:
Point center = group.Transform(toImage);
那么toImage和center就都表示在现状坐标系下的点了。然后计算两个点之间的差值。
这里需要注意:在计算transer.X和transer.Y的时候,toImage点乘以了scaler,也就是相当于把toImage进行了缩放。这是因为center是toImage点经过group变换来的。而group中包含有scaler。如果toImage不乘以scaler,则transer.X和transer.Y会很大,在点下鼠标右键的时候,图片会“跳”到另外一个点上去。
平移和旋转,都用到了MouseMove事件,因此可以将其结合,写成这样:
// 鼠标移动:有可能是移动图片,也有可能是旋转图片
private void ImgMouseMove(object sender, MouseEventArgs e)
{
var mouseEnd = e.GetPosition(Part_ImageContainer); // 鼠标移动时,获取鼠标相对图片容器的点
if(mouseDown)
{
if (e.LeftButton == MouseButtonState.Pressed) // 按下鼠标左键,移动图片
{
DoMove(mouseEnd);
}
else if (e.RightButton == MouseButtonState.Pressed) // 按下鼠标右键,旋转图片
{
double angle = (mouseEnd.Y - rotateStart.Y) * 0.5;
DoRotate(angle);
rotateStart = mouseEnd;
}
}
}
3.3 缩放:
我需要的是,以鼠标点为中心,滚动鼠标滚轮,进行放大或缩小。因此有如下代码:
// 鼠标滚轮滚动
private void ImgMouseWheel(object sender, MouseWheelEventArgs e)
{
var point = e.GetPosition(Part_Image);
var delta = e.Delta * 0.002;
DoScale(point, delta);
} /// <summary>
/// 缩放图片。最小为0.1倍,最大为30倍
/// </summary>
/// <param name="point">相对于图片的点,以此点为中心缩放</param>
/// <param name="delta">缩放的倍数增量</param>
private void DoScale( Point point, double delta)
{
// 限制最大、最小缩放倍数
if (scaler.ScaleX + delta < 0.1 || scaler.ScaleX + delta > 30) return; scaler.ScaleX += delta;
scaler.ScaleY += delta; transer.X -= point.X * delta;
transer.Y -= point.Y * delta;
}
在ImgMouseWheel()方法中,直接获取了相对于Part_Image的点。因为缩放操作,实际上将所有点的坐标,都缩放指定的倍数。例如点(1,1),缩放5倍,就变成了(5,5)。但我要的是鼠标指定的点位不能变,因此就需要将点(5,5)拉回到(1,1)的位置上。于是就需要设置transer.X和transer.Y的值。
总结:
WPF的变换确实效率比较高,相比自己计算图片的移动、缩放、旋转,代码量少很多。指示它的坐标空间不是那么容易理解。
以上是个人学习心得,如有错误,还请指教。
WPF学习 - 用鼠标移动、缩放、旋转图片(1)的更多相关文章
- Magnifier.js - 支持鼠标滚轮缩放的图片放大镜效果
Magnifier.js 是一个 JavaScript 库,能够帮助你在图像上实现放大镜效果,支持使用鼠标滚轮放大/缩小功能.放大的图像可以显示在镜头本身或它的外部容器中.Magnifier.js 使 ...
- WPF通过鼠标滑轮缩放显示图片
如果你使用WinForm比较难实现通过滚动鼠标滑轮来对图片进行缩放显示,那么,你应该考虑一下使用WPF,既然是下一代Windows客户端开发平台,明显是有一定优势的,不然,MS是吃饱了撑着. 首先 ...
- WPF学习笔记——为BUTTON添加背景图片
首先要肯定,代码: <Style x:Key="UserItemButton" TargetType="Button"> <Setter Pr ...
- 在WPF里面实现以鼠标位置为中心缩放移动图片
原文:在WPF里面实现以鼠标位置为中心缩放移动图片 在以前的文章使用WPF Resource以及Transform等技术实现鼠标控制图片缩放和移动的效果里面,介绍了如何在WPF里面移动和放大缩小图片, ...
- WPF/Silverlight中图形的平移,缩放,旋转,倾斜变换演示
原文:WPF/Silverlight中图形的平移,缩放,旋转,倾斜变换演示 为方便描述, 这里仅以正方形来做演示, 其他图形从略. 运行时效果图:XAML代码:// Transform.XAML< ...
- 【Thumbnailator】java 使用Thumbnailator实现等比例缩放图片,旋转图片等【转载】
Thumbnailator概述: Thumbnailator是与Java界面流畅的缩略图生成库.它简化了通过提供一个API允许精细的缩略图生成调整生产从现有的图像文件的缩略图和图像对象的过程, ...
- Java图片缩略图裁剪水印缩放旋转压缩转格式-Thumbnailator图像处理
前言 java开发中经常遇到对图片的处理,JDK中也提供了对应的工具类,不过处理起来很麻烦,Thumbnailator是一个优秀的图片处理的开源Java类库,处理效果远比Java API的好,从API ...
- 【WPF学习】第五十三章 动画类型回顾
创建动画面临的第一个挑战是为动画选择正确的属性.期望的结果(例如,在窗口中移动元素)与需要使用的属性(在这种情况下是Canvas.Left和Canvas.Top属性)之间的关系并不总是很直观.下面是一 ...
- WPF学习之路初识
WPF学习之路初识 WPF 介绍 .NET Framework 4 .NET Framework 3.5 .NET Framework 3.0 Windows Presentation Found ...
- 在WPF设计工具Blend2中制作立方体图片效果
原文:在WPF设计工具Blend2中制作立方体图片效果 ------------------------------------------------------------------------ ...
随机推荐
- SICP:惰性求值、流和尾递归(Python实现)
求值器完整实现代码我已经上传到了GitHub仓库:TinySCM,感兴趣的童鞋可以前往查看.这里顺便强烈推荐UC Berkeley的同名课程CS 61A. 即使在变化中,它也丝毫未变. --赫拉克利特 ...
- auto main()-> int的含义是什么?
42 https://stackoverflow.com/questions/21085446/what-is-the-meaning-of-auto-main-int/21085530 C++1 ...
- 逍遥自在学C语言 | 指针的基础用法
前言 在C语言中,指针是一项重要的概念,它允许我们直接访问和操作内存地址. 可以说,指针是C语言一大优势.用得好,你写程序如同赵子龙百万军中取上将首级:用得不好,则各种问题层出不穷,有种双拳难敌四手的 ...
- 从AWS中学习如何使用AmazonDynamoDB存储卷
目录 <35. <从 AWS 中学习如何使用 Amazon DynamoDB 存储卷>>:从 AWS 中学习如何使用 Amazon DynamoDB 存储卷 随着云计算技术的迅 ...
- SpringBoot中的yml文件中读取自定义配置信息
SpringBoot中的yml文件中读取自定义配置信息 开发中遇到的问题,百度的答案我都没有找到,去找大佬获取到的经验总结,这只是其中的一种方法,如果其他大佬有新的方法,可以分享分享. 一.非静态属性 ...
- PostgreSQL 12 文档: 系统表
第 51 章 系统目录 目录 51.1. 概述 51.2. pg_aggregate 51.3. pg_am 51.4. pg_amop 51.5. pg_amproc 51.6. pg_attrde ...
- mysql高级进阶(存储过程、游标、触发器)
废话不多说,直接进入正题... 一.存储过程 a.概述 存储过程可以看成是对一系列 SQL 操作的批处理: 使用存储过程的好处 代码封装,保证了一定的安全性: 代码复用: 由于是预先编译,因此具有很高 ...
- 《Among Us》火爆全球,实时语音助力派对游戏开启第二春
今年在全球"宅经济"的影响下,社交派对类游戏意外的迎来了爆发. 8月份,<糖豆人:终极淘汰赛>突然爆火,创造了首日150万玩家.首周Steam 200万销量.单周Twi ...
- Prompt Playground: 一个简易的提示词调试工具
Prompt Playground: 一个简易的提示词调试工具 将LLM引入到日常的开发工作中后,会面临大量的提示词调试的工作,由于LLM不确定性,这个工作会变得非常的繁琐,需要不断的调整,甚至需要大 ...
- 给SqlSugar一个优化建议
声明:本作者无恶意只是觉得这个功能很不错,平常工作当中经常用到,自己框架也做了相应的支持,本着技术共享目的. 一.对象组合设置列更新支持 建议度:高 业务场景 1.更新列表需统一设置 例如:修改人ID ...