程序员的知识教程库

网站首页 > 教程分享 正文

WPF自定义Accordion(wpf自定义控件)

henian88 2024-08-26 15:32:56 教程分享 5 ℃ 0 评论

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


具体效果如下:



非常好用!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表