Click here to Skip to main content
15,886,362 members
Articles / Desktop Programming / WPF
Article

Create a movable usercontrol in WPF part I

Rate me:
Please Sign up or sign in to vote.
2.43/5 (4 votes)
6 Oct 2008CPOL4 min read 37.5K   17   4
introduces how to create a usercontrol and enable animation to do a random movement within the parent control area

Introduction

This article introduces how to create a usercontrol and enable animation to do a random movement within the parent control area. I am still new to WPF, so I am not sure if I am doing the right thing, so if not, please let me know. Thanks

Background

There are major two ways to implement an animation in WPF:

  1. StoryBoard with using DoubleAnimation, VectorAnimation, or anything similar
  2. DispatcherTimer

I have read some online blogs say using StoryBoard is better than using the timer. It probably is true, but in this article, I am using the timer because it's easier to code... well, maybe I should say it uses less lines of code to get what I want. Believe it or not, at the beginning, I tried to use DoubleAnimation, but apparently I used it in a wrong way. Please let me briefly introduce some basic:  

When using an XXXAnimation object,  we need to specify From, To, and Duration.

  • From: is the animation start's point
  • To: is the animation end's point 
  • Duration: is the length of time for the animation to play   

So, if we want a 2D linear animation, we need at least two animation objects, one for horizontal movement, another for vertical movement.  

Suppose we specify X.From(100) and Y.From(50), X.To(200)  and Y.To(200), and duration to 2 seconds for both of them. It means we want to object to move from (100, 50) to (200, 200) in 2 seconds.   

However, allow me tell you what mistake I made, so you won't do it: 

Previously, I tried to set a random maximum distance that the control can move. It's really hard because when the control hits the boundary, it must bounce back. So you can see, given a control location, I need to generate a random angle first, then calculate the maximum movable distance, calculate the time by a given pixel per millisecond speed. Things got really messy. 

The correct way should be (at least I think):

  1. Randomly generate two small movement amount for X and Y
  2. Define a very small duration   
Suppose the control original location is at (0,0), and I got a random (2,3), the duration is 2 millisecond. It means in this 2 millisecond, the control will slightly move from (0,0) to (2,3).

Furthermore, in the Completed event handler, I should check if the control reaches the parent boundary yet, if not, start the animation again, otherwise, generate a different X and Y values to move to another direction.

I think that should work much better than what I am doing now. In fact, I am using the similar technique but with DispatcherTimer.   

One thing I really hate in the timer is, the interval value depends on how hard the animation is, so I can't make sure the speed is constant for all moving controls. In MSDN, it explains the interval as follows:   

Timers are not guaranteed to execute exactly when the time interval occurs, but they are guaranteed to not execute before the time interval occurs.

See that? I didn't try the StoryBoard way, but I believe that will be better, because a storyboard can make sure the animation finish within the specified duration.    

Using the code

If you have read my previous entry, in order to use a UserControl in an XAML file, we have to create a base class, such as
 

C#
public class MovableUserControl : System.Windows.Controls.UserControl 		 

In the constructor, we have

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

The constructor adds a TranslateTransform class so it can be used to do the 2D linear transformation. 

First, we need to find a way to fingure out the parent control's area:

C#
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....");
    }
}

Also, we want to initialize some values before we can start the animation:

C#
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 is a variable to indicate if we need to restart the animation. 

hasStartedOnce is a trick, the default is false, once the Start is called at the first time, it will be set to true. See reason blow. 

In the Start and Stop methods, we have:

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


C#
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;
}

Now you see why I need to have that hasStartedOnce variable. 

Here comes tothe most important part, the interval handler:

C#
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;
    }
}

First, I calculate the random movement, then I will update it, finally, I check if the control has hit the boundary, if yes, I need to tell the "calculator" to restart. 

C#
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;
    }
}

In this RecalculateMovement method, I always check restart value first, because the default is false, so when the first time Start is called, it will fall to the if statement. I also have a piece of code to determine the moving direction. And there is a small trick here, I expose an Event, which will delegate the inherited class to do anything interesting once it knows which direction is going. It will be useful and you will see in the future post. 

The UpdateMovement method is straightforward. 

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

 

Points of Interest

I uploaded the source code of this file in here, and you can take a look. Next time I will show you how to inherit from this base class and do something interesting.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer ImageComponent.NET
Canada Canada
I am a full-time employee @ a large corporation, a part-time master student @ University of Manitoba, and a part-time freelance developer @ ImageComponent.NET, and a full-time husband, a part-time house cleaner, law mower, and so on. God, I don't have time for my Wii!

Comments and Discussions

 
GeneralTimer is not recommended for WPF animation Pin
Frank W. Wu2-Apr-09 8:56
Frank W. Wu2-Apr-09 8:56 
GeneralI'm sure it is very simple... Pin
Thomas ST24-Feb-09 22:20
Thomas ST24-Feb-09 22:20 
GeneralPosting code isn't hard Pin
Dewey6-Oct-08 16:21
Dewey6-Oct-08 16:21 
GeneralRe: Posting code isn't hard Pin
Hong Zhang (Sowen)6-Oct-08 16:43
Hong Zhang (Sowen)6-Oct-08 16:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.