Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Disable Viewbox Resize for Specific Elements

0.00/5 (No votes)
30 Jun 2016 1  
How to make items inside a Viewbox not being scaled by it

Introduction

In this tip, we are going to see how we can make a specific item contained in a viewbox not being scaled automatically by it. This is useful if we want to have an element with a specific size that is inside a Viewbox. For example, some type of marker in a resizable image.

Background

In this article, we can see an easy way to accomplish this to apply the invert of the Transform that the viewbox is applying to his child item. This can be done from XAML directly but we need to add this code for each XAML element.

<Button.LayoutTransform>
    <MultiBinding Converter="{bc:ExemptFromViewboxTransformConverter}">
       <Binding Source="{x:Reference Name=viewboxName}" />
       <!-- The ActualWidth/ActualHeight bindings are only used to ensure 
            the transform updates when the window is resized. -->
        <Binding Source="{x:Reference Name=viewboxName}" Path="ActualWidth" />
        <Binding Source="{x:Reference Name=viewboxName}" Path="ActualHeight" />
    </MultiBinding>
</Button.LayoutTransform>

And we need to create a Converter that implements the following code:

((ContainerVisual)VisualTreeHelper.GetChild((DependencyObject)viewbox, 0)).Transform.Inverse

This is not hard to do but it's not easy either and the resulting XAML can be very ugly.

Using the Code

In order to accomplish this, we have created a new class that will hold the new attached properties for the Viewbox that will do the trick.

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace ViewBoxPrototype
{
    public class ViewBoxExtra
    {
        /// <summary>
        /// Setter for the DisableScaling attached property
        /// </summary>
        /// <param name="element"></param>
        /// <param name="value"></param>
        public static void SetDisableScaling(DependencyObject element, bool value)
        {
            element.SetValue(DisableScalingProperty, value);
        }

        /// <summary>
        /// Getter for the DisableScaling attached property
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        public static bool GetDisableScaling(DependencyObject element)
        {
            return (bool)element.GetValue(DisableScalingProperty);
        }

        /// <summary>
        /// This property is the one that we will set in the XAML elements 
        /// in order to indicate that we don't want that item to resize
        /// </summary>
        public static readonly DependencyProperty DisableScalingProperty = 
         DependencyProperty.RegisterAttached("DisableScaling",
                 typeof(bool), typeof(Viewbox), new UIPropertyMetadata(AddScalingHandler));

        /// <summary>
        /// Setter for the NonScaledObjects attached property
        /// </summary>
        /// <param name="element"></param>
        /// <param name="value"></param>
        public static void SetNonScaledObjectsProperty
         (DependencyObject element, List<DependencyObject> value)
        {
            element.SetValue(NonScaledObjectsProperty, value);
        }

        /// <summary>
        /// Getter for the NonScaledObjects attached property
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        public static List<DependencyObject> 
         GetNonScaledObjectsProperty(DependencyObject element)
        {
            return (List<DependencyObject>)element.GetValue(NonScaledObjectsProperty);
        }

        /// <summary>
        /// This property is the one that will hold the references 
        /// to the objects that will not be resized for that viewbox
        /// </summary>
        public static readonly DependencyProperty NonScaledObjectsProperty = 
               DependencyProperty.RegisterAttached("NonScaledObjects",
                 typeof(List<DependencyObject>), typeof(Viewbox), null);

        /// <summary>
        /// Handler used to prepare the NonScaleObjects list and 
        /// to link the size changed event of the viewbox to our handler
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void AddScalingHandler
          (DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            try
            {
                var viewBox = ObtainParentViewbox(d);
                var nonScaleObjects = 
                (List<DependencyObject>)viewBox.GetValue(NonScaledObjectsProperty);
                if ((bool)e.NewValue && nonScaleObjects == null)
                {
                    nonScaleObjects = new List<DependencyObject>();
                    viewBox.SetValue(NonScaledObjectsProperty, nonScaleObjects);
                    viewBox.SizeChanged += ViewBox_SizeChanged;
                }

                if ((bool)e.NewValue)
                {
                    nonScaleObjects.Add(d);
                }
                else
                {
                    nonScaleObjects.Remove(d);
                }
            }
            catch (NullReferenceException exc)
            {
                throw new Exception
                ("The element must be contained inside an ViewBoxExtra", exc);
            }
        }

        /// <summary>
        /// This will be called when a viewBox has any item with the property DisableScaling=true
        /// The main process to invert the object is done here.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void ViewBox_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            var viewBox = sender as Viewbox;
            var transform = 
            ((ContainerVisual)VisualTreeHelper.GetChild(sender as DependencyObject, 0)).Transform;
            if (transform != null && viewBox != null)
            {
                foreach (var nonScaleObject in 
                (List<DependencyObject>)viewBox.GetValue(NonScaledObjectsProperty))
                {
                    var element = (FrameworkElement)nonScaleObject;
                    element.LayoutTransform = (Transform)transform.Inverse;
                }
            }
        }

        /// <summary>
        /// Method in order to obtain the viewbox that contains the item
        /// </summary>
        /// <param name="d"></param>
        /// <returns></returns>
        private static Viewbox ObtainParentViewbox(DependencyObject d)
        {
            var parent = VisualTreeHelper.GetParent(d);
            return parent is Viewbox ? parent as Viewbox : ObtainParentViewbox(parent);
        }
    }
}

We just need to add this class to the project or the content in any class that you create in your project.

In order to use it from the XAML, we just need to add ViewBoxExtra.DisableScaling="true".

<Viewbox Grid.Row="1">
            <Canvas  Width="1024" Height="334">
                <Rectangle Width="50" Height="50" 
                Canvas.Left="10" Canvas.Top="10" 
                Stroke="Red" local:ViewBoxExtra.DisableScaling="true"/>
                <Rectangle Width="50" Height="50" 
                Canvas.Left="100" Canvas.Top="10" 
                Stroke="Green"/>
                <Rectangle Width="50" Height="50" 
                Canvas.Left="200" Canvas.Top="10" 
                Stroke="Yellow" local:ViewBoxExtra.DisableScaling="true"/>
                <Rectangle Width="50" Height="50" 
                Canvas.Left="300" Canvas.Top="10" 
                Stroke="Blue" local:ViewBoxExtra.DisableScaling="true"/>
            </Canvas>
        </Viewbox>

This is it!

The XAML file is clean and understandable and we have the desired effect.

Flow of Execution

  1. We set the DisableScaling property to true for an XAML element.
  2. The AddScalingHandler is called for each element.
  3. When a resize is done to the viewbox, the ViewBox_SizeChanged is invoked.
  4. For each element inside the NonScaledObjects, we apply the inverse matrix to the transform in order to draw the element with his correct size.

Points of Interest

I wanted to do this because I wanted to get the final result but without soiling the XAML file and making it ugly. A clean XAML is something that we aspire but it is very hard to obtain.

This was my first article, so if there is something to improve on, please comment on it and I will gladly provide an answer.

History

  • 06/30/2016: First version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here