最近做控件上了瘾,现在把做的一个类似于QQ面板的控件放上来。

【分析】

从整体来看,这个控件应该同ListBox,ListView这类控件一样,是一个ItemsControl,而中间的项,就是它的Item。

因此,为了完成一个这样的控件,至少需要两个东西:

GroupShelf:也就是充当容器角色的控件

GroupShelfItem:即这个控件中的项

其中,GroupShelf需要保证某项的展开同时,其他项被折叠。而GroupShelfItem需要提供Header和Content,同时,需要能支持展开的空能。

【控件的实现】

GroupShelfItem

首先,我们从GroupShelfItem入手,因为它比较单纯,在HeaderedContentControl的基础上提供展开,收缩功能即可:

Code
    /// <summary>
    /// GroupShelfItem
    /// </summary>
    public class GroupShelfItem : HeaderedContentControl
    {
        #region IsExpanded

public bool IsExpanded
        {
            get { return (bool)GetValue(IsExpandedProperty); }
            set { SetValue(IsExpandedProperty, value); }
        }

// Using a DependencyProperty as the backing store for IsSelected.  This enables animation, styling, binding, etc
        public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(
            "IsExpanded", typeof(bool), typeof(GroupShelfItem), new PropertyMetadata(false, new PropertyChangedCallback(OnIsExpandedChanged)));

private static void OnIsExpandedChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            GroupShelfItem item = sender as GroupShelfItem;
            if (item != null)
            {
                item.OnIsExpandedChanged(e);
            }
        }

protected virtual void OnIsExpandedChanged(DependencyPropertyChangedEventArgs e)
        {
            bool newValue = (bool)e.NewValue;

if (newValue)
            {
                this.OnExpanded();
            }
            else
            {
                this.OnCollapsed();
            }
        }

#endregion

#region Selection Events

/// <summary>
        /// Raised when selected
        /// </summary>
        public event RoutedEventHandler Expanded
        {
            add { AddHandler(ExpandedEvent, value); }
            remove { RemoveHandler(ExpandedEvent, value); }
        }

public static RoutedEvent ExpandedEvent = EventManager.RegisterRoutedEvent(
            "Expanded", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(GroupShelfItem));

/// <summary>
        /// Raised when unselected
        /// </summary>
        public event RoutedEventHandler Collapsed
        {
            add { AddHandler(CollapsedEvent, value); }
            remove { RemoveHandler(CollapsedEvent, value); }
        }

public static RoutedEvent CollapsedEvent = EventManager.RegisterRoutedEvent(
            "Collapsed", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(GroupShelfItem));

protected virtual void OnExpanded()
        {
            GroupShelf parentGroupShelf = this.ParentGroupShelf;
            if (parentGroupShelf != null)
            {
                parentGroupShelf.ExpandedItem = this;
            }
            RaiseEvent(new RoutedEventArgs(ExpandedEvent, this));
        }

protected virtual void OnCollapsed()
        {
            RaiseEvent(new RoutedEventArgs(CollapsedEvent, this));            
        }

#endregion

#region ExpandCommand

public static RoutedCommand ExpandCommand = new RoutedCommand("Expand", typeof(GroupShelfItem));

private static void OnExecuteExpand(object sender, ExecutedRoutedEventArgs e)
        {
            GroupShelfItem item = sender as GroupShelfItem;
            if (!item.IsExpanded)
            {
                item.IsExpanded = true;
            }
        }

private static void CanExecuteExpand(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = sender is GroupShelfItem;
        }

#endregion

public GroupShelf ParentGroupShelf
        {
            get { return ItemsControl.ItemsControlFromItemContainer(this) as GroupShelf; }
        }

static GroupShelfItem()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(GroupShelfItem), new FrameworkPropertyMetadata(typeof(GroupShelfItem)));

CommandBinding expandCommandBinding = new CommandBinding(ExpandCommand, OnExecuteExpand, CanExecuteExpand);
            CommandManager.RegisterClassCommandBinding(typeof(GroupShelfItem), expandCommandBinding);
        }        
    }

默认的Style

