Click here to Skip to main content
Click here to Skip to main content

Create a movable usercontrol in WPF part I

, 6 Oct 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
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
 

public class MovableUserControl : System.Windows.Controls.UserControl 		 

In the constructor, we have

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

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:

        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:

        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:

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

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

Here comes tothe most important part, the interval handler:

        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. 

        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. 

        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)

Share

About the Author

Hong Zhang (Sowen)
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 PinmemberFrank W. Wu2-Apr-09 8:56 
GeneralI'm sure it is very simple... PinmemberThomas ST24-Feb-09 22:20 
GeneralPosting code isn't hard PinmemberDewey6-Oct-08 16:21 
GeneralRe: Posting code isn't hard PinmemberHong Zhang (Sowen)6-Oct-08 16:43 

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

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

| Advertise | Privacy | Mobile
Web02 | 2.8.141022.2 | Last Updated 6 Oct 2008
Article Copyright 2008 by Hong Zhang (Sowen)
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid