65.9K
CodeProject 正在变化。 阅读更多。
Home

创建可移动的 WPF 用户控件 - 第一部分

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.43/5 (4投票s)

2008年10月6日

CPOL

4分钟阅读

viewsIcon

38426

介绍如何创建用户控件并启用动画以在父控件区域内进行随机移动

引言

本文介绍如何创建一个用户控件并启用动画,使其在父控件区域内随机移动。我对 WPF 还是新手,不确定我这样做是否正确,如果不对,请告诉我。谢谢。

背景

在 WPF 中实现动画主要有两种方法:

  1. 使用 DoubleAnimation、VectorAnimation 或类似对象的 StoryBoard
  2. DispatcherTimer

我读过一些在线博客说使用 StoryBoard 比使用计时器更好。这可能确实如此,但在这篇文章中,我使用的是计时器,因为它更容易编写……好吧,也许我应该说,用更少的代码行就能达到我想要的效果。信不信由你,一开始我尝试使用 DoubleAnimation,但显然我用错了。请允许我简单介绍一些基本知识: 

使用 XXXAnimation 对象时,我们需要指定 FromToDuration

  • From:动画的起始点
  • To:动画的终点 
  • Duration:动画播放的时长   

所以,如果我们想要一个二维线性动画,我们至少需要两个动画对象,一个用于水平移动,另一个用于垂直移动。 

假设我们指定 X.From(100) 和 Y.From(50),X.To(200) 和 Y.To(200),持续时间都为 2 秒。这意味着我们希望对象在 2 秒内从 (100, 50) 移动到 (200, 200)。   

然而,请允许我告诉你我犯的错误,这样你就不会犯同样的错误: 

以前,我曾尝试设置控件可以移动的最大随机距离。这非常困难,因为当控件撞到边界时,它必须弹回。所以你看,给定一个控件位置,我需要先生成一个随机角度,然后计算最大可移动距离,然后根据给定的每毫秒像素速度计算时间。事情变得非常混乱。 

正确的方法应该是(至少我认为):

  1. 随机生成 X 和 Y 的两个小的移动量
  2. 定义一个非常短的持续时间   
假设控件的原始位置是 (0,0),我得到一个随机的 (2,3),持续时间是 2 毫秒。这意味着在这 2 毫秒内,控件将从 (0,0) 轻微移动到 (2,3)。

此外,在 Completed 事件处理程序中,我应该检查控件是否已到达父边界,如果还没有,则再次启动动画,否则,生成不同的 X 和 Y 值以移动到另一个方向。

我认为这比我目前所做的要好得多。事实上,我使用的是类似的技术,但使用了 DispatcherTimer。   

我真的不喜欢计时器的一点是,间隔值取决于动画的“强度”,所以我无法确保所有移动控件的速度都恒定。MSDN 对间隔的解释如下:   

不能保证计时器在时间间隔发生时执行,但可以保证它们不会在时间间隔发生之前执行。

看到了吗?我没有尝试 StoryBoard 的方式,但我相信那会更好,因为 StoryBoard 可以确保动画在指定的时间内完成。    

使用代码

如果您阅读了我之前的 文章,为了在 XAML 文件中使用 UserControl,我们必须创建一个基类,例如:
 

public class MovableUserControl : System.Windows.Controls.UserControl 		 

在构造函数中,我们有:

        public MovableUserControl()
	 {
            TransformGroup tg = new TransformGroup();
            tg.Children.Add(Translate);
            this.RenderTransform = tg;
            timer.Tick += new EventHandler(timer_Tick);
        } 
        private TranslateTransform _translate = new TranslateTransform();
        public TranslateTransform Translate
        {
            get { return _translate; }
        }

构造函数添加了一个 TranslateTransform 类,因此它可以用于执行二维线性变换。 

首先,我们需要找到一种方法来确定父控件的区域。

        protected virtual void TryLoadParentSize()
        {
            // the parent control should always be a UIElement, I am not very sure if it can have another case
            if (Parent is UIElement)
            {
                parentWidth = (Parent as UIElement).RenderSize.Width;
                parentHeight = (Parent as UIElement).RenderSize.Height;
            }
            else
            {
                throw new Exception("The Parent is not a UIElement, I am not sure what to do....");
            }
        } 

