Silverlight

Scale other content on top of a Deep Zoom Image

Marthinus asked, in a comment:

“is there ANY way to overwrite this, so that I can make the msi use a multiscalesubimage WITH an extra canvas ontop (which would then move and scale WITH the subimage)?”

Now, I’ve never done overlays with collections and sub-images, and one thing you definitely can’t do is interleave other content with subimages of a deep zoom image (since the MultiScaleImage is a single UI element). But I have done things which have locked an overlay panel to the movement of the MultiScaleImage. Here’s a sample. First, the Xaml.


<UserControl x:Class="DeepZoomCanvas.MainPage"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
 mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
 <Grid x:Name="LayoutRoot">
 <MultiScaleImage x:Name="msi" Source="http://www.uslot.com/ClientBin/dzc_output.xml"/>
 <Grid 
 IsHitTestVisible="False"
 x:Name="overlay">
 <Grid.RenderTransform>
 <TransformGroup>
 <ScaleTransform x:Name="overlayScale"/>
 <TranslateTransform x:Name="overlayTranslate"/>
 </TransformGroup>
 </Grid.RenderTransform>
 <Border 
 CornerRadius="8" 
 Background="#44FFFFFF" 
 VerticalAlignment="Center" 
 HorizontalAlignment="Center" 
 Padding="30">
 <StackPanel>
 <TextBlock 
 FontFamily="Arial" 
 FontSize="24" 
 MaxWidth="400" 
 FontWeight="Bold" 
 TextWrapping="Wrap">
 This shows some elements which 
 scale and translate locked to 
 the underlying MultiScaleImage. 
 Any layout items can be used here.
 </TextBlock>
 <Border Background="#55FF0000" Padding="10" HorizontalAlignment="Center" VerticalAlignment="Top">
 <StackPanel>
 <TextBlock 
 FontFamily="Arial"
 FontSize="0.1"
 >This is a tiny line of text which still scales in concert with the deep zoom image.</TextBlock>
 </StackPanel>
 </Border>
 </StackPanel>
 </Border>
 </Grid>
 </Grid>
</UserControl>

And the corresponding C# code:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace DeepZoomCanvas
{
 public partial class MainPage : UserControl
 {
 public MainPage()
 {
 InitializeComponent();
 msi.MouseLeftButtonDown += new MouseButtonEventHandler(msi_MouseLeftButtonDown);
 msi.MouseMove += new MouseEventHandler(msi_MouseMove);
 msi.MouseLeftButtonUp += new MouseButtonEventHandler(msi_MouseLeftButtonUp);
 msi.MouseWheel += new MouseWheelEventHandler(msi_MouseWheel);

 // Track changes to the multi scale image
 msi.ViewportChanged += new RoutedEventHandler(msi_ViewportChanged);
 }

 /// <summary>
 /// This is the code which locks the overlay to the underlying deep zoom image.
 /// All it really does is set the scale factor and offset of the overlay
 /// based on the current setting of the deep zoom image.
 /// </summary>
 ///
<param name="sender">event sender</param>
 ///
<param name="e">event args</param>
 void msi_ViewportChanged(object sender, RoutedEventArgs e)
 {
 // This event is called during animations of the image.
 // Match the scaling of the canvas with the image
 Point viewportOrigin = msi.ViewportOrigin;
 double viewportWidth = msi.ViewportWidth;

 // The scale factor is just the inverse of the ViewportWidth
 overlayScale.ScaleX = 1 / viewportWidth;
 overlayScale.ScaleY = 1 / viewportWidth;

 // The offset is calculated by finding the location of the origin of the dzi
 // in element coordinates.
 Point newO = LogicalToElement(new Point(), viewportOrigin, viewportWidth);
 overlayTranslate.X = newO.X;
 overlayTranslate.Y = newO.Y;
 }

 private Point LogicalToElement(Point p, Point Origin, double Width)
 {
 return new Point(((p.X - Origin.X) / Width) * msi.ActualWidth,
 ((p.Y - Origin.Y) / Width) * msi.ActualWidth);
 }

 public Point ElementToLogical(Point p, Point Origin, double Width)
 {
 return new Point(Origin.X + (p.X * Width) / msi.ActualWidth,
 Origin.Y + (p.Y * Width) / msi.ActualWidth);
 }

#region Mouse handling
 void msi_MouseWheel(object sender, MouseWheelEventArgs e)
 {
 if (e.Delta < 0)
 {
 Point logicalPoint = msi.ElementToLogicalPoint(lastMousePos);
 msi.ZoomAboutLogicalPoint(0.8, logicalPoint.X, logicalPoint.Y);

 }
 else
 {
 Point logicalPoint = msi.ElementToLogicalPoint(lastMousePos);
 msi.ZoomAboutLogicalPoint(1.2, logicalPoint.X, logicalPoint.Y);
 }
 }

 void msi_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
 {
 if (IsMouseDown)
 {
 msi.ReleaseMouseCapture();
 if (IsDrag == false)
 {
 if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
 {
 Point logicalPoint = msi.ElementToLogicalPoint(lastMousePos);
 msi.ZoomAboutLogicalPoint(0.8, logicalPoint.X, logicalPoint.Y);
 }
 else
 {
 Point logicalPoint = msi.ElementToLogicalPoint(lastMousePos);
 msi.ZoomAboutLogicalPoint(1.2, logicalPoint.X, logicalPoint.Y);
 }
 }
 IsMouseDown = false;
 IsDrag = false;
 }
 }

 void msi_MouseMove(object sender, MouseEventArgs e)
 {
 lastMousePos = e.GetPosition(msi);
 if (IsMouseDown)
 {
 IsDrag = true;
 }
 if (IsDrag)
 {
 Point newPoint = lastMouseViewPort;
 newPoint.X += (lastMouseDownPos.X - lastMousePos.X) / msi.ActualWidth * msi.ViewportWidth;
 newPoint.Y += (lastMouseDownPos.Y - lastMousePos.Y) / msi.ActualWidth * msi.ViewportWidth;
 msi.ViewportOrigin = newPoint;
 }
 }

 bool IsMouseDown = false;
 bool IsDrag = false;
 private Point lastMouseDownPos = new Point();
 private Point lastMouseViewPort = new Point();
 private Point lastMousePos = new Point();

 void msi_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
 {
 IsMouseDown = true;
 msi.CaptureMouse();
 lastMouseDownPos = e.GetPosition(msi);
 lastMouseViewPort = msi.ViewportOrigin;
 }

 #endregion    
 }
}

