The official Fatica Labs Blog! RSS 2.0
# Monday, 21 March 2011

This is an example on implementing a behavior for zooming a 2D representations in WPF in a CAD friendly fashion, in the sense we need some facilities to have the same line thickness at different zoom level as well as text size. This because in 2D cad application we usually need to zoom into details but line thickness are actually zero width lines so they must not appear thicker as we look near to them, and even text sometimes need the same strategy. The same as we zoom “far” from the drawing, line thickness should appear the same. In this sample we focalize just a first zoom mode, the one handling the mouse wheel, that is simple because it does not require much user interaction handling ( in comparison to a “zoom redct” function ), but as in a real 2D cad application, we want a real zoom at point.

We implement this function as a behavior, so zooming seems “a thing attached” in XAML. An attached behavior, in WPF term is, as a per John Gossman definition:

The Attached Behavior pattern encapsulates "behavior" (usually user interactivity) into a class outside the visual heirarchy and allows it to be applied to a visual element by setting an attached property and hooking various events on the visual element.

Designing a behavior can start by the XAML, so we start by writing the XAML we expect to use in the way we like, as for example:

<Canvas z:Zoomable.Mode="Wheel" Background="White" SnapsToDevicePixels="True" x:Name="topCanvas">

 

The intent of this XAML is ( hopefully ) clear: we want to handle the zoom functionality on the Canvas in the mode “Wheel”. The underlined portion show where we attach the behavior. I did underline the Background assignment too: this is not exactly related to this behavior, but in general to get the canvas firing the mouse wheel events ( and any other mouse events ) it is necessary to specify a non transparent background, and these events are necessary to our behavior to work.

Implementing a behavior is done by using an attached property, and binding to the UIElement that property is applied to an event handler. In our implementation we use a class called ZoomHandler to handle the events:

public static readonly DependencyProperty ModeProperty =
           DependencyProperty.RegisterAttached("Mode", typeof(ZoomMode)
, typeof(Zoomable)
, new UIPropertyMetadata(ZoomMode.Disabled, new PropertyChangedCallback(OnModeChanged)));

       protected static void OnModeChanged(DependencyObject depo, DependencyPropertyChangedEventArgs depa)
       {
           Panel panel = depo as Panel;
           
           if (null == panel)
               throw new Exception("ZoomBehavior can only be attached to  objects derived from Panel");
           if (!attachedHandlers.ContainsKey(panel))
           {
               attachedHandlers[panel] = new ZoomHandler(panel);
           }
           attachedHandlers[panel].SetMode((ZoomMode)depa.NewValue);
       }

We attach a ZoomHandler class instance for each panel we want to zoom-enable, and we keep collected these object in an internal list. We define another attached property called UnityThickness. This is an important property in implementing CAD like behaviors: it is bound to the value of thickness of a line that appear to be one pixel thick at the selected zoom level. This property value is valuated from the ZoomHandler class, at the same time we apply the scaling factor, here below the function:

void canvas_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
       {
           if (currentMode == ZoomMode.Wheel || currentMode == ZoomMode.WheelAndMouse)
           {
               var reference = panel.Parent as IInputElement;
               if (null == reference)
                   reference = panel;
               Point pt = e.MouseDevice.GetPosition(reference);
               Transform mt = panel.RenderTransform;
               Matrix m = mt.Value;
               double amount = 1.0 + Math.Sign(e.Delta) * .1;
              
               m.ScaleAt(amount, amount, pt.X, pt.Y);
               currentAmount = m.M11;
               panel.RenderTransform= new MatrixTransform(m);
               Zoomable.SetUnityThickness(panel, 1.0 / currentAmount);
           }
       }