Code
<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:GroupShelfDemo.Controls">
    <Style TargetType="{x:Type local:GroupShelfItem}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:GroupShelfItem}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <DockPanel>
                            <Button DockPanel.Dock="Top"
                                          Content="{TemplateBinding Header}"
                                          ContentTemplate="{TemplateBinding HeaderTemplate}"
                                          ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
                                          ContentStringFormat="{TemplateBinding HeaderStringFormat}"
                                          Command="{Binding Source={x:Static local:GroupShelfItem.ExpandCommand}}"/>
                            <ContentPresenter x:Name="ContentHost" DockPanel.Dock="Bottom"
                                              Content="{TemplateBinding Content}"
                                              ContentTemplate="{TemplateBinding ContentTemplate}"
                                              ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
                                              ContentStringFormat="{TemplateBinding ContentStringFormat}">
                                <ContentPresenter.LayoutTransform>
                                    <ScaleTransform x:Name="ContentHostHeightTransform" ScaleY="0.0"/>
                                </ContentPresenter.LayoutTransform>
                            </ContentPresenter>
                        </DockPanel>
                    </Border>
                    <ControlTemplate.Resources>
                        <Storyboard x:Key="OnExpanded">
                            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                                           Storyboard.TargetName="ContentHostHeightTransform" 
                                                           Storyboard.TargetProperty="ScaleY">
                                <SplineDoubleKeyFrame KeyTime="00:00:00.08" Value="1"/>
                            </DoubleAnimationUsingKeyFrames>                           
                        </Storyboard>
                        <Storyboard x:Key="OnCollapsed">
                            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                                           Storyboard.TargetName="ContentHostHeightTransform" 
                                                           Storyboard.TargetProperty="ScaleY">
                                <SplineDoubleKeyFrame KeyTime="00:00:00.08" Value="0"/>
                            </DoubleAnimationUsingKeyFrames>                            
                        </Storyboard>
                    </ControlTemplate.Resources>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsExpanded" Value="True">
                            <Trigger.EnterActions>
                               <BeginStoryboard Storyboard="{StaticResource OnExpanded}"/> 
                            </Trigger.EnterActions>
                            <Trigger.ExitActions>
                                <BeginStoryboard Storyboard="{StaticResource OnCollapsed}"/>
                            </Trigger.ExitActions>
                        </Trigger>                         
                    </ControlTemplate.Triggers>                     
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

GroupShelfItem提供了一个Command来操作它的展开和收缩。同时,在Expand的时候通知GroupShelf处理。在默认的控件模板中通过按钮来触发这个Command。

GroupShelfPanel

GroupShelf的主要工作就是维护GroupShelfItem展开和收缩时的状态处理。但是,按照WPF的方式,这个布局的工作不应该由它来完成,而是由我们提供一个ItemsPanel给它。所以,在GroupShelf之前,GroupShelfPanel应运而生。

写一个Panel最重要的工作就是重载MeasureOverride和ArrangeOverride两个方法。分析GroupShelfPanel的行为,其实是“指定的孩子填充剩余空间”。就系统提供的Panel来说,DockPanel跟它的行为是最接近的,因为DockPanel提供了LastChildFill的行为。既然如此,我们就打开Reflector,从DockPanel里面“借”点代码过来用用:

Code
    public class GroupShelfPanel : Panel
    {
        /// <summary>
        /// 要填充的孩子
        /// </summary>
        public UIElement ChildToFill
        {
            get { return (UIElement)GetValue(ChildToFillProperty); }
            set { SetValue(ChildToFillProperty, value); }
        }

public static readonly DependencyProperty ChildToFillProperty = DependencyProperty.Register(
            "ChildToFill", typeof(UIElement), typeof(GroupShelfPanel),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure));

