One of the things I enjoy doing most is teaching developers how to write Windows Store apps using XAML and C#. I’ve been doing a lot of that lately, both for Microsoft and for other customers as well. But it has become clear to me as I teach these classes that in a typical class, half the people in the room know XAML pretty well, and half have little or no experience with it. That makes designing a course a challenge, because you either have to bore half the class by teaching them something they already know, or risk confusing the other half of the class when, for example, you demonstrate how to use Manipulation events to implement pinch-zooms and that half of the class doesn’t know what a ScaleTransform is.
Which is why I developed a new course entitled XAML Deep Dive. I’m teaching it for the first time this week at Microsoft. And I have lots of cool stuff to show, including a session on writing custom controls. Besides teaching folks how to build custom items controls like the CoverFlow control I introduced here a few months ago, I’ll be teaching them how to build custom layout controls by deriving from the framework’s Panel class. Which brings me to the reason for this blog post.
I built a couple of custom layout controls that you can download and learn from or use in your own apps. One, called RadialPanel, lays out its children in a circular array and provides lots of cool features, including built-in gesture support for spinning items round and round. The other, called CarouselPanel, is similar to RadialPanel, but it adds a third dimension that allows it to be tilted in and out of the plane of the screen.
The screen shot below shows RadialPanel arranging its items – a set of rectangles – in a circle.
Here’s the XAML that declares the RadialPanel and fills it with rectangles:
<local:RadialPanel Radius="250"> <Rectangle Width="50" Height="50" Fill="Red" /> <Rectangle Width="50" Height="50" Fill="Orange" /> <Rectangle Width="50" Height="50" Fill="Yellow" /> <Rectangle Width="50" Height="50" Fill="Green" /> <Rectangle Width="50" Height="50" Fill="Blue" /> <Rectangle Width="50" Height="50" Fill="Darkblue" /> <Rectangle Width="50" Height="50" Fill="Purple" /> <Rectangle Width="50" Height="50" Fill="#ff80c0" /> <Rectangle Width="50" Height="50" Fill="Red" /> <Rectangle Width="50" Height="50" Fill="Orange" /> <Rectangle Width="50" Height="50" Fill="Yellow" /> <Rectangle Width="50" Height="50" Fill="Green" /> <Rectangle Width="50" Height="50" Fill="Blue" /> <Rectangle Width="50" Height="50" Fill="Darkblue" /> <Rectangle Width="50" Height="50" Fill="Purple" /> <Rectangle Width="50" Height="50" Fill="#ff80c0" /> </local:RadialPanel>
Here’s another example that uses the same RadialPanel control, but this time, the items are TextBlocks arranged so that they radiate outward like the spokes on a wheel:
Here’s the corresponding XAML. This time, we set RadialPanel’s ItemOrientation property to “Rotated” to allow the items to radiate out. (The default is “Upright,” which rotates the items around their centers to keep them upright, like buckets on a Ferris wheel.) We also set ItemHorizontalAlignment to “Left” so the items are anchored at their leftmost point. (Other possible values include “Center,” which is the default, and “Right.”)
<local:RadialPanel Radius="125" ItemHorizontalAlignment="Left" ItemOrientation="Rotated"> <TextBlock Text="Jeff Prosise" Foreground="Red" /> <TextBlock Text="Jeffrey Richter" Foreground="Orange" /> <TextBlock Text="John Robbins" Foreground="Yellow" /> <TextBlock Text="Steve Porter" Foreground="Green" /> <TextBlock Text="Keith Rome" Foreground="Blue" /> <TextBlock Text="Rik Robinson" Foreground="Darkblue" /> <TextBlock Text="Mitch Harpur" Foreground="Purple" /> <TextBlock Text="Sergio Loscialo" Foreground="#ff80c0" /> <TextBlock Text="John Garland" Foreground="Red" /> <TextBlock Text="Josh Caroll" Foreground="Orange" /> <TextBlock Text="Jeremy Likness" Foreground="Yellow" /> <TextBlock Text="Dave Baskin" Foreground="Green" /> <TextBlock Text="David Banister" Foreground="Blue" /> <TextBlock Text="Jon Lester" Foreground="Darkblue" /> <TextBlock Text="Scott Roberts" Foreground="Purple" /> <TextBlock Text="Josh Lane" Foreground="#ff80c0" /> <TextBlock Text="Jeff Prosise" Foreground="Red" /> <TextBlock Text="Jeffrey Richter" Foreground="Orange" /> <TextBlock Text="John Robbins" Foreground="Yellow" /> <TextBlock Text="Steve Porter" Foreground="Green" /> <TextBlock Text="Keith Rome" Foreground="Blue" /> <TextBlock Text="Rik Robinson" Foreground="Darkblue" /> <TextBlock Text="Mitch Harpur" Foreground="Purple" /> <TextBlock Text="Sergio Loscialo" Foreground="#ff80c0" /> <TextBlock Text="John Garland" Foreground="Red" /> <TextBlock Text="Josh Caroll" Foreground="Orange" /> <TextBlock Text="Jeremy Likness" Foreground="Yellow" /> <TextBlock Text="Dave Baskin" Foreground="Green" /> <TextBlock Text="David Banister" Foreground="Blue" /> <TextBlock Text="Jon Lester" Foreground="Darkblue" /> <TextBlock Text="Scott Roberts" Foreground="Purple" /> <TextBlock Text="Josh Lane" Foreground="#ff80c0" /> </local:RadialPanel>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/white-space: pre;/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Finally, here’s RadialPanel’s cousin, CarouselPanel, arranging a collection of ellipses in a 3D circle:
And here’s the XAML that makes it happen:
<local:CarouselPanel Radius="250"> <Ellipse Width="65" Height="65" Fill="Red" /> <Ellipse Width="65" Height="65" Fill="Orange" /> <Ellipse Width="65" Height="65" Fill="Yellow" /> <Ellipse Width="65" Height="65" Fill="Green" /> <Ellipse Width="65" Height="65" Fill="Blue" /> <Ellipse Width="65" Height="65" Fill="Darkblue" /> <Ellipse Width="65" Height="65" Fill="Purple" /> <Ellipse Width="65" Height="65" Fill="#ff80c0" /> <Ellipse Width="65" Height="65" Fill="Red" /> <Ellipse Width="65" Height="65" Fill="Orange" /> <Ellipse Width="65" Height="65" Fill="Yellow" /> <Ellipse Width="65" Height="65" Fill="Green" /> <Ellipse Width="65" Height="65" Fill="Blue" /> <Ellipse Width="65" Height="65" Fill="Darkblue" /> <Ellipse Width="65" Height="65" Fill="Purple" /> <Ellipse Width="65" Height="65" Fill="#ff80c0" /> </local:CarouselPanel>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/white-space: pre;/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Here’s the cool part. By default, RadialPanel and CarouselPanel lay out their items statically – that is, the items don’t move. But add a GesturesEnabled=”True” attribute to either control, and you can use your finger (or your mouse if you’re not using a touch screen) to rotate the items around the center. In the first example, the rectangles remain upright no matter what angle they’re rotated to. In the second, the TextBlocks spin like the Wheel of Fortune. In the third, the items rotate around the center in response to pointer input, and their Z-indexes are manipulated so that items closer to the user’s eye clip the items behind them. You get inertia, too. If your finger is still moving when it breaks contact with the screen, the items continue spinning, with the motion constantly decelerating as the items approach their final position. (One of the challenges I encountered as I wrote the code was algorithmically determining whether the path traced by your finger should induce clockwise or counterclockwise motion. I ended up implementing an algorithm that computes the cross-product of two 2D vectors; the sign of the cross-product reveals the direction of motion. Remind me to go back and thank my high school math teachers.)
You can download the Visual Studio 2012 solution containing both controls as well as a Windows Store app that demos them from here. And you can use them as the basis for advanced UIs, such as CD covers that the user can spin around on a carousel, or ListBoxes with radial layouts. The demo app uses a FlipView control so you can swipe back and forth to view the different layouts.
For a lesson in how custom layout controls work, check out the source code for RadialPanel. To build a custom layout control, you derive from the Panel base class and override a couple of key virtual methods: MeasureOverride and ArrangeOverride. In MeasureOverride, you tell the system how much space you’d like to have for all the items. RadialPanel’s MeasureOverride method returns a width and height computed by adding the width of the widest child and the height of the tallest item to the diameter of the circle. In ArrangeOverride, the system tells you much space you’ve been allocated, and you position your items to fit into that space. In RadialPanel, ArrangeOverride uses some simple trigonometry to compute an anchor point along the circle for each item, and then positions the items at the anchor points. (Kudos again to those math teachers!) That’s the big picture. There are lots of details, too, including local RotateTransforms used to keep items upright by rotating them if ItemOrientation=”Upright”, and Manipulation events used internally to rotate the items in response to pointer input if GesturesEnabled=”True”. It’s all there in the source code if you care to dig in.
I’ll be publishing more custom controls for Windows Store apps soon, including one that will be the granddaddy of them all. Stay tuned.