WPF: Display polyline and points simultaneously - wpf

I have a list of line segments and each line segment contains a list of points. Being contained on the same canvas, I want to display each line segment and simultaneously mark each point location (ie w/ an ellipse). I can use an ItemsControl to display the segments but I'm stuck at how to display the points. I began implementing a custom control derived from Shape, but there must be an easier way. Thanks in advance for the help.
public class VesselAnatomy : IEnumerable, INotifyCollectionChanged
{
...
List<BaseVessel> _Segments;
...
}
public class BaseVessel : INotifyPropertyChanged
{
...
ObservableCollection<Point> _VesselPoints;
public ObservableCollection<Point> VesselPoints
{
get
{
return _VesselPoints;
}
}
...
}
public MainWindow()
{
...
VesselAnatomy Vessels = new VesselAnatomy();
...
MasterContainer.DataContext = Vessels;
...
}
<ItemsControl x:Name="VesselDisplay"
Height="750"
Width="750"
ItemsSource="{Binding}">
<Polyline Points="{Binding VesselPoints, Converter={StaticResource ObsListPointConverter}}"
Stroke="Red"
StrokeThickness="7">
<Polyline.ToolTip>
<ToolTip>
<TextBlock Text="{Binding Name}"/>
</ToolTip>
</Polyline.ToolTip>
</Polyline>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

You can use an ItemsControl for the points aswell just change the ItemsPanel and bind the elements positions.
<Window ... >
<Window.Resources>
<PointCollection x:Key="points">
<Point X="20" Y="20" />
<Point X="40" Y="35" />
<Point X="60" Y="40" />
<Point X="80" Y="60" />
<Point X="100" Y="40" />
<Point X="120" Y="30" />
<Point X="140" Y="40" />
<Point X="160" Y="20" />
</PointCollection>
<DataTemplate DataType="{x:Type Point}">
<Ellipse Width="9" Height="9" Fill="White" Stroke="DodgerBlue" StrokeThickness="1" x:Name="e">
<Ellipse.RenderTransform>
<TransformGroup>
<TranslateTransform X="-4" Y="-4" />
<TranslateTransform X="{Binding X}" Y="{Binding Y}" />
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
</DataTemplate>
</Window.Resources>
<Grid>
<Polyline x:Name="line" Stroke="LightBlue" StrokeThickness="2" Points="{StaticResource points}" />
<ItemsControl x:Name="ptsdisplay" ItemsSource="{StaticResource points}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</Window>
If you have lots of points and this method is too slow try http://msdn.microsoft.com/en-us/magazine/dd483292.aspx

Related

Canvas with static content and items

