在 WPF 中将形状转换为可重用的几何图形 [英] Convert Shape into reusable Geometry in WPF

查看:29
本文介绍了在 WPF 中将形状转换为可重用的几何图形的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试转换 (X & Y) 并在任何这些属性发生更改时发出通知.此通知用于通过 UIElement.InvalidateVisual().

公共类 DataPoint : DependencyObject, INotifyPropertyChanged{公共静态只读 DependencyProperty XProperty = DependencyProperty.Register("XProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged));公共静态只读 DependencyProperty YProperty = DependencyProperty.Register("YProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged));私有静态无效 DataPoint_PropertyChanged(DependencyObject 发件人,DependencyPropertyChangedEventArgs e){DataPoint dp = (DataPoint)sender;dp.RaisePropertyChanged(e.Property.Name);}公共事件 PropertyChangedEventHandler PropertyChanged;受保护的无效 RaisePropertyChanged(字符串名称){if (PropertyChanged != null){PropertyChanged(this, new PropertyChangedEventArgs(name));}}公共双X{获取{返回(双)GetValue(XProperty);}设置 { SetValue(XProperty, (double)value);}}公共双Y{获取{返回(双)GetValue(YProperty);}设置 { SetValue(YProperty, (double)value);}}}

LineGraph.cs
这是控制.它包含数据点的集合并提供重新呈现数据点的机制(对 WPF 设计器有用).特别重要的是上面发布的逻辑,它位于 UIElement.OnRender() 方法.

