既然Silverlight号称是Ajax杀手,而且相比javascript更接近桌面应用,那么这种拖拽的效果自然是手到擒来。
注:已对拖拽不够灵活以及自动放下控件的问题做了改进,详情请见“【原】改进了的"利用Silverlight实现类似iGoogle的浮动拖拽效果"”
Silverlight提供了Canvas、Grid、StackPanel三种布局容器,基本能满足各种需要。个人习惯是用Grid做框架布局,具体内容利用StackPanel自组织,而Canvas则在某些特定场合,比如打字游戏的界面中使用。
一方面在自适应浏览器大小的角度上,Grid提供了两方面的自由度,StackPanel提供了一个方向的自由度,而Canvas没有自由度,也就不能随这浏览器大小改变而自动适应大小;另一方面,Grid和StackPanel都可以不用直接指定坐标进行布局,而Canvas则需要指定确定的坐标,维护起来肯定麻烦;第三点,则是主要针对于可拖拽效果的,StackPanel是唯一可以通过添加、删除内部控件而自动调整内部其他控件位置的布局容器。
所以最后决定:用Grid做外部布局,每一列都设定ColumnDefinition,再在每一列添加一个StackPanel做为单列的容器:
<uc:ContainerGrid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </uc:ContainerGrid.ColumnDefinitions> <uc:ContainerPanel Grid.Column="0" x:Name="LeftPanel"> </uc:ContainerPanel> <uc:ContainerPanel Grid.Column="1" x:Name="CenterPanel"> </uc:ContainerPanel> <uc:ContainerPanel Grid.Column="2" x:Name="RightPanel"> </uc:ContainerPanel>
这里的ContainerGrid和ContainerPanel分别继承自Grid和Panel,主要是添加了几个方便以后查询以及扩展的属性。
可拖拽控件可以继承自任何控件,这里用的是UserControl。
public partial class DragableGrid : UserControl
那么对于DragableGrid,最主要的事件是三个:MouseLeftButtonDown(左键按下),MouseLeftButtonUp(左键抬起),MouseMove(左键移动)。MouseLeftButtonDown标识拖拽开始,MouseLeftButtonUp说明拖拽结束,而MouseMove则是拖拽过程触发。此外,考虑到当鼠标移动到控件可移动范围之外的情况,还有MouseLeave事件,和MouseLeftButtonUp的意义一样。所以需要在构造函数中添加它们的处理逻辑:
this.MouseLeftButtonDown += new MouseButtonEventHandler(DragableGrid_MouseLeftButtonDown); this.MouseLeftButtonUp += new MouseButtonEventHandler(DragableGrid_MouseLeftButtonUp); this.MouseMove += new MouseEventHandler(DragableGrid_MouseMove); this.MouseLeave += new MouseEventHandler(DragableGrid_MouseLeave);
void DragableGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { //标示开始拖拽 IsDraging = true; //鼠标的起始位置(控件内部) _beginPoint = e.GetPosition(this); //鼠标相对于Grid的位置 Point marginPoint = e.GetPosition(_parentGrid); ContainerPanel parentPanel = this.Parent as ContainerPanel; shadowGrid = new ShadowGrid(this, parentPanel); //设置初始Margin。 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); (this.Parent as Panel).Children.Remove(this); if (this.Parent == null) { _parentGrid.Children.Add(this); } this.SetValue(Grid.ColumnSpanProperty, _parentGrid.PanelCount); this.Margin = beginMargin; }
首先设置IsDraging标识位,然后分别获取鼠标点击点相对于控件左上角以及相对于ContainerGrid左上角的位移,从而获得可拖拽控件相对于ContainerGrid的Margin值。注意鼠标相对控件左上角的位移在拖拽过程中是不变的,所以作为可拖拽控件的一个私有变量存储起来。然后将DragableGrid从父Panel中移除,添加到ContainerGrid中,使它能在整个Grid的范围内移动。
这里仿照iGoogle和其他类似的可拖拽框架的模式,当拖拽一个可拖拽控件时,会在可拖拽控件的原处位置添加一个背影控件(ShadowGrid),用来占位:
public ShadowGrid(DragableGrid originGrid,ContainerPanel parentPanel) { InitializeComponent(); this._orginGrid = originGrid; //设置影子Grid和原始Grid的样式一致。 this.Width = _orginGrid.ActualWidth; this.Height = _orginGrid.ActualHeight; this.Text = _orginGrid.Text; this.Margin = _orginGrid.Margin; //设置影子Grid的父Panel并插入。 _vIndex = parentPanel.Children.IndexOf(_orginGrid); _hIndex = parentPanel.Index; parentPanel.Children.Insert(_vIndex + 1, this); }
先判断IsDraging 标识位,之后根据鼠标的新位置设置控件的Margin:
if (IsDraging) { Point newPosition = e.GetPosition(_parentGrid); 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是使用不变的。
接下来就是最麻烦的部分:根据新位置,判断控件的拖拽状况,这里有左右移动和上下移动两种情况。
首先是比较简单的左右移动:
//向左移 if (this.Margin.Left < _parentGrid.PanelWidth * (shadowGrid.HIndex - 0.5)) { if (shadowGrid.HIndex > 0) { ContainerPanel panel = this._parentGrid.ChildPanels[shadowGrid.HIndex - 1]; shadowGrid.MoveTo(panel); } } //向右移 else if (this.Margin.Left > _parentGrid.PanelWidth * (shadowGrid.HIndex + 0.5)) { if (shadowGrid.HIndex < _parentGrid.PanelCount - 1) { ContainerPanel panel = this._parentGrid.ChildPanels[shadowGrid.HIndex + 1]; shadowGrid.MoveTo(panel); } }
_parentGrid.PanelWidth代表每一列的宽度,而shadowGrid.HIndex则是shadowGrid所在列的序号,shadowGrid的MoveTo方法实现了将shadowGrid水平插入另一个Panel中:
public void MoveTo(ContainerPanel panel) { ParentPanel.Children.Remove(this); if (panel.Children.Count > _vIndex) { panel.Children.Insert(_vIndex, this); } else { _vIndex = panel.Children.Count; panel.Children.Add(this); } this._hIndex = panel.Index; }
其中_vIndex代表shadowGrid在所在列中的位置。
之后则是比较复杂的上下移动:
//向上移 bool hasMove = false; if (shadowGrid.VIndex > 0) { Control upControl = shadowGrid.ParentPanel.Children[shadowGrid.VIndex - 1] as Control; Point upPoint = e.GetPosition(upControl); if ((upPoint.Y - _beginPoint.Y) < upControl.ActualHeight / 2) { shadowGrid.MoveTo(shadowGrid.VIndex - 1); hasMove = true; } } //向下移 if (!hasMove && shadowGrid.VIndex < shadowGrid.ParentPanel.Children.Count - 1) { Control downControl = shadowGrid.ParentPanel.Children[shadowGrid.VIndex + 1] as Control; Point downPoint = e.GetPosition(downControl); if ((_beginPoint.Y - downPoint.Y) < this.ActualHeight / 2) { shadowGrid.MoveTo(shadowGrid.VIndex + 1); } }
由于Silverlight中并没有像WPF那样提供直接获取两个控件相对位移的方法,而由于控件的大小未必固定,所以不能像左右移动那样根据Panel的宽度来计算是否移动。幸好我们可以利用鼠标的MouseEventArgs e来获取鼠标当前位置和其他控件的位移,从而间接算出两个控件的位置。shadowGrid的另一个重载的MoveTo方法实现了将shadowGrid在一个Panel中从一个位置移到另一个位置:
public void MoveTo(int newVIndex) { ContainerPanel panel = ParentPanel; panel.Children.Remove(this); _vIndex = newVIndex; panel.Children.Insert(_vIndex, this); }
抬起和离开的逻辑是一样的:
void DragableGrid_MouseLeave(object sender, MouseEventArgs e) { _parentGrid.Children.Remove(this); RealeaseShadow(); IsDraging = false; }
public void RealeaseShadow() { if (this.shadowGrid != null) { this.shadowGrid.Release(); } this.shadowGrid = null; }
ShadowGrid的Release方法,将自己从父容器中移出,并将原始的可拖拽控件放入ShadowGrid所在的位置。
public void Release() { this._orginGrid.Margin = this.Margin; ContainerPanel panel = ParentPanel; panel.Children.Remove(this); panel.Children.Insert(_vIndex, _orginGrid); }
this.Width = this.ActualWidth; this.Height = this.ActualHeight; this.VerticalAlignment = VerticalAlignment.Top; this.HorizontalAlignment = HorizontalAlignment.Left; //设置初始Margin。 Thickness beginMargin = new Thickness(marginPoint.X - _beginPoint.X, marginPoint.Y - _beginPoint.Y,0,0);
源代码下载:可拖拽Silverlight控件源码