ZoomableCanvas

ZoomableCanvas is the Panel that displays almost all of the elements in Code Canvas.  Code Canvas is actually made up of several layers of ZoomableCanvases that are stacked on top of each other, and they are synchronized by simply sharing the same values for Scale and Offset.  Scale and Offset are the driving forces behind the ZoomableCanvas, because they are what control the location (and sometimes the size) of the elements when they are shown on the screen.

The reason that Scale only sometimes controls the size of the elements on screen is because the ZoomableCanvas has a bool property called ApplyTransform, and if ApplyTransform = false then the ZoomableCanvas will not affect the size of elements at all.  ApplyTransform is set to true by default, which means you get scaling automatically for free, but sometimes you may wish to turn it off so you can do custom effects instead, such as semantic zoom.  When ApplyTransform = false, the Scale will still affect the position of the elements (i.e. they get closer together as you zoom out, and farther apart as you zoom in), but changing the size of the elements is up to you.  If you forget to change the size, then you’ll end up with your elements appearing to “float above” the canvas, and in fact the search results and definition labels in Code Canvas do exactly that on purpose.  Check out the various Code Canvas videos to see that effect in action.

ZoomableCanvas is similar to Chris Lovett’s VirtualCanvas, but it has some important differences.  The main difference is that ZoomableCanvas is meant to feel like a built-in WPF component, just like the default Canvas, and so it supports the three most common methods of populating a Panel in WPF:

1. Adding children directly to the canvas, either via the Children property or via XAML.
2. Adding children with the Items property of a parent ItemsControl (like a ListBox).
3. Binding to a data source with the ItemsSource property of a parent ItemsControl.

In the first case, you can use it exactly how you’d use a normal Canvas:

     <ZoomableCanvas>
        <Rectangle Canvas.Top="10" Canvas.Left="20"
                   Width="30" Height="40" Fill="LightBlue"/>

        <Button    Canvas.Top="10" Canvas.Left="60"
                   Content="Hello, ZoomableCanvas!"/>

        <Calendar  Canvas.Top="40" Canvas.Left="60"
                   DisplayDate="6/18/2008"/>
    </ZoomableCanvas>

In the second case, you can also use it in exactly the same way that you would if you were to use a normal Canvas within an ItemsControl:

     <ListBox>
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <ZoomableCanvas/>
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>

        <ListBox.ItemContainerStyle>
            <Style>
                <Setter Property="Canvas.Top"
                        Value="{Binding Path=Content.(Canvas.Top),
                                RelativeSource={RelativeSource Self}}"/>
                
                <Setter Property="Canvas.Left"
                        Value="{Binding Path=Content.(Canvas.Left),
                                RelativeSource={RelativeSource Self}}"/>
            </Style>
        </ListBox.ItemContainerStyle>

        <ListBox.Items>
            <Rectangle Canvas.Top="10" Canvas.Left="20"
                       Width="30" Height="40" Fill="LightBlue"/>

            <Button    Canvas.Top="10" Canvas.Left="60"
                       Content="Hello, ZoomableCanvas!"/>

            <Calendar  Canvas.Top="40" Canvas.Left="60"
                       DisplayDate="6/18/2008"/>
        </ListBox.Items>
    </ListBox>

 

The fact that you have to set <ListBox.ItemContainerStyle> is a bit ugly, but it’s not something caused by ZoomableCanvas.  Since ItemsControls always wrap their items in ContentPresenters, you end up with an extra level of indirection between the item and the Panel, so you have to use a <Style> on the ContentPresenter to make the connection.  This is a well-known issue for anyone who has ever tried to use a Grid or a DockPanel in a ListBox, for example.

The third case is probably the most likely, and it’s basically the same as the last one except that you set ItemsSource="{Binding ...}" instead of specifying the <ListBox.Items> directly.  This is also the only way to get data virtualization.

Speaking of virtualization, ZoomableCanvas supports both kinds of virtualization: UI virtualization and data virtualization.  UI virtualization is when you have lots of data items loaded into memory (plain .NET objects, not Visuals), but you only create the Visuals for those items that can be seen on the screen.  Data virtualization is when you don’t even load all of the data items into memory; you only load a subset of the data, but you “pretend” that the full data set is there.  You can even have both kinds of virtualization going on at once.  For example, imagine you’re creating an application that draws a social networking graph of all of the users on the Internet.  Your back-end database would likely have millions (or billions) of nodes and edges, but someone using a 24” monitor would realistically only be able to see a few thousand at once.  Your users probably don’t have enough RAM to load the entire dataset into memory, but they probably don’t have enough bandwidth to issue thousands of queries on-the-fly as they pan and zoom around.  So you can use data virtualization to fetch and cache only some moderate portion of the full data set (e.g. a couple hundred thousand nodes around the neighborhood they’re viewing), and use UI virtualization to only create Visuals for the few hundreds of nodes that are actually on their screen.

ZoomableCanvas provides some default UI virtualization for free out of the box by using VirtualPanel and PriorityQuadTree, both posted previously on my blog.  You can also improve the performance even further by having your data source implement ZoomableCanvas.ISpatialItemsSource.  When the value you gave to ItemsSource implements ISpatialItemsSource, the ZoomableCanvas will call the Query method instead of enumerating over the whole list.  The Query method simply takes the current viewbox as a parameter and returns the indexes of the items that fall within that viewbox.  This is also known as a spatial index, and it’s what you can use to implement data virtualization as well.  If you don’t implement ISpatialItemsSource, then the ZoomableCanvas will build its own spatial index internally using the PriorityQuadTree, but it will have to enumerate the whole data list and create each Visual one time to do it.  The Visuals that are off-screen will quickly be re-virtualized, but the ZoomableCanvas needs to create them temporarily so that it can inspect the Canvas.Top and Canvas.Left properties to see where the item needs to go.  By having your data source implement ISpatialItemsSource, you can completely bypass this step.

A final thing to keep in mind is that, although ZoomableCanvas provides easy-to-use properties like Offset, Scale, and even Viewbox, Stretch, and StretchDirection, it does not provide any default keyboard or mouse gesture handling.  The ZoomableCanvas is meant to be like any other built-in WPF component, and it’s expected that each application will be responsible for handling their own user input differently.  For example, the built-in Canvas doesn’t provide mouse handling to let users drag items around on the canvas, even though that’s what most people do with it.  In the same vein, ZoomableCanvas doesn’t provide any mouse handling for dragging items or panning or zooming.  But I will be posting a blog series on the various cool things you can do with ZoomableCanvas, and you will quickly see that adding these things is a piece of cake.

I’ve attached the full source code for ZoomableCanvas to this post, but it relies on the VirtualPanel, PriorityQuadTree, MathExtensions, and LinkedListExtensions from the other posts on my blog.  Once you’ve downloaded those files and put them all in a class library, you can throw in some XmlnsDefinitions and you’ll have a ZoomableCanvas that looks like it came straight from WPF!  Enjoy!

ZoomableCanvas.cs