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