65.9K
CodeProject 正在变化。 阅读更多。
Home

开发多选控件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (17投票s)

2009年11月29日

BSD

15分钟阅读

viewsIcon

69536

downloadIcon

1327

一个 MultiSelector 派生控件,支持多选模式、作用域选择、可自定义的套索工具和选择逻辑

引言

MultiSelector 类由 .NET 3.5 SP1 引入。这个原始类的主要好处在于,它暴露了足够的 Selector 内部机制,使开发者能够编写一个能够处理自定义选择的优秀控件,而无需继承 ListBox 并处理其历史遗留问题。SelectionArea 就是这样一个控件,它包含了一些可能在某些情况下很有用的增强功能。首先,让我们简要回顾一下主要功能和组件,然后分析代码。

项目容器

SelectionAreaItem 是一个非常简单的类。它继承自 ContentControl,并分别为 Selector.SelectedEventSelector.UnselectedEvent 路由事件以及 Selector.IsSelectedProperty 属性添加了自身作为所有者。当该属性发生变化时,会相应地引发这两个事件之一。

它还重写了 OnMouseLeftButtonDown 方法,通过调用 SelectionArea 的一个内部方法来通知其父容器它已被点击(选择逻辑将在那里处理)。

internal SelectionArea ParentSelectionArea
{
   get
   {
      return ItemsControl.ItemsControlFromItemContainer(this) as SelectionArea;
   }
}

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
   base.OnMouseLeftButtonDown(e);

   var parent = ParentSelectionArea;

   if (parent != null)
   {
      parent.NotifyItemClicked(this);
   }

   e.Handled = true;
}

选择模式

SelectionArea 可以处理四种类型的选择。这些由 SelectionType 枚举定义。

public enum SelectionType
{
   Single,
   Multiple,
   Extended,
   Lasso
}

前三个值借用了 ListBox 使用的 SelectionMode,并且操作几乎相同,但有一个例外:Extended 模式不处理 Shift 键修改器(用于选择连续的项目)。在本文中,我们不会过多地纠结于这三种模式,因为它们在使用和实现上都非常清晰。第四个值允许您使用套索来选择项目。这是默认值。

public static readonly DependencyProperty SelectionTypeProperty =
   DependencyProperty.Register(
      "SelectionType",
      typeof(SelectionType),
      typeof(SelectionArea),
      new PropertyMetadata(SelectionType.Lasso));

当我们通过点击项目容器来选择项目时,我们称之为“直接点击选择”,这与“套索选择”相对——即通过套索捕获项目来选择。前三种模式仅支持直接点击选择。Lasso 模式则两者都支持。在进行直接点击选择时,Lasso 模式的功能与 Extended 模式完全相同。直接点击选择逻辑由 NotifyItemClicked 方法处理。实际的套索选择在点击空白区域(即直接点击 ItemsPanel)、(可选地)按住鼠标按钮移动然后释放时触发。OnMouseLeftButtonDownOnMouseMoveOnMouseLeftButtonUp 方法会被重写以处理此类选择。

作用域选择

有时您可能需要两个或多个 Selector 控件协同工作,形成一个单元,以便在给定时刻只有一个控件可以拥有选定的项目——您希望将选择视为整个单元整体。在 SelectionArea 中,通过使用作用域(scopes)来实现这一点。一个作用域是一组 SelectionArea,其中任意两个 SelectionArea 不能同时拥有选定的项目。在使用作用域时,根据 SelectionType,选择行为可能有所不同。

  • 对于非累加*模式,以及对于未按 Ctrl 键的累加模式,当在某个区域内选择项目时,如果存在同一作用域内拥有已选项目的其他区域,则该区域将被清空(所有选定的项目都将被取消选择)。
  • 对于按 Ctrl 键的累加模式,如果一个区域至少有一个选定的项目,您只能在该区域内选择;在同一作用域内的其他区域选择将不可能实现。
* 我们将“累加”模式定义为按 Ctrl 键时可以“累加”项目的选择类型(选择之前未选的项目并保留当前已选的项目);在这方面,ExtendedLasso 模式是累加的;相反,SingleMultiple 模式是非累加的。

