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:话说怎么样可以让博文的样式更好看一些?)

这里是源码:https://git.oschina.net/smile0905/AccordionClient.git

时间: 2024-10-12 21:08:20

WPF 简易手风琴 (ListBox+Expander)的相关文章

WPF绑定的ListBox获取ListBoxItem及GoToState应用

现公司项目中需要制作一个扇形菜单,菜单项是用ListBox重写Style实现的,其数据是绑定的.菜单的每一项都有Normal,MouseOver和Selected三种状态,这三种状态当然可以通过鼠标移动和点击控制,但现在要通过代码来改变控件外观实现三种状态切换,该如何处理呢?   1.WPF绑定的ListBox获取ListBoxItem WPF中如果ListBox的ItemSource为绑定的,则ListBox.Items为绑定的数据源,而非ListBoxItem.如果直接通过如下代码会发现无法

解决WPF程序中ListBox ItemsSource变化时不重置ScrollBar的问题

解决WPF程序中ListBox ItemsSource变化时不重置ScrollBar的问题 当我们改变ListBox的ItemsSource时,会发现这样一个问题:数据源变化时,虽然控件中的内容会跟着变化,但滚动条却不会重置. 举个例子: 将ListBox绑定到一百个字符串:listbox.ItemsSource = Enumerable.Range(0, 100).Select(i => "## " + i);. 将ListBox的滚动条拖到最后,使之能看到最后的"#

【JQuery】jQuery自制简易手风琴效果(附实现原理)

手风琴效果经常会用在网页左侧的导航栏,当导航内容比较多时使用手风琴的展示方式更有利于信息的传递和排版,下面就分享一个自己制作的简易手风琴效果,没有用图片,背景颜色也是随意设定的,在实际项目中大家可适当修改. 效果图: 实现原理: 1.当鼠标点击span标签(即一级导航)时,先判断子目录li是否已经展开(此处使用一个on类来做标记): 2.如果是,则收缩当前的li,移出on类标记,修改span右边的提示符为加号: 3.如果不是,则展开当前的li,增加on类标记,修改span右边的提示符为减号. 源

wpf中手风琴控件Accordion编辑模板后控件不正常。

昨天有个网友Accordion控件从sl迁移到wpf时候显示不正常.也是就没有效果. 我也是sl做的比较多,wpf玩的少,Accordion模板里触发器,状态组调了一早上都没达到满意效果, 无奈只有百度,发现了老外一个Accordion模板的例子http://blogs.u2u.be/diederik/post/2010/02/20/How-to-play-the-Accordion-WPF-Toolkit.aspx .向我项目里迁移的时候发现,居然不生效!!!! 最后一一对比发现我项目里有sy

简易手风琴

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>手风琴效果</title> <style type="text/css"> .accordions { overflow: hidden; width: 800px; height: 350px; margin: 0 auto; } .yhd-switchable-

[WPF自定义控件库]自定义Expander

1. 前言 上一篇文章介绍了使用Resizer实现Expander简单的动画效果,运行效果也还好,不过只有展开/折叠而缺少了淡入/淡出的动画(毕竟Resizer模仿Expander只是附带的功能).这篇继续Measure的话题,自定义了一个带有动画的ExtendedExpander. 2. ExtendedExpander的需求 使用Resizer实现的简易Expander没办法在折叠时做淡出动画,因为ControlTemplate中的ExpandSite在Collapsed状态下直接设置为隐藏

wpf CollectionViewSource与ListBox的折叠、分组显示,及输入关键字 Filter的筛选

在wpf中虽然ObservableCollection<T>作为ListBox的Itemsource,很好,很强大!但是CollectionViewSource与ListBox才是天作之合! wpf中ListBox支持分组显示,CollectionViewSource.GroupDescriptions为其实现了分组.废话不多说,下面上ListBox分组显示的Demo代码: XAML: <Window x:Class="WpfListGroup.MainWindow"

在WPF中让ListBox和ComboBox的快速检索功能失效

问题来源: 自定义一个ComboBox,用来显示日期.后台数据使用的是DateTime,经过Converter转化成“2015年01月01日”样子的成字符串用于显示. 但是,在实际使用中,不停的按下“[”键,光标会从以一个元素一下一下的向下移动. 经过调查,这是ComboBox的“快速检索”功能在作祟. 关于快速检索: WPF中ListBox和ComboBox有一个“快速检索”的功能. 比如在ListBox里,按下“a”键,光标会定位到第一个首字母为“a”的Item上. ComboBox也是一样

创建一个显示所有预定义WPF颜色的ListBox

原文 https://stuff.seans.com/2011/02/14/creating-a-listbox-that-shows-all-predefined-wpf-colors/ 在WPF中,您可以使用Colors类访问一系列预定义颜色,这些颜色定义为Colors类的静态属性.您只需使用颜色名称引用每种颜色. 作为参考,这里有一个小应用程序,显示ListBox中的所有颜色.(感谢casperOne,在stackoverflow文章中展示了如何创建一个封装Colors类中属性列表的对象)