All the work is done in the ViewportChanged event handler (which fires every time the viewport changes, even during animations). I’ve set a scale transform and a translate transform on the grid (which could be a canvas if you want) and we adjust the scale and translate values to match the underlying image.

All the rest of the code is just bog-standard deep zoom image mouse handling.

Try zooming into the red square to see a very small line of text.

Hope this helps.

Advertisements

Why don’t I get MouseUp events in the Bing Maps Silverlight Component

The Bing Maps Silverlight component (currently in CTP) allows you to overlay items on the map, but it does some odd things to mouse events. Take the following Xaml:


<UserControl x:Class="BingBlogPost.MainPage"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 xmlns:map="clr-namespace:Microsoft.VirtualEarth.MapControl;assembly=Microsoft.VirtualEarth.MapControl"
 mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
 <Grid x:Name="LayoutRoot">
 <map:Map>
 <Border Padding="8"
 Background="Gray"
 HorizontalAlignment="Center"
 VerticalAlignment="Center"
 MouseLeftButtonUp="Message_Click"
 >
 <TextBlock Text="Click here"/>
 </Border>
 </map:Map>
 </Grid>
</UserControl>

This will show the world map, with our ‘control’ in the centre. Now, mouse handling is interesting using the map component. You’ll notice that if you click and drag on the control, the map underneath will drag. In this example we’ve got an event handler which is set to receive the button up event (to simulate a mouse click). here’s the event handling code:

private void Message_Click(object sender, MouseButtonEventArgs e)
{
     MessageBox.Show(“Clicked!”);
}

But you’ll notice that this event doesn’t fire on a single click. Oddly, it does fire after a double-click. So clearly, the map itself is doing something tricky with events.

If you want to get a click event, there’s two things you can do. You could put in an actual button. This handles mouse events properly and will pass on its own Click event as you’d expect. But if you want to roll your own, there’s a simple trick: add a handler on the control for MouseLeftButtonDown and handle it with this code:

private void Message_MouseDown(object sender, MouseButtonEventArgs e)
{
     e.Handled = true;
}

This is enough to stop the map eating the mouse down event and capturing the mouse (which is why you never get the event). You’ll get the Up event as you expect and you can handle it normally.

Wrangling very large Deep Zoom images

My current project features a large Deep Zoom image. Very large. About 123 Gigapixels, in fact. Here’s the raw numbers:

  • 20 tile levels (0-19)
  • about 20GB of jpeg images
  • about 2.5 million tiles
  • almost 1.9 million tiles just in the highest level of zoom

First of all, what’s impressive is that Silverlight doesn’t even break a sweat with this image. Deep Zoom is designed such that it almost doesn’t matter how big the raw image is, it only matters how many pixels you’ve got on your screen, because it will always show you the resolution and section of the picture that you want. The main constraint is how fast you can serve (and download) the tiles.

But such a large image and tile set brings some interesting problems, and I wanted to post a bit about some of the issues we’ve found, and how we’ve addressed them.

Lots of files in one place are bad

Although not cripplingly so…

As mentioned earlier, our top level of tiles (the native resolution level) contained almost 1.9 million tiles. This is how Deep Zoom composer arranges its output and how Silverlight’s MultiScaleImage expects to find them. And it still works. Although one thing we learned early on is that FAT32 can’t cope with that number of files. We use removable hard drives for convenience, and I would always forget to reformat them to NTFS, then spend hours trying to build our image, only to fail because it ran out of directory entries. Grr.

