利用WPF InkCanvas水墨控件圈选移动笔画
项目的开发需要,用到了WPF原生提供的InkCanvas控件,也有叫水墨控件。 需要开发的功能为鼠标光标随意圈选笔画,选中完成后移动圈选的笔画到画布其他地方。功能实现的效果如下所示:
本文只讲解实现的核心代码:
1:类似Photoshop的 Lasso工具的效果如何实现?
(锦上添花的UI效果:photoshop里圈选后的线框专业说法叫蚁行线,如果能做到动画更完美了)
2:套选工具选择后,高亮的填充透明层如何实现?
3:移动套选区域后,笔画如何跟随移动并从原本InkCanvas中移除?
首先看第一个问题:Lasso效果的实现:
这个很简单,通过MouseLeftButtonDown/Move/up的组合事件完成。
<Grid ><InkCanvas x:Name="ink" PreviewMouseDown="InkCanvas_PreviewMouseDown" PreviewMouseMove="InkCanvas_PreviewMouseMove"PreviewMouseLeftButtonUp="ink_PreviewMouseLeftButtonUp"Background="AliceBlue" Height="{Binding ActualHeight,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" Width="{Binding ActualWidth,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" ></InkCanvas></Grid>
Stroke lassoStroke;Point startPoint = new Point(0, 0);Point endPoint = new Point(0, 0);DrawingAttributes drawAttr;
private void InkCanvas_PreviewMouseDown(object sender, MouseButtonEventArgs e){startPoint = e.GetPosition(ink);}private void InkCanvas_PreviewMouseMove(object sender, MouseEventArgs e){if (e.LeftButton == MouseButtonState.Pressed || (e.RightButton == MouseButtonState.Pressed)){if (startPoint == new Point(0, 0)){startPoint = e.GetPosition(ink);}endPoint = e.GetPosition(ink);if (lassoStroke == null){StylusPointCollection pts = new StylusPointCollection();pts.Add(new StylusPoint(startPoint.X, startPoint.Y));pts.Add(new StylusPoint(endPoint.X, endPoint.Y));lassoStroke = new LassoStroke(pts, drawAttr) { DashStyle = new DashStyle(new double[] { 4, 10 }, 0) };this.ink.Strokes.Add(lassoStroke);}else{this.lassoStroke.StylusPoints.Add(new StylusPoint(endPoint.X, endPoint.Y));}}}private void ink_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e){if (lassoStroke == null){return;}//ink.Strokes.Remove(lassoStroke);lassoStroke.StylusPoints.Add(lassoStroke.StylusPoints[0]);startPoint = new Point(0, 0);endPoint = new Point(0, 0);lassoStroke = null;}
此时运行程序,在界面上随意拖拽,实现效果如下图所示
2:再看第二个问题:圈选后的Lasso 填充和切割笔画移动;
这个是核心功能:对于Lasso 填充移动,我这里用的是WPF Adorner 技术;就是给inkCanvas一个装饰层,在装饰层里绘制当前的Lasso路径;接着我们实现这个功能;
编写LassoAdorner类继承Adorner,override Render方法;在Render方法里需要绘制 套索封闭圈本身的路径、套索切割InkCanvas控件的笔画数据等;
这里是通过下面两行代码实现的:
CreatLassoDrawing(drawingContext);// 绘制切割后的线条
lassoStroke.Draw(drawingContext);// 绘制虚线圈
private Stroke lassoStroke;// 套索的边框外形private StrokeCollection clipStrokes;// 套索切割的笔画private StrokeCollection lassoEraseFinalStrokes; // lasso 擦除区域外围产生的笔画
void CreatLassoDrawing(DrawingContext drawingContext){if (clipStrokes == null)return;foreach (Stroke stroke in clipStrokes){Brush brush = new SolidColorBrush(stroke.DrawingAttributes.Color);if (stroke.DrawingAttributes.IsHighlighter){brush.Opacity = 0.5;}Pen pen = new Pen{StartLineCap = PenLineCap.Round,EndLineCap = PenLineCap.Round,Brush = new SolidColorBrush(stroke.DrawingAttributes.Color),Thickness = stroke.DrawingAttributes.Width,};stroke.Draw(drawingContext);}}
其中clipStrokes的实例化逻辑如下:给LassoAdorner定义一个Show方法,在此方法中实例化clipStrokes对象,它代码被套索区域所切割后的笔画集合:
internal void Show(Stroke stroke){this.Visibility = System.Windows.Visibility.Visible;lassoStroke = stroke;lassoStroke.StylusPoints.Add(stroke.StylusPoints[0]);//在UI上形成封闭圈效果GenerateClipStrokes();GenerateEraseLassoStroke();if (clipStrokes == null || clipStrokes.Count == 0){Hide();return;}this.InvalidateVisual();}
其中的GenerateClipStrokes 和GenerateEraseLassoStroke分别是计算套索切割内的笔画集合和套索区域外被切割后的笔画集合;这里算是核心代码了,百度不到也没有发现可用的资源,经过本人不断尝试和对WPF 2D Drawing相关章节的研究学习得出下面的代码:
/// <summary>/// 创建 Lasso 套选住的笔画 集合/// </summary>void GenerateClipStrokes(){StreamGeometry pg = (StreamGeometry)this.lassoStroke.GetGeometry();PathGeometry pathG = pg.GetOutlinedPathGeometry();InkCanvas ink = (this.AdornedElement as InkCanvas);for (int i = 0; i < pathG.Figures.Count() - 1; i++){PathFigure pfc = pathG.Figures[i];PathSegmentCollection psc = pfc.Segments;List<Point> pointList = new List<Point>();#region 找出需要裁切的笔画集合foreach (var ps in psc){if (ps is PolyLineSegment){PolyLineSegment pls = ps as PolyLineSegment;pointList.AddRange(pls.Points.ToArray());}else if (ps is LineSegment){LineSegment ls = ps as LineSegment;pointList.Add(ls.Point);}else if (ps is BezierSegment){BezierSegment bs = ps as BezierSegment;pointList.Add(bs.Point1);pointList.Add(bs.Point2);pointList.Add(bs.Point3);}else if (ps is PolyBezierSegment){PolyBezierSegment pbs = ps as PolyBezierSegment;pointList.AddRange(pbs.Points.ToArray());}else if (ps is ArcSegment){ArcSegment asm = ps as ArcSegment;pointList.Add(asm.Point);}else if (ps is PolyQuadraticBezierSegment){PolyQuadraticBezierSegment pbs = ps as PolyQuadraticBezierSegment;pointList.AddRange(pbs.Points.ToArray());}else if (ps is QuadraticBezierSegment){QuadraticBezierSegment qbs = ps as QuadraticBezierSegment;pointList.Add(qbs.Point1);pointList.Add(qbs.Point2);}}#endregionStrokeCollection clipStrokesTemp = new StrokeCollection();if (clipStrokes == null){foreach (Stroke sk in ink.Strokes){StrokeCollection sc = sk.GetClipResult(pointList); //后一个封闭 圈出来的切割后的线段;clipStrokesTemp.Add(sc);}clipStrokes = new StrokeCollection(clipStrokesTemp);}else{foreach (Stroke sk in ink.Strokes){StrokeCollection sc = sk.GetClipResult(pointList); //后一个封闭 圈切割后的线段;clipStrokesTemp.Add(sc);}clipStrokes.Add(clipStrokesTemp);}} }/// <summary>/// 获取擦除结果/// </summary> void GenerateEraseLassoStroke(){StreamGeometry pg = (StreamGeometry)this.lassoStroke.GetGeometry();PathGeometry pathG = pg.GetOutlinedPathGeometry();InkCanvas ink = (this.AdornedElement as InkCanvas);for (int i = 0; i < pathG.Figures.Count() - 1; i++){PathFigure pfc = pathG.Figures[i];PathSegmentCollection psc = pfc.Segments;List<Point> pointList = new List<Point>();#region 找出需要裁切的笔画集合foreach (var ps in psc){if (ps is PolyLineSegment){PolyLineSegment pls = ps as PolyLineSegment;pointList.AddRange(pls.Points.ToArray());}else if (ps is LineSegment){LineSegment ls = ps as LineSegment;pointList.Add(ls.Point);}else if (ps is BezierSegment){BezierSegment bs = ps as BezierSegment;pointList.Add(bs.Point1);pointList.Add(bs.Point2);pointList.Add(bs.Point3);}else if (ps is PolyBezierSegment){PolyBezierSegment pbs = ps as PolyBezierSegment;pointList.AddRange(pbs.Points.ToArray());}else if (ps is ArcSegment){ArcSegment asm = ps as ArcSegment;pointList.Add(asm.Point);}else if (ps is PolyQuadraticBezierSegment){PolyQuadraticBezierSegment pbs = ps as PolyQuadraticBezierSegment;pointList.AddRange(pbs.Points.ToArray());}else if (ps is QuadraticBezierSegment){QuadraticBezierSegment qbs = ps as QuadraticBezierSegment;pointList.Add(qbs.Point1);pointList.Add(qbs.Point2);}}#endregionStrokeCollection eraseStrokesTemp = new StrokeCollection();if (lassoEraseFinalStrokes == null){foreach (Stroke sk in ink.Strokes){StrokeCollection sc = sk.GetEraseResult(pointList); //后一个封闭 圈出来的切割后的线段;eraseStrokesTemp.Add(sc);}lassoEraseFinalStrokes = new StrokeCollection(eraseStrokesTemp);}else{// 从新产生的strokes中 重新获取累计的擦除结果foreach (Stroke sk in lassoEraseFinalStrokes){StrokeCollection sc = sk.GetEraseResult(pointList); //后一个封闭 圈切割后的线段;eraseStrokesTemp.Add(sc);}lassoEraseFinalStrokes = new StrokeCollection(eraseStrokesTemp);}} }
下图是方便理解:做了一个示意图,分别说明clipStrokes和 lassoEraseFinalStrokes的具体作用;
最后看下圈选后的移动:这里是用Thumb对象来实现的,具体代码不做说明,看下源码就明白了。这里贴一个Thumb移动完成的事件处理:
private void OnMoveHandlerDragDelta(object sender, DragDeltaEventArgs e){double offsetX = e.HorizontalChange;double offsetY = e.VerticalChange;if (lassoStroke == null){return;}Rect bounds = lassoStroke.GetBounds();if (e.HorizontalChange < 0 && bounds.Left < 0){offsetX = 0;}if (e.VerticalChange < 0 && bounds.Top < 0){offsetY = 0;}if (bounds.Right + offsetX>=Application.Current.MainWindow.ActualWidth){offsetX = 0;}if (bounds.Bottom + offsetY >= Application.Current.MainWindow.ActualHeight-40) // 40 为menu Title的高度{offsetY = 0;}dragOffSetX += offsetX;dragOffSetY += offsetY;lassoStroke.Transform(new Matrix(1, 0, 0, 1, offsetX, offsetY), false);clipStrokes.Transform(new Matrix(1, 0, 0, 1, offsetX, offsetY), false);this.InvalidateVisual();}
如需要源码,请下载本人上传的资源,下载路径如下:
https://download.csdn.net/download/elie_yang/10566298
利用WPF InkCanvas水墨控件圈选移动笔画相关推荐
- html 控件坐标定位,利用JS改变html控件位置
8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? 利用JS改变html控件位置 我想写一个贪吃蛇小游戏,所以需要完成蛇的自动移动效果,这就需要改变html控件位置.本来我 ...
- WPF源码控件库《Newbeecoder.UI》轮播
轮播控件是一种强大且视觉上吸引人的方式来呈现多个数据项,本文讨论Newbeecoder.UI轮播控件的原理和一个简单的演示应用程序. 轮播控件是包含Canvas控件的 WPF 用户控件,项目控件是的子 ...
- 【Excel2019(二十一):经典动态图实现原理】【动态图表实现原理+利用OFFSET函数与控件创建动态图表】
上一篇:[Excel2019(二十):图表基础][认识图表中的元素+创建并美化柱形图] 文章目录 动态图表实现原理 理解图表中的数据系列 手工修改系列中的数值与坐标轴数据 利用IF创建简单的动态图表 ...
- WPF 自定义DataGrid控件样式
WPF 自定义DataGrid控件样式 样式一: 样式代码: <!--DataGrid样式--><Style TargetType="DataGrid">& ...
- 有关WPF中DataGrid控件的基础应用总结
基础说明 DataGrid是WPF提供的基础控件,它可以非常轻松的呈现出一张表格,本文章会按照从易到难的顺序依次将DataGrid的使用方法进行解说,除了MSDN上给出的最基本的例子之外,给出了三个比 ...
- WPF中DataGrid控件
WPF中DataGrid控件的个别属性使用 //设置不可自动拉伸宽度dataGrid.CanUserResizeColumns = false;//第一列不可见dataGrid.HeadersVisi ...
- WPF查找子控件和父控件方法
原文:WPF查找子控件和父控件方法 public List<T> GetChildObjects<T>(DependencyObject obj, string name) w ...
- WPF 动画显示控件
原文:WPF 动画显示控件 当我们要显示一个控件的时候,不仅仅要显示这个控件,还要有动画的效果. 主要用到了DoubleAnimation类. public static void ShowAnima ...
- WPF的Timer控件的使用
原文:WPF的Timer控件的使用 通过System.Threaing.Timer控件来实现"初始加载页面时为DataGrid的模版列赋初始值" System.Threaing.T ...
最新文章
- hibernate中持久化对象的生命周期(三态:自由态,持久态,游离态 之间的转换)...
- pythonanywhere使用:进入虚拟机及修改django项目的css样式
- 借力 Docker ,三分钟搞定 MySQL 主从复制!
- centos7环境下MySQL安装教程
- 一行一个链接代码_AI最优论文+代码查找神器:966个ML任务、8500+论文任你选
- 关于CRM库存初始化的一点小总结
- OpenCV图像几何变换——转置,镜像,倒置
- 【转载】关于.NET下开源及商业图像处理(PSD)组件
- mysql几百万的表关联_mysql SQL优化,百万级2张表关联,从40分钟到3秒
- 正确方式安装Acrobat DC(附安装包)
- php数据库太小要怎么改,PHP入坑之 MySqli对数据库增删改查
- HIVE SQL分位数percentile使用方法案例
- linux编程常用指令
- CentOS 7网卡网桥设置
- 薅羊毛的机会了,点个“赚”即有机会赚取高额佣金
- 『Reprint』GRADUAL
- 双向晶闸管控制AC220V电机
- java开发之异常处理_SimpleMappingExceptionResolver
- 网上销售平台--需求分析(二)
- 20-python学习笔记之日期