WPF 实现个简单MVVM的TreeComboBox
树行下拉框,使用UserControl制作,先看下效果图吧#
源码下载:
https://files-cdn.cnblogs.com/files/wandia/TreeComboBoxDemo.zip
实现原理:#
通过修改ComboBox中的样式模板,在里面放一个TreeView就是最简单的思路, 这里简单列下修改后ComboBox最简单的控件模板
<ControlTemplate x:Key="ComboBoxTemplate" TargetType="{x:Type ComboBox}">
<Grid x:Name="templateRoot" SnapsToDevicePixels="true">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="0"/>
</Grid.ColumnDefinitions>
<Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" Height="auto"
IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}"
Placement="Bottom">
<Border x:Name="shadow">
<ScrollViewer>
<TreeView x:Name="InnerTreeView"/>
</ScrollViewer>
</Border>
</Popup>
<ToggleButton x:Name="toggleButton" />
<ContentPresenter x:Name="contentPresenter" />
</Grid>
</ControlTemplate>
<Style x:Key="CmbTreeComboBox" TargetType="{x:Type ComboBox}">
<Setter Property="Template" Value="{StaticResource ComboBoxTemplate}"/>
<Style/>
自定义用户控件放ComboBox即可#
<UserControl x:Class="Alarm.Main.Controls.TreeComboBox"
d:DesignHeight="30" d:DesignWidth="100">
<ComboBox Style="{StaticResource CmbTreeComboBox}" x:Name="ComboBox"/>
</UserControl>
为了达到基本的定制开发定义下最简单所需的依赖属性#
选中值 SelectedItem#
public object SelectedItem
{
get { return (object)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register(
"SelectedItem", typeof(object), typeof(TreeComboBox),
new PropertyMetadata(null, OnSelectedItem_Changed));
TreeView所需的数据源 ItemsSource#
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource", typeof(IEnumerable), typeof(TreeComboBox),
new PropertyMetadata(null, OnItemsSource_Changed));
TreeView选中项的呈现字符串值DisplayPath#
TreeView选中TreeViewItem后, 用于在ComboBox显示相应的值,该属性类似下拉框的DisplayMemberPath
public string DisplayPath
{
get { return (string)GetValue(DisplayPathProperty); }
set { SetValue(DisplayPathProperty, value); }
}
public static readonly DependencyProperty DisplayPathProperty = DependencyProperty.Register(
"DisplayPath", typeof(string), typeof(TreeComboBox),
new PropertyMetadata("", OnDisplayPath_Changed));
一个Style类型的属性TreeItemStyle用于定制化TreeViewItem中样式#
public Style TreeItemStyle
{
get { return (Style)GetValue(TreeItemStyleProperty); }
set { SetValue(TreeItemStyleProperty, value); }
}
public static readonly DependencyProperty TreeItemStyleProperty = DependencyProperty.Register(
"TreeItemStyle", typeof(Style), typeof(TreeComboBox),
new PropertyMetadata(null, OnTreeItemStyle_Changed));
一个HierarchicalDataTemplate类型的属性TreeItemItemTemplate用于定制化TreeViewer的子项数据模板#
public HierarchicalDataTemplate TreeItemItemTemplate
{
get { return (HierarchicalDataTemplate)GetValue(TreeItemItemTemplateProperty); }
set { SetValue(TreeItemItemTemplateProperty, value); }
}
public static readonly DependencyProperty TreeItemItemTemplateProperty = DependencyProperty.Register(
"TreeItemItemTemplate", typeof(HierarchicalDataTemplate), typeof(TreeComboBox),
new PropertyMetadata(null, OnTreeItemItemTemplate_Changed));
增加路由事件和命令满足基本的用法#
定义选中值发生改变事件#
// Register a custom routed event using the Bubble routing strategy.
public static readonly RoutedEvent SelectedItemChangedEvent = EventManager.RegisterRoutedEvent(
name: "SelectedItemChanged",
routingStrategy: RoutingStrategy.Bubble,
handlerType: typeof(RoutedEventHandler),
ownerType: typeof(TreeComboBox));
// Provide CLR accessors for assigning an event handler.
public event RoutedEventHandler SelectedItemChanged
{
add { AddHandler(SelectedItemChangedEvent, value); }
remove { RemoveHandler(SelectedItemChangedEvent, value); }
}
定义命令实现MVVM#
这里命令由TreeViewItem的点击事件MouseDown触发
public ICommand TreeViewItemClickCommand
{
get { return (ICommand)GetValue(TreeViewItemClickCommandProperty); }
set { SetValue(TreeViewItemClickCommandProperty, value); }
}
public static readonly DependencyProperty TreeViewItemClickCommandProperty =
DependencyProperty.Register("TreeViewItemClickCommand", typeof(ICommand), typeof(TreeComboBox), new PropertyMetadata(null));
public object TreeViewItemClickCommandParameter
{
get { return (object)GetValue(TreeViewItemClickCommandParameterProperty); }
set { SetValue(TreeViewItemClickCommandParameterProperty, value); }
}
public static readonly DependencyProperty TreeViewItemClickCommandParameterProperty =
DependencyProperty.Register("TreeViewItemClickCommandParameter", typeof(object), typeof(TreeComboBox), new PropertyMetadata(null));
public IInputElement TreeViewItemClickTarget
{
get { return (IInputElement)GetValue(TreeViewItemClickTargetProperty); }
set { SetValue(TreeViewItemClickTargetProperty, value); }
}
public static readonly DependencyProperty TreeViewItemClickTargetProperty =
DependencyProperty.Register("TreeViewItemClickTarget", typeof(IInputElement), typeof(TreeComboBox), new PropertyMetadata(null));
实现要点#
控件显示Load事件加载#
自定义控件的ComboBox中无法直接获取TreeView, 可以借助Load事件,获取控件可简单通过当前控件.Template.FindName("控件名",当前控件)
直接获取
在Load时对TreeView进行以上定义依赖属性的设置,如 TreeItemItemTemplate、 ItemsSource、 TreeItemStyle 等
this.Loaded += OnThis_Loaded;
private void OnThis_Loaded(object sender, RoutedEventArgs e)
{
if (DropDownTreeViewer == null)
{
DropDownTreeViewer = this.ComboBox.Template.FindName("InnerTreeView", this.ComboBox) as TreeView;
if (DropDownTreeViewer != null)
{
if (DropDownTreeViewer.ItemTemplate != this.TreeItemItemTemplate)
{
DropDownTreeViewer.ItemTemplate = this.TreeItemItemTemplate;
}
if (DropDownTreeViewer.ItemsSource != this.ItemsSource)
{
DropDownTreeViewer.Items.Clear();
DropDownTreeViewer.ItemsSource = this.ItemsSource;
}
if (DropDownTreeViewer.ItemContainerStyle != this.TreeItemStyle)
{
DropDownTreeViewer.ItemContainerStyle = this.TreeItemStyle;
}
}
}
if (this.SelectedItem != null && string.IsNullOrEmpty(this.ComboBox.Text))
{
SetDisplayText(this.SelectedItem);
}
else if (this.SelectedItem == null)
{
SetDisplayText(null);
}
}
SetDisplayText方法#
此方法用于让Combox对SelectedItem显示相应的值,使用反射简单实现
private void SetDisplayText(object item)
{
if (item == null)
{
this.ComboBox.Items.Clear();
this.ComboBox.Text = "";
return;
}
if (!string.IsNullOrEmpty(this.DisplayPath))
{
string[] propName = this.DisplayPath.Split('.');
if (propName.Length > 0)
{
object tmpVAlue = item;
for (int i = 0; i < propName.Length; i++)
{
PropertyInfo property = tmpVAlue.GetType().GetProperty(propName[i], BindingFlags.Instance | BindingFlags.Public);
if (property != null)
{
tmpVAlue = property.GetValue(tmpVAlue);
}
if (tmpVAlue == null || i == propName.Length - 1)
{
this.ComboBox.Items.Clear();
string value = (tmpVAlue ?? "NaN??").ToString();
this.ComboBox.Items.Add(value);
this.ComboBox.Text = value;
return;
}
}
}
}
this.ComboBox.Items.Clear();
this.ComboBox.Items.Add(this.SelectedItem.ToString());
this.ComboBox.Text = this.ComboBox.Items[0].ToString();
}
下拉时在TreeView中选中相应的项#
TreeView是无法直接对SelectedValue进行赋值,因为只读,此出通过ComboBox的DropDownOpen事件完成,见代码,
另外TreeView中如果发生选中项ComboBox会自动关闭,这里用开启线程重新触发选中TreeViewItem
同时,如果选中项在TreeViewItem中也会出现这个问题
this.ComboBox.DropDownOpened += OnComboBox_DropDownOpened;
private void OnComboBox_DropDownOpened(object sender, EventArgs e)
{
if (DropDownTreeViewer != null && this.SelectedItem != null)
{
if (DropDownTreeViewer.SelectedValue != this.SelectedItem)
{
this.SetTreeViewSelectedItem(this.SelectedItem);
}
}
}
private void SetTreeViewSelectedItem(object selectedValue)
{
if (selectedValue == null) return;
if (this.DropDownTreeViewer != null)
{
if (this.DropDownTreeViewer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
SelectedTheTreeViewItem(selectedValue);
}
else
{
EventHandler[] eventHandler = new EventHandler[1];
eventHandler[0] = new EventHandler((o1, e1) =>
{
if (this.DropDownTreeViewer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
this.DropDownTreeViewer.ItemContainerGenerator.StatusChanged -= eventHandler[0];
SelectedTheTreeViewItem(selectedValue);
};
});
this.DropDownTreeViewer.ItemContainerGenerator.StatusChanged += eventHandler[0];
}
}
}
private void SelectedTheTreeViewItem(object selectedValue)
{
for (int i = 0; i < this.DropDownTreeViewer.ItemContainerGenerator.Items.Count; i++)
{
TreeViewItem treeViewItem = this.DropDownTreeViewer.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (treeViewItem != null)
{
object item = this.DropDownTreeViewer.ItemContainerGenerator.ItemFromContainer(treeViewItem);
if (item == selectedValue)
{
treeViewItem.IsSelected = true;
TunThreadSetIsOpen();
return;
}
else
{
SetTreeViewItemSelected(treeViewItem, selectedValue);
}
}
}
}
private void TunThreadSetIsOpen()
{
//这个事件实在下拉时候发生的,因为选中了之后会触发下拉框自动回收,这就很操蛋,只能延迟后重新打开
new System.Threading.Thread(() =>
{
System.Threading.Thread.Sleep(100);
this.Dispatcher.Invoke(() =>
{
if (this.ComboBox.IsDropDownOpen == false)
{
this.ComboBox.IsDropDownOpen = true;
}
});
}).Start();
}
void SetTreeViewItemSelected(TreeViewItem treeViewItem, object selectedValue)
{
if (treeViewItem == null || selectedValue == null) return ;
if(treeViewItem.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
for (int i = 0; i < treeViewItem.ItemContainerGenerator.Items.Count; i++)
{
TreeViewItem childTreeViewItem = treeViewItem.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
if (childTreeViewItem != null)
{
object item = treeViewItem.ItemContainerGenerator.ItemFromContainer(childTreeViewItem);
if (item == selectedValue)
{
childTreeViewItem.IsSelected = true;
TunThreadSetIsOpen();
return;
}
else
{
SetTreeViewItemSelected(childTreeViewItem, selectedValue);
}
}
}
}
else
{
EventHandler[] eventHandler = new EventHandler[1];
eventHandler[0] = new EventHandler((o1, e1) =>
{
if (treeViewItem.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
treeViewItem.ItemContainerGenerator.StatusChanged -= eventHandler[0];
SetTreeViewItemSelected(treeViewItem, selectedValue);
};
});
treeViewItem.ItemContainerGenerator.StatusChanged += eventHandler[0];
}
}
监听路由事件#
监听TreeView.SelectedItemChanged用于更改ComboBox的显示值
EventManager.RegisterClassHandler(typeof(TreeView), TreeView.SelectedItemChangedEvent,
new RoutedPropertyChangedEventHandler<object>(OnTreeView_SelectedItemChangedEvent));
private void OnTreeView_SelectedItemChangedEvent(object sender, RoutedPropertyChangedEventArgs<object> e)
{
TreeView treeView = sender as TreeView;
if (treeView != this.DropDownTreeViewer) return;
if (e.NewValue != null)
{
this.SelectedItem = e.NewValue;
SetDisplayText(e.NewValue);
}
//写这个线程启动因为选中项之后下拉框不会关闭,如果在选中时关闭,将会重新触发一次此事件,导致选中的值回归选中前
new System.Threading.Thread(() =>
{
System.Threading.Thread.Sleep(50);
this.Dispatcher.Invoke(() =>
{
this.ComboBox.IsDropDownOpen = false;
});
}).Start();
}
监听TreeViewItem.MouseLeftButtonUp用于ComboBox没正确关闭时重新关闭
EventManager.RegisterClassHandler(typeof(TreeViewItem), TreeViewItem.MouseLeftButtonDownEvent,
new MouseButtonEventHandler(OnTreeViewItem_MouseDown));
private void OnTreeViewItem_MouseUP(object sender, MouseButtonEventArgs e)
{
if (this.ComboBox.IsDropDownOpen == true)
{
this.ComboBox.IsDropDownOpen = false;
}
}
监听TreeViewItem.MouseLeftButtonDown用于触发简单的Command,此时SelectedItem并未马上绑定,这里通过
通过DataContext将其从CommandParameter传递到命令调用
EventManager.RegisterClassHandler(typeof(TreeViewItem), TreeViewItem.MouseLeftButtonDownEvent,
new MouseButtonEventHandler(OnTreeViewItem_MouseDown));
private void OnTreeViewItem_MouseDown(object sender, MouseButtonEventArgs e)
{
TreeViewItem treeViewItem = sender as TreeViewItem;
if (treeViewItem != null)
{
object commandParameter = TreeViewItemClickCommandParameter;
if (commandParameter == null)
commandParameter = treeViewItem.DataContext;
TreeView treeParent = FindVisualParent<TreeView>(treeViewItem);
if (treeParent != null && treeParent == this.DropDownTreeViewer)
{
RoutedCommand command = TreeViewItemClickCommand as RoutedCommand;
if (command != null)
command.Execute(commandParameter, TreeViewItemClickTarget);
else if (TreeViewItemClickCommand != null)
this.TreeViewItemClickCommand.Execute(commandParameter);
}
}
}
public static T FindVisualParent<T>(DependencyObject obj) where T : DependencyObject
{
if (obj == null) return null;
DependencyObject parent = VisualTreeHelper.GetParent(obj);
while (parent != null)
{
if (parent is T)
return parent as T;
else
parent = VisualTreeHelper.GetParent(parent);
}
return parent as T;
}
用法请见源码#
xaml#
<Controls:TreeComboBox Width="120" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left"
TreeItemStyle="{StaticResource TreeViewItemStyle1}"
ItemsSource="{Binding TreeComboxItemsSource}"
SelectedItem="{Binding SelectedItem,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
SelectedItemChanged="TreeComboBox_SelectedItemChanged"
DisplayPath="Data.StepName"
TreeViewItemClickCommand="{Binding CMD_TreeViewItemClick}">
<Controls:TreeComboBox.TreeItemItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Childeren}">
<TextBlock Text="{Binding Data.StepName}" />
</HierarchicalDataTemplate>
</Controls:TreeComboBox.TreeItemItemTemplate>
</Controls:TreeComboBox>
#
源码下载:
https://files-cdn.cnblogs.com/files/wandia/TreeComboBoxDemo.zip
作者:老板娘的神秘商店
出处:https://www.cnblogs.com/wandia/p/16255759.html
版权:本作品采用「Base On WTFPL License」许可协议进行许可。
都打工的,贴出来不收费,干啥不CV
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?