前言
樹形下拉選單是許多WPF應用程式中常見的使用者介面元素,它能夠以分層的方式展示資料,提供更好的使用者體驗。本文將深入探討如何基於WPF建立一個可定製的樹形下拉選單控制元件,涵蓋從原理到實際實現的關鍵步驟。
一、需求分析
樹形下拉選單控制元件的核心是將ComboBox與TreeView結合起來,以實現下拉時的樹狀資料展示。在WPF中,可以透過自定義控制元件模板、樣式和資料繫結來實現這一目標。
我們首先來分析一下ComboBox控制元件的模板。
<ControlTemplate x:Key="ComboBoxTemplate" TargetType="{x:Type ComboBox}"> <Grid x:Name="templateRoot" SnapsToDevicePixels="true"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/> </Grid.ColumnDefinitions> <Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" Margin="1" Placement="Bottom" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}"> <theme:SystemDropShadowChrome x:Name="shadow" Color="Transparent" MinWidth="{Binding ActualWidth, ElementName=templateRoot}" MaxHeight="{TemplateBinding MaxDropDownHeight}"> <Border x:Name="dropDownBorder" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1"> <ScrollViewer x:Name="DropDownScrollViewer"> <Grid x:Name="grid" RenderOptions.ClearTypeHint="Enabled"> <Canvas x:Name="canvas" HorizontalAlignment="Left" Height="0" VerticalAlignment="Top" Width="0"> <Rectangle x:Name="opaqueRect" Fill="{Binding Background, ElementName=dropDownBorder}" Height="{Binding ActualHeight, ElementName=dropDownBorder}" Width="{Binding ActualWidth, ElementName=dropDownBorder}"/> </Canvas> <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </Grid> </ScrollViewer> </Border> </theme:SystemDropShadowChrome> </Popup> <ToggleButton x:Name="toggleButton" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Grid.ColumnSpan="2" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" Style="{StaticResource ComboBoxToggleButton}"/> <ContentPresenter x:Name="contentPresenter" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" Content="{TemplateBinding SelectionBoxItem}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> </Grid> </ControlTemplate>
從以上程式碼可以看出,其中的Popup控制元件就是下拉部分,那麼按照常理,我們在Popup控制元件中放入一個TreeView控制元件即可實現該需求,但是現實情況遠沒有這麼簡單。我們開發一個控制元件,不僅要從外觀上實現功能,還需要考慮資料繫結、事件觸發、自定義模板等方面的問題,顯然,直接放置一個TreeView控制元件雖然也能實現功能,但是從封裝的角度看,它並不優雅,使用也不方便。那麼有沒有更好的方法滿足以上需求呢?下面提供另一種思路,其核心思想就是融合ComboBox控制元件與TreeView控制元件模板,讓控制元件既保留TreeView的特性,又擁有ComboBox的外觀。
二、程式碼實現
2.1 編輯TreeView模板;
2.2 提取ComboBox的模板程式碼;
2.3 將ComboBox的模板程式碼移植到TreeView模板中;
2.4 將TreeView模板包含ItemsPresenter部分的關鍵程式碼放入ComboBox模板中的Popup控制元件內;
以下為融合後的xaml程式碼
<ControlTemplate TargetType="{x:Type local:TreeComboBox}"> <Grid x:Name="templateRoot" SnapsToDevicePixels="true"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="0" MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" /> </Grid.ColumnDefinitions> <Popup x:Name="PART_Popup" Grid.ColumnSpan="2" MaxHeight="{TemplateBinding MaxDropDownHeight}" Margin="1" AllowsTransparency="true" IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" Placement="Bottom" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}"> <Border x:Name="PART_Border" Width="{Binding RelativeSource={RelativeSource AncestorType=local:TreeComboBox}, Path=ActualWidth}" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1" SnapsToDevicePixels="true"> <ScrollViewer x:Name="_tv_scrollviewer_" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}" CanContentScroll="false" Focusable="false" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"> <ItemsPresenter /> </ScrollViewer> </Border> </Popup> <ToggleButton x:Name="toggleButton" Grid.ColumnSpan="2" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" Style="{StaticResource ComboBoxToggleButton}" /> <ContentPresenter x:Name="contentPresenter" Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Content="{TemplateBinding SelectionBoxItem}" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" IsHitTestVisible="False" /> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsEnabled" Value="false"> <Setter TargetName="PART_Border" Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" /> </Trigger> <Trigger Property="VirtualizingPanel.IsVirtualizing" Value="true"> <Setter TargetName="_tv_scrollviewer_" Property="CanContentScroll" Value="true" /> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsGrouping" Value="true" /> <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false" /> </MultiTrigger.Conditions> <Setter Property="ScrollViewer.CanContentScroll" Value="false" /> </MultiTrigger> </ControlTemplate.Triggers> </ControlTemplate>
以下為使用控制元件的程式碼。
<TreeComboBox Width="315" MinHeight="30" Padding="5" HorizontalAlignment="Center" VerticalAlignment="Top" VerticalContentAlignment="Stretch" IsAutoCollapse="True" ItemsSource="{Binding Collection}"> <TreeComboBox.SelectionBoxItemTemplate> <ItemContainerTemplate> <Border> <TextBlock VerticalAlignment="Center" Text="{Binding Property1}" /> </Border> </ItemContainerTemplate> </TreeComboBox.SelectionBoxItemTemplate> <TreeComboBox.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Collection}"> <TextBlock Margin="5,0,0,0" VerticalAlignment="Center" Text="{Binding Property1}" /> </HierarchicalDataTemplate> </TreeComboBox.ItemTemplate> </TreeComboBox>
三、執行效果
3.1 單選效果
3.2 多選效果
四、個性化外觀
當控制元件預設外觀無法滿足需求時,我們可以透過編輯樣式的方式來實現個性化外觀,也可以引用第三方UI庫樣式,以下為使用MaterialDesign的效果。
4.1 單選效果
4.2 多選效果