But Fat32 aside, serving the images from NTFS on these tiny hard drives was very smooth. It didn’t seem to worry that there were so many files. The problem comes when copying the files, particularly to a network server. The copy would always start out fairly nippy, but when it got to the larger directories, it would just grind to a halt, probably because it was doing a directory enumeration each time over the network. It was definitely slowing down over time.

The solution I decided to use was to rearrange the files. I decided to take the X index of the filename and use that to generate a subdirectory in which to store the file. This would mean that, for example, if I had a file 142_1232.jpg in level 19, it would move from 19\142_1232.jpg to 19\1\4\2\142_1232.jpg. This method means that the maximum number of files in any single directory would now depend on the height of the image, rather than the area – in our specific case limiting us to 1718 files maximum.

Now, this is all very well, but the MultiScaleImage expects the files in their original places, so how do we fix that problem. Two ways would work. The first would be to use URL rewriting on the server side (mod_rewrite on Apache, for example) which would work OK, but we’d need two different solutions depending whether it’s Apache or WIndows serving the files. The second way is to write a custom tile source for MultiScaleImage.

Advantages of a Custom TileSource

The main advantage is that we don’t care where the images are hosted – they remain just files in a directory structure, and can be served fast by the web server. Also, avoiding mod_rewrite etc. might be useful as that’s more work for the servers to do that could be better performed by the client. It also means that the same solution works whether we’re serving locally (for testing) or serving from a dev server (which would be running Apache, probably).

Problems with a Custom TileSource

Although it’s easy enough to inherit from the base MultiScaleTileSource class, they’ve not made it very easy to do exactly what you need to do. I wanted to be able to create a class which had a constructor which took a Uri, and which read from an Xml file to get information about size etc. But this seems to be impossible. MultiScaleTileSource expects you to know already the dimensions of your image when the constructor is called, and doesn’t allow any way to initialise these values after the constructor is called, due to the protection level of other members. And since Silverlight doesn’t have a synchronous way to read from a file, you can’t open the file in the constructor. Annoying.

In the end, I cheated, because I know how big my image is already. I’ve already got code which takes the Uri of the image from the host HTML page, so I adjusted that to take the path to the new files, along with the width and height, as parameters which I can then pass to my new constructor, bypassing the need to read from a file. It’s not ideal, and I hope that this process is opened up a little in future.

Once I’d arrived at this way of initialising my class, writing the override method to return the tile paths was a little easier, although again I had to hard-code some specific information about my particular image – My image has a virtual square shape, but is actually rectangular, so there are lots of virtual tiles which don’t actually exist. DeepZoomImageTileSource handles this with the information in the xml file (it’s all part of the ‘sparse’ nature of deep zoom images) but I just hard-coded the limits of my tileset.

This solution works well for us. I haven’t tested it to see if it makes a difference with serving the images, but it definitely drastically reduced the time it takes to deploy our images to a server – from something that had already taken days and was slowing down, to something that completed within four hours. So that was a win for us.

I’m not sure how useful this code would be, but I’m including it anyway, for illustration.

public class HashedDeepZoomTileSource : MultiScaleTileSource
{
    private string RootPath;

    public HashedDeepZoomTileSource(string root, int imageWidth, int imageHeight) : base(imageWidth,imageHeight,256,256,1)
    {
        RootPath = root;

    }

    ///

    /// Constructs a tilesource given a string describing the root of the image
    /// And the image size.
    ///

    ///string containing the root directory (relative to XAP file) and the
    /// width and height packed in the following form:
    /// root|width|height
    ///
    /// so an image with a root directory GeneratedImages/uk3_files and width of 232000
    /// and a height of 445000 would have a string:
    ///
    /// GeneratedImages/uk3_files|232000|445000
    ///
    /// If the path is a regular path, then we simply construct a normal DeepZoomImageTileSource.
    ///     /// Either A HashedDeepZoomTileSource object or a normal DeepZoomImageTileSource
    public static MultiScaleTileSource UnpackPath(string packedRoot)
    {
        string[] parts = packedRoot.Split(‘|’);
        int width;
        int height;
        if (parts.Length != 3 || !int.TryParse(parts[1], out width) || !int.TryParse(parts[2], out height))
        {
            return new DeepZoomImageTileSource(new Uri(packedRoot, UriKind.Relative));
        }
        return new HashedDeepZoomTileSource(parts[0], width, height);
    }

    ///

    /// This is a hack, It (and maxheights) describes the maximum tile ID available
    /// at all the tile heights in the image.
    ///

    int[] maxwidths =
    {
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        1,
        2,
        4,
        8,
        17,
        34,
        68,
        136,
        273,
        546,
        1093

    };

    int[] maxheights =
    {
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        1,
        3,
        6,
        13,
        26,
        53,
        107,
        214,
        429,
        859,
        1718
    };

    ///

