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 namedGeneric.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 theSimpleGraph
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"); }