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
{
public class ElementsCloudItem : System.Windows.Controls.Grid
{
private ScaleTransform itemScaling;
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;
}
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;
}
public Point3D CenterPoint
{
get { return (Point3D)GetValue(CenterPointProperty); }
set { SetValue(CenterPointProperty, value); }
}
#endregion
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:
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.
double slowDownCouner
- rotation speed
List<ElementsCloudItem> elementsCollection
- collection of the contained items
RotationType rotationType
- has two options: using mouse or automatically
Point rotateDirection
- defines rotation direction
Canvas canvas
- contains the items
Some main properties:
ScaleRatioProperty
- It has been already explained in the previous class (ElementsCloudItem
)
OpacityRatioProperty
- It has been already explained in the previous class (ElementsCloudItem
)
namespace ElementsCloud
{
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;
}
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;
}
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;
}
public double OpacityRatio
{
get { return (double)GetValue(OpacityRatioProperty); }
set { SetValue(OpacityRatioProperty, value); }
}
#endregion
#region Other properties
public RotationType RotationType
{
get { return rotationType; }
set { rotationType = value; }
}
public List<ElementsCloudItem> ElementsCollection
{
get { return elementsCollection; }
set { elementsCollection = value; }
}
#endregion
#endregion
#region Methods
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);
}
}
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();
}
}
#region Private Methods
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);
}
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]);
}
}
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);
}
}
}
private ElementsCloudItemSize GetElementsSize()
{
return new ElementsCloudItemSize
{
XOffset = canvas.ActualWidth / 2.0,
YOffset = canvas.ActualHeight / 2.0
};
}
#endregion
#endregion
#region Events
private void OnPageSizeChanged(object sender, SizeChangedEventArgs e)
{
if (elementsCollection != null)
{
ElementsCloudItemSize size = GetElementsSize();
foreach (ElementsCloudItem button in elementsCollection)
{
button.Redraw(size, scaleRatio, opacityRatio);
}
}
}
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();
}
}
private void OnGridMouseEnter(object sender, MouseEventArgs e)
{
if (rotationType == RotationType.Mouse && isRunRotation == false)
{
this.MouseMove += OnGridMouseMove;
isRunRotation = true;
slowDownCounter = 500.0;
}
}
private void OnGridMouseLeave(object sender, MouseEventArgs e)
{
if (rotationType == RotationType.Mouse && isRunRotation == true)
{
this.MouseMove -= OnGridMouseMove;
isRunRotation = false;
GC.Collect();
}
}
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