Unable to center a canva inside a canva - wpf

I've a view in WPF where I need to center a canvas within a canvas.
I know it's not the most suitable container for this, but we have other components that will come in this canvas where it will simplify a lot our job.
So basically, I've currently this code:
<Canvas Name="RootContainer" Background="#373B3F" ClipToBounds="True" MouseLeftButtonDown="OnMainCanvasMouseLeftButtonDown">
<Canvas.DataContext>
<local:SomeViewModel/>
</Canvas.DataContext>
<Canvas Name="SomeContainer" Background="#373B3F" MouseMove="OnCanvasMouseMove" MouseWheel="OnCanvasMouseWheel">
<Canvas.Left>
<MultiBinding Converter="{StaticResource CenterValueConverter}">
<Binding ElementName="RootContainer" Path="ActualWidth" />
<Binding ElementName="SomeContainer" Path="ActualWidth" />
</MultiBinding>
</Canvas.Left>
<Canvas.Top>
<MultiBinding Converter="{StaticResource CenterValueConverter}">
<Binding ElementName="RootContainer" Path="ActualHeight" />
<Binding ElementName="SomeContainer" Path="ActualHeight" />
</MultiBinding>
</Canvas.Top>
<ItemsControl ItemsSource="{Binding SomeChilds}" Name="ItemsControl">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Left}" />
<Setter Property="Canvas.Top" Value="{Binding Top}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}" ContentTemplateSelector="{StaticResource ConventionBasedDataTemplateSelector}"
MouseLeftButtonDown="OnMouseLeftButtonDown" PreviewMouseLeftButtonUp="OnPreviewMouseLeftButtonUp" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Some other controls over here -->
</Canvas>
</Canvas>
And this converter:
public class CenterValueConverter : IMultiValueConverter
{
public object Convert(object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values == null || values.Length != 2)
{
return null;
}
double totalWidth = (double)values[0];
double size = (double)values[1];
return totalWidth / 2 - (size / 2);
}
public object[] ConvertBack(object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
The issue is that the second value(SomeContainer.ActualWidth/ActualHeight) always comes with a value of 0(even when I've some real elements it).
Any idea why and how to fix this? Or another XAML way of centering SomeContainer inside of the RootContainer ?
EDIT
Maybe some additional informations on why I planned to use so much Canvas.
The RootContainer one is because SomeContainer will have transformation(scale for zooming) and translation for panning
The SomeContainer could be something different I guess
The Canvas inside the ItemControls is because each elements will be positioned as a very specific place.

I guess your ActualHeight/ActualWidth are = 0 because you didn't load the window yet.
To make these calculous taking in account dimensions of Canvas you may use 'OnContentRendered' event, that will be launched after the window is displayed, so you will have their dimensions appearing. If your Canvas may change dimensions, you may also use SizeChanged event.
Even I think it is a bit complicated, and you could just use that XAML code taken from Clemen's answer :
<Canvas Background="Transparent"
SizeChanged="ViewportSizeChanged"
MouseLeftButtonDown="ViewportMouseLeftButtonDown"
MouseLeftButtonUp="ViewportMouseLeftButtonUp"
MouseMove="ViewportMouseMove"
MouseWheel="ViewportMouseWheel">
<Canvas x:Name="canvas" Width="1000" Height="600">
<Canvas.RenderTransform>
<MatrixTransform x:Name="transform"/>
</Canvas.RenderTransform>
<Ellipse Fill="Red" Width="100" Height="100" Canvas.Left="100" Canvas.Top="100"/>
<Ellipse Fill="Green" Width="100" Height="100" Canvas.Right="100" Canvas.Bottom="100"/>
</Canvas>
</Canvas>

Related

WPF own bar chart using grid

Let's say we have 25 products A and 14 products B. I want to create chart that is representing them using rectangles and grid. I wrote this code below and it works, but the chart generated with it is very inaccurate. Any ideas how to fix it?
<StackPanel Orientation="Horizontal">
<!--Products A-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.2*"/>
<RowDefinition Height="percentage1*"/>
</Grid.RowDefinitions>
<Rectangle Fill="Red" HorizontalAlignment="Stretch" Grid.Row="1"/>
</Grid>
<!--Products B-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.2*"/>
<RowDefinition Height="percentage2*"/>
</Grid.RowDefinitions>
<Rectangle Fill="Red" HorizontalAlignment="Stretch" Grid.Row="1"/>
</Grid>
</StackPanel>
percentage1 = 25 products / (25 products + 14 products)
percentage2 = 14 products / (25 products + 14 products)
You should not control the size of the Rectangle with the width of a Grid row. Instead host the Rectangle elements in a ItemsControl and calculate the final rendered width based on the available space:
rectangle_width = ratio * available_width
= value / max_value_in_chart * available_width
It's best to move the logic and layout to a custom Control or UserControl. And because the Rectangle bars are hosted in an ItemsControl, you can as many bars as you need to without any hassle (opposed to modifying a Grid to add more rows):
Usage
<local:SimpleBarChart x:Name="BarChart"
BarThickness="64"
Orientation="Horizontal"/>
public MainWindow()
{
InitializeComponent();
// Better use data binding
this.BarChart.BarValues = new List<double> { 12, 24, 36 };
}
SimpleBarChart.xaml.cs
public partial class SimpleBarChart : UserControl
{
public IList<double> BarValues
{
get => (IList<double>)GetValue(BarValuesProperty);
set => SetValue(BarValuesProperty, value);
}
public static readonly DependencyProperty BarValuesProperty = DependencyProperty.Register(
"BarValues",
typeof(IList<double>),
typeof(SimpleBarChart),
new PropertyMetadata(default));
public double BarThickness
{
get => (double)GetValue(BarThicknessProperty);
set => SetValue(BarThicknessProperty, value);
}
public static readonly DependencyProperty BarThicknessProperty = DependencyProperty.Register(
"BarThickness",
typeof(double),
typeof(SimpleBarChart),
new PropertyMetadata(default));
public SimpleBarChart()
{
InitializeComponent();
}
}
SimpleBarChart.xaml
<UserControl>
<UserControl.Resources>
<local:BarValueToLengthConverter x:Key="BarValueToLengthConverter" />
</UserControl.Resources>
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=BarValues}"
HorizontalAlignment="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=HorizontalContentAlignment}"
HorizontalContentAlignment="Stretch"
VerticalAlignment="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=VerticalalContentAlignment}"
VerticalContentAlignment="Stretch">
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Margin"
Value="0,0,0,12" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Fill="Orange"
Height="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=BarThickness}"
HorizontalAlignment="Left">
<Rectangle.Width>
<MultiBinding Converter="{StaticResource BarValueToLengthConverter}">
<Binding RelativeSource="{RelativeSource AncestorType=UserControl}"
Path="BarValues" />
<Binding />
<Binding RelativeSource="{RelativeSource AncestorType=UserControl}"
Path="BarThickness" />
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}"
Path="ActualWidth" />
</MultiBinding>
</Rectangle.Width>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>
BarValueToLengthConverter.cs
public class BarValueToLengthConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
IList<double>? barValues = values.OfType<IList<double>>().FirstOrDefault();
IList<double>? doubleValues = values.OfType<double>().ToList();
double barValue = doubleValues[0];
double barThickness = doubleValues[1];
double barHostWidth = doubleValues[2];
return barValue / barValues.Max() * barHostWidth;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