公共类 LineGraph : FrameworkElement{public static readonly DependencyProperty DataPointShapeProperty = DependencyProperty.Register("DataPointShapeProperty", typeof(Shape), typeof(LineGraph), new FrameworkPropertyMetadata(default(Shape), FrameworkPropertyMetadataOptions.AffectsRender, DataPointShapeChanged));public static readonly DependencyProperty DataPointsProperty = DependencyProperty.Register("DataPointsProperty", typeof(ObservableCollection), typeof(LineGraph), new FrameworkPropertyMetadata(default(ObservableCollection), FrameworkPropertyMetadataOptions.AffectsRender, DataPointsChanged));私有静态无效 DataPointShapeChanged(DependencyObject 发件人,DependencyPropertyChangedEventArgs e){LineGraph g = (LineGraph)sender;g.InvalidateVisual();}私有静态无效 DataPointsChanged(DependencyObject 发件人,DependencyPropertyChangedEventArgs e){//集合引用设置或取消设置.LineGraph g = (LineGraph)sender;INotifyCollectionChanged oldValue = e.OldValue as INotifyCollectionChanged;INotifyCollectionChanged newValue = e.NewValue as INotifyCollectionChanged;如果(旧值!= null)oldValue.CollectionChanged -= g.DataPoints_CollectionChanged;如果(新值!= null)newValue.CollectionChanged += g.DataPoints_CollectionChanged;//更新点视觉效果.g.InvalidateVisual();}私有无效 DataPoints_CollectionChanged(对象发送者,NotifyCollectionChangedEventArgs e){//集合已更改(添加/删除).if (e.OldItems != null)foreach(e.OldItems 中的 INotifyPropertyChanged n){n.PropertyChanged -= DataPoint_PropertyChanged;}if (e.NewItems != null)foreach(e.NewItems 中的 INotifyPropertyChanged n){n.PropertyChanged += DataPoint_PropertyChanged;}无效视觉();}私有无效 DataPoint_PropertyChanged(对象发送者,PropertyChangedEventArgs e){//当 DataPoint 的属性发生变化时,重新渲染 LineGraph.无效视觉();}公共形状数据点形状{获取{返回(形状)GetValue(DataPointShapeProperty);}set { SetValue(DataPointShapeProperty, (Shape)value);}}public ObservableCollection<DataPoint>数据点{get { return (ObservableCollection<DataPoint>)GetValue(DataPointsProperty);}设置 { SetValue(DataPointsProperty, (ObservableCollection)value);}}公共线图(){//为数据点集合提供实例特定的值,而不是共享静态实例.SetCurrentValue(DataPointsProperty, new ObservableCollection());}受保护的覆盖无效 OnRender(DrawingContext dc){如果(数据点形状!= null){钢笔 shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);foreach(数据点中的数据点 dp){几何geo = DataPointShape.RenderedGeometry.Clone();TranslateTransform 翻译 = 新 TranslateTransform(dp.X, dp.Y);geo.Transform = 翻译;dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);}}}}

<块引用>

编辑 2:
针对 Peter Duniho 的回答,我想提供在创建自定义控件时向 Visual Studio 撒谎的替代方法.要创建自定义控件,请执行以下步骤:

  • 在名为 Themes
  • 的项目根目录下创建文件夹
  • Themes 文件夹中创建名为 Generic.xaml
  • 的资源字典
  • 在资源字典中为控件创建样式.
  • 应用控件的 C# 代码中的样式.

Generic.xaml
这是 Peter 描述的 SimpleGraph 的示例.

最后,在 SimpleGraph 构造函数中应用这样的样式:

public SimpleGraph(){DefaultStyleKey = typeof(SimpleGraph);DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");}

解决方案

我认为您可能没有以最好的方式解决这个问题.根据您发布的代码,您似乎正在尝试手动执行 WPF 在自动处理方面相当擅长的事情.

主要的棘手部分(至少对我而言…我几乎不是 WPF 专家)是您似乎想要使用实际的 Shape 对象作为图形数据点图形的模板,而且我不完全确定允许以编程方式或声明方式替换该模板而不暴露控制图上定位的底层转换机制的最佳方法.

所以这是一个忽略该特定方面的示例(我将在下面评论替代方案),但我相信它可以满足您的确切需求.

首先,我创建了一个自定义的 ItemsControl 类(在 Visual Studio 中,我通过撒谎并告诉 VS 我想添加一个 UserControl 来做到这一点,这让我得到了一个 XAML- 项目中的项目…我立即将 .xaml 和 .xaml.cs 文件中的UserControl"替换为ItemsControl"):

XAML:

<ItemsControl x:Class="TestSO28332278SimpleGraphControl.SimpleGraph"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:local="clr-命名空间:TestSO28332278SimpleGraphControl"麦克:忽略=d"x:名称=根"d:DesignHeight="300" d:DesignWidth="300"><ItemsControl.Resources><EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5"/></ItemsControl.Resources><ItemsControl.ItemsPanel><项目面板模板><Canvas IsItemsHost="True"/></ItemsPanelTemplate></ItemsControl.ItemsPanel><ItemsControl.ItemTemplate><DataTemplate DataType="{x:Type local:DataPoint}"><路径数据="{绑定元素名称=root,路径=DataPointGeometry}"Fill="Red" Stroke="Black" StrokeThickness="1"><Path.RenderTransform><TranslateTransform X="{绑定 X}" Y="{绑定 Y}"/></Path.RenderTransform></路径></数据模板></ItemsControl.ItemTemplate></ItemsControl>

C#:

公共部分类 SimpleGraph : ItemsControl{公共几何数据点几何{获取{返回(几何)GetValue(DataPointShapeProperty);}设置 { SetValue(DataPointShapeProperty, 值);}}公共静态 DependencyProperty DataPointShapeProperty = DependencyProperty.Register("DataPointGeometry", typeof(Geometry), typeof(SimpleGraph));公共简单图(){初始化组件();DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");}}

这里的关键是我有一个 ItemsControl 类,其默认 ItemTemplate 具有单个 Path 对象.该对象的几何体绑定到控件的 DataPointGeometry 属性,其 RenderTransform 绑定到数据项的 XY 值作为平移变换的偏移量.

一个简单的Canvas 用于ItemsPanel,因为我只需要一个地方来绘制东西,没有任何其他布局功能.最后,有一个资源定义了要使用的默认几何图形,以防调用者不提供.

关于那个来电者…

这是一个如何使用上述内容的简单示例:

<窗口.资源><PathGeometry x:Key="dataPointGeometry"数字="M 0.5000,0.0000L 0.6176,0.33820.9755,0.34550.6902,0.56180.7939,0.90450.5000,0.70000.2061,0.90450.3098,0.56180.0245,0.34550.3824,0.3382 Z"><PathGeometry.Transform><ScaleTransform ScaleX="20" ScaleY="20"/></PathGeometry.Transform></路径几何></Window.Resources><网格><Border Margin="3" BorderBrush="Black" BorderThickness="1"><local:SimpleGraph Width="450" Height="300" DataPointGeometry="{StaticResource dataPointGeometry}"><local:SimpleGraph.Items><local:DataPoint X="10" Y="10"/><local:DataPoint X="25" Y="25"/><local:DataPoint X="40" Y="40"/><local:DataPoint X="55" Y="55"/></local:SimpleGraph.Items></local:SimpleGraph></边界></网格></窗口>

在上面,唯一真正有趣的是我声明了一个 PathGeometry 资源,然后将该资源绑定到控件的 DataPointGeometry 属性.这允许程序为图形提供自定义几何图形.

WPF 通过隐式数据绑定和模板处理其余部分.如果任何 DataPoint 对象的值发生变化,或者数据集合本身被修改,图表将自动更新.

它是这样的:


我会注意到上面的例子只允许你指定几何.其他形状属性在数据模板中硬编码.这似乎与您要求做的略有不同.但请注意,您在这里有一些替代方案可以满足您的需求,而无需在示例中重新引入所有额外的手动绑定/更新代码:

  1. 只需添加其他属性,以类似于 DataPointGeometry 属性的方式绑定到模板 Path 对象.例如.DataPointFillDataPointStroke

  2. 继续并允许用户指定 Shape 对象,然后使用该对象的属性来填充绑定到模板对象属性的特定属性.这主要是为调用者提供方便;如果有的话,这会增加图形控件本身的复杂性.

  3. 全力以赴并允许用户指定一个 Shape 对象,然后您可以使用 XamlWriter 将其转换为模板,从而为对象,将必要的 Transform 元素添加到 XAML 并将其包装在 DataTemplate 声明中(例如,通过将 XAML 作为内存中的 DOM 加载以修改 XAML),以及然后使用 XamlReader 将 XAML 作为模板加载,然后您可以将其分配给 ItemTemplate 属性.

选项#3 对我来说似乎是最复杂的.事实上如此复杂,以至于我没有费心使用它来制作一个示例的原型…我做了一些研究,在我看来它应该可以工作,但我承认我没有亲自验证它是否有效.但就调用者的绝对灵活性而言,这肯定是黄金标准.

I am trying to convert a System.Windows.Shapes.Shape object into a System.Windows.Media.Geometry object.

With the Geometry object, I am going to render it multiple times with a custom graph control depending on a set of data points. This requires that each instance of the Geometry object has a unique TranslateTransform object.

Now, I am approaching the issue in two different ways, but neither seems to be working correctly. My custom control uses the following code in order to draw the geometry:

//Create an instance of the geometry the shape uses.
Geometry geo = DataPointShape.RenderedGeometry.Clone();
//Apply transformation.
TranslateTransform translation = new TranslateTransform(dataPoint.X, dataPoint.Y);
geo.Transform = translation;
//Create pen and draw geometry.
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);

I have also tried the following alternate code:

//Create an instance of the geometry the shape uses.
Geometry geo = DataPointShape.RenderedGeometry;
//Apply transformation.
TranslateTransform translation = new TranslateTransform(dataPoint.X, dataPoint.Y);
dc.PushTransform(translation);
//Create pen and draw geometry.
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
dc.Pop(); //Undo translation.

The difference is that the second snippet doesn't clone or modify the Shape.RenderedGeometry property.

Oddly enough, I occasionally can view the geometry used for the data points in the WPF designer. However, the behavior is inconsistent and difficult to figure out how to make the geometry always appear. Also, when I execute my application, the data points never appear with the specified geometry.

EDIT:
I have figured out how to generate the appearance of the geometry. But this only works in design-mode. Execute these steps:

  • Rebuild project.
  • Go to MainWindow.xaml and click in the custom shape object so that the shape's properties load into Visual Studio's property window. Wait until the property window renders what the shape looks like.
  • Modify the data points collection or properties to see the geometry rendered properly.

Here is what I want the control to ultimately look like for now:

How can I convert a Shape object to a Geometry object for rendering multiple times?

Your help is tremendously appreciated!


Let me give the full context of my problem, as well as all necessary code to understanding how my control is set up. Hopefully, this might indicate what problems exist in my method of converting the Shape object to a Geometry object.

MainWindow.xaml

<Window x:Class="CustomControls.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:CustomControls">
<Grid>
    <local:LineGraph>
        <local:LineGraph.DataPointShape>
            <Ellipse Width="10" Height="10" Fill="Red" Stroke="Black" StrokeThickness="1" />
        </local:LineGraph.DataPointShape>
        <local:LineGraph.DataPoints>
            <local:DataPoint X="10" Y="10"/>
            <local:DataPoint X="20" Y="20"/>
            <local:DataPoint X="30" Y="30"/>
            <local:DataPoint X="40" Y="40"/>
        </local:LineGraph.DataPoints>
    </local:LineGraph>
</Grid>

DataPoint.cs
This class just has two DependencyProperties (X & Y) and it gives a notification when any of those properties are changed. This notification is used to trigger a re-render via UIElement.InvalidateVisual().

public class DataPoint : DependencyObject, INotifyPropertyChanged
{
    public static readonly DependencyProperty XProperty = DependencyProperty.Register("XProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged));
    public static readonly DependencyProperty YProperty = DependencyProperty.Register("YProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged));

    private static void DataPoint_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        DataPoint dp = (DataPoint)sender;
        dp.RaisePropertyChanged(e.Property.Name);
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged(string name)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }

    public double X
    {
        get { return (double)GetValue(XProperty); }
        set { SetValue(XProperty, (double)value); }
    }
    public double Y
    {
        get { return (double)GetValue(YProperty); }
        set { SetValue(YProperty, (double)value); }
    }
}

LineGraph.cs
This is the control. It contains the collection of data points and provides mechanisms for re-rendering the data points (useful for WPF designer). Of particular importance is the logic posted above which is inside of the UIElement.OnRender() method.

public class LineGraph : FrameworkElement
{
    public static readonly DependencyProperty DataPointShapeProperty = DependencyProperty.Register("DataPointShapeProperty", typeof(Shape), typeof(LineGraph), new FrameworkPropertyMetadata(default(Shape), FrameworkPropertyMetadataOptions.AffectsRender, DataPointShapeChanged));
    public static readonly DependencyProperty DataPointsProperty = DependencyProperty.Register("DataPointsProperty", typeof(ObservableCollection<DataPoint>), typeof(LineGraph), new FrameworkPropertyMetadata(default(ObservableCollection<DataPoint>), FrameworkPropertyMetadataOptions.AffectsRender, DataPointsChanged));

    private static void DataPointShapeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        LineGraph g = (LineGraph)sender;
        g.InvalidateVisual();
    }

    private static void DataPointsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {   //Collection referenced set or unset.
        LineGraph g = (LineGraph)sender;
        INotifyCollectionChanged oldValue = e.OldValue as INotifyCollectionChanged;
        INotifyCollectionChanged newValue = e.NewValue as INotifyCollectionChanged;
        if (oldValue != null)
            oldValue.CollectionChanged -= g.DataPoints_CollectionChanged;
        if (newValue != null)
            newValue.CollectionChanged += g.DataPoints_CollectionChanged;

        //Update the point visuals.
        g.InvalidateVisual();
    }

    private void DataPoints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {   //Collection changed (added/removed from).
        if (e.OldItems != null)
            foreach (INotifyPropertyChanged n in e.OldItems)
            {
                n.PropertyChanged -= DataPoint_PropertyChanged;
            }
        if (e.NewItems != null)
            foreach (INotifyPropertyChanged n in e.NewItems)
            {
                n.PropertyChanged += DataPoint_PropertyChanged;
            }

        InvalidateVisual();
    }

    private void DataPoint_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        //Re-render the LineGraph when a DataPoint has a property that changes.
        InvalidateVisual();
    }

    public Shape DataPointShape
    {
        get { return (Shape)GetValue(DataPointShapeProperty); }
        set { SetValue(DataPointShapeProperty, (Shape)value); }
    }

    public ObservableCollection<DataPoint> DataPoints
    {
        get { return (ObservableCollection<DataPoint>)GetValue(DataPointsProperty); }
        set { SetValue(DataPointsProperty, (ObservableCollection<DataPoint>)value); }
    }

    public LineGraph()
    {    //Provide instance-specific value for data point collection instead of a shared static instance.
        SetCurrentValue(DataPointsProperty, new ObservableCollection<DataPoint>());
    }

    protected override void OnRender(DrawingContext dc)
    {
        if (DataPointShape != null)
        {
            Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
            foreach (DataPoint dp in DataPoints)
            {
                Geometry geo = DataPointShape.RenderedGeometry.Clone();
                TranslateTransform translation = new TranslateTransform(dp.X, dp.Y);
                geo.Transform = translation;
                dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
            }
        }
    }
}

EDIT 2:
In response to this answer by Peter Duniho, I would like to provide the alternate method to lying to Visual Studio in creating a custom control. For creating the custom control execute these steps:

  • Create folder in root of project named Themes
  • Create resource dictionary in Themes folder named Generic.xaml
  • Create a style in the resource dictionary for the control.
  • Apply the style from the control's C# code.

Generic.xaml
Here is an example of for the SimpleGraph described by Peter.

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:CustomControls">
    <Style TargetType="local:SimpleGraph" BasedOn="{StaticResource {x:Type ItemsControl}}">
        <Style.Resources>
            <EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5"/>
        </Style.Resources>
        <Style.Setters>
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <Canvas IsItemsHost="True"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="ItemTemplate">
                <Setter.Value>
                    <DataTemplate DataType="{x:Type local:DataPoint}">
                        <Path Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointFill}" 
                                Stroke="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointStroke}" 
                                StrokeThickness="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointStrokeThickness}" 
                                Data="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointGeometry}">
                            <Path.RenderTransform>
                                <TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
                            </Path.RenderTransform>
                        </Path>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style.Setters>
    </Style>
</ResourceDictionary>

Lastly, apply the style like so in the SimpleGraph constructor:

public SimpleGraph()
{
    DefaultStyleKey = typeof(SimpleGraph);
    DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");
}

解决方案

I think that you are probably not approaching this in the best way. Based on the code you posted, it seems that you are trying to do manually things that WPF is reasonably good at handling automatically.

The main tricky part (at least for me…I'm hardly a WPF expert) is that you appear to want to use an actual Shape object as the template for your graph's data point graphics, and I'm not entirely sure of the best way to allow for that template to be replaced programmatically or declaratively without exposing the underlying transformation mechanic that controls the positioning on the graph.

So here's an example that ignores that particular aspect (I will comment on alternatives below), but which I believe otherwise serves your precise needs.

First, I create a custom ItemsControl class (in Visual Studio, I do this by lying and telling VS I want to add a UserControl, which gets me a XAML-based item in the project…I immediately replace "UserControl" with "ItemsControl" in both the .xaml and .xaml.cs files):

XAML:

<ItemsControl x:Class="TestSO28332278SimpleGraphControl.SimpleGraph"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
              xmlns:local="clr-namespace:TestSO28332278SimpleGraphControl"
              mc:Ignorable="d" 
              x:Name="root"
              d:DesignHeight="300" d:DesignWidth="300">

  <ItemsControl.Resources>
    <EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5" />
  </ItemsControl.Resources>

  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <Canvas IsItemsHost="True" />
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>

  <ItemsControl.ItemTemplate>
    <DataTemplate DataType="{x:Type local:DataPoint}">
      <Path Data="{Binding ElementName=root, Path=DataPointGeometry}"
            Fill="Red" Stroke="Black" StrokeThickness="1">
        <Path.RenderTransform>
          <TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
        </Path.RenderTransform>
      </Path>
    </DataTemplate>
  </ItemsControl.ItemTemplate>

</ItemsControl>

C#:

public partial class SimpleGraph : ItemsControl
{
    public Geometry DataPointGeometry
    {
        get { return (Geometry)GetValue(DataPointShapeProperty); }
        set { SetValue(DataPointShapeProperty, value); }
    }

    public static DependencyProperty DataPointShapeProperty = DependencyProperty.Register(
        "DataPointGeometry", typeof(Geometry), typeof(SimpleGraph));

    public SimpleGraph()
    {
        InitializeComponent();

        DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");
    }
}

The key here is that I have an ItemsControl class with a default ItemTemplate that has a single Path object. That object's geometry is bound to the controls DataPointGeometry property, and its RenderTransform is bound to the data item's X and Y values as offsets for a translation transform.

A simple Canvas is used for the ItemsPanel, as I just need a place to draw things, without any other layout features. Finally, there is a resource defining a default geometry to use, in case the caller doesn't provide one.

And about that caller…

Here is a simple example of how one might use the above:

<Window x:Class="TestSO28332278SimpleGraphControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TestSO28332278SimpleGraphControl"
        Title="MainWindow" Height="350" Width="525">

  <Window.Resources>
    <PathGeometry x:Key="dataPointGeometry"
                  Figures="M 0.5000,0.0000
                  L 0.6176,0.3382
                  0.9755,0.3455
                  0.6902,0.5618
                  0.7939,0.9045
                  0.5000,0.7000
                  0.2061,0.9045
                  0.3098,0.5618
                  0.0245,0.3455
                  0.3824,0.3382 Z">
      <PathGeometry.Transform>
        <ScaleTransform ScaleX="20" ScaleY="20" />
      </PathGeometry.Transform>
    </PathGeometry>
  </Window.Resources>

  <Grid>
    <Border Margin="3" BorderBrush="Black" BorderThickness="1">
      <local:SimpleGraph Width="450" Height="300" DataPointGeometry="{StaticResource dataPointGeometry}">
        <local:SimpleGraph.Items>
          <local:DataPoint X="10" Y="10" />
          <local:DataPoint X="25" Y="25" />
          <local:DataPoint X="40" Y="40" />
          <local:DataPoint X="55" Y="55" />
        </local:SimpleGraph.Items>
      </local:SimpleGraph>
    </Border>
  </Grid>
</Window>

In the above, the only truly interesting thing is that I declare a PathGeometry resource, and then bind that resource to the control's DataPointGeometry property. This allows the program to provide a custom geometry for the graph.

WPF handles the rest through implicit data binding and templating. If the values of any of the DataPoint objects change, or the data collection itself is modified, the graph will be updated automatically.

Here's what it looks like:


I will note that the above example only allows you to specify the geometry. The other shape attributes are hard-coded in the data template. This seems slightly different from what you asked to do. But note that you have a few alternatives here that should address your need without requiring the reintroduction of all the extra manual-binding/updating code in your example:

  1. Simply add other properties, bound to the template Path object in a fashion similar to the DataPointGeometry property. E.g. DataPointFill, DataPointStroke, etc.

  2. Go ahead and allow the user to specify a Shape object, and then use the properties of that object to populate specific properties bound to the properties of the template object. This is mainly a convenience to the caller; if anything, it's a bit of added complication in the graph control itself.

  3. Go whole-hog and allow the user to specify a Shape object, which you then convert to a template by using XamlWriter to create some XAML for the object, add the necessary Transform element to the XAML and wrap it in a DataTemplate declaration (e.g. by loading the XAML as an in-memory DOM to modify the XAML), and then using XamlReader to then load the XAML as a template which you can then assign to the ItemTemplate property.

Option #3 seems the most complicated to me. So complicated in fact that I did not bother to prototype an example using it…I did a little research and it seems to me that it should work, but I admit that I did not verify for myself that it does. But it would certainly be the gold standard in terms of absolute flexibility for the caller.

这篇关于在 WPF 中将形状转换为可重用的几何图形的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