由于在 xaml 体系中,控件没有传统 WebForm 中的 Left、Top、Right、Bottom 这些属性,取而代之的是按比例(像 Grid)等等的响应布局。但是,传统的这些设置 Left、Top 的硬编码的需求仍然存在,所以,在所有的 xaml 体系中,均存在一个代替的控件——Canvas。本文基于 Canvas 来实现控件的拖拉效果。
在整个控件拖拉的过程当中,可以分解为 3 个部分,第一个部分是输入设备点击控件,第二个部分是保持按下的状态下移动输入设备,第三个部分是释放输入设备。那么,就必须对这 3 个过程做出相应的逻辑处理。
那么,有一个重要的问题来了,如何标识控件是否处于拖拉状态。C# 不像 javascript 那样,能够随便修改对象,添加属性。但是,这难不倒微软的工程师,在 xaml 体系中,有附加属性这样一个东西。
1 public static class DragHelper 2 { 3 public static readonly DependencyProperty IsPraggingProperty = DependencyProperty.RegisterAttached("IsDragging", typeof(bool), typeof(DragHelper), new PropertyMetadata(false)); 4 }
这里使用 IsDragging 来标识控件是否处于拖放过程中。
然后接下来我们需要一个初始化的方法来使控件可以拖放。
public static class DragHelper { public static bool Dragable(this UIElement control) { // TODO } }
这里使用扩展方法,使调用方简洁一些。该方法返回一个布尔值,指示是否操作成功。对于控件的父对象为 Canvas 的,我们返回 true,否则返回 false。
public static class DragHelper { public static bool Dragable(this UIElement control) { if(control==null) { throw new ArgumentNullException("control"); } if(VisualTreeHelper.GetParent(control) is Canvas) { // TODO return true; } else { return false; } } }
由于控件没有所谓的 Parent 属性,因此我们需要使用可视树来获取父控件。
接下来将一开始的 3 个过程映射到相应的对象事件。
1、输入设备点击控件:UIElement.PointertPressed
2、输入设备移动:Window.Current.CoreWindow.PointerMoved
3、释放输入设备:Window.Current.CoreWindow.PointerReleased
public static bool Dragable(this UIElement control) { // null 判断,参考上面 if(VisualTreeHelper.GetParent(control) is Canvas) { control.PointerPressed+=(sender,e)=> { // 设置控件进入拖放状态。 control.SetValue(IsDraggingProperty, true); // TODO }; var coreWindow = Window.Current.CoreWindow; coreWindow.PointerMoved+=(sender,args)=> { if((bool)control.GetValue(IsDraggingProperty)) { // TODO } }; coreWindow.PointerReleased+=(sender,args)=> { // TODO }; return true; } else { return false; } }
接下来,在移动的过程中,我们需要不断设置控件的 Left、Top 这两个 Canvas 的附加属性来达到拖放的效果。可以通过
args.CurrentPoint.Position
来获得输入设备当前的位置。那么每一次设置的位置就等于初始位置加上当次位置。
总体代码:
1 public static class DragHelper 2 { 3 public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.RegisterAttached( 4 "IsDragging", typeof(bool), typeof(DragHelper), new PropertyMetadata(false)); 5 6 public static readonly DependencyProperty StartLeftProperty = DependencyProperty.RegisterAttached("StartLeft", 7 typeof(double), typeof(DragHelper), new PropertyMetadata(0.0d)); 8 9 public static readonly DependencyProperty StartTopProperty = DependencyProperty.RegisterAttached("StartTop", 10 typeof(double), typeof(DragHelper), new PropertyMetadata(0.0d)); 11 12 public static readonly DependencyProperty StartPositionProperty = 13 DependencyProperty.RegisterAttached("StartPosition", typeof(Point), typeof(DragHelper), 14 new PropertyMetadata(default(Point))); 15 16 public static bool Dragable(this UIElement control) 17 { 18 if (control == null) 19 { 20 throw new ArgumentNullException("control"); 21 } 22 if (VisualTreeHelper.GetParent(control) is Canvas) 23 { 24 control.PointerPressed += (sender, e) => 25 { 26 control.SetValue(IsDraggingProperty, true); 27 control.SetValue(StartLeftProperty, Canvas.GetLeft(control)); 28 control.SetValue(StartTopProperty, Canvas.GetTop(control)); 29 control.SetValue(StartPositionProperty, e.GetCurrentPoint(null).Position); 30 }; 31 var coreWindow = Window.Current.CoreWindow; 32 coreWindow.PointerMoved += (sender, args) => 33 { 34 if ((bool)control.GetValue(IsDraggingProperty)) 35 { 36 var currentPosition = args.CurrentPoint.Position; 37 var startPosition = (Point)control.GetValue(StartPositionProperty); 38 var deltaX = currentPosition.X - startPosition.X; 39 var deltaY = currentPosition.Y - startPosition.Y; 40 var startLeft = (double)control.GetValue(StartLeftProperty); 41 var startTop = (double)control.GetValue(StartTopProperty); 42 Canvas.SetLeft(control, startLeft + deltaX); 43 Canvas.SetTop(control, startTop + deltaY); 44 } 45 }; 46 coreWindow.PointerReleased += (sender, args) => control.SetValue(IsDraggingProperty, false); 47 48 return true; 49 } 50 else 51 { 52 return false; 53 } 54 } 55 } 56 }
在第一步保存控件相关信息到附加属性当中便于移动状态使用。
效果:
应用:
例如开发一个 ListView 返回顶部的小插件。
由于 Button 会吞掉 PointerPressed 这个事件,因此这里使用了 Border 来模拟。
Border 相关的 xaml 代码:
1 <Canvas> 2 <Border x:Name="btn" 3 Canvas.Left="300" 4 Canvas.Top="400" 5 Width="50" 6 Height="50" 7 CornerRadius="50" 8 BorderBrush="Gray" 9 BorderThickness="3" 10 Background="Red" 11 Tapped="Btn_OnTapped"> 12 <TextBlock Text="顶" 13 HorizontalAlignment="Center" 14 VerticalAlignment="Center" 15 FontSize="25" 16 Foreground="Gold" /> 17 </Border> 18 </Canvas>
ListView 滚回到顶部的代码。
private void Btn_OnTapped(object sender, TappedRoutedEventArgs e) { // Lvw 为 ListView 控件。 var item = Lvw.Items.FirstOrDefault(); if (item != null) { Lvw.ScrollIntoView(item); } }