protected override Size ArrangeOverride(Size arrangeSize)
        {
            UIElementCollection internalChildren = base.InternalChildren;

int count = internalChildren.Count;

// 未指定ChildToFill,则最后一个child填充
            int childToFillIndex = ChildToFill == null ? count - 1 : internalChildren.IndexOf(ChildToFill);

double y = 0.0;

Rect rectForFill = new Rect(0, 0, arrangeSize.Width, arrangeSize.Height);

if (childToFillIndex != -1)
            {
                // 正序排列ChildToFill之前的元素
                for (int i = 0; i < childToFillIndex + 1; i++)
                {
                    UIElement element = internalChildren[i];
                    if (element != null)
                    {
                        Size desiredSize = element.DesiredSize;
                        Rect finalRect = new Rect(0, y, Math.Max(0.0, arrangeSize.Width), Math.Max(0.0, arrangeSize.Height - y));
                        if (i < childToFillIndex)
                        {
                            finalRect.Height = desiredSize.Height;
                            y += desiredSize.Height;
                            element.Arrange(finalRect);
                        }
                        else
                        {
                            // 留给剩下的元素的面积
                            rectForFill = finalRect;
                        }
                    }
                }

y = 0.0;

// 逆序排列ChildToFill之后的元素(包括ChildToFill)
                for (int i = count - 1; i > childToFillIndex; i--)
                {
                    UIElement element = internalChildren[i];
                    if (element != null)
                    {
                        Size desiredSize = element.DesiredSize;
                        Rect finalRect = new Rect(0, arrangeSize.Height - y - desiredSize.Height, Math.Max(0.0, arrangeSize.Width), Math.Max(0.0, desiredSize.Height));

element.Arrange(finalRect);
                        y += desiredSize.Height;
                    }
                }
                rectForFill.Height -= y;
                InternalChildren[childToFillIndex].Arrange(rectForFill);
            }

return arrangeSize;
        }

protected override Size MeasureOverride(Size constraint)
        {
            UIElementCollection internalChildren = base.InternalChildren;
            double num = 0.0;
            double num2 = 0.0;
            double num3 = 0.0;
            double num4 = 0.0;
            int index = 0;
            int count = internalChildren.Count;

while (index < count)
            {
                UIElement element = internalChildren[index];
                if (element != null)
                {
                    Size availableSize = new Size(Math.Max((double)0.0, (double)(constraint.Width - num3)), Math.Max((double)0.0, (double)(constraint.Height - num4)));
                    element.Measure(availableSize);
                    Size desiredSize = element.DesiredSize;

num = Math.Max(num, num3 + desiredSize.Width);
                    num4 += desiredSize.Height;

}
                index++;
            }
            num = Math.Max(num, num3);
            return new Size(num, Math.Max(num2, num4));

}

当然,改动还是比较大的。主要的改动集中在ArrangeOverride上。排列的逻辑应该是:把“要填充的孩子”之前的元素从上到下排列,把“要填充的孩子”之后的元素从下往上排列。剩余的空间都留给这个“要填充的孩子”。而对于MeasureOverride,我们要做的就是去掉DockPanel里面对于Left和Right的判断。(当然,如果考虑到以后要提供两种布局的方向:Horizontal和Vertical的话,还是需要保留一下的)

GroupShelf

上面两个都完成后,GroupShelf的工作就很简单了。它就是在Item的Expand发生变化时,通知别的Item Collapse,然后通知GroupPanel去重新布局。

Code
    /// <summary>
    /// GroupShelf
    /// </summary>
    [TemplatePart(Name = "PART_ItemsHost", Type = typeof(GroupShelfPanel))]
    public class GroupShelf : ItemsControl
    {
        private GroupShelfPanel _itemsHost;

#region ExpandedItem

public object ExpandedItem
        {
            get { return (object)GetValue(ExpandedItemProperty); }
            set { SetValue(ExpandedItemProperty, value); }
        }

// Using a DependencyProperty as the backing store for SelectedItem.  This enables animation, styling, binding, etc
        public static readonly DependencyProperty ExpandedItemProperty = DependencyProperty.Register(
            "ExpandedItem", typeof(object), typeof(GroupShelf),
            new UIPropertyMetadata(null, new PropertyChangedCallback(OnExpandedItemChanged)));

private static void OnExpandedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            GroupShelf shelf = sender as GroupShelf;
            if (shelf != null)
            {
                shelf.OnExpandedItemChanged(e.OldValue, e.NewValue);
            }
        }

protected virtual void OnExpandedItemChanged(object oldValue, object newValue)
        {
            GroupShelfItem oldItem = this.ItemContainerGenerator.ContainerFromItem(oldValue) as GroupShelfItem;
            GroupShelfItem newItem = this.ItemContainerGenerator.ContainerFromItem(newValue) as GroupShelfItem;
            if (oldItem != null)
            {
                oldItem.IsExpanded = false;
            }
            if (newItem != null)
            {
                if (this._itemsHost != null)
                {
                    this._itemsHost.ChildToFill = newItem;
                }
            }
        }

