The world of WPF gives you an extreme amount of flexibility right off the bat - with things like styles, control templates, and the composability of almost anything, at first it seems like everything is right there at your fingertips. And while there is a lot immediately accessible, there are still cases where you have to get down into the nitty-gritty. Today we are going to take a look at how to create a WPF custom control - more specifically, a custom panel. The available panels in WPF are great (perhaps we will have a tutorial on how to use them all at some point in the future). But maybe you have a very specific need, and none of the panels quite work the way you want?
Well, in that case, it is custom controls to the rescue! The custom panel we are going to write today is going to be extremely similar to the wrap panel, except that we are going to add animation into the picture. When items in the panel need to wrap to the next line because the size of the panel changed, they won't just switch positions - they will animate to the new position. Useful? Not really. But it is a cool example and fun to play with.
Here's a couple screenshots of the panel in action (the second one is in the middle of an animated move):
Ok, so the first thing we need to do is create the skeleton of the class for the new custom control. In my case, I called it AnimatedWrapPanel, and it will extend the base Panel class:
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Media.Animation;
namespace AnimatedWrapPanel
{
public class AnimatedWrapPanel : Panel
{
}
}
Ok, so there is our base structure. Technically, this is already a control, but it wouldn't actually do anything yet. To actually get it to do something useful, there are two very important methods that we need to override:
{
}
protected override Size ArrangeOverride(Size finalSize)
{
}
WPF uses a two pass layout system to determine the positions and sizes of parents and children. Each of these methods corresponds to one of those steps. In the first step, the Measure step, parents are supposed to ask their children what their desired size is, given an available size. In the second step, the Arrange step, parents position their children and tell their children how much size they are actually getting. There are a couple things to note about each of these steps, so lets take a look at them one at a time.
The simplest MeasureOverride method (that actually works) for a panel would be the following:
{
Size resultSize = new Size(0,0);
foreach (UIElement child in Children)
{
child.Measure(availableSize);
resultSize.Width = Math.Max(resultSize.Width, child.DesiredSize.Width);
resultSize.Height = Math.Max(resultSize.Height, child.DesiredSize.Height);
}
resultSize.Width = double.IsPositiveInfinity(availableSize.Width) ?
resultSize.Width : availableSize.Width;
resultSize.Height = double.IsPositiveInfinity(availableSize.Height) ?
resultSize.Height : availableSize.Height;
return resultSize;
}
So what are we doing here? Well, first, in the MeasureOverride function, you are required to call Measure on every child. If you don't, then the child's desired size is never determined - and this will break the Arrange pass of the layout system. The other thing to note is that you can't just return the passed in availableSize as your desired size. This is because availableSize could potentially be infinite (think about inside of a scroll view) - but your desired size can never be infinite. So what we are doing here is using the size of our largest child to determine our desired size, but only when the availableSize is infinite.
Thats the bare minimum you can do in MeasureOverride - but for our custom panel, we need to do a little bit more than that. Heres what the MeasureOverride looks like in the AnimatedWrapPanel code:
{
Size infiniteSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
double curX = 0, curY = 0, curLineHeight = 0;
foreach (UIElement child in Children)
{
child.Measure(infiniteSize);
if (curX + child.DesiredSize.Width > availableSize.Width)
{ //Wrap to next line
curY += curLineHeight;
curX = 0;
curLineHeight = 0;
}
curX += child.DesiredSize.Width;
if(child.DesiredSize.Height > curLineHeight)
curLineHeight = child.DesiredSize.Height;
}
curY += curLineHeight;
Size resultSize = new Size();
resultSize.Width = double.IsPositiveInfinity(availableSize.Width) ?
curX : availableSize.Width;
resultSize.Height = double.IsPositiveInfinity(availableSize.Height) ?
curY : availableSize.Height;
return resultSize;
}
Essentially, we are doing more work here to come back with a better desired size. We do the wrap layout, keeping track of how big we end up being. To every child, we hand 'infinite' as the available size, so the children can be as big as they want to be. And in the end, if our available space is infinite, we use the size we calculated through the layout, but otherwise we take up all the room.
Ok, so thats measure. How about arrange? Here is the simplest possible arrange method:
{
foreach (UIElement child in Children)
{
child.Arrange(new Rect(0, 0, child.DesiredSize.Width, child.DesiredSize.Height));
}
return finalSize;
}
Again, you will want to call Arrange on every child, otherwise that child will not get placed/rendered. What we are doing here is placing every child at (0,0) and letting them be their desired size. The finalSize that gets passed into the ArrangeOverride can never be infinite, so we are allowed to just return it if we want to take all the room our parent has given us.
Thats a simple ArrangeOverride method, but it doesn't actually do any real layout of the children. Lets take a look at some code that actually acts like a wrap panel:
{
if (this.Children == null || this.Children.Count == 0)
return finalSize;
double curX = 0, curY = 0, curLineHeight = 0;
foreach (UIElement child in Children)
{
if (curX + child.DesiredSize.Width > finalSize.Width)
{ //Wrap to next line
curY += curLineHeight;
curX = 0;
curLineHeight = 0;
}
child.Arrange(new Rect(curX, curY, child.DesiredSize.Width,
child.DesiredSize.Height));
curX += child.DesiredSize.Width;
if (child.DesiredSize.Height > curLineHeight)
curLineHeight = child.DesiredSize.Height;
}
return finalSize;
}
So here we are actually doing some layout - very similar to the MeasureOverride shown above. By calling Arrange with the new position and size of each child, the children are laid out in wrap-panel-like fashion.
Ok, ok, I can hear you complaining already "But where is my animation? You said this would be animated!" Well, actually, all that we need to do is modify this ArrangeOverride function a bit. Instead of just placing the child at its new position, we will use a TranslateTransform and animate it to the new position:
{
if (this.Children == null || this.Children.Count == 0)
return finalSize;
TranslateTransform trans = null;
double curX = 0, curY = 0, curLineHeight = 0;
foreach (UIElement child in Children)
{
trans = child.RenderTransform as TranslateTransform;
if (trans == null)
{
child.RenderTransformOrigin = new Point(0, 0);
trans = new TranslateTransform();
child.RenderTransform = trans;
}
if (curX + child.DesiredSize.Width > finalSize.Width)
{ //Wrap to next line
curY += curLineHeight;
curX = 0;
curLineHeight = 0;
}
child.Arrange(new Rect(0, 0, child.DesiredSize.Width, child.DesiredSize.Height));
trans.BeginAnimation(TranslateTransform.XProperty,
new DoubleAnimation(curX, TimeSpan.FromMilliseconds(200)),
HandoffBehavior.Compose);
trans.BeginAnimation(TranslateTransform.YProperty,
new DoubleAnimation(curY, TimeSpan.FromMilliseconds(200)),
HandoffBehavior.Compose);
curX += child.DesiredSize.Width;
if (child.DesiredSize.Height > curLineHeight)
curLineHeight = child.DesiredSize.Height;
}
return finalSize;
}
So what are we doing here? Well, we are adding a TranslateTransform to each child. Then, when the time comes to move the child to a new position, we tell the transform to animate to the new position (using a DoubleAnimation that takes 200 milliseconds). And thats it! Man, animation is so easy in WPF.
Now, if we throw all that code together, we get all we need for a custom animated wrap panel:
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Media.Animation;
namespace AnimatedWrapPanel
{
public class AnimatedWrapPanel : Panel
{
private TimeSpan _AnimationLength = TimeSpan.FromMilliseconds(200);
protected override Size MeasureOverride(Size availableSize)
{
Size infiniteSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
double curX = 0, curY = 0, curLineHeight = 0;
foreach (UIElement child in Children)
{
child.Measure(infiniteSize);
if (curX + child.DesiredSize.Width > availableSize.Width)
{ //Wrap to next line
curY += curLineHeight;
curX = 0;
curLineHeight = 0;
}
curX += child.DesiredSize.Width;
if(child.DesiredSize.Height > curLineHeight)
curLineHeight = child.DesiredSize.Height;
}
curY += curLineHeight;
Size resultSize = new Size();
resultSize.Width = double.IsPositiveInfinity(availableSize.Width)
? curX : availableSize.Width;
resultSize.Height = double.IsPositiveInfinity(availableSize.Height)
? curY : availableSize.Height;
return resultSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
if (this.Children == null || this.Children.Count == 0)
return finalSize;
TranslateTransform trans = null;
double curX = 0, curY = 0, curLineHeight = 0;
foreach (UIElement child in Children)
{
trans = child.RenderTransform as TranslateTransform;
if (trans == null)
{
child.RenderTransformOrigin = new Point(0, 0);
trans = new TranslateTransform();
child.RenderTransform = trans;
}
if (curX + child.DesiredSize.Width > finalSize.Width)
{ //Wrap to next line
curY += curLineHeight;
curX = 0;
curLineHeight = 0;
}
child.Arrange(new Rect(0, 0, child.DesiredSize.Width,
child.DesiredSize.Height));
trans.BeginAnimation(TranslateTransform.XProperty,
new DoubleAnimation(curX, _AnimationLength), HandoffBehavior.Compose);
trans.BeginAnimation(TranslateTransform.YProperty,
new DoubleAnimation(curY, _AnimationLength), HandoffBehavior.Compose);
curX += child.DesiredSize.Width;
if (child.DesiredSize.Height > curLineHeight)
curLineHeight = child.DesiredSize.Height;
}
return finalSize;
}
}
}
But now that we have this awesome custom panel, how do we use it? Well, it is actually not that hard at all - just a couple lines of XAML code:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ARP="clr-namespace:AnimatedWrapPanel"
Title="Animated Wrap Panel Test" Height="300" Width="300">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ARP:AnimatedWrapPanel>
<Image Source="Images\Aquarium.jpg" Stretch="Uniform" Width="100" Margin="5"/>
<Image Source="Images\Ascent.jpg" Stretch="Uniform" Width="50" Margin="5" />
<Image Source="Images\Autumn.jpg" Stretch="Uniform" Width="200" Margin="5"/>
<Image Source="Images\Crystal.jpg" Stretch="Uniform" Width="75" Margin="5"/>
<Image Source="Images\DaVinci.jpg" Stretch="Uniform" Width="125" Margin="5"/>
<Image Source="Images\Follow.jpg" Stretch="Uniform" Width="100" Margin="5"/>
<Image Source="Images\Friend.jpg" Stretch="Uniform" Width="50" Margin="5"/>
<Image Source="Images\Home.jpg" Stretch="Uniform" Width="150" Margin="5"/>
<Image Source="Images\Moon flower.jpg" Stretch="Uniform" Width="100" Margin="5"/>
</ARP:AnimatedWrapPanel>
</ScrollViewer>
</Window>
First, in your Window tag (or page, or UserControl) you need to define the namespace that this custom control exists in. In this case the namespace is "AnimatedWrapPanel" - and so I added the attribute xmlns:ARP="clr-namespace:AnimatedWrapPanel". Now, to use it, you just add the tag:
</ARP:AnimatedWrapPanel>
And to give it children, you use it like any other WPF control that can have children - you throw them between the opening and closing tags!
<Image Source="Images\Home.jpg" Stretch="Uniform" Width="150" Margin="5"/>
<Image Source="Images\Moon flower.jpg" Stretch="Uniform" Width="100" Margin="5"/>
</ARP:AnimatedWrapPanel>
Thats all you need to do to use a custom control. Pretty simple, eh?
Well, thats it for this tutorial on how to create a custom panel control in WPF. If you want to play around with the control we made here, the animated wrap panel, you can download the visual studio project here. And, as always, if you ahve any questions or comments, feel free to leave them below.
03/29/2008 - 10:49
Thank you for this great tutorial. I was able to get a working example up in no time. You might want to clarify what MeasureOverride and ArrangeOverride are supposed to do. I was thought I knew what you meant, but I had to check another site to be sure.
Thanks again!
04/10/2008 - 15:55
Very useful. Thanks.
04/16/2008 - 22:46
Very good article.It works for me.
Thax a lot
04/19/2008 - 04:46
Great tutorial. It helped me a lot.
05/01/2008 - 13:50
Instead of initializaing the images in xaml, how would you load it up using the code behind? This is for the case that the set of images is specified by the directory a user selects.
06/17/2008 - 00:21
Beautiful example.
07/23/2008 - 07:24
Do you have any info regarding child element events? For example: I like to know if your child elements are user controls and you want to use selected item property much like a listbox or a list view. How would you achive that with a wrap panel?
Thanks
Ray C Akkanson
03/14/2009 - 13:15
I have to tell you, I've written many custom panels, but still refer to this page quite often. Nice work.
03/15/2009 - 17:47
Awesome tutorial !
Thanks, you saved my ass :D
03/16/2009 - 07:35
It really offers a very good insight into building CustomPanels...
Thanks a ton...
:)
05/03/2009 - 08:42
Nice tutorial
05/07/2009 - 11:21
This is the second time I've referred to your excellent tutorial so I thought it'd be polite to say thank you.
Here goes: Thank you!!!
My recent work needed a bit more performance and less flexibility so I turned your sample into an animated wrapping grid! (all cells the same size as the largest item as 99+% of my items will be the same size).
I also had several of these panels in a stackPanel in a grid with a draggable splitter on it. When I dragged the splitter, all of the panels resized causing lousy performance with all of the animations. By returning out of the arrange method very early if the dimensions of the panel hadn't changed enough to alter the "cell"s that each child was in I got a massive improvement (otherwise you re-issue the animation for each pixel that the splitter is dragged).
I can send me code to anyone that is interested (chrisrothery curly at sign hotmail.com) or post here and I'll make a stupidly big followup comment!
Chris
05/07/2009 - 14:25
If you want, you can throw your code in a forum post, so we can check it out.
05/08/2009 - 04:44
Cool, I'm glad you're interested in my code (I've registered now so not anonymous anymore!).
It could do with some tidying but it seems to be working well at the moment. Most of the mess comes from working out the rows/columns.
If we're willing to accept more restrictions (which I think I can for my application), you could fix the item size which would allow you to make decisions in the measure phase about whether the offered size has changed enough to warrant recalculating the number of rows/columns (although making that decision would probably cancel out any gains!).
Hope this helps someone and if you can assist improving it I'm all ears!
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace MyNamespace
{
public class AnimatedGridPanel : Panel
{
// grid size taken from largest child
private static double gridItemWidth;
private static double gridItemHeight;
private static double gridItemMargin = 3;
public Double GridItemMargin
{
set
{
gridItemMargin = value;
}
get { return gridItemMargin; }
}
private int nrRows = 0;
private int nrCols = 0;
private Boolean nrRowsColsChangedRecently = false;
static AnimatedGridPanel()
{
}
protected override Size MeasureOverride(Size availableSize)
{
Size resultSize = new Size(0, 0);
Size maxCellSize = new Size(0, 0);
// important step, children dont like not being measured
foreach (UIElement child in Children)
{
child.Measure(availableSize);
maxCellSize.Width = Math.Max(resultSize.Width, child.DesiredSize.Width);
maxCellSize.Height = Math.Max(resultSize.Height, child.DesiredSize.Height);
}
gridItemWidth = maxCellSize.Width;
gridItemHeight = maxCellSize.Height;
int newNrCols = 0;
if (Double.IsPositiveInfinity(availableSize.Width))
{
newNrCols = Children.Count;
resultSize.Width = Children.Count * (maxCellSize.Width + GridItemMargin);
}
else
{
newNrCols = (int)Math.Ceiling(availableSize.Width / (maxCellSize.Width + GridItemMargin));
resultSize.Width = availableSize.Width;
}
if (newNrCols != nrCols)
{
nrCols = newNrCols;
nrRowsColsChangedRecently = true;
}
int newNrRows = 0;
if (Double.IsPositiveInfinity(availableSize.Height))
{
newNrRows = (int)Math.Ceiling((double)Children.Count / nrCols); // how many do we need
resultSize.Height = newNrRows * (maxCellSize.Height + GridItemMargin);
}
else
{
newNrRows = (int)Math.Ceiling(availableSize.Height / (maxCellSize.Height + GridItemMargin)); // how many could we fit
resultSize.Height = availableSize.Height;
}
if (newNrRows != nrRows)
{
nrRows = newNrRows;
nrRowsColsChangedRecently = true;
}
return resultSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
// update children only if the
// number of columns has changed for this item and animates those children that
// need to move
int row = 0;
int col = 0;
double x = 0;
double y = 0;
TranslateTransform trans = null;
// I didn't think not arranging a child would work but it seems to
if (!nrRowsColsChangedRecently) return finalSize;
int n = 0;
nrRowsColsChangedRecently = false;
foreach (UIElement child in Children)
{
if (child is ContentPresenter) continue;
trans = child.RenderTransform as TranslateTransform;
if (trans == null)
{
child.RenderTransformOrigin = new Point(0, 0);
trans = new TranslateTransform();
child.RenderTransform = trans;
}
x = ((gridItemWidth + gridItemMargin) * col) + gridItemMargin;
y = ((gridItemHeight + gridItemMargin) * row) + gridItemMargin;
child.Arrange(new Rect(0, 0, child.DesiredSize.Width, child.DesiredSize.Height));
DoubleAnimation xda = new DoubleAnimation(x, TimeSpan.FromMilliseconds(250));
DoubleAnimation yda = new DoubleAnimation(y, TimeSpan.FromMilliseconds(250));
trans.BeginAnimation(TranslateTransform.XProperty,
xda,
HandoffBehavior.Compose);
trans.BeginAnimation(TranslateTransform.YProperty,
yda,
HandoffBehavior.Compose);
col++;
if (col >= nrCols - 1)
{
col = 0;
row++;
}
n++;
}
return finalSize;
}
}
}
Add Comment
[language] [/language]
Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]
See here for supported languages.
Javascript must be enabled to submit anonymous comments - or you can login.