此外,我们希望在开始动画之前初始化一些值。

        protected virtual void initializeMovement()
        {
            if (!hasStartedOnce)
            {
                // intialize the original position
                originalPoint = VisualTreeHelper.GetOffset(this);
                currentX = originalPoint.X;
                currentY = originalPoint.Y;
            }

            timer.Interval = TimeSpan.FromMilliseconds(10);
            restart = true;
        }

restart 是一个指示是否需要重新启动动画的变量。 

hasStartedOnce 是一个技巧,默认为 false,一旦第一次调用 Start,它将被设置为 true。原因如下。 

在 Start 和 Stop 方法中,我们有:

        public virtual void Start()
        {
            TryLoadParentSize();
            initializeMovement();
            run();
        } 


        public virtual void Stop()
        {
            this.timer.Stop();

            // if the user manually stops, one thing frustrating is next time I call GetOffset, which
            // stil returns the orignal placement location, instead the location that the control is stopping. 
            // so I have to tell this control, we have started once, don't call GetOffset again
            hasStartedOnce = true;
        } 

现在您知道我为什么需要 hasStartedOnce 变量了。 

现在到了最重要的部分,间隔处理程序。

        protected virtual void timer_Tick(object sender, EventArgs e)
        {
            RecalculateMovement();
            UpdateMovement();
            if (currentX + this.ActualWidth >= parentWidth + RightOffset || 
                currentY + this.ActualHeight >= parentHeight + BottomOffset || 
                currentX <= LeftOffset || 
                currentY <= TopOffset)
            {
                restart = true;
            }
        }

首先,我计算随机移动量,然后更新它,最后,我检查控件是否撞到了边界,如果撞到了,我需要通知“计算器”重新开始。 

        protected virtual void RecalculateMovement()
        {
            if (restart)
            {
                movementX = rand.Next(0, 5);
                movementY = rand.Next(0, 5);

                movementX = (currentX > halfWidth) ? -movementX : movementX;
                movementY = (currentY > halfHeight) ? -movementY : movementY;

                if (movementX == 0 && movementY == 0)
                {
                    movementX = (currentX > halfWidth) ? -1 : 1;
                    movementY = (currentY > halfHeight) ? -1 : 1;
                }

                // determine the direction
                if (movementX < 0 && movementY < 0)
                    Direction = MovingDirection.Upper_Left;
                else if (movementX < 0 && movementY == 0)
                    Direction = MovingDirection.Left;
                else if (movementX < 0 && movementY > 0)
                    Direction = MovingDirection.Down_Left;
                else if (movementX == 0 && movementY < 0)
                    Direction = MovingDirection.Upper;
                else if (movementX == 0 && movementY > 0)
                    Direction = MovingDirection.Downward;
                else if (movementX > 0 && movementY < 0)
                    Direction = MovingDirection.Upper_Right;
                else if (movementX > 0 && movementY > 0)
                    Direction = MovingDirection.Down_Right;
                else if (movementX > 0 && movementY == 0)
                    Direction = MovingDirection.Right;

                if (MovementCalculated != null) MovementCalculated();

                restart = false;
            }
        }

在此 RecalculateMovement 方法中,我总是首先检查 restart 值,因为默认值为 false,所以在第一次调用 Start 时,它会进入 if 语句。我还有一段代码来确定移动方向。这里有一个小技巧,我暴露了一个事件,一旦它知道移动方向,就会委托继承类来执行任何有趣的操作。这会很有用,您将在以后的文章中看到。 

UpdateMovement 方法很简单。 

        protected virtual void UpdateMovement()
        {
            currentX += movementX;
            currentY += movementY;
            Translate.X += movementX;
            Translate.Y += movementY;
        }  

 

关注点

我已将此文件的源代码上传到 此处,您可以看一下。下次我将向您展示如何继承此类并进行一些有趣的操作。    
在 WPF 中创建可移动用户控件 - 第一部分 - CodeProject - 代码之家
© . All rights reserved.