Jeff Prosise's Blog

More Fun with Silverlight 3's WriteableBitmap

I'm currently preparing a couple of new talks for TechEd Europe, which takes place next month in Berlin. One of the talks is entitled "Cool Graphics, Hot Code: Ten Visual Effects to Make You the Envy of Your Peers." In it, I plan to present techniques for producing eye-popping graphics and visual effects in Silverlight 3. I've been writing new code samples and revising existing ones. And thanks to Silverlight 3's WriteableBitmap class, I was able to make a dramatic improvement to a sample that I originally wrote for Silverlight 2 (and before that, for Silverlight 1.0).

The sample I'm referring to is the Magnifier demo that lets you drag a virtual magnifying glass around a scene:

Magnifier Demo 

This demo works by creating a shadow copy of the scene that's 4 times wider and 4 times higher than the original. The shadow copy is normally invisible, but when the left mouse button goes down, a portion of the shadow copy is revealed to the user through a circular clipping region, as pictured below. The only real trick is making sure that what's shown in the clipping region maps to what's under the cursor in the foreground scene. I accomplish that by moving the shadow copy with each mouse move so that the cursor's logical coordinates in both scenes are always exactly the same.

How Magnifier Works

In the Silverlight 2 version of this sample, I had to declare, in XAML, two identical copies of the scene--one to represent the scene in the foreground (the one that the user sees), and another to represent the shadow copy. I wanted to generate the shadow copy programmatically, but Silverlight 2 gave me no facility for doing that.

Enter Silverlight 3 and WriteableBitmap, which provides an elegant solution to the problem of generating the shadow copy. In the revised Magnifier demo, I use WriteableBitmap.Render with a ScaleTransform to generate a 4x image from the foreground scene:

  // Render the main Canvas into the image at 4 times its nominal size

  WriteableBitmap bitmap = new WriteableBitmap((int)_image.Width, (int)_image.Height);

  ScaleTransform transform = new ScaleTransform();

  transform.ScaleX = transform.ScaleY = 4.0;

  bitmap.Render(MainCanvas, transform);

  bitmap.Invalidate();

It's now this image that the user sees through the magnifying glass rather than a duplicate Canvas. The benefit is obvious: if I want to change something in the scene, I no longer have to make the change in two places. You can judge the quality of the effect yourself by running the app online. And you can download the source code to see how it works.

If I have time before TechEd, I might factor out the magnifying glass into a custom control that can be applied to any scene (or part of a scene). Or perhaps the magnifying glass should be a behavior rather than a control. That's something to think about as I continue building code samples for the talk.

UPDATE: I just finished converting the magnifier into a custom behavior named MagnifierBehavior. Using it is now as simple as adding a reference to WintellectBehaviors.dll to your project and then adding a MagnifierBehavior to a Canvas. Thereafter, you can click anywhere in the Canvas to display the magnifying glass and everything on the Canvas is magnified through the lens. Moreover, the behavior exposes properties named ZoomFactor and LensRadius that you can use to customize the look:

  <Canvas Width="340" Height="340" Background="White">

    <i:Interaction.Behaviors>

      <local:MagnifierBehavior ZoomFactor="4.0" LensRadius="160.0" />

    </i:Interaction.Behaviors>

      ...

  </Canvas>

 

I'm really becoming sold on behaviors; as I mentioned in an earlier blog post, they add a whole new dimension to Silverlight. I'll be demoing this new behavior (and probably others, too) at TechEd Europe.

On Oct 29 2009 4:27 AMBy jprosise With 11 Comments

Comments (11)

  1. Nice one. You can make it faster if you implement the magnifier in a HLSL pixel shader.

    I'm looking forward to your TechEd talk in Berlin. :)

    -----------------------------------
    http://blog.rene-schulte.info
    http://twitter.com/rschu

  2. Great post, as always, Jeff. I was wondering if you were going to post the MagnifierBehavior on the Expression Blend gallery? It's not in the download. Behaviors are definitely THE way to go for this kind of UX!

  3. I'll either post the behavior in the gallery or provide a download link from my blog. I'm working on a couple more cool behaviors and will package them in a single project and assembly.

  4. Sounds good, Jeff. I managed to convert this to a behavior myself after a good night's sleep . I'm sure you accoutned for this, but in most cases, the image to magnify may not be loaded when the behavior is attached to its canvas. So for now, I just moved all of the lens setup code to a separate method called from the OnMouseLeftButtonDown event handler, created a new class level private variable that represents the Canvas to magnify, and pointed that to the AssociatedObject in the OnAttached event handler, along wth registering the event handlers for mouse actions. I'm sure the code can be beefed up from there, but it works great, and being able to set the ZoomFactor and LensRadius from the markup is an added bonus. Thanks again for a great post.

  5. I did the same thing in the behavior: moved the code that creates the shadow copy to the MouseLeftButtonDown handler. It's actually cleaner that way, because if the contents of the Canvas change between clicks, you get a fresh copy.

  6. Excellent Jeff. I'm wondering where i can find the assembly "MagnifierBehavior" so that I can use in the project?

  7. Excellent Jeff. I'm wondering where i can find the assembly "MagnifierBehavior" so that I can use in the project?

  8. Based on your ideas Jeff & Bob i wrote the similar Magnifier using the behaviour approach.
    One issue i'm facing is once the image magnified i want to position on the mouse click, rather what happens magnified image goes sometimes left or right.
    _image.SetValue(Canvas.LeftProperty, x);
    _image.SetValue(Canvas.TopProperty, y);
    where x & y are
    double x = e.GetPosition(this.AssociatedObject.Parent as UIElement).X;
    double y = e.GetPosition(this.AssociatedObject.Parent as UIElement).Y;
    Any help would be highly appreciated.

  9. Thanks Jeff and Bob, based on your ideas above i wrote an assembly and used Behaviour approach. The issue i'm running into is if I place the zoomed image on top of original image it goes off the mouse track either left or right. Not sure how to position correctly.

    double x = e.GetPosition(this.AssociatedObject.Parent as UIElement).X;
    double y = e.GetPosition(this.AssociatedObject.Parent as UIElement).Y;

    _image.SetValue(Canvas.LeftProperty, x);
    _image.SetValue(Canvas.TopProperty, y);

Leave a Comment