1. 程式人生 > >如何實現具有層次結構的 TreeView (WPF/TreeView/Style/Template)

如何實現具有層次結構的 TreeView (WPF/TreeView/Style/Template)

打造使用者介面 (UI)

在上一節中,我們已經完成了資料的繫結工作,並通過 DataTemplate 實現了 Name 的縱向顯示。本節將主要介紹下面幾個內容:

1、對 TreeView 進行佈局,實現層次結構的檢視

2、設定 TreeViewItem 的樣式,令其在不同操作下呈現出相應的狀態

值得一提的是,在 dotNET 4.0 中提供的標準控制元件引入了 VisualStateGroup 類,有興趣的朋友可以在自定義控制元件中試試。由於此處部分狀態存在互斥的情況,所以仍使用了3.0的動畫定義方式。

1、對 TreeView 進行佈局,實現層次結構的檢視

預設情況下 TreeView 中的節點是自上而下排列,從左向右展開。在 WPF 中要改變這樣的佈局並不困難,只需要改變承載節點的容器即可。

<TreeView.Style>
    <Style TargetType="{x:Type TreeView}">
        <Setter Property="Background"
                Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
        <Setter Property="BorderBrush"
                Value="#FFB0C4DE"/>
        <Setter Property="BorderThickness"
                Value="1"/>
        <Setter Property="Foreground"
                Value="#FF042271"/>
        <Setter Property="Padding"
                Value="15,20"/>
        <Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
                Value="Auto"/>
        <Setter Property="ScrollViewer.VerticalScrollBarVisibility"
                Value="Auto"/>
        <Setter Property="ScrollViewer.CanContentScroll"
                Value="true"/>
        <Setter Property="VerticalContentAlignment"
                Value="Center"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type TreeView}">
                    <Grid>
                        <Rectangle x:Name="Rt"
                                   Opacity="0"
                                   Fill="#40000000">
                            <Rectangle.Effect>
                                <BlurEffect/>
                            </Rectangle.Effect>
                        </Rectangle>
                        <Border x:Name="Bd"
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                SnapsToDevicePixels="true">
                            <ScrollViewer x:Name="Scroll"
                                          Padding="-20,0,0,0">
                                <!-- 預設樣式中的標記是 <ItemsPresenter/> 使用的應該是 StackPanel -->
                                <!-- 此處我將其改為 WrapPanel 並設定 IsItemsHost="true" -->
                                <WrapPanel x:Name="ItemsHost"
                                           IsItemsHost="True"
                                           Margin="{TemplateBinding Padding}"
                                           Orientation="Horizontal"/>
                            </ScrollViewer>
                        </Border>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsGrouping"
                                 Value="true">
                            <Setter Property="ScrollViewer.CanContentScroll"
                                    Value="false"/>
                        </Trigger>
                        <Trigger Property="IsMouseOver"
                                 Value="true">
                            <Trigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="Rt"
                                                         Storyboard.TargetProperty="Opacity"
                                                         Duration="0:0:0.5"
                                                         To="1"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.EnterActions>
                            <Trigger.ExitActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="Rt"
                                                         Storyboard.TargetProperty="Opacity"
                                                         Duration="0:0:0.5"
                                                         To="0"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.ExitActions>
                        </Trigger>
                        <Trigger Property="IsEnabled"
                                 Value="false">
                            <Setter TargetName="Bd"
                                    Property="Background"
                                    Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                            <Setter TargetName="Bd"
                                    Property="BorderBrush"
                                    Value="{DynamicResource {x:Static SystemColors.InactiveBorderBrushKey}}"/>
                        </Trigger>
                        <EventTrigger RoutedEvent="Loaded">
                            <BeginStoryboard>
                                <Storyboard>
                                    <ThicknessAnimation Storyboard.TargetName="Scroll"
                                                        Storyboard.TargetProperty="Padding"
                                                        DecelerationRatio="0.9"
                                                        Duration="0:0:3"
                                                        To="0"/>
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</TreeView.Style>
應用了該樣式後將看到如下圖所示的介面:

Image

此時,我們已經實現了節點的橫向排列以及從上往下展開。下面將介紹如何利用模板和觸發器定製節點的樣式。

2、設定 TreeViewItem 的樣式,令其在不同操作下呈現出相應的狀態

下面給出的 XAML 完整的程式碼,重新定義了 TreeViewItem 的邊框、表示含有子節點的箭頭、連線子節點的線段以及承載子節點的容器等。

至於操作狀態,主要包括: 節點被選中、TreeView 失去焦點以及節點無效(IsEnabled=false)等。

