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 就能接收到点击并正常勾选了。
}
}
}
}