#endregion
       
        static GroupShelf()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(GroupShelf), new FrameworkPropertyMetadata(typeof(GroupShelf)));
        }

#region Overrides

protected override void ClearContainerForItemOverride(DependencyObject element, object item)
        {
            base.ClearContainerForItemOverride(element, item);
        }

protected override DependencyObject GetContainerForItemOverride()
        {
            return new GroupShelfItem();
        }

protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is GroupShelfItem;
        }

public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            this._itemsHost = GetTemplateChild("PART_ItemsHost") as GroupShelfPanel;
        }

#endregion
    }

一个需要注意的地方是它的模板:

Code
<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:GroupShelfDemo.Controls">
    <Style TargetType="{x:Type local:GroupShelf}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:GroupShelf}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <local:GroupShelfPanel x:Name="PART_ItemsHost" IsItemsHost="True"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

我放置了一个GroupPanel,并且指定了它是ItemsHost,而不是放置ItemsPresenter。这一点是比较重要的,因为只有当ItemsPanel是GroupPanel的时候,才能够有指定孩子填充的效果,因此这个GroupPanel需要是TemplatePart。如果修改了这个模板,换成别的Panel,比如StackPanel,则行为会有所不同。

【使用该控件】

这个控件的使用是非常简单的:

Code
        <l:GroupShelf>
            <l:GroupShelfItem Header="我的好友">
                <ListBox>
                    <TextBlock Text="好友1"/>
                    <TextBlock Text="好友2"/>
                    <TextBlock Text="好友3"/>
                    <TextBlock Text="好友4"/>
                </ListBox>
            </l:GroupShelfItem>
            <l:GroupShelfItem Header="我的同学">
                <ListBox>
                    <TextBlock Text="同学1"/>
                    <TextBlock Text="同学2"/>
                    <TextBlock Text="同学3"/>
                    <TextBlock Text="同学4"/>
                </ListBox>
            </l:GroupShelfItem>
            <l:GroupShelfItem Header="我的同事">
                <ListBox>
                    <TextBlock Text="同事1"/>
                    <TextBlock Text="同事2"/>
                    <TextBlock Text="同事3"/>
                    <TextBlock Text="同事4"/>
                </ListBox>
            </l:GroupShelfItem>
            <l:GroupShelfItem Header="我的家人">
                <ListBox>
                    <TextBlock Text="家人1"/>
                    <TextBlock Text="家人2"/>
                    <TextBlock Text="家人3"/>
                    <TextBlock Text="家人4"/>
                </ListBox>
            </l:GroupShelfItem>
            <l:GroupShelfItem Header="我的老师">
                <ListBox BorderThickness="1" BorderBrush="Black">
                    <TextBlock Text="老师1"/>
                    <TextBlock Text="老师2"/>
                    <TextBlock Text="老师3"/>
                    <TextBlock Text="老师4"/>
                </ListBox>
            </l:GroupShelfItem>
        </l:GroupShelf>

代码下载http://files.cnblogs.com/RMay/AccordianDemo.rar

注:我用的是.Net 3.5 Sp1,如果是3.0-3.5请删除模板中的ContentStringFormat相关的东西

修正一下:

在GroupShelf的模板中,直接写

<local:GroupShelfPanel IsItemsHost="True" ChildToFill="{TemplateBinding ExpandedItem}"/>

即可,而在GroupShelf中的相关字段和方法都可以删除,UI和逻辑解耦。

转载于:https://www.cnblogs.com/RMay/archive/2008/08/22/1273927.html

