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