Sunday, February 16, 2014

Displaying Listview/Listbox items in a Canvas in Windows Presentation Foundation

For a WPF project I wanted to create a graphical representation of a list of items. I computed some X,Y coordinates for each item and started changing the XAML of a Listview in order to reflect the position of each item on a Canvas. Well, it is just as easy as you imagine: change the ItemsPanel property to a Canvas and then style each item as whatever you want. The gotcha comes when trying to set the coordinates. The thing is that for each item in a listview a container is constructed and inside the item template is displayed. So here you have all you items displayed exactly as you want them, except the coordinates don't work, since what needs to be placed on the Canvas are the generated containers, not the items. Here is the solution:
        <local:CoordinateConverter x:Key="CoordinateConverter"/>
        <DataTemplate DataType="{x:Type vm:KernelItem}"> <!-- the template for the data items -->
                <Ellipse Fill="{Binding Background}" Width="100" Height="100" Stroke="DarkGray" Name="ellipse"
                         ToolTip="{Binding Tooltip}"/>
                <TextBlock Text="{Binding Text}" MaxWidth="75"  MaxHeight="75"
                           HorizontalAlignment="Center" VerticalAlignment="Center"
                           ToolTip="{Binding Tooltip}" />
    <ListView ItemsSource="{Binding KernelItems}" Name="lvKernelItems"
              SelectedItem="{Binding SelectedItem,Mode=TwoWay}"
                <Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Black" />
            <Style TargetType="{x:Type ListViewItem}">
                <Setter Property="FocusVisualStyle" Value="{x:Null}"/><!-- no highlight of selected items -->
                <Setter Property="Foreground" Value="White"/>
                <Setter Property="(Canvas.Left)" >
                        <MultiBinding Converter="{StaticResource CoordinateConverter}">
                            <Binding Path="X"/>
                            <Binding Path="ActualWidth" ElementName="lvKernelItems"/>
                            <Binding Path="DataContext.Zoom" RelativeSource="{RelativeSource AncestorType={x:Type Window}}"/>
                <Setter Property="(Canvas.Top)" >
                        <MultiBinding Converter="{StaticResource CoordinateConverter}">
                            <Binding Path="Y"/>
                            <Binding Path="ActualHeight" ElementName="lvKernelItems"/>
                            <Binding Path="DataContext.Zoom" RelativeSource="{RelativeSource AncestorType={x:Type Window}}"/>
                <Style.Resources><!-- no highlight of selected items -->
                    <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
                    <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="Transparent" />
                    <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black" />
                    <SolidColorBrush x:Key="{x:Static SystemColors.ControlTextBrushKey}" Color="Black" />
                    <Trigger Property="IsSelected" Value="True"><!-- custom selected item template -->
                        <Setter Property="Foreground" Value="Cyan"/>
                        <Setter Property="Effect">
                                <DropShadowEffect ShadowDepth="0" Color="White" Opacity="0.5" BlurRadius="10"/>
                        <Setter Property="Canvas.ZIndex" Value="1000"/>

As a bonus, you see the way to remove the default selection of an item: the ugly dotted line and the highlighting background.