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

 

【WPF】一个类似于QQ面板的GroupShelf控件【WPF】一个类似于QQ面板的GroupShelf控件

  

【分析】

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

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

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

GroupShelfItem:即这个控件中的项

 

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

 

【控件的实现】

 

GroupShelfItem

 

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

 


    /// 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【WPF】一个类似于QQ面板的GroupShelf控件
        public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(
            
"IsExpanded"typeof(bool), typeof(GroupShelfItem), new PropertyMetadata(falsenew 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(thisas GroupShelf; }
        }

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

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

 

默认的Style

 

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里面“借”点代码过来用用:

 

 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(00, 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去重新布局。

 

 


    /// 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【WPF】一个类似于QQ面板的GroupShelf控件
        public static readonly DependencyProperty ExpandedItemProperty = DependencyProperty.Register(
            
"ExpandedItem"typeof(object), typeof(GroupShelf),
            
new UIPropertyMetadata(nullnew 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
    }

 

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

 

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,则行为会有所不同。

 

【使用该控件】

   

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

 


            <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>

 

代码下载https://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和逻辑解耦。

相关文章:

  • 2021-11-16
  • 2021-06-26
  • 2022-12-23
  • 2022-12-23
  • 2021-06-09
  • 2022-12-23
  • 2021-08-28
  • 2022-12-23
猜你喜欢
  • 2022-12-23
  • 2022-12-23
  • 2022-02-02
  • 2022-02-19
  • 2021-08-28
  • 2022-12-23
  • 2021-08-31
相关资源
相似解决方案