【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   南柯之石  阅读(8587)  评论(8编辑  收藏  举报

编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库

导航

< 2010年2月 >
31 1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 1 2 3 4 5 6
7 8 9 10 11 12 13

统计

点击右上角即可分享
微信分享提示