动态数据显示图形的可变数量的矩形

本文关键字:显示 显示图 图形 动态 数据 | 更新日期: 2023-09-27 18:03:51

我正在尝试绘制用户的输入数据,这些数据最终将在图中成为一系列矩形(不同大小和位置,不重叠)。我读过的所有示例要么只有点数可变的情节线,要么在XAML中对形状进行硬编码。但我不知道数据需要多少个矩形。我的理想情况是遵循MVVM,简单地从XAML绑定到我可以修改的ObservableCollection,但我看到的大多数示例似乎都使用了代码隐藏,直接访问ChartPlotter。以下是我绘制的一些简单矩形和一个修改后的矩形,它可以工作:

VisualizationWindow.xaml

<Window x:Class="View.VisualizationWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d3="http://research.microsoft.com/DynamicDataDisplay/1.0"
        Title="VisualizationWindow" MinHeight="300" MinWidth="500" Height="300" Width="500">
    <Grid>
        <d3:ChartPlotter Name="Chart">
            <d3:RectangleHighlight Name="Rect1" Bounds="-1,-1.5,.5,2" StrokeThickness="3" Fill="Blue" ToolTip="Blue!"></d3:RectangleHighlight>
            <d3:RectangleHighlight Name="Rect2" Bounds="1,1.5,2,.5" StrokeThickness="1" Fill="Red" ToolTip="Red!"></d3:RectangleHighlight>
        </d3:ChartPlotter>
    </Grid>
</Window>

VisualizationWindow.xaml.cs

public partial class VisualizationWindow : Window
{
    public VisualizationWindow(ListViewModel vm)
    {
        InitializeComponent();
        this.DataContext = vm;
        Chart.Viewport.Visible = new Rect(-1, -1, .5, .5);
        Rect1.Bounds = new Rect(0,0,.3,.3);
    }
}

关于动态数据显示的文档几乎不存在。我很想知道,如果D3不能优雅地做到这一点,其他库是否可以更容易地做到这一点。

动态数据显示图形的可变数量的矩形

为现有类添加ItemsSource处理。

似乎他们已经做了一切使ChartPlotter只接受IPlotterElements。没有ItemsSource属性,所以Children总是返回实际的元素。RectangleHighlight的Bounds属性是不可绑定的,并且类是密封的,禁止任何方法重写该属性。

我们可以从类中派生来"注入"ItemsSource处理。它不会像真正的交易那样工作,因为没有一种非hack的方式来让Children属性反映数据绑定。但是,我们仍然可以这样分配ItemsSource。

我们需要一些东西。我们需要实际的ItemsSource属性。一种对设定做出反应的方式。如果我们想要绑定到数据对象,一种处理数据模板的方法。我还没有深入研究现有的源代码。然而,我确实想出了一种方法来处理没有DataTemplateSelector的DataTemplates。但是它也不能与DataTemplateSelector一起工作,除非你修改我的例子。

这个答案假设你知道绑定是如何工作的,所以我们将直接跳到初始类,不做太多的指导。

Xaml第一:

<local:DynamicLineChartPlotter Name="Chart" ItemsSource="{Binding DataCollection}">
    <local:DynamicLineChartPlotter .Resources>
        <DataTemplate DataType{x:Type local:RectangleHighlightDataObject}>
            <d3:RectangleHighlight 
                Bounds="{Binding Bounds}" 
                StrokeThickness="{Binding StrokeThickness}"
                Fill="{Binding Fill}"
                ToolTip="{Binding ToolTip}"
            />
        </DataTemplate>
    </local:DynamicLineChartPlotter .Resources>
</local:DynamicLineChartPlotter >

类:

public class RectangleHighlightDataObject
{
    public Rect Bounds { get; set; }
    public double StrokeThickness { get; set; }
    public Brush Fill { get; set; }
    public String ToolTip { get; set; }
}

public class VisualizationWindow
{
    public VisualizationWindow() 
    {
         DataCollection.Add(new RectangleHighlightDataObject()
         {
             Bounds = new Rect(-1,-1.5,.5,2),
             StrokeThickness = 3,
             Fill = Brushes.Blue,
             ToolTip = "Blue!"
         });
         DataCollection.Add(new RectangleHighlightDataObject()
         {
             Bounds = new Rect(1,1.5,2,.5),
             StrokeThickness = 1,
             Fill = Brushes.Red,
             ToolTip = "Red!"
         });
    }
    public ObservableCollection<RectangleHighlightDataObject> DataCollection = 
             new ObservableCollection<RectangleHighlightDataObject>();
}

你必须使用一个来自ChartPlotter的派生类,而不是实现ItemsSource。

从如何实现动态D3类型的讨论中收集的示例。我修改为使用datatemplate而不是实际的对象元素,这是为了支持op中的数据绑定。

