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






2.43/5 (4投票s)
介绍如何创建用户控件并启用动画以在父控件区域内进行随机移动
引言
本文介绍如何创建一个用户控件并启用动画,使其在父控件区域内随机移动。我对 WPF 还是新手,不确定我这样做是否正确,如果不对,请告诉我。谢谢。
背景
在 WPF 中实现动画主要有两种方法:
- 使用 DoubleAnimation、VectorAnimation 或类似对象的 StoryBoard
- DispatcherTimer
我读过一些在线博客说使用 StoryBoard 比使用计时器更好。这可能确实如此,但在这篇文章中,我使用的是计时器,因为它更容易编写……好吧,也许我应该说,用更少的代码行就能达到我想要的效果。信不信由你,一开始我尝试使用 DoubleAnimation,但显然我用错了。请允许我简单介绍一些基本知识:
使用 XXXAnimation 对象时,我们需要指定 From、To 和 Duration。
- From:动画的起始点
- To:动画的终点
- Duration:动画播放的时长
所以,如果我们想要一个二维线性动画,我们至少需要两个动画对象,一个用于水平移动,另一个用于垂直移动。
假设我们指定 X.From(100) 和 Y.From(50),X.To(200) 和 Y.To(200),持续时间都为 2 秒。这意味着我们希望对象在 2 秒内从 (100, 50) 移动到 (200, 200)。
然而,请允许我告诉你我犯的错误,这样你就不会犯同样的错误:
以前,我曾尝试设置控件可以移动的最大随机距离。这非常困难,因为当控件撞到边界时,它必须弹回。所以你看,给定一个控件位置,我需要先生成一个随机角度,然后计算最大可移动距离,然后根据给定的每毫秒像素速度计算时间。事情变得非常混乱。
正确的方法应该是(至少我认为):
- 随机生成 X 和 Y 的两个小的移动量
- 定义一个非常短的持续时间
此外,在 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;
}