在WPF中将Shape转换为可重用的几何体

Nic*_*ler 3 c# wpf geometry shape custom-controls

我试图将System.Windows.Shapes.Shape对象转换为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);
Run Code Online (Sandbox Code Playgroud)

我还尝试了以下替代代码:

//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.
Run Code Online (Sandbox Code Playgroud)

区别在于第二个代码段不会克隆或修改Shape.RenderedGeometry属性.

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

编辑:
我已经想出如何生成几何的外观.但这仅适用于设计模式.执行以下步骤:

  • 重建项目.
  • 转到MainWindow.xaml并单击自定义形状对象,以便将形状的属性加载到Visual Studio的属性窗口中.等到属性窗口呈现形状的样子.
  • 修改数据点集合或属性以查看正确呈现的几何体.

这就是我希望控件最终看起来像现在的样子: 在此输入图像描述

如何将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>
Run Code Online (Sandbox Code Playgroud)

DataPoint.cs
此类只有两个DependencyProperties(X和Y),并在更改任何属性时发出通知.此通知用于通过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); }
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑2:
为了回应Peter Duniho的这个答案,我想提供另一种方法来为Visual Studio创建一个自定义控件.要创建自定义控件,请执行以下步骤:

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

Generic.xaml
这是SimpleGraphPeter描述的一个例子.

<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>
Run Code Online (Sandbox Code Playgroud)

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

public SimpleGraph()
{
    DefaultStyleKey = typeof(SimpleGraph);
    DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");
}
Run Code Online (Sandbox Code Playgroud)

Pet*_*iho 5

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

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

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

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

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>
Run Code Online (Sandbox Code Playgroud)

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

这里的关键是我有ItemsControl一个默认ItemTemplate具有单个Path对象的类.该对象的几何体与controls 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>
Run Code Online (Sandbox Code Playgroud)

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

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

这是它的样子:

图表截图


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

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

  2. 继续并允许用户指定Shape对象,然后使用该对象的属性来填充绑定到模板对象属性的特定属性.这主要是给来电者带来方便; 如果有的话,它在图形控件本身中增加了一些复杂性.

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

选项#3对我来说似乎最复杂.事实上很复杂,我没有费心去做一个使用它的例子......我做了一些研究,在我看来它应该有用,但我承认我没有验证它确实如此.但就呼叫者的绝对灵活性而言,它肯定是黄金标准.