可以通过将 UseScopedSelection 设置为 true 并指定 Scope 来激活作用域选择(此属性的默认值为 "Scope",因此如果您只有一个作用域,则无需设置它,除非您想赋予它更具意义的名称)。

public static readonly DependencyProperty UseScopedSelectionProperty =
   DependencyProperty.Register(
      "UseScopedSelection",
      typeof(bool),
      typeof(SelectionArea));

public static readonly DependencyProperty ScopeProperty =
   DependencyProperty.Register(
      "Scope",
      typeof(string),
      typeof(SelectionArea),
      new PropertyMetadata("Scope", OnScopeChanged));

直接点击选择

当点击一个项目容器时,SelectionAreaItem 会处理该事件,并通过调用 NotifyItemClicked 来通知其父容器。

internal void NotifyItemClicked(SelectionAreaItem item)
{
  clickedItem = null;
  
  switch (SelectionType)
  {
     case SelectionType.Single:
        if (!item.IsSelected)
        {
           ClearTargetArea();
           Select(item);
        }
        else if (IsControlKeyPressed)
        {
           Unselect(item);
        }
        break;
     case SelectionType.Multiple:
        if (UseScopedSelection && FocusedArea != this)
        {
           ClearTargetArea();
        }
        ToggleSelect(item);
        break;
     case SelectionType.Extended:
     case SelectionType.Lasso:
        if (!CanPerformSelection)
        {
           return;
        }

        if (IsControlKeyPressed)
        {
           ToggleSelect(item);
        }
        else if (!item.IsSelected)
        {
           ClearTargetArea();
           Select(item);
        }
        else
        {
           clickedItem = item;
        }
     break;
  }

  FocusedArea = this;
  Mouse.Capture(this);
}

前两种情况不言自明。Single 模式一次只允许选择一个项目。Multiple 模式在您点击时切换选中状态。在使用作用域选择时,这些条件必须与限制相结合,即每个作用域中只有一个 SelectionArea 可以拥有选定的项目。ClearTargetArea 方法会取消选中“目标区域”中的所有项目。这个目标区域要么是当前区域(被点击项目的父容器),要么是作用域选择时所处的聚焦区域(同一作用域内上一次成功选择发生所在的区域,该区域可能就是当前区域,也可能不是)。

private void ClearTargetArea()
{
   var area = UseScopedSelection ? FocusedArea : this;

   if (area != null)
   {
      area.UnselectAll();
   }
}

在处理累加模式时,选择并非总是能够进行。具体来说,在使用作用域选择且按住 Ctrl 键时:在这种情况下,您只能在聚焦区域内选择项目。所以,要么这个区域是当前区域,要么它没有选定的项目(要么是 null,这是在之前没有在此作用域内进行任何选择的情况)。在任何其他情况下,都不允许选择。

private bool IsControlKeyPressed
{
   get
   {
      return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
   }
}

private bool CanPerformSelection
{
   get
   {
      return !UseScopedSelection ||
             !IsControlKeyPressed ||
             FocusedArea == null ||
             FocusedArea == this ||
             FocusedArea.SelectedItems.Count == 0;
   }
}

当点击一个已选项目,且未按 Ctrl 键时,会保存该项目的引用。我们需要这样做,因为在 SelectionArea 中,累加模式不会在鼠标按下时取消选择其他项目;它们将在鼠标释放时取消选择,因此我们需要知道不应取消选择哪个项目。这样做的逻辑是,我们可能希望在鼠标移动时对(所有)选定的项目执行某些操作(例如,使用附加行为将它们拖动到其他位置,或其他任何操作)。

如果成功执行了选择(或取消选择),则当前区域将成为其作用域内的聚焦区域(如果使用了作用域)。

套索选择

Lasso 模式下,如果您点击项目容器之间的空白区域,而不是直接点击它们,则会开始套索选择。套索使用 AdornerLayer 进行渲染,因此需要一个 AdornerDecorator(如果您重新设计了顶部控件的模板,请确保在视觉树中显式添加一个)。如果无法获取 AdornerLayer 对象,在初始化时实际上会抛出异常。套索的实际外观和行为并非内置于控件中。用户可以通过设置两个属性来自定义这些方面:LassoTemplateLassoGeometry

