Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Elements Cloud

0.00/5 (No votes)
4 May 2011 1  
A WPF Elements Cloud User Control

Introduction

I suppose everyone knows such a thing as Tags Cloud. Usually, you can see it on some kinds of web-sites, where this control represents a container with certain quantity of links and when you enter a mouse cursor on it, these links move on the virtual ellipsoid (or sphere) while you move the mouse.

In this article, I try to explain how to create such a control using WPF. Also you can export this code to Silverlight (making some adjustments) and make a Silverlight User Control.

To start making the control, create a WPF User Control Application and remove auto-generated UserControl.xaml file because we don't need it.

Using the Code

So let's start now. At the beginning, I want to explain the main concept of the control. This control is just a container but with specific properties. The items which will be inserted into this container must have specific properties too and now I want to start the explanation of the class CloudItem.

CloudItem

namespace ElementsCloud
{
    /// <summary>
    /// ElementsCloudItem represent a grid which is able to 
    /// contain any UIElements objects
    /// </summary>
    public class ElementsCloudItem : System.Windows.Controls.Grid
    {
        private ScaleTransform itemScaling;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="point3D">Center point of the object</param>
        /// <param name="element">Object which will
        /// be added to Children collection and be shown at a window</param>
        public ElementsCloudItem(Point3D point3D, UIElement element)
        {
            CenterPoint = point3D;
            itemScaling = new ScaleTransform();
            this.LayoutTransform = itemScaling;
            Children.Add(element);
            itemScaling.CenterX = CenterPoint.X;
            itemScaling.CenterY = CenterPoint.Y;
        }
        /// <summary>
        /// Constructor
        /// </summary>
        public ElementsCloudItem()
        {
            CenterPoint = new Point3D();
            itemScaling = new ScaleTransform();
            this.LayoutTransform = itemScaling;
            itemScaling.CenterX = CenterPoint.X;
            itemScaling.CenterY = CenterPoint.Y;
        }

        #region CenterPoint property

        public static DependencyProperty CenterPointProperty =
            DependencyProperty.Register("CenterPoint", typeof(Point3D), 
            typeof(ElementsCloudItem), new FrameworkPropertyMetadata(
            new PropertyChangedCallback(OnCenterPointChanged)));

        private static void OnCenterPointChanged(DependencyObject sender, 
                            DependencyPropertyChangedEventArgs e)
        {
            ElementsCloudItem elementsCloudItem = (ElementsCloudItem)sender;
            elementsCloudItem.CenterPoint = (Point3D)e.NewValue;
        }
        //================================================================
        /// <summary>
        /// Center point of the item
        /// </summary>
        public Point3D CenterPoint
        {
            get { return (Point3D)GetValue(CenterPointProperty); }
            set { SetValue(CenterPointProperty, value); }
        }

        #endregion

        //=================================================================
        /// <summary>
        /// Update options of the item and redraw it
        /// </summary>
        public void Redraw(ElementsCloudItemSize size, 
                    double scaleRatio, double opacityRatio)
        {
            itemScaling.ScaleX = itemScaling.ScaleY = 
               Math.Abs((16 + CenterPoint.Z * 4) * scaleRatio);
            Opacity = CenterPoint.Z + opacityRatio;

            Canvas.SetLeft(this, (size.XOffset + CenterPoint.X * 
                                  size.XRadius) - (ActualWidth / 2.0));
            Canvas.SetTop(this, (size.YOffset - CenterPoint.Y * 
                                 size.YRadius) - (ActualHeight / 2.0));
            Canvas.SetZIndex(this, (int)(CenterPoint.Z * 
                             Math.Min(size.XRadius, size.YRadius)));
        }
    }
}

As you can see, this class is simple. It has one field - ScaleTransform for zooming of the items. The items will be zoomOut if they are on the back side of the front items and the items will be zoomIn if they are on the front side of the back items. It's hard to explain it using text, especially with my not so good English, but I hope you got the sense. :) It will be clearer when you'll see it in the demo.