    /// Overrides the GetTileLayers method to provide a URL for the given tile source.
    /// We have to create a fully qualified domain path – it doesn’t like relative
    /// paths.
    ///

    ///Which level of tile resolution (0-19 e.g.)     ///X position of required tile     ///Y Position of required tile     ///List to populate with Uris pointing at the tiles they want     protected override void GetTileLayers(int tileLevel, int tilePositionX, int tilePositionY, System.Collections.Generic.IList tileImageLayerSources)
    {
        if (tileLevel >= 0)
        {
            // MASSIVE KLUDGE
            // Since our map is ‘sparse’ in that its logical size if 480000 square
            // but we only have tiles for a width of 280000 so we have to not return
            // non-existent URLs.
            // We look up the maximum X position value from the maxwidths

            if (tilePositionX <= maxwidths[tileLevel] && tilePositionY <= maxheights[tileLevel])             {                 StringBuilder path = new StringBuilder(RootPath);                 if (RootPath.EndsWith("/") == false)                 {                     path.Append("/");                 }                 path.AppendFormat("{0}/", tileLevel);                 foreach (char digit in tilePositionX.ToString().ToCharArray())                 {                     path.AppendFormat("{0}/", digit);                 }                 path.AppendFormat("{0}_{1}.jpg", tilePositionX, tilePositionY);                 string s = App.Current.Host.Source.ToString();                 s = s.Substring(0, s.LastIndexOf('/') + 1);                 tileImageLayerSources.Add(new Uri(s + path.ToString(), UriKind.Absolute));             }         }     } } [/sourcecode]

Funky Popups using Tweener

Please note: There is an update to this post, Funky Popups using Silverlight 3, which describes how to achieve the same effect using the built-in easing functions of silverlight 3. However, this post remains a good desription of what easing does.

On a current project, we have some controls which have to pop up more info when clicked or using MouseOver. Because this is Silverlight, obviously we want it to look nice, so just switching Visibility from Collapsed to Visible doesn’t really cut the mustard. And I feel there are definite benefits from having some form of animation which conceptually links the box that pops up with the object you used to pop it up – this has been the case right back to the first Mac when new windows would grow and shrink accompanied by a simple animated border. I’ve been playing with various ways to animate these transitions, including using the Tweener class from the SilverlightContrib project.

In Silverlight we’ve got a lot of ways we can make information appear – sliding, fading in, scaling, and my default ‘appearance’ animation tended to be a simple fade combined with a linear scale.

Here’s a XAML for an illustration of this.


<UserControl x:Class="FunkyPopup.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0"
MouseEnter="simple_MouseEnter"
MouseLeave="simple_MouseLeave">
<Grid.Resources>
<Storyboard x:Name="simpleFadeBoardIn">
<DoubleAnimation
Storyboard.TargetName="simpleScaleTransform"
Storyboard.TargetProperty="ScaleX"
From="0.6"
To="1"
Duration="0:0:0.5"/>
<DoubleAnimation
Storyboard.TargetName="simpleScaleTransform"
Storyboard.TargetProperty="ScaleY"
From="0.6"
To="1"
Duration="0:0:0.5"/>
<DoubleAnimation
Storyboard.TargetName="simpleBorder"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.5"/>
</Storyboard>
<Storyboard x:Name="simpleFadeBoardOut">
<DoubleAnimation
Storyboard.TargetName="simpleScaleTransform"
Storyboard.TargetProperty="ScaleX"
From="1"
To="0.6"
Duration="0:0:0.5"/>
<DoubleAnimation
Storyboard.TargetName="simpleScaleTransform"
Storyboard.TargetProperty="ScaleY"
From="1"
To="0.6"
Duration="0:0:0.5"/>
<DoubleAnimation
Storyboard.TargetName="simpleBorder"
Storyboard.TargetProperty="Opacity"
From="1"
To="0"
Duration="0:0:0.5"/>
</Storyboard>
</Grid.Resources>
<Border VerticalAlignment="Center" HorizontalAlignment="Center" BorderThickness="3" BorderBrush="Black" Background="White" CornerRadius="5">
<TextBlock Margin="8" Text="Simple Fade and Scale"/>
</Border>
<Border
x:Name="simpleBorder"
VerticalAlignment="Center"
HorizontalAlignment="Center"
BorderThickness="3"
BorderBrush="Black"
Background="White"
CornerRadius="5"
RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="simpleScaleTransform" ScaleX="0" ScaleY="0"/>
</TransformGroup>
</Border.RenderTransform>
<TextBlock Margin="8" Width="250" TextWrapping="Wrap" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eu lacus in magna posuere sagittis. Curabitur vel odio nec enim mollis cursus. Etiam turpis. Nullam risus nisl, semper et, facilisis vitae, ultricies sed, quam. Sed pharetra scelerisque est. Vivamus lorem. Sed id elit sed mi ultrices feugiat. Mauris luctus tellus. Nullam vestibulum porta lorem. Sed congue pellentesque ipsum. Praesent dolor. Aliquam leo felis, euismod at, adipiscing at, auctor quis, urna."/>
</Border>
</Grid>
</Grid>
</UserControl>

