Continuous Tile Panel

A couple of weeks ago someone suggested to me an idea for a particular type of layout panel. The idea was to be able to display a fixed set of tiles which are “wrapped” horizontally. So as you scroll horizontally the tiles will move off one edge of the panel and eventually re-appear on the other side. Think of how we commonly see the earth projected onto a rectangle – scrolling left or right to bring the particular geography into the centre of the screen.

I jumped into this without giving too much thought about how I would want to interact with the panel from an API perspective. For that reason its probably I’ve taken altogether the wrong approach but I thought I would post it up here anyway, ‘cause its unlikely I’ll take it any further than this proof of concept.


Define some XAML like so:

<panels:TilePanel x:Name="tilePanel" Height="170"
Rows="3" Background="LightYellow" ClipToBounds="True"
XOffset="{Binding ElementName=xOffsetSlider,Path=Value,Mode=OneWay}"
YOffset="{Binding ElementName=yOffsetSlider,Path=Value,Mode=OneWay}"
Scale="{Binding ElementName=scaleSlider,Path=Value,Mode=OneWay}">
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="32pt"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Padding" Value="3"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="Background" Value="LightBlue"/>

This generates the following layout where each of the items is laid out in three rows.

Continuous Tile Panel - Letters at Start

Items that are beyond the right edge are actually wrapped back to the left hand side (by subtracting the total width of all columns). So that when we supply a horizontal offset we get the desired effect.

Continuous Tile Panel - Letters Shifted

Note that in the example each column width is automatically calculated based on the widest element. Likewise row heights are calculated based on the tallest elements. Although this allows for different widths/heights per column/row its works well when all items are the same width and height.

One example usage of this control would be as an ItemsPanel for an ItemsControl that displays tiled images that form a single scene. It so happens that ICE generates these types of tiles (at various zoom levels) for publishing its panoramas.

<ItemsControl ItemsSource="{Binding TileSet}">
<Border BorderThickness="1" BorderBrush="LightGray">
<Image Source="{Binding ImagePath}" Stretch="Uniform" Height="Auto"/>
                    <TextBlock Margin="4" TextAlignment="Center" FontSize="6pt" Foreground="White" Opacity="0.5" 
VerticalAlignment="Center" HorizontalAlignment
="Center"> <TextBlock.Text> <MultiBinding StringFormat="{}{0},{1}" > <Binding Path="ColumnIndex"/> <Binding Path="RowIndex"/> </MultiBinding> </TextBlock.Text> </TextBlock> </Grid> </Border> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <panels:TilePanel Margin="10" Background="LightYellow" ClipToBounds="True" Rows="{Binding TileSet.RowCount}" XOffset="{Binding ElementName=xOffsetSlider,Path=Value,Mode=OneWay}" YOffset="{Binding ElementName=yOffsetSlider,Path=Value,Mode=OneWay}" Scale="{Binding ElementName=scaleSlider,Path=Value,Mode=OneWay}"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl>

Continuous Tile Panel - ICE



To load the ICE images into a collection of “Tiles” I wrote the following.

    public static class TileLoader
public static TileSet LoadFromFolder( string path, int zoomLevel )
var result = new TileSet(path, zoomLevel);
if ( !Directory.Exists( path ) )
throw new ArgumentException( "The specified tile folder does not exist.", "path" );
if ( zoomLevel < 0 )
throw new ArgumentException( "The zoom level must be >= 0.", "zoomLevel" );
DirectoryInfo tileLevelFolder;
var rootFolder = new DirectoryInfo( path );
var tileFolder = new DirectoryInfo( Path.Combine( path, "tiles" ) );
tileLevelFolder = new DirectoryInfo( Path.Combine( tileFolder.FullName, "l_" + zoomLevel ) );
catch ( System.IO.IOException ex )
throw new ArgumentException( 
string.Format( "The tile set specified by the path {0} is corrupt.", path ), "path", ex ); } var column = 0;
// Order the directories by numerical ascending, e.g. c_3 before c_21 foreach ( var columnFolder in tileLevelFolder.GetDirectories( "c_*" )
.OrderBy( d => int.Parse( d.Name.Substring( 2 ) ) ) ) { var row = 0; foreach ( var imageFile in columnFolder.GetFiles( "tile_*.wdp" )
.OrderBy( i => int.Parse( i.Name.Substring( 5, i.Name.IndexOf( '.' ) - 5 ) ) ) ) { var tile = new Tile() { ColumnIndex = column, RowIndex = row, ImagePath = imageFile.FullName }; result.Add( tile ); row++; if ( result.RowCount < row ) result.RowCount = row; } column++; } return result; } } [DebuggerDisplay("Column: {ColumnIndex}, Row: {RowIndex}, {ImagePath}")] public class
Tile { public string ImagePath { get; set; } public int ColumnIndex { get; set; } public int RowIndex { get; set; } } public class TileSet : List<Tile> { public TileSet( string path, int zoomLevel ) { Path = path; ZoomLevel = zoomLevel; } public string Path { get; protected set; } public int ZoomLevel { get; protected set; } public int RowCount { get; set; } }

The panel’s arrange override code is as follows:

protected override Size ArrangeOverride(Size finalSize)
int row = 0;
int column = 0;
double fullWidth = _columnWidths.Sum();
double fullHeight = _rowHeights.Sum();
int maxColumns = (int) Math.Ceiling( (double) finalSize.Width / (double) _maxChildWidth);
double x = -( XOffset * _maxChildWidth );
double y = - YOffset;
foreach (UIElement child in Children)
x = x % fullWidth;
if (x >= Math.Min(_maxChildWidth * maxColumns, fullWidth - _maxChildWidth + 1))
x -= fullWidth;
else if (x < - _maxChildWidth)
x += fullWidth;
if ( x > -_maxChildWidth && x <= _maxChildWidth * maxColumns &&
y > -_maxChildHeight && y <= fullHeight )
new Rect( new Point( x * Scale, y * Scale ),
new Size( child.DesiredSize.Width * Scale, child.DesiredSize.Height * Scale ) ) ); child.Visibility = Visibility.Visible; }
else { child.Visibility = Visibility.Hidden; child.Arrange( new Rect( new Point( 0, 0 ), new Size( child.DesiredSize.Width * Scale, child.DesiredSize.Height * Scale ) ) ); } y += _rowHeights[ row ]; row = ( row + 1 ) % Rows; if ( row == 0 ) { x += _columnWidths[ column ]; //column * _childWidth ; y = -YOffset ; column++; } } //RenderTransform = new ScaleTransform(Scale, Scale, 0.0, YOffset); return finalSize; }

This is far from complete – sample project can be found here. [It’s a VS2010 project but should be easy enough to re-assemble for VS2008.]


One thought on “Continuous Tile Panel”

  1. Awesome stuff.. I can’t wait to have a good look at this stuff.. Nicely done. Do you have any thoughts on possible virtualisation of the tile items, assuming a large array?

Comments are closed.