Loading,你用IE,难怪你打不开

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

posted @ 2022-05-10 23:02  老板娘的神秘商店  阅读(963)  评论(2编辑  收藏  举报