Drawing Ellipse Line/PolyLine with a double

I have been working with this example to draw bar graph. The bar part is done and it represents the volume/quantity in a transaction. It's an associated price in range 20 - 30. What I want now is to draw points to represent price associated with volumes and connect those points. Two changes I've made in EDIT part of the linked example (1) removed the TextBlock from the DataTemplate of ItemsControl and added an Ellipse instead and (2) edited the canvas to add price/volume axis label. Here's how it looks like now:
How to add those Ellipse in right position and connect those with Line/PolyLine?
EDIT
Here's what I've now in ItemsControl:
<ItemsControl ScrollViewer.CanContentScroll="True"
Height="135"
ItemsSource="{Binding RectCollection}"
Margin="50 0 50 0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas Width="20">
<Canvas.LayoutTransform>
<ScaleTransform ScaleY="-1"/>
</Canvas.LayoutTransform>
<Rectangle Width="18"
Margin="0 0 2 0"
VerticalAlignment="Bottom"
Opacity=".5" Fill="LightGray">
<Rectangle.Height>
<MultiBinding Converter="{StaticResource VConverter}">
<Binding Path="ActualHeight"
RelativeSource="{RelativeSource AncestorType=ItemsControl}"/>
<Binding Path="DataContext.HighestPoint"
RelativeSource="{RelativeSource AncestorType=ItemsControl}"/>
<Binding Path="Volume"/>
</MultiBinding>
</Rectangle.Height>
</Rectangle>
<Line Stroke="DarkGreen" StrokeThickness="1"
X1="10" X2="30"
Y2="{Binding PreviousPrice, Converter={StaticResource PConverter}}"
Y1="{Binding CurrentPrice, Converter={StaticResource PConverter}}">
<Line.Style>
<Style TargetType="Line">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=PreviousPrice}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Line.Style>
</Line>
<Ellipse Fill="Red" Width="6" Height="6" Margin="-3" Canvas.Left="10"
Canvas.Top="{Binding CurrentPrice, Converter={StaticResource PConverter}}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer
VerticalScrollBarVisibility="Hidden"
Background="{TemplateBinding Panel.Background}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
oh! I forgot to add those ValueConverters, here're those:
public class VolumeConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var height = (double)values[0];
var higest = (double)values[1];
var value = (double)values[2];
return value * height / higest;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class PriceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is double)) return null;
var price = (double)value;
var remainingHeight = 90;
var priceRange = 30 - 20.0;
return 45 + ((price - 20) * remainingHeight / priceRange);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
and here's how it looks like:
As suggested by #Clemens, I've to have another double?, in case where Insert(0, ... ) is used on ObservableCollection instead of Add(...) to add the last item in first place and removed the AternationCount/Index stuff.
The following example uses a vertically flipped Canvas to invert the y-axis order, so that it goes upwards. So PConverter should return positive y values.
Besides the Rectangle and Ellipse elements it draws a Line element from the previous data value to the current one by means of RelativeSource={RelativeSource PreviousData} in the value binding. It also uses a DataTrigger on the AlternationIndex to hide the first line.
<ItemsControl ... AlternationCount="2147483647">
...
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas Width="20">
<Canvas.LayoutTransform>
<ScaleTransform ScaleY="-1"/>
</Canvas.LayoutTransform>
<Rectangle Fill="LightGray" Margin="1" Width="18"
Height="{Binding Value1, Converter={StaticResource PConverter}}"/>
<Line Stroke="DarkGreen" StrokeThickness="3"
X1="-10" X2="10"
Y1="{Binding Price,
Converter={StaticResource PConverter},
RelativeSource={RelativeSource PreviousData}}"
Y2="{Binding Price,
Converter={StaticResource PConverter}}">
<Line.Style>
<Style TargetType="Line">
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=(ItemsControl.AlternationIndex),
RelativeSource={RelativeSource
AncestorType=ContentPresenter}}"
Value="0">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Line.Style>
</Line>
<Ellipse Fill="Red" Width="6" Height="6" Margin="-3" Canvas.Left="10"
Canvas.Top="{Binding Price, Converter={StaticResource PConverter}}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
Since the value converter is now also called for a non-existing value (for PreviousData of the first item), you have to make sure that it checks if the passed value is actually a double:
public object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is double)) return 0d;
...
}

