Wpf实现TreeSelect多选
AIStudio框架汇总及介绍
实现思路:
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里而已,不封装进行使用更加简单灵活。