I have a Canvas which contains static elements - those elements use binding to draw it in the right places. Now I need to draw other elements depending on items in a collection. I want to use ItemsControl, but I don't know how to do it correctly. My current pseudo-code:
<UserControl.Resources>
<RectangleGeometry x:Key="MyGeometry1">
<RectangleGeometry.Rect>
<MultiBinding Converter="{StaticResource RectConverter}">
<Binding Path="ActualWidth" ElementName="m_Canvas" />
<Binding Path="ActualHeight" ElementName="m_Canvas" />
</MultiBinding>
</RectangleGeometry.Rect>
</RectangleGeometry>
</UserControl.Resources>
<Canvas x:Name="m_Canvas">
<!-- "static" content -->
<Line x:Name="Line1" X1="{Binding Line1X1}" X2="{Binding Line1X2}" Y1="{Binding Line1Y1}" Y2="{Binding Line1Y2}"/>
<Line X1="0" X2="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Canvas}}}" Y1="0" Y2="0">
<Path Fill="#35A500"
Opacity="0.15">
<Path.Data>
<GeometryGroup FillRule="EvenOdd">
<StaticResource ResourceKey="MyGeometry1" />
</GeometryGroup>
</Path.Data>
</Path>
<Line X1="{Binding Items[0].X1}" X2="{Binding Items[0].X2}" Y1="{Binding Items[0].Y1}" Y2="{Binding Items[0].Y2}"/>
<Line X1="{Binding Items[1].X1}" X2="{Binding Items[1].X2}" Y1="{Binding Items[1].Y1}" Y2="{Binding Items[1].Y2}"/>
<Line X1="{Binding Items[2].X1}" X2="{Binding Items[2].X2}" Y1="{Binding Items[2].Y1}" Y2="{Binding Items[2].Y2}"/>
</Canvas>
I tried something like this:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas>
<!-- "static" content -->
<Line x:Name="Line1" X1="{Binding Line1X1}" X2="{Binding Line1X2}" Y1="{Binding Line1Y1}" Y2="{Binding Line1Y2}"/>
<Line X1="0" X2="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Canvas}}}" Y1="0" Y2="0">
</Canvas>
</ItemsPanelTemplate>
<!-- (...) other code -->
</ItemsControl>
But I have an error:
Error XDG0062 Cannot explicitly modify Children collection of Panel used as ItemsPanel for ItemsControl. ItemsControl generates child elements for Panel.
I understand that is because I put Line1 inside Canvas but how to make such Canvas with some kind static content and also as a container for items?
The "static content" could be put into another Canvas above or below the ItemsPresenter in the ControlTemplate of the ItemsControl:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<Grid>
<Canvas>
<!-- "static" content -->
</Canvas>
<ItemsPresenter/>
</Grid>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>

PointCollection resource from individual Point resources in XAML?

If I had to create a collection of points as XAML resource, I'd do this:
<Window.Resources>
<PointCollection x:Key="points">
<Point>0,30</Point>
<Point>20,50</Point>
<Point>40,10</Point>
</PointCollection>
</Window.Resources>
In my case the points are already resources:
<Window.Resources>
<Point x:Key="a" X="100" Y="100"/>
<Point x:Key="b" X="200" Y="100"/>
<Point x:Key="b1a" X="100" Y="0"/>
<Point x:Key="b1b" X="200" Y="0"/>
</Window.Resources>
and this way (which is probably already over-killing) doesn't work, as X/Y are not dependency properties:
<Window.Resources>
<PointCollection x:Key="b1points">
<Point X="{Binding Source={StaticResource b1a}, Path=X}"
Y="{Binding Source={StaticResource b1a}, Path=Y}"/>
<Point X="{Binding Source={StaticResource b1b}, Path=X}"
Y="{Binding Source={StaticResource b1b}, Path=Y}"/>
<Point X="{Binding Source={StaticResource b}, Path=X}"
Y="{Binding Source={StaticResource b}, Path=Y}"/>
</Window.Resources>
The collection is used in a Bezier segment later:
<PolyBezierSegment Points="{StaticResource b1points}"/>
but the points must be declared individually, so that they can be used like:
<Ellipse Canvas.Left="{Binding Source={StaticResource a}, Path=X}"
Canvas.Top="{Binding Source={StaticResource a}, Path=Y}"
Width="3" Height="3" Fill="Red"/>
Is someone able to suggest a mean in XAML? and even more difficult, without a converter?
This should work:
<Window.Resources>
<Point x:Key="a" X="100" Y="100"/>
<Point x:Key="b" X="200" Y="100"/>
<Point x:Key="b1a" X="100" Y="0"/>
<Point x:Key="b1b" X="200" Y="0"/>
<PointCollection x:Key="b1points">
<StaticResource ResourceKey="b1a"/>
<StaticResource ResourceKey="b1b"/>
<StaticResource ResourceKey="a"/>
<StaticResource ResourceKey="b"/>
</PointCollection>
</Window.Resources>
...
<PolyBezierSegment Points="{StaticResource b1points}"/>
...
<Path Fill="Red">
<Path.Data>
<EllipseGeometry Center="{StaticResource a}" RadiusX="1.5" RadiusY="1.5"/>
</Path.Data>
</Path>

