Chapter 5. Layout with Panels

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

StackPanel

WrapPanel

DockPanel

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.

Figure 5.1 The Buttons in a Canvas from Listing 5.1.

image

Listing 5.1 Buttons Arranged in a Canvas

image

Warning: Elements can’t use more than two of the 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.

Table 5.1 Canvas’s Interaction with Child Layout Properties

image

Tip

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:

image

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):

image

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 Buttons, with no properties set other than Background and Content, in two StackPanels with only their Orientation set.

Figure 5.2 Buttons in a StackPanel, using both Orientations.

image

Table 5.2 evaluates the way that some of the child layout properties apply to elements inside a StackPanel.

Table 5.2 StackPanel’s Interaction with Child Layout Properties

image

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.

Figure 5.3 The yellow Button is rotated 80° then 90° using LayoutTransform.

image

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.

Tip

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.

Figure 5.4 Buttons arranged in a WrapPanel with its default Horizontal Orientation, as the Window width shrinks.

image

Figure 5.5 Buttons arranged in a WrapPanel with Vertical Orientation, as the Window height shrinks.

image

Table 5.3 evaluates the way that some of the child layout properties apply to elements inside a WrapPanel.

Table 5.3 WrapPanel’s Interaction with Child Layout Properties

image

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 Buttons in a DockPanel (with LastChildFill left as true), each marked with its Dock setting:

image

Figure 5.6 Buttons arranged in a DockPanel.

image

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:

image

Figure 5.7 Buttons arranged in a DockPanel that don’t occupy all the space given to them.

image

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 Buttons 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.

Figure 5.8 Buttons arranged in a DockPanel in a different order than Figure 5.6.

image

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.

Figure 5.9 Multiple elements can be docked in all directions.

image

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.

Table 5.4 DockPanel’s Interaction with Child Layout Properties

image

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.

Tip

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 GroupBoxes in some of its cells.

Listing 5.2 A First Attempt at a Visual Studio–Like Start Page with a Grid

image

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.

Figure 5.10 The first attempt at a Visual Studio–like start page is not very satisfactory.

image

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:

image

and adding this to the Label in Listing 5.2, you get a much better result, shown in Figure 5.11:

image

Figure 5.11 Using RowSpan and ColumnSpan improves the Visual Studio–like start page.

image

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:

image

Figure 5.12 The final Visual Studio–like start page uses autosizing in the first row and first column.

image

FAQ

image How can I give Grid cells background colors, padding, and borders, as with cells of an HTML TABLE?

There is no intrinsic mechanism to give Grid cells such properties, but you can simulate them pretty easily, thanks to the fact that multiple elements can appear in any Grid cell. To give a cell a background color, you can simply plop in a Rectangle with the appropriate Fill, which stretches to fill the cell by default. To give a cell padding, you can use autosizing and set the Margin on the appropriate child element. For borders, you can again use a Rectangle but give it an explicit Stroke of the appropriate color, or you can use a Border element instead.

Just be sure to add such Rectangles or Borders to the Grid before adding any of the other children (or explicitly mark them with the ZIndex attached property), so their Z order puts them behind the main content.

Tip

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.

Figure 5.13 Using ShowGridLines on a Grid.

image

Sizing the Rows and Columns

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.

Figure 5.14 Proportional-sized Grid columns in action.

image

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.

FAQ

image Why doesn’t WPF, like HTML, provide built-in support for percentage sizing?

The most common use of percentage sizing in HTML—setting the width or height of an item to 100%—is handled by setting an element’s HorizontalAlignment or VerticalAlignment property to Stretch inside most panels. For more complicated scenarios, Grid’s proportional sizing effectively provides percentage sizing, but with a syntax that takes a little getting used to. For example, to have a column always occupy 25% of a Grid’s width, you can mark it with * and ensure that the remaining columns have a total width of 3*.

The WPF team chose this syntax so developers wouldn’t have to worry about keeping the sum of percentages equal to 100 as rows or columns are dynamically added or removed. In addition, the fact that proportional sizing is specified relative to the remaining space (as opposed to the entire Grid) makes its behavior more understandable than an HTML table when mixing proportional rows or columns with fixed-size rows or columns.

Interactive Sizing with 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.

Tip

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.

Table 5.5 The Cells Directly Affected When Dragging a GridSplitter with Various Alignment Settings

image

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.

Tip

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!

Sharing Row and Column Sizes

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:

image

Figure 5.15 A simple Grid that doesn’t use SharedSizeGroup.

