利用 Silverlight 实现类似 iGoogle 的浮动层拖拽效果

觉得写得不错,刚好自己也遇到类似的问题,所以借用来参考。 

Demo: 

布局容器选择:

Silverlight提供了Canvas、Grid、StackPanel三种布局容器,基本能满足各种需要。个人习惯是用Grid做框架布局,具体内容利用StackPanel自组织,而Canvas则在某些特定场合,比如打字游戏的界面中使用。

一方面在自适应浏览器大小的角度上,Grid提供了两方面的自由度,StackPanel提供了一个方向的自由度,而Canvas没有自由度,也就不能随这浏览器大小改变而自动适应大小;另一方面,Grid和StackPanel都可以不用直接指定坐标进行布局,而Canvas则需要指定确定的坐标,维护起来肯定麻烦;第三点,则是主要针对于可拖拽效果的,StackPanel是唯一可以通过添加、删除内部控件而自动调整内部其他控件位置的布局容器。

所以最后决定:用Grid做外部布局,每一列都设定ColumnDefinition,再在每一列添加一个StackPanel做为单列的容器:
  1. <uc:ContainerGrid.ColumnDefinitions>
  2.     <ColumnDefinition></ColumnDefinition>
  3.     <ColumnDefinition></ColumnDefinition>
  4.     <ColumnDefinition></ColumnDefinition>
  5. </uc:ContainerGrid.ColumnDefinitions>
  6. <uc:ContainerPanel Grid.Column="0" x:Name="LeftPanel">
  7. </uc:ContainerPanel>
  8. <uc:ContainerPanel Grid.Column="1" x:Name="CenterPanel">
  9. </uc:ContainerPanel>
  10. <uc:ContainerPanel Grid.Column="2" x:Name="RightPanel">
  11. </uc:ContainerPanel>
复制代码
这里的ContainerGrid和ContainerPanel分别继承自Grid和Panel,主要是添加了几个方便以后查询以及扩展的属性。

可拖拽控件:

可拖拽控件可以继承自任何控件,这里用的是UserControl。

public partial class DragableGrid : UserControl那么对于DragableGrid,最主要的事件是三个:MouseLeftButtonDown(左键按下),MouseLeftButtonUp(左键抬起),MouseMove(左键移动)。MouseLeftButtonDown标识拖拽开始,MouseLeftButtonUp说明拖拽结束,而MouseMove则是拖拽过程触发。此外,考虑到当鼠标移动到控件可移动范围之外的情况,还有MouseLeave事件,和MouseLeftButtonUp的意义一样。所以需要在构造函数中添加它们的处理逻辑:
  1. this.MouseLeftButtonDown += new MouseButtonEventHandler(DragableGrid_MouseLeftButtonDown);
  2. this.MouseLeftButtonUp += new MouseButtonEventHandler(DragableGrid_MouseLeftButtonUp);
  3. this.MouseMove += new MouseEventHandler(DragableGrid_MouseMove);
  4. this.MouseLeave += new MouseEventHandler(DragableGrid_MouseLeave);左键按下(拖拽开始):
  5. void DragableGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  6. {
  7.     //标示开始拖拽
  8.     IsDraging = true;
  9.     //鼠标的起始位置(控件内部)
  10.     _beginPoint = e.GetPosition(this);
  11.     //鼠标相对于Grid的位置
  12.     Point marginPoint = e.GetPosition(_parentGrid);
  13.     ContainerPanel parentPanel = this.Parent as ContainerPanel;
  14.     shadowGrid = new ShadowGrid(this, parentPanel);
  15.     
  16.     //设置初始Margin。
  17.     Thickness beginMargin = new Thickness(marginPoint.X - _beginPoint.X, marginPoint.Y - _beginPoint.Y, _parentGrid.ActualWidth - this.ActualWidth - marginPoint.X + _beginPoint.X, _parentGrid.ActualHeight - this.ActualHeight - marginPoint.Y + _beginPoint.Y);
  18.     (this.Parent as Panel).Children.Remove(this);
  19.     if (this.Parent == null)
  20.     {
  21.         _parentGrid.Children.Add(this);
  22.     }
  23.     this.SetValue(Grid.ColumnSpanProperty, _parentGrid.PanelCount);
  24.     this.Margin = beginMargin;
  25. }
复制代码
首先设置IsDraging标识位,然后分别获取鼠标点击点相对于控件左上角以及相对于ContainerGrid左上角的位移,从而获得可拖拽控件相对于ContainerGrid的Margin值。注意鼠标相对控件左上角的位移在拖拽过程中是不变的,所以作为可拖拽控件的一个私有变量存储起来。然后将DragableGrid从父Panel中移除,添加到ContainerGrid中,使它能在整个Grid的范围内移动。

这里仿照iGoogle和其他类似的可拖拽框架的模式,当拖拽一个可拖拽控件时,会在可拖拽控件的原处位置添加一个背影控件(ShadowGrid),用来占位:
  1. public ShadowGrid(DragableGrid originGrid,ContainerPanel parentPanel)
  2. {
  3.     InitializeComponent();
  4.     this._orginGrid = originGrid;
  5.     //设置影子Grid和原始Grid的样式一致。
  6.     this.Width = _orginGrid.ActualWidth;
  7.     this.Height = _orginGrid.ActualHeight;
  8.     this.Text = _orginGrid.Text;
  9.     this.Margin = _orginGrid.Margin;
  10.     //设置影子Grid的父Panel并插入。
  11.     _vIndex = parentPanel.Children.IndexOf(_orginGrid);
  12.     _hIndex = parentPanel.Index;
  13.     parentPanel.Children.Insert(_vIndex + 1, this);
  14. }
复制代码
而且这里影子控件和可拖拽控件互相引用,方便以后的操作。

鼠标移动:

先判断IsDraging 标识位,之后根据鼠标的新位置设置控件的Margin:
  1. if (IsDraging)
  2. {
  3.     Point newPosition = e.GetPosition(_parentGrid);
  4.     this.Margin = new Thickness(newPosition.X - _beginPoint.X, newPosition.Y - _beginPoint.Y, _parentGrid.ActualWidth - this.ActualWidth - newPosition.X + _beginPoint.X, _parentGrid.ActualHeight - this.ActualHeight - newPosition.Y + _beginPoint.Y);
复制代码
这里利用了鼠标相对控件的位移,也就是_beginPoint是使用不变的。

接下来就是最麻烦的部分:根据新位置,判断控件的拖拽状况,这里有左右移动和上下移动两种情况。

首先是比较简单的左右移动:
  1. //向左移
  2. if (this.Margin.Left < _parentGrid.PanelWidth * (shadowGrid.HIndex - 0.5))
  3. {
  4.     if (shadowGrid.HIndex > 0)
  5.     {
  6.         ContainerPanel panel = this._parentGrid.ChildPanels[shadowGrid.HIndex - 1];
  7.         shadowGrid.MoveTo(panel);
  8.     }
  9. }
  10. //向右移
  11. else if (this.Margin.Left > _parentGrid.PanelWidth * (shadowGrid.HIndex + 0.5))
  12. {
  13.     if (shadowGrid.HIndex < _parentGrid.PanelCount - 1)
  14.     {
  15.         ContainerPanel panel = this._parentGrid.ChildPanels[shadowGrid.HIndex + 1];
  16.         shadowGrid.MoveTo(panel);
  17.     }
  18. }
复制代码
_parentGrid.PanelWidth代表每一列的宽度,而shadowGrid.HIndex则是shadowGrid所在列的序号,shadowGrid的MoveTo方法实现了将shadowGrid水平插入另一个Panel中:
  1. public void MoveTo(ContainerPanel panel)
  2. {
  3.     ParentPanel.Children.Remove(this);

  4.     if (panel.Children.Count > _vIndex)
  5.     {
  6.         panel.Children.Insert(_vIndex, this);
  7.     }
  8.     else
  9.     {
  10.         _vIndex = panel.Children.Count;
  11.         panel.Children.Add(this);
  12.     }
  13.     this._hIndex = panel.Index;
  14. }
复制代码
其中_vIndex代表shadowGrid在所在列中的位置。

之后则是比较复杂的上下移动:
  1. //向上移
  2. bool hasMove = false;
  3. if (shadowGrid.VIndex > 0)
  4. {
  5.     Control upControl = shadowGrid.ParentPanel.Children[shadowGrid.VIndex - 1] as Control;
  6.     Point upPoint = e.GetPosition(upControl);
  7.     if ((upPoint.Y - _beginPoint.Y) < upControl.ActualHeight / 2)
  8.     {
  9.         shadowGrid.MoveTo(shadowGrid.VIndex - 1);
  10.         hasMove = true;
  11.     }
  12. }
  13. //向下移
  14. if (!hasMove && shadowGrid.VIndex < shadowGrid.ParentPanel.Children.Count - 1)
  15. {
  16.     Control downControl = shadowGrid.ParentPanel.Children[shadowGrid.VIndex + 1] as Control;
  17.     Point downPoint = e.GetPosition(downControl);
  18.     if ((_beginPoint.Y - downPoint.Y) < this.ActualHeight / 2)
  19.     {
  20.         shadowGrid.MoveTo(shadowGrid.VIndex + 1);
  21.     }
  22. }
复制代码
由于Silverlight中并没有像WPF那样提供直接获取两个控件相对位移的方法,而由于控件的大小未必固定,所以不能像左右移动那样根据Panel的宽度来计算是否移动。幸好我们可以利用鼠标的MouseEventArgs e来获取鼠标当前位置和其他控件的位移,从而间接算出两个控件的位置。shadowGrid的另一个重载的MoveTo方法实现了将shadowGrid在一个Panel中从一个位置移到另一个位置:
  1. public void MoveTo(int newVIndex)
  2. {
  3.     ContainerPanel panel = ParentPanel;
  4.     panel.Children.Remove(this);

  5.     _vIndex = newVIndex;
  6.     panel.Children.Insert(_vIndex, this);
  7. }
复制代码
鼠标左键抬起或离开(拖拽结束)
抬起和离开的逻辑是一样的:
  1. void DragableGrid_MouseLeave(object sender, MouseEventArgs e)
  2. {
  3.     _parentGrid.Children.Remove(this);
  4.     RealeaseShadow();
  5.     IsDraging = false;
  6. }public void RealeaseShadow()
  7. {
  8.     if (this.shadowGrid != null)
  9.     {
  10.         this.shadowGrid.Release();
  11.     }
  12.     this.shadowGrid = null;
  13. }
复制代码
ShadowGrid的Release方法,将自己从父容器中移出,并将原始的可拖拽控件放入ShadowGrid所在的位置。
  1. public void Release()
  2. {
  3.     this._orginGrid.Margin = this.Margin;
  4.     ContainerPanel panel = ParentPanel;
  5.     panel.Children.Remove(this);
  6.     panel.Children.Insert(_vIndex, _orginGrid);
  7. }
复制代码
注意事项:

由于鼠标移动是一个很快的过程,MouseMove事件可能触发多次,如果移动过快,就可能出现运算不及,导致鼠标脱离可拖拽控件的情况,之后鼠标再返回控件,可能会出现问题。所以在一些地方,需要做一些看似多余的检测,比如RealeaseShadow方法中,检测shadowGrid 是否为空。 

由于使用了StackPanel和Grid,里面的控件默认会采用Strech样式,即充满整个容器,对于Panel中的控件,就没有指定它的Width,所以这里用到的都是控件的ActualWidth属性,也就是实际显示的宽度,这个不可设的属性会在控件Rend之后获得。 

如果外层的Grid的大小会改变,那么就不能利用分别设置上下左右的Margin来设置控件的位置,否则控件的大小也会随着Grid的大小而改变,那么就需要先确保控件的Width和Height有确定的值,然后设置控件的左对齐和上对齐,那么此时控件的Margin-Right和Maring-Bottom无效,则只要通过指定Margin-Top,和Margin-left,就可以对控件在Grid里做定位。
  1. this.Width = this.ActualWidth;
  2. this.Height = this.ActualHeight;
  3. this.VerticalAlignment = VerticalAlignment.Top;
  4. this.HorizontalAlignment = HorizontalAlignment.Left;
  5. //设置初始Margin。
  6. Thickness beginMargin = new Thickness(marginPoint.X - _beginPoint.X, marginPoint.Y - _beginPoint.Y,0,0); 
复制代码
(作者:smjack)
posted @ 2010-01-10 20:51  molin  阅读(655)  评论(0编辑  收藏  举报