在WPF中将形状转换为可重复使用的几何图形

3

我正在尝试将一个 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和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:
针对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个回答

5
我认为你可能没有用最好的方式处理这个问题。根据你发布的代码,似乎你试图手动完成WPF可以自动处理的事情。
最棘手的部分(至少对我来说……我几乎不是WPF专家)是你似乎想使用实际的Shape对象作为图形数据点模板,而我并不完全确定允许以编程或声明方式替换该模板的最佳方法,而不暴露控制图中位置的基本转换机制。
因此,这里有一个忽略了特定方面(我会在下面评论备选项)的示例,但我认为它否则满足你的需要。
首先,我创建了一个自定义的ItemsControl类(在Visual Studio中,我通过欺骗VS并告诉它我想添加一个UserControl来做到这一点,这使我得到了一个基于XAML的项目项......我立即在.xaml和.xaml.cs文件中都用"ItemsControl"替换"UserControl"): 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绑定到数据项的X和Y值作为平移变换的偏移量。
一个简单的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 对象的值发生更改,或者数据集合本身被修改,图形将自动更新。
以下是它的样子:
图片截图
我要注意的是,上面的示例仅允许您指定几何形状。其他形状属性在数据模板中是硬编码的。这似乎与您要求做的略有不同。但请注意,您有几个替代方案可以满足您的需求,而无需重新引入所有额外的手动绑定/更新代码:
1. 只需添加其他属性,以类似于 DataPointGeometry 属性的方式绑定到模板 Path 对象。例如 DataPointFill、DataPointStroke 等。
2. 允许用户指定 Shape 对象,然后使用该对象的属性填充绑定到模板对象属性的特定属性。这主要是对调用者的方便;如果有什么问题,它实际上会增加图形控件本身的复杂性。
3. 完全允许用户指定 Shape 对象,然后使用 XamlWriter 创建一些 XAML 代码将其转换为模板,添加必要的 Transform 元素到 XAML 中,并将其包装在 DataTemplate 声明中(例如通过将 XAML 加载为内存 DOM 来修改 XAML),然后使用 XamlReader 将 XAML 作为模板加载,然后将其分配给 ItemTemplate 属性。
对我来说,选项 #3 最复杂。实际上,它非常复杂,以至于我没有费心使用它来创建一个示例...我进行了一些研究,我认为它应该可以工作,但我承认我没有验证过它是否可以。但它肯定是绝对灵活性的黄金标准。

彼得,非常感谢你对我解决这个问题的精心关注! 顺便说一下,您不需要欺骗Visual Studio。相反,您可以创建一个Themes文件夹,并在其中创建名为Generic.xaml的资源字典。此XAML包含自定义控件的样式。有关此信息的一些信息,请参见MSDN页面:Control Authoring Overview。 创建样式后,您可以在控件的构造函数中使用DefaultStyleKey = typeof(SimpleGraph);来应用���。 - Nicholas Miller
@NickMiller:谢谢你的提示。听起来它并不完全与从“UserControl”开始相同(即,您必须通过代码显式地将XAML样式绑定到控件,而不是在项目中成对出现的XAML+代码后备),但听起来这是一种非常有用的技术。感谢分享! - Peter Duniho
对于那些感兴趣的人,我已经将我用于测试的“Generic.xaml”添加到了原帖的底部。 - Nicholas Miller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接