So let's continue. The class has one property - CenterPoint. It's used for ScaleTransform to scale the items correctly.

The most interesting thing here is the method Redraw. It is called each time when the item must be redrawn according its new position on the virtual ellipsoid. The item accepts new size (gets smaller when the item moves straight to the back side of the virtual ellipsoid) and new opacity (gets more transparent when the item moves straight to the back side of the virtual ellipsoid) values. The parameters are simple to understand. Use specific values (through special properties which will be described later) for ScaleRatio and OpacityRatio to define the size of the ScaleProperty and transparency level for the OpacityProperty of the items.

The last parameter defines the displacement of the item on the virtual ellipse. You can try to change the constant values used in the properties XRadius and YRadius. In this case, you will get the "ellipsoid" with other parameters.

namespace ElementsCloud
{
    public class ElementsCloudItemSize 
    {
        public double XOffset { get; set; }
        public double YOffset { get; set; }
        
        public double XRadius
        {
            get { return XOffset * 6 / 10; }
        }
        
        public double YRadius
        {
            get { return YOffset * 2 / 3; }
        }
    }
}

ElementCloud

This class is our main UserControl, although it extends Grid. Let's see what fields we have here:

  1. RotateTransform3D rotateTransform - is used for rotating (or moving, because actually the items move on the Canvas. Further, I'm going to use "the items rotate" and not "the items move" although it's the same) the items.
  2. double slowDownCouner - rotation speed
  3. List<ElementsCloudItem> elementsCollection - collection of the contained items
  4. RotationType rotationType - has two options: using mouse or automatically
  5. Point rotateDirection - defines rotation direction
  6. Canvas canvas - contains the items

Some main properties:

  1. ScaleRatioProperty - It has been already explained in the previous class (ElementsCloudItem)
  2. OpacityRatioProperty - It has been already explained in the previous class (ElementsCloudItem)
namespace ElementsCloud
{ 
    /// <summary>
    /// User Control - "ElementsCloud"
    /// </summary>
    public class ElementsCloud : Grid
    {
        private readonly RotateTransform3D rotateTransform;
        private bool isRunRotation;
        private double slowDownCounter;
        private List<ElementsCloudItem> elementsCollection;
        private RotationType rotationType;
        private Point rotateDirection;
        private Canvas canvas;

        private double scaleRatio;
        private double opacityRatio;

        public ElementsCloud()
        {
            this.Background = Brushes.Transparent;

            canvas = new Canvas(){
                VerticalAlignment = System.Windows.VerticalAlignment.Stretch,
                HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch
            };
            this.Children.Add(canvas);
            rotateTransform = new RotateTransform3D();

            SizeChanged += OnPageSizeChanged;

            rotationType = RotationType.Mouse;
            rotateDirection = new Point(100, 0);
            slowDownCounter = 500;
            elementsCollection = new List<ElementsCloudItem>();
            scaleRatio = 0.09;
            opacityRatio = 1.3;
        }

        #region Properties

        #region RotateDirectionProperty

        public static DependencyProperty RotateDirectionProperty =
            DependencyProperty.Register("RotateDirection", typeof(Point), 
            typeof(ElementsCloud), new FrameworkPropertyMetadata(new Point(100, 0),
            new PropertyChangedCallback(OnRotateDirectionChanged)));

        private static void OnRotateDirectionChanged(DependencyObject sender, 
                            DependencyPropertyChangedEventArgs e)
        {
            ElementsCloud ElementsCloud = (ElementsCloud)sender;
            ElementsCloud.rotateDirection = (Point)e.NewValue;
        }
        //===============================================================================
        /// <summary>
        /// Defines the direction of rotation
        /// </summary>
        public Point RotateDirection
        {
            get { return (Point)GetValue(RotateDirectionProperty); }
            set
            {
                SetValue(RotateDirectionProperty, value);
                SetRotateTransform(value);
            }
        }

