[uwp]自定义图形裁切控件

简介: 原文:[uwp]自定义图形裁切控件     开始之前,先上一张美图。图中的花叫什么,我已经忘了,或者说从来就不知道,总之谓之曰“野花”。只记得花很美,很香,春夏时节,漫山遍野全是她。这大概是七八年前的记忆了,不过她依旧会很准时的在山上沐浴春光,灿烂盛开,只是我看不到罢了。
原文: [uwp]自定义图形裁切控件

 

  开始之前,先上一张美图。图中的花叫什么,我已经忘了,或者说从来就不知道,总之谓之曰“野花”。只记得花很美,很香,春夏时节,漫山遍野全是她。这大概是七八年前的记忆了,不过她依旧会很准时的在山上沐浴春光,灿烂盛开,只是我看不到罢了。

 

  文艺过后,就要看到重点了。上图是Windows10自带的图片裁切工具,应该是作为插件集成在“照片”应用中。当然不止于此,几乎所有涉及照片上传类的APP,都会提供裁切图片这个基本功能。实现方式有很多种,我这儿给出自己的一种解决方案。

  先上效果图:

  大致分析如下:

    图片本身不作为裁切工具的一部分,只是把裁切控件放在图片上层,然后调整四个按钮,选出想要裁切的区域,计算出裁切区域的坐标和长宽信息,然后根据比例应用到图片上面,从而实现裁              切。这篇博文主要描述怎么实现裁切控件本身,而实际裁切图片等不进行讨论。

 

  知道了要干什么,接着就要想想怎么办。

 

  由于最终要计算出一个裁切区域,所以控件实现一个自定义附加属性,用来对外提供裁切区域信息,为了简单,直接选用Windows.Foundation.Rect这个结构体来描述。

  就该控件自身结构来讲由Canvas+Path+Button * 4这六个主要的控件来实现。

 

    Canvas:作为容器,用来承载Path,Button等,关键是方便操作子元素的位置等。

    Button:很明显的四个拖拽点,这儿用Button.Template重写了Button的外观,将其改为一个圆(Ellipse)

        改变中间矩形区域大小就是通过拖拽Button来实现,显然Button支持拖拽,这儿我用自定义Behavior实现它的拖拽功能,关于该Behavior的实现,可以参看上一篇博文《[uwp]自定义Behavior之随意拖动》

    Path:一个填充路径。看到上图中黑色半透明部分,就是该对象的可视部分。具体是通过两个矩形减去重叠区域实现,第一个矩形就是和Canvas等大的一个矩形,第二个矩形就是中间透明区域的矩形,两个矩形进行减去重叠区域的运算后,就可以得到Path的区域。具体的减去操作也很简单,通过GeometryGroup实现,设置其填充规则为FillRule.EvenOdd即可。事实上,经过分解这个Path后,最终就回归到怎么计算中间透明区域大小的问题上,而这个问题,可以通过四个Button的位置来计算。

 

  通过上面的分析,只需要计算四个Button的位置信息即可,那么这个时候,就可以利用XAML强大的依赖属性系统(DependencyProperty),通过数据绑定等技巧来实际操作。

  针对四个Button的位置信息,分析如下:

  1.四个Button,为了在拖动的任意时刻,保持一个矩形区域,当一个按钮移动时,和他同行或者同列的按钮会跟着动,变化量相同。(此处用左上,右上,左下,右下来标识四个Button)

   所以可以选左上和右下两个Button为主动点,他们的位置定了,另外两个也就定了。值得注意的是,Button的位置是通过附加属性Canvas.Left和Canvas.Top来确定的。所以让左下的Canvas.Left和左上的Canvas.Left绑定,左下的Canvas.Top和右下的Canvas.Top绑定;让右上的Canvas.Left和右下的Canvas.Left绑定,右上Canvas.Top和左上的Canvas.Top绑定。经过绑定之后,左上和右下的位置变化,就能引起左下和右上的位置变化,如果将以上绑定全部设置为双向绑定,那么左下和右上的变化也就同样能引起其他连个主动点的变化。

  2.确定了两个主动点后,便可以自定义一个类来表示这两个主动点的一些信息了(设置坐标X1,Y1,X2,Y2)。在接下来的实现中,用PointModel这个类来表示。  

  

  最终,只需要关注PointModel中两个主动点坐标的变化即可。

  为了检测这种变化,PointModel中定义了四个属性X1,Y1,X2,Y2,在他们的Set方法中,包含了控制矩形大小和主动点自身位置(边界检测和两个Button靠近检测)的一些逻辑。

  接着贴出PointModel的代码:

        public class PointModel : INotifyPropertyChanged
        {
            private double _x1;//代表左上Button的Canvas.Left
            public double X1
            {
                get { return _x1; }
                set
                {
                    double abspos = 0 - _buttonWidth / 2.0;//button最左可以到达的位置
                    if (value < abspos)//如果实际位置还小于该最小位置,
                    {
                        _x1 = abspos;//则强制修改Button的位置到最边界处
                        _call?.Invoke("X1", _x1);//通知修改Button位置
                        _rectcall?.Invoke();//修改矩形区域位置
                        return;
                    }

                    if ((_x2 - value) >= _minRectWidth)//如果Button和同行的button间距大于_minRectWidth,属正常情况
                    {
                        _x1 = value;
                        OnPropertyChanged();
                    }
                    else//如果小于该最小间距
                    {
                        _x1 = _x2 - _minRectWidth;//根据最小间距,强制修改Button位置。
                        _call?.Invoke("X1", _x1);//通知修改Button位置
                    }
                    _rectcall?.Invoke();//修改矩形区域位置
                }
            }

            private double _y1;
            public double Y1
            {
                get { return _y1; }
                set
                {
                    double abspos = 0 - _buttonWidth / 2.0;
                    if (value < abspos)
                    {
                        _y1 = abspos;
                        _call?.Invoke("Y1", _y1);
                        _rectcall?.Invoke();
                        return;
                    }
                    if ((_y2 - value) >= _minRectWidth)
                    {
                        _y1 = value;
                        OnPropertyChanged();
                    }
                    else
                    {
                        _y1 = _y2 - _minRectWidth;
                        _call?.Invoke("Y1", _y1);

                    }
                    _rectcall?.Invoke();
                }
            }

            private double _x2;
            public double X2
            {
                get { return _x2; }
                set
                {

                    double abspos = CanvasRect.Width - _buttonWidth / 2.0;
                    if (value > abspos)
                    {
                        _x2 = abspos;
                        _call?.Invoke("X2", _x2);
                        _rectcall?.Invoke();
                        return;
                    }

                    if ((value - _x1) >= _minRectWidth)
                    {
                        _x2 = value;
                        OnPropertyChanged();
                    }
                    else
                    {
                        _x2 = _minRectWidth + _x1;
                        _call?.Invoke("X2", _x2);
                    }
                    _rectcall?.Invoke();
                }
            }

            private double _y2;
            public double Y2
            {
                get { return _y2; }
                set
                {
                    double abspos = CanvasRect.Height - _buttonWidth / 2.0;
                    if (value > abspos)
                    {
                        _y2 = abspos;
                        _call?.Invoke("Y2", _y2);
                        _rectcall?.Invoke();
                        return;
                    }

                    if ((value - _y1) >= _minRectWidth)
                    {
                        _y2 = value;
                        OnPropertyChanged();
                    }
                    else
                    {
                        _y2 = _y1 + _minRectWidth;
                        _call?.Invoke("Y2", _y2);
                    }
                    _rectcall?.Invoke();
                }
            }


            public event PropertyChangedEventHandler PropertyChanged;
            public void OnPropertyChanged([CallerMemberName] string propertyName = "")
            {
                var handler = PropertyChanged;
                handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }

            /// <summary>
            /// 用于限制Button靠近边界和互相靠近的回调方法
            /// </summary>
            private Action<String, double> _call;
            /// <summary>
            /// 用于改变矩形区域大小的回调方法
            /// </summary>
            private Action _rectcall;

            private Rect _canvasRect;//代表中间透明矩形区域
            public Rect CanvasRect
            {
                get { return _canvasRect; }
                set
                {
                    _canvasRect = value;
                    OnPropertyChanged();
                }
            }

            private double _buttonWidth; //Button的宽度
            private double _minRectWidth;//中间透明矩形区域的最小宽度,不能让四个点重合,这儿最小宽度和最小高度都用这个来表示
            public PointModel(Action<string, double> pointAction, Action rectAction, double btnWidth, double minRectWidth)
            {
                _call = pointAction;
                _rectcall = rectAction;
                _buttonWidth = btnWidth;
                _minRectWidth = minRectWidth;
            }
        }
        public class RectModel : INotifyPropertyChanged
        {
            private GeometryGroup _group;
            public GeometryGroup Group
            {
                get { return _group; }
                set
                {
                    _group = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Group"));
                }
            }
            public event PropertyChangedEventHandler PropertyChanged;
        }    
View Code

  其中最繁杂的部分就是Set方法里面控制Button位置的代码,主要有以下两部分:

    1.保证Button不超出边界

    2.保证Button和其他Button的最小间距。

 

  在PointModel的构造器中,加入了两个Action,一个SetStaticPoint用来控制Button位置,另一个SetRect用来控制中间透明矩形的大小

    1.针对SetStaticPoint,不同的坐标执行不同的设置方法。

    2.针对SetRect,里面包含了构造Path的方法,如下

        private void SetRect()
        {
            if (group == null)
            {
                group = new GeometryGroup();
                group.FillRule = FillRule.EvenOdd;//设置规则为减去重叠部分。
            }
            group.Children.Clear();
            group.Children.Add(new RectangleGeometry() { Rect = new Rect { X = 0, Y = 0, Height = surface.ActualHeight, Width = surface.ActualWidth } });//大矩形区域,和Canvas同样大小

            ClipRect = new Rect { X = Points.X1 + ButtonWidth / 2.0, Y = Points.Y1 + ButtonWidth / 2.0, Width = Points.X2 - Points.X1, Height = Points.Y2 - Points.Y1 };//中间透明区域大小
            group.Children.Add(new RectangleGeometry() { Rect = ClipRect });

            RectPath.Group = group;
        }    

 

  大致核心如上,其他部分都是细枝末节了。最后我把整个逻辑用UserContrl做了一个简单的整合,弄了一个ClipRectangle控件。

  可以直接作为普通控件使用,只需要设置该控件的Width和Height即可,最后的裁切区域结果,通过一个自定义属性ClipRectProperty来提供。

  

  

  在写这个控件过程中,遇到几个问题和心得如下:

    1.x:Bind:这种绑定方式好像不支持针对附加属性的双向绑定,单向没问题

    2.起初一直尝试直接用Rectangle来实现,但都不行,直到后来用Blend把两个矩形进行相减合并时,发现XAML中把原来的Rectangle换成了Path,于是才有了想法。

    3.DependencyObject是可以说是XAML的核心,这东西一定要学好(现在只是大概会用而已)

  

  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~分割线

 

  点击这儿下载源码

 