LassoTemplate 定义了套索的外观。内部,SelectionArea 使用一个 TemplatedAdorner 对象来绘制套索。TemplatedAdorner 类非常简单,我们不再详细介绍代码:它有一个单独的 Control 可视子元素;SelectionArea 为此子元素提供了一个模板——用户设置的 LassoTemplate,以及用于排列它的 Rect

public static readonly DependencyProperty LassoTemplateProperty =
   DependencyProperty.Register(
      "LassoTemplate",
      typeof(ControlTemplate),
      typeof(SelectionArea),
      new PropertyMetadata(OnLassoTemplateChanged));

private static void OnLassoTemplateChanged
	(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
   (target as SelectionArea).adorner.Refresh(e.NewValue as ControlTemplate);
}

LassoTemplate 更改时,adorner 会用新模板更新其可视子元素。

LassoGeometry 定义了选择逻辑。它是一个 Geometry 对象,用于在鼠标释放时进行命中测试。任何完全位于此几何形状内部或被其相交的项目都会被选中。这提供了相当大的灵活性,允许您以不同的方式选择项目。最常用的是经典的矩形选择,但如果您愿意,也可以指定更“奇特”的选择行为——任何几何形状都可以。

public static DependencyProperty LassoGeometryProperty =
   DependencyProperty.Register(
      "LassoGeometry",
      typeof(Geometry),
      typeof(SelectionArea),
      new PropertyMetadata(OnLassoGeometryChanged));

private static void OnLassoGeometryChanged
	(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
   var area = target as SelectionArea;
   var geometry = e.NewValue as Geometry;

   if (area.LassoArrangeType == LassoArrangeType.LassoBounds && geometry != null)
   {
      area.adorner.Refresh(geometry.Bounds);
   }
}

套索模板和几何形状通常依赖于鼠标移动。为了适应这种情况,SelectionArea 暴露了两个只读的 Point 类型 DependencyPropertiesStartPosition(保存左键按下时相对于 SelectionArea 的位置)和 CurrentPosition(跟踪当前位置,并在按住按钮移动鼠标时更新)。这两个属性涵盖了自定义套索工具的大部分需求。

套索 adorner 可以有两种排列方式:在 LassoGeometryBounds 内部,或在定义 SelectionArea 剪裁区域的几何形状的 Bounds 内部。您可以通过将 LassoArrangeType 属性设置为其中一个值来选择排列类型。

public enum LassoArrangeType
{
   LassoBounds,
   ClipBounds
}

如果您的模板依赖于 StartPosition 和/或 CurrentPosition,则必须使用 ClipBounds 进行排列——相对于 SelectionArea 进行排列是有意义的,因为模板依赖于相对于它的位置。如果模板在几何定位方面不依赖于 SelectionArea,则使用 LassoBounds——在这种情况下,模板必须设计成当排列边界发生变化时外观也会随之改变,前提是您想要一个流畅的套索。您可以有一个不依赖于鼠标移动的静态模板(不要将其绑定到任何与鼠标相关的属性,并使用 ClipBounds 进行排列),但这将不会很有趣。

public static readonly DependencyProperty LassoArrangeTypeProperty =
   DependencyProperty.Register(
      "LassoArrangeType",
      typeof(LassoArrangeType),
      typeof(SelectionArea),
      new PropertyMetadata(OnLassoArrangeTypeChanged));

private static void OnLassoArrangeTypeChanged
	(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
   var area = target as SelectionArea;
   Geometry geometry = null;
   Transform transform = null;

   switch ((LassoArrangeType)e.NewValue)
   {
      case LassoArrangeType.LassoBounds:
         geometry = area.LassoGeometry;
         break;
      case LassoArrangeType.ClipBounds:
         geometry = area.clipGeometry;

         if (geometry != null)
         {
            transform = (Transform)geometry.Transform.Inverse;
         }
         break;
      default:
         break;
   }

   if (geometry != null)
   {
      area.adorner.Refresh(geometry.Bounds, transform);
   }
}

