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

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

问题描述

我试图转换一个(X& Y),当任何这些属性发生变化时它会发出通知。此通知用于通过 UIElement.InvalidateVisual()

  public class DataPoint:DependencyObject,INotifyPropertyChanged 
$ b public static readonly DependencyProperty XProperty = DependencyProperty.Register(XProperty,typeof(double),typeof(DataPoint),新的FrameworkPropertyMetadata(0.0d,DataPoint_PropertyChanged));
public static readonly DependencyProperty YProperty = DependencyProperty.Register(YProperty,typeof(double),typeof(DataPoint),新的FrameworkPropertyMetadata(0.0d,DataPoint_PropertyChanged));

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

公共事件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);} }


code
$ b LineGraph.cs

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

  public class LineGraph:FrameworkElement $ (DataPointShapeProperty),typeof(Shape),typeof(LineGraph),新的FrameworkPropertyMetadata(默认(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引用的set或unset。
LineGraph g =(LineGraph)发件人;
INotifyCollectionChanged oldValue = e.OldValue作为INotifyCollectionChanged;
INotifyCollectionChanged newValue = e.NewValue作为INotifyCollectionChanged;
if(oldValue!= null)
oldValue.CollectionChanged - = g.DataPoints_CollectionChanged;
if(newValue!= null)
newValue.CollectionChanged + = g.DataPoints_CollectionChanged;

//更新点视觉效果。
g.InvalidateVisual();
}

private void DataPoints_CollectionChanged(对象发送者,NotifyCollectionChangedEventArgs e)
{//集合已更改(添加/删除)。
if(e.OldItems!= null)
foreach(e.OldItems中的INotifyPropertyChanged)
{
n.PropertyChanged - = DataPoint_PropertyChanged;

if(e.NewItems!= null)
foreach(e.NewItems中的INotifyPropertyChanged n)
{
.PropertyChanged + = DataPoint_PropertyChanged;
}

InvalidateVisual();
}

private void DataPoint_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
//当DataPoint的属性发生变化时重新呈现LineGraph。
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()
{//为数据点集合提供实例特定的值而不是共享的静态实例。
SetCurrentValue(DataPointsProperty,new ObservableCollection< DataPoint>());

$ b $保护覆盖无效OnRender(DrawingContext dc)
{
if(DataPointShape!= null)
{
Pen shapePen = new笔(DataPointShape.Stroke,DataPointShape.StrokeThickness);
foreach(DataPoint中的DataPoint dp)
{
Geometry geo = DataPointShape.RenderedGeometry.Clone();
TranslateTransform translation = new TranslateTransform(dp.X,dp.Y);
geo.Transform = translation;
dc.DrawGeometry(DataPointShape.Fill,shapePen,geo);






$ blockquote

编辑2:
为了回应 Peter Duniho的回答,我想提供一种替代方法来向Visual Studio讲述创建自定义控件。创建自定义控件的步骤如下:


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



Generic.xaml
下面的例子描述了 SimpleGraph 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:SimpleGraphBasedOn ={StaticResource {x:Type ItemsControl}}>
< Style.Resources>
< EllipseGeometry x:Key =defaultGraphGeometryCenter =5,5RadiusX =5RadiusY =5/>
< /Style.Resources>
< Style.Setters>
< Setter Property =ItemsPanel>
< Setter.Value>
< ItemsPanelTemplate>
< Canvas IsItemsHost =True/>
< / ItemsPanelTemplate>
< / 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>
< /Style.Setters>
< / style>
< / ResourceDictionary>

最后,在 SimpleGraph

  public SimpleGraph()
{
DefaultStyleKey = typeof(SimpleGraph);
DataPointGeometry =(几何)FindResource(defaultGraphGeometry);
}



解决方案

<我认为你可能没有以最好的方式来解决这个问题。根据您发布的代码,似乎您正尝试手动执行WPF自动处理的相关内容。



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



下面是一个例子,忽略那个特定的方面(我会对下面的替代方案进行评论),但是我相信否则它会满足您的确切需求。首先,我创建一个自定义 ItemsControl class(在Visual Studio中,我通过说谎和告诉VS来做到这一点)我想添加一个 UserControl ,这使我得到一个基于XAML的项目在项目中......我立即替换UserCon trol在.xaml和.xaml.cs文件中都带有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-兼容性/ 2006
xmlns:d =http://schemas.microsoft.com/expression/blend/2008
xmlns:local =clr-namespace:TestSO28332278SimpleGraphControl
mc: Ignorable =d
x:Name =root
d:DesignHeight =300d:DesignWidth =300>

< ItemsControl.Resources>
< EllipseGeometry x:Key =defaultGraphGeometryCenter =5,5RadiusX =5RadiusY =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 =RedStroke =BlackStrokeThickness =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 =(几何)FindResource(defaultGraphGeometry);




$ b $ p
$ b

这里的关键是我有一个 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 =MainWindowHeight =350Width =525>

< Window.Resources>
图形=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 =20ScaleY =20/>
< /PathGeometry.Transform>
< / PathGeometry>
< /Window.Resources>

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

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



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



下面是它的样子:







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

  • 只需以类似于 DataPointGeometry Path 对象的其他属性>属性。例如。 DataPointFill DataPointStroke 等。

  • 继续并允许用户指定 Shape 对象,然后使用该对象的属性来填充绑定到模板对象属性的特定属性。这主要是为了方便呼叫者;如果有的话,这在图形控件本身中会增加一些复杂性。
  • 全盘使用并允许用户指定 Shape 对象,然后使用 XamlWriter 为对象创建一些XAML,然后将其转换为模板,添加必要的元素转换为XAML并将其包装在 DataTemplate 声明中(例如,将XAML作为内存中的DOM加载以修改XAML)然后使用 XamlReader 来加载XAML作为模板,然后可以将它分配给 ItemTemplate 属性。 p>


  • 选项#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天全站免登陆