树形下拉菜单是许多WPF应用程序中常见的用户界面元素,它能够以分层的方式展示数据,提供更好的用户体验。本文将深入探讨如何基于WPF创建一个可定制的树形下拉菜单控件,涵盖从原理到实际实现的关键步骤。
一.需求分析
树形下拉菜单控件的核心是将ComboBox与TreeView结合起来,以实现下拉时的树状数据展示。在WPF中,可以通过自定义控件模板、样式和数据绑定来实现这一目标。
我们首先来分析一下ComboBox控件的模板。
<span><<span>ControlTemplate</span> <span>x:Key</span>=<span>\"ComboBoxTemplate\"</span> <span>TargetType</span>=<span>\"{x:Type ComboBox}\"</span>></span> <span><<span>Grid</span> <span>x:Name</span>=<span>\"templateRoot\"</span> <span>SnapsToDevicePixels</span>=<span>\"true\"</span>></span> <span><<span>Grid.ColumnDefinitions</span>></span> <span><<span>ColumnDefinition</span> <span>Width</span>=<span>\"*\"</span>/></span> <span><<span>ColumnDefinition</span> <span>MinWidth</span>=<span>\"{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}\"</span> <span>Width</span>=<span>\"0\"</span>/></span> <span></<span>Grid.ColumnDefinitions</span>></span> <span><<span>Popup</span> <span>x:Name</span>=<span>\"PART_Popup\"</span> <span>AllowsTransparency</span>=<span>\"true\"</span> <span>Grid.ColumnSpan</span>=<span>\"2\"</span> <span>IsOpen</span>=<span>\"{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}\"</span> <span>Margin</span>=<span>\"1\"</span> <span>Placement</span>=<span>\"Bottom\"</span> <span>PopupAnimation</span>=<span>\"{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}\"</span>></span> <span><<span>theme:SystemDropShadowChrome</span> <span>x:Name</span>=<span>\"shadow\"</span> <span>Color</span>=<span>\"Transparent\"</span> <span>MinWidth</span>=<span>\"{Binding ActualWidth, ElementName=templateRoot}\"</span> <span>MaxHeight</span>=<span>\"{TemplateBinding MaxDropDownHeight}\"</span>></span> <span><<span>Border</span> <span>x:Name</span>=<span>\"dropDownBorder\"</span> <span>Background</span>=<span>\"{DynamicResource {x:Static SystemColors.WindowBrushKey}}\"</span> <span>BorderBrush</span>=<span>\"{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}\"</span> <span>BorderThickness</span>=<span>\"1\"</span>></span> <span><<span>ScrollViewer</span> <span>x:Name</span>=<span>\"DropDownScrollViewer\"</span>></span> <span><<span>Grid</span> <span>x:Name</span>=<span>\"grid\"</span> <span>RenderOptions.ClearTypeHint</span>=<span>\"Enabled\"</span>></span> <span><<span>Canvas</span> <span>x:Name</span>=<span>\"canvas\"</span> <span>HorizontalAlignment</span>=<span>\"Left\"</span> <span>Height</span>=<span>\"0\"</span> <span>VerticalAlignment</span>=<span>\"Top\"</span> <span>Width</span>=<span>\"0\"</span>></span> <span><<span>Rectangle</span> <span>x:Name</span>=<span>\"opaqueRect\"</span> <span>Fill</span>=<span>\"{Binding Background, ElementName=dropDownBorder}\"</span> <span>Height</span>=<span>\"{Binding ActualHeight, ElementName=dropDownBorder}\"</span> <span>Width</span>=<span>\"{Binding ActualWidth, ElementName=dropDownBorder}\"</span>/></span> <span></<span>Canvas</span>></span> <span><<span>ItemsPresenter</span> <span>x:Name</span>=<span>\"ItemsPresenter\"</span> <span>KeyboardNavigation.DirectionalNavigation</span>=<span>\"Cont<strong>ai</strong>ned\"</span> <span>SnapsToDevicePixels</span>=<span>\"{TemplateBinding SnapsToDevicePixels}\"</span>/></span> <span></<span>Grid</span>></span> <span></<span>ScrollViewer</span>></span> <span></<span>Border</span>></span> <span></<span>theme:SystemDropShadowChrome</span>></span> <span></<span>Popup</span>></span> <span><<span>ToggleButton</span> <span>x:Name</span>=<span>\"toggleButton\"</span> <span>Background</span>=<span>\"{TemplateBinding Background}\"</span> <span>BorderBrush</span>=<span>\"{TemplateBinding BorderBrush}\"</span> <span>BorderThickness</span>=<span>\"{TemplateBinding BorderThickness}\"</span> <span>Grid.ColumnSpan</span>=<span>\"2\"</span> <span>IsChecked</span>=<span>\"{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}\"</span> <span>Style</span>=<span>\"{StaticResource ComboBoxToggleButton}\"</span>/></span> <span><<span>ContentPresenter</span> <span>x:Name</span>=<span>\"contentPresenter\"</span> <span>ContentStringFormat</span>=<span>\"{TemplateBinding SelectionBoxItemStringFormat}\"</span> <span>ContentTemplate</span>=<span>\"{TemplateBinding SelectionBoxItemTemplate}\"</span> <span>Content</span>=<span>\"{TemplateBinding SelectionBoxItem}\"</span> <span>ContentTemplateSelector</span>=<span>\"{TemplateBinding ItemTemplateSelector}\"</span> <span>HorizontalAlignment</span>=<span>\"{TemplateBinding HorizontalContentAlignment}\"</span> <span>IsHitTestVisible</span>=<span>\"false\"</span> <span>Margin</span>=<span>\"{TemplateBinding Padding}\"</span> <span>SnapsToDevicePixels</span>=<span>\"{TemplateBinding SnapsToDevicePixels}\"</span> <span>VerticalAlignment</span>=<span>\"{TemplateBinding VerticalContentAlignment}\"</span>/></span> <span></<span>Grid</span>></span><span></<span>ControlTemplate</span>></span>
从以上代码可以看出,其中的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代码
<span><<span>ControlTemplate</span> <span>TargetType</span>=<span>\"{x:Type local:TreeComboBox}\"</span>></span> <span><<span>Grid</span> <span>x:Name</span>=<span>\"templateRoot\"</span> <span>SnapsToDevicePixels</span>=<span>\"true\"</span>></span> <span><<span>Grid.ColumnDefinitions</span>></span> <span><<span>ColumnDefinition</span> <span>Width</span>=<span>\"*\"</span> /></span> <span><<span>ColumnDefinition</span> <span>Width</span>=<span>\"0\"</span> <span>MinWidth</span>=<span>\"{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}\"</span> /></span> <span></<span>Grid.ColumnDefinitions</span>></span> <span><<span>Popup</span> <span>x:Name</span>=<span>\"PART_Popup\"</span> <span>Grid.ColumnSpan</span>=<span>\"2\"</span> <span>MaxHeight</span>=<span>\"{TemplateBinding MaxDropDownHeight}\"</span> <span>Margin</span>=<span>\"1\"</span> <span>AllowsTransparency</span>=<span>\"true\"</span> <span>IsOpen</span>=<span>\"{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}\"</span> <span>Placement</span>=<span>\"Bottom\"</span> <span>PopupAnimation</span>=<span>\"{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}\"</span>></span> <span><<span>Border</span> <span>x:Name</span>=<span>\"PART_Border\"</span> <span>Width</span>=<span>\"{Binding RelativeSource={RelativeSource AncestorType=local:TreeComboBox}, Path=ActualWidth}\"</span> <span>Background</span>=<span>\"{DynamicResource {x:Static SystemColors.WindowBrushKey}}\"</span> <span>BorderBrush</span>=<span>\"{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}\"</span> <span>BorderThickness</span>=<span>\"1\"</span> <span>SnapsToDevicePixels</span>=<span>\"true\"</span>></span> <span><<span>ScrollViewer</span> <span>x:Name</span>=<span>\"_tv_scrollviewer_\"</span> <span>Padding</span>=<span>\"{TemplateBinding Padding}\"</span> <span>Background</span>=<span>\"{TemplateBinding Background}\"</span> <span>CanContentScroll</span>=<span>\"false\"</span> <span>Focusable</span>=<span>\"false\"</span> <span>HorizontalScrollBarVisibility</span>=<span>\"{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}\"</span> <span>SnapsToDevicePixels</span>=<span>\"{TemplateBinding SnapsToDevicePixels}\"</span> <span>VerticalScrollBarVisibility</span>=<span>\"{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}\"</span>></span> <span><<span>ItemsPresenter</span> /></span> <span></<span>ScrollViewer</span>></span> <span></<span>Border</span>></span> <span></<span>Popup</span>></span> <span><<span>ToggleButton</span> <span>x:Name</span>=<span>\"toggleButton\"</span> <span>Grid.ColumnSpan</span>=<span>\"2\"</span> <span>Background</span>=<span>\"{TemplateBinding Background}\"</span> <span>BorderBrush</span>=<span>\"{TemplateBinding BorderBrush}\"</span> <span>BorderThickness</span>=<span>\"{TemplateBinding BorderThickness}\"</span> <span>IsChecked</span>=<span>\"{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}\"</span> <span>Style</span>=<span>\"{StaticResource ComboBoxToggleButton}\"</span> /></span> <span><<span>ContentPresenter</span> <span>x:Name</span>=<span>\"contentPresenter\"</span> <span>Margin</span>=<span>\"{TemplateBinding Padding}\"</span> <span>HorizontalAlignment</span>=<span>\"{TemplateBinding HorizontalContentAlignment}\"</span> <span>VerticalAlignment</span>=<span>\"{TemplateBinding VerticalContentAlignment}\"</span> <span>Content</span>=<span>\"{TemplateBinding SelectionBoxItem}\"</span> <span>ContentTemplate</span>=<span>\"{TemplateBinding SelectionBoxItemTemplate}\"</span> <span>IsHitTestVisible</span>=<span>\"False\"</span> /></span> <span></<span>Grid</span>></span> <span><<span>ControlTemplate.Triggers</span>></span> <span><<span>Trigger</span> <span>Property</span>=<span>\"IsEnabled\"</span> <span>Value</span>=<span>\"false\"</span>></span> <span><<span>Setter</span> <span>TargetName</span>=<span>\"PART_Border\"</span> <span>Property</span>=<span>\"Background\"</span> <span>Value</span>=<span>\"{DynamicResource {x:Static SystemColors.ControlBrushKey}}\"</span> /></span> <span></<span>Trigger</span>></span> <span><<span>Trigger</span> <span>Property</span>=<span>\"VirtualizingPanel.IsVirtualizing\"</span> <span>Value</span>=<span>\"true\"</span>></span> <span><<span>Setter</span> <span>TargetName</span>=<span>\"_tv_scrollviewer_\"</span> <span>Property</span>=<span>\"CanContentScroll\"</span> <span>Value</span>=<span>\"true\"</span> /></span> <span></<span>Trigger</span>></span> <span><<span>MultiTrigger</span>></span> <span><<span>MultiTrigger.Conditions</span>></span> <span><<span>Condition</span> <span>Property</span>=<span>\"IsGrouping\"</span> <span>Value</span>=<span>\"true\"</span> /></span> <span><<span>Condition</span> <span>Property</span>=<span>\"VirtualizingPanel.IsVirtualizingWhenGrouping\"</span> <span>Value</span>=<span>\"false\"</span> /></span> <span></<span>MultiTrigger.Conditions</span>></span> <span><<span>Setter</span> <span>Property</span>=<span>\"ScrollViewer.CanContentScroll\"</span> <span>Value</span>=<span>\"false\"</span> /></span> <span></<span>MultiTrigger</span>></span> <span></<span>ControlTemplate.Triggers</span>></span><span></<span>ControlTemplate</span>></span>
以下为使用控件的代码。
<span><<span>TreeComboBox</span> <span>Width</span>=<span>\"315\"</span> <span>MinHeight</span>=<span>\"30\"</span> <span>Padding</span>=<span>\"5\"</span> <span>HorizontalAlignment</span>=<span>\"Center\"</span> <span>VerticalAlignment</span>=<span>\"Top\"</span> <span>VerticalContentAlignment</span>=<span>\"Stretch\"</span> <span>IsAutoCollapse</span>=<span>\"True\"</span> <span>ItemsSource</span>=<span>\"{Binding Collection}\"</span>></span> <span><<span>TreeComboBox.SelectionBoxItemTemplate</span>></span> <span><<span>ItemContainerTemplate</span>></span> <span><<span>Border</span>></span> <span><<span>TextBlock</span> <span>VerticalAlignment</span>=<span>\"Center\"</span> <span>Text</span>=<span>\"{Binding Property1}\"</span> /></span> <span></<span>Border</span>></span> <span></<span>ItemContainerTemplate</span>></span> <span></<span>TreeComboBox.SelectionBoxItemTemplate</span>></span> <span><<span>TreeComboBox.ItemTemplate</span>></span> <span><<span>HierarchicalDataTemplate</span> <span>ItemsSource</span>=<span>\"{Binding Collection}\"</span>></span> <span><<span>TextBlock</span> <span>Margin</span>=<span>\"5,0,0,0\"</span> <span>VerticalAlignment</span>=<span>\"Center\"</span> <span>Text</span>=<span>\"{Binding Property1}\"</span> /></span> <span></<span>HierarchicalDataTemplate</span>></span> <span></<span>TreeComboBox.ItemTemplate</span>></span><span></<span>TreeComboBox</span>></span>
三.运行效果
3.1 单选效果
单选效果
3.2 多选效果
多选效果
四.个性化外观
当控件默认外观无法满足需求时,我们可以通过编辑样式的方式来实现个性化外观,也可以引用第三方UI库样式,以下为使用MaterialDesign的效果。
4.1 单选效果
单选效果
4.2 多选效果
多选效果