Convert Shape into reusable Geometry in WPF

房东的猫 提交于 2019-11-28 02:06:16

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!