除了设置用于排列的 Rect 之外,在使用 ClipBounds 时,我们还会将一个 Transform 应用于 adorner 的子元素。这是因为我们使用的是相对于 SelectionArea 的位置,但套索是在 AdornerLayer 上渲染的,因此我们需要一个变换来翻译坐标。这个变换已经计算好了,是间接的:与 SelectionArea 关联的 AdornerLayer 对象会被裁剪,以使套索不会超出 SelectionArea 的边界。每次区域大小改变时,都会重新计算裁剪几何形状。

protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
   base.OnRenderSizeChanged(sizeInfo);

   clipGeometry = new RectangleGeometry(VisualTreeHelper.GetDescendantBounds(this));
   clipGeometry.Transform = (Transform)TransformToVisual(adornerLayer);
   clipGeometry.Freeze();

   if (LassoArrangeType == LassoArrangeType.ClipBounds)
   {
      adorner.Refresh(clipGeometry.Bounds, (Transform)clipGeometry.Transform.Inverse);
   }
}

我们对裁剪几何形状应用一个变换,将坐标从 SelectionArea 翻译到 AdornerLayer。对于套索绘制,我们需要完全相反的效果:将坐标从 AdornerLayer 翻译到 SelectionArea,因此我们取该变换的逆并将其传递给 adorner。

始终牢记套索 adorner 在此边界内进行排列是很重要的。如果您想突破这些边界,您可以随时在 LassoTemplate 中为您的元素应用变换,但要小心:如果您为套索工具提供了视觉表示(正如我们稍后将看到的,这不是必需的),那么期望屏幕上显示的几何表示与用于选择的几何逻辑之间的一致性是很自然的。

以下是处理套索选择的方法。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
   base.OnMouseLeftButtonDown(e);

   switch (SelectionType)
   {
      case SelectionType.Single:
      case SelectionType.Multiple:
      case SelectionType.Extended:
         break;
      case SelectionType.Lasso:
         if (!CanPerformSelection)
         {
            return;
         }

         if (!IsControlKeyPressed)
         {
            ClearTargetArea();
         }

         StartPosition = Mouse.GetPosition(this);
         CurrentPosition = StartPosition;
         
         clickedItem = null;
         FocusedArea = this;

         adornerLayer.Clip = clipGeometry;
         adornerLayer.Add(adorner);

         isMouseDown = true;
         e.Handled = true;

         Mouse.Capture(this);
         break;
      default:
         break;
   }
}

套索选择与之前讨论的直接点击选择一样,受到相同的范围限制。所以,如果选择无法执行,我们只需返回。如果不按 Ctrl 键,我们会清除目标区域。否则,我们会保留已选项目不变,并且通过几何命中测试新选定的项目(如果有)将被添加到结果集中。我们还设置了初始鼠标位置并裁剪 AdornerLayer 对象,以便套索保持在当前 SelectionArea 的边界内。

protected override void OnMouseMove(MouseEventArgs e)
{
   base.OnMouseMove(e);

   if (!isMouseDown)
   {
      return;
   }

   CurrentPosition = Mouse.GetPosition(this);
   e.Handled = true;
}

当鼠标移动时,我们检查是否已启动有效的套索选择。如果是,则更新 CurrentPosition。如果您的套索模板或几何形状依赖于 CurrentPosition,adorner 也会随之更新。这是在相应属性的回调处理程序中完成的,如前所述。

命中测试

选择逻辑在鼠标释放时执行。

protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
   base.OnMouseLeftButtonUp(e);
   Mouse.Capture(null);

   if (clickedItem != null)
   {
      UnselectAllExceptThisItem(clickedItem);
   }

   if (!isMouseDown)
   {
      return;
   }

   SelectionAreaItem item = null;

   if (LassoGeometry != null)
   {
      var capturedItems = new List<SelectionAreaItem>();

      VisualTreeHelper.HitTest(this,
         a =>
         {
            item = a as SelectionAreaItem;

            if (item != null && item.ParentSelectionArea == this)
            {
               return HitTestFilterBehavior.ContinueSkipChildren;
            }

            return HitTestFilterBehavior.ContinueSkipSelf;
         },
         a =>
         {
            switch (((GeometryHitTestResult)a).IntersectionDetail)
            {
               case IntersectionDetail.FullyInside:
               case IntersectionDetail.Intersects:
                  capturedItems.Add(a.VisualHit as SelectionAreaItem);
                  break;
            }

            return HitTestResultBehavior.Continue;
         },
         new GeometryHitTestParameters(LassoGeometry));

      Select(capturedItems);
   }

   if (SelectedItems.Count == 0)
   {
      item = ItemsControl.ContainerFromElement(null, this) as SelectionAreaItem;

      if (item != null)
      {
         var area = item.ParentSelectionArea;

         if (area != null)
         {
            area.NotifyItemClicked(item);
         }
      }
   }

   StartPosition = new Point(double.NegativeInfinity, double.NegativeInfinity);
   CurrentPosition = StartPosition;

   isMouseDown = false;
   adornerLayer.Remove(adorner);
}

如果我们点击了一个已选项目,且未按住 Ctrl 键,则在鼠标释放时,我们会取消选择除点击项目之外的所有其他已选项目。否则,如果已启动有效的套索选择,我们将利用 VisualTreeHelper.HitTest 方法和 LassoGeometry 来命中测试项目。如果在命中测试后未选择任何项目(且之前没有项目被选中),我们会检查 SelectionArea 是否不属于 SelectionAreaItemVisualTree(以防嵌套区域)。如果是,我们只需对该项目执行常规的直接点击选择。

命中测试值得稍加注意。如果您曾经调试过针对任意视觉树的命中测试方法,那么在过滤器回调和结果回调之间来回跳转会非常令人困惑,除非您理解一个非常重要的规则:如果您希望您的控件与 VisualTreeHelper.HitTest 良好配合,**务必**重写 HitTestCore 方法。如果您不这样做,您的控件将永远无法到达结果回调。这就是 SelectionAreaItem 的实现方式。

protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
   if (VisualTreeHelper.GetDescendantBounds(this).Contains(hitTestParameters.HitPoint))
   {
      return new PointHitTestResult(this, hitTestParameters.HitPoint);
   }

   return null;
}

protected override GeometryHitTestResult HitTestCore
		(GeometryHitTestParameters hitTestParameters)
{
   var geometry = new RectangleGeometry(VisualTreeHelper.GetDescendantBounds(this));
   return new GeometryHitTestResult
	(this, geometry.FillContainsWithDetail(hitTestParameters.HitGeometry));
}

另外,请确保您理解 Geometry 及其 Bounds 之间的区别。这两者在几何上等价的情况仅限于 RectangleGeometry。几何形状的边界是一个 Rect 对象,其大小足以完全包含该几何形状,但该几何形状可能具有不规则的形状。在过滤器回调中,您会获得所有位于几何形状边界内的视觉元素。此时,您可以通过修剪视觉树来过滤掉视觉元素,以加快命中测试速度。在 SelectionArea 中,我们只保留当前区域的项目容器,而不深入探究——但这些并不是将被选中的项目。再次强调,它们只是位于几何形状边界内,但这并不一定意味着它们被几何形状本身相交或包含。此测试在结果回调中完成,这就是为什么重写 HitTestCore 方法至关重要。

示例

以下是一些您可以实现的功能及其实现方式的快照。

Canvas 作为 ItemsPanel 上进行椭圆选择。
Selection using an ellipse as lasso and an EllipseGeometry for hit testing

Selection using an ellipse as lasso and an EllipseGeometry for hit testing

<c:SelectionArea LassoArrangeType="LassoBounds">
...
   <c:SelectionArea.LassoTemplate>
     <ControlTemplate>
       <Ellipse
         Stroke="Gray"
         StrokeThickness="1"
         StrokeDashedArray="{Binding Source={x:Static DashStyles.Dash},
				Path=Dashes, Mode=OneTime}" />
     </ControlTemplate>
   </c:SelectionArea.LassoTemplate>

   <c:SelectionArea.LassoGeometry>
     <MultiBinding Converter="{c:EllipseGeometryConverter}">
       <Binding RelativeSource="{RelativeSource Self}" Path="StartPosition" />
       <Binding RelativeSource="{RelativeSource Self}" Path="CurrentPosition" />
     </MultiBinding>
   </c:SelectionArea.LassoGeometry>
