301 lines
11 KiB
C#
301 lines
11 KiB
C#
|
|
using System.Collections;
|
|||
|
|
using System.Collections.Specialized;
|
|||
|
|
|
|||
|
|
|
|||
|
|
namespace NeumUI.Controls;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 多选树形视图控件。
|
|||
|
|
/// </summary>
|
|||
|
|
public sealed class MultiTreeView : TreeView
|
|||
|
|
{
|
|||
|
|
// 内部标志位,用于防止在控件处理逻辑时,响应外部对SelectedItems集合的修改,避免冲突
|
|||
|
|
private bool isUpdatingSelection;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 多选树形视图控件,允许用户选择多个节点。
|
|||
|
|
/// 继承自TreeView类,并扩展了多选功能。
|
|||
|
|
/// </summary>
|
|||
|
|
public MultiTreeView()
|
|||
|
|
{
|
|||
|
|
// 为SelectedItems属性提供一个默认的集合实例
|
|||
|
|
SetValue(SelectedItemsProperty, new List<object>());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#region SelectedItems Dependency Property
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 MultiTreeView 控件中当前选中的项的集合的依赖属性。
|
|||
|
|
/// 该属性用于绑定到外部数据源,以反映用户在树形视图中的选择状态。
|
|||
|
|
/// 当此属性值更改时,会触发相关联的逻辑来同步UI的勾选状态,并更新内部对新集合的事件监听。
|
|||
|
|
/// </summary>
|
|||
|
|
public static readonly DependencyProperty SelectedItemsProperty =
|
|||
|
|
DependencyProperty.Register(
|
|||
|
|
nameof(SelectedItems),
|
|||
|
|
typeof(IList),
|
|||
|
|
typeof(MultiTreeView),
|
|||
|
|
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取或设置 MultiTreeView 控件中当前选中的项的集合。
|
|||
|
|
/// 该属性允许外部数据源绑定到控件,以便反映用户在树形视图中的选择状态。
|
|||
|
|
/// 当此属性值更改时,会触发相关联的逻辑来同步UI的勾选状态,并更新内部对新集合的事件监听。
|
|||
|
|
/// </summary>
|
|||
|
|
public IList SelectedItems
|
|||
|
|
{
|
|||
|
|
get => (IList)GetValue(SelectedItemsProperty);
|
|||
|
|
set => SetValue(SelectedItemsProperty, value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Item Container Overrides
|
|||
|
|
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
protected override DependencyObject GetContainerForItemOverride() => new MultiTreeViewItem();
|
|||
|
|
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
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<object>();
|
|||
|
|
|
|||
|
|
// 高效地比较并更新集合,避免不必要的事件触发
|
|||
|
|
var currentSelected = SelectedItems.OfType<object>().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 void UpdateParentItemCheckState(MultiTreeViewItem item)
|
|||
|
|
{
|
|||
|
|
var children = GetGeneratedChildren(item);
|
|||
|
|
if (!children.Any()) 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<MultiTreeViewItem> GetDescendants(ItemsControl parent, bool includeSelf)
|
|||
|
|
{
|
|||
|
|
var descendants = new List<MultiTreeViewItem>();
|
|||
|
|
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<MultiTreeViewItem> GetAllGeneratedItems() => GetDescendants(this, false);
|
|||
|
|
private List<MultiTreeViewItem?> GetGeneratedChildren(ItemsControl parent) =>
|
|||
|
|
Enumerable.Range(0, parent.Items.Count)
|
|||
|
|
.Select(i => parent.ItemContainerGenerator.ContainerFromIndex(i) as MultiTreeViewItem)
|
|||
|
|
.Where(c => c != null)
|
|||
|
|
.ToList();
|
|||
|
|
private static MultiTreeViewItem? GetParentItem(TreeViewItem item) => ItemsControl.ItemsControlFromItemContainer(item) as MultiTreeViewItem;
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 用于多选树形视图中的树形视图项。
|
|||
|
|
/// </summary>
|
|||
|
|
public class MultiTreeViewItem : TreeViewItem
|
|||
|
|
{
|
|||
|
|
// 内部标志位,用于防止在父控件更新IsChecked状态时,再次触发不必要的通知
|
|||
|
|
internal bool IsUpdatingCheckState { get; set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 表示 MultiTreeViewItem 是否被选中的依赖属性。
|
|||
|
|
/// 该属性支持三态:true 表示选中,false 表示未选中,null 表示半选状态(部分子项被选中)。
|
|||
|
|
/// 当此属性值更改时,会触发相关联的逻辑来更新其所有子孙节点以及父节点的选中状态,
|
|||
|
|
/// 并同步更新 SelectedItems 集合以反映最新的用户选择。
|
|||
|
|
/// </summary>
|
|||
|
|
public static readonly DependencyProperty IsCheckedProperty =
|
|||
|
|
DependencyProperty.Register(
|
|||
|
|
nameof(IsChecked),
|
|||
|
|
typeof(bool?),
|
|||
|
|
typeof(MultiTreeViewItem),
|
|||
|
|
new FrameworkPropertyMetadata(false, OnIsCheckedChanged));
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取或设置当前 MultiTreeViewItem 是否被选中。
|
|||
|
|
/// 该属性支持三态:true 表示选中,false 表示未选中,null 表示半选状态(部分子项被选中)。
|
|||
|
|
/// 当此属性值更改时,会触发相关联的逻辑来更新其所有子孙节点以及父节点的选中状态,
|
|||
|
|
/// 并同步更新 SelectedItems 集合以反映最新的用户选择。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <value>类型为 <see cref="bool"/> 的值,指示当前项是否被选中。</value>
|
|||
|
|
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 MultiTreeView))
|
|||
|
|
{
|
|||
|
|
parent = VisualTreeHelper.GetParent(parent);
|
|||
|
|
}
|
|||
|
|
return parent as MultiTreeView;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重写这两个方法是自定义TreeViewItem的标准做法
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
protected override DependencyObject GetContainerForItemOverride() => new MultiTreeViewItem();
|
|||
|
|
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
protected override bool IsItemItsOwnContainerOverride(object item) => item is MultiTreeViewItem;
|
|||
|
|
}
|