<TreeView.ItemContainerStyle>
    <Style TargetType="{x:Type TreeViewItem}">
        <Setter Property="Background"
                Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
        <Setter Property="BorderBrush"
                Value="#FF336699"/>
        <Setter Property="BorderThickness"
                Value="1"/>
        <Setter Property="HorizontalAlignment"
                Value="Center"/>
        <Setter Property="HorizontalContentAlignment"
                Value="Center"/>
        <Setter Property="Foreground"
                Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
        <Setter Property="Margin"
                Value="5,0"/>
        <Setter Property="Padding"
                Value="10"/>
        <Setter Property="HorizontalContentAlignment"
                Value="Center"/>
        <Setter Property="VerticalContentAlignment"
                Value="Center"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type TreeViewItem}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Grid Grid.Row="0"
                              HorizontalAlignment="Center">
                            <!-- 陰影效果 -->
                            <Rectangle x:Name="el_Shadow"
                                       Fill="#30000000">
                                <Rectangle.Effect>
                                    <BlurEffect />
                                </Rectangle.Effect>
                            </Rectangle>
                            <!-- 底色 -->
                            <Rectangle Fill="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
                            <!-- 邊框 -->
                            <Border x:Name="el_Border"
                                    Background="{TemplateBinding Background}"
                                    BorderBrush="{TemplateBinding BorderBrush}"
                                    BorderThickness="{TemplateBinding BorderThickness}"
                                    HorizontalAlignment="Center"
                                    Padding="{TemplateBinding Padding}"
                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
                                <ContentPresenter x:Name="PART_Header"
                                                  Grid.Row="0"
                                                  ContentSource="Header"
                                                  HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                                  SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            </Border>
                        </Grid>
                        <!-- 用於表示節點展開/摺疊的箭頭 -->
                        <Grid x:Name="el_Arrow" Grid.Row="1"
                                Height="15">
                            <Path Data="M 1,1.5 L 4.5,5 L 8,1.5"
                                  Stroke="{TemplateBinding BorderBrush}"
                                  StrokeThickness="1"
                                  HorizontalAlignment="Center"
                                  VerticalAlignment="Center"
                                  SnapsToDevicePixels="false"/>
                        </Grid>
                        <!-- 節點展開後與子節點間的連線線 -->
                        <Rectangle x:Name="el_Line"
                                   Grid.Row="1"
                                   Fill="{TemplateBinding BorderBrush}"
                                   Width="1" Height="0"
                                   Opacity="0"
                                   SnapsToDevicePixels="True"/>
                        <!-- 表示子節點範圍的線段 -->
                        <Border x:Name="el_Range"
                                Grid.Row="2"
                                Background="Transparent"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="1,1,1,0"
                                Height="0"
                                Opacity="0"
                                SnapsToDevicePixels="True"/>
                        <!-- 承載子節點的容器 -->
                        <WrapPanel x:Name="el_Host"
                                   Grid.Row="3"
                                   IsItemsHost="True"
                                   Orientation="Horizontal"
                                   Visibility="Collapsed"/>
                    </Grid>
                    <!-- 動畫觸發器 -->
                    <ControlTemplate.Triggers>
                        <MultiTrigger>
                            <!-- 當 ListViewItem 的 HasItems/IsExpanded 都為 true 時觸發動畫 -->
                            <MultiTrigger.Conditions>
                                <Condition Property="HasItems"
                                           Value="true"/>
                                <Condition Property="IsExpanded"
                                           Value="true"/>
                            </MultiTrigger.Conditions>
                            <!-- 1、顯示承載子節點的容器 el_Host(WrapPanel) -->
                            <Setter TargetName="el_Host"
                                    Property="Visibility"
                                    Value="Visible"/>
                            <!-- 2、分別對 el_Line/el_Range/elArrow 物件的 Opacity/Height 屬性應用動畫效果 0.2s -->
                            <MultiTrigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="el_Line"
                                                         Storyboard.TargetProperty="Height"
                                                         To="15"
                                                         Duration="0:0:0.2"/>
                                        <DoubleAnimation Storyboard.TargetName="el_Line"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="1"
                                                         Duration="0:0:0.2"/>
                                        <DoubleAnimation Storyboard.TargetName="el_Range"
                                                         Storyboard.TargetProperty="Height"
                                                         To="15"
                                                         Duration="0:0:0.2"/>
                                        <DoubleAnimation Storyboard.TargetName="el_Range"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="1"
                                                         Duration="0:0:0.2"/>
                                        <DoubleAnimation Storyboard.TargetName="el_Arrow"
                                                         Storyboard.TargetProperty="Height"
                                                         To="0"
                                                         Duration="0:0:0.2"/>
                                        <DoubleAnimation Storyboard.TargetName="el_Arrow"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="0"
                                                         Duration="0:0:0.2"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </MultiTrigger.EnterActions>
                            <MultiTrigger.ExitActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="el_Line"
                                                         Storyboard.TargetProperty="Height"
                                                         To="0"
                                                         Duration="0:0:0.2"/>
                                        <DoubleAnimation Storyboard.TargetName="el_Line"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="0"
                                                         Duration="0:0:0.2"/>
                                        <DoubleAnimation Storyboard.TargetName="el_Range"
                                                         Storyboard.TargetProperty="Height"
                                                         To="0"
                                                         Duration="0:0:0.2"/>
                                        <DoubleAnimation Storyboard.TargetName="el_Range"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="0"
                                                         Duration="0:0:0.2"/>
                                        <DoubleAnimation Storyboard.TargetName="el_Arrow"
                                                         Storyboard.TargetProperty="Height"
                                                         To="15"
                                                         Duration="0:0:0.2"/>
                                        <DoubleAnimation Storyboard.TargetName="el_Arrow"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="1"
                                                         Duration="0:0:0.2"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </MultiTrigger.ExitActions>
                        </MultiTrigger>
                        <!-- 展開/摺疊節點觸發器 顯示/隱藏 承載子節點的容器 el_Host(WrapPanel) -->
                        <Trigger Property="IsExpanded"
                                 Value="true">
                            <Trigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="el_Host"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="1"
                                                         Duration="0:0:0.2"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.EnterActions>
                            <Trigger.ExitActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="el_Host"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="0"
                                                         Duration="0:0:0.2"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.ExitActions>
                        </Trigger>
                        <!-- 針對 WrapPanel 的 Opacity=0 時觸發的動畫 -->
                        <Trigger SourceName="el_Host"
                                 Property="Opacity"
                                 Value="0">
                            <Setter TargetName="el_Host"
                                    Property="Visibility"
                                    Value="Collapsed"/>
                        </Trigger>
                        <!-- 若不包含子節點 即 ListViewItem.HasItems=false 則隱藏 el_Host(WrapPaenl)/el_Arrow(指示箭頭) -->
                        <Trigger Property="HasItems"
                                 Value="false">
                            <Setter TargetName="el_Host"
                                    Property="Visibility"
                                    Value="Collapsed"/>
                            <Setter TargetName="el_Arrow"
                                    Property="Visibility"
                                    Value="Collapsed"/>
                        </Trigger>
                        <!-- 單個子節點觸發器 此時將隱藏表示子節點範圍的線段 el_Range 物件 -->
                        <DataTrigger Binding="{Binding ElementName=el_Host, Path=Children.Count}"
                                     Value="1">
                            <Setter TargetName="el_Range"
                                    Property="Visibility"
                                    Value="Collapsed"/>
                        </DataTrigger>
                        <!-- 節點被選中觸發器 -->
                        <Trigger Property="IsSelected"
                                 Value="true">
                            <Setter TargetName="el_Border"
                                    Property="Background"
                                    Value="#FFC5D9F1"/>
                            <Trigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <ColorAnimation Storyboard.TargetName="el_Shadow"
                                                        Storyboard.TargetProperty="Fill.Color"
                                                        To="#AB000000"
                                                        Duration="0:0:0.2"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.EnterActions>
                            <Trigger.ExitActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <ColorAnimation Storyboard.TargetName="el_Shadow"
                                                        Storyboard.TargetProperty="Fill.Color"
                                                        To="#30000000"
                                                        Duration="0:0:0.2"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.ExitActions>
                        </Trigger>
                        <!-- 節點被選中但 TreeView 失去焦點時的觸發器 -->
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsSelected"
                                           Value="true"/>
                                <Condition Property="IsSelectionActive"
                                           Value="false"/>
                            </MultiTrigger.Conditions>
                            <Setter TargetName="el_Border"
                                    Property="Background"
                                    Value="#FFDDD9C3"/>
                        </MultiTrigger>
                        <!-- 有效性 IsEnabled 觸發器 -->
                        <Trigger Property="IsEnabled"
                                 Value="false">
                            <Setter Property="Foreground"
                                    Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</TreeView.ItemContainerStyle>

至此,這篇文章已經完成。寫這篇文章的原因,一是為了總結最近的學習成果,整理出一個完整的思路;二是分享成果,希望能與感興趣的朋友共同提高,文中不足望高手指點!