目录
相关文章
|
算法 Windows
Winform控件优化之实现无锯齿的圆角窗体(或任意图形的无锯齿丝滑的窗体或控件)【借助LayeredWindow】
在一般能搜到的所有实现圆角窗体的示例中,都有着惨不忍睹的锯齿...而借助于Layered Windows,是可以实现丝滑无锯齿效果的Form窗体的,其具体原理就是分层窗体....
976 0
Winform控件优化之实现无锯齿的圆角窗体(或任意图形的无锯齿丝滑的窗体或控件)【借助LayeredWindow】
|
前端开发 C#
WPF 自定义的图表(适用大量数据绘制)下
原文:WPF 自定义的图表(适用大量数据绘制)下 上一篇文章中讲了WPF中自定义绘制大量数据的图标,思路是先将其绘制在内存,然后一次性加载到界面,在后续的调试过程中,发现当数据量到达10W时,移动鼠标显示数据有明显的延迟。
1394 0
|
程序员 开发工具 C语言
Qt编写自定义控件45-柱状标尺控件
一、前言 这个控件写了很久了,是最早期的一批控件中的一个,和温度计控件类似,都是垂直的进度条,可以设置不同的背景颜色,左侧的刻度也可以自由设定,还提供了动画效果,其实就是开启定时器慢慢的进度到设定的目标值,如果设定的值比当前值大,则递增,反之递减。
1092 0
Qt编写自定义控件28-颜色滑块面板
一、前言 相比于上一个颜色按钮面板,此控件就要难很多,颜色值有三种表示形式,除了程序员最常用的RGB以外,还有HSB和CMY方式。RGB色彩模式是工业界的一种颜色标准,是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。
887 0
|
开发工具 C语言
Qt编写自定义控件7-自定义可拖动多边形
一、前言 自定义可拖动多边形控件,原创作者是赵彦博(QQ:408815041 zyb920@hotmail.com),创作之初主要是为了能够在视频区域内用户自定义可拖动的多个区域,即可用来作为警戒区域,也可用来其他的处理,拿到对应的多边形坐标集合,本控件的主要难点是如何计算一个点在一个多边形区域内,何时完成一个多边形区域,支持多个多边形。
822 0
|
C#
自定义WPF 窗口样式
原文:自定义WPF 窗口样式 自定义 Window 在客户端程序中,经常需要用到自定义一个 Window ,大部分是为了好看吧。
1289 0
|
C# 前端开发 JavaScript
WPF绘制自定义窗口
原文:WPF绘制自定义窗口 WPF是制作界面的一大利器,下面就用WPF模拟一下360的软件管理界面,360软件管理界面如下:   界面不难,主要有如下几个要素: 窗体的圆角 自定义标题栏及按钮 自定义状态栏 窗体的半透明效果 窗体4周有一圈半透明阴影(抓的图上看不出来) 实现思路很简单,首先隐藏默认窗口的标题栏和边框,然后用WPF的Border或Canvas等元素模拟定义窗体的标题栏、内容区和状态栏。
1473 0
|
Java Linux PHP
WPF 自定义BarChartControl(可左右滑动的柱状图)
原文:WPF 自定义BarChartControl(可左右滑动的柱状图) 自定义可左右滑动、拖拽滑动的平面柱状图   在做这种样式控件之前,可先浏览我之前预研的控件: A、自定义左右滑动ScrollViewer(可拖动滑动) B、自定义Bar柱状图  OK,现在说下控件具体设计过程: 1)采用Grid布局,这样可以将Y轴的标题设置平均高度,X轴的柱子也可以平均。
887 0
|
C# 前端开发
WPF 自定义的图表(适用大量数据绘制)
原文:WPF 自定义的图表(适用大量数据绘制) 在WPF中绘制图表比较简单,有很多的第三方控件,但是在绘制大量数据的时候,就显得有些吃力,即便是自己用StreamGeometry画也达不到理想的效果,要达到绘制大量数据而不会顿卡现象,只有一个途径,就是首先在内存中绘制好所有的图形,再一次性加载(或者说绘制)到界面控件Canvas或Grid中。
1147 0

热门文章

最新文章