在前面的章节里,我们完成了UIPlace的开发,来载入一个场景。有时候,会遇见这样的问题:当一个场景很大的时候(大大超出了显示器的大小),或者仅需要在屏幕的某一个区域中显示这个场景。这意味着我们在执行屏幕输出的时候,只需要在输出场景的一部分,至于其他的部分,需要通过和用户交互的手段(例如将鼠标移到显示区域的边缘,然后按下鼠标)来切换。为此,在虚拟实验室的引擎中,我引入了一个Stage对象,专门负责显示部分场景,以及和用户交互(接受鼠标点击滚动舞台)。
舞台和场景
舞台和场景的关系如下图所示:
上图中,舞台的大小远远小于场景(UIPlace),因此,我可以通过鼠标点击地图滚动区域(浅黄色部分)来移动舞台的位置(实际上是移动UIPlace的X属性和Y属性),来滚动场景。
Stage和UIPlace的UML关系图如下:
public Stage Stage { get; set; }
同样的,需要按照以下方式定义Stage类:
public class Stage : Canvas { protected UIPlace m_Place; public UIPlace Place { get { return m_Place; } set { if (value != m_Place) { if (m_Place != null) { Children.Remove(m_Place); m_Place.Stage = null; } m_Place = value; if (m_Place != null) { Children.Insert(0, m_Place); m_Place.Stage = this; } } } } }
此外,还要修改Stage的Clip属性,把Clip矩形的大小设置成和Stage一样大,否则,Silverlight在渲染时,还是会把UIPlace超出Stage部分的区域显示出来,可以通过监听Canvas的SizeChanged事件达到这个效果:
protected RectangleGeometry m_ViewRect = new RectangleGeometry(); public Size Size { get { return new Size(m_ViewRect.Rect.Width, m_ViewRect.Rect.Height); } } public Stage() { this.Clip = m_ViewRect; this.MinWidth = 480; this.MinHeight = 320; this.SizeChanged += (sender, e) => { m_ViewRect.Rect = new Rect() { Width = e.NewSize.Width, Height = e.NewSize.Height }; }; }
滚动场景
在Silverlight中,要平滑地滚动场景(移动UIPlace的位置),最简单的方法莫过于使用Storyboard了:
protected Storyboard MoveStoryBoard { get; set; } protected DoubleAnimation MoveXAnimation { get; set; } protected DoubleAnimation MoveYAnimation { get; set; }
以上代码中的两个DoubleAnimation分别表示水平移动和垂直移动。
我用两个整形来表示该如何移动UIPlace:
protected int MoveX { get; set; } protected int MoveY { get; set; }
例如,MoveX=1,表示水平向右移动1个单位,而MoveX=-2,则表示UIPlace水平向左移动两个单位。
在Stage的构造函数中初始化这些对象:
public Stage() { //………… #region 初始化移动用的StoryBoard MoveStoryBoard = new Storyboard(); MoveXAnimation = new DoubleAnimation(); MoveYAnimation = new DoubleAnimation(); MoveXAnimation.Duration = new Duration(TimeSpan.FromSeconds(0.05)); MoveYAnimation.Duration = new Duration(TimeSpan.FromSeconds(0.05)); MoveStoryBoard.Children.Add(MoveXAnimation); MoveStoryBoard.Children.Add(MoveYAnimation); MoveStoryBoard.Completed += new EventHandler(OnMoveComplete); #endregion //………… }
此外,将这些对象和UIPlace挂钩:
public UIPlace Place { //………… set { if(value!=m_Place) { //………… if(m_Place!=null) { //………… MoveStoryBoard.Stop(); Storyboard.SetTarget(MoveXAnimation, m_Place); Storyboard.SetTargetProperty(MoveXAnimation, new PropertyPath("(Canvas.Left)")); Storyboard.SetTarget(MoveYAnimation, m_Place); Storyboard.SetTargetProperty(MoveYAnimation, new PropertyPath("(Canvas.Top)")); } } } }
监听鼠标事件
Stage和用户交互的方式为:当用户鼠标在“地图滚动区域”按下时,开始滚动场景,当鼠标松开时,停止滚动:
this.MouseLeftButtonDown += new MouseButtonEventHandler(OnMouseDown); this.MouseLeftButtonUp += new MouseButtonEventHandler(OnMouseUp);
鼠标按下时的监听函数:
protected virtual void OnMouseDown(object sender, MouseButtonEventArgs e) { //当前点击区域 Point point = e.GetPosition(this); //如果在地图滚动区域中,这里区域大小设置为80 if (point.X <= 80 || point.Y <= 80 || point.X >= Size.Width - 80 || point.Y >= Size.Height - 80) { if (point.X <= 80) { MoveX = 2; } if (point.Y <= 80) { MoveY = 2; } if (point.X >= Size.Width - 80) { MoveX = -2; } if (point.Y >= Size.Height - 80) { MoveY = -2; } //滚动地图 MoveStage(); } }
鼠标送开始的监听函数:
protected virtual void OnMouseUp(object sender, MouseButtonEventArgs e) { MoveX = 0; MoveY = 0; }
MoveStage函数
我通过MoveStage函数,判断地图能否移动(例如当地图滚动到最右边缘时,无论鼠标点多少下,地图都不能继续向左移动了),然后,计算并修改MoveXAnimation和MoveYAnimation的To属性,告诉Storyboard该如何移动地图,达到平滑移动的效果:
protected virtual void MoveStage() { if (Place == null) return; //得到UIPlace当前坐标 var x = Canvas.GetLeft(Place); var y = Canvas.GetTop(Place); //判断能否向右移动 if ((MoveX > 0 && x < 0)) { if (x + VPlace.GRIDSIZE * MoveX > 0) { MoveXAnimation.To = 0; } else { MoveXAnimation.To = x + VPlace.GRIDSIZE * MoveX; } } //判断能否向左移动 if (MoveX < 0 && x > Size.Width - Place.Width) { if (x + VPlace.GRIDSIZE * MoveX < Size.Width - Place.Width) { MoveXAnimation.To = Size.Width - Place.Width; } else { MoveXAnimation.To = x + VPlace.GRIDSIZE * MoveX; } } //判断能否向下移动 if (MoveY > 0 && y < 0) { if (y + VPlace.GRIDSIZE * MoveY > 0) { MoveYAnimation.To = 0; } else { MoveYAnimation.To = y + VPlace.GRIDSIZE * MoveY; } } //判断能否向上移动 if (MoveY < 0 && y > Size.Height - Place.Height) { if (y + VPlace.GRIDSIZE * MoveY < Size.Height - Place.Height) { MoveYAnimation.To = Size.Height - Place.Height; } else { MoveYAnimation.To = y + VPlace.GRIDSIZE * MoveY; } } MoveStoryBoard.Begin(); }
完成移动
当调用Storyboard的Begin函数,开始移动地图,一系列移动的动画结束后,Storyboard对象产生Completed事件。我用OnMoveComplete函数来处理这个事件,需要做两件事:如果MoveX或MoveY不等于0(即MouseLeftButtonUp 事件还没有产生,用户鼠标处于按下状况),则再调用一次MoveStage方法,继续滚动地图;如果MoveX和MoveY都等于0,那么意味着用户的鼠标已经松开了,这时,可以调用Storyboard的Stop方法来停止移动地图。
需要注意的是,Storyboard完成移动后,虽然改变UIPlace的顶点坐标,但一旦调用Stop方法,UIPlace的顶点坐标又回到移动前的状态了,所以,在调用Storyboard的Stop方法前后,要手动修改UIPlace的顶点坐标:
protected void OnMoveComplete(object sender, EventArgs e) { if (MoveX != 0 || MoveY != 0) { MoveStage(); } else { var x = Canvas.GetLeft(m_Place); var y = Canvas.GetTop(m_Place); MoveStoryBoard.Stop(); Canvas.SetLeft(m_Place,x); Canvas.SetTop(m_Place,y); } }
使用Stage
最后看看Stage如何使用
从资源库的自定义控件中拖动一个Stage对象到画布上:
对应的xmal文件
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Test.Page" Width="Auto" Height="Auto" xmlns:view="clr-namespace:Test.View;assembly=Test" > <view:Stage x:Name="Stage" Width="Auto" Height="Auto" Margin="10,10,10,10"> </view:Stage> </UserControl>
修改Page.xmal.cs文件如下:
namespace Test { public partial class Page : UserControl { public Page() { // 需要初始化变量 InitializeComponent(); String personString = "<person name='周慊' centerX='73' centerY='144' x='1100' " + "y='780' file='engine1/person.xml' id='80d76e6b-08d6-4225-bdcc-bf68ca19ca58' " + "actionid='default' />"; String placeString ="<place name='NBA' width='1800' height='1200' centerX='0' " + "centerY='0' x='-800' y='-500' file='engine1/place1/place.xml' />"; VPlace vp = new VPlace(placeString); VItem vi = new VItem(personString); var ui = vi.UI; var pui = vp.UI; Stage.Place = (pui as UIPlace); vp.AddItem(vi); } } }