在 WPF 中将 Shape 转换为可重用的 Geometry

Convert Shape into reusable Geometry in WPF

我正在尝试转换 System.Windows.Shapes.Shape object into a System.Windows.Media.Geometry 对象。

对于 Geometry 对象,我将根据一组数据点使用自定义图形控件多次渲染它。这要求 Geometry 对象的每个实例都有一个唯一的 TranslateTransform 对象。

现在,我以两种不同的方式处理这个问题,但似乎都没有正常工作。我的自定义控件使用以下代码来绘制几何图形:

//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);

我也试过以下替代代码:

//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.

不同之处在于,第二个片段不会克隆或修改 Shape.RenderedGeometry 属性。

奇怪的是,我偶尔可以在 WPF 设计器中查看用于数据点的几何图形。但是,行为不一致并且很难弄清楚如何使几何体 始终出现 。此外,当我执行我的应用程序时,数据点永远不会以指定的几何形状出现。

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:

如何将 Shape 对象转换为 Geometry 对象以进行多次渲染?

非常感谢您的帮助!


让我给出我的问题的完整上下文,以及所有必要的代码来理解我的控件是如何设置的。希望这可能表明我将 Shape 对象转换为 Geometry 对象的方法存在哪些问题。

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
这个class只有两个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
这就是控制。它包含数据点的集合并提供重新呈现数据点的机制(对 WPF 设计器很有用)。特别重要的是上面发布的逻辑,它位于 UIElement.OnRender() 方法内部。

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 , 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");
}

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

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

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

首先,我创建了一个自定义 ItemsControl class(在 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-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");
    }
}

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

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

关于那个来电者……

这是一个简单的例子,说明如何使用上面的内容:

<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>

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

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

这是它的样子:


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

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

  2. 继续并允许用户指定一个 Shape 对象,然后使用该对象的属性填充绑定到模板对象属性的特定属性。这主要是为了方便调用者;如果有的话,那就是图形控件本身有点复杂。

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

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