        #endregion

        #region ScaleRatioProperty

        public static DependencyProperty ScaleRatioProperty =
         DependencyProperty.Register("ScaleRatio", typeof(double), 
         typeof(ElementsCloud), new FrameworkPropertyMetadata(0.09, 
         new PropertyChangedCallback(OnScaleRatioChanged)));

        private static void OnScaleRatioChanged(DependencyObject sender, 
                            DependencyPropertyChangedEventArgs e)
        {
            ElementsCloud elementsCloud = (ElementsCloud)sender;
            elementsCloud.scaleRatio = (double)e.NewValue;
        }
        //===============================================================================
        /// <summary>
        /// Defines a scaling of ElementsCloudItems
        /// when they stays further than other elements
        /// </summary>
        public double ScaleRatio
        {
            get { return (double)GetValue(ScaleRatioProperty); }
            set { SetValue(ScaleRatioProperty, value); }
        }

        #endregion

        #region OpacityRatioProperty

        public static DependencyProperty OpacityRatioProperty =
            DependencyProperty.Register("OpacityRatio", typeof(double), 
              typeof(ElementsCloud), 
              new FrameworkPropertyMetadata(1.3, 
              new PropertyChangedCallback(OnOpacityRatioChanged)));

        private static void OnOpacityRatioChanged(DependencyObject sender, 
                DependencyPropertyChangedEventArgs e)
        {
            ElementsCloud elementsCloud = (ElementsCloud)sender;
            elementsCloud.opacityRatio = (double)e.NewValue;
        }
        //===============================================================================
        /// <summary>
        /// Defines a strength of opacity when
        /// ElementsCloudItem stays behind other elements
        /// </summary>
        public double OpacityRatio
        {
            get { return (double)GetValue(OpacityRatioProperty); }
            set { SetValue(OpacityRatioProperty, value); }
        }

        #endregion

        #region Other properties
        //===============================================================================
        /// <summary>
        /// Allow to switch between manual or mouse rotation
        /// </summary>
        public RotationType RotationType
        {
            get { return rotationType; }
            set { rotationType = value; }
        }

        //===============================================================================
        /// <summary>
        /// Collection of elements
        /// </summary>
        public List<ElementsCloudItem> ElementsCollection
        {
            get { return elementsCollection; }
            set { elementsCollection = value; }
        }
        #endregion

        #endregion

        #region Methods
        //===============================================================================
        /// <summary>
        /// Stop rotation
        /// </summary>
        public void Stop()
        {
            if (isRunRotation == true)
            {
                CompositionTarget.Rendering -= OnCompositionTargetRendering;
                this.MouseEnter -= OnGridMouseEnter;
                this.MouseLeave -= OnGridMouseLeave;

                slowDownCounter = 500.0;
                isRunRotation = false;
                rotateTransform.Rotation = 
                  new AxisAngleRotation3D(new Vector3D(0, 0, 0), 0);
            }

        }
        //===============================================================================
        /// <summary>
        /// Start rotation
        /// </summary>
        public void Run()
        {
            if (isRunRotation == false)
            {
                CompositionTarget.Rendering += OnCompositionTargetRendering;
                this.MouseEnter += OnGridMouseEnter;
                this.MouseLeave += OnGridMouseLeave;
                this.MouseMove += OnGridMouseMove;
                slowDownCounter = 500.0;
                isRunRotation = true;
                //rotateTransform.Rotation = 
                //   new AxisAngleRotation3D(new Vector3D(0.8, 0.6, 0), 0.5);

                SetRotateTransform(rotateDirection);
                RedrawElements();
            }
        }

