【WPF】实现QQ中的分组面板

要实现的面板的效果如下图所示:

clip_image002 clip_image004

一个面板打开了,其它的面板会自动收起。而且打开的面板会填充所有可用空间。那么这样的效果在WPF里应该如何实现呢?

1. 多个面板,排成一排,感觉可以用ListBox。

2. 然后里面的东西,点一下打开,再点一下收起。感觉就是一个Expander嘛。

3. 一个打开,其它所有的收起。可以把Expander的IsExpanded与SelectedItem绑定。

第一步:ListBox + Expander + Style

上面所有的功能都可以用现有的控件做到,没有必要做个自定义控件,一个Style就可以搞定了。

为了让代码窄一点。所以分成了几个部分。Style部分如下所示。

<ControlTemplate x:Key="ExpandListItemTemplate" TargetType="{x:Type ListBoxItem}">
    <Border BorderThickness="1">
        <Border CornerRadius="3" Padding="2,1,2,2" BorderThickness="1" BorderBrush="#FF666670">
            <Expander Header="Header" Content="{TemplateBinding Content}" 
                      IsExpanded="{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}}"/>
        </Border>
    </Border>
</ControlTemplate>
<Style x:Key="ExpandListItemStyle" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Template" Value="{StaticResource ExpandListItemTemplate}"/>
</Style>
<Style x:Key="MostSimple" TargetType="{x:Type ListBox}">
    <Setter Property="SelectionMode" Value="Single"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ItemContainerStyle" Value="{StaticResource ExpandListItemStyle}"/>
</Style>

主体部分如下所示。

<ListBox Style="{StaticResource MostSimple}">
    <ListBoxItem>
        <ListBox>
            <Button Content="OK"/>
            <Button Content="Cancel"/>
            <Button Content="Yes"/>
            <Button Content="No"/>
        </ListBox>
    </ListBoxItem>
    <ListBoxItem>
        <ListBox>
            <CheckBox Content="Close"/>
            <CheckBox Content="Open"/>
            <CheckBox Content="Copy"/>
            <CheckBox Content="Paste"/>
        </ListBox>
    </ListBoxItem>
    <ListBoxItem>
        <ListBox>
            <RadioButton Content="Stay"/>
            <RadioButton Content="Leave"/>
            <RadioButton Content="Stay"/>
            <RadioButton Content="Leave"/>
        </ListBox>
    </ListBoxItem>
</ListBox>

我们来看一下效果。

clip_image006

看上去差不多了,点一下如何?

clip_image008

完了,打开的Expander没有填充。由于ListBoxItem默认是放在VirtualizedStackPanel中的,所以里面的Item都是向上对齐,不填充。我的第一感觉就是用个会填充的当ItemsPanel就可以了。记得DockPanel有LastChildFill的功能。试试。

<ItemsPanelTemplate x:Key="ExpandListItemsPanelTemplate">
    <DockPanel/>
</ItemsPanelTemplate>
<Style x:Key="ExpandListItemStyle" TargetType="{x:Type ListBoxItem}">
    <Setter Property="DockPanel.Dock" Value="Top"/>
    <Setter Property="Template" Value="{StaticResource ExpandListItemTemplate}"/>
</Style>
<Style x:Key="MostSimple" TargetType="{x:Type ListBox}">
    <Setter Property="SelectionMode" Value="Single"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ItemContainerStyle" Value="{StaticResource ExpandListItemStyle}"/>
    <Setter Property="ItemsPanel" Value="{StaticResource ExpandListItemsPanelTemplate}"/>
</Style>

看看效果。

clip_image010

寒,还没有点就展开了。早该想到的。

想了半天如何动态地改变Last Child是哪个。但是没有想出啥好办法,一个方法是把Item一个个取出来重新放回去。这也太变态了。再想想好像也没有可用的Panel了。完了,又要写一个自定义Panel了。还好这个Panel的功能不复杂。

这个面板要做的就是把其中的一个item填充。其它的就按其期望的最小大小放上去。顺序不能乱就可以了。

想了一个简单的逻辑。就是LargestChildFill,找出最大的Child,让它填充剩下的所有空间。

/// <summary>
/// The logic is very simple. Make the largest child fill.
/// </summary>
public class GroupPanel : Panel
{
    private UIElement largestChild;
    private double totalHeight;

    protected override Size MeasureOverride(Size availableSize)
    {
        totalHeight = 0;
        double width = 0;

        largestChild = null;
        foreach (UIElement child in Children)
        {
            child.Measure(availableSize);

            if (largestChild == null || child.DesiredSize.Height >= largestChild.DesiredSize.Height)
            {
                largestChild = child;
            }

            totalHeight += child.DesiredSize.Height;
            if (child.DesiredSize.Width > width)
            {
                width = child.DesiredSize.Width;
            }
        }

        return new Size(width, totalHeight);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        double yOffset = 0;
        foreach (UIElement child in Children)
        {
            if (child == largestChild)
            {
                double finalHeight = child.DesiredSize.Height + finalSize.Height - totalHeight;
                child.Arrange(new Rect(0, yOffset, finalSize.Width, finalHeight));
                yOffset += finalHeight;
            }
            else
            {
                child.Arrange(new Rect(0, yOffset, finalSize.Width, child.DesiredSize.Height));
                yOffset += child.DesiredSize.Height;
            }
        }

        return finalSize;
    }
}

然后把ItemsPanelTemplate改一下。

<ItemsPanelTemplate x:Key="ExpandListItemsPanelTemplate"> 
    <c:GroupPanel/> 
</ItemsPanelTemplate> 

搞定。看下效果。

clip_image012clip_image014

就是想要的效果。目前这些面板可以全部折叠起来,如果你想让至少一个展开。一个简单的方法就是。当展开后,把Expander的Header里的ToggleButton禁用。

再常见一些的需求就是这个面板的展开过程要有动画什么的。这里就不再演示了。

目前为到止,写在博客里的自定义Panel就有仨了。其实我想说的是,WPF自带的几个Panel的功能实在是很基本的。 了解如何实现自定义Panel,对于实现一些常见的功能还是很有帮助的。

posted on 2010-02-12 15:42  南柯之石  阅读(8580)  评论(8编辑  收藏  举报

导航