虚拟实验室引擎的开发和实现(八、舞台)

本文介绍了一种用于滚动大型场景的舞台控件实现方案。通过Stage对象显示部分场景,并通过鼠标交互来平滑移动整个场景,实现了有限显示区域内自由浏览大场景的需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在前面的章节里,我们完成了UIPlace的开发,来载入一个场景。有时候,会遇见这样的问题:当一个场景很大的时候(大大超出了显示器的大小),或者仅需要在屏幕的某一个区域中显示这个场景。这意味着我们在执行屏幕输出的时候,只需要在输出场景的一部分,至于其他的部分,需要通过和用户交互的手段(例如将鼠标移到显示区域的边缘,然后按下鼠标)来切换。为此,在虚拟实验室的引擎中,我引入了一个Stage对象,专门负责显示部分场景,以及和用户交互(接受鼠标点击滚动舞台)。

舞台和场景

舞台和场景的关系如下图所示:

image

上图中,舞台的大小远远小于场景(UIPlace),因此,我可以通过鼠标点击地图滚动区域(浅黄色部分)来移动舞台的位置(实际上是移动UIPlace的X属性和Y属性),来滚动场景。

Stage和UIPlace的UML关系图如下:

image  即在UIPlace中,需要添加一个Stage属性的对象:

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函数,判断地图能否移动(例如当地图滚动到最右边缘时,无论鼠标点多少下,地图都不能继续向左移动了),然后,计算并修改MoveXAnimationMoveYAnimation的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对象到画布上:

image

对应的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);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值