WPF: Zoom In An Image On Application Load

[4 minute read]

A Little Background…

I recently decided to create a new image viewer for Windows. I’m not a fan of the default Windows Photo Viewer and think most third party image viewers are even worse. The only one I’ve liked quite a bit is the viewer that comes bundled with Google’s Picasa. However, Picasa itself no longer really fits into my digital photo organization strategy and I really hate to install all of Picasa just so I can have the peripheral 5% that constitutes its image viewer. So I decided to roll my own using WPF, a framework I’ve never really used before, and blog about my adventures along the way. The project is open source and can be found here: SupaFly Image Viewer.


Visually, SupaFly doesn’t have much going on. (Nor should it.) It consists of a translucent main window, an image in the center, and - at the time of this writing - a grand total of three buttons: one to exit the application, and two for navigating the current directory of images. Functionally, it’s equally minimalistic. Display an image, one at a time, zoom in and out, and navigate to other images in the same directory. That’s really it. The application’s real pizzazz will come in the form of smooth, slick animations that I’m intent on including. For starters, I want the application to load the initial image with zero width and height then immediately zoom out to a predetermined size over the course of a 0.5 second animation. After the animation finishes, users can manually increase or decrease the zoom via keyboard input. Not exactly a tall order. But for this WPF neophyte, there were a few gotchas lying in wait that took more googling to unravel than I would have preferred.

‘Gotchas’ might be the wrong word, since that implies some sort of logical inconsistency; I simply wasn’t aware of one essential tenant of WPF development: Animations have precedence over bindings for property values. Take the following xaml snippet for example:

<!-- Assume a data context with all the appropriate bindings -->
<Window
    <Window.InputBindings>
        <KeyBinding Key="Up" Command="{Binding ZoomIn}" />
        <KeyBinding Key="Down" Command="{Binding ZoomOut}" />
    </Window.InputBindings>
    <Grid>
        <Image Source="{Binding MyPath}" HorizontalAlignment="Center"
        VerticalAlignment="Center" Width="{Binding DisplayedWidth}" >
            <Image.Triggers>
                <EventTrigger RoutedEvent="Image.Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="Width" From="0"
                            To="{Binding DisplayedWidth}" Duration="0:0:0.5" />
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Image.Triggers>
        </Image>
    </Grid>
</Window>

You might expect that snippet to do exactly as I said I wanted. I certainly did. But it has a couple flaws, one of which I still haven’t entirely figured out. The most immediately apparent problem is that the key bindings won’t work if I include the animation. Remove the animation, and they work just fine. Why? As it turns out, by default, the end value of the animation will override any other binding applied to a property. In order to stop the animation from doing this, you have to explicitly set the FillBehavior to Stop, like so:

<Storyboard>
    <DoubleAnimation Storyboard.TargetProperty="Width" From="0" FillBehavior="Stop"
    To="{Binding DisplayedWidth}" Duration="0:0:0.5" />
</Storyboard>

Now, when the animation completes, it will relinquish its control over the Width property back to the original binding. Okay! So now when the application starts, the image is loaded at a size of 0 by 0 pixels, immediately zooms out to the width set by DisplayedWidth, and at the end of it all a user can manually zoom in or out using their up and down arrow keys. Yes?

Sometimes.

If the image is relatively small, it works perfectly. If, on the other hand, it’s relatively large - say, a few megabytes - the animation doesn’t run. Or, at least, it doesn’t on my development machine. Instead, the application starts and BOOM! instant, full-sized image. If I run the animation on repeat with a large image, zooming in and out every half a second, there is no discernible lag. But on the initial load there is definitely something interfering with the animation. Unfortunately, I don’t have a good explanation for this one yet. My sense is there’s some sort of race condition occurring between the time (a) Image.Loaded fires, (b) when the image is actually fully loaded by the application, and (c) when the the animation is triggered. Right now my guess is that (a) is triggering (c) before (b) has actually occurred. A cursory Google search for “WPF image load performance” seems to support my theory, but at this point I really don’t know for sure. Once I do, I’ll follow up with another post. Alas, for now, I have a workaround. Adding BeginTime="0:0:1" to the animation seems to give the application a sufficient buffer to zoom large images without lag. It’s not a very satisfying solution, though for now it’s good enough.