用过easyui的都知道,easyui有个很好用的控件:Accordion。该控件可以折叠,每一个折叠项中可以放入任何内容的东西,这个控件在做后台管理界面时很实用。WPF并没有该控件,但我们可以通过自己的方式去实现一个。
Easyui中的Accordion长这样:
首先我们来梳理下原理:
1. 一个Accordion可以包含多个折叠项(即AccordionItem),表明Accordion应该是一个ItemsControl,Accordion可以像ComboBox一样被选中,被选中的子项展开,未被选中的折叠,所以Accordion可以直接继承Selector(备注:Selector继承ItemsControl)
2. Accordion的子项AccordionItem可以被折叠,所以AccordionItem可以直接继承Expander
难点:
1. AccordionItem(Expander)样式调整
2. 一个Expander展开时,其它的Exanpder要折叠
3. Accordion在发生尺寸调整时,滚动条显示异常
先实现AccordionItem,后台代码:
public class AccordionItem : Expander
{
}
直接继承,不需要额外代码,这里说明下,WPF的Expander控件的Header,是一个Object的类型,是可以放任何东西的,所以不用为了放图片之类的单独注册个类似Icon类的属性
前台代码:
<Style x:Key="AccordionExpanderHeaderFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Border>
<Rectangle Margin="0" SnapsToDevicePixels="true" Stroke="Black" StrokeThickness="1" StrokeDashArray="1 2"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="AccordionExpanderDownHeaderStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Background" Value="#34495e"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="{TemplateBinding Background}" BorderThickness="0">
<Grid SnapsToDevicePixels="False" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="19"/>
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="content" Margin="4,0,0,0" Grid.Column="0" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Center"/>
<Grid Grid.Column="1" >
<Grid.LayoutTransform>
<TransformGroup>
<TransformGroup.Children>
<TransformCollection>
<RotateTransform Angle="180"/>
</TransformCollection>
</TransformGroup.Children>
</TransformGroup>
</Grid.LayoutTransform>
<Path x:Name="arrow" Data="M 1,4.5 L 4.5,1 L 8,4.5" HorizontalAlignment="Center" SnapsToDevicePixels="false" Stroke="White" StrokeThickness="2" VerticalAlignment="Center"/>
</Grid>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="true">
<Setter Property="Data" TargetName="arrow" Value="M 1,1.5 L 4.5,5 L 8,1.5"/>
<!--<Setter Property="Background" Value="{StaticResource WindowBackgroundBrush}"/>-->
<Setter TargetName="content" Property="TextBlock.Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Cursor" Value="Hand"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type local:AccordionItem}">
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:AccordionItem}">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}" CornerRadius="0" SnapsToDevicePixels="true">
<DockPanel>
<ToggleButton x:Name="HeaderSite" ContentTemplate="{TemplateBinding HeaderTemplate}"
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
Content="{TemplateBinding Header}"
DockPanel.Dock="Top"
Foreground="{TemplateBinding Foreground}"
FocusVisualStyle="{StaticResource AccordionExpanderHeaderFocusVisual}"
FontStyle="{TemplateBinding FontStyle}" FontStretch="{TemplateBinding FontStretch}"
FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
MinWidth="0" MinHeight="0" Padding="{TemplateBinding Padding}"
Style="{StaticResource AccordionExpanderDownHeaderStyle}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
<Border Background="#293a4a">
<ContentPresenter Margin="{TemplateBinding Padding}" x:Name="ExpandSite"
DockPanel.Dock="Bottom" Focusable="false"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Visibility="Collapsed" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
</DockPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="true">
<Setter Property="Visibility" TargetName="ExpandSite" Value="Visible"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
调整后的样子如下图:
接下来实现Accordion,该控件的样式不复杂,复杂的是尺寸变化时对展开项高度的计算(有想过使用DockPanel,但还是否决了)
前台样式:
<Style TargetType="{x:Type local:Accordion}">
<Setter Property="Background" Value="#34495e"/>
<Setter Property="BorderBrush" Value="#34495e"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.PanningMode" Value="Both"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Accordion}">
<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="0" SnapsToDevicePixels="true">
<ScrollViewer x:Name="PART_ScrollViewer" Focusable="false" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsGrouping" Value="true"/>
</MultiTrigger.Conditions>
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
后台实现:
代码比较复杂,不去详解具体代码,讲几个重要的点,继承Seletor,需要重写以下几个方法,
1. IsItemItsOwnContainerOverride,校验子项是否为AccordionItem,若不是,则自动包裹
2. GetContainerForItemOverride
3. PrepareContainerForItemOverride
为计算尺寸,注册了AccordionItem_Loaded ,Accordion_SizeChanged两个事件。具体代码如下:
public class Accordion : Selector
{
Dictionary<AccordionItem, DependencyPropertyDescriptor> Children = new Dictionary<AccordionItem, DependencyPropertyDescriptor>();
public Accordion()
{
this.SizeChanged += Accordion_SizeChanged;
}
void Accordion_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (this.IsLoaded)
{
AccordionItem pandedItem = null;
double height = 0;
double total = 0;
foreach (var item in Children)
{
if (item.Key.IsExpanded == true)
{
pandedItem = item.Key;
}
else
{
height += item.Key.ActualHeight;
}
}
if (pandedItem != null)
{
double reslut = (this.ActualHeight - height) > minHeight ? (this.ActualHeight - height) : minHeight;
pandedItem.Height = reslut;
total = height + reslut;
}
if (total > this.ActualHeight)
{
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Visible);
}
else
{
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Disabled);
}
}
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
if (item is AccordionItem)
{
return true;
}
else
{
notItem++;
return false;
}
}
protected override DependencyObject GetContainerForItemOverride()
{
var count = this.Items.Count - this.Items.OfType<AccordionItem>().Count();
AccordionItem expander = new AccordionItem();
expander.Content = null;
Listen(expander);
if (notItem == count)
{
expander.Loaded += AccordionItem_Loaded;
}
return expander;
}
ScrollViewer sv;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
sv = this.GetTemplateChild("PART_ScrollViewer") as ScrollViewer;
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
//if (element is YAccordionItem)
//{
// (element as YAccordionItem).DataContext = item;
//}
base.PrepareContainerForItemOverride(element, item);
}
int notItem = 0;
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
var items = this.Items;
bool isLoaded = this.IsLoaded;
NotifyCollectionChangedAction str = e.Action;
switch (str)
{
case NotifyCollectionChangedAction.Reset:
if (!isLoaded)
{
int i = 0;
AccordionItem temp = null;
foreach (var item in items)
{
AccordionItem expander = null;
if (item is AccordionItem)
{
expander = (AccordionItem)item;
temp = expander;
Listen(expander);
i++;
}
}
if (notItem == 0)
{
if (items != null && i == items.Count && items.Count > 0)
{
temp.Loaded += AccordionItem_Loaded;
}
}
}
break;
case NotifyCollectionChangedAction.Add:
foreach (var item in e.NewItems)
{
AccordionItem expander = null;
if (item is AccordionItem)
{
expander = (AccordionItem)item;
}
Listen(expander);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (var item in e.OldItems)
{
if (item is AccordionItem)
{
RemoveListen((AccordionItem)item);
}
}
break;
}
base.OnItemsChanged(e);
}
/// <summary>
/// 最低阈值
/// </summary>
double minHeight = 150;
void AccordionItem_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
AccordionItem expander = sender as AccordionItem;
double height = 0;
var item = Children.FirstOrDefault(p => p.Key.IsExpanded == false);
var pandedItem = Children.FirstOrDefault(p => p.Key.IsExpanded == true);
if (item.Key != null)
{
height = (item.Key.ActualHeight) * (double)Children.Count;
if (height > this.ActualHeight)
{
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Visible);
if (pandedItem.Key != null)
{
pandedItem.Key.Height = minHeight;
}
}
else
{
if (pandedItem.Key != null)
{
double h = (item.Key.ActualHeight) * (double)(Children.Count - 1) + pandedItem.Key.ActualHeight;
if (h > this.ActualHeight)
{
h = (item.Key.ActualHeight) * (double)(Children.Count - 1) + minHeight;
if (h > this.ActualHeight)
{
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Visible);
pandedItem.Key.Height = minHeight;
}
else
{
pandedItem.Key.Height = this.ActualHeight - (item.Key.ActualHeight) * (double)(Children.Count);
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Disabled);
}
}
else
{
pandedItem.Key.Height = this.ActualHeight - (item.Key.ActualHeight) * (double)(Children.Count - 1);
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Disabled);
}
}
else
{
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Disabled);
}
}
}
oldHeight = item.Key.ActualHeight;
}
private void Listen(AccordionItem expander)
{
DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(AccordionItem.IsExpandedProperty, typeof(AccordionItem));
descriptor.AddValueChanged(expander, new EventHandler(ListenIsExpandedProperty));
Children.Add(expander, null);
}
private void RemoveListen(AccordionItem expander)
{
DependencyPropertyDescriptor descriptor = Children[expander];
descriptor.RemoveValueChanged(expander, new EventHandler(ListenIsExpandedProperty));
Children.Remove(expander);
}
bool isChangeOver = true;
double oldHeight = 0;
private void ListenIsExpandedProperty(object sender, EventArgs e)
{
if (this.IsLoaded && isChangeOver == true)
{
AccordionItem expander = sender as AccordionItem;
if (expander.IsExpanded == true)
{
isChangeOver = false;
double height = 0;
foreach (var item in Children)
{
if (!expander.Equals(item.Key))
{
item.Key.Height = oldHeight;//在布局更新前获得以往高度
item.Key.IsExpanded = false;
}
}
this.UpdateLayout();//强制布局更新,可以获取更新后的高度
foreach (var item in Children)
{
if (!expander.Equals(item.Key))
{
height += item.Key.ActualHeight;
}
}
if ((this.ActualHeight - height) > minHeight)
{
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Disabled);
expander.Height = this.ActualHeight - height;
}
else
{
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Visible);
expander.Height = minHeight;
}
isChangeOver = true;
}
else
{
if (oldHeight > 0)
{
expander.Height = oldHeight;
this.UpdateLayout();//强制布局更新,可以获取更新后的高度
double height = 0;
foreach (var item in Children)
{
height += item.Key.ActualHeight;
}
if (height > this.ActualHeight)
{
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Visible);
}
else
{
ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Disabled);
}
}
}
}
}
}
我们来看下写点代码运行测试:
<Window x:Class="CustomAccordion.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CustomAccordion"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<local:Accordion>
<local:AccordionItem IsExpanded="True" Header="Expander1">
<TextBlock Foreground="White" Margin="20">这是第一个展开项</TextBlock>
</local:AccordionItem>
<local:AccordionItem Header="Expander2">
<TextBlock Foreground="White" Margin="20">This is a beautiful city.</TextBlock>
</local:AccordionItem>
<local:AccordionItem Header="Expander3"></local:AccordionItem>
<local:AccordionItem Header="Expander4"></local:AccordionItem>
<local:AccordionItem Header="Expander5"></local:AccordionItem>
</local:Accordion>
</Grid>
</Window>
具体效果如下:
非常好用!
本文暂时没有评论,来添加一个吧(●'◡'●)