namespace Melskin.Controls { /// /// Accordion 控件提供了一个可折叠的面板容器,允许用户通过点击标题来展开或收起内容。 /// 该控件继承自 ItemsControl,因此可以使用数据绑定来动态添加 AccordionItem 子项。 /// 每个 AccordionItem 可以独立地设置其标题和是否默认展开的状态。 /// public class Accordion : ItemsControl { static Accordion() { DefaultStyleKeyProperty.OverrideMetadata(typeof(Accordion), new FrameworkPropertyMetadata(typeof(Accordion))); } /// protected override bool IsItemItsOwnContainerOverride(object item) { return item is AccordionItem; } /// protected override DependencyObject GetContainerForItemOverride() { return new AccordionItem(); } ///// //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; // } // } //} /// /// 重写此方法是处理数据绑定时添加事件的更可靠方式 /// /// /// 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; } } /// 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 就能接收到点击并正常勾选了。 } } } }