【WPF】一个类似于QQ面板的GroupShelf控件相关推荐

  1. 2017-5-5 QQ面板 (用户控件、timer控件,轮询实现聊天功能)

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...

  2. 【转】WPF从我炫系列3---内容控件的用法

    今天我来给大家讲解WPF中内容控件的用法,在WPF中的内容控件,通俗的讲,是指具有Content属性的控件,在content属性里面可以嵌套放置任意其他类型的控件,但是Content只能接受单个元素, ...

  3. 【愚公系列】2023年07月 WPF+上位机+工业互联 002-WPF布局控件

    文章目录 前言 一.WPF布局控件 1.无边框设计 2.理解布局 2.1 WPF的布局处理 2.1 布局原则 2.3 布局过程 3.布局控件 3.1 Grid控件 3.1.1 属性 3.1.2 案例 ...

  4. WPF 使用依赖属性(DependencyProperty) 定义用户控件中的Image Source属性

    原文:WPF 使用依赖属性(DependencyProperty) 定义用户控件中的Image Source属性 如果你要自定义一个图片按钮控件,那么如何在主窗体绑定这个控件上图片的Source呢? ...

  5. WPF中自定义的DataTemplate中的控件,在Window_Loaded事件中加载机制初探

    原文:WPF中自定义的DataTemplate中的控件,在Window_Loaded事件中加载机制初探 最近因为项目需要,开始学习如何使用WPF开发桌面程序.使用WPF一段时间之后,感觉WPF的开发思 ...

  6. WPF编程,通过KeyFrame 类型制作控件线性动画的一种方法。

    WPF编程,通过KeyFrame 类型制作控件线性动画的一种方法. 原文: WPF编程,通过KeyFrame 类型制作控件线性动画的一种方法. 版权声明:我不生产代码,我只是代码的搬运工. https ...

  7. 想建一个带分隔条的label 控件;

    想建一个带分隔条的label 控件: Delphi / Windows SDK/API http://www.delphi2007.net/DelphiBase/html/delphi_2006120 ...

  8. 一个自定义的安卓验证码输入框控件、银行卡归属类型查询

    一个自定义的安卓验证码输入框控件.银行卡归属类型查询. GitHub:https://github.com/longer96/VerifyCode Dependency Gradle dependen ...

  9. 一个类似于QQ语音聊天时的拖拽移动悬浮小球

    原文出处: Booooooooom    闲来无事,分享一个最近在某个地方借鉴的一个demo(原谅我真的忘了在哪里看到的了,不然也就贴地址了)这个demo的逻辑思路并不是很难,推敲一下,很快就能理解, ...

最新文章

  1. 百度:2020年十大科技趋势
  2. 面试:说说你对 ThreadLocal 的认识?
  3. 如何利用TensorFlow.js部署简单AI版「你画我猜」
  4. 复习----使用链表实现栈(后进先出)及迭代
  5. 深入学习二叉树(二) 线索二叉树
  6. 大数据开发初学者学习路线_初学者的Web开发路线图
  7. maya为什么不能导出fbx_Maya在操作中最容易出现的几个问题,现在注意还来得及...
  8. 线程安全的几种单例模式
  9. 学习PLC到底要不要买PLC?
  10. pic单片机c语言計數,单片机教程:PIC单片机C语言程序设计(三)
  11. @【基础测绘计算】(坐标正反算)
  12. 教你打造 Win7 中的高清设备图标
  13. 荣耀畅玩5a android5.0,华为荣耀畅玩5A有几个版本?华为荣耀5A各版本区别对比介绍...
  14. 计算机机房通风,机房为什么要装通风系统?
  15. PostgreSQL12.3——pgAdmin4表格的创建
  16. C语言课设销售管理系统设计
  17. RHCA-红帽认证架构师
  18. stm32中断源有哪些_143条 超详细整理STM32单片机学习笔记(必看)
  19. 手把手玩转KVM虚拟化--KVM网络管理
  20. 【OCR】文字检测:传统算法、CTPN、EAST

热门文章

  1. CMSimple内容管理系统
  2. PHP常见缓存技术分析,让重复的调用缓存以加快速度
  3. Node.js: 如何退出node命令或者node server
  4. Swift中文教程(九) 类与结构
  5. Objective-C 2.0 with Cocoa Foundation --- 2,从Hello,World!开始
  6. Python_多项式拟合
  7. 神奇的python(二)之python打包成应用程序
  8. spring IOC容器 Bean 管理——基于注解方式
  9. Python——pip安装报错:is not a supported wheel on this platform
  10. 【AI视野·今日CV 计算机视觉论文速览 第202期】Thu, 20 May 2021