Use different template for last item in a WPF itemscontrol

I'm using a custom template in my itemscontrol to display the following result:
item 1, item 2, item3,
I want to change the template of the last item so the result becomes:
item 1, item2, item3
The ItemsControl:
<ItemsControl ItemsSource="{Binding Path=MyCollection}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}"/>
<TextBlock Text=", "/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Is there anyone who can give a solution for my problem? Thank you!
I've found the solution for my problem using only XAML. If there is anybody who needs to do the same, use this:
<ItemsControl ItemsSource="{Binding Path=MyCollection}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="comma" Text=", "/>
<TextBlock Text="{Binding}"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}">
<Setter TargetName="comma" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
You can use DataTemplateSelector, in SelectTemplate() method you can check whether item is the last and then return an other template.
In XAML:
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentPresenter
ContentTemplateSelector = "{StaticResource MyTemplateSelector}">
In Code behind:
private sealed class MyTemplateSelector: DataTemplateSelector
{
public override DataTemplate SelectTemplate(
object item,
DependencyObject container)
{
// ...
}
}
This solution affects the last row and updates with changes to the underlying collection:
CodeBehind
The converter requires 3 parameters to function properly - the current item, the itemscontrol, the itemscount, and returns true if current item is also last item:
class LastItemConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
int count = (int)values[2];
if (values != null && values.Length == 3 && count>0)
{
System.Windows.Controls.ItemsControl itemsControl = values[0] as System.Windows.Controls.ItemsControl;
var itemContext = (values[1] as System.Windows.Controls.ContentPresenter).DataContext;
var lastItem = itemsControl.Items[count-1];
return Equals(lastItem, itemContext);
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML
The Data-Trigger for a DataTemplate, that includes a textbox named 'PART_TextBox':
<DataTemplate.Triggers>
<DataTrigger Value="True" >
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource LastItemConverter}">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" />
<Binding RelativeSource="{RelativeSource Self}"/>
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" Path="Items.Count"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Foreground" TargetName="PART_TextBox" Value="Red" />
</DataTrigger>
</DataTemplate.Triggers>
The converter as a static resource in the Xaml
<Window.Resources>
<local:LastItemConverter x:Key="LastItemConverter" />
</Window.Resources>
SnapShot
And a snapshot of it in action
The code has been added to the itemscontrol from this 'codeproject'
https://www.codeproject.com/Articles/242628/A-Simple-Cross-Button-for-WPF
Note the last item's text in red
One question... I see you're using an ItemsControl as opposed to say a ListBox and that it appears to be bound to a collection of strings, and that you're only trying to display the resulting text without formatting the individual parts, which makes me wonder if your desired output is actually the string itself as mentioned in the question, and not an actual ItemsControl per se.
If I'm correct about that, have you considered just using a simple TextBlock bound to the items collection, but fed through a converter? Then Inside the converter, you would cast value to an array of strings, then in the Convert method, simply Join them using a comma as the separator which will automatically, only add them between elements, like so...
var strings = (IEnumerable<String>)value;
return String.Join(", ", strings);

