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

  文艺过后,就要看到重点了。上图是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. MyBait 符号大于 小于理解

    EQ 就是 EQUAL等于 NQ 就是 NOT EQUAL不等于 GT 就是 GREATER THAN大于 LT 就是 LESS THAN小于 GE 就是 GREATER THAN OR EQUAL ...

  2. 关于directshow的SmartTee

    可以通过CLSID_SmartTee创建: CComPtr<IBaseFilter> pSmartTee; // Create the Smart Tee (CLSID_SmartTee) ...

  3. 获取select值及判断是否是数字

    代码片段 <div class="container-fluid"> <div class="row"> <div class=& ...

  4. ubuntu ufw防火墙

    由于LInux原始的防火墙工具iptables过于繁琐,所以ubuntu默认提供了一个基于iptable之上的防火墙工具ufw. ubuntu 9.10默认的便是UFW防火墙,它已经支持界面操作了.在 ...

  5. jQuery的基础dom和css操作

    1.元素以及内容操作 $(function () { // alert($("a").html()); // 获取元素中间的html内容,包括标签和文本内容 // alert($( ...

  6. Git学习笔记——从一台电脑上传文件到Github上

    目标:从一台电脑上传文件到Github上 前提: 1.这里假定已在Github上创建了仓库,建立了仓库 2.已在这台电脑上安装了Git客户端 实验环境: 1.Windows 10 64位,已安装了Gi ...

  7. ACTIVITI 5.14事件监听器的BUG

    在ACTIVITI 5.14中,测试内部子流程时发现事件定义的事件监听器不能触发. <activiti:executionListener event="start" del ...

  8. python的return

    关于python的return用法,在stackoverflow里的问题: Python — return, return None, and no return at all Consider th ...

  9. 超级详细的解决方法 (CentOS7) :永久修改 mysql read-only 问题 could not retrieve transation read-only status server

    一.查看mysql的事物隔离级别 SHOW VARIABLES LIKE '%iso%'; 二.临时修改事物隔离级别 SET GLOBAL tx_isolation='READ-COMMITTED'; ...

  10. PL/SQL Developer 窥探事务

    一次登录代表一个连接 一个SQL Window 代表一个会话(session),有唯一的SID 事务(transaction) 由 insert .update 或者 delete 开启 由 comm ...