</c:SelectionArea>

LassoTemplate 只是一个带有虚线的灰色 Ellipse。对于 LassoGeometry,我们使用了一个 IMultiValueConverter,它根据 SelectionArea 暴露的两个 Point 返回一个 EllipseGeometry

public sealed class EllipseGeometryConverter : MarkupExtension, IMultiValueConverter
{
   public override object ProvideValue(IServiceProvider serviceProvider)
   {
      return this;
   }

   public object Convert(object[] values, Type targetType,
		object parameter, CultureInfo culture)
   {
      return new EllipseGeometry(new Rect((Point)values[0], (Point)values[1]));
   }

   public object[] ConvertBack(object value, Type[] targetTypes,
		object parameter, CultureInfo culture)
   {
      throw new NotImplementedException();
   }
}

就是这样。您可以以任何您想要的方式定义选择逻辑。用于矩形选择的代码是相同的;只需将 Ellipse 替换为 Rectangle,将 EllipseGeometry 替换为 RectangleGeometry

这些示例使用了数据绑定的 SelectionArea,具有两种类型的数据项:类型 1 以简单的 TextBlock 作为 DataTemplate,类型 2 以 SelectionArea 作为 DataTemplate。类型 2 可以有类型 1 或类型 2 的子项,依此类推,因此我们可以有嵌套的 SelectionArea。当项目被选中时,其边框颜色会变为红色(在实际应用中,您可能想做一些更有趣的事情)。

您不必一定要为套索提供 ControlTemplate 才能选择项目。在下面的示例中,模板已被移除。套索几何形状是一个由两个 LineGeometries 组成的 GeometryGroup——一条垂直线和一条水平线,它们的交点是 CurrentPosition。只需点击空白处,任何被其中一条线相交的项目都会被选中。

单点选择,套索没有 ControlTemplate,在 StackPanel 作为 ItemsPanel 上进行。
Selection using a single click and a GeometryGroup for hit testing

Selection using a single click and a GeometryGroup for hit testing

在我们之前看到的椭圆选择中,椭圆被内嵌在由 StartPositionCurrentPosition 定义的矩形内。它的中心、长半径和短半径随着鼠标的移动而不断变化。我们可能希望它的中心固定,并且半径随着我们离中心越来越远或越来越近而增大或减小。下面是这种情况的代码,其中中心与 StartPosition 重合,并且两个半径都等于 CurrentPositionStartPosition 之间的距离(当然,您可以设置它们不同,例如长半径是短半径的两倍,或者任何适合您需求的值)。

public sealed class EllipseGeometryConverter : MarkupExtension, IMultiValueConverter
{
   public override object ProvideValue(IServiceProvider serviceProvider)
   {
      return this;
   }

   public object Convert(object[] values, Type targetType,
		object parameter, CultureInfo culture)
   {
      var p1 = (Point)values[0];
      var p2 = (Point)values[1];
      var offset = p2 - p1;

      return new EllipseGeometry(p1, offset.Length, offset.Length);
   }

   public object[] ConvertBack(object value, Type[] targetTypes,
		object parameter, CultureInfo culture)
   {
      throw new NotImplementedException();
   }
}

我们还可以做得更好。凭借一点想象力,我们可以手工制作一个自由形式的选择工具。

WrapPanel 作为 ItemsPanel 上进行自由形式选择。
Selection using a single click and a GeometryGroup for hit testing

Selection using a single click and a GeometryGroup for hit testing

我们模拟了 InkCanvas 的行为(尽管效果不如其好)。为了实现这一点,我们构建了两个转换器:一个用于模板,一个用于几何形状。绑定将设置在 CurrentPosition 上。

public sealed class PolylineConverter : MarkupExtension, IValueConverter
{
   private List<point> points = new List<Point>();

   public override object ProvideValue(System.IServiceProvider serviceProvider)
   {
      return this;
   }

   public object Convert(object value, System.Type targetType,
	object parameter, System.Globalization.CultureInfo culture)
   {
      var p = (Point)value;

      if (p.X != double.NegativeInfinity && p.Y != double.NegativeInfinity)
      {
         points.Add(p);
      }
      else
      {
         points.Clear();
      }

      var template = new ControlTemplate(typeof(Control));
      template.VisualTree = new FrameworkElementFactory(typeof(Polyline));
      template.VisualTree.SetValue(Polyline.StrokeProperty,
			new SolidColorBrush(Colors.Gray));
      template.VisualTree.SetValue(Polyline.StrokeThicknessProperty, 1.0);
      template.VisualTree.SetValue(Polyline.StrokeDashArrayProperty,
			DashStyles.Dash.Dashes);
      template.VisualTree.SetValue(Polyline.PointsProperty, new PointCollection(points));

      return template;
   }

   public object ConvertBack(object value, System.Type targetType,
	object parameter, System.Globalization.CultureInfo culture)
   {
      throw new System.NotImplementedException();
   }
}

我们需要存储鼠标移动时遇到的点列表。利用这个列表,我们构建一个 Polyline 并将其设置为模板的 VisualTree。用于命中测试的几何形状是 PathGeometry,它随着鼠标的移动添加小段。

public sealed class PolylineGeometryConverter : MarkupExtension, IValueConverter
{
   private PathGeometry pathGeometry;
   private PathFigure pathFigure;

   public PolylineGeometryConverter()
   {
      pathGeometry = new PathGeometry();
      pathFigure = new PathFigure();
      pathFigure.IsClosed = true;
      pathGeometry.Figures.Add(pathFigure);
   }

   public override object ProvideValue(System.IServiceProvider serviceProvider)
   {
      return this;
   }

   public object Convert(object value, System.Type targetType,
	object parameter, System.Globalization.CultureInfo culture)
   {
      var p = (Point)value;

      if (p.X != double.NegativeInfinity && p.Y != double.NegativeInfinity)
      {
         var lineSegment = new LineSegment(p, false);

         if (pathFigure.Segments.Count == 0)
         {
            pathFigure.StartPoint = p;
         }

         pathFigure.Segments.Add(lineSegment);
      }
      else
      {
         pathFigure.Segments.Clear();
      }

      return pathGeometry;
   }

   public object ConvertBack(object value, System.Type targetType,
	object parameter, System.Globalization.CultureInfo culture)
   {
      throw new System.NotImplementedException();
   }
}

在上面的转换器中,我们使用了一些“内部”知识:最初,以及每次鼠标释放后,StartPositionCurrentPosition 的坐标都被设置为 double.NegativeInfinity。这样我们就知道何时选择已结束。

限制

  1. 尽管您可以将 SelectionArea 与任何类型的 ItemsPanel 和任何类型的项目容器 DataTemplate 一起使用,但如果容器之间没有间隙,您将无法进行套索选择。您需要一个“空白区域”才能进行此操作。如果此条件不满足,并且您也不想利用作用域选择,那么您可以不使用它。请明智地选择您的控件。
  2. LassoTemplate 更改时,adorner 会丢弃其当前模板并拾取新的模板。如果您有一个转换器,在 CurrentPosition 每次更改时都返回一个新模板,就像在具有两条垂直线或自由形式选择的示例中一样,这种切换会非常频繁。在这种情况下,拥有一个复杂的模板会对性能产生严重影响。
  3. 整个套索排列机制感觉不是非常稳固。正在寻找更好的替代方案。

下一篇

ItemsControl 完全围绕数据。而数据必须交换。在下一篇文章中,我们将讨论数据绑定 ItemsControl 之间的拖放操作。

反馈

对于任何错误报告、建议或进一步改进的想法,请发表评论,以便相应地修改源代码。

历史

  • 2009 年 12 月 3 日 - 修复:clickedItem 在鼠标按下时(在 NotifyItemClickedOnMouseLeftButtonDown 方法中)被置为 null,而不是在鼠标释放时。这是必需的,因为,万一鼠标释放的相应隧道事件被第三方(如附加行为)处理了,您将拥有一个悬空的不存在的已点击项,那么下一次直接点击选择就会损坏并出现故障。
  • 2009 年 11 月 27 日 - 创建了文章
© . All rights reserved.