Charles Petzold



Random Rectangles in Silverlight (using WriteableBitmap)

October 30, 2009
New York, N.Y.

I remember going to COMDEX in Las Vegas sometime in the early 90s when Microsoft Windows had just reached some kind of tipping point (at least among manufacturers if not users) and the floor of the Convention Center was ablaze with Windows machines, most of them running random-rectangle programs.

The traditional random-rectangle program is short, simple, silly, and ridiculously hypnotic as it covers the display with an ever increasing number of overlapping rectangles of random sizes and colors. Some of those early random-rectangle programs worked off the Windows timer, but the really fast ones replaced the normal message loop with a special PeekMessage loop and used all available CPU cycles for the magnificant splattering of color.

Back when I was writing my book on the Windows Presentation Foundation, Applications = Code + Markup, I wanted to include a WPF random-rectangle program for old-times' sake. I even created a new project named RandomRectangles, but as I started to type the code, I stopped dead in my tracks.

It can't be done.

Well, that's not entirely true. But I realized that any kind of simple random-rectangle program in WPF would just be wrong, wrong, wrong.

Here's the problem: When programming for the Windows API and frameworks prior to WPF, drawing a rectangle involves an API call that goes through the video device driver to render pixels on the display in the form of a rectangle. By the time the call returns to the application, the rectangle is on the screen. But WPF is a retained-mode graphics system. In WPF, the video display is maintained in a visual-composition layer that retains all the visual objects. To draw a rectangle, you actually create an object derived from UIElement, and you typically display that object by adding it to a panel. The object itself remains alive.

A random-rectangle program for WPF (or Silverlight, for that matter) would create thousands of rectangle objects every few minutes. All these rectangles would remain alive while the program continued running, requiring more and more memory every second!

Well, I wouldn't be writing this blog entry if I didn't have a solution. Let's switch to Silverlight for the remainder of this discussion.

With the knowledge that the simple approach is not the way to write a Silverlight random-rectangle program, let's do so anyway. You can download the BadRandomRectangle project or just follow along with my discussion. The MainPage.xaml file has just one item in the main Grid:

<Grid x:Name="LayoutRoot">
    <TextBlock Name="txt" Canvas.ZIndex="1" />
</Grid>

This TextBlock is for a counter displayed at the upper-left corner. The value of the ZIndex just ensures that it stays on top of everything else (which will have a default ZIndex of 0). The code part of the MainPage class looks like this:

public partial class MainPage : UserControl
{
    Random rand = new Random();
    ulong count = 0;

    public MainPage()
    {
        InitializeComponent();
        CompositionTarget.Rendering += OnRendering;
    }

    void OnRendering(object sender, EventArgs args)
    {
        int width = (int)LayoutRoot.ActualWidth;
        int height = (int)LayoutRoot.ActualHeight;

        if (width == 0 || height == 0)
            return;

        int x1 = rand.Next(width);
        int x2 = rand.Next(width);
        int y1 = rand.Next(height);
        int y2 = rand.Next(height);

        byte[] bytes = new byte[4];
        rand.NextBytes(bytes);
        Color clr = Color.FromArgb(bytes[0], bytes[1], 
                                   bytes[2], bytes[3]);
        Path path = new Path()
        {
            Fill = new SolidColorBrush(clr),
            Data = new RectangleGeometry()
            {
                Rect = new Rect(Math.Min(x1, x2), 
                                Math.Min(y1, y2),
                                Math.Abs(x1 - x2), 
                                Math.Abs(y1 - y2))
            }
        };

        LayoutRoot.Children.Add(path);
        txt.Text = (++count).ToString();
    }
}

I use the CompositionTarget.Rendering event for creating each rectangle. This event is based on the frame rate of the video display, so it's pretty fast and a good choice for visuals. On receipt of that event, the program creates a Path object with a random color, a random transparency value, and random dimensions. This is added to the Grid named LayoutRoot.

You can run the program from here:


BadRandomRectangles.html

At first it seems to work fine, but it doesn't take long before it begins to slow down dramatically. Every time a new Path element is added to the Grid, it forces a whole new layout cycle. Grid will then iterate through all its children in measure and arrange passes. Obviously as the number of children gets larger, this process becomes slower. At some point, I suspect some exception will be raised, but I don't know what that exception is, or if it's worthwhile waiting until it happens.

So that's obviously the bad way to write a Silverlight random-rectangle program. What can be done to fix it?