This function is the real one doing the zoom work. A scaling matrix is computed, to scale at an origin point defined by the current mouse coordinates. Then the attached UnityThickness property is set on the panel this ZoomHandler is handling. The scale amount is extracted form the first diagonal value of the RenderMatrix, by doing this we suppose we are using uniform scaling in zoom. Since we probably need to draw geometry with different thickness, we need some helper to bound the UnityThickness with different factors. The same can be done for font sizes. In order to achieve this, an helper class called ScaledThicknessSource is created: whith this class we can provide a scaled thickness by the current scale factor, here we can write:

  <Canvas z:Zoomable.Mode="Wheel" Background="White" SnapsToDevicePixels="True" x:Name="topCanvas">
            <z:ScaledThicknessSource  Thickness="1.0"  x:Name="Thickness1" UnityThickness="{Binding Path=(z:Zoomable.UnityThickness), RelativeSource={RelativeSource AncestorType=Canvas}}"  />
            <z:ScaledThicknessSource  Thickness="3.0"  x:Name="Thickness3" UnityThickness="{Binding Path=(z:Zoomable.UnityThickness), RelativeSource={RelativeSource AncestorType=Canvas}}"  />
            <z:ScaledThicknessSource   Thickness="22.0"  x:Name="FontSize12" UnityThickness="{Binding Path=(z:Zoomable.UnityThickness), RelativeSource={RelativeSource AncestorType=Canvas}}"  />
            <TextBlock Canvas.Left="20"  Canvas.Top="40" Foreground="Goldenrod" FontSize="{Binding Path=ActualThickness,ElementName=FontSize12}" >Non scaling text</TextBlock>
            <TextBlock Canvas.Left="240"  Canvas.Top="40" Foreground="Goldenrod" FontSize="22" >Scaling text</TextBlock>
            <Rectangle Fill="RosyBrown" StrokeThickness="2" Canvas.Left="60" Canvas.Top="80" Stroke="DeepSkyBlue" Width="100" Height="100"/>
            <Ellipse Fill="Brown" StrokeThickness="{Binding Path=ActualThickness,ElementName=Thickness1}" Canvas.Left="160" Canvas.Top="80" Stroke="Blue" Width="100" Height="100"/>
            <Path Canvas.Left="20" Canvas.Top="200" Stroke="BurlyWood" Data="M 45,0 L 35,30 L 0,30 L 30,55 L 20,85 L 45,60 L 70,85 L 60,55 L 90,30 L 55,30 L 45,0"/>
            <Path Canvas.Left="120" StrokeThickness="{Binding Path=ActualThickness,ElementName=Thickness3}" Canvas.Top="200" Stroke="DarkCyan" Data="M 45,0 L 35,30 L 0,30 L 30,55 L 20,85 L 45,60 L 70,85 L 60,55 L 90,30 L 55,30 L 45,0"/>
            <Image Canvas.Left="280" Canvas.Top="80" Source="/Assets/leo-mini.png" Width="128" Height="128"></Image>
        </Canvas>

In the example above, we create some instances of ScaledThickessSource, and we bound the property UnityThickess to the UnityThickness of the Zoomable behavior. Please note the syntax used:the () is necessary for an attached property. The thickness value is the desired one, at any zoom level. Then the examples geometry and text are bound tho the ActualThickness property of the helper instance, in order to have the proper scaled value.

Now we can look some examples of the behavior at work:

image The canvas with some example element. A test mantaining the dimension at any zoom level, a text affected by the zoom. The square has a standard thick border, the circle a correted thick border. The left star is with a standard border, and the rightr one has a fix tick border of two pixel. The image is added to show the zoom behavior working on raster images too.
image Here we show how the test is kept at each zoom level.
image Zooming in. The circle border thickness is kept, the square not.
image The same for the star. Notice as the left one appear to be bigger when we zoom in, this is because the left star border is not scaled.
image Zoom in works on Raster images…
image And zoom out too.

Another little thing, even if not necessary, the anti aliasing effect can blur a little the lines drawing, we can disable it in the following way:

 <Canvas z:Zoomable.Mode="Wheel" Background="White" SnapsToDevicePixels="True" x:Name="topCanvas">

Panning is not implemented yet. I would probably enable the ScrollViewer able to handle the pan, at present a standard ScrollViewer does simply not work since canvas does not measures the contained children total occupied size.

Monday, 21 March 2011 21:01:30 (GMT Standard Time, UTC+00:00)  #    Comments [3] - Trackback
Programmin | WPF

My Stack Overflow
Contacts

Send mail to the author(s) E-mail

Tags
profile for Felice Pollano at Stack Overflow, Q&A for professional and enthusiast programmers
About the author/Disclaimer

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

© Copyright 2019
Felice Pollano
Sign In
Statistics
Total Posts: 157
This Year: 0
This Month: 0
This Week: 0
Comments: 127
This blog visits
All Content © 2019, Felice Pollano
DasBlog theme 'Business' created by Christoph De Baene (delarou) and modified by Felice Pollano