What this does is put two Borders containing text in a grid column. One is the ‘control’ and the other is the information we want to pop up. The popup border has a name and a ScaleTransform with both scales set to 0. Then there are two storyboards, one to make the info pop up, the other to hide it again.

The storyboards animate the opacity, and also scale up from 0.7 to 1, giving a fairly pleasant effect when it appears. The transition takes half a second, a little longer than you’d actually want in real life, but this is intended to demonstrate the various options in  animating, rather than be a finished item. Slower transitions make it easier to see what’s happening.

The associated code-behind is here, and merely handles the MouseEnter and MouseLeave events, firing the appropriate storyboards.


using System.Windows.Controls;
using System.Windows.Input;

namespace FunkyPopup
{
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
}

private void simple_MouseEnter(object sender, MouseEventArgs e)
{
simpleFadeBoardIn.Begin();
}

private void simple_MouseLeave(object sender, MouseEventArgs e)
{
simpleFadeBoardOut.Begin();
}
}
}

If you run this code, you’ll get a border/text ‘control’. If you move the mouse over the control the info pane will pop up, and it will vanish when the mouse leaves the control. You’ll notice I set the RenderTransformOrigin property on the info grid so it scales from the centre, otherwise it scales from the top left of the grid.

So far, this is just a simple linear animation. Looks fine but there are some simple ways to do a bit more.

Tweening

A lot of animation makes use of tweening which is a process of taking the startpoint and endpoint of a desired animation and generating the steps in between. The whole Silverlight animation process does this for you whenever you use

DoubleAnimation

for example. It takes the From and To values, sees how long the animation should take, and each time a frame needs to be drawn, it mathematically calculates what the ‘in-between’ value should be – halfway through the animation the value will be the mean of the first and last value.

That’s fine for quite a lot of animation, but often you want something more – perhaps you want the animation to start quickly and slow down as it nears the end. Or vice versa. Silverlight has a built-in way to achieve some if these effects – see the SplineDoubleKeyFrame class for a pointer to how it works – but I’m looking at a different way of achieving these results.

What we want is a way of saying ‘Here’s the start point, and the end point, please animate between these points using a specific type of ‘in-between’ calculation. This is what the Tweener class does for you. I’ll show an example that would be difficult to do otherwise, then discuss what it’s doing.

To use the Tweener library, you’ll have to first download the SilverlightContrib dlls (get the source as well if you’re interested) and add a reference to the SilverlightContrib.dll in your project. Then in your XAML you’ll need to add a new namespace like so:


xmlns:tween="clr-namespace:SilverlightContrib.Tweener;assembly=SilverlightContrib"

I add a new ‘control’ to the test application, in the next grid column like so:


<Grid Grid.Column="1"
MouseEnter="bounce_MouseEnter"
MouseLeave="bounce_MouseLeave">
<Grid.Resources>
<Storyboard x:Name="bounceFadeBoardIn">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="bounceScaleTransform"
Storyboard.TargetProperty="ScaleX"
tween:Tween.From="0.6"
tween:Tween.To="1"
tween:Tween.Fps="30"
tween:Tween.TransitionType="EaseOutBounce"
Duration="0:0:0.5"/>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="bounceScaleTransform"
Storyboard.TargetProperty="ScaleY"
tween:Tween.From="0.6"
tween:Tween.To="1"
tween:Tween.Fps="30"
tween:Tween.TransitionType="EaseOutBounce"
Duration="0:0:0.5"/>
<DoubleAnimation
Storyboard.TargetName="bounceBorder"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.5"/>
</Storyboard>
<Storyboard x:Name="bounceFadeBoardOut">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="bounceScaleTransform"
Storyboard.TargetProperty="ScaleX"
tween:Tween.Fps="30"
tween:Tween.TransitionType="EaseInBounce"
tween:Tween.From="1"
tween:Tween.To="0.6"
Duration="0:0:0.5"/>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="bounceScaleTransform"
Storyboard.TargetProperty="ScaleY"
tween:Tween.Fps="30"
tween:Tween.TransitionType="EaseInBounce"
tween:Tween.From="1"
tween:Tween.To="0.6"
Duration="0:0:0.5"/>
<DoubleAnimation
Storyboard.TargetName="bounceBorder"
Storyboard.TargetProperty="Opacity"
From="1"
To="0"
Duration="0:0:0.5"/>
</Storyboard>
</Grid.Resources>
<Border VerticalAlignment="Center" HorizontalAlignment="Center" BorderThickness="3" BorderBrush="Black" Background="White" CornerRadius="5">
<TextBlock Margin="8" Text="Bounce Scale and fade"/>
</Border>
<Border
x:Name="bounceBorder"
VerticalAlignment="Center"
HorizontalAlignment="Center"
BorderThickness="3"
BorderBrush="Black"
Background="White"
CornerRadius="5"
RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="bounceScaleTransform" ScaleX="0" ScaleY="0"/>
</TransformGroup>
</Border.RenderTransform>
<TextBlock Margin="8" Width="250" TextWrapping="Wrap" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eu lacus in magna posuere sagittis. Curabitur vel odio nec enim mollis cursus. Etiam turpis. Nullam risus nisl, semper et, facilisis vitae, ultricies sed, quam. Sed pharetra scelerisque est. Vivamus lorem. Sed id elit sed mi ultrices feugiat. Mauris luctus tellus. Nullam vestibulum porta lorem. Sed congue pellentesque ipsum. Praesent dolor. Aliquam leo felis, euismod at, adipiscing at, auctor quis, urna."/>
</Border>
</Grid>

