实现思路:
1.定义一个树形控件对应的基类,有IsChecked属性。
2.XAML里面绑定上面那个类对象。CheckBox绑定IsChecked属性。
3.处理数据设置与文本显示,用逗号分开。

XAML代码:同Wpf实现TreeSelect
CS代码:和单选代码是在一块的,为了实现多选进行了改造。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
namespace Util.Controls
{
/// <summary>
/// TreeSelect.xaml 的交互逻辑
/// </summary>
[TemplatePart(Name = "PART_TreeView", Type = typeof(TreeView))]
public partial class TreeSelect : ComboBox
{
public bool IsMulti
{
get { return (bool)GetValue(IsMultiProperty); }
set { SetValue(IsMultiProperty, value); }
}
public static readonly DependencyProperty IsMultiProperty =
DependencyProperty.Register("IsMulti", typeof(bool), typeof(TreeSelect), new PropertyMetadata(false));
public IList SelectedItems
{
get { return (IList)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(IList), typeof(TreeSelect), new PropertyMetadata(OnSelectedItemsChanged));
private static void OnSelectedItemsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
((TreeSelect)sender).UpdateSelectedItems(e.NewValue as IList, e.OldValue as IList);
}
public new string DisplayMemberPath
{
get { return (string)GetValue(DisplayMemberPathProperty); }
set { SetValue(DisplayMemberPathProperty, value); }
}
public new static readonly DependencyProperty DisplayMemberPathProperty =
DependencyProperty.Register("DisplayMemberPath", typeof(string), typeof(TreeSelect));
public new string SelectedValuePath
{
get { return (string)GetValue(SelectedValuePathProperty); }
set { SetValue(SelectedValuePathProperty, value); }
}
public new static readonly DependencyProperty SelectedValuePathProperty =
DependencyProperty.Register("SelectedValuePath", typeof(string), typeof(TreeSelect));
/// <summary>
/// Selected item of the TreeView
/// </summary>
public new object SelectedItem
{
get { return (object)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public new static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(TreeSelect), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedItemChanged)));
private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
((TreeSelect)sender).UpdateSelectedItem();
}
public new object SelectedValue
{
get { return (object)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public new static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(object), typeof(TreeSelect), new PropertyMetadata(null));
public new string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(TreeSelect));
/// <summary>
/// Gets or sets text separator.
/// </summary>
public string TextSeparator
{
get { return (string)GetValue(TextSeparatorProperty); }
set { SetValue(TextSeparatorProperty, value); }
}
public static readonly DependencyProperty TextSeparatorProperty =
DependencyProperty.Register("TextSeparator", typeof(string), typeof(TreeSelect), new PropertyMetadata(","));
/// <summary>
/// Gets or sets max text length.
/// </summary>
public int? MaxTextLength
{
get { return (int?)GetValue(MaxTextLengthProperty); }
set { SetValue(MaxTextLengthProperty, value); }
}
public static readonly DependencyProperty MaxTextLengthProperty =
DependencyProperty.Register("MaxTextLength", typeof(int?), typeof(TreeSelect));
/// <summary>
/// Gets or sets text filler when text length exceeded.
/// </summary>
public string ExceededTextFiller
{
get { return (string)GetValue(ExceededTextFillerProperty); }
set { SetValue(ExceededTextFillerProperty, value); }
}
public static readonly DependencyProperty ExceededTextFillerProperty =
DependencyProperty.Register("ExceededTextFiller", typeof(string), typeof(TreeSelect), new PropertyMetadata("..."));
static TreeSelect()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TreeSelect), new FrameworkPropertyMetadata(typeof(TreeSelect)));
}
public TreeSelect()
{
Loaded -= TreeSelect_Loaded;
Loaded += TreeSelect_Loaded;
SizeChanged -= TreeSelect_SizeChanged;
SizeChanged += TreeSelect_SizeChanged;
}
private ExtendedTreeView _treeView;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this._treeView = Template.FindName("PART_TreeView", this) as ExtendedTreeView;
if (this._treeView != null)
{
//this._treeView.SelectedItemChanged += _TreeView_SelectedItemChanged;
_treeView.OnHierarchyMouseUp += OnTreeViewHierarchyMouseUp;
_treeView.AddHandler(System.Windows.Controls.TreeViewItem.SelectedEvent, new System.Windows.RoutedEventHandler(treeview_Selected));
}
}
//protected override void OnDropDownClosed(EventArgs e)
//{
// base.OnDropDownClosed(e);
// this.SelectedItem = _treeView.SelectedItem;
// this.UpdateText();
//}
//protected override void OnDropDownOpened(EventArgs e)
//{
// base.OnDropDownOpened(e);
// this.UpdateText();
//}
private bool _interChanged = false;
/// <summary>
/// Handles clicks on any item in the tree view
/// </summary>
private void OnTreeViewHierarchyMouseUp(object sender, RoutedEventArgs e)
{
if (IsMulti)
{
_interChanged = true;
SelectedItems.Clear();
foreach (var item in GenerateMultiObject(Items))
{
SelectedItems.Add(item);
}
_interChanged = false;
}
else
{
//This line isn't obligatory because it is executed in the OnDropDownClosed method, but be it so
this.SelectedItem = _treeView.SelectedItem;
base.SelectedItem = this.SelectedItem;
this.IsDropDownOpen = false;
}
this.UpdateText();
}
private void treeview_Selected(object sender, RoutedEventArgs e)
{
TreeViewItem item = (e.OriginalSource as TreeViewItem);
if (item != null)
{
item.BringIntoView();
}
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
if (item is ComboBoxItem)
return true;
else
return false;
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
var uie = element as FrameworkElement;
if (!(item is ComboBoxItem))
{
var textBinding = new Binding(DisplayMemberPath);
textBinding.Source = item;
uie.SetBinding(ContentPresenter.ContentProperty, textBinding);
}
base.PrepareContainerForItemOverride(element, item);
}
private void TreeSelect_SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateText();
}
private void TreeSelect_Loaded(object sender, RoutedEventArgs e)
{
UpdateText();
}
private void UpdateSelectedItem()
{
if (this.SelectedItem == null || string.IsNullOrEmpty(this.SelectedValuePath))
{
SelectedValue = null;
}
else
{
SelectedValue = this.SelectedItem.GetType().GetProperty(SelectedValuePath).GetValue(this.SelectedItem, null);
}
base.SelectedItem = this.SelectedItem;
UpdateText();
}
private void UpdateSelectedItems(IList newitem, IList olditem)
{
if (olditem != null)
{
foreach (var item in olditem)
{
if (item.GetType().GetProperty("IsChecked") != null)
{
item.GetType().GetProperty("IsChecked").SetValue(item, false);
}
}
((INotifyCollectionChanged)olditem).CollectionChanged -= TreeSelect_CollectionChanged;
}
if (newitem != null)
{
foreach (var item in newitem)
{
if (item.GetType().GetProperty("IsChecked") != null)
{
item.GetType().GetProperty("IsChecked").SetValue(item, true);
}
}
((INotifyCollectionChanged)newitem).CollectionChanged += TreeSelect_CollectionChanged;
}
UpdateText();
}
//主要是外部改变的时候更新数据,如果没有这种场景,可以不用。
private void TreeSelect_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_interChanged) return;
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
if (item.GetType().GetProperty("IsCheckedOnlySelf") != null)
{
item.GetType().GetProperty("IsCheckedOnlySelf").SetValue(item, true);
}
}
}
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
if (item.GetType().GetProperty("IsCheckedOnlySelf") != null)
{
item.GetType().GetProperty("IsCheckedOnlySelf").SetValue(item, false);
}
}
}
UpdateText();
}
private void _TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
UpdateText();
base.SelectedItem = this.SelectedItem;
}
private void UpdateText()
{
if (!IsLoaded)
return;
if (IsMulti == false)
{
Text = GenerateText(SelectedItem);
}
else
{
Text = GenerateMultiText();
}
}
public string GenerateText(object selectedItem)
{
var text = "";
if (selectedItem == null)
{
text = "";
}
else if (selectedItem is ComboBoxItem)
{
var msi = selectedItem as ComboBoxItem;
text += msi.Content.ToString();
}
else
{
if (!string.IsNullOrEmpty(DisplayMemberPath) && selectedItem.GetType().GetProperty(DisplayMemberPath) != null)
text += selectedItem.GetType().GetProperty(DisplayMemberPath).GetValue(selectedItem, null).ToString();
else
text += selectedItem.ToString();
if (selectedItem.GetType().GetProperty("IsSelected") != null)
{
selectedItem.GetType().GetProperty("IsSelected").SetValue(selectedItem, true);
}
}
return text;
}
private string GenerateMultiText()
{
var text = "";
var isFirst = true;
if (SelectedItems != null)
{
foreach (var item in SelectedItems)
{
string txt = null;
if (item is ComboBoxItem)//这个还未支持多选,按单选处理
{
var msi = item as ComboBoxItem;
txt = msi.Content.ToString();
}
else
{
if (item.GetType().GetProperty("IsChecked") != null && item.GetType().GetProperty("IsChecked").GetValue(item, null).ToString() == "True")
{
if (!string.IsNullOrEmpty(DisplayMemberPath) && item.GetType().GetProperty(DisplayMemberPath) != null)
txt = item.GetType().GetProperty(DisplayMemberPath).GetValue(item, null).ToString();
else
txt = item.ToString();
}
}
if (!isFirst)
text += TextSeparator;
else
isFirst = false;
text += txt;
if (MaxTextLength == null)
{
if (!ValidateStringWidth(text + ExceededTextFiller))
{
if (text.Length == 0)
return null;
text = text.Remove(text.Length - 1);
while (!ValidateStringWidth(text + ExceededTextFiller))
{
if (text.Length == 0)
return null;
text = text.Remove(text.Length - 1);
}
return text + ExceededTextFiller;
}
}
else if (text.Length >= MaxTextLength)
{
return text.Cut((int)MaxTextLength, ExceededTextFiller);
}
}
}
return text;
}
private List<object> GenerateMultiObject(System.Collections.IEnumerable items)
{
List<object> objs = new List<object>();
foreach (var item in items)
{
object obj = null;
if (item is ComboBoxItem)//这个还未支持多选,按单选处理
{
var msi = item as ComboBoxItem;
if (msi.IsSelected)
{
obj = item;
}
}
else
{
if (item.GetType().GetProperty("IsChecked") != null && item.GetType().GetProperty("IsChecked").GetValue(item, null).ToString() == "True")
{
obj = item;
}
}
if (obj != null)
objs.Add(obj);
if (item.GetType().GetProperty("Children") != null)
{
objs.AddRange(GenerateMultiObject(item.GetType().GetProperty("Children").GetValue(item, null) as System.Collections.IEnumerable));
}
}
return objs;
}
private bool ValidateStringWidth(string text)
{
var size = MeasureString(text);
if (size.Width > (ActualWidth - Padding.Left - Padding.Right - 30))
return false;
else
return true;
}
private Size MeasureString(string candidate)
{
var formattedText = new FormattedText(candidate, System.Globalization.CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), FontSize, Brushes.Black, new NumberSubstitution(), TextFormattingMode.Display);
return new Size(formattedText.Width, formattedText.Height);
}
}
}
然后绑定的类对象如下:
public abstract class BaseTreeItemViewModel : INotifyPropertyChanged, IBaseTreeItemViewModel//组织机构树节点 { #region 基本属性 private string _name; public string Name { get { return _name; } set { _name = value; OnPropertyChanged("Name"); } } #endregion #region private ObservableCollection<BaseTreeItemViewModel> _children; public ObservableCollection<BaseTreeItemViewModel> Children { get { if (_children == null) { _children = new ObservableCollection<BaseTreeItemViewModel>(); _children.CollectionChanged += new NotifyCollectionChangedEventHandler(OnChildrenChanged); } return _children; } } protected void OnChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) { // Note: This section does not account for multiple items being involved in change operations. // Note: This section does not account for the replace operation. if (e.Action == NotifyCollectionChangedAction.Add) { BaseTreeItemViewModel child = (BaseTreeItemViewModel)e.NewItems[0]; child.Parent = this; } else if (e.Action == NotifyCollectionChangedAction.Remove) { BaseTreeItemViewModel child = (BaseTreeItemViewModel)e.OldItems[0]; if (child.Parent == this) { child.Parent = null; } } } public BaseTreeItemViewModel Parent { get; set; } private bool _isExpanded = true; public bool IsExpanded { get { return _isExpanded; } set { if (_isExpanded != value) { _isExpanded = value; OnPropertyChanged("IsExpanded"); } } } private bool _isSelected; public bool IsSelected { get { return _isSelected; } set { if (_isSelected != value) { _isSelected = value; OnPropertyChanged("IsSelected"); } } } private bool _isChecked = false; public bool IsChecked { get { return _isChecked; } set { if (_isChecked != value) { _isChecked = value; OnPropertyChanged("IsChecked"); SetChildChecked(_isChecked); } } } public bool IsCheckedOnlySelf { get { return _isChecked; } set { _isChecked = value; OnPropertyChanged("IsChecked"); } } private void SetChildChecked(bool isChecked) { if (Children != null) { foreach (var child in Children) { child.IsChecked = isChecked; child.SetChildChecked(isChecked); } } } public void SetChecked(bool isChecked) { _isChecked = isChecked; OnPropertyChanged("IsChecked"); } public int Level { get { if (Parent == null) { return 0; } else { return Parent.Level + 1; } } } #region TreeDataGrid专用 public double MarginLeft { get { return Level * 20 + 10; } } public Visibility ChildVisible { get { if (Children.Count == 0) { return Visibility.Collapsed; } else { return Visibility.Visible; } } } public void TreeDataGridOnPropertyChanged() { OnPropertyChanged("MarginLeft"); OnPropertyChanged("ChildVisible"); } #endregion /// <summary> /// 设置所有子项展开状态 /// </summary> /// <param name="isExpanded"></param> public void SetChildrenExpanded(bool isExpanded) { foreach (BaseTreeItemViewModel child in Children) { child.IsExpanded = isExpanded; child.SetChildrenExpanded(isExpanded); } } public void InsertChild(int index, BaseTreeItemViewModel child) { if (!Children.Contains(child)) { child.Parent = this; child.TreeDataGridOnPropertyChanged(); Children.Insert(index, child); TreeDataGridOnPropertyChanged(); } } public void AddChild(BaseTreeItemViewModel child) { if (!Children.Contains(child)) { child.Parent = this; child.TreeDataGridOnPropertyChanged(); Children.Add(child); TreeDataGridOnPropertyChanged(); } } public void AddChildRange(IEnumerable<BaseTreeItemViewModel> childs) { foreach (var child in childs) { if (!Children.Contains(child)) { child.Parent = this; child.TreeDataGridOnPropertyChanged(); Children.Add(child); } } TreeDataGridOnPropertyChanged(); } public void RemoveChild(BaseTreeItemViewModel child) { if (Children.Contains(child)) { child.Parent = null; Children.Remove(child); TreeDataGridOnPropertyChanged(); } } public void ClearChild() { if (_children != null) { _children.Clear(); TreeDataGridOnPropertyChanged(); } } #endregion public IEnumerable<BaseTreeItemViewModel> GetHierarchy() { return GetAscendingHierarchy().Reverse(); } public IEnumerable<BaseTreeItemViewModel> GetChildren() { return this.Children; } private IEnumerable<BaseTreeItemViewModel> GetAscendingHierarchy() { var vm = this; yield return vm; while (vm.Parent != null) { yield return vm.Parent; vm = vm.Parent; } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
使用代码
<util:TreeSelect Width="200" ItemsSource="{Binding Data}" SelectedItems="{Binding SelectedDatas}" ItemTemplate="{x:Null}" DisplayMemberPath="Name" IsMulti="True" xmlns:util="https://astudio.github.io/utilcontrol"> <util:TreeSelect.Resources> <HierarchicalDataTemplate DataType="{x:Type viewmodel:Person}" ItemsSource="{Binding Path=Children}"> <StackPanel Orientation="Horizontal"> <CheckBox Margin="2,0,0,0" IsChecked="{Binding IsChecked,Mode=TwoWay}" /> <Grid Margin="2,0,2,0"> <TextBlock x:Name="txtName" Text="{Binding Name, Mode=TwoWay}" Width="Auto" /> </Grid> </StackPanel> </HierarchicalDataTemplate> </util:TreeSelect.Resources> </util:TreeSelect>
实现是实现了,绑定对象还需要有定义好的属性。
这个东西在一些场景下没有必要封装成控件,其实就是一个树形控件的处理,封装在ComboBox里而已,不封装进行使用更加简单灵活。