为ListView添加矩形选择功能

来源:互联网 发布:德国拜发 软件 编辑:程序博客网 时间:2024/04/30 14:26

 

ListView矩形选择功能预览

 

ListView矩形选择预览


也许微软会在以后的版本中加入对矩形选择列表项的支持, 但是现在还没有,所以有必要将其实现并记录下来。

对现有ListView控件添加矩形选择功能的最佳实现应该是使用关联属性,这样可以将使用该功能的复杂性降到最低,具体使用方式应该按照如下所示:

<ListView local:ListBoxExtention.IsRectSelectionEnabled="True">

            <ListView.ItemsPanel>

                <ItemsPanelTemplate>

                    <UniformGrid/>

                </ItemsPanelTemplate>

            </ListView.ItemsPanel>

            <ListViewItem Content="1"/>

            <ListViewItem Content="2"/>

            <ListViewItem Content="3"/>

            <ListViewItem Content="4"/>

            <ListViewItem Content="5"/>

            <ListViewItem Content="6"/>

            <ListViewItem Content="7"/>

            <ListViewItem Content="8"/>           

        </ListView>

Attached Property被定义在ListBoxExtention类上,为什么类名为ListBox*呢?因为我发现ListView其实使用ListBox继承,ListViewItemListBoxItem继承,所以对ListBox实现的矩形选择功能,对ListView同样可用。

ListBoxExtention类的定义如下:

public static class ListBoxExtention

  {

    public static bool GetIsRectSelectionEnabled(DependencyObject obj)

    {

      return (bool)obj.GetValue(IsRectSelectionEnabledProperty);

    }

 

    public static void SetIsRectSelectionEnabled(DependencyObject obj, bool value)

    {

      obj.SetValue(IsRectSelectionEnabledProperty, value);

    }

 

    public static readonly DependencyProperty IsRectSelectionEnabledProperty = DependencyProperty.RegisterAttached(

      "IsRectSelectionEnabled", typeof(bool), typeof(ListBoxRectSelection), new FrameworkPropertyMetadata(OnIsRectSelectionEnabledChanged));

 

    private static void OnIsRectSelectionEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)

    {

      if(!(sender is ListBox))

      {

        throw new ArgumentException("sender", "Target should be a ListBox and its sub class instance."); ;

      }

 

      bool isEnabled = (bool)e.NewValue;

 

      if (isEnabled)

      {

        ListBoxRectSelection rectSelection = ListBoxExtention.GetRectSelection(sender) ;

        if (rectSelection == null)

        {

          rectSelection = new ListBoxRectSelection(sender as ListView);

          ListBoxExtention.SetRectSelection(sender, rectSelection);

        }

 

        rectSelection.IsEnabled = isEnabled;       

      }

    }

 

    private static ListBoxRectSelection GetRectSelection(DependencyObject obj)

    {

      return (ListBoxRectSelection)obj.GetValue(RectSelectionProperty);

    }

 

    private static void SetRectSelection(DependencyObject obj, ListBoxRectSelection value)

    {

      obj.SetValue(RectSelectionProperty, value);

    }

 

    private static readonly DependencyProperty RectSelectionProperty =

        DependencyProperty.RegisterAttached("RectSelection", typeof(ListBoxRectSelection), typeof(ListBoxExtention));

  }

 

可以看到,关联属性IsRectSelectionEnabled 主要通过设置ListBoxRectSelection IsEnabled属性用来启用或者禁用ListBoxRectSelection 类定义的功能, ListBoxRectSelection 类的对象被存储在一个私有关联属性里面。定义如下:

public class ListBoxRectSelection

  {

    private ListBox _TargetList;

    private RectangleSelectionGesture _RectangleSelectionGesture;

   

    public bool IsEnabled

    {

      get { return _RectangleSelectionGesture.IsEnabled; }

      set { _RectangleSelectionGesture.IsEnabled = value; }

    }

 

    public ListBoxRectSelection(ListBox target)

    {

      _TargetList = target;

      _RectangleSelectionGesture = new RectangleSelectionGesture(target);

      _RectangleSelectionGesture.RectangleSelectionChanging += RectangleSelectionChanging;

      _RectangleSelectionGesture.RectangleSelectionStarted += RectangleSelectionStarted;

    }

 

    private void RectangleSelectionStarted(object sender, RecangleSelectionEventArgs e)

    {

      _TargetList.Focus();

    }   

 

    private void RectangleSelectionChanging(object sender, RecangleSelectionEventArgs e)

    {

      _TargetList.SelectedItems.Clear();

      VisualTreeHelper.HitTest(_TargetList, ItemHitTestFilter, ItemHitTestCallback, new GeometryHitTestParameters(new RectangleGeometry(e.SelectionRect)));

    }

 

    private HitTestFilterBehavior ItemHitTestFilter(DependencyObject visual)

    {

      if (visual is ListBoxItem)

      {

        ((ListBoxItem)visual).IsSelected = true;

        return HitTestFilterBehavior.ContinueSkipChildren;

      }

 

      return HitTestFilterBehavior.Continue;

    }

 

    private HitTestResultBehavior ItemHitTestCallback(HitTestResult result)

    {

      return HitTestResultBehavior.Continue;

    }

  }

 

