项目的开发需要,用到了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);
}

}

#endregion
StrokeCollection 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);
}

}

#endregion
StrokeCollection 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

————————————————
版权声明:本文为CSDN博主「大洋彼岸789」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/elie_yang/article/details/81083441