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.