You can replace the default StackPanel used as the items panel for a ListBox with any other panel element. If you have items that you want to display at arbitrary locations, you can use a Canvas for your items panel.
The example below presents a list of cities, where each city is placed at its proper latitude and longitude.
Assuming that we have a City class that accepts a name and latitude/longitude values passed to its constructor, we can create a list of cities:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
CityList = new ObservableCollection<City>
{
new City("Duluth", 46.83, 92.18),
new City("Redmond", 44.27, 121.15),
new City("Tucson", 32.12, 110.93),
new City("Denver", 39.75, 104.87),
new City("Boston", 42.37, 71.03),
new City("Tampa", 27.97, 82.53)
};
}
private ObservableCollection<City> cityList;
public ObservableCollection<City> CityList
{
get { return cityList; }
set
{
cityList = value;
RaisePropertyChanged("CityList");
}
}
// INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void RaisePropertyChanged(string propName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
We then bind a ListBox to this list of cities. We also:
- Use its ItemContainerStyle to map latitude and longitude values to the attached Top and Left properties of the Canvas element
- Use value converters to convert latitude and longitude values to canvas positions
- Specify the Canvas as the ItemsPanel
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow"
Width="470" Height="310">
<Window.Resources>
<ResourceDictionary>
<local:LatValueConverter x:Key="latValueConverter" />
<local:LongValueConverter x:Key="longValueConverter" />
<sys:Double x:Key="mapWidth">440</sys:Double>
<sys:Double x:Key="mapHeight">240</sys:Double>
</ResourceDictionary>
</Window.Resources>
<StackPanel Orientation="Horizontal" Margin="5" >
<ListBox ItemsSource="{Binding CityList}"
DisplayMemberPath="Name">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Canvas.Left"
Value="{Binding Longitude, Converter={StaticResource longValueConverter},
ConverterParameter={StaticResource mapWidth}}"/>
<Setter Property="Canvas.Top"
Value="{Binding Latitude, Converter={StaticResource latValueConverter},
ConverterParameter={StaticResource mapHeight}}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True"
Width="{StaticResource mapWidth}"
Height="{StaticResource mapHeight}"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</StackPanel>
</Window>
Here is the implementation of the value converters:
public static class Constants
{
public const double LatTop = 50.0;
public const double LatBottom = 24.0;
public const double LongLeft = 125.0;
public const double LongRight = 66.0;
}
public class LatValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double latitude = (double)value;
double height = (double)parameter;
int top = (int)((Constants.LatTop - latitude) / (Constants.LatTop - Constants.LatBottom) * height);
return top;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class LongValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double longitude = (double)value;
double width = (double)parameter;
int left = (int)((Constants.LongLeft - longitude) / (Constants.LongLeft - Constants.LongRight) * width);
return left;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The end result is that the city names are displayed at their proper locations on the canvas. Note that because the cities are displayed in a ListBox, we can still select one of them.