public class DynamicLineChartPlotter : Microsoft.Research.DynamicDataDisplay.ChartPlotter
{
    public static DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource",
                                        typeof(IEnumerable),
                                        typeof(DynamicLineChartPlotter),
                                        new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Bindable(true)]
    public IEnumerable ItemsSource
    {
        get
        {
            return (IEnumerable)GetValue(ItemsSourceProperty);
        }
        set
        {
            if (value == null)
                ClearValue(ItemsSourceProperty);
            else
                SetValue(ItemsSourceProperty, value);
        }
    }
    private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        DynamicLineChartPlotter control = (DynamicLineChartPlotter)d;
        IEnumerable oldValue = (IEnumerable)e.OldValue;
        IEnumerable newValue = (IEnumerable)e.NewValue;
        if (e.OldValue != null)
        {
            control.ClearItems();
        }
        if (e.NewValue != null)
        {
            control.BindItems((IEnumerable)e.NewValue);
        }
    }
    private void ClearItems()
    {
        Children.Clear();
    }
    private void BindItems(IEnumerable items)
    {
        foreach (var item in items)
        {
            var template = GetTemplate(item);
            if (template == null) continue;
            FrameworkElement obj = template.LoadContent() as FrameworkElement;
            obj.DataContext = item;
            Children.Add((IPlotterElement)obj);
        }
    }
    private DataTemplate GetTemplate(object item)
    {
        foreach (var key in this.Resources.Keys)
        {
            if (((DataTemplateKey)key).DataType as Type == item.GetType())
            {
                return (DataTemplate)this.Resources[key];
            }
        }
        return null;
    }
}

这就是你碰壁的地方。

RectangleHighlight Bounds属性不能被数据绑定。你也不能通过推导来解决这个问题。

我们可以通过拉出数据模板并生成一个静态的RectangleHighlight来解决这个问题,但是如果数据值发生了变化,我们就完了。

那么,如何解决这个问题呢?

我们可以使用附加属性!

使用附加属性

我们将创建一个静态类来处理附加的属性。它响应OnPropertyChanged来手动创建和设置真实的属性。这只能用一种方法。如果碰巧更改了属性,则不会更新附加的属性。但是,这应该不是问题,因为我们只需要更新我们的数据对象。

添加这个类

public class BindableRectangleBounds : DependencyObject
{
    public static DependencyProperty BoundsProperty = DependencyProperty.RegisterAttached("Bounds", typeof(Rect), typeof(BindableRectangleBounds), new PropertyMetadata(new Rect(), OnBoundsChanged));
    public static void SetBounds(DependencyObject dp, Rect value)
    {
        dp.SetValue(BoundsProperty, value);
    }
    public static void GetBounds(DependencyObject dp)
    {
        dp.GetValue(BoundsProperty);
    }
    public static void OnBoundsChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
    {
        var property = dp.GetType().GetProperty("Bounds");
        if (property != null)
        {
            property.SetValue(dp, args.NewValue, null);
        }
    }
}

然后从

更改XAML行
                Bounds="{Binding Bounds}" 

                local:BindableRectangleBounds.Bounds="{Binding Bounds}" 

响应收集更改。

到目前为止一切顺利。但是OP注意到,如果他更改了分配给ItemsSource的集合,控件中没有任何变化。那是因为我们只在ItemsSource被赋值时添加子节点。现在,除非知道ItemsControl是如何实现ItemsSource的。我知道我们可以通过在集合更改时注册ObservableCollection的事件来解决这个问题。我放了一个简单的方法,在集合发生变化时重新绑定控件。这只会在ItemsSource是由ObservableCollection分配的情况下起作用。但我认为这将有ItemsControl没有ObservableCollection相同的问题。

我们仍然没有重新分配dataContext。所以不要指望改变Children会正确地重新绑定。但是,如果您直接更改子节点,而不是ItemsControl中的ItemsSource,则会丢失绑定。所以我很可能走上正轨。唯一的奇怪之处在于,当你在设置ItemsSource后引用Children时,ItemsControl会返回ItemsSource。我还没有解决这个问题。我能想到的唯一一件事就是隐藏children属性,但这不是一件好事,如果你把这个控件称为ChartPlotter,那就行不通了。

添加以下

    public DynamicLineChartPlotter()
    {
        _HandleCollectionChanged = new NotifyCollectionChangedEventHandler(collection_CollectionChanged);
    }
    private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        DynamicLineChartPlotter control = (DynamicLineChartPlotter)d;
        IEnumerable oldValue = (IEnumerable)e.OldValue;
        IEnumerable newValue = (IEnumerable)e.NewValue;
        INotifyCollectionChanged collection = e.NewValue as INotifyCollectionChanged;
        INotifyCollectionChanged oldCollection = e.OldValue as INotifyCollectionChanged;
        if (e.OldValue != null)
        {
            control.ClearItems();
        }
        if (e.NewValue != null)
        {
            control.BindItems((IEnumerable)e.NewValue);
        }
        if (oldCollection != null)
        {
            oldCollection.CollectionChanged -= control._HandleCollectionChanged;
            control._Collection = null;
        }
        if (collection != null)
        {
            collection.CollectionChanged += control._HandleCollectionChanged;
            control._Collection = newValue;
        }
    }
    void collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        ClearItems();
        BindItems(_Collection);
    }
    NotifyCollectionChangedEventHandler _HandleCollectionChanged;
    IEnumerable _Collection;