multiple items in ItemsControl on a Canvas

In the XAML below, please fill in the "WhatGoesHere" node and explain to me how that won't mess up the coordinates on the Canvas.
<ItemsControl ItemsSource="{Binding ViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<WhatGoesHere?>
<Path Stroke="CornflowerBlue" StrokeThickness="2">
<Path.Data>
<PathGeometry Figures="{Binding Figures}"/>
</Path.Data>
</Path>
<Path Stroke="Red" StrokeThickness="2">
<Path.Data>
<PathGeometry Figures="{Binding Figures2}"/>
</Path.Data>
</Path>
</WhatGoesHere?>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
My example has two of the same type of object in the template, but I will have several other control types in there as well.
You have more than one element within a DataTemplate. You would have to put your two Path objects into some kind of Panel, like for example a Grid. The question is where are all your the canvas coordinates computed so that you can bind the Grid to it? In your view model? Then you could bind to it and it could look somewhat like this:
<ItemsControl ItemsSource="{Binding ViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Canvas.Top="{Binding Y}" Canvas.Left="{Binding X}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Path Stroke="CornflowerBlue" StrokeThickness="2">
<Path.Data>
<PathGeometry Figures="{Binding Figures}"/>
</Path.Data>
</Path>
<Path Stroke="Red" StrokeThickness="2" Grid.Row="1">
<Path.Data>
<PathGeometry Figures="{Binding Figures2}"/>
</Path.Data>
</Path>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In case you do not have these coordinates, then I would recommend to use another value for ItemsPanelTemplate like a VirtualizedStackPanel. This might look like this:
<ItemsControl ItemsSource="{Binding ViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizedStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Path Stroke="CornflowerBlue" StrokeThickness="2">
<Path.Data>
<PathGeometry Figures="{Binding Figures}"/>
</Path.Data>
</Path>
<Path Stroke="Red" StrokeThickness="2" Grid.Row="1">
<Path.Data>
<PathGeometry Figures="{Binding Figures2}"/>
</Path.Data>
</Path>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
If you could tell me what your actually trying to achieve, I'm sure I can help you in a better way.

Bind the height of a Polygon to the StackPanel height

I can't figure how to bind the height of a polygon to the height of my stack panel.
If I wanted to add a rectangle, all I had to do is something like that:
<Rectangle Width="75" >
<Rectangle.Fill>
<SolidColorBrush Color="Red" />
</Rectangle.Fill>
</Rectangle>
This one won't brake the height of the panel. but with the polygon it seems like I can't leave some of the points as blank so that will scale with the parent panel.
Thanks
Wrap your polygon with a <Viewbox>.
The Viewbox automatically scales its content to its size. Exactly how it does so can be tweaked with the Stretch and StretchDirection properties.
this solution works too
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Border BorderBrush="Black" BorderThickness="1,1,0,1">
<StackPanel Orientation="Horizontal">
<TextBlock Text="TextBlock1" Margin="2" />
<TextBlock Text="TextBlock2" Margin="2" />
<TextBlock Text="TextBlock3" Margin="2" />
<TextBlock Text="TextBlock4" Margin="2" />
<TextBlock Text="TextBlock5" Margin="2" />
</StackPanel>
</Border>
<Path Fill="Yellow" Stroke="Black" StrokeThickness="1"
Width="50" Stretch="Fill">
<Path.Data>
<PathGeometry>
<PathFigure IsClosed="True" StartPoint="1,0.5">
<LineSegment Point="0,0" IsSmoothJoin="True" />
<LineSegment Point="0,1" IsSmoothJoin="True" />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
</StackPanel>
</Grid>

Why does WPF render two identical objects differently?

