Yet another carousel control
I meant to write something about MEF after my holiday, but I’ve been sidetracked
for a while. One of the things that happened to divert my notoriously unstable attention was a quite excellent five day Blend course by Brennon Williams. My preferred method of writing XAML has been, so far, in a text editor, but Herr Williams was quite persuasive in demonstrating that Blend is, in fact, far, far better for the job.
As part of the course, we wrote a small application. I ended up hacking together a 3d carousel user control for this, which I felt was pretty cool, so I set about rewriting it as a custom control for future use. No, I did
not use Blend. Yes, I did notice it’s a damn sight harder to do it in pure code than it was in Blend.
Warning: This post contains some math.
This control displays one (or, indeed zero) or more Visual objects in a 3d space. Elements are arranged on equally sized panels placed around a central point – imagine a number of controls glued to the sides of a box, or a prism. The control can also be rotated to present a given face to the user.
Since the control takes instances of Visual as content, it can support most WPF elements. The contents are wrapped in instances of Viewport3DVisual2D, which keeps the child element interactive.
The math (warned you, didn’t I?)
To lay out the Viewport3DVisual2D elements, we need to throw in a bit of math. The idea is to rotate each element by a number of degrees, so that the form a closed shape. The angle will always be 360 degrees, divided by the number of elements. If we have four sides, for example, each face would be rotated 90 degrees; the first element would be at 0 degrees, the second at 90, the third at 180, and the last one at 270. Since all sides are equal in size, this will always give us a closed shape.
Getting the angle may be easy, but it’s only part of the process – we also need to work out an offset, so that all the sides line up. (see diagram). Luckily, this isn’t much more complicated. We know that all the sides need to be the same distance from the centre of the shape, and we already know what the angle between two corners of the shape and the centre of the shape is (angle a in the diagram). We also know the length of one side – which we do, since this is actually the width of the Viewport3DVisual2D.
All we need now is a right angle, which we get by drawing a line (dotted grey in diagram) from the midpoint of the Viewport3DVisual2D (dark black line in diagram). This will also bisect angle a, so what we have is a right angled triangle with a known side and a known angle. This allows us to calculate the distance of the line from the centre to the face.
Angle b is adjacent to the line we want to find, and directly opposite the line whose length is half the width of a face. This lets us use the formula:
Tan(b) = opposite / adjacent
Tan(b) = (Width / 2) / adjacent
Tan(b) * adjacent = width / 2
adjacent = (width / 2) / Tan(b)
... which will give us the length of the dotted grey line in the image. We will use this to specify the z coordinate for the axis of rotation for all the face - each face is generated centred on the origin (0,0,0), so any offset will result in an actual coordinate.
There, that wasn't too bad was it?
Attack of the clones
While I was writing this control, I had a bit of a problem when attaching the child elements - WPF was complaining because the elements already had a parent. Luckily, Marlon came to the rescue and suggested that the elements be cloned, and the clone attached to the Viewport3DVisual2D elements. This isn't as painful as it sounds, since the XamlWriter and XamlReader classes offer very simple methods to convert an element to and from raw xaml:
string s = XamlWriter.Save(visual); StringReader stringReader = new StringReader(s); using (XmlReader xmlReader = XmlTextReader.Create(stringReader, new XmlReaderSettings())) element.Visual = (UIElement)XamlReader.Load(xmlReader);
(Updated, 18th October 2008)
This did leave a bit of an issue with events - entirely my bad, should have RTFM. MSDN clearly states that XamlWriter.Save will not retain events, which is a bit of a problem considering we're working with controls that are meant to be interactive here. The fix for this (as well as the problem with element parents) turned out to be quite straightforward - detach all the visuals from the Viewport2DVisual3D elements:
foreach (Viewport2DVisual3D child in Children) child.Visual = null; this.Children.Clear();
The code download has been updated to reflect this.
As Marlon also pointed out to me, Dr. WPF's CodeProject article about conceptual children also provides another way to skin the proverbial cat.
Using the control
This control inherits ModelVisual3D, so it can be dropped into any Viewport3D and used like any other 3D control. The elements that will make up the faces of the carousel have to be placed in the Visuals collection. The carousel control also exposes the ElementWidth and ElementHeight properties, which will define the aspect ratio of the faces. Finally, SelectedIndex determines which side is presented to the user.