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 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM