You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF图形库开发:如何将ItemsControl Items转为可视化元素集合?

WPF图表库绑定ViewModel集合而非ChartElement集合的问题解决

问题背景

现有图表库核心逻辑为:所有图表继承自BaseChart(继承自ItemsControl),依赖ChartElements列表完成渲染,ChartContainer负责管理多图表并触发统一渲染。当前实现仅支持绑定ChartElement集合,存在两个痛点:

  • 强制ViewModel创建ObservableCollection<ChartElement>,造成视图层类型耦合
  • 因需修改ChartElement的尺寸和Canvas位置,同一数据无法在多图表复用

尝试改用绑定ViewModel集合+ItemTemplate的方案时,Items属性中存储的是ViewModel实例,无法直接转换为ChartElement;调用ItemContainerGenerator.ContainerFromItem(item)始终返回null,重写IsItemItsOwnContainerOverride和GetContainerForItemOverride也未解决问题。

解决方案

1. 修正BaseChart的容器生成逻辑

确保BaseChart将ChartElement作为每个Item的容器,并适配ViewModel集合的监听逻辑:

public abstract class BaseChart : ItemsControl
{
    public static readonly RoutedEvent DataChangedEvent;
    public static readonly DependencyProperty ColorProperty;
    private Canvas drawArea;

    public SolidColorBrush Color
    {
        get => (SolidColorBrush)GetValue(ColorProperty);
        set => SetValue(ColorProperty, value);
    }

    public event RoutedEventHandler DatasChanged
    {
        add => AddHandler(DataChangedEvent, value);
        remove => RemoveHandler(DataChangedEvent, value);
    }

    static BaseChart()
    {
        ColorProperty = DependencyProperty.Register("Color", typeof(SolidColorBrush), typeof(BaseChart));
        DataChangedEvent = EventManager.RegisterRoutedEvent("DatasChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BaseChart));
    }

    public BaseChart()
    {
        Items.SortDescriptions.Add(new SortDescription("HorizontalValue", ListSortDirection.Ascending));
        ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        drawArea = GetTemplateChild("PART_Draw") as Canvas;
        if (drawArea != null)
        {
            RaiseEvent(new RoutedEventArgs(DataChangedEvent));
            drawArea.SizeChanged += (sender, e) => Render();
        }
    }

    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        RaiseEvent(new RoutedEventArgs(DataChangedEvent));

        // 移除旧集合监听
        if (oldValue is INotifyCollectionChanged oldCollection)
        {
            oldCollection.CollectionChanged -= OnItemsCollectionChanged;
        }

        // 添加新集合监听
        if (newValue is INotifyCollectionChanged newCollection)
        {
            newCollection.CollectionChanged += OnItemsCollectionChanged;
        }
    }

    private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
    {
        // 容器生成完成后绑定ChartElement的属性变化事件
        if (ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
        {
            foreach (var item in Items)
            {
                if (ItemContainerGenerator.ContainerFromItem(item) is ChartElement element)
                {
                    element.ValuesChanged -= OnValueChanged;
                    element.ValuesChanged += OnValueChanged;
                }
            }
            RaiseEvent(new RoutedEventArgs(DataChangedEvent));
        }
    }

    private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // 处理集合新增/移除项的事件绑定
        if (e.NewItems != null)
        {
            foreach (var item in e.NewItems)
            {
                if (ItemContainerGenerator.ContainerFromItem(item) is ChartElement element)
                {
                    element.ValuesChanged += OnValueChanged;
                }
            }
        }

        if (e.OldItems != null)
        {
            foreach (var item in e.OldItems)
            {
                if (ItemContainerGenerator.ContainerFromItem(item) is ChartElement element)
                {
                    element.ValuesChanged -= OnValueChanged;
                }
            }
        }

        RaiseEvent(new RoutedEventArgs(DataChangedEvent));
    }

    private void OnValueChanged(object sender, RoutedEventArgs e)
    {
        RaiseEvent(new RoutedEventArgs(DataChangedEvent));
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return item is ChartElement;
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ChartElement();
    }

    public abstract void Render();

    public double GetVerticalPosition(double value)
    {
        double range = MaxVertical - MinVertical;
        double ratioValue = 1.0 / range * (value - MinVertical);
        return drawArea.ActualHeight - drawArea.ActualHeight * ratioValue;
    }

    public double GetHorizontalPosition(double value)
    {
        double range = MaxHorizontal - MinHorizontal;
        double ratioValue = 1.0 / range * (value - MinHorizontal);
        return drawArea.ActualWidth * ratioValue;
    }

    // 补充原有UpdateDrawingArea、MaxVertical/MinHorizontal等属性的实现
}

2. 改用ItemContainerStyle绑定ViewModel属性

无需使用ItemTemplate,通过ItemContainerStyle将ViewModel的属性直接绑定到ChartElement的依赖属性上,自动完成ViewModel到ChartElement容器的包装:

<tools:ChartContainer>
    <tools:BarChart Color="DarkOrange" ItemsSource="{Binding myViewModelCollection}">
        <tools:BarChart.ItemContainerStyle>
            <Style TargetType="tools:ChartElement">
                <Setter Property="HorizontalValue" Value="{Binding myViewModelProp1}"/>
                <Setter Property="VerticalValue" Value="{Binding myViewModelProp2}"/>
            </Style>
        </tools:BarChart.ItemContainerStyle>
    </tools:BarChart>
</tools:ChartContainer>

3. 修改Render方法,遍历ChartElement容器

原Render方法直接遍历Items(ViewModel实例),现在需要通过ItemContainerGenerator获取每个Item对应的ChartElement容器:

public class ScatterChart : BaseChart
{
    static ScatterChart()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ScatterChart), new FrameworkPropertyMetadata(typeof(ScatterChart)));
    }

    public override void Render()
    {
        if (drawArea == null || Items == null || Items.Count == 0)
            return;

        UpdateDrawingArea();

        foreach (var item in Items)
        {
            if (ItemContainerGenerator.ContainerFromItem(item) is ChartElement element)
            {
                element.Width = double.NaN;
                element.Height = double.NaN;
                Canvas.SetTop(element, GetVerticalPosition(element.VerticalValue));
                Canvas.SetLeft(element, GetHorizontalPosition(element.HorizontalValue));
            }
        }
    }
}

关键说明

  • 容器生成时机:ItemContainerGenerator是异步生成容器的,必须在StatusChanged事件中判断ContainersGenerated状态后再获取容器,避免因控件未加载完成导致返回null
  • 数据复用:ViewModel集合可被多图表绑定,每个图表会生成独立的ChartElement容器,修改容器的尺寸/位置不会影响原数据,解决了复用问题
  • 属性同步:若ViewModel实现INotifyPropertyChanged,绑定会自动同步属性变化到ChartElement,触发ValuesChanged事件进而通知ChartContainer重新渲染

内容的提问来源于stack exchange,提问作者JhonWPF

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 18:07:01