Search code examples
wpfcanvasitemscontrolpathgeometry

ItemPresenter messes with PathGeometry absolute position inside ItemTemplate with a Canvas ItemPanel


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>

Solution

  • 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));
    }