and the event handlers in the C# file:


private void bounce_MouseEnter(object sender, MouseEventArgs e)
{
bounceFadeBoardIn.Begin();
}

private void bounce_MouseLeave(object sender, MouseEventArgs e)
{
bounceFadeBoardOut.Begin();
}

If you run it, you’ll see that the info pops up quicker and gives a little bounce before it settles down into its 100% size and position. So what’s it doing? And how?

The key difference between the two storyboards is that the second ones are DoubleAnimationUsingKeyframes instead of the simpler DoubleAnimation. But you’ll also notice that I’m not supplying any keyframes as you would normally expect. That’s what Tweener does for you. I replaced the From and To properties with:


tween:Tween.Fps="30"
tween:Tween.TransitionType="EaseInBounce"
tween:Tween.From="1"
tween:Tween.To="0.6"

These are Attached Properties supplied by the Tween class, and what happens when these properties are attached to the DoubleAnimationUsingKeyframes timeline is that the Tweener library will generate the required keyframes for the required animation for you. It needs to know the From and To values, and you have to specify the number of frames per second to generate but this merely tells the tweener how many keyframes to generate – it doesn’t affect the ultimate smoothness of the animation since each generated keyframe will also have extra in-between frames generated for it by the Silverlight animation system. The really interesting one is the TransitionType. This specifies which equation to use out of a selection of possible ways to transition from one value to another. In this example we’re using the EaseOutBounce type for the appearance and EaseInBounce for the vanishing.

The list of types is fairly long (intellisense lists them) and their naming tells you something about them. EaseInXXX means the particular type of effect happens at the start of the animation. EaseOutXXX means it happens at the end (which is why we use EaseOutBounce for the popup appearing). You could try changing the transition type in the XAML to see what effect each of them gives. Generally the transitions are designed to ease the animation. A simple, linear animation looks very artificial. Items start and stop very suddenly, and there’s no sense of acceleration or deceleration. It’s this effect which easing is designed to solve. Most of the easing functions go straight from the first value to the last, with varying extremes of acceleration (and by using EaseInOutXXX you can get the acceleration both at the start and the end) and others, like the EaseOutBounce we used here, give a slightly funkier effect where the animation overshoots the endpoint, then bounces back and forth a little. It gives a more organic feel than a linear animation.

Some of these easing effects can be replicated using the KeySpline property of SplineDoubleKeyFrame, but it can be hard to know exactly what shape spline curve to use to achieve the required effect. These transitions originated in the Flash world, where they are quite commonly used, as it’s much easier to select the type of transition by name. Also, a simple spline can’t replicate the bouncy effects.

So, now we’ve got the box appearing with a little more wit, is there anything else we can do? Well, you can get some even nicer effects simply by playing with the timings of the animations, making the scaling asymmetric. I’ll add yet another of our ‘controls’. Here’s the full XAML for the new item:


<Grid Grid.Column="2"
MouseEnter="swish_MouseEnter"
MouseLeave="swish_MouseLeave">
<Grid.Resources>
<Storyboard x:Name="swishFadeBoardIn">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="swishScaleTransform"
Storyboard.TargetProperty="ScaleX"
tween:Tween.From="0.3"
tween:Tween.To="1"
tween:Tween.Fps="10"
tween:Tween.TransitionType="EaseOutBounce"
Duration="0:0:0.5"
BeginTime="0:0:0.2"/>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="swishScaleTransform"
Storyboard.TargetProperty="ScaleY"
tween:Tween.From="0"
tween:Tween.To="1"
tween:Tween.Fps="10"
tween:Tween.TransitionType="EaseOutBounce"
Duration="0:0:0.5"/>
<DoubleAnimation
Storyboard.TargetName="swishBorder"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.1"/>
</Storyboard>
<Storyboard x:Name="swishFadeBoardOut">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="swishScaleTransform"
Storyboard.TargetProperty="ScaleX"
tween:Tween.Fps="30"
tween:Tween.TransitionType="EaseInBounce"
tween:Tween.From="1"
tween:Tween.To="0.3"
Duration="0:0:0.3"/>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="swishScaleTransform"
Storyboard.TargetProperty="ScaleY"
tween:Tween.Fps="30"
tween:Tween.TransitionType="EaseInBounce"
tween:Tween.From="1"
tween:Tween.To="0"
Duration="0:0:0.3"
BeginTime="0:0:0.2"/>
<DoubleAnimation
Storyboard.TargetName="swishBorder"
Storyboard.TargetProperty="Opacity"
From="1"
To="0"
BeginTime="0:0:0.45"
Duration="0:0:0.05"/>
</Storyboard>
</Grid.Resources>
<Border VerticalAlignment="Center" HorizontalAlignment="Center" BorderThickness="3" BorderBrush="Black" Background="White" CornerRadius="5">
<TextBlock Margin="8" Text="Swish Scale and fade"/>
</Border>
<Border
x:Name="swishBorder"
VerticalAlignment="Center"
HorizontalAlignment="Center"
BorderThickness="3"
BorderBrush="Black"
Background="White"
CornerRadius="5"
RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="swishScaleTransform" ScaleX="0.3" ScaleY="0"/>
</TransformGroup>
</Border.RenderTransform>
<TextBlock Margin="8" Width="250" TextWrapping="Wrap" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eu lacus in magna posuere sagittis. Curabitur vel odio nec enim mollis cursus. Etiam turpis. Nullam risus nisl, semper et, facilisis vitae, ultricies sed, quam. Sed pharetra scelerisque est. Vivamus lorem. Sed id elit sed mi ultrices feugiat. Mauris luctus tellus. Nullam vestibulum porta lorem. Sed congue pellentesque ipsum. Praesent dolor. Aliquam leo felis, euismod at, adipiscing at, auctor quis, urna."/>
</Border>
</Grid>

And the inevitable event handlers in the csharp file:


private void swish_MouseEnter(object sender, MouseEventArgs e)
{
swishFadeBoardIn.Begin();
}

private void swish_MouseLeave(object sender, MouseEventArgs e)
{
swishFadeBoardOut.Begin();
}

This time I’ll delve a bit deeper into the slight changes I’ve made get the effect. What I wanted was for the info pane to appear as if it’s first being stretched vertically, then horizontally. So I needed to change the timing and duration of the scaling animations. Here’s the animation for the X scale factor when it’s appearing.


<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="swishScaleTransform"
Storyboard.TargetProperty="ScaleX"
tween:Tween.From="0.3"
tween:Tween.To="1"
tween:Tween.Fps="10"
tween:Tween.TransitionType="EaseOutBounce"
Duration="0:0:0.3"
BeginTime="0:0:0.2"/>

The key changes here are:

  • The From value starts at 0.3 instead of 0. If we left it at 0, the first part of the Y animation would be invisible because it would be stretching a zero-width block.
  • The BeginTime and Duration have been adjusted so that it doesn’t start until 0.2 secs into the animation, so the first part of the animation is a pure vertical stretch

<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="swishScaleTransform"
Storyboard.TargetProperty="ScaleY"
tween:Tween.From="0"
tween:Tween.To="1"
tween:Tween.Fps="10"
tween:Tween.TransitionType="EaseOutBounce"
Duration="0:0:0.5"/>

The Y stretch runs for the full half second, but it starts at 0 so the box does appear from nowhere.


<DoubleAnimation
Storyboard.TargetName="swishBorder"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.1"/>

The Opacity animation has been made much faster, so it completes almost immediately, so we get to see the full effect of the stretching. Without this, part of the initial appearance would not really be visible.

The inverse animation is fairly straightforward, it’s just a matter of inverting the timings, so we start the X scale immediately, but the duration remains 0.3s. The Y scale keeps its full duration (although you can vary the effect by playing with the length and start point of the Y scale as well). Because we’re scaling down, we use the EaseInBounce instead of the EaseOutBounce we used for the appearance.

One current disadvantage of using Tweener is that Blend doesn’t recognise it, so there’s no tool support for it. I have some ideas of ways around that, but this post is already far too long, so I’ll save those for another day.

Here’s the full source code for this project.

Programmatically Create Deep Zoom Collections

The Expression team have released a new version of Deep Zoom Composer, and at last they’ve supplied a .NET assembly for creating Deep Zoom images. This has always been possible, using the SparseImageTool.exe that was supplied as part of Composer, but that was never a very good solution, particularly for large collections of images.

We have a potential use case where we’d want to create millions of deep zoom images, and expose these as collections, but without necessarily knowing which images were in a collection ahead of time. Or we might want to add new images to a large collection. Until now, that has always been impossible just using SparseImageTool, but using the new DLL it’s easy. Here’s some sample code which I just threw together to see how the API worked. You need two classes. The ImageCreator class creates individual Deep Zoom images, which was always easy with SparseImageTool. But the CollectionCreator can then take these images, and build a composition of all those images.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.DeepZoomTools;
using Path = System.IO.Path;