ListBoxRectSelection通过使用RectangleSelectionGesture提供的矩形选择功能对ListBox进行点击测试,将命中的ListBoxItemIsSelected属性设置为True,特别要注意的是要使用HitTestFilterCallback参数来获取命中测试的目标,而不是使用HitTestResultCallback参数。看上去HitTestResultCallback好像能满足我们的需求,但是实际结果并不能如我所愿,MSDN也并没有对HitTestResultCallback进行详尽的注释。VisualTreeHelper HitTest方法提供了多种命中测试方法,可以使用Point进行命中测试,也可以进行使用Geometry进行命中测试,对我们来说,使用Geometry参数进行的命中测试最符合我的需求。

在整个选择过程中,最重要的就是RectangleSelectionGesture 类了。该类定义如下:

public class RectangleSelectionGesture

  {

    #region Static Members

 

    public static ComponentResourceKey SelectionVisualStyleKey { get; private set; }

 

    private static readonly Style _DefaultSelectionVisualStyle;

 

    static RectangleSelectionGesture()

    {

      _DefaultSelectionVisualStyle = new Style();

      _DefaultSelectionVisualStyle.Setters.Add(new Setter(Shape.StrokeProperty, Brushes.Blue));

      _DefaultSelectionVisualStyle.Setters.Add(new Setter(Shape.FillProperty, new SolidColorBrush(Color.FromArgb(66, 0, 0, 255))));

      SelectionVisualStyleKey = new ComponentResourceKey(typeof(RectangleSelectionGesture), "SelectionVisualStyle");

    }

 

    #endregion

 

    private FrameworkElement _TargetElement;

    private RectangleGeometry _SelectionRectGeometry;

    private Path _SelectionVisual;

    private bool _Watching;

    private Point _StartPoint;

    private bool _IsEnabled;

    private CustomAdorner _CustomAdorner;

    private AdornerLayer _TargetAdornerLayer;

 

    /// <summary>

    /// Deffer to get AdornerLayer of _TargetElement.

    /// </summary>

    private AdornerLayer TargetAdornerLayer

    {

      get

      {

        if (_TargetAdornerLayer == null)

        {

          _TargetAdornerLayer = AdornerLayer.GetAdornerLayer(_TargetElement);

        }

 

        return _TargetAdornerLayer;

      }

    }   

 

    #region Rectangle Selection Events

  

    public event EventHandler<RecangleSelectionEventArgs> RectangleSelectionStarted;

    public event EventHandler<RecangleSelectionEventArgs> RectangleSelectionCompleted;

    public event EventHandler<RecangleSelectionEventArgs> RectangleSelectionChanging;

 

    protected void OnRectangleSelectionStarted(Rect rect)

    {

      if (RectangleSelectionStarted != null)

      {

        RectangleSelectionStarted(this, new RecangleSelectionEventArgs(rect));

      }

    }

 

    protected void OnRectangleSelectionCompleted(Rect rect)

    {

      if (RectangleSelectionCompleted != null)

      {

        RectangleSelectionCompleted(this, new RecangleSelectionEventArgs(rect));

      }

    }

 

    protected void OnRectangleSelectionChanging(Rect rect)

    {

      if (RectangleSelectionChanging != null)

      {

        RectangleSelectionChanging(this, new RecangleSelectionEventArgs(rect));

      }

    }

 

    #endregion

 

    #region Constructor

 

    public RectangleSelectionGesture(FrameworkElement targetElement, Style selectionVisualStyle)

    {

      _TargetElement = targetElement;

      IntializeSelectionVisual(selectionVisualStyle);

      _CustomAdorner = new CustomAdorner(_TargetElement, _SelectionVisual);

    }

 

    public RectangleSelectionGesture(FrameworkElement targetElement)

      : this(targetElement, null)

    { }

 

    private void IntializeSelectionVisual(Style style)

    {

      _SelectionVisual = new Path();

 

      if (style != null)

      {

        _SelectionVisual.Style = style;

      }

      else

      {

        style = _TargetElement.TryFindResource(SelectionVisualStyleKey) as Style;

 

        if (style != null)

        {

          _SelectionVisual.Style = style;

        }

        else

        {

          _SelectionVisual.Style = _DefaultSelectionVisualStyle;

        }

      }

 

      _SelectionRectGeometry = new RectangleGeometry(new Rect(0, 0, 0, 0));

      _SelectionVisual.Data = _SelectionRectGeometry;

    }

 

    #endregion

  

    private void TargetElementLostMouseCapture(object sender, MouseEventArgs e)

    {

      _Watching = false;

 

      Adorner[] adorners = TargetAdornerLayer.GetAdorners(_TargetElement);

      if (Array.IndexOf(adorners, _CustomAdorner) != -1)

      {

        TargetAdornerLayer.Remove(_CustomAdorner);

      }     

    }

 

    private void TargetElementMouseLeftButtonDown(object sender, MouseButtonEventArgs e)

    {

      _Watching = true;

      _StartPoint = e.GetPosition(_TargetElement);

    }

 

    private void TargetElementMouseMove(object sender, MouseEventArgs e)

    {

      if (_Watching)

      {

        Point curPos = e.GetPosition(_TargetElement);

        _SelectionRectGeometry.Rect = new Rect(_StartPoint, curPos);

 

        if (_TargetElement.IsMouseCaptured)

        {

          OnRectangleSelectionChanging(_SelectionRectGeometry.Rect);

        }

        else if (_SelectionRectGeometry.Rect.Width > SystemParameters.MinimumHorizontalDragDistance

          || _SelectionRectGeometry.Rect.Height > SystemParameters.MinimumVerticalDragDistance)

        {

          if (_TargetElement.CaptureMouse())

          {

            TargetAdornerLayer.Add(_CustomAdorner);

            OnRectangleSelectionStarted(_SelectionRectGeometry.Rect);

          }

        }

      }

    }

 

    private void TargetElementMouseLeftButtonUp(object sender, MouseButtonEventArgs e)

    {

      _Watching = false;     

 

      if (_TargetElement.IsMouseCaptured)

      {

        _TargetElement.ReleaseMouseCapture();

        Point curPos = e.GetPosition(_TargetElement);

        Rect selectionRect = new Rect(_StartPoint, curPos);

        OnRectangleSelectionCompleted(selectionRect);

        TargetAdornerLayer.Remove(_CustomAdorner);

      }

    }

 

    public bool IsEnabled

    {

      get

      {

        return _IsEnabled;

      }

      set

      {

        if (value != _IsEnabled)

        {

          _IsEnabled = value;

 

          if (value)

          {

            _TargetElement.MouseLeftButtonDown += TargetElementMouseLeftButtonDown;

            _TargetElement.MouseMove += TargetElementMouseMove;

            _TargetElement.MouseLeftButtonUp += TargetElementMouseLeftButtonUp;

            _TargetElement.LostMouseCapture += TargetElementLostMouseCapture;

          }

          else

          {

            _TargetElement.MouseLeftButtonDown -= TargetElementMouseLeftButtonDown;

            _TargetElement.MouseMove -= TargetElementMouseMove;

            _TargetElement.MouseLeftButtonUp -= TargetElementMouseLeftButtonUp;

            _TargetElement.LostMouseCapture -= TargetElementLostMouseCapture;

          }

        }

      }

    }

  }

 