Rescale listboxes or other items? but not text (WPF)

The question is quite clear. I have listviews or listboxes in my view.
If i make my screen smaller, i want that my text (labels etc) stay the same size. But the listviews and listboxes have to become smaller, eventually with a scrollbar?
how do i do this?
Thanks
the ListBox has a ScrollViewer built in.
<ListBox ScrollViewer.VerticalScrollBarVisibility="Auto" />
The question is quite clear.
Well, not exactly. But I think this is what you're looking for. First, create a value converter that takes a Double and returns its reciprocal:
public class ReciprocalValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Double? d = value as Double?;
return (d == null || d == 0)
? null
: 1/d;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Now you can scale any content control, using a ScaleTransform, and use the ReciprocalValueConverter to keep any individual element contained within it to the original scale. So if the content control's scale is doubled, the content that you want to remain unchanged has its scale halved.
This example shows both scaling content controls and "unscaling" the items in a list box by assigning the LayoutTransform to each item:
<Window x:Class="ScaleTransformDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ScaleTransformDemo="clr-namespace:ScaleTransformDemo" Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ScaleTransformDemo:ReciprocalValueConverter x:Key="Reciprocal" />
</Window.Resources>
<DockPanel>
<Slider x:Name="ScaleSlider"
Orientation="Vertical"
Minimum=".2"
Maximum="4"
Value="1" />
<DockPanel>
<DockPanel.LayoutTransform>
<ScaleTransform ScaleX="{Binding ElementName=ScaleSlider, Path=Value}"
ScaleY="{Binding ElementName=ScaleSlider, Path=Value}" />
</DockPanel.LayoutTransform>
<Label DockPanel.Dock="Top">
The content of this label scales with the slider.
</Label>
<Label DockPanel.Dock="Top">
<Label.LayoutTransform>
<ScaleTransform ScaleX="{Binding ElementName=ScaleSlider, Path=Value, Converter={StaticResource Reciprocal}}"
ScaleY="{Binding ElementName=ScaleSlider, Path=Value, Converter={StaticResource Reciprocal}}" />
</Label.LayoutTransform>
<Label.Content>
This label's content stays the same size.
</Label.Content>
</Label>
<Label DockPanel.Dock="Top">
The ListBox below scales with the slider, too, but the ListBoxItems don't:
</Label>
<ListBox Height="50"
DockPanel.Dock="Top">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="LayoutTransform">
<Setter.Value>
<ScaleTransform ScaleX="{Binding ElementName=ScaleSlider, Path=Value, Converter={StaticResource Reciprocal}}"
ScaleY="{Binding ElementName=ScaleSlider, Path=Value, Converter={StaticResource Reciprocal}}" />
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBoxItem>Item 1</ListBoxItem>
<ListBoxItem>Item 2</ListBoxItem>
<ListBoxItem>Item 3</ListBoxItem>
<ListBoxItem>Item 4</ListBoxItem>
<ListBoxItem>Item 5</ListBoxItem>
<ListBoxItem>Item 6</ListBoxItem>
</ListBox>
<TextBlock DockPanel.Dock="Top" />
</DockPanel>
</DockPanel>
</Window>

How to convert X/Y position to Canvas Left/Top properties when using ItemsControl

