UI Frontiers - Thinking Outside the Grid
By Charles Petzold | May 2010
The Canvas is one of several layout options available in Windows Presentation Foundation (WPF) and Silverlight, and it’s the one most firmly rooted in tradition. When filling the Canvas with children, you position each child by specifying coordinates using the Canvas.Left and Canvas.Top attached properties. This is quite a different paradigm from the other panels, which arrange child elements based on simple algorithms without any need for the programmer to figure out the actual locations.
When you hear the word “canvas,” you probably think about painting and drawing. For that reason, perhaps, programmers using WPF and Silverlight tend to relegate the Canvas to the display of vector graphics. Yet, when you use the Canvas to display Line, Polyline, Polygon and Path elements, the elements themselves include coordinate points that position them within the Canvas. As a result, you don’t need to bother with the Canvas.Left and Canvas.Top attached properties.
So why use a Canvas if you don’t need the attached properties it provides? Is there a better approach?
Canvas vs. Grid
Over the years, I have increasingly tended to reject the Canvas for displaying vector graphics, gravitating instead toward the use of a single-cell Grid. A single-cell Grid is just like a regular Grid except without any row or column definitions. If the Grid has only one cell, you can put multiple elements into the Grid cell and you don’t use any of the Grid’s attached properties to indicate rows or columns.
Initially, using a Canvas or a single-cell Grid seems very similar. Regardless which one you use for vector graphics, Line, Polyline, Polygon and Path elements will be positioned relative to the upper-left corner of the container based on their coordinate points.
The difference between the Canvas and the single-cell Grid is in how the container appears to the rest of the layout system. WPF and Silverlight incorporate a two-pass, top-down layout where every element interrogates the size of its children and is then responsible for arranging its children relative to itself. Within this layout system, the Canvas and the single-cell Grid are very different:
- To its children, the Grid has the same dimensions as the dimensions of its own parent. These are usually finite dimensions, but the Canvas always appears to have infinite dimensions to its children.
- The Grid reports the composite size of its children to its parent. However, the Canvas always has an apparent size of zero, regardless of the children it contains.
Suppose you have a bunch of Polygon elements that form some kind of cartoon-like vector graphics image. If you put all these Polygon elements in a single-cell Grid, the size of the Grid is based on the maximum horizontal and vertical coordinates of the polygons. The Grid can then be treated as a normal finite-sized element within the layout system because its size properly reflects the size of the composite image. (Actually, this works correctly only if the upper-left corner of the image is at the point (0, 0), and there are no negative coordinates.)
Put all those polygons in a Canvas, however, and the Canvas reports to the layout system that it has a size of zero. In general, when integrating a composite vector graphics image into your application, you almost certainly want the behavior of the single-cell Grid rather than the Canvas.
So is the Canvas entirely useless? Not at all. The trick is to use the peculiarities of the Canvas to your advantage. In a very real sense, the Canvas doesn’t participate in layout. Hence, you can use it whenever you need to transcend layout—to display graphics that break the bounds of the layout system and float outside it. By default the Canvas doesn’t clip its children, so even if it is very small, it can still host children outside its bounds. The Canvas is more of a reference point for displaying elements or graphics than it is a container.
The Canvas is great for techniques I have come to regard as “thinking outside the Grid.” Although I’ll be showing the code examples in Silverlight, you can use the same techniques in WPF. The downloadable source code that accompanies this article is a Visual Studio solution named ThinkingOutsideTheGrid, and you can play with the programs at http://charlespetzold.com/.
Visual Linking of Controls
Suppose you have a bunch of controls in your Silverlight or WPF application and you need to provide some kind of visual link between two or more controls. Perhaps you want to draw a line from one control to another, and perhaps this line will cross other controls in between.
Certainly this line must react to changes in layout, perhaps as the window or page is resized by the user. Being informed when a layout is updated is an excellent application of the LayoutUpdated event—an event I never had occasion to use before exploring the problems I describe in this article. LayoutUpdated is defined by UIElement in WPF and by FrameworkElement in Silverlight. As the name suggests, the event is fired after a layout pass has rearranged elements on the screen.
When processing the LayoutUpdated event, you don’t want to do anything that will cause another layout pass and get you embroiled in an infinite recursion. That’s where the Canvas comes in handy: Because it always reports a zero size to its parent, you can alter elements in the Canvas without affecting layout.
The XAML file of the ConnectTheElements program is structured like this:
<UserControl ... > <Grid ... > <local:SimpleUniformGrid ... > <Button ... /> <Button ... /> ... </local:SimpleUniformGrid> <Canvas> <Path ... /> <Path ... /> <Path ... /> </Canvas> </Grid> </UserControl>
The Grid contains a SimpleUniformGrid that calculates the number of rows and columns to display its children based on its overall size and aspect ratio. As you change the size of the window, the number of rows and columns will change and cells will shift around. Of the 32 buttons in this SimpleUniformGrid, two of the buttons have names of btnA and btnB. The Canvas occupies the same area as the SimpleUniformGrid, but sits on top of it. This Canvas contains Path elements that the program uses to draw ellipses around the two named buttons and a line between them.
The code-behind file does all its work during the LayoutUpdated event. It needs to find the location of the two named buttons relative to the Canvas, which conveniently is also aligned with the SimpleUniformGrid, the Grid and MainPage itself.
To find a location of any element relative to any other element in the same visual tree, use the TransformToVisual method. This method is defined by the Visual class in WPF and by UIElement in Silverlight, but it works the same way in both environments. Suppose the element el1 is somewhere within the area occupied by el2. (In ConnectTheElements, el1 is a Button and el2 is MainPage.) This method call returns an object of type GeneralTransform, which is the abstract parent class to all the other graphics transform classes:
You can’t really do anything with GeneralTransform except call its Transform method, which transforms a point from one coordinate space to another.
Suppose you want to find the center of el1 but in el2’s coordinate space. Here’s the code:
Point el1Center = new Point( el1.ActualWidth / 2, el1.ActualHeight / 2); Point centerInEl2 = el1.TransformToVisual(el2).Transform(el1Center);
If el2 is either the Canvas or aligned with the Canvas, you can then use that centerInEl2 point to set a graphic in the Canvas that will seemingly be positioned in the center of el1.
ConnectTheElements performs this transform in its WrapEllipseAroundElement method to draw ellipses around the two named buttons, and then calculates the coordinates of the line between the ellipses, based on an intersection of the line between the centers of the buttons. Figure 1 shows the result.
Figure 1 The ConnectTheElements Display
If you try this program in WPF, change the SimpleUniformGrid to a WrapPanel for a more dynamic change in layout as you resize the program’s window.
Changing graphics and other visuals in response to changes in a scrollbar or slider is very basic, and in WPF and Silverlight you can do it in either code or a XAML binding. But what if you want to align the graphics exactly with the actual slider thumb?
This is the idea behind the TriangleAngles project, which I conceived as a type of interactive trigonometry demonstration. I arranged two sliders, one vertical and one horizontal, at right angles to each other. The two slider thumbs define the two vertices of a right triangle, as shown in Figure 2.
Figure 2 The TriangleAngles Display
Notice how the semi-transparent triangle sits on top of the two sliders. As you move the slider thumbs, the sides of the triangle change size and proportion, as indicated by the inscribed angles and the labels on the vertical and horizontal legs.
This is obviously another job for a Canvas overlay, but with an added layer of complexity because the program needs to get access to the slider thumb. That slider thumb is part of a control template: the thumbs are assigned names within the template, but unfortunately these names can’t be accessed outside the template.
Instead, the frequently essential VisualTreeHelper static class comes to the rescue. This class lets you walk (or rather climb) any visual tree in WPF or Silverlight through the GetParent, GetChildenCount and GetChild methods. To generalize the process of locating a specific type child, I wrote a little recursive generic method:
T FindChild<T>(DependencyObject parent) where T : DependencyObject
I call it like this:
Thumb vertThumb = FindChild<Thumb>(vertSlider); Thumb horzThumb = FindChild<Thumb>(horzSlider);
At that point, I could use TransformToVisual on the two thumbs to obtain their coordinates relative to the Canvas overlay.
Well, it worked for one slider, but not the other, and it took me awhile to recall that the control template for the Slider contains two thumbs—one for the horizontal orientation and one for the vertical. Depending on the orientation set for the Slider, half the template has its Visibility property set to Collapsed. I added a second argument to the FindChild method called mustBeVisible and used that to abandon the search down any child branch where an element is not visible.
Setting HitTestVisible to false on the Polygon that forms the triangle helped prevent it from interfering with mouse input to the Slider thumb.
Scrolling Outside the ItemsControl
Suppose you’re using an ItemsControl or a ListBox with a DataTemplate to display the objects in the control’s collection. Can you include a Canvas in that DataTemplate so information concerning a particular item can be displayed outside the control, but seems to track the item as the control is scrolled?
I haven’t found a good way to do precisely that. The big problem seems to be a clipping region imposed by the ScrollViewer. This ScrollViewer clips any Canvas that happens to dangle outside its boundary, and consequently anything on that Canvas.
However, with a little additional knowledge of the ItemsControl inner workings, you can do something close to what you want.
I think of this feature as a pop-out in that it’s something that pertains to an item in an ItemsControl, but is actually popped out of the ItemsControl itself. The ItemsControlPopouts project demonstrates the technique. To provide something for the ItemsControl to display, I created a little database called ProduceItems.xml that resides in the Data subdirectory of ClientBin. ProduceItems consists of a number of elements with the tag name of ProduceItem, each of which contains a Name attribute, a Photo attribute referencing a bitmap picture of the item and an optional Message, which will be displayed “popped-out” of the ItemsControl. (The photos and other artwork are Microsoft Office clip art.)
The ProduceItem and ProduceItems classes provide code support for the XML file, and ProduceItemsPresenter reads the XML file and deserializes it into a ProduceItems object. This is set to the DataContext property of the visual tree that contains the ScrollViewer and ItemsControl. The ItemsControl contains a simple DataTemplate for displaying the items.
By now you may detect a bit of a problem. The program is effectively inserting business objects of type ProduceItem into the ItemsControl. Internally the ItemsControl is building a visual tree for each item based on the DataTemplate. To track the movement of these items you need access to that item’s internal visual tree to figure out where exactly the items are relative to the rest of the program.
This information is available. ItemsControl defines a get-only property named ItemContainerGenerator that returns an object of type ItemContainerGenerator. This is the class responsible for generating the visual trees associated with each item in the ItemsControl, and it contains handy methods such as ContainerFromItem, which provides the container (which is actually a ContentPresenter) for each object in the control.
Like the two other programs, the ItemsControlPopouts program covers the whole page with a Canvas. Once again the LayoutUpdated event allows the program to check whether something on the Canvas needs to be altered. The LayoutUpdated handler in this program enumerates through the ProduceItem objects in the ItemsControl and checks for a non-null and non-empty Message property. Each of these Message properties should correspond to an object of type PopOut in the Canvas. The PopOut is simply a small class that derives from ContentControl with a template to display a line and the message text. If the PopOut is not present, it’s created and added to the Canvas. If it is present, it’s simply reused.
The PopOut then must be positioned within the Canvas. The program obtains the container that corresponds to the data object and transforms its location relative to the canvas. If that location is between the top and bottom of the ScrollViewer, the PopOut has its Visibility property set to Visible. Otherwise the PopOut is hidden.
Breaking out of the Cell
WPF and Silverlight have certainly given the great gift of ease in layout. The Grid and other panels put elements neatly in cells and ensure that’s where they stay. It would be a shame if you then assumed that convenience was a necessary limitation to the freedom to put elements wherever you want them.
Charles Petzold is a longtime contributing editor to MSDN Magazine. His most recent book is “The Annotated Turing: A Guided Tour Through Alan Turing’s Historic Paper on Computability and the Turing Machine” (Wiley, 2008). Petzold blogs on his Web site charlespetzold.com.
Thanks to the following technical experts for reviewing this article: Arathi Ramani and the WPF Layout Team