You may notice that the Silverlight
TileBrush
is missing some key properties which would enable it to actually tile a brush. The
WPF
TileBrush
has properties such as TileMode, Viewbox, and ViewportUnits that can be used to tile
an image as a fill or as a background for a UIElement. For some reason, they’re not
implemented in Silverlight.
The Silverlight object framework is far more closed than the equivalent WPF
framework. There are many more sealed class types and methods in
Silverlight than in WPF. For example, Canvas in Silverlight is sealed. If you want
to make a more interesting Canvas of some sort, you need to implement it from a base
Panel. It’s not clear why classes such as Canvas are sealed in Silverlight.
Back to the TileBrush.
It’s not practical to try to extend the existing TileBrush in Silverlight to provide
the tiling support offered by WPF so I took a very different approach.
As my tiling needs were straightforward, I created a new control, derived from
Panel. The new control is called TilePanel. The TilePanel’s one and only task is,
given an ImageBrush, and a few tile size values, tiles the brush to fill it’s
container completely.
The use of the TilePanel is straightforward:
<local:TilePanel x:Name="pnlTile" TileWidth="32" TileHeight="16">
<local:TilePanel.Image>
<ImageBrush ImageSource="LED64x32-rect.png" />
</local:TilePanel.Image>
</local:TilePanel>
Above, you’ll see the panel declaration, the width and height of the tile, and the
brush that is used as the tiled image.
The TilePanel handles all of the details.
I created a small demo program to show off the tile support.
We have an exercise bicycle with an LED style display that inspired the simple demo
program. It displays statistics and messages through the use of the bicycle. The
demo allows you to change the text and the shape of the LED. Additionally, it shows
how the tiles can be semi-transparent (the images used are 24-bit PNGs), allowing
the content underneath to show through. In this case, it’s a large TextBlock which
is animated within a clipped Canvas.
DispatcherTimer tmr = new DispatcherTimer();
tmr.Interval = TimeSpan.FromMilliseconds(250);
tmr.Tick += new EventHandler(UpdateCharacterPosition);
tmr.Start();
Any time the overall canvas changes size, the code adjusts the Clip property to a
new rectangle (otherwise the text appears outside of the boundaries of the LED
display).
private void TextHolderSizeChanged(object sender,
SizeChangedEventArgs e)
{
if (sender is Canvas)
{
Canvas c = sender as Canvas;
RectangleGeometry rc = new RectangleGeometry();
rc.Rect = new Rect(new Point(0, 0), e.NewSize);
c.Clip = rc;
}
}
To more efficiently update the TilePanel when multiple properties are being set at
one time, I took a queued approach to the update:
protected void TileAdjustmentNeededAsync()
{
// by doing this sync, we can queue up several request
// but then really only handle the last one.
_needsUpdate = true;
// async call the adjust tiles
this.Dispatcher.BeginInvoke(delegate
{
AdjustTiles();
});
}
This way, a few properties can be set together, without the TilePanel needlessly
recalculating and rebuilding the tiles.
The demo and the source code are located
here
(zip).
A few notes for those interested …
For maximum efficiency, if you use the TilePanel, try to make the Tiles sized so
that fewer tiles are actually necessary to tile (create the tiles so that there are
tiles within the brush you use). Instead of a tile that has only one image in it for
example, consider building a tile which is really a 2 x 2 set of tiles.
One detail of the implementation that is important: you cannot reliably add new
controls/UIElements to the Children collection of a Panel within the MeasureOverride
or ArrangeOverride methods. Although the child UIElements are added without error,
they typically will not render or display correctly. Building something like this
TilePanel required a trick where the actual updates to the tiles always happen
outside of the scope of an measure-arrange pass.