Take this Window as an example:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" ResizeMode="NoResize" SizeToContent="WidthAndHeight" SnapsToDevicePixels="True">
<Grid Width="17" Margin="1">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<RepeatButton Grid.Row="0" SnapsToDevicePixels="True">
<Polyline RenderOptions.EdgeMode="Aliased" Stretch="Uniform" Margin="1" Fill="Red">
<Polyline.Points>
<Point X="0" Y="3" />
<Point X="3" Y="0" />
<Point X="6" Y="3" />
</Polyline.Points>
</Polyline>
</RepeatButton>
<RepeatButton Grid.Row="1" SnapsToDevicePixels="True">
<Polyline RenderOptions.EdgeMode="Aliased" Stretch="Uniform" Margin="1" Fill="Red">
<Polyline.Points>
<Point X="0" Y="3" />
<Point X="3" Y="0" />
<Point X="6" Y="3" />
</Polyline.Points>
</Polyline>
</RepeatButton>
</Grid>
</Window>
Once the application has been ran, the topmost RepeatButton is taller than the bottom one (consequently the top triangle is also bigger than the bottom one). Why?
If I create 4 rows of identical RepeatButtons, then the 1-st and 3-rd RepeatButtons are of equal size and are bigger than the 2-nd and 4-th RepeatButton?!?
I'm thinking this must be a bug in the WPF layout system, but how to work around this problem? I can't use fixed heights (which does solve the problem), because I need the RepeatButtons and triangles to strecth as the container gets bigger (the example I provided is simplifed just to show the issue, I know I can't resize the example window...).
Edit:
In reply to Ben's comments:
Yes, with the added style the triangles do come out as 9px and 8px tall (I could just as well through out the RepeatButtons alltogether and leave only the polylines as the grids children, that would give the same result). Because the triangles are equal sided, then giving the grid a width of 17 will indeed cause the height to become 17 as well, which of course is not enough for two equal height triangles..
What I'm actually trying to do is create a NumericUpDown control. I've found that by default a spinner width of 17 and a UserControl MinHeight of 24 looks very good. The only problem is, that if I drop this UserControl into a Grid, then the top triangle always pushes itself 1px to tall, ruining the look. No matter how I've tried to mingle with the internal Margins and Paddings, the top triangle always makes itself 1px taller than necessary. So in essence what I want is to have a NumericUpDown, that when put into a Grid, doesn't distort itself. By default it should look perfect from the get go (no Grid RowHeight="Auto") and scale properly (no fixed heights). It must be possible, because by looking at the pixels physically then everything can fit into the given dimensions nicely.
Here is my NumericUpDown, I've stripped out all the non essential things to make it more compact:
<UserControl x:Class="HRS.NumericUpDown"
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"
MinWidth="40" MinHeight="24" Name="ucNUPD" Background="White" SnapsToDevicePixels="True">
<UserControl.Resources>
<Style TargetType="{x:Type RepeatButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Border Name="borderOuter" BorderThickness="1" BorderBrush="Red">
<Border Name="borderInner" BorderThickness="1" BorderBrush="Blue">
<ContentPresenter Margin="2,1" />
</Border>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="triangleStyle" TargetType="{x:Type ContentControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContentControl}">
<Polyline HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="Green" RenderOptions.EdgeMode="Aliased" Stretch="Uniform">
<Polyline.Points>
<Point X="0" Y="3" />
<Point X="3" Y="0" />
<Point X="6" Y="3" />
</Polyline.Points>
</Polyline>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Border BorderThickness="1" BorderBrush="#ABADB3">
<DockPanel>
<UniformGrid Margin="1" DockPanel.Dock="Right" Rows="2" MinWidth="17" Width="17">
<RepeatButton Name="repeatButtonUp" Grid.Row="0">
<ContentControl Style="{StaticResource triangleStyle}" />
</RepeatButton>
<RepeatButton Name="repeatButtonDown" Grid.Row="1">
<ContentControl Style="{StaticResource triangleStyle}" RenderTransformOrigin="0.5, 0.5">
<ContentControl.RenderTransform>
<ScaleTransform ScaleY="-1" />
</ContentControl.RenderTransform>
</ContentControl>
</RepeatButton>
</UniformGrid>
<TextBox BorderThickness="0" VerticalContentAlignment="Center" Text="0" />
</DockPanel>
</Border>
</UserControl>
Here is a picture of what the end result looks like:
(The image doesn't fit a 100%, but you can still see all the relevant details).
On the right side you can see a zoom-in of the NumericUpDowns. The bottom one looks correct, but only because the grid's row Height is set to Auto. The top one is distorted, but by default I want it to look exactly like the bottom one.
Hmmm...
I might just have found a workable solution:
It seems that by setting the Margin of the ContentPresenter in my NumericUpDown to "3,1", everything looks perfect. Preliminary testing is very promising as everything seems to be exactly the way it should be...
I'll test it some more tommorow and if all goes good will mark Ben's answer as correct :)
With SizeToContent="WidthAndHeight" the height will be 17 as you you set the Grid's Width to 17. But with 17/2=8.5 one row will be 9 (rounding occurs becouse of SnapsToDevicePixels="True") but the other will be 8 pixel tall. If you set the Width to 18 they will be equal.
Proof of my theory:
<Grid Width="17" Margin="0">
<Grid.Resources>
<Style TargetType="{x:Type RepeatButton}">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ButtonBase}">
<ContentPresenter Margin="0"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<RepeatButton Grid.Row="0" SnapsToDevicePixels="True">
<Polyline RenderOptions.EdgeMode="Aliased" Stretch="Uniform" Margin="0" Fill="Red">
<Polyline.Points>
<Point X="0" Y="3" />
<Point X="3" Y="0" />
<Point X="6" Y="3" />
</Polyline.Points>
</Polyline>
</RepeatButton>
<RepeatButton Grid.Row="1" SnapsToDevicePixels="True">
<Polyline RenderOptions.EdgeMode="Aliased" Stretch="Uniform" Margin="0" Fill="Red">
<Polyline.Points>
<Point X="0" Y="3" />
<Point X="3" Y="0" />
<Point X="6" Y="3" />
</Polyline.Points>
</Polyline>
</RepeatButton>
With this snippet you gain a Triangle that has 9 pixel height, and one with 8 pixels.
But if you want for a solution try this:
<RepeatButton Grid.Row="1" SnapsToDevicePixels="True" Height="{Binding RelativeSource={RelativeSource Self}, Path=ActualWidth}" >
<Polyline RenderOptions.EdgeMode="Aliased" Stretch="Uniform" Margin="0" Fill="Red">
<Polyline.Points>
<Point X="0" Y="3" />
<Point X="3" Y="0" />
<Point X="6" Y="3" />
</Polyline.Points>
</Polyline>
</RepeatButton>
This way the width and the height of the buttons will be equal.
If think you can write a little converter too, that will can do some nasty things:
<RepeatButton Grid.Row="1" SnapsToDevicePixels="True" Height="{Binding RelativeSource={RelativeSource Self}, Path=ActualWidth, Converter={StaticResource TriangleWidthConverter}}" >
<Polyline RenderOptions.EdgeMode="Aliased" Stretch="Uniform" Margin="0" Fill="Red">
<Polyline.Points>
<Point X="0" Y="3" />
<Point X="3" Y="0" />
<Point X="6" Y="3" />
</Polyline.Points>
</Polyline>
</RepeatButton>
public class TriangleWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
int width = 0;
if (int.TryParse(value.ToString(), out width))
return width + 1; // Do some fun here.
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
I hope these will help.
No idea why you are seeing this behavior, but this might work to fix it: try setting the height on each of your your rows to .5* if you have 2 rows or .25* if you have 4 rows (or .1* if you have 10 rows, etc.).

Resources