image

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.

Tip

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.

Figure 5.16 The Grid from Figure 5.15, but with the first and last columns in the same SharedSizeGroup.

image

The XAML for the Grid shown in Figure 5.16 is as follows:

image

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:

image

The “Putting It All Together: Creating a Visual Studio–Like Collapsible, Dockable, Resizable Pane” section at the end this chapter leverages SharedSizeGroup across multiple Grids to create a useful user interface.

Comparing Grid to Other Panels

Grid 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 GroupBoxes). 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.

Mimicking 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.

Mimicking 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.

Mimicking 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.

Table 5.6 Grid’s Interaction with Child Layout Properties

image

Tip

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.

Primitive Panels

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 ToolBars sequentially (horizontally by default), and it also enables you to drag them around to form additional rows or compress/expand neighboring ToolBars.

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 Buttons are added to it.

Figure 5.17 Eight Buttons added to a UniformGrid.

image

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.

Handling Content Overflow

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

Scrolling

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

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 UIElements 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).

Figure 5.18 ClipToBounds determines whether children can be rendered outside their panel.

image

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).

Figure 5.19 ClipToBounds can be used on a control such as Button to affect the rendering of its inner content.

image

Tip

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.

Warning: Clipping occurs before 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.

Scrolling

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:

image

Figure 5.20 shows the Window containing the simple StackPanel, with and without a ScrollViewer.

Figure 5.20 ScrollViewer enables scrolling of an element that is larger than the space given to it.

image

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 ScrollBars:

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 Windows 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.

Figure 5.21 Although the horizontal ScrollBar is invisible in both cases, the different values for HorizontalScrollBarVisibility drastically alter the layout of the WrapPanel.

image

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.

Tip

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:

image

Scaling

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.

Figure 5.22 The shapes representing the playing card do not scale with the Window.

image

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.

Figure 5.23 Each of the four values for Viewbox’s Stretch property changes the playing card’s layout.

image

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:

image

Simply changing the ScrollViewer element to Viewbox (and updating the Window’s Title) produces the result in Figure 5.24:

image

Figure 5.24 The StackPanel used in Figure 5.20, but now wrapped in a Viewbox instead of ScrollViewer.

image

Just like that, you can now see all eight buttons, regardless of the Window size!

Warning: 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 Buttons from Figure 5.21, but replacing ScrollViewer with Viewbox.

Figure 5.25 The WrapPanel used in Figure 5.21 has no need to wrap when placed in a Viewbox instead of a ScrollViewer.

image

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.

Figure 5.26 Giving the Viewbox from Figure 2.25 a StretchDirection="UpOnly" prevents the Buttons from shrinking but doesn’t affect the inner WrapPanel’s layout.

image

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.

Putting It All Together: Creating a Visual Studio–Like Collapsible, Dockable, Resizable Pane

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.

Figure 5.27 Both panes start out hidden, so you see only their buttons docked on the right.

image

Figure 5.28 Hovering over the Toolbox button presents the undocked Toolbox pane, which stays open unless the mouse wanders onto the main content or a different pane’s button.

image

Figure 5.29 An undocked pane can be resized, and it still overlaps the main content.

image

Figure 5.30 The Toolbox pane is docked by clicking the pushpin, making the main content shrink to fit beside it and making the Toolbox button on the right disappear.

image

Figure 5.31 The docked pane can still be resized with the GridSplitter, but this time the main content stretches and shrinks in unison.

image

Figure 5.32 Hovering over the Solution Explorer button presents the undocked Solution Explorer pane, which overlaps all other content (including the docked Toolbox pane). The undocked pane can be resized independently to overlap more or less of the other content.

image

Figure 5.33 The Solution Explorer pane is docked by clicking the pushpin, pushing the Toolbox pane over, and making the entire rightmost bar disappear because there are no more undocked pane buttons to show.

image

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 GridSplitters 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 Grids—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 Grids in sync when they need to be (that is, the docked case). Figure 5.34 illustrates the structure of these three Grids and how they are tied together.

Figure 5.34 The three independent Grids used to implement two collapsible, dockable, resizable panes.

image

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 Grids 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).

Listing 5.3 VisualStudioLikePanes.xaml—The XAML Implementation of the Application in Figures 5.27 to 5.33

image

image

image

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” Grids. 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.

Listing 5.4 VisualStudioLikePanes.xaml.cs—The C# Implementation of the Application in Figures 5.27 to 5.33

image

image

image

image

image

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 Buttons 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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset