Pixel perfect positioning of list elements in WPF

21. januar 2010

This is something I had to look up several times myself, so I thought I should write it down for later reference.

I was building a simple level editor for SilverTile (more about that in another post later) and wanted to place the map tiles in a grid-like fashion. This could be done using my dynamic Grid control, but I wanted pixel perfect control of where the tiles were placed on screen. Note the gaps between the grid rows in the following screen shot.

Fail!

Fail! Note the gaps between the tiles!

To do this I would need to use a Canvas control and place each tile using the Canvas.Left and Canvas.Top properties to set the position.

Win! This is what we want. Pixel perfect placement of our images!

The challenge is that the Canvas control doesn’t support list binding directly, so you must use some other control to accomplish this. Luckily there is a solution available by using the ItemsControl class. The ItemsControl is a list control that lets you specify which control should be the root element. In our case we will use a Canvas control as the root and then use data binding to specify the values for the Canvas.Left and Canvas.Top properties.

Using the ItemsControl

To exemplify the usage of the ItemsControl I first created a simple view model for the application. This was simply a list of TileData objects where each describes one tile. Each TileData object has an image source and Left and Top properties. The Left and Top properties tell where the tile should be placed on screen. In a real application your view model would probably be more complex and possibly calculate the positions based on some other data.

// The TileData class describes a tile
public class TileData
{
    public Uri Image { get; set; }
    public double Left { get; set; }
    public double Top { get; set; }
}

Now, with the view model in place we can setup our application.

<Grid Name="Root" Loaded="Root_Loaded">
    <Grid.Resources>
        <Style x:Key="ContainerStyle">
            <Setter Property="Canvas.Left" Value="{Binding Left}" />
            <Setter Property="Canvas.Top" Value="{Binding Top}" />
        </Style>
    </Grid.Resources>
    <ItemsControl Name="map" ItemsSource="{Binding}" ItemContainerStyle="{StaticResource ContainerStyle}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas Width="{Binding Path=Root.ActualWidth}" Height="{Binding Path=Root.ActualHeight}" />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Image Source="{Binding Image}" />
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>

Here we have a Grid as the root element of the application. I’ve used a grid since it will be sized dynamically when the window is resized, something the Canvas control doesn’t. We then have the ItemsControl that has its ItemsSource bound to our data context. The ItemsSource is the list of TileData objects that is used to describe the map.

And now we get to the interesting part: The ItemsControl.ItemsPanel property. This property defines which control should be the container into which all our list items are placed. By setting this to a Canvas control we end up with a layout where you can set your child controls position as you want.

Further we have the ItemsControl.ItemsTemplate that tells how you want each object in your ItemsSource to be rendered. In our case we render it as an Image control with its source bound to the Image property of your TileData object.

The final missing part is how we place our controls. The obvious way to do this would be to set the Canvas.Left and Canvas.Top properties of our Image control by binding them to the Left and Top properties of our TileData object. One would expect this to work but if you try it you will see that all our controls are placed at the top left corner of the canvas, with Canvas.Left = 0 and Canvas.Top = 0. The solution is to create a style that sets the Canvas.Left and Canvas.Top properties and to assign that to the ItemContainerStyle property of the ItemsContainer. By doing this you will see that our Image controls are placed correctly where we want them.

Using Snoop to find out what really happened here

To understand what really happened here we need to examine the control hierarchy our Xaml code generates. A good tool to do this is Snoop. It lets you attach to a running Wpf application to examine all properties of all generated controls.

Using Snoop to look at our applications control hierarchy

Look at the above screen shot. Here you can see the control hierarchy of the GridWin application. If you look down the tree you will find an ItemsControl control with a child Border control. The Border contains an ItemsPresenter Control that contains a Canvas. This Canvas control is the one we assigned to our ItemsControl.ItemsPanel property.

The Canvas contains a bunch of ContentPresenter controls that each contains one Image control. And this is the reason why we cannot set the Canvas.Left and Canvas.Top properties of our Image controls directly. Since the images are contained in the ContentPresenter controls we need to set the properties here instead. And this is what we do when we set the ItemContainerStyle property. The style is applied to the ContentPresenter which is placed at the correct position.

Using the code

I am using this approach in a level editor application for SilverTile that I will be writing about soon. The application stores the map as a list of Tile objects where each Tile knows which row and column they should be placed in. The Canvas.Left and Top properties are then bound to the X and Y properties that are caluclated by multiplying the row or column number with the tiles width or height in pixels. The tile list is used as the ItemsSource for an ItemsControl that renders the tiles at the wanted position.

The ItemsControl is available in Silverlight as well as in WPF, so this approach works just as well there. The only challenge is that the binding of the Canvas' size to the size of the Root grid cannot be done in Silverlight 3 since it doesn't support element to element binding. This is fixed in Silverlight 4 though, so there you should be able to use the exact same approach.

kick it on DotNetKicks.com

.NET, Silverlight, WPF , ,