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

时间:2015-02-04 21:58:34

标签: c# wpf geometry custom-controls shape

我正在尝试将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);

我还尝试了以下备用代码:

//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设计器中查看用于数据点的几何体。但是,行为不一致,很难弄清楚如何使几何始终显示。此外,当我执行我的应用程序时,数据点永远不会出现指定的几何体。

  

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

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

以下是我希望控件最终看起来像现在的样子:   enter image description here

如何将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
这个类只有两个DependencyProperties(X&amp; 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); }
    }
}

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

编辑2:
为了回应this answer by Peter Duniho,我想提供另一种方法来向Visual Studio创建自定义控件。要创建自定义控件,请执行以下步骤:

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

Generic.xaml
以下是Peter描述的SimpleGraph的示例。

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

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

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

1 个答案:

答案 0 :(得分:5)

我认为你可能没有以最好的方式接近这个。根据您发布的代码,您似乎正在尝试手动执行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-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类,默认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个对象的值发生更改,或者数据集合本身被修改,则图表将自动更新。

这是它的样子:

Graph screenshot


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

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

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

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

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