At first I thought I could write some logic to remove rectangles from the Grid when they are sufficiently covered up so they are no longer visible. That might work for completely opaque rectangles, but when rectangles are partially transparent — and in WPF and Silverlight you really don't want to display boring opaque rectangles — the entire concept of a rectangle becoming completely covered up is no longer quite viable.

In WPF, you could always write the random-rectangle logic as a Win32 or WinForms control, and then host that control in the WPF application, but that approach doesn't interest me very much.

The really interesting solution involves a WPF class named RenderTargetBitmap, and the similar class new in Silverlight 3 called WriteableBitmap. (Just to make things confusing for the dual-platform programmer, WPF also has a class named WriteableBitmap but it's different from Silverlight's WriteableBitmap.)

If you're familiar with the Windows API, Silverlight's WriteableBitmap is conceptually similar to the memory DC, in that it allows you to draw actual graphics on the surface of a bitmap. With the old memory DC, you used GDI drawing commands; with the WriteableBitmap the image plastered on the bitmap is an object derived from UIElement. Once the visual appearance of that UIElement has been transferred to the surface of the bitmap, the UIElement itself is no longer needed and can be eliminated by the garbage collector.

Here's another way to think about it: A WriteableBitmap is just like the video display surface of an old-fashioned graphical environment before anybody thought about retained-mode graphics and composition layers. Just draw on it and throw the paintbrush away (metaphorically speaking).

I discussed the WPF versions of RenderTargetBitmap and WriteableBitmap in my MSDN Magazine article "Bitmaps and Pixel Bits". Jeff Prosise has written about the Silverlight 3 WriteableBitmap here, here, and here, and you'll find more examples of WriteableBitmap if you bingle it.

A random-rectangle program for Silverlight can consist of an Image element containing a big WriteableBitmap. During each timer tick, a new rectangle can be rendered on this bitmap without any buildup of thousands of rectangle elements.

You can download the BetterRandomRectangles project or follow along here. The XAML file puts both an Image and a TextBlock in the Grid:

<Grid x:Name="LayoutRoot"
      SizeChanged="OnGridSizeChanged">
    <Image Name="img" />
    <TextBlock Name="txt" />
</Grid>

The code section includes a field of type WriteableBitmap field and creates a new one every time the size of the Grid changes. This WritableBitmap is displayed by the sole Image element:

public partial class MainPage : UserControl
{
    Random rand = new Random();
    ulong count = 0;
    WriteableBitmap bitmap;

    public MainPage()
    {
        InitializeComponent();
        CompositionTarget.Rendering += OnRendering;
    }

    void OnGridSizeChanged(object sender, SizeChangedEventArgs args)
    {
        if (args.NewSize.IsEmpty)
            return;

        bitmap = new WriteableBitmap((int)args.NewSize.Width, 
                                     (int)args.NewSize.Height);
        img.Source = bitmap;
        count = 0;
    }

I thought about copying the contents of the existing WriteableBitmap to the new WriteableBitmap, but I decided to keep the code simple. The OnRendering event handler is very similar to the earlier one, except that the Path object is rendered on the WriteableBitmap:

    void OnRendering(object sender, EventArgs args)
    {
        if (bitmap == null)
            return;

        int width = bitmap.PixelWidth;
        int height = bitmap.PixelHeight;

        int x1 = rand.Next(width);
        int x2 = rand.Next(width);
        int y1 = rand.Next(height);
        int y2 = rand.Next(height);

        byte[] bytes = new byte[4];
        rand.NextBytes(bytes);
        Color clr = Color.FromArgb(bytes[0], bytes[1], 
                                   bytes[2], bytes[3]);
        Path path = new Path()
        {
            Fill = new SolidColorBrush(clr),
            Data = new RectangleGeometry()
            {
                Rect = new Rect(Math.Min(x1, x2), 
                                Math.Min(y1, y2),
                                Math.Abs(x1 - x2), 
                                Math.Abs(y1 - y2))
            }
        };

        path.Measure(new Size(width, height));
        path.Arrange(new Rect(new Point(), path.DesiredSize));

        bitmap.Render(path, null);
        bitmap.Invalidate();
        txt.Text = (++count).ToString();
    }
}

Notice that the program calls Measure and Arrangle on the Path element to give it a finite size. The documentation indicates that these calls are necessary for any element that is not part of an existing visual tree, but if you comment them out, the program still works. The Path is rendered on the WriteableBitmap with a call to Render. To update the bitmap as it appears in the Image element, a call to Invalidate is also required. You can run the program from here:


BetterRandomRectangles.html

I have insufficient hubris to call this program BestRandomRectangles, but I really haven't been able to think of a better approach.