130 lines
6.0 KiB
C#
130 lines
6.0 KiB
C#
namespace VariaStudio.Controls
|
||
{
|
||
/// <summary>
|
||
/// Accordion 控件提供了一个可折叠的面板容器,允许用户通过点击标题来展开或收起内容。
|
||
/// 该控件继承自 ItemsControl,因此可以使用数据绑定来动态添加 AccordionItem 子项。
|
||
/// 每个 AccordionItem 可以独立地设置其标题和是否默认展开的状态。
|
||
/// </summary>
|
||
public class Accordion : ItemsControl
|
||
{
|
||
static Accordion()
|
||
{
|
||
DefaultStyleKeyProperty.OverrideMetadata(typeof(Accordion), new FrameworkPropertyMetadata(typeof(Accordion)));
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
protected override bool IsItemItsOwnContainerOverride(object item)
|
||
{
|
||
return item is AccordionItem;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
protected override DependencyObject GetContainerForItemOverride()
|
||
{
|
||
return new AccordionItem();
|
||
}
|
||
|
||
///// <inheritdoc />
|
||
//protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||
//{
|
||
// base.OnItemsChanged(e);
|
||
|
||
// if (e.NewItems != null)
|
||
// {
|
||
// // 注意:这里需要确保 e.NewItems 中的项是 AccordionItem 类型
|
||
// // 当通过 ItemsSource 绑定时,它们最初是数据项,在 GetContainerForItemOverride 之后才被包装
|
||
// // 因此,事件处理最好在容器生成后进行。
|
||
// // 为了简单起见,我们在此处添加事件,但这依赖于 ItemsSource 中的项已经是 AccordionItem。
|
||
// // 一个更健壮的方法是重写 PrepareContainerForItemOverride。
|
||
// foreach (AccordionItem item in e.NewItems)
|
||
// {
|
||
// item.PreviewMouseLeftButtonDown += AccordionItem_PreviewMouseLeftButtonDown;
|
||
// }
|
||
// }
|
||
|
||
// if (e.OldItems != null)
|
||
// {
|
||
// foreach (AccordionItem item in e.OldItems)
|
||
// {
|
||
// item.PreviewMouseLeftButtonDown -= AccordionItem_PreviewMouseLeftButtonDown;
|
||
// }
|
||
// }
|
||
//}
|
||
|
||
/// <summary>
|
||
/// 重写此方法是处理数据绑定时添加事件的更可靠方式
|
||
/// </summary>
|
||
/// <param name="element"></param>
|
||
/// <param name="item"></param>
|
||
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
|
||
{
|
||
base.PrepareContainerForItemOverride(element, item);
|
||
if (element is AccordionItem accordionItem)
|
||
{
|
||
accordionItem.PreviewMouseLeftButtonDown -= AccordionItem_PreviewMouseLeftButtonDown; // 防止重复添加
|
||
accordionItem.PreviewMouseLeftButtonDown += AccordionItem_PreviewMouseLeftButtonDown;
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
protected override void ClearContainerForItemOverride(DependencyObject element, object item)
|
||
{
|
||
if (element is AccordionItem accordionItem)
|
||
{
|
||
accordionItem.PreviewMouseLeftButtonDown -= AccordionItem_PreviewMouseLeftButtonDown;
|
||
}
|
||
|
||
base.ClearContainerForItemOverride(element, item);
|
||
}
|
||
|
||
private void AccordionItem_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||
{
|
||
if (sender is AccordionItem clickedItem)
|
||
{
|
||
// 获取点击源的模板父级(即该视觉元素属于哪个控件)
|
||
var originalSource = e.OriginalSource as FrameworkElement;
|
||
var parentControl = originalSource?.TemplatedParent as System.Windows.Controls.Primitives.ToggleButton;
|
||
|
||
// 【核心修复】:判断是否为有效的标题点击
|
||
// 1. 必须是 ToggleButton (HeaderButton 是 ToggleButton)
|
||
// 2. 必须 不是 CheckBox (排除内容里的复选框)
|
||
// 3. 必须 不是 RadioButton (排除内容里的单选框)
|
||
bool isHeaderClick = parentControl != null
|
||
&& parentControl is not System.Windows.Controls.CheckBox
|
||
&& parentControl is not System.Windows.Controls.RadioButton;
|
||
|
||
// 只有确信点击的是标题栏(HeaderButton)时,才由 Accordion 接管处理
|
||
if (isHeaderClick)
|
||
{
|
||
if (clickedItem.IsExpanded)
|
||
{
|
||
clickedItem.IsExpanded = false;
|
||
}
|
||
else
|
||
{
|
||
// 折叠其他所有项
|
||
// 注意:ItemContainerGenerator 可能只包含已生成的容器,直接遍历 Items 有时更安全,
|
||
// 但如果用了虚拟化,ContainerFromIndex 可能会返回 null。
|
||
// 对于 Accordion 通常项数不多,直接遍历 Items 对应的 Container 即可。
|
||
foreach (var item in this.Items)
|
||
{
|
||
if (this.ItemContainerGenerator.ContainerFromItem(item) is AccordionItem container && container != clickedItem)
|
||
{
|
||
container.IsExpanded = false;
|
||
}
|
||
}
|
||
|
||
clickedItem.IsExpanded = true;
|
||
}
|
||
|
||
// 标记事件已处理,防止事件继续冒泡触发 ToggleButton 自身的点击行为
|
||
// (因为我们在这里手动切换了 IsExpanded,如果 ToggleButton 再处理一次可能会抵消)
|
||
e.Handled = true;
|
||
}
|
||
|
||
// 如果不是 Header 点击(例如点了 CheckBox),什么都不做,让事件继续传播,
|
||
// 这样 CheckBox 就能接收到点击并正常勾选了。
|
||
}
|
||
}
|
||
}
|
||
} |