- A+
本文接上一篇WPF源码阅读 -- InkCanvas选择模式,本文介绍笔迹的选择过程及选中后的高亮显示方法,文中若有理解错误的地方,欢迎大家指正。选择效果如下图所示:
InkCanvas是WPF中用于墨迹书写的控件,其具有书写、选择、擦除等模式。根据上图,可以看出笔迹的选择功能由如下三部分组成:
- 选择笔迹(Lasso Stroke)
- 动态选择
- 选中后高亮显示
本文将首先介绍选择模式的激活过程,然后介绍如上三部分内容WPF是如何实现的。
选择模式的激活
从图中可以看出,切换到选择模式后,鼠标按下移动绘制的效果为黄色点状虚线(Lasso),根据Lasso及一定的算法进行笔迹的选中与取消选中。
先看InkCanvas切换到选择模式后的动作。切换到选择模式后,EditingMode改变,调用OnEditingModeChanged方法,该方法调用RaiseEditingModeChanged方法。RaiseEditingModeChanged方法中,调用了_editingCoordinator.UpdateEditingState方法,并通过OnEditingModeChanged引发事件。
切换到EditingCoordinator类,可以看到依次调用UpdateEditingState() -> ChangeEditingBehavior() -> PushEditingBehavior()。UpdateEditingState方法调用GetBehavior方法拿到新Behavior(SelectionEditor)。PushEditingBehavior方法会吊销之前的Behavior,并激活新的Behavior,即SelectionEditor会被激活。
切换到SelectionEditor类,在其OnActivate方法中监听了事件,在OnAdornerMouseButtonDownEvent方法中,调用了EditingCoordinator.ActivateDynamicBehavior方法激活了LassoSelectionBehavior。至此,选择模式已激活,并将随着设备移动绘制Lasso。
// InkCanvas private static void OnEditingModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ( (InkCanvas)d ).RaiseEditingModeChanged( new RoutedEventArgs( InkCanvas.EditingModeChangedEvent, d)); } private void RaiseEditingModeChanged(RoutedEventArgs e) { Debug.Assert(e != null, "EventArg can not be null"); _editingCoordinator.UpdateEditingState(false /* EditingMode */); this.OnEditingModeChanged(e); } // EditingCoordinator internal void UpdateEditingState(bool inverted) { // ... EditingBehavior newBehavior = GetBehavior(ActiveEditingMode); ... ChangeEditingBehavior(newBehavior); // ... } private void ChangeEditingBehavior(EditingBehavior newBehavior) { // ... PushEditingBehavior(newBehavior); // ... } private void PushEditingBehavior(EditingBehavior newEditingBehavior) { // ... behavior.Deactivate(); ... newEditingBehavior.Activate(); } // SelectionEditor private void OnAdornerMouseButtonDownEvent(object sender, MouseButtonEventArgs args) { // ... EditingCoordinator.ActivateDynamicBehavior(EditingCoordinator.LassoSelectionBehavior, args.StylusDevice != null ? args.StylusDevice : args.Device); }
选择笔迹(Lasso Stroke)
LassoSelectionBehavior继承自StylusEditingBehavior,随着设备的移动,会调用AddStylusPoints方法。该方法会调用StylusInputBegin、StylusInputContinue方法。StylusInputBegin会调用StartLasso方法,该方法创建了LassoHelper对象,该对象将绘制Lasso。
// StylusEditingBehavior void IStylusEditing.AddStylusPoints(StylusPointCollection stylusPoints, bool userInitiated) { // ... if ( !EditingCoordinator.UserIsEditing ) { EditingCoordinator.UserIsEditing = true; StylusInputBegin(stylusPoints, userInitiated); } else StylusInputContinue(stylusPoints, userInitiated); } // LassoSelectionBehavior private void StartLasso(List<Point> points) { // ... _lassoHelper = new LassoHelper(); // ... } // LassoHelper public const double MinDistanceSquared = 49.0; const double DotRadius = 2.5; const double DotCircumferenceThickness = 0.5; const double ConnectLineThickness = 0.75; const double ConnectLineOpacity = 0.75; static readonly Color DotColor = Colors.Orange; static readonly Color DotCircumferenceColor = Colors.White; private void AddLassoPoint(Point lassoPoint) { // ... dc.DrawEllipse(_brush, _pen, lassoPoint, DotRadius, DotRadius); // ... }
动态选择
LassoSelectionBehavior中有一个IncrementalLassoHitTester对象,该对象实现Lasso的hit-testing。当选中的笔迹有变化时,会引发其SelectionChanged事件,该事件参数LassoSelectionChangedEventArgs包含SelectedStrokes集合(动态选中的笔迹)与DeselectedStrokes集合(动态取消选中的笔迹)。该事件响应函数调用InkCanvas的UpdateDynamicSelection方法,进而实现笔迹的动态选中与取消选中。
// LassoSelectionBehavior private void StartLasso(List<Point> points) { // ... _incrementalLassoHitTester = this.InkCanvas.Strokes.GetIncrementalLassoHitTester(_percentIntersectForInk); _incrementalLassoHitTester.SelectionChanged += new LassoSelectionChangedEventHandler(OnSelectionChanged); // ... if (0 != lassoPoints.Length) _incrementalLassoHitTester.AddPoints(lassoPoints); // ... } private void OnSelectionChanged(object sender, LassoSelectionChangedEventArgs e) { this.InkCanvas.UpdateDynamicSelection(e.SelectedStrokes, e.DeselectedStrokes); } // InkCanvas internal void UpdateDynamicSelection(StrokeCollection strokesToDynamicallySelect, StrokeCollection strokesToDynamicallyUnselect) { // ... if (strokesToDynamicallySelect != null) { foreach (Stroke s in strokesToDynamicallySelect) { _dynamicallySelectedStrokes.Add(s); s.IsSelected = true; } } if (strokesToDynamicallyUnselect != null) { foreach (Stroke s in strokesToDynamicallyUnselect) { System.Diagnostics.Debug.Assert(_dynamicallySelectedStrokes.Contains(s)); _dynamicallySelectedStrokes.Remove(s); s.IsSelected = false; } } }
选中高亮
从上文中可以看出,选中后的笔迹其IsSelected属性为true。直接看Stroke的DrawCore方法,如下代码中的_drawAsHollow的值由IsSelected决定。可以看出WPF绘制笔迹高亮的方式其实是绘制了两次Geometry,先绘制宽点的,再绘制窄点的,两个Geometry重叠后就形成了高亮效果。根据WPF注释描述,采用这种方法的效率是GetOutlinePathGeometry方法的五倍。
protected virtual void DrawCore(DrawingContext drawingContext, DrawingAttributes drawingAttributes) { //... if (_drawAsHollow == true) { Matrix innerTransform, outerTransform; DrawingAttributes selectedDA = drawingAttributes.Clone(); selectedDA.Height = Math.Max(selectedDA.Height, DrawingAttributes.DefaultHeight); selectedDA.Width = Math.Max(selectedDA.Width, DrawingAttributes.DefaultWidth); CalcHollowTransforms(selectedDA, out innerTransform, out outerTransform); selectedDA.StylusTipTransform = outerTransform; SolidColorBrush brush = new SolidColorBrush(drawingAttributes.Color); brush.Freeze(); drawingContext.DrawGeometry(brush, null, GetGeometry(selectedDA)); selectedDA.StylusTipTransform = innerTransform; drawingContext.DrawGeometry(Brushes.White, null, GetGeometry(selectedDA)); } // ... }