Mouse Wheel Zooming UIElements via Behaviors

I got an email the other day from Lab49’s MVP asking how I would easily zoom a picture using the mouse wheel in WPF. Since I don’t feel like arbitrarily restricting myself to only zooming images, I’ll show you how to use a behavior to apply this functionality to any UIElement.

First, I’ll show you how I have traditionally implemented this kind of functionality: Attached Behaviors. This is a concept that has been discussed extensively in the WPF community, so I’ll just show the interesting bits. I’ve created a Zoom static class with a couple of public attached properties: MinimumScale, MaximumScale, EnableMouseWheel. When the EnableMouseWheel property changes, we either attach or detach the PreviewMouseWheel event handler on the UIElement (depending on the new value). Then, when the event is raised, we actually change the RenderTransform of the element to be zoomed:

static void OnEnableMouseWheelChanged(DependencyObject d,  
    DependencyPropertyChangedEventArgs e)  
{
    UIElement elem = d as UIElement;
    if (elem == null)
        throw new NotSupportedException(
            "Can only set the Zoom.EnableMouseWheel attached behavior on a UIElement.");

    if ((bool)e.NewValue)
        elem.PreviewMouseWheel += OnMouseWheel;
    else
        elem.PreviewMouseWheel -= OnMouseWheel;
}

static void OnMouseWheel(object sender, MouseWheelEventArgs e)  
{
    SetZoom(sender as UIElement, e.Delta / 1000.0);
}

private static void SetZoom(UIElement elem, double delta)  
{
    if (GetEnableMouseWheel(elem))
    {
        EnsureRenderTransform(elem);
        ScaleTransform xform = (ScaleTransform)elem.RenderTransform;

        double toSet = (double)elem.GetValue(ScaleFactorProperty);
        elem.SetValue(ScaleFactorProperty, toSet + delta);
        toSet = Math.Pow(Math.E,
            (double)elem.GetValue(ScaleFactorProperty));

        xform.ScaleX =
            xform.ScaleY = toSet;
    }
}

private static void EnsureRenderTransform(UIElement elem)  
{
    if (elem.RenderTransform == null ||
        !(elem.RenderTransform is ScaleTransform))
    {
        elem.RenderTransform = new ScaleTransform(1, 1);
    }
}

I’m using an exponential function to provide a more natural zooming experience. Since zooming out will eventually cause scales < 1, an exponential function causes these changes to appear more linear. Likewise when zooming in very far, a linear function would appear to slow down the rate of zooming.

To use the attached behavior, you would do the following:

<Image Source="flatiron.jpg"  
       RenderTransformOrigin=".5,.5"   
       lcl:Zoom.EnableMouseWheel="True" />

Having just started using Blend 3, I decided to turn this Attached Behavior into a Blend Behavior. This was fairly trivial, and was basically turning the attached properties into regular dependency properties. Also, instead of having an EnableMouseWheel property, I had to override a couple of methods in the behavior:

protected override void OnAttached()  
{
    base.OnAttached();

    AssociatedObject.PreviewMouseWheel += OnMouseWheel;
}

protected override void OnDetaching()  
{
    base.OnDetaching();

    AssociatedObject.PreviewMouseWheel -= OnMouseWheel;
}

The AssociatedObject is strongly typed to be a UIElement, since this Behavior derives from System.Windows.Interactivity.Behavior<UIElement>. This is used in a slightly more verbose way in XAML, but it is far easier to apply in blend:

<Image Source="flatiron.jpg" RenderTransformOrigin=".5,.5">  
    <i:Interaction.Behaviors>
        <lcl:ZoomingBehavior MaximumScale="100"/>
    </i:Interaction.Behaviors>
</Image>  

There you go, an easy way of adding mouse wheel zooming to an element in WPF. I am not entirely happy with using a behavior to modify the render transform of an element, so maybe in a future post I’ll create a custom control that can be used to wrap content in a zoom-able container.

Here is the source code including both the attached behavior and the blend behavior: ZoomBehavior.zip

-AH


View or Post Comments