WPF 简易手风琴 (ListBox+Expander)
概述
之前听说很多大神的成长之路,几乎都有个习惯——写博文,可以有效的对项目进行总结、从而提高开发的经验。所以初学WPF的我想试试,顺便提高一下小学作文的能力。O(∩_∩)O哈哈~
读万卷书不如行万里路,实践是最好的导师!最近在学习WPF,也尝试着做了一些小Demo,但并没有真正的使用WPF的开发模式——数据推动UI,最近偶然的机会也是工作需求,就尝试着写了一个简易的手风琴控件,
因为初学的原因,可能在逻辑上,代码上有些欠缺,还请大神们多多指点,在这里先感谢各位!(下面是效果图)
思路
剖析效果,拆分控件,我当时的想法是Grid容器+基本控件进行手绘、最后在添加一些展开收起的动画,想法很美好,现实很残酷!后来询问了一下大神,给出的建议是ListBox+Expander实现,就这样在大神们的指点下开始了。
数据结构
ExpanderClass类:标题图片、标题名称、按钮图片集合 ; ImgUrlClass类:按钮图片。
public class DataSourceClass { /// <summary> /// 数据源 /// </summary> public static List<ExpanderClass> GetDateSource() { List<ExpanderClass> exLst = new List<ExpanderClass>(); List<ImgUrlClass> lst = new List<ImgUrlClass>(); for (int i = 1; i < 10; i++) { lst.Add(new ImgUrlClass() { ImageUrl = string.Format("Images/h{0}.png", i) }); } exLst.Add(new ExpanderClass() { Title = "我是第一行哦!", ImgUrl = "Images/Left.png", ImgLst = lst }); lst = new List<ImgUrlClass>(); for (int i = 1; i < 10; i++) { lst.Add(new ImgUrlClass() { ImageUrl = string.Format("Images/h1{0}.png", i) }); } exLst.Add(new ExpanderClass() { Title = "我是第二行哦!!", ImgUrl = "Images/Right.png", ImgLst = lst }); lst = new List<ImgUrlClass>(); for (int i = 1; i < 10; i++) { lst.Add(new ImgUrlClass() { ImageUrl = "Images/h10.png" }); } exLst.Add(new ExpanderClass() { Title = "我是第三行哦!!!", ImgUrl = "Images/Up.png", ImgLst = lst }); return exLst; } } public class ExpanderClass { /// <summary> /// 标题 /// </summary> public string Title { get; set; } /// <summary> /// 标题图片 /// </summary> public string ImgUrl { get; set; } /// <summary> /// 按钮图片集合 /// </summary> public List<ImgUrlClass> ImgLst { get; set; } public ExpanderClass() { Title = string.Empty; ImgUrl = string.Empty; } } public class ImgUrlClass { public ImgUrlClass() { ImageUrl = string.Empty; } /// <summary> /// 按钮图片 /// </summary> public string ImageUrl { get; set; } }
Expander
首先添加一个WPF用户控件AccordionControl 然后添加一个Expander控件,然后在里面添加个Image后运行看一下效果
<Expander x:Name="expander" Header="Expander" > <Grid Background="#FFE5E5E5"> <Image Source="Image/Button/h1.png" /> </Grid> </Expander>
这样一个展开收起的Expander 就OK了,接下里根据设计需求分析,我们需要修改Expander的 Header、Content 两部分。
Header(头部):修改背景色、添加标题图片、标题。
Content(内容):多个图片按钮组成(这里需要思考一下,结果集是多张图片,所以需要循环加载图片,所以这里使用了ListBox控件)。
<Expander> <Expander.Header> <StackPanel Orientation="Horizontal" Background="#3399ff" > <Image Source="Image/Button/h1.png" Height="16"/> <TextBlock Text="我是标题哦" VerticalAlignment="Center"/> </StackPanel> </Expander.Header> <Expander.Content > <ListBox ItemsSource={binding}> <ListBox.ItemTemplate> <DataTemplate> <Image Source="Image/Button/h1.png" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Expander.Content> </Expander>
绑定数据源运行后
距离我们的设计是不是进了一步,我们需要思考一下 设计图给出的内容中按钮是 横向 展示,并且根据宽度自动换行? WPF中我知道的可以实现此功能的控件 WrapPanel、TextBlock,WrapPanel更好用,我们来修改下ListBox中ItemsPanel模板,并且将ListBox水平滚动条取消。
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled" > <ListBox.ItemsPanel> <ItemsPanelTemplate> <WrapPanel Orientation="Horizontal" Background="Transparent" /> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.ItemTemplate> <DataTemplate> <Image Source="{Binding ImageUrl}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
运行,效果不错吧。
这样一个简单的Expander完成了,实际工作中我们可能需要多个这样的Expander组合使用,很简单,ListBox嵌套Expander 进去就可以了。
完善功能
运行Demo,可能会发现一个问题,点击第一行的Expander时候 其它的并没有收起,怎么实现点击其中一个让其他自动收起呢?
方案一:RadioButton; 单选的效果是RadioButton被分配到相同的组中 GroupName ,结合Demo 我们可以将Expander封装到RadionButton中,然后给GroupName赋值 这样就可以实现效果。
方案二:ListBox的ListBoxItem.IsSelected 是否选中; Expander中IsExpanded属性的意思是内容窗口是否可见,当我们选择一个ListBoxItem时将IsDelected绑定到IsExpanded中就可以实现。
最初,我是使用RadionButton实现的,后大神提出建议为何不试试ListBoxItem呢?所以我修改了源码。
<ListBox x:Name="ItemBox" ItemsSource="{Binding}" Width="400" > <ListBox.ItemTemplate> <DataTemplate> <Expander Width="{Binding Path=Width, ElementName=ItemBox}" Tag="{Binding}" IsExpanded="{Binding RelativeSource ={ RelativeSource Mode=FindAncestor, AncestorType=ListBoxItem}, Path=IsSelected}" > <Expander.Header> <StackPanel Orientation="Horizontal" Background="#3399ff" Width="{Binding Path=Width, ElementName=ItemBox}" > <Image Source="{Binding RelativeSource ={ RelativeSource Mode=FindAncestor, AncestorType=Expander}, Path=Tag.ImgUrl}" Height="16" Width="16" VerticalAlignment="Center"/> <TextBlock VerticalAlignment="Center" Text="{Binding RelativeSource ={ RelativeSource Mode=FindAncestor, AncestorType=Expander}, Path=Tag.Title}" /> </StackPanel> </Expander.Header> <Expander.Content > <ListBox ItemsSource="{Binding RelativeSource ={ RelativeSource Mode=FindAncestor, AncestorType=Expander}, Path=Tag.ImgLst}" > <ListBox.ItemsPanel> <ItemsPanelTemplate> <WrapPanel Orientation="Horizontal" HorizontalAlignment="Center" /> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.ItemTemplate> <DataTemplate> <Image Source="{Binding ImageUrl}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Expander.Content> </Expander> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
解析下数据绑定:
WPF基础Binding(绑定)这里用到两种方式(概念上的可以参考百度这里不再熬述):
1、ElementName指定Source:在C#代码中可以直接把对象作为Source赋值给Binding,但是XAML无法访问对象,只能使用Name属性来找到对象。
Width="{Binding Path=Width, ElementName=ItemBox}"
2、RelataveSource:通过Binding的RelataveSource属性相对的指定Source:当控件需要关注自己的、自己容器的或者自己内部某个属性值就需要使用这种办法。
IsExpanded="{Binding RelativeSource ={ RelativeSource Mode=FindAncestor, AncestorType=ListBoxItem}, Path=IsSelected}"
* 这里有个注意点,Expander.Header中并不能直接得到数据源 我这里是将数据源绑定到Expander.Tag中 然后通过 RelataveSource 向上查找的方式获取数据源
<TextBlock VerticalAlignment="Center" Text="{Binding RelativeSource ={ RelativeSource Mode=FindAncestor, AncestorType=Expander}, Path=Tag.Title}" />
这样整体的数据绑定就完成了。
现在思考一下这样个需求:当我们点击按钮后更改背景图片,点击其他按钮后,点击过的按钮还原?
分析:根据需求分解两个动作效果.
1、按钮之间的切换并且还原样式:这里的效果类似于之前的Expander 之间的展开与闭合,所以这里依然使用 RadioButton 来实现,RadionButton改变样式成为图片按钮 。
2、点击按钮更改背景图片:根据RadionButton的IsChecked属性来实现,将Image 的Source 与IsChecked 关联起来,通过转换器根据相应的值返回不同的图片。
<RadioButton x:Name="rdoImg" GroupName="rdoImgGroup" Cursor="Hand" Width="80" Height="80" > <RadioButton.Template> <ControlTemplate> <Image> <Image.Source> <MultiBinding Converter="{StaticResource IsDataConverter}" > <Binding Path="IsChecked" ElementName="rdoImg" /> <Binding Path="ImageUrl" /> </MultiBinding> </Image.Source> </Image> </ControlTemplate> </RadioButton.Template> </RadioButton>
MultiBinding:多番绑定
在开发的过程中遇到了一个很有意思的事情,我为了方便直接将整个实体绑定进去,然后在转换器中进行判断可以得到图片进行处理,运行后但图片没变化。难道转换器无效,IsChecked没变?
带着疑问我修改了一下,只绑定IsChecked 看看到底是否发生变化,运行,结果发生变化,图片也变了(这里修改了转换器 返回一个固定图片)。
我个人的理解 Binding 应该只针对一个属性才有效,可是我想把图片路径也传过去,这样更好的处理,那么MultiBinding就用到了 。
这里需要注意一点:MultiBinding 转换器 继承 IMultiValueConverter 接口,Binding 继承 IValueConverter 接口。
MultiBinding : IMultiValueConverter
Binding : IValueConverter
这样需求就实现了,运行程序
当我们切换Expander时,发现个问题按钮并没有还原?
思路:在点击Expander时,将RadioButton的IsChecked改为False就可以了,这是因为我们已经将Image的Source与RadioButton的IsChecked关联。
所以只要将RadioButton的IsChecked 与 Expander的IsExpanded 关联就可以。
<RadioButton x:Name="rdoImg" GroupName="rdoImgGroup" Cursor="Hand" Width="80" Height="80" IsChecked="{Binding Path=IsExpanded,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Expander},Converter={StaticResoce IsCheckedConvert},Mode=OneWay}" >
这里需要注意一点 IsChecked 与 IsExpanded 的绑定关系是单向的 Mode=OneWay,这样保证每次点击Expander 返回的都是False,不然会报错,因为RadionButton 同一组中不可能同时选中!
好了除了样式外功能实现完毕,当然还可以继续拓展O(∩_∩)O哈哈~!
样式
Expande样式模板分为三个部分:
Expander-Header:一个写好样式的 ToggleButton。
Expander-Content: Border是内容部分 <ContentPresenter /> 这句话千万不要遗忘,不然什么都不会显示!
Expander-Triggers: 动画效果。
*这里有个注意点,Header 与 Content 一定要放在 StackPanel 内,不然运行后Expander会重叠。
样式模板具体可以百度查询,或者用Blend查看样式源码,这里不再熬述。
<!--ToggleButton样式代码:--> <Style x:Key="ToggleButtonStyle" TargetType="{x:Type ToggleButton}"> <Setter Property="FocusVisualStyle" Value="{x:Null}"/> <Setter Property="Width" Value="{Binding Path=Width, ElementName=ItemBox}"/> <Setter Property="Height" Value="35" /> <Setter Property="Background" Value="{StaticResource OrangeG}" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="FontSize" Value="16" /> <Setter Property="Foreground" Value="White" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ToggleButton}"> <Canvas Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True"> <Image Source="{Binding RelativeSource ={ RelativeSource Mode=FindAncestor, AncestorType=Expander}, Path=Tag.ImgUrl}" Height="16" Canvas.Left="5" Canvas.Top="8" /> <TextBlock Text="{Binding RelativeSource ={ RelativeSource Mode=FindAncestor, AncestorType=Expander}, Path=Tag.Title}" Canvas.Left="25" Canvas.Top="8" Foreground="{TemplateBinding Foreground}"/> <TextBlock Text="+" Foreground="White" Canvas.Top="6" Canvas.Right="10" FontSize="{TemplateBinding FontSize}" x:Name="txtSymbol"/> <ContentPresenter /> </Canvas> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="True"> <Setter Property="Background" Value="#3399ff"/> <Setter Property="Text" TargetName="txtSymbol" Value="-"/> </Trigger> <Trigger Property="IsChecked" Value="False"> <Setter Property="Background" Value="{StaticResource OrangeG}"/> <Setter Property="Text" TargetName="txtSymbol" Value="+"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <!--Expander样式代码:--> <Style x:Key="ExpanderStyle" TargetType="{x:Type Expander}"> <Setter Property="Background" Value="Transparent"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Expander}"> <StackPanel Background="{TemplateBinding Background}" > <ToggleButton x:Name="HeaderSite" Style="{DynamicResource ToggleButtonStyle}" IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" /> <Border x:Name="ExpandSite" Visibility="Collapsed" Canvas.Top="40" Focusable="false" BorderThickness="1" Width="{Binding ElementName=HeaderSite,Path=Width}" HorizontalAlignment="Center" > <ContentPresenter /> </Border> </StackPanel> <ControlTemplate.Triggers> <Trigger Property="IsExpanded" Value="true"> <Setter Property="Visibility" TargetName="ExpandSite" Value="Visible"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
最后调用样式
<Expander Style="{DynamicResource ExpanderStyle}"
这样 简易的手风琴就完成了,但是这里还是有个小问题,就是内容模板的高度没有办法拉伸,以及WrapPanel内会出现左右边距没有办法居中,如果有知道解决办法的大神们请告知谢谢!!!
结束语
第一次写博文,终于体会其中的奥妙之处,重新梳理下思路,对自己进行一次总结,进而增加深刻的印象,这种感觉棒棒哒,在这里感谢为我指点的大神们,同时也希望大家为我指点其中的不足之处,进而改之,让我可以快速的成长,再次感谢各位,谢谢!!!
(PS:话说怎么样可以让博文的样式更好看一些?)