该类的主要功能是使用CustomAdorner类为目标元素表面添加一个矩形元素,该元素能描述用户拖拽产生的选择区域 。该类提供了三个事件,如下:

 

public event EventHandler<RecangleSelectionEventArgs> RectangleSelectionStarted;

public event EventHandler<RecangleSelectionEventArgs> RectangleSelectionCompleted;

public event EventHandler<RecangleSelectionEventArgs> RectangleSelectionChanging;

 

三个事件用来通知矩形选择Gesture的开始,变化和结束。一般情况下,接听RectangleSelectionChanging 就能够满足实现矩形选择列表项的需求。类的客户可以从事件参数中得到当前选择矩形的信息,并做相应操作。矩形信息通过RecangleSelectionEventArgs 类传递,该类的定义如下:

public class RecangleSelectionEventArgs : EventArgs

  {

    public Rect SelectionRect { get; private set; }

 

    public RecangleSelectionEventArgs(Rect selectionRect)

    {

      SelectionRect = selectionRect;

    }

  }

 

在用户在界面上进行矩形选择的时候, 为了使用户可以定制选择矩形的样式, 该类的构造函数接受一个Style参数,该参数实际上必须是针对Path类型的一个Style,用来样式化选择矩形(用Path表示)。如果传递null值,那么该类将会从目标界面元素中查找KeySelectionVisualStyleKey

 的资源并将其转化为Style对象,如果查找失败,那么将使用默认样式,默认样式有静态字段_DefaultSelectionVisualStyle 定义。

 

