Layout is a critical component of an application’s usability on a wide range of devices, but without good platform support, getting it right can be extremely difficult. Arranging the pieces of a user interface simply with static pixel-based coordinates and static pixel-based sizes can work in limited environments, but these types of interfaces start to crumble under the influence of many varying factors: different screen resolutions and dimensions, user settings such as font sizes, or content that changes in unpredictable ways (such as text being translated into different languages). Plus, applications that don’t allow users to resize various pieces of them (and take advantage of the extra space intelligently) frustrate most users.
On my 1024×600 netbook screen, Outlook 2010 adapts nicely, but many programs, such as Visual Studio 2010, do not fare so well. If I change the screen to portrait mode (600×1024), Outlook 2010 does an admirable job of using the space intelligently, but the experience of other programs (such as Visual Studio 2010) gets far worse. (This is especially ironic because Visual Studio is at least partially a WPF application, whereas Outlook does not use WPF. However, this specific outcome is not really a result of the technologies being used, but rather the priority that the teams placed on handling small or unusual screen sizes.)
WPF contains built-in panels that can make it easy to avoid layout pitfalls. This chapter begins by examining the five main built-in panels, all in the System.Windows.Controls
namespace, in increasing order of complexity (and general usefulness):
• Canvas
• Grid
For completeness, this chapter also looks at a few rarely used “primitive panels.” Then, after a section on content overflow (which happens when parents and children can’t agree on the use of available space), this chapter ends with a large example. This example applies a variety of layout techniques to make a relatively sophisticated user interface found in applications such as Visual Studio that would be hard to construct without the help of WPF’s layout features.
Canvas
Canvas
is the most basic panel. It’s so basic, in fact, that you probably should never bother using it for arranging typical user interfaces. Canvas
only supports the “classic” notion of positioning elements with explicit coordinates, although at least those coordinates are device-independent pixels, unlike in older user interface systems. Canvas
also enables you to specify coordinates relative to any corner, not just the top-left corner.
You can position elements in a Canvas
by using its attached properties: Left
, Top
, Right
, and Bottom
. By setting a value for Left
or Right
, you’re stating that the closest edge of the element should remain a fixed distance from that edge of the Canvas
. And the same goes for setting a value for Top
or Bottom
. In essence, you choose the corner in which to “dock” each element, and the attached property values serve as margins (to which the element’s own Margin
values are added). If an element doesn’t use any of these attached properties (leaving them with their default value of Double.NaN
), it is placed in the top-left corner of the Canvas
(the equivalent of setting Left
and Top
to 0
). This is demonstrated in Listing 5.1, and the result is shown in Figure 5.1.
Canvas
attached properties!If you attempt to set Canvas.Left
and Canvas.Right
simultaneously, Canvas.Right
gets ignored. And if you attempt to set Canvas.Top
and Canvas.Bottom
simultaneously, Canvas.Bottom
gets ignored. Therefore, you can’t dock an element to more than one corner of a Canvas
at a time.
Table 5.1 evaluates the way that some of the child layout properties discussed in the preceding chapter apply to elements inside a Canvas
.
The default Z order (defining which elements are “on top of” other elements) is determined by the order in which the children are added to the parent. In XAML, this is the order in which children are listed in the file. Elements added later are placed on top of elements added earlier. So in Figure 5.1, the orange Button
is on top of the red Button
, and the green Button
is on top of the yellow Button
. This is relevant not just for the built-in panels that enable elements to overlap (such as Canvas
) but whenever a RenderTransform
causes an element to overlap another (as shown in Figures 4.7, 4.8, 4.11, 4.12, and 4.13 in the preceding chapter).
However, you can customize the Z order of any child element by marking it with the ZIndex
attached property that is defined on Panel
(so it is inherited by all panels). ZIndex
is an integer with a default value of 0
that you can set to any number (positive or negative). Elements with larger ZIndex
values are rendered on top of elements with smaller ZIndex
values, so the element with the smallest value is in the back, and the element with the largest value is in the front. In the following example, ZIndex
causes the red button to be on top of the orange button, despite being an earlier child of the Canvas
:
If multiple children have the same ZIndex
value, the order is determined by their order in the panel’s Children
collection, as in the default case.
Therefore, programmatically manipulating Z order is as simple as adjusting the ZIndex
value. To cause the preceding red button to be rendered behind the orange button, you can set the attached property value to any number less than or equal to zero. The following line of C# does just that (assuming that the red button’s name is redButton
):
Although Canvas
is too primitive a panel for creating flexible user interfaces, it is the most lightweight panel. So, you should keep it in mind for maximum performance when you need precise control over the placement of elements. For example, Canvas
is very handy for precise positioning of primitive shapes in vector-based drawings, discussed in Chapter 15, “2D Graphics.”
StackPanel
StackPanel
is a popular panel because of its simplicity and usefulness. As its name suggests, it simply stacks its children sequentially. Examples in previous chapters use StackPanel
because it doesn’t require the use of any attached properties to get a reasonable-looking user interface. In fact, StackPanel
is one of the few panels that doesn’t even define any of its own attached properties!
With no attached properties for arranging children, you just have one way to customize the behavior of StackPanel
—setting its Orientation
property (of type System.Windows.Controls.Orientation
) to Horizontal
or Vertical
. Vertical
is the default Orientation
. Figure 5.2 shows simple Button
s, with no properties set other than Background
and Content
, in two StackPanel
s with only their Orientation
set.
Table 5.2 evaluates the way that some of the child layout properties apply to elements inside a StackPanel
.
The final sentence discussing LayoutTransform
in Table 5.2 needs a little more explanation. Figure 5.3 reveals that when an element that normally would be stretched is rotated, the stretching occurs only when edges of the element are parallel or perpendicular to the direction of stretching. This behavior isn’t specific to StackPanel
but can be seen whenever an element is stretched in only one direction. This odd-looking behavior only applies to LayoutTransform
; it doesn’t happen with RenderTransform
.
WrapPanel
WrapPanel
is similar to StackPanel
. But in addition to stacking its child elements, it wraps them to additional rows or columns when there’s not enough space for a single stack. This is useful for displaying an indeterminate number of items with a more interesting layout than a simple list, much like what Windows Explorer does.
Like StackPanel
, WrapPanel
has no attached properties for controlling element positions. It defines three properties for controlling its behavior:
• Orientation
—This is just like StackPanel
’s property, except Horizontal
is the default. Horizontal Orientation
is like Windows Explorer’s Thumbnails view: Elements are stacked left to right and then wrap top to bottom. Vertical Orientation
is like Windows Explorer’s List view: Elements are stacked top to bottom and then wrap left to right.
• ItemHeight
—A uniform height for all child elements. The way each child fills that height depends on its own VerticalAlignment
, Height
, and so forth. Any elements taller than ItemHeight
get clipped.
• ItemWidth
—A uniform width for all child elements. The way each child fills that width depends on its own HorizontalAlignment
, Width
, and so forth. Any elements wider than ItemWidth
get clipped.
By default, ItemHeight
and ItemWidth
are not set (or, rather, they are set to Double.NaN
). In this case, a WrapPanel
with Vertical Orientation
gives each column the width of its widest element, whereas a WrapPanel
with Horizontal Orientation
gives each row the height of its tallest element. So no intra-WrapPanel
clipping occurs by default.
You can force WrapPanel
to arrange elements in a single row or column by setting its Width
(for Horizontal Orientation
) or Height
(for Vertical Orientation
) to Double.MaxValue
or Double.PositiveInfinity
. In XAML, this must be done with the x:Static
markup extension because neither of these values is supported by the type converter for System.Double
.
Figure 5.4 shows four snapshots of a WrapPanel
with Horizontal Orientation
in action, because it is inside a Window
that is being resized. Figure 5.5 shows the same thing for a WrapPanel
with Vertical Orientation
. When a WrapPanel
has plenty of space and ItemHeight
/ItemWidth
aren’t set, WrapPanel
looks just like StackPanel
.
Table 5.3 evaluates the way that some of the child layout properties apply to elements inside a WrapPanel
.
WrapPanel
is typically not used for laying out controls in a Window
, but rather for controlling layout inside controls. Chapter 10 explains how this is done.
DockPanel
DockPanel
enables easy docking of elements to an entire side of the panel, stretching it to fill the entire width or height. (This is unlike Canvas
, which enables you to dock elements to a corner only.) DockPanel
also enables a single element to fill all the remaining space unused by the docked elements.
DockPanel
has a Dock
attached property (of type System.Windows.Controls.Dock
), so children can control their docking with one of four possible values: Left
(the default when Dock
isn’t applied), Top
, Right
, and Bottom
. Note that there is no Fill
value for Dock
. Instead, the last child added to a DockPanel
fills the remaining space unless DockPanel
’s LastChildFill
property is set to false
. With LastChildFill
set to true
(the default), the last child’s Dock
setting is ignored. With it set to false
, it can be docked in any direction (Left
by default).
Figure 5.6 displays the following five Button
s in a DockPanel
(with LastChildFill
left as true
), each marked with its Dock
setting:
The order in which these controls are added to the DockPanel
is indicated by their number (and color).
As with StackPanel
, any stretching of elements is due to their default HorizontalAlignment
or VerticalAlignment
values of Stretch
. Individual elements can choose different alignments if they don’t want to fill the entire space that DockPanel
gives them. Figure 5.7 demonstrates this with explicit HorizontalAlignment
and VerticalAlignment
values added to all but one Button
rendered in Figure 5.6:
Notice that although four of the elements have chosen not to occupy all the space given to them, the space is not reclaimed for use by other elements.
DockPanel
is useful for arranging a top-level user interface in a Window
or Page
, where most docked elements are actually other panels containing the real meat. For example, applications typically dock a Menu
on top, perhaps a panel on the side, and a StatusBar
on the bottom, and then fill the remaining space with the main content.
The order in which children are added to DockPanel
matters because each child is given all the space remaining on the docking edge. (This is somewhat like people selfishly claiming both armrests when they’re the first to sit down in an airplane or auditorium.)
Figure 5.8 displays five Button
s in a DockPanel
as in Figure 5.6, but added in a different order (indicated by their number and color). Notice how the layout differs from that in the preceding figure.
DockPanel
supports an indefinite number of children—not just five. When multiple elements are docked in the same direction, they are simply stacked in the appropriate direction. Figure 5.9 shows a DockPanel
with eight elements—three docked on the left, two docked on the top, two docked on the bottom, and one filling the remaining space.
Therefore, DockPanel
’s functionality is actually a superset of StackPanel
’s functionality. With LastChildFill
set to false
, DockPanel
behaves like a horizontal StackPanel
when all children are docked to the left, and it behaves like a vertical StackPanel
when all children are docked to the top.
Table 5.4 evaluates the way that some of the child layout properties apply to elements inside a DockPanel
.
Grid
Grid
is the most versatile panel and probably the one you’ll use most often. (Visual Studio and Expression Blend use Grid
by default for their projects.) It enables you to arrange its children in a multirow and multicolumn fashion, without relying on wrapping (like WrapPanel
), and it provides a number of features to control the rows and columns in interesting ways. Working with Grid
is a lot like working with a TABLE
in HTML.
WPF also contains a class called Table
in the System.Windows.Documents
namespace that exposes similar features to Grid
. However, Table
is not a Panel
(or even a UIElement
). It is a FrameworkContentElement
designed for the display of document content, whereas Grid
is a Panel
. Table
is covered in Chapter 11.
Rather than continue to use simple colored buttons to demonstrate layout, Listing 5.2 uses Grid
to build a user interface somewhat like Visual Studio’s start page in older versions. It defines a 4×2 Grid
and arranges a Label
and four GroupBox
es in some of its cells.
For the basic usage of Grid
, you define the number of rows and columns by adding that number of RowDefinition
and ColumnDefinition
elements to its RowDefinitions
and ColumnDefinitions
properties. (This is a little verbose but handy for giving individual rows and columns distinct sizes.) You can then position child elements in the Grid
using its Row
and Column
attached properties, which are zero-based integers. When you don’t explicitly specify any rows or columns, a Grid
is implicitly given a single cell. And when you don’t explicitly set Grid.Row
or Grid.Column
on child elements, the value 0
is used for each.
Grid
cells can be left empty, and multiple elements can appear in the same Grid
cell. In this case, elements are simply rendered on top of one another according to their Z order. As with Canvas
, child elements in the same cell don’t interact with each other in terms of layout; they simply overlap.
Figure 5.10 shows the rendered result of Listing 5.2.
The most noticeable problem with Figure 5.10 is that the list of online articles is too small. Also, it would probably look better if the “Start Page” label spanned the entire width of the Grid
. Fortunately, we can solve both of these problems with two more attached properties defined by Grid
: RowSpan
and ColumnSpan
.
RowSpan
and ColumnSpan
are set to 1
by default and can be set to any number greater than 1
to make an element span that many rows or columns. (If a value greater than the number of rows or columns is given, the element simply spans the maximum number that it can.) Therefore, by simply adding this to the last GroupBox
in Listing 5.2:
and adding this to the Label
in Listing 5.2, you get a much better result, shown in Figure 5.11:
The Grid
in Figure 5.11 still looks a little strange, however, because by default the heights of all rows and the widths of all columns are equal. Ideally, we’d make more room for the list of online articles, and we wouldn’t let the Label
on top take up so much space. We can easily fix this by making the first row and first column size to their content. This autosizing can be done by setting the appropriate RowDefinition
’s Height
and ColumnDefinition
’s Width
to the case-insensitive string Auto
. Therefore, updating the definitions in Listing 5.2 as follows gives the result shown in Figure 5.12:
Grid
has a simple ShowGridLines
property that can be set to true
to highlight the edges of cells with blue and yellow dashed lines. Applications in production have no use for this, but this feature can be a helpful aid to “debug” the layout of a Grid
. Figure 5.13 shows the result of setting ShowGridLines="True"
on the Grid
used in Figure 5.12.
Unlike FrameworkElement
’s Height
and Width
properties, RowDefinition
’s and ColumnDefinition
’s corresponding properties do not default to Auto
(or Double.NaN
). And unlike almost all other Height
and Width
properties in WPF, theirs are of type System.Windows.GridLength
rather than double
. This way, Grid
can uniquely support three different types of RowDefinition
and ColumnDefinition
sizing:
• Absolute sizing—Setting Height
or Width
to a numeric value representing device-independent pixels (like all other Height
and Width
values in WPF). Unlike the other types of sizing, an absolute-sized row or column does not grow or shrink as the size of the Grid
or size of the elements changes.
• Autosizing—Setting Height
or Width
to Auto
(seen previously), which gives child elements the space they need and no more (like the default setting for other Height
and Width
values in WPF). For a row, this is the height of the tallest element, and for a column, this is the width of the widest element. This is a better choice than absolute sizing whenever text is involved to be sure it doesn’t get cut off because of different font settings or localization.
• Proportional sizing (sometimes called star sizing)—Setting Height
or Width
to special syntax to divide available space into equal-sized regions or regions based on fixed ratios. A proportional-sized row or column grows and shrinks as the Grid
is resized.
Absolute sizing and autosizing are straightforward, but proportional sizing needs more explanation. It is done with star syntax that works as follows:
• When a row’s height or column’s width is set to *
, it occupies all the remaining space.
• When multiple rows or columns use *
, the remaining space is divided equally between them.
• Rows and columns can place a coefficient in front of the asterisk (like 2*
or 5.5*
) to take proportionately more space than other columns using the asterisk notation. A column with width 2*
is always twice the width of a column with width *
(which is shorthand for 1*
) in the same Grid
. A column with width 5.5*
is always twice the width of a column with width 2.75*
in the same Grid
.
The “remaining space” is the height or width of the Grid
minus any rows or columns that use absolute sizing or autosizing. Figure 5.14 demonstrates these different scenarios with simple columns in a Grid
.
The default height and width for Grid
rows and columns is *
. That’s why the rows and columns are evenly distributed in Figures 5.10 and 5.11.
GridSplitter
Another attractive feature of Grid
is its support for interactive resizing of rows and columns using a mouse or keyboard (or stylus or finger, depending on your hardware). This is accomplished with the GridSplitter
class from the same namespace. You can add any number of GridSplitter
children to a Grid
and give them Grid.Row
, Grid.Column
, Grid.RowSpan
, and/or Grid.ColumnSpan
attached property values like any other children. Dragging a GridSplitter
resizes at least one cell. Whether the other cells resize or simply move depends on whether they use proportional or nonproportional sizing.
Although GridSplitter
fits in one cell by default, its resizing behavior always affects the entire column (when dragging horizontally) or the entire row (when dragging vertically). Therefore, it’s best to give it a ColumnSpan
or RowSpan
value to ensure that it stretches across the entire height or width of the Grid
.
By default, which cells are directly affected by the resizing depends on GridSplitter
’s alignment values. Table 5.5 summarizes the behavior and also indicates in blue what the GridSplitter
looks like with the various settings, treating the cells of the table as cells of a Grid
.
GridSplitter
has a default HorizontalAlignment
of Right
and a default VerticalAlignment
of Stretch
, so it docks to the right side of the specified cell by default. Any reasonable use of GridSplitter
should set Stretch
alignment in at least one direction. Otherwise, it ends up looking like a small dot, as seen in Table 5.5.
When all rows or columns are proportionally sized, dragging GridSplitter
changes the coefficients for the two affected rows or columns accordingly. When all rows or columns are absolutely sized, dragging GridSplitter
only changes the size of the topmost or leftmost of the two affected cells (depending on the resize direction). The remaining cells get pushed down or to the right to make room. This same behavior applies for autosized rows and columns, although the row or column that gets resized is switched to absolute sizing on the fly.
Although you can control all aspects of the resizing behavior and direction with GridSplitter
’s alignment properties, GridSplitter
also has two properties for explicitly and independently controlling these aspects: ResizeDirection
(of type GridResizeDirection
) and ResizeBehavior
(of type GridResizeBehavior
). ResizeDirection
defaults to Auto
and can be changed to Rows
or Columns
, but this has an effect only when GridSplitter
is stretching in both directions (the bottom-right cell in Table 5.5). ResizeBehavior
defaults to BasedOnAlignment
to get the behavior in Table 5.5 but can be set to PreviousAndCurrent
, CurrentAndNext
, or PreviousAndNext
to control which two rows or columns should be directly affected by the resizing.
The best way to use GridSplitter
is to place it in its own autosized row or column. That way, it doesn’t overlap the existing content in the adjacent cells. If you do place it in a cell with other elements, however, be sure to add it last (or choose an appropriate ZIndex
value) so it has the topmost Z order!
RowDefinitions
and ColumnDefinitions
have a property called SharedSizeGroup
that enables multiple rows and/or columns to remain the same length as each other, even as any of them change length at runtime (via GridSplitter
, for example). SharedSizeGroup
can be set to a simple case-sensitive string value representing an arbitrary group name, and any rows or columns with that same group name are kept in sync.
For a simple example, consider the following three-column Grid
shown in Figure 5.15 that doesn’t use SharedSizeGroup
:
The first column is autosized and has both a Label
and a GridSplitter
. The two remaining columns are both *
-sized and contain only a Label
. As the first column is enlarged, the remaining two *
-sized columns split the shrunken space evenly.
GridSplitter
must be given an explicit Width
(or Height
, depending on orientation) in order to be seen and usable.
In contrast, Figure 5.16 shows what happens with the same Grid
when the first and last columns are marked with the same SharedSizeGroup
. First, all members in the SharedSizeGroup
are initialized to the largest auto or absolute size. Then, as the first column is enlarged, the last column is enlarged to match. The middle column is now effectively the only *
-sized column, and it fills whatever space remains.
The XAML for the Grid
shown in Figure 5.16 is as follows:
The reason that the IsSharedSizeScope
property needs to be set is that size groups can be shared across multiple grids! To avoid potential name collisions (and to cut down on the amount of logical tree walking that needs to be done), all uses of the same SharedSizeGroup
must be under a common parent, with IsSharedSizeScope
set to true
. Besides being a dependency property of Grid
, it’s also an attached property that can be used on non-Grid
parents. Here’s an example:
The “Putting It All Together: Creating a Visual Studio–Like Collapsible, Dockable, Resizable Pane” section at the end this chapter leverages SharedSizeGroup
across multiple Grid
s to create a useful user interface.
Grid
to Other PanelsGrid
is the best choice for most complex layout scenarios because it can do everything done by the previous panels and more, except for the wrapping feature of WrapPanel
. Grid
can also accomplish layout that would otherwise require multiple panels. For example, the start page displayed in Figure 5.12 could have been created with a DockPanel
and a StackPanel
. The DockPanel
would be the outermost element, with the Label
docked on top, the StackPanel
docked to the left (which would contain the first three GroupBox
es). The last GroupBox
would be left to fill the DockPanel
’s remaining space.
To prove that Grid
is usually the best choice, it’s interesting to see how to mimic the behavior of the other panels with Grid
, knowing that you can take advantage of Grid
’s extra features at any time.
Canvas
with Grid
If you leave Grid
with a single row and column and set the HorizontalAlignment
and VerticalAlignment
of all children to values other than Stretch
, the children get added to the single cell just as they do in a Canvas
. Setting HorizontalAlignment
to Left
and VerticalAlignment
to Top
is like setting Canvas.Left
and Canvas.Top
to 0
. Setting HorizontalAlignment
to Right
and VerticalAlignment
to Bottom
is like setting Canvas.Right
and Canvas.Bottom
to 0
. Furthermore, applying Margin
values to each element can give you the same effect as setting Canvas
’s attached properties to the same values. This is what the Visual Studio designer does when the user places and moves items on the design surface.
StackPanel
with Grid
A single-column Grid
with autosized rows looks just like a vertical StackPanel
when each element is manually placed in consecutive rows. Similarly, a single-row Grid
with autosized columns looks just like a horizontal StackPanel
when each element is manually placed in consecutive columns.
DockPanel
with Grid
With RowSpan
and ColumnSpan
, you can easily arrange the outermost elements to be docked and stretched against a Grid
’s edges just like what you would see with DockPanel
. In Figure 5.12, the start page’s Label
is effectively docked along the top.
As with the previous panels, Table 5.6 evaluates the way that some of the child layout properties apply to elements inside a Grid
.
Although Grid
looks like it can practically do it all, StackPanel
and WrapPanel
are better choices when dealing with an indeterminate number of child elements (typically as an items panel for an items control, described in Chapter 10. Also, a DockPanel
with complicated subpanels is sometimes a better choice than a single Grid
panel because the isolation provided by subpanels is more manageable when the user interface changes. With a single Grid
, you might need to adjust RowSpan
and ColumnSpan
values to keep the docking illusion while rows and columns are added to the Grid
.
The previous panels are generally useful for both application layout and control layout. But WPF also ships a few lightweight panels that are likely to be useful only inside controls, whether you’re simply restyling a built-in control (covered in Chapter 14, “Styles, Templates, Skins, and Themes”) or creating a custom control (covered in Chapter 20, “User Controls and Custom Controls”). They aren’t nearly as general purpose as the previous panels but are worth a quick look. All these panels are in the System.Windows.Controls.Primitives
namespace, except for ToolBarTray
, which is in System.Windows.Controls
.
TabPanel
TabPanel
is a lot like WrapPanel
, but with limitations in some areas and extra features in other areas. As its name indicates, it is used in the default style for TabControl
to arrange its tabs. Unlike WrapPanel
, it supports only horizontal stacking and vertical wrapping. When wrapping occurs, it evenly stretches elements so that all rows consume the entire width of the panel. TabControl
is covered in Chapter 10.
ToolBarPanel
ToolBarPanel
, used by the default style of ToolBar
, is like StackPanel
. However, it works in conjunction with an overflow panel (covered next) to arrange items that don’t fit in its own bounds (the ToolBar
’s main area). ToolBar
is covered in Chapter 10.
ToolBarOverflowPanel
ToolBarOverflowPanel
is a simplified WrapPanel
that supports only horizontal stacking and vertical wrapping, used by the default style of ToolBar
to display the extra elements in the overflow area. Above and beyond WrapPanel
’s functionality, it adds a WrapWidth
property that acts like a Padding
property. But there’s no compelling reason to use this panel over WrapPanel
.
ToolBarTray
ToolBarTray
supports only ToolBar
children (and throws an InvalidOperationException
if you try to add children of any other type). It arranges the ToolBar
s sequentially (horizontally by default), and it also enables you to drag them around to form additional rows or compress/expand neighboring ToolBar
s.
UniformGrid
UniformGrid
is an interesting primitive panel, although its usefulness is questionable. It’s a simplified form of Grid
in which all rows and columns are of size *
and can’t be changed. Because of this, UniformGrid
has two simple double
properties to set the number of rows and columns rather than the more verbose RowDefinitions
and ColumnDefinitions
collections. It also has no attached properties; children are added in row-major order, and there can be only one child per cell.
Furthermore, if you don’t explicitly set the number of rows and columns (or if the number of children exceeds the explicit number of cells), UniformGrid
automatically chooses suitable values. For example, it automatically places 2–4 elements in a 2×2 arrangement, 5–9 elements in a 3×3 arrangement, 10–16 elements in a 4×4 arrangement, and so on. Figure 5.17 demonstrates UniformGrid
’s default layout when eight Button
s are added to it.
SelectiveScrollingGrid
SelectiveScrollingGrid
is a Grid
subclass used by the default style of the DataGridRow
control. On top of Grid
’s functionality, it adds the ability to “freeze” cells while the rest of them scroll. This behavior is controlled by the SelectiveScrollingOrientation
property, which can be set to one of the following values:
• None
—The cells will not scroll in either direction.
• Horizontal
—The cells can scroll only horizontally.
• Vertical
—The cells can scroll only vertically.
• Both
—The cells can scroll in any direction. This is the default value.
The built-in panels make their best effort to accommodate the size needs of their children. But sometimes they are forced to give children smaller space than they would like, and sometimes children refuse to render completely within that smaller space. For example, perhaps an element is marked with an explicit width that’s wider than the containing panel. Or perhaps a control such as ListBox
contains so many items that they can’t all fit within the containing Window
. In such cases, a content overflow problem exists.
You can deal with content overflow by using several different strategies:
• Clipping
• Scaling
• Wrapping
• Trimming
The first three strategies are examined in this section. You’ve already seen examples of wrapping with WrapPanel
(plus TabPanel
and ToolBarOverflowPanel
). This is the only built-in way to get wrapping behavior for content other than text (the layout of which is covered in Chapter 11).
Trimming refers to a more intelligent form of clipping. It is only supported for text by the TextBlock
and AccessText
elements. They have a TextTrimming
property (of type System.Windows.TextTrimming
) that can be set to None
(the default), CharacterEllipsis
, or WordEllipsis
. With the latter two values, text gets trimmed with ellipses (...) rather than simply being truncated at an arbitrary place.
Clipping (that is, truncating or cropping) children is the default way that panels handle them when they are too large. Clipping can happen at the edges of a panel or within a panel (such as at the edges of a Grid
cell or the fill area of a DockPanel
). This behavior can be controlled to some degree, however.
All UIElement
s have a Boolean ClipToBounds
property that controls whether child elements can be rendered outside its bounds. If an outer element’s edge coincides with the outer Window
’s or Page
’s edge, however, clipping still occurs. This mechanism is not a means to draw outside the bounds of a Window
. (However, nonrectangular windows are discussed in Chapter 7, “Structuring and Deploying an Application.”)
Despite the fact that all panels inherit a ClipToBounds
property, most panels automatically clip their children regardless of this property’s value. Canvas
and UniformGrid
do not clip their children by default, and they both support setting ClipToBounds
to true
to force clipping.
Figure 5.18 shows the difference that ClipToBounds
makes with a Button
that isn’t entirely contained within its parent Canvas
(which has a tan background).
This behavior means that unless you set ClipToBounds
to true
, the size of Canvas
is irrelevant; it can be given a Height
and Width
of 0
, yet all its contents will be rendered as if the Canvas
occupied the whole screen!
Controls can also control the clipping of their own content with ClipToBounds
. For example, Button
has ClipToBounds
set to false
by default. Figure 5.19 demonstrates the effect of setting it to true
when its text is scaled with ScaleTransform
(applied as a RenderTransform
).
Canvas
can be used as an intermediate element to prevent clipping in other panels. For example, if a large Button
gets clipped at the edge of a Grid
, you can make it render past the edge of the Grid
if you instead place a Canvas
in that cell (which gets sized to fit the cell) and then place the Button
inside that Canvas
. Of course, you need to write some code if you want the Button
to get the same stretching behavior it would have gotten by being a direct child of the Grid
.
You can use the same approach to work around clipping within inner cells of a Grid
, but increasing an element’s RowSpan
and/or ColumnSpan
is usually the best way to enable it to “bleed” into adjacent cells.
RenderTransforms
are applied!When enlarging an element with ScaleTransform
as a RenderTransform
, the element can easily surpass the bounds of the parent panel yet doesn’t get clipped (unless it reaches the edge of the Window
or Page
). Shrinking an element with ScaleTransform
as a RenderTransform
is more subtle. If the unscaled element would have been clipped because it exceeds its parent’s bounds, the scaled element is still clipped exactly the same way, even if the entire element can fit! That’s because clipping is part of the layout process and already determined by the time RenderTransform
is applied. If you need to shrink a large element by using ScaleTransform
, applying it as a LayoutTransform
might suit your needs better.
For many applications, the ability to scroll through content that is too large to view all at once is critical. WPF makes this easy because all you need to do is wrap an element in a System.Windows.Controls.ScrollViewer
control, and the element instantly becomes scrollable. ScrollViewer
makes use of ScrollBar
controls and hooks them up to your content automatically.
ScrollViewer
has a Content
property that can be set to a single item, typically an entire panel. Because Content
is ScrollViewer
’s content property in the XAML sense, you can place the item requiring scrolling as its child element:
Figure 5.20 shows the Window
containing the simple StackPanel
, with and without a ScrollViewer
.
The ScrollBar
controls respond to a variety of input, such as arrow keys for fine-grained scrolling, Page Up and Page Down for coarser scrolling, and Ctrl+Home or Ctrl+End to jump to the beginning or end, respectively.
ScrollViewer
exposes several properties and methods for more advanced or programmatic manipulation of scrolling, but its two most important properties are VerticalScrollBarVisibility
and HorizontalScrollBarVisibility
. Both of these properties are of type ScrollBarVisibility
, an enumeration that defines four distinct states specific to its two ScrollBar
s:
• Visible
—The ScrollBar
is always visible, regardless of whether it’s needed. When it’s not needed, it has a disabled look and doesn’t respond to input. (But this is different from the ScrollBarVisibility
value called Disabled
.)
• Auto
—The ScrollBar
is visible if the content is big enough to require scrolling in that dimension. Otherwise, the ScrollBar
disappears.
• Hidden
—The ScrollBar
is always invisible but still logically exists, in that scrolling can still be done with arrow keys. Therefore, the content is still given all the length it wants in that dimension.
• Disabled
—The ScrollBar
is not only invisible but doesn’t exist, so scrolling is not possible via mouse or keyboard. In this case, the content is only given the length of its parent rather than all the length it wants.
The default value for VerticalScrollBarVisibility
is Visible
, and the default value for HorizontalScrollBarVisibility
is Auto
, to match the scrolling behavior used by most applications.
Depending on the content inside ScrollViewer
, the subtle difference between Hidden
and Disabled
can be not so subtle. For example, Figure 5.21 shows two different Window
s containing a ScrollViewer
with exactly the same WrapPanel
. The only difference is that in one Window
the ScrollViewer
has HorizontalScrollBarVisibility
set to Hidden
, and in the other Window
the ScrollViewer
has it set to Disabled
.
In the Hidden
case, the WrapPanel
is given as much width as it desires (the same as if HorizontalScrollBarVisibility
were set to Visible
or Auto
), so it makes use of it and arranges all children on the same row. In the Disabled
case, the WrapPanel
is only given the width of the parent Window
, so wrapping occurs as if no ScrollViewer
existed.
Chapter 3, “WPF Fundamentals,” reveals that the default visual tree for ListBox
contains a ScrollViewer
. You can set its VerticalScrollBarVisibility
and HorizontalScrollBarVisibility
properties as attached properties on the ListBox
to impact the behavior of the implicit ScrollViewer
:
Although scrolling is a popular and long-standing way to deal with large content, dynamically shrinking or enlarging content to “just fit” in a given space is more appropriate for several scenarios. As a simple example, imagine that you want to create a card game. You need some playing cards, and you probably want them to scale proportionally with the game’s Window
.
Figure 5.22 displays some shapes that form a vector representation of a playing card (shown with its source XAML in Chapter 20). These shapes are placed inside a Canvas
, which is inside a Window
. Because of their explicit sizes, they do not change size as the Window
gets resized (even if they were placed in a Grid
rather than a Canvas
), and, obviously, the shapes are currently far too big.
ScaleTransform
can scale elements relative to their own size (and easily help with the size of the playing card), but it doesn’t provide a mechanism to scale elements relative to their available space without writing some custom code. Fortunately, System.Windows.Controls.Viewbox
provides an easy mechanism to scale arbitrary content within a given space.
Viewbox
is a type of class known as a decorator, a panel-like class that can have only one child element. It derives from System.Windows.Controls.Decorator
, along with classes such as Border
. By default, Viewbox
(like most controls) stretches in both dimensions to fill the space given to it. But it also has a Stretch
property to control how its single child gets scaled within its bounds. The property is a System.Windows.Media.Stretch
enumeration, which has the following values (demonstrated in Figure 5.23 by wrapping the Canvas
inside a Viewbox
):
• None
—No scaling is done. This is the same as not using Viewbox
at all.
• Fill
—The child’s dimensions are set to equal the Viewbox
’s dimensions. Therefore, the child’s aspect ratio is not necessarily preserved.
• Uniform
—The child is scaled as large as it can be while still fitting entirely within the Viewbox
and preserving its aspect ratio. Therefore, there will be extra space in one dimension if its aspect ratio doesn’t match. This is the default value.
• UniformToFill
—The child is scaled to entirely fill the Viewbox
while preserving its aspect ratio. Therefore, the content will be cropped in one dimension if its aspect ratio doesn’t match.
Although it’s unrealistic for a card game to want its cards to be the size of the Window
, the same techniques apply for making the cards occupy a certain fraction of the Window
’s size. In Figure 5.23, Viewbox
is the child element of the Window
, but in a real application, you would likely place the Viewbox
inside an appropriately sized Grid
cell.
A second property of Viewbox
controls whether you want to use it only to shrink content or enlarge content (as opposed to doing either). This property is called StretchDirection
, and it is a System.Windows.Controls.StretchDirection
enumeration with the following values:
• UpOnly
—Enlarges the content, if appropriate. If the content is already too big, Viewbox
leaves the current content size as is.
• DownOnly
—Shrinks the content, if appropriate. If the content is already small enough, Viewbox
leaves the current content size as is.
• Both
—Enlarges or shrinks the content, whichever is needed to get the stretching described earlier. This is the default value.
It’s pretty amazing how easy it is to choose between a scrolling strategy and a scaling strategy for dealing with large content. Consider the following Window
that is shown in Figure 5.20:
Simply changing the ScrollViewer
element to Viewbox
(and updating the Window
’s Title
) produces the result in Figure 5.24:
Just like that, you can now see all eight buttons, regardless of the Window
size!
Viewbox
removes all wrapping!Viewbox
is very handy for many situations, but it’s not a good choice for content you’d normally like to wrap, such as a paragraph of text or any content in a WrapPanel
. That’s because the content is given as much space as it needs in both directions before it is potentially scaled. Figure 5.25 demonstrates this by using the WrapPanel
with eight Button
s from Figure 5.21, but replacing ScrollViewer
with Viewbox
.
The result is a single line of content that could potentially be much smaller than you would have liked. Giving Viewbox
a StretchDirection
of UpOnly
rather than the default of Both
doesn’t help either. The layout of Viewbox
’s content happens before any potential scaling. Therefore, UpOnly
prevents the Buttons
from shrinking, but they are still arranged in a single line, as shown in Figure 5.26.
The result of this is similar to the use of HorizontalScrollBarVisibility="Hidden"
in Figure 5.21, except that there’s no way to scroll to the remaining content, even with the keyboard.
Let’s put WPF’s layout features to the test and create a more complex piece of user interface. In this section, we create some Visual Studio–like panes that can be docked next to the window’s main content or collapsed to a button along the edge of the window. In this collapsed form, hovering over the button shows the pane, but rather than being docked, it overlaps on top of the main content. Whether it is docked or undocked, each pane is resizable using a splitter. Figures 5.27 through 5.33 walk through several sequential states of the user interface as it is being used.
When both panes are undocked, they resize independently from the main content and each other. When both panes are docked (as in Figure 5.33), the user interface behaves like a single Grid
with three cells that can be resized but never overlap.
So, how do you go about implementing such a user interface? Because splitters are needed for interactive resizing, using Grid
with GridSplitter
s is a natural choice. No other built-in panels provide an interactive splitter. But because undocked panes need to overlap and resize independently from one another, a single Grid
won’t do. Instead, this example uses three independent Grid
s—one for the main content and one for each pane—layered on top of each other. SharedSizeGroup
is then used to keep these three independent Grid
s in sync when they need to be (that is, the docked case). Figure 5.34 illustrates the structure of these three Grid
s and how they are tied together.
The bottom layer (Layer 0) contains the main content that stretches to fill the Grid
when both panes are collapsed. Hovering over either pane’s button switches the appropriate pane’s visibility in Layers 1 or 2 from Collapsed
to Visible
. Each pane’s splitter can be used to adjust the space between itself and the column to the left (which is empty, revealing the content from Layer 0 behind it).
The main trickery occurs when it’s time to dock a panel. When docking Pane 1, the main content needs to be squeezed to match the width of the empty 0th column in Layer 1. Therefore, an empty column is dynamically added to Layer 0 and given the same width as Pane 1. Because a SharedSizeGroup
is used rather than a hard-coded width, the bottom layer stays up to date as the splitter in Layer 1 is used.
The same technique is used when docking Pane 2, except that the dummy column needs to be added to all layers underneath (both Layers 0 and 1). This enables both docked panes to be seen simultaneously with no overlap, and it enables the main content on Layer 0 to be sized appropriately in the presence of zero, one, or two docked panes. Note that the ordering of the panes when both are docked is predetermined.
These three Grid
s are placed in (what else?) a Grid
with a single row and column, so they can completely overlap each other while stretching to completely fill the space given to them. Although Layer 0 always has the bottommost Z order, the Z order between the other layers can get swapped so the current undocked pane is always on top.
Listing 5.3 contains the XAML for the application shown in Figures 5.27 to 5.33, with some of the irrelevant parts removed for brevity. The entire project appears with this book’s source code (available on the book’s website, http://informit.com/title/9780672331190).
The Window
’s top-level panel is a DockPanel
, which arranges a Menu
, the “button bar” StackPanel
(rotated 90° with a RotateTransform
), and a single-cell grid containing the three “layer” Grid
s. Notice that the Menu
is added to the DockPanel
before the StackPanel
so it stretches all the way across the top.
Each layer Grid
has only one column containing any content, and that content happens to be encased in a Grid
in all three cases. Each GridSplitter
is docked on the left inside the column with the content, so it doesn’t overlap any content from the other layers. One subtlety is that a TextBlock
is used for each pane’s header instead of a Label
so that TextTrimming="CharacterEllipsis"
can be set to get a more polished effect than simply clipping the text when the pane is resized.
Listing 5.4 contains the C# code-behind file for Listing 5.3.
The C# code is hard-coded to work with exactly two panes. You would be more likely to generalize the code and abstract it into a custom control, but as far as layout goes, the concepts are the same.
Notice that there is no code to hide the “button bar” when all panes have been docked or to reveal it when at least one pane is undocked. This happens automatically because the StackPanel
sizes to its content by default, so collapsing both Button
s ends up collapsing the StackPanel
.
Although Listing 5.4 doesn’t contain very much code (or any complex code), it achieves a relatively sophisticated user interface.
With all the features described in this chapter and the preceding chapter, you can control layout in many interesting ways. This isn’t like the old days, where your only options were pretty much just choosing a size and choosing an (X,Y) point on the screen.
The built-in panels—notably Grid
—are a key part of WPF’s capability to enable rapid application development. But one of the most powerful aspects of WPF’s layout is that parent panels can themselves be children of other panels. Although each panel was examined in isolation in this chapter, panels can be nested to provide impressive versatility.