namespace DZCtest
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        private void Create_Click(object sender, RoutedEventArgs e)
        {
            ImageCreator creator = new ImageCreator();
            creator.TileFormat = ImageFormat.Jpg;
            creator.TileOverlap = 1;
            creator.TileSize = 256;

            List<string> files = new List<string>()
            {
                @"D:\Users\Pictures\DadsArmy\hi000350807.jpg",
                @"D:\Users\Pictures\DadsArmy\hi000350820.jpg",
                @"D:\Users\Pictures\DadsArmy\hi000360009.jpg"
            };
            string root = @"D:\Users\James\Visual Studio 2008\Projects\DeepZoomProjects\DeepZoomTests_Web\ClientBin\GeneratedImages\";
            List<string> dzi = new List<string>();
            foreach (var name in files)
            {
                string output = Path.Combine(root, Path.GetFileNameWithoutExtension(name) + ".dzi");
                dzi.Add(output);
                creator.Create(name, output);
            }

            CollectionCreator ccreator = new CollectionCreator();
            ccreator.TileFormat = ImageFormat.Jpg;
            ccreator.TileOverlap = 1;
            ccreator.TileSize = 256;
            ccreator.Create(dzi, Path.Combine(root, "da.dzc"));
        }
    }
}

You’ll have to add a Reference to DeepZoomTools.dll which is installed as part of Deep Zoom Composer. All this code does is take three hardcoded image paths, generates individual deep zoom images for them, then creates an overall collection of all three.

One thing there doesn’t seem to be is any way to lay out the collection other than having all the images on top of one another, but since any code which makes use of a collection would probably handle its own layout in code, this probably isn’t much of a drawback. And there might well another way to do that – I’ve literally only just thrown this code together, so I haven’t delved very far into what’s exposed.

It’ll be interesting to see what the performance is like. In a perfect world, I’d be able to do a search across millions of images and dynamically generate a collection of a few hundred images in response to a user request, but that might be asking a bit too much.

UPDATE: 9 minutes to process 305 images. 20 seconds to build the collection from those images. 20 seconds is probably a bit more than you’d want for a completely dynamic search, for example, but it’s probably absolutely fine if you’re uploading images to a more static collection like a picture album. 2 seconds to process a typical digital camera image is really rather good. Next test will be some of the really large images that DZC choked on last time I tried it.

Why does LayoutUpdated pass a null sender?

Because it does not mean what you think it means.

As Dave Relyea explains, LayoutUpdated is fired every time layout did anything in the tree so it’s not specific to a particular object, and you can’t use the sender to tell you what you’re interested in.

What you need is the SizeChanged event. LayoutUpdated is going to get fired an awful lot, while SizeChanged gets fired when the object you’re interested in has its size changed. It won’t get fired if the position of the object changes, but that’s probably what you want most of the time.

My particular code had a UserControl in a popup, and the UserControl had a variable size. I wanted it to always remain on screen, intelligently positioning itself so that it’s fully on screen (like a menu would) but when it’s first created (and indeed, laid out) its actual size is {0,0} because (I believe) that it hasn’t been added to the visual tree yet because the parent Popup is closed. I needed to wait until the size of the object was fully calculated before I positioned the popup, and I thought LayoutUpdated would do the trick, so I had some code like this:

void infoPane_LayoutUpdated(object sender, EventArgs e)
{
WalkInfoPane pane = sender as WalkInfoPane;
if (pane != null && pane.ActualHeight > 0 && pane.ActualWidth > 0)
{
Point anchor = PopupAnchor.TransformToVisual(null).Transform(new Point());
if (anchor.Y < 240) { PanePopup.VerticalOffset = -240 + (240 - anchor.Y); } else { PanePopup.VerticalOffset = -240; } pane.MakeVisible(); } } [/sourcecode] (PanePopup is the parent Popup. I was expecting the sender to point at the child control, but it gives null.) Not surprisingly, this code was never getting called because sender is always null. Using the same code in a SizeChanged event works perfectly. It's a shame MSDN doesn't cover details like this.

“Value does not fall within the expected range.” when adding a UserControl to a Panel

This just took me a while to puzzle out. I’d built a custom UserControl which worked perfectly while I was initially testing it, but when I modified the application so that I was using several of them, all of a sudden any attempt to add the control to the panel was giving the error “Value does not fall within the expected range.” But it wasn’t giving any clue as to which value was the problem.

One reason it took me a little longer to find the solution is that as well as this custom control (which has a bunch of visual states) I was inserting it into a custom panel, so I wasn’t sure if my code in the panel was causing the problem. But in the end it was something fairly obscure.

My control had an x:Name attribute set on its UserControl root element, added by Blend as part of the visual transitions, and trying to add two copies of the control to the panel meant adding two things with the same name attribute, which is forbidden, and it’s this which was causing the error. If I removed the name attribute, all worked normally.

This does raise an issue, though. If you can’t set a name attribute, how do you declaratively define storyboards which affect properties on the root usercontrol itself? I still don’t know the answer to this. Luckily, I’d already refactored my control such that the transitions could be applied to the LayoutRoot container instead, but that might not be a solution for all scenarios.