使用SelectionVisualStyleKey定义样式如下:

<Style TargetType="{x:Type Path}" x:Key="{x:Static local:RectangleSelectionGesture.SelectionVisualStyleKey}">

                <Setter Property="Stroke" Value="Red"/>

                <Setter Property="Fill" Value="#3300FF00"/>

            </Style>

 

该样式必须定义在目标界面元素(ListView或者ListBox)或者其祖先的资源当中。

 

此外该类的IsEnabled属性用来订阅或取消订阅UI对象的鼠标事件。

 

由于RectangleSelectionGesture类要在VisualTree中添加一个矩形元素,所以最好的办法就是用AdornerAdornerLayer中添加该矩形元素,这样就可以使添加的矩形元素不会影响目标UI对象的VisualTree,更不会依赖于它。关于Adorner的使用,在 Msdn中有比较详细的描述。本示例中使用了CustomAdorner作为矩形选择元素的宿主。该类允许将任何元素添加到AdornerLayer中,装饰元素将会被ContentPresenter类呈现到AdornerLayer中。该类的定义如下:

public class CustomAdorner : Adorner

  {

    public CustomAdorner(UIElement adornedElement, UIElement adornerElement)

      : base(adornedElement)

    {

      _VisualChildren = new VisualCollection(this);

      _Presenter = new ContentPresenter();

      _Presenter.Content = adornerElement;

      _VisualChildren.Add(_Presenter);

    }

 

    private VisualCollection _VisualChildren;

    private ContentPresenter _Presenter;

 

    protected override Visual GetVisualChild(int index)

    {

      return _VisualChildren[index];

    }

 

    protected override int VisualChildrenCount

    {

      get

      {

        return _VisualChildren.Count;

      }

    }

 

    protected override Size ArrangeOverride(Size finalSize)

    {

      _Presenter.Arrange(new Rect(finalSize));

      return _Presenter.RenderSize;

    }

  }

 

adornedElement需要传入被装饰的元素,adornerElement需要传入装饰元素,后者会使用前者的左上角作为自己的(00)坐标。使用VisualCollection _Presenter加入到集合中特别重要,VisualCollection会将集合内的元素自动添加到VisualTree中,这样才能保证UI元素正常的显示。

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 微信视频已过期或已清理怎么办 视频已过期或已被清理怎么办 小孩作业不会老婆天天吵骂打怎么办 苹果手机在太阳下屏幕变暗怎么办 斗鱼的鱼丸竞猜主播结算了怎么办 附近有个小姐姐想加她好友怎么办 孩子出现听别人说话语速很快怎么办 苹果手机上的邮件删了怎么办 手机qq邮箱独立密码忘记了怎么办 哺乳期吃了人参回奶了怎么办? 扣扣邮箱里的邮件过期了怎么办 一体机的管理员账号被删除了怎么办 手机里的邮箱重要吗删除了怎么办 华为荣耀10账号邮箱忘记了怎么办 大陆微信号在台湾登录不上怎么办 威纶触摸屏被禁止到反编译了怎么办 微信公众号邮箱被占用怎么办 京东绑定的手机号不用了怎么办 绑定微信的手机号不用了怎么办 绑定支付宝的手机号不用了怎么办 百度账号手机号换了密码忘了怎么办 换手机好了华为账号密码忘了怎么办 苹果手机忘了id账号和密码怎么办 金立手机账号密码忘了怎么办 乐视手机账号密码忘了怎么办 企业邮箱发出去邮件撤不回来怎么办 餐厅加热保温设备零线带电怎么办 小米手机不小心把照片删了怎么办 华为手机不小心把照片删了怎么办 网易邮箱被改成别人的姓名怎么办 苹果手机忘记id密码和邮箱怎么办 163邮箱下载的附件没有了怎么办 小米自带浏览器打开网页太慢怎么办 再歪一点授权码忘记了怎么办 注册支付宝的手机号不用了怎么办 手机号被别人注册了支付宝怎么办 支付宝账号密码都忘了怎么办 申请微信公众号邮箱被占用怎么办 邮箱注册微博需要手机验证怎么办 苹果手机的ad码忘记了怎么办 苹果手机酷狗音乐没有声音怎么办