WPF学习 - 用鼠标移动、缩放、旋转图片(1)
阅读原文时间:2023年09月05日阅读:1

  其实我的需求很简单。就是想做一个图片查看器,可以通过鼠标来平移、缩放、旋转图片。

  WPF中的UIElement提供了RenderTransform属性,用于承载各种Transform,例如TranslateTransform(平移转换)、ScaleTransform(缩放转换)、RotateTransform(旋转转换)、SkewTransform(倾斜转换)MatrixTransform(矩阵转换)和TransformGroup(组合转换)。因此我们可以用它们来实现需要的功能。

  题外话:  

    实际上,TranslateTransformScaleTransformRotateTransformSkewTransform等都是预定义好的MatrixTransform。他们的底层逻辑也就是MatrixTransform。而TransformGroup,可以将各种转换打包到一起,同时起作用。

    本文的着重点不在介绍这些转换的基本用法,有需要的朋友,可以自行搜索,有非常多的文章介绍基础用法。本文的重点在于,如何用鼠标控制他们。

首先,上xaml代码

1 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    13 14                  15 16 17 18 19 20 21

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的变换确实效率比较高,相比自己计算图片的移动、缩放、旋转,代码量少很多。指示它的坐标空间不是那么容易理解。

  以上是个人学习心得,如有错误,还请指教。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章