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

  文艺过后,就要看到重点了。上图是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 = - _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 = - _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;
}

  其中最繁杂的部分就是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 = , Y = , 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的核心,这东西一定要学好(现在只是大概会用而已)

  

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

  点击这儿下载源码

[uwp]自定义图形裁切控件的更多相关文章

  1. UWP 自定义密码框控件

    1. 概述 微软官方有提供自己的密码控件,但是控件默认的行为是输入密码,会立即显示掩码,比如 *.如果像查看真实的文本,需要按查看按钮. 而我现在自定义的密码控件是先显示你输入的字符2s,然后再显示成 ...

  2. C#自定义Button按钮控件

    C#自定义Button按钮控件 在实际项目开发中经常可以遇到.net自带控件并不一定可以满足需要,因此需要自定义开发一些新的控件,自定义控件的办法也有多种,可以自己绘制线条颜色图形等进行重绘,也可以采 ...

  3. Win10 UWP开发系列——开源控件库:UWPCommunityToolkit

    在开发应用的过程中,不可避免的会使用第三方类库.之前用过一个WinRTXamlToolkit.UWP,现在微软官方发布了一个新的开源控件库—— UWPCommunityToolkit 项目代码托管在G ...

  4. kettle系列-[KettleUtil]kettle插件,类似kettle的自定义java类控件

    该kettle插件功能类似kettle现有的定义java类插件,自定java类插件主要是支持在kettle中直接编写java代码实现自定特殊功能,而本控件主要是将自定义代码转移到jar包,就是说自定义 ...

  5. (转)sl简单自定义win窗体控件

    sl简单自定义win窗体控件      相信大家接触过不少win窗体控件ChildWin子窗口就的sl自带的一个  而且网上也有很多类似的控件,而今天我和大家分享下自己制作个win窗体控件,希望对初学 ...

  6. WPF自定义控件与样式(8)-ComboBox与自定义多选控件MultComboBox

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 下拉选 ...

  7. C# Winform 通过FlowLayoutPanel及自定义的编辑控件,实现快速构建C/S版的编辑表单页面

    个人理解,开发应用程序的目的,不论是B/S或是C/S结构类型,无非就是实现可供用户进行查.增.改.删,其中查询用到最多,开发设计的场景也最为复杂,包括但不限于:表格记录查询.报表查询.导出文件查询等等 ...

  8. [转]Oracle分页之二:自定义web分页控件的封装

    本文转自:http://www.cnblogs.com/scy251147/archive/2011/04/16/2018326.html 上节中,讲述的就是Oracle存储过程分页的使用方式,但是如 ...

  9. jquery和css自定义video播放控件

    下面介绍一下通过jquery和css自定义video播放控件. Html5 Video是现在html5最流行的功能之一,得到了大多数最新版本的浏览器支持.包括IE9,也是如此.不同的浏览器提供了不同的 ...

随机推荐

  1. Linux内核优化(未注释)

    Nginx代理服务内核优化 # Kernel sysctl configuration file for Red Hat Linux # # For binary values, 0 is disab ...

  2. OpenCL 图像卷积 2

    ▶ 上一篇图像卷积 http://www.cnblogs.com/cuancuancuanhao/p/8535569.html.这篇使用了 OpenCV 从文件读取彩色的 jpeg 图像,进行边缘检测 ...

  3. 人脸识别-<转>

    人脸检测库libfacedetection介绍 libfacedetection是于仕琪老师放到GitHub上的二进制库,没有源码,它的License是MIT,可以商用.目前只提供了windows 3 ...

  4. 迷你MVVM框架 avalonjs 0.92发布

    本版本最大的改进是引入ms-class的新风格支持,以前的不支持大写类名及多个类名同时操作,新风格支持了.还有对2维监控数组的支持.并着手修复UI框架. 重构 class, hover, active ...

  5. ajax基本常识及get请求方式

    <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"% ...

  6. 01b-1: 性能测度

  7. linux shell脚本编程笔记(三): 三种引号的区别

    双引号.单引号.反引号的区别 测试用例: OPDATE=`date -d '-1 day' +%Y%m%d` ) do FILEDATE=`date -d "-$i day" +% ...

  8. golang之panic,recover,defer

    defer,recover: 运行时恐慌一旦被引发,就会向调用方传播直至程序崩溃. recover内建函数用于“拦截”运行时恐慌,可以使当前的程序从恐慌状态中恢复并重新获得流程控制权. recover ...

  9. 一些json在js和c++ jsoncpp的操作

    1.对于javascript部分,如果将字符串转为json对象? var aa ={ keyword:"zoumm", requestcount:"5", ne ...

  10. Android系统编译与测试

    1.Android系统分析 2.下载Android源代码(不包括Linux内核部分) 下载好了的Android_5.01.tar.gz,通过samba复制到ubuntu里,再解压之. 可以看到Andr ...