        #region Private Methods
        //===============================================================================
        /// <summary>
        /// Configure rotate transformation
        /// </summary>
        ///<param name="position">Defines the direction of rotation</param>
        private void SetRotateTransform(Point position)
        {
            ElementsCloudItemSize size = GetElementsSize();

            double x = (position.X - size.XOffset) / size.XRadius;
            double y = (position.Y - size.YOffset) / size.YRadius;
            double angle = Math.Sqrt(x * x + y * y);
            rotateTransform.Rotation = 
               new AxisAngleRotation3D(new Vector3D(-y, -x, 0.0), angle);
        }
        //===============================================================================
        /// <summary>
        /// Redraw all elements in Canvas
        /// </summary>
        private void RedrawElements()
        {
            canvas.Children.Clear();

            int length = elementsCollection.Count;
            for (int i = 0; i < length; i++)
            {
                double a = Math.Acos(-1.0 + (2.0 * i) / length);
                double d = Math.Sqrt(length * Math.PI) * a;
                double x = Math.Cos(d) * Math.Sin(a);
                double y = Math.Sin(d) * Math.Sin(a);
                double z = Math.Cos(a);

                elementsCollection[i].CenterPoint = new Point3D(x, y, z);
                canvas.Children.Add(elementsCollection[i]);
            }
        }
        //===============================================================================
        /// <summary>
        /// Rotate blocks
        /// </summary>
        private void RotateBlocks()
        {
            ElementsCloudItemSize size = GetElementsSize();

            foreach (ElementsCloudItem ElementsCloudItem in elementsCollection)
            {
                Point3D point3D;
                if (rotateTransform.TryTransform(
                        ElementsCloudItem.CenterPoint, out point3D))
                {
                    ElementsCloudItem.CenterPoint = point3D;
                    ElementsCloudItem.Redraw(size, scaleRatio, opacityRatio);
                }
            }
        }

        //===============================================================================
        /// <summary>
        /// Get new size for all elements depending of screen resolution
        /// </summary>
        private ElementsCloudItemSize GetElementsSize()
        {
            return new ElementsCloudItemSize
            {
                XOffset = canvas.ActualWidth / 2.0,
                YOffset = canvas.ActualHeight / 2.0
            };
        }
        #endregion

        #endregion

        #region Events
        //===============================================================================
        /// <summary>
        /// Redraw buttons with new size
        /// </summary>
        private void OnPageSizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (elementsCollection != null)
            {
                ElementsCloudItemSize size = GetElementsSize();

                foreach (ElementsCloudItem button in elementsCollection)
                {
                    button.Redraw(size, scaleRatio, opacityRatio);
                }
            }
        }
        //===============================================================================
        /// <summary>
        /// Rendering
        /// </summary>
        private void OnCompositionTargetRendering(object sender, EventArgs e)
        {
            if (!(isRunRotation || (slowDownCounter <= 0)))
            {
                AxisAngleRotation3D axis = (AxisAngleRotation3D)rotateTransform.Rotation;
                axis.Angle *= slowDownCounter / 500;
                rotateTransform.Rotation = axis;
                slowDownCounter--;
            }
            if (((AxisAngleRotation3D)rotateTransform.Rotation).Angle > 0.05)
            {
                RotateBlocks();
            }
        }
        //===============================================================================
        /// <summary>
        /// Attach new event to grid when mouse enter to grid
        /// </summary>
        private void OnGridMouseEnter(object sender, MouseEventArgs e)
        {
            if (rotationType == RotationType.Mouse && isRunRotation == false)
            {
                this.MouseMove += OnGridMouseMove;
                isRunRotation = true;
                slowDownCounter = 500.0;
            }
        }
        //===============================================================================
        /// <summary>
        /// Detach event when mouse leave grid
        /// </summary>
        private void OnGridMouseLeave(object sender, MouseEventArgs e)
        {
            if (rotationType == RotationType.Mouse && isRunRotation == true)
            {
                this.MouseMove -= OnGridMouseMove;
                isRunRotation = false;
                GC.Collect();
            }
        }
        //===============================================================================
        /// <summary>
        /// Move and rotate buttons when mouse position changed
        /// </summary>
        private void OnGridMouseMove(object sender, MouseEventArgs e)
        {
            if (rotationType == RotationType.Mouse)
                rotateDirection = e.GetPosition(canvas);
            SetRotateTransform(rotateDirection);
        }
        #endregion
    }
}

