Grid
Consider the document properties dialog shown in
Figure 2-11. This is from the Microsoft SDK help viewer application,
and we are going to add a similar dialog to our document viewer. Notice how the
main area of the form is arranged as two columns. The column on the left
contains labels, and the column in the middle contains information.
Achieving this kind of layout with a StackPanel is
difficult, because it is not designed with two-dimensional alignment in mind.
We could try to use nesting: Example
2-7 shows a vertical StackPanel with three rows, each with a
horizontal StackPanel.
Example 2-7. Ineffective use of StackPanel
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock>Protocol:</TextBlock>
<TextBlock>Unknown Protocol</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock>Type:</TextBlock>
<TextBlock>Not available</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock>Connection:</TextBlock>
<TextBlock>Not encrypted</TextBlock>
</StackPanel>
</StackPanel>
The result, shown in Figure
2-12, is not what we want at all. Each row has been arranged
independently, and there is no consistency.
The Grid panel solves this problem. Rather than working
a single row or a single column at a time, it aligns all elements into a grid
that covers the whole area of the panel. This allows consistent positioning
from one row to the next. Example 2-8
shows the same elements as Example 2-7,
but arranged with a Grid rather than StackPanels.
Example 2-8. Grid Layout
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0">Protocol:</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="0">Unknown Protocol</TextBlock>
<TextBlock Grid.Column="0" Grid.Row="1">Type:</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="1">Not available</TextBlock>
<TextBlock Grid.Column="0" Grid.Row="2">Connection:</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="2">Not encrypted</TextBlock>
</Grid>
The Grid needs to know how many columns and rows we
require, and we indicate this by specifying a series of ColumnDefinition
and RowDefinition elements at the start. This may seem rather verbosea
simple pair of properties on the Grid itself might seem like a simpler
solution. However, you will typically want to control the characteristics of
each column and row independently, so in practice, it makes sense to have
elements representing them.
Notice that each element in the grid has its column and row
specified explicitly using attached properties. This is mandatory: without
these, everything ends up in column 0, row 0. (Grid uses a zero-based
numbering scheme, so these correspond to the top-left corner.)
|
You might be wondering why the Grid doesn't simply put
items into the grid in the order in which they appearthis would remove the need
for the Grid.Row and Grid.Column attached properties. There
are a couple of reasons why it doesn't work this way.
Grid cells can be empty. If the grid's children simply filled
the cells in order, you would need to put placeholders of some kind to indicate
blank cells. But since elements indicate their grid position, you can leave
cells empty simply by providing no content for that cell.
Cells can also contain multiple elements. In this case, the
order in which the relevant elements are listed in the markup determines which
appears "on top." Elements that appear later in the document are drawn over
those that appear earlier. The order in which overlapping elements are drawn is
usually referred to as the Z order . This
is because the x- and y-axes are traditionally the ones used for drawing
onscreen, so the z-axis would logically be used to determine how overlapping
elements are ordered.
In general, panels that allow their children to overlap (e.g., Grid
and Canvas) rely on the order in which elements appear in the XAML to
determine the Z order.
|
Figure 2-13 shows
the result of Example 2-8.
This example has lines showing the grid outline, because we enabled the ShowGridLines
property. You would not normally do this on a finalized designthis feature is
intended to make it easy to see how the Grid has divided up the
available space. With grid lines displayed, it is clear that the Grid has
made all the columns the same width and all the rows the same height.
This default "one size fits all" behavior is useful when you
want all the items in the grid to be the same size, but it's not what we want
here. It would make more sense for the column on the left to be wide enough to
contain the labels and for the column on the right to be allocated the
remaining space. Fortunately, the Grid provides a variety of options
for managing column width and row height.
2.4.1. Grid Column Widths and Row Heights
The column widths and row heights in
a Grid are configured using the ColumnDefinition and RowDefinition
elements. There are three options: fixed size, automatic size, and proportional
sizing.
Fixed sizing is the simplest to understand, but often the most
effort to use, as you end up having to do all of the work yourself. You can
specify the Width of a column or the Height of a row in
logical pixels. (A logical pixel is 1/96th of
an inch. WPF's coordinate system is described in
Chapter 7.) Example 2-9
shows a modified version of the column definitions in
Example 2-8, specifying a fixed width for the first column.
Example 2-9. Fixed column width
...
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
...
Figure 2-14 illustrates
the main problem with using fixed column widths .
If you make the column too narrow, the contents will simply be cropped. Fixed
widths and heights may seem an attractive idea because they give you complete
control, but in practice they tend to be inconvenient. If you change the text
or the font, you will need to modify the sizes to match. Localization of
strings will also require the sizes to be changed. (See
Chapter 6 for more information about localization.) So, in practice,
fixed widths and heights are not what you will normally want to use.
The most appropriate sizing strategy for our label column will
be automatic sizing. This tells the Grid to make the column wide
enough to contain the widest element.
Example 2-10 shows a modified version of the column and row definitions
from Example 2-8, specifying
automatic width for the first column and automatic heights for all of the rows.
Example 2-10. Automatic width and height
...
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
...
This is not quite right yetas you can see from
Figure 2-15, the Grid has not left any space around the text,
so the results seem rather cramped. The solution is exactly the same as it was
for the StackPanel: we simply use the Margin property on the TextBlock
elements in the Grid to indicate that we want some breathing room
around the text. The Grid will honor this, giving us the layout we
require.
If the idea of adding a Margin attribute to every
single element sounds tedious, don't worry. We can give all of the TextBlock
elements the same margin by defining a style, as
Example 2-11 shows.
Example 2-11. Applying a consistent margin with a style
<Grid ShowGridLines="True">
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="5,3" />
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
... as before
As Figure 2-16
shows, this provides the better-spaced layout we require. (Styles are described
in detail in Chapter 5.)
The final mechanism for specifying width and height in a Grid
is the proportional method . This is sometimes
called "star" sizing because of the syntax used to represent it in XAML. If you
set the width or height of a column or row to be "*", this tells the Grid
that it should fill all the leftover space after any fixed and automatic items
have taken their share. If you have multiple items set to "*", the
space is shared evenly between them.
The default value for column width and row height is "*",
so you have already seen the effect of this. As
Figure 2-13 shows, when we don't specify column widths or row heights,
each cell ends up with exactly the same amount of space.
The star syntax is a little more
flexible than this. Rather than dividing up space evenly between all the rows
or columns marked with a star, we can choose a proportional distribution.
Consider this set of row definitions:
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
Here, the first row has been set to size automatically, and the
other two rows both use proportional sizing. However, the middle row has been
marked as "2*". This indicates that it wants to be given twice as much
of the available space as the row marked with "*". For example, if the
grid's total height was 350, and the first row's automatic height came out as
50, this would leave 300 for the other rows. The second row's height would be
200, and the third row's height would be 100. The results are shown in
Figure 2-17.
The numbers before the "*" specify relative sizes, not
absolute sizes. If the example above were modified to use "6*" and "3*"
instead of "2*" and "*", the net result would be exactly the
same. It's equivalent to saying that you want the rows to use six ninths and
three ninths of the available space, instead of saying that you want them to
use two thirds and one thirdit's just two ways of expressing the same ratio.
 |
If you are familiar with HTML, you may have been
wondering whether you can use percentage sizes. You can't, but this * mechanism
lets you achieve the same effects.
|
|
You may have noticed that regardless of which of the three grid
sizing strategies we choose, we end up using the same Width and Height
properties. Each of these properties contains both a number and the sizing
system. Width and Height are both of type GridLength
. The GridLength type holds a number and a unit type. The number is
stored as a Double, and the unit type is represented by the GridUnitType
enumeration.
For a fixed size, the unit type is Pixel.
In XAML, this is indicated by providing just a number. For automatic sizing
, the unit type is Auto and no number is required. In XAML, this is
indicated by the string "Auto". For proportional sizing, the unit type
is Star. In XAML this is indicated either by just "*" or a
number and a stare.g., "3.5*". If you use a mixture of unit types in a
single grid, the grid will allocate space to fixed-size and auto-sized columns
or rows first, and then divide the remaining space up amongst any star-sized
items.
2.4.2. Spanning Multiple Rows and Columns
Looking at the Properties dialog shown earlier in
Figure 2-11, there is a feature we have left out. The dialog has two
horizontal lines dividing the UI into three sections. However, the aligned
columns span the whole window, straddling these dividing lines.
It would be inconvenient to try to achieve a layout like this
with multiple grids. If you used one for each section of the window, you could
keep the columns aligned in all the grids by using fixed column widths. As
discussed earlier, use of fixed widths is inconvenient because it tends to
require manual adjustment of the widths whenever anything changes. With this
layout, it becomes triply inconvenient: you would have to change all three
grids every time anything changed.
Fortunately, it is possible to add these dividing lines without
splitting the UI up into separate grids. The way to do this is to put the
dividing lines into cells that span all of the columns in the grid. An element
indicates to its parent Grid that it would like to span multiple
columns by using the attached Grid.ColumnSpan property.
Example 2-12 illustrates this approach.
Example 2-12. Using Grid.ColumnSpan
<Grid>
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="5,3" />
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0">Title:</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="0">Information Overload</TextBlock>
<Rectangle Grid.Row="1" Grid.ColumnSpan="2" Margin="5"
Height="1" Fill="Black" />
<TextBlock Grid.Column="0" Grid.Row="2">Protocol:</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="2">Unknown Protocol</TextBlock>
<TextBlock Grid.Column="0" Grid.Row="3">Type:</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="3">Not available</TextBlock>
<TextBlock Grid.Column="0" Grid.Row="4">Connection:</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="4">Not encrypted</TextBlock>
<Rectangle Grid.Row="5" Grid.ColumnSpan="2" Margin="5"
Height="1" Fill="Black" />
<TextBlock Grid.Column="0" Grid.Row="6">Created:</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="6">Not available</TextBlock>
<TextBlock Grid.Column="0" Grid.Row="7">Modified:</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="7">Not available</TextBlock>
</Grid>
Example 2-12 uses
a single Grid to show three sets of properties. These sets are
separated by thin Rectangle elements, using Grid.ColumnSpan to
fill the whole width of the Grid. Because a single Grid is
used for all three sections, the columns remain aligned across all three
sections, as you can see in Figure
2-18. If we had used three separate grids with the left-most column set
to use automatic width, each would have chosen its own width, causing the
right-hand columns to be misaligned.
The Grid class also defines a Grid.RowSpan attached
property. This works in exactly the same way as Grid.ColumnSpan, but
vertically.
You are free to use both Grid.RowSpan and Grid.ColumnSpan
on the same elementany element may occupy as many grid cells as it likes. Also,
note that you are free to put multiple overlapping items into each cell.
Example 2-13 illustrates
both of these techniques. It adds two Rectangle elements to color in
areas of the grid. The first spans multiple rows, and the second spans both
multiple rows and multiple columns. Both Rectangle elements occupy
cells in the Grid that are also occupied by text.
Example 2-13. Multiple Items in a Grid Cell
<Rectangle Grid.Column="1" Grid.Row="2" Grid.RowSpan="3"
Margin="5,3" Fill="Cyan" />
<Rectangle Grid.Column="0" Grid.Row="6" Grid.ColumnSpan="2" Grid.RowSpan="2"
Margin="5,3" Fill="Khaki" />
<TextBlock Grid.Column="0" Grid.Row="0">Title:</TextBlock>
...as before
Figure 2-19 shows
the results. Note that the order in which the elements appear in the markup is
crucial, as it determines the Z order for overlapping elements. In
Example 2-13, the Rectangle elements were added before the TextBlock
items whose cells they share. This means that the colored rectangles appear
behind the text, rather than obscuring them. If the rectangles had been added
at the end of the Grid after the text, they would have been drawn over
the text.
This example illustrates why the Grid requires the row
and column of each item to be specified explicitly, rather than being implied
by the order of the elements. Cells can be shared by multiple elements.
Elements can span multiple cells. This makes it impossible for the Grid
to guess which element goes in which cell.
2.4.3. Consistency Across Multiple Grids
Although the row and column spanning features described in the
previous section often make it possible to arrange your UI as you need, it will
not always be possible to put all of the information you wish to present into a
single Grid element. For example, consider a scrollable Grid with
headings. You could just put headings and contents into a single Grid,
and then place that Grid in a ScrollViewer to make it
scrollable, but this suffers from a problem, which
Example 2-14 illustrates.
Example 2-14. Grid in ScrollViewer
<ScrollViewer>
<Grid>
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="5,3" />
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Grid.Column="0" Grid.Row="0"
Background="LightGray" BorderBrush="Gray"
BorderThickness="1">
<TextBlock>Title</TextBlock>
</Border>
<Border Grid.Column="1" Grid.Row="0"
Background="LightGray" BorderBrush="Gray"
BorderThickness="1">
<TextBlock>Location</TextBlock>
</Border>
<Border Grid.Column="2" Grid.Row="0" Background="LightGray"
BorderBrush="Gray" BorderThickness="1">
<TextBlock>Rank</TextBlock>
</Border>
<TextBlock Grid.Column="0" Grid.Row="1">
Mastering Visual Studio .NET
</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="1">O'Reilly Media, Inc.</TextBlock>
<TextBlock Grid.Column="2" Grid.Row="1">1</TextBlock>
<TextBlock Grid.Column="0" Grid.Row="2">IanG on Tap</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="2">The Internet</TextBlock>
<TextBlock Grid.Column="2" Grid.Row="2">2</TextBlock>
</Grid>
</ScrollViewer>
Figure 2-20 shows
the results. If you look at the right-hand side you can see that the scrollbar
runs the entire height of the Grid, including the header line with the
titles. This means that as soon as you scroll down, the headings will
disappear. This is not particularly helpful.
We could solve this by using two grids, one for the header, and
one for the main results area. Only the second grid would be placed inside a ScrollViewer.
Figure 2-21 shows the
results.
The scrollbar is now applied just to the part that needs to be
scrollable, but the alignment is all wrong. Each Grid has arranged its
columns independently, so the headings no longer line up with the main
contents.
The Grid supports shared-size groups to solve this
problem. A shared-size group is simply a named group of columns that
will all have the same width, even though they are in different grids. We can
use this approach to keep the headings Grid consistent with the
scrollable contents Grid, as shown in
Example 2-15.
Example 2-15. Shared-size groups
<DockPanel Grid.IsSharedSizeScope="True">
<DockPanel.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="5,3" />
</Style>
</DockPanel.Resources>
<Grid DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Location" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Rank" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1"
Background="LightGray" BorderBrush="Gray">
<TextBlock>Title</TextBlock>
</Border>
<Border Grid.Column="1" Grid.Row="0" BorderThickness="1"
Background="LightGray" BorderBrush="Gray">
<TextBlock>Location</TextBlock>
</Border>
<Border Grid.Column="2" Grid.Row="0" BorderThickness="1"
Grid.ColumnSpan="2"
Background="LightGray" BorderBrush="Gray">
<TextBlock>Rank</TextBlock>
</Border>
<FrameworkElement Grid.Column="3"
Width="{DynamicResource {x:Static SystemParameters.ScrollWidthKey}}" />
</Grid>
<ScrollViewer>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Location" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Rank" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0">
Mastering Visual Studio .NET
</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="0">
O'Reilly Media, Inc.
</TextBlock>
<TextBlock Grid.Column="2" Grid.Row="0">1</TextBlock>
<TextBlock Grid.Column="0" Grid.Row="1">IanG on Tap</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="1">The Internet</TextBlock>
<TextBlock Grid.Column="2" Grid.Row="1">2</TextBlock>
</Grid>
</ScrollViewer>
</DockPanel>
Example 2-15 illustrates
the use of shared-size groups . The overall layout
is defined by a DockPanel, using the attached Dock.Top property
to position the header Grid at the top, and allowing the ScrollViewer
to fill the remaining space.
Shared-size groups are identified by strings. Strings are prone
to name collisionsit's quite possible that two developers independently working
on different parts of the user interface might end up choosing the same name
for their shared-size groups, inadvertently causing unrelated columns to have
the same size. To avoid this problem, WPF requires that we indicate the scope
within which we wish to use a particular shared-size group. This is the purpose
of the Grid.IsSharedSizeScope attached property on the DockPanelit
indicates that the DockPanel is the common ancestor, and prevents the
groups defined inside the DockPanel from being associated with any
groups defined elsewhere in the UI.
Having defined the scope of the names, using shared-size groups
is very straightforward. We just apply the SharedSizeGroup attribute
to the "Location" and "Rank" ColumnDefinition, ensuring that
the columns are sized consistently across the two grids.
Figure 2-22 shows the results.
The ScrollViewer adds a scrollbar to the display, and
this meant that a small hack was required to get this working on the
pre-release build of WPF that was current at the time of writing. This
scrollbar takes away some space from the main Grid, making it slightly
narrower than the header Grid. Remember that the "Title" column's
size is set to "*" meaning that it should fill all available space.
The ScrollViewer's scrollbar eats into this space, making the "Title"
column in the main Grid slightly narrower than the one in the header Grid,
destroying the alignment.
You might think that we could fix this by adding a shared-size
group for the title column. Unfortunately, specifying a shared-size group seems
to disable the "*" behaviorthe column reverts to automatic sizing.
The fix for this is to add an extra column to the header row.
This row needs to be exactly the same width as the scrollbar added by the ScrollViewer.
So we have added a fourth column, containing a FrameworkElement, with
its Width set to the system scroll-width metric in order to make sure
that it is exactly the same width as a scrollbar. (We are using a DynamicResource
reference to retrieve this system parameter. This technique is described in
Chapter 6.) It's unusual to use a FrameworkElement directly,
but since we just need something that takes up space but has no appearance, it
makes a good lightweight filler. Its presence keeps all of the columns
perfectly aligned across the two grids. (A more elegant solution will no doubt
be possible in the final release of WPF.)
 |
The Grid is the most powerful of the
built-in panels. You can get the Grid to do anything that DockPanel
and StackPanel can dothose simpler elements are provided for
convenience. For nontrivial user interfaces, the Grid is likely to be
the best choice for your top-level GUI layout, as well as being useful for
detailed internal layout.
|
|
 |