I have an ItemsControl
with a Canvas
as its ItemsPanel
and Path
as the ItemTemplate
. The goal is to plot a graph, so the Path.Data
should contain a geometry to be drawn in the Canvas with absolute coordinates.
If I instantiate a Canvas and put the path directly inside it, it works fine.
But if I use an ItemsControl, every Path ends up wrapped inside a ContentPresenter, and then the coordinates are lost, since the ContentPresenter aligns to Canvas origin.
Here is my code:
<ItemsControl
ItemsSource="{Binding Signals}"
>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Path
Stroke="Red"
StrokeThickness="1"
StrokeDashCap="Round"
StrokeLineJoin="Round"
Stretch="Fill"
Data={Binding Converter=SignalToGeometryConverter}
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
If you really need to be using a Canvas
for the ItemsPanel
in that ItemsControl
, and you really need to fill the parent, you may be in a tough spot. If you can get by with a Grid
for the ItemsPanel
, you're all set.
What's getting you here isn't the ContentPresenter
, it's the Stretch
. That's causing the Path
to fill its parent -- but not in the way you think. "M 100,100 L 200,200"
, with any Stretch
other than None
, will draw a line starting at the upper left corner of the parent. The bounding box it's going to use is the maximum and minimum x and y values the path geometry actually uses. It assumes you only care about areas where you bothered to go. If you're superimposing multiple paths which have different upper left (and lower right) bounds, they'll all be scaled and offset differently, making a total hash of everything.
When you think about it, even if the stretching did start at 0,0 regardless, how would it know what to use for lower right bounds? The paths don't know about each other. If you had one Path, and added each of your current things to it as a different figure, that would be one thing. But this way, there's no common frame of reference that they're aware of.
So the quickest fix is to remove Stretch="Fill"
, but then nothing will stretch. And as long as you're using Canvas
for an ItemsPanel
, you may as well not try to stretch the paths, because (as far as I can tell by testing) they won't stretch anyhow in that case.
If you do want to stretch, first you need to all of your paths to have the same bounding box, whether or not they actually use the whole thing. That means first calculating how much horizontal and vertical space you're going to need, and prefixing each path data with
M 0,0 M xmax,ymax
...before moving to the actual point you want to start the Path
at.
And then change your Canvas
to a Grid
.
Here's the code I tested with:
XAML
<Grid>
<ItemsControl
ItemsSource="{Binding Signals, RelativeSource={RelativeSource AncestorType=Window}}"
>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Path
Stroke="DeepSkyBlue"
StrokeThickness="1"
Data="{Binding}"
Stretch="Fill"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
C#
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
int left = 10;
int top = 100;
int bottom = 200;
var signals =
Enumerable.Range(0, 19)
.Select(n => $"M 0,0 M 300,300 M {(n * 10) + left},{top} L {(n * 15) + left},{bottom}")
.ToList();
// Show shared bounding box
signals.Add("M 0,0 L 300,0 L 300,300 L 0,300 Z");
Signals = signals;
}
public IList Signals
{
get { return (IList)GetValue(SignalsProperty); }
set { SetValue(SignalsProperty, value); }
}
public static readonly DependencyProperty SignalsProperty =
DependencyProperty.Register("Signals", typeof(IList), typeof(MainWindow),
new PropertyMetadata(null));
}