So let's analyze the code step by step.

The main method here is Run().

public void Run()
{
    if (isRunRotation == false)
    {
        CompositionTarget.Rendering += OnCompositionTargetRendering;
        this.MouseEnter += OnGridMouseEnter;
        this.MouseLeave += OnGridMouseLeave;
        this.MouseMove += OnGridMouseMove;
        slowDownCounter = 500.0;
        isRunRotation = true;

        SetRotateTransform(rotateDirection);
        RedrawElements();
    }
}

It must be called each time you want to start the UserControl. The method attaches all essential events, sets some parameters and configures rotation option using method SetRotateTransform().

private void SetRotateTransform(Point position)
{
 ElementsCloudItemSize size = GetElementsSize();

 double x = (position.X - size.XOffset) / size.XRadius;
 double y = (position.Y - size.YOffset) / size.YRadius;
 double angle = Math.Sqrt(x * x + y * y);
 rotateTransform.Rotation = new AxisAngleRotation3D(new Vector3D(-y, -x, 0.0), angle);
}  

(Depending of the window size, rotation must be configured correctly). After all the configurations, the method Run() draws the items calling RedrawsElements().

private void RedrawElements()
{
    canvas.Children.Clear();

    int length = elementsCollection.Count;
    for (int i = 0; i < length; i++)
    {
        double a = Math.Acos(-1.0 + (2.0 * i) / length);
        double d = Math.Sqrt(length * Math.PI) * a;
        double x = Math.Cos(d) * Math.Sin(a);
        double y = Math.Sin(d) * Math.Sin(a);
        double z = Math.Cos(a);

        elementsCollection[i].CenterPoint = new Point3D(x, y, z);
        canvas.Children.Add(elementsCollection[i]);
    }
} 

These items are positioned according of the specific formula which sets the items on the edges of the virtual ellipsoid.

The last important thing is to override OnCompositionTargetRendering():

private void OnCompositionTargetRendering(object sender, EventArgs e)
{
    if (!(isRunRotation || (slowDownCounter <= 0)))
    {
        AxisAngleRotation3D axis = (AxisAngleRotation3D)rotateTransform.Rotation;
        axis.Angle *= slowDownCounter / 500;
        rotateTransform.Rotation = axis;
        slowDownCounter--;
    }
    if (((AxisAngleRotation3D)rotateTransform.Rotation).Angle > 0.05)
    {
        RotateBlocks();
    }
}

and put the code there for re-rendering of the items - RotateBlocks():

private void RotateBlocks()
{
     ElementsCloudItemSize size = GetElementsSize();

     foreach (ElementsCloudItem ElementsCloudItem in elementsCollection)
     {
            Point3D point3D;
         if (rotateTransform.TryTransform(ElementsCloudItem.CenterPoint, out point3D))
         {
             ElementsCloudItem.CenterPoint = point3D;
             ElementsCloudItem.Redraw(size, scaleRatio, opacityRatio);
         }
     }
}

The rendering of the items must not depend of the size of the screen, therefore firstly it's very important to get the screen size and divide them in 2 to define the center point. After that, you can run a loop for the collection of the items and redraw each item at the certain position, but before you will do it, transform the current center point of the item. If you don't do it, the items won't be rotated.

The last thing I want to say is why ElementCloud extends Grid but not UserControl. You should not use UserControl class if you want to make a control which has a collection of some elements (such as grid, etc.) because in this case you will not be able to set Name property for the items in the collection. That's why you should remove .xaml class (optional, but actually, in .xaml, you can change root element from UserControl to Grid, but creating this control I didn't need this file) create a new one (ore change the current) and inherit grid but not UserControl.

So that's the end of the article. Hope you liked it. I'll try to answer any questions you've got.

History

  • 4th May 2011 - Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here