using System.Collections; using System.Collections.Specialized; namespace Melskin.Controls; /// /// 多选树形视图控件。 /// public sealed class MultiTreeView : TreeView { // 内部标志位,用于防止在控件处理逻辑时,响应外部对SelectedItems集合的修改,避免冲突 private bool isUpdatingSelection; /// /// 多选树形视图控件,允许用户选择多个节点。 /// 继承自TreeView类,并扩展了多选功能。 /// public MultiTreeView() { // 为SelectedItems属性提供一个默认的集合实例 SetValue(SelectedItemsProperty, new List()); } #region SelectedItems Dependency Property /// /// 表示 MultiTreeView 控件中当前选中的项的集合的依赖属性。 /// 该属性用于绑定到外部数据源,以反映用户在树形视图中的选择状态。 /// 当此属性值更改时,会触发相关联的逻辑来同步UI的勾选状态,并更新内部对新集合的事件监听。 /// public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register( nameof(SelectedItems), typeof(IList), typeof(MultiTreeView), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged)); /// /// 获取或设置 MultiTreeView 控件中当前选中的项的集合。 /// 该属性允许外部数据源绑定到控件,以便反映用户在树形视图中的选择状态。 /// 当此属性值更改时,会触发相关联的逻辑来同步UI的勾选状态,并更新内部对新集合的事件监听。 /// public IList SelectedItems { get => (IList)GetValue(SelectedItemsProperty); set => SetValue(SelectedItemsProperty, value); } #endregion #region Item Container Overrides /// protected override DependencyObject GetContainerForItemOverride() => new MultiTreeViewItem(); /// protected override bool IsItemItsOwnContainerOverride(object item) => item is MultiTreeViewItem; #endregion #region Event Handling and Logic // 当绑定的SelectedItems属性本身发生变化时(例如,从ViewModel中设置了一个新的集合) private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var treeView = (MultiTreeView)d; if (treeView.isUpdatingSelection) return; // 移除对旧集合的事件监听 if (e.OldValue is INotifyCollectionChanged oldCollection) { oldCollection.CollectionChanged -= treeView.OnSelectedItemsCollectionChanged; } // 添加对新集合的事件监听 if (e.NewValue is INotifyCollectionChanged newCollection) { newCollection.CollectionChanged += treeView.OnSelectedItemsCollectionChanged; } // 根据新集合的内容,完全同步UI的勾选状态 treeView.SyncUiFromSelectedItems(); } // 当绑定的SelectedItems集合内部发生Add/Remove/Clear等操作时 private void OnSelectedItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (isUpdatingSelection) return; SyncUiFromSelectedItems(); } // 核心方法: 由MultiTreeViewItem调用,表示一个节点的IsChecked状态被用户改变 internal void OnItemCheckChanged(MultiTreeViewItem item) { isUpdatingSelection = true; // 1. 向下更新:更新所有子孙节点的选中状态,使其与当前节点一致 var descendants = GetDescendants(item, true); foreach (var descendant in descendants) { SetItemIsChecked(descendant, item.IsChecked == true); } // 2. 向上更新:逐级更新所有父节点的选中状态(可能变为全选、不选或半选) var ancestor = GetParentItem(item); while (ancestor != null) { UpdateParentItemCheckState(ancestor); ancestor = GetParentItem(ancestor); } // 3. 数据同步:根据当前UI的勾选状态,更新SelectedItems集合 SyncSelectedItemsFromUi(); isUpdatingSelection = false; } #endregion #region Synchronization Methods // 从SelectedItems集合出发,反向同步整个UI树的勾选状态 private void SyncUiFromSelectedItems() { isUpdatingSelection = true; var allItems = GetAllGeneratedItems(); foreach (var item in allItems) { var shouldBeChecked = SelectedItems?.Contains(item.DataContext) ?? false; SetItemIsChecked(item, shouldBeChecked); } // 更新所有父节点的半选状态,确保UI一致性 foreach (var item in allItems.Where(i => !i.HasItems)) { var parent = GetParentItem(item); while (parent != null) { UpdateParentItemCheckState(parent); parent = GetParentItem(parent); } } isUpdatingSelection = false; } // 从UI树的当前勾选状态出发,同步更新SelectedItems集合 private void SyncSelectedItemsFromUi() { var selectedDataItems = GetAllGeneratedItems() .Where(item => item.IsChecked == true) .Select(item => item.DataContext) .ToList(); // SelectedItems ??= new List(); // 高效地比较并更新集合,避免不必要的事件触发 var currentSelected = SelectedItems.OfType().ToList(); var toAdd = selectedDataItems.Except(currentSelected).ToList(); var toRemove = currentSelected.Except(selectedDataItems).ToList(); foreach (var item in toRemove) SelectedItems.Remove(item); foreach (var item in toAdd) SelectedItems.Add(item); } #endregion #region Helper Methods // 安全地设置一个MultiTreeViewItem的IsChecked状态,同时抑制其事件通知 private static void SetItemIsChecked(MultiTreeViewItem item, bool isChecked) { if (item.IsChecked == isChecked) return; item.IsUpdatingCheckState = true; item.IsChecked = isChecked; item.IsUpdatingCheckState = false; } // 根据子项的勾选状态,更新父项的状态(可能是 true, false, or null for indeterminate) private static void UpdateParentItemCheckState(MultiTreeViewItem item) { var children = GetGeneratedChildren(item); if (children.Count == 0) return; bool? newCheckState; if (children.All(c => c?.IsChecked == true)) newCheckState = true; else if (children.All(c => c is { IsChecked: false or null })) newCheckState = false; else newCheckState = null; item.IsUpdatingCheckState = true; item.IsChecked = newCheckState; item.IsUpdatingCheckState = false; } // 获取一个节点的所有已生成的子孙节点 (UI-based) private static List GetDescendants(ItemsControl parent, bool includeSelf) { var descendants = new List(); if (includeSelf && parent is MultiTreeViewItem self) { descendants.Add(self); } // ItemContainerGenerator是WPF中获取项容器的标准方式 for (var i = 0; i < parent.Items.Count; i++) { if (parent.ItemContainerGenerator.ContainerFromIndex(i) is MultiTreeViewItem child) { descendants.AddRange(GetDescendants(child, true)); } } return descendants; } private List GetAllGeneratedItems() => GetDescendants(this, false); private static List GetGeneratedChildren(ItemsControl parent) => [.. Enumerable.Range(0, parent.Items.Count) .Select(i => parent.ItemContainerGenerator.ContainerFromIndex(i) as MultiTreeViewItem) .Where(c => c != null)]; private static MultiTreeViewItem? GetParentItem(TreeViewItem item) => ItemsControl.ItemsControlFromItemContainer(item) as MultiTreeViewItem; #endregion } /// /// 用于多选树形视图中的树形视图项。 /// public class MultiTreeViewItem : TreeViewItem { // 内部标志位,用于防止在父控件更新IsChecked状态时,再次触发不必要的通知 internal bool IsUpdatingCheckState { get; set; } /// /// 表示 MultiTreeViewItem 是否被选中的依赖属性。 /// 该属性支持三态:true 表示选中,false 表示未选中,null 表示半选状态(部分子项被选中)。 /// 当此属性值更改时,会触发相关联的逻辑来更新其所有子孙节点以及父节点的选中状态, /// 并同步更新 SelectedItems 集合以反映最新的用户选择。 /// public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register( nameof(IsChecked), typeof(bool?), typeof(MultiTreeViewItem), new FrameworkPropertyMetadata(false, OnIsCheckedChanged)); /// /// 获取或设置当前 MultiTreeViewItem 是否被选中。 /// 该属性支持三态:true 表示选中,false 表示未选中,null 表示半选状态(部分子项被选中)。 /// 当此属性值更改时,会触发相关联的逻辑来更新其所有子孙节点以及父节点的选中状态, /// 并同步更新 SelectedItems 集合以反映最新的用户选择。 /// /// 类型为 的值,指示当前项是否被选中。 public bool? IsChecked { get => (bool?)GetValue(IsCheckedProperty); set => SetValue(IsCheckedProperty, value); } static MultiTreeViewItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiTreeViewItem), new FrameworkPropertyMetadata(typeof(MultiTreeViewItem))); } private static void OnIsCheckedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var item = (MultiTreeViewItem)d; // 如果是父控件的内部逻辑在更新状态,则直接返回,避免无限循环 if (item.IsUpdatingCheckState) return; // 向上查找父控件MultiTreeView,并通知它状态发生了用户驱动的变化 var parentTreeView = GetParentTreeView(item); parentTreeView?.OnItemCheckChanged(item); } // 辅助方法,用于在可视化树中向上查找父MultiTreeView private static MultiTreeView? GetParentTreeView(DependencyObject item) { var parent = VisualTreeHelper.GetParent(item); while (parent != null && parent is not MultiTreeView) { parent = VisualTreeHelper.GetParent(parent); } return parent as MultiTreeView; } // 重写这两个方法是自定义TreeViewItem的标准做法 /// protected override DependencyObject GetContainerForItemOverride() => new MultiTreeViewItem(); /// protected override bool IsItemItsOwnContainerOverride(object item) => item is MultiTreeViewItem; }