I recently had need of an Accordian control, such as that found in JQuery UI.
I decided to implement this as a simple Panel. This AccordianPanel can contain any controls, but has special behaviour for child Expander controls: It will only allow a single one to be expanded at any one time.
public AccordianPanel()
{
AddHandler(Expander.ExpandedEvent, new RoutedEventHandler(ChildExpanded));
}
Starting with the constructor, we register a new event handler for the Expander.ExpandedEvent:
void ChildExpanded(object sender, RoutedEventArgs e)
{
foreach (UIElement child in InternalChildren)
{
Expander expander = FindExpander(child);
if (expander != null && expander != e.OriginalSource)
{
expander.IsExpanded = false;
}
}
}
When we detect a child Expander's Expanded state has changed, we look for any other child Expanders and unexpand them. The FindExpander function allows for the Expander to be parented:
Expander FindExpander(UIElement e)
{
while (e != null && !(e is Expander))
{
if (VisualTreeHelper.GetChildrenCount(e) == 1)
{
e = VisualTreeHelper.GetChild(e, 0) as UIElement;
}
else
{
e = null;
}
}
return (Expander)e;
}
Finally, we implement MeasureOverride:
protected override Size MeasureOverride(Size availableSize)
{
double requiredHeight = 0;
double resizableHeight = 0;
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
requiredHeight += child.DesiredSize.Height;
if (CanResize(child))
{
resizableHeight += child.DesiredSize.Height;
}
}
if (requiredHeight > availableSize.Height)
{
double pixelsToLose = requiredHeight - availableSize.Height;
foreach (UIElement child in InternalChildren)
{
double height = child.DesiredSize.Height;
if (CanResize(child))
{
height -= (child.DesiredSize.Height / resizableHeight) * pixelsToLose;
child.Measure(new Size(availableSize.Width, height));
}
}
}
return base.MeasureOverride(availableSize);
}
And ArrangeOverride:
protected override Size ArrangeOverride(Size finalSize)
{
double totalHeight = 0;
double resizableHeight = 0;
foreach (UIElement child in Children)
{
totalHeight += child.DesiredSize.Height;
if (CanResize(child))
{
resizableHeight += child.DesiredSize.Height;
}
}
double pixelsToLose = totalHeight - finalSize.Height;
double y = 0;
foreach (UIElement child in InternalChildren)
{
double height = child.DesiredSize.Height;
if (pixelsToLose > 0 && CanResize(child))
{
height -= (child.DesiredSize.Height / resizableHeight) * pixelsToLose;
}
child.Arrange(new Rect(0, y, finalSize.Width, height));
y += height;
}
return base.ArrangeOverride(finalSize);
}
Plus the small Utility function CanResize:
bool CanResize(UIElement e)
{
Expander expander = FindExpander(e);
return expander != null && expander.IsExpanded;
}
And that's it! I initially had problems with the fact that the layout for this type of panel requires two passes, which doesn't immediately seem to be supported by WPF. However, following a question on the ever-reliable StackOverflow, it seems it can in fact be done.
You can even make it animated using something like the AnimatedPanel from here.
Thank you very much. really useful
ReplyDelete