I am trying to use a Canvas to display objects that have "world" location (rather than "screen" location). The canvas is defined like this:
<Canvas Background="AliceBlue">
<ItemsControl Name="myItemsControl" ItemsSource="{Binding MyItems}">
<Image x:Name="myMapImage" Panel.ZIndex="-1" />
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<TextBlock Canvas.Left="{Binding WorldX}" Canvas.Top="{Binding WorldY}"
Text="{Binding Text}"
Width="Auto" Height="Auto" Foreground="Red" />
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
MyItem is defined like this:
public class MyItem
{
public MyItem(double worldX, double worldY, string text)
{
WorldX = worldX;
WorldY = worldY;
Text = text;
}
public double WorldX { get; set; }
public double WorldY { get; set; }
public string Text { get; set; }
}
In addition, I have a method to convert between world and screen coordinates:
Point worldToScreen(double worldX, double worldY)
{
// Note that the conversion uses an internal m_mapData object
var size = m_mapData.WorldMax - m_mapData.WorldMin;
var left = ((worldX - m_currentMap.WorldMin.X) / size.X) * myMapImage.ActualWidth;
var top = ((worldY - m_currentMap.WorldMin.Y) / size.Y) * myMapImage.ActualHeight;
return new Point(left, top);
}
With the current implementation, the items are positioned in the wrong location, because their location is not converted to screen coordinates.
How can I apply the worldToScreen method on the MyItem objects before they are added to the canvas?
Edit:
I got a little confused whether I'm going in the right way, so I posted another question: How to use WPF to visualize a simple 2D world (map and elements)
There is a helpful and complete answer there also for this question
The main problem with the code you presented is that the Canvas.Left and Canvas.Top properties are relative to a Canvas that is in the DataTemplate for the ItemsControl. This keeps "resetting" the origin. Instead you can:
remove the Canvas from the DataTemplate
make the ItemsPanel for the ListBox a Canvas
position the ItemsPresenter that wraps the ItemsControl items with Canvas.Top and Canvas.Left
ensure that the Image and the Canvas have the same coordinates, or switch to using the `Canvas
Here is a complete XAML-only example of positioning ItemsControl items on a Canvas with an Image behind the Canvas:
<Grid>
<Image x:Name="image" Height="100" Width="Auto" Source="http://thecybershadow.net/misc/stackoverflow.png"/>
<ItemsControl Name="myItemsControl">
<ItemsControl.ItemsSource>
<PointCollection>
<Point X="10" Y="10"/>
<Point X="30" Y="30"/>
</PointCollection>
</ItemsControl.ItemsSource>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="Text" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Grid>
You can apply this conversion within a value converter in your binding. Value converters implement the IValueConverter interface (http://msdn.microsoft.com/en-us/library/system.windows.data.ivalueconverter.aspx). The problem is that your conversion requires both the X and Y component of your item. A simple solution to this would be to bind to MyItem, rather than MyItem.WorldX. You can achieve this by using "Path=.", if you then create the following value converter ...
public class CoordinateLeftConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
MyItem item = value as MyItem;
return worldToScreen(item.WorldX, item.WorldY).X;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
}
}
You can use it in your binding as follows:
<TextBlock Canvas.Left="{Binding Path=.,Converter={StaticResource CoordinateLeftConverter}" ... />
Where you create an instance of CoordinateLeftConverter in your page Resources:
<UserControl.Resources>
<CoordinateLeftConverter x:Key="CoordinateLeftConverter"/>
</UserControl.Resources>
You would then of course need to add another converter for the Canvas.Top property, or supply a ConverterParameter to switch between the X / Y property of the transformed Point.
However, a simpler solution might be to perform the conversion within your MyItem class, removing the need for a converter!
I had a similar problem when I was trying to bind the Canvas.Top property to a ViewModel's object that has a CanvasTop property, the Canvas.Top property would first get the value, but then it gets reset somehow and loses the binding expression. But I did a little work around from the code here. And since I'm using Silverlight, there's no ItemsContainerStyle property, so I used ItemsControl.Resources instead, so given the above example, my code looks something like this:
<Grid>
<Image x:Name="image" Height="100" Width="Auto" Source="http://thecybershadow.net/misc/stackoverflow.png"/>
<ItemsControl Name="myItemsControl">
<ItemsControl.Resources>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.Resources>
<ItemsControl.ItemsSource>
<PointCollection>
<Point X="10" Y="10"/>
<Point X="30" Y="30"/>
</PointCollection>
</ItemsControl.ItemsSource>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="Text" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

Resources