- Windows Presentation Foundation 4.5 Cookbook
- Pavel Yosifovich
- 1314字
- 2021-08-05 18:54:44
Creating a table-like user interface
Table layout is a popular placement strategy, supported by the Grid
panel. Let's examine the Grid
and see what it's capable of.
Getting ready
Make sure Visual Studio is up and running.
How to do it...
We'll create a simple UI that benefits from a grid-like layout and demonstrate some of its features:
- Create a new WPF application named
CH03.GridDemo
. - Open
MainWindow.xaml
. There's already aGrid
placed inside theWindow
. That's because theGrid
is typically used as the main layout panel within a window. - Change the
Title
ofWindow
toGrid Demo
. - Inside the
Grid
, add the following markup to create some rows and columns:<Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition /> </Grid.ColumnDefinitions>
- This creates a 4 rows by 2 columns
Grid
. Now let's add some elements and controls to host the grid:<TextBlock Grid.ColumnSpan="2" HorizontalAlignment="Center" Text="Book Details" FontSize="20" Margin="4"/> <TextBlock Grid.Row="1" HorizontalAlignment="Right" Text="Name:" Margin="4" /> <TextBlock Grid.Row="2" HorizontalAlignment="Right" Text="Author:" Margin="4" /> <TextBlock Grid.Row="1" Grid.Column="1" Text="Windows internals" Margin="4" /> <TextBlock Grid.Row="2" Grid.Column="1" Text="Mark Russinovich" Margin="4" /> <Rectangle Grid.Column="1" Grid.Row="3" Margin="4" StrokeThickness="4" Stroke="Black" Fill="Red" /> <TextBlock Grid.Column="1" Grid.Row="3" Text="Book Cover" VerticalAlignment="Center" FontSize="16" HorizontalAlignment="Center"/>
- Run the application. It should look as follows:
- Resize the window and watch the layout changes. Note that the
Grid
rows marked with aHeight
ofAuto
remain fixed in size, while the row that has noHeight
setting fills the remaining space (it is the same idea for the columns):
How it works...
The Grid
panel creates a table-like layout of cells. The number of rows and columns is not specified by simple properties. Instead, it's specified using RowDefinition
objects (for rows) and ColumnDefinition
objects (for columns). The reason has to do with the size and behavior that can be specified on a row and/or column basis.
A RowDefinition
has a Height
property, while a ColumnDefintion
has a Width
property. Both are of type GridLength
. There are three options for setting a GridLength
:
- A specific length
- A "star" (relative) based factor (this is the default, and factor equals 1)
- Automatic length
Setting Height
(of a RowDefintion
) or Width
(of a ColumnDefinition
) to a specific number makes that row/column that particular size. In code it's equivalent to new GridLength(len)
.
Setting Height
or Width
to Auto
(in XAML) makes the row/column as high/wide as it needs to be based on the tallest/widest element placed within that row/column. In code, it's equivalent to GridLength.Auto
.
The last option (which is the default) is setting Height
/Width
to n*
in XAML, where n is some number (1 if omitted). This sets up a relationship with other rows/columns that have a "star" length. For example, here are three rows of a Grid
:
<RowDefinition Height="2*" /> <RowDefinition /> <RowDefinition Height="3*" />
This means that the first row is twice as tall as the second row (Height="*"
). The last row is three times taller than the second row and is one and a half times taller than the first row. These relations are maintained even if the window is resized.
There's more...
Elements are placed in grid cells using the attached Grid.Row
and Grid.Column
properties (both default to zero, meaning the first row/column).
Elements occupy one cell by default. This can be changed by using the Grid.RowSpan
and Grid.ColumnSpan
properties (these were set for the first TextBlock
in the code). If we need an element to stretch to the end of the Grid
(for example, through all columns from some starting column), it's ok to specify some large number (it will be constrained to the actual number of rows/columns of the Grid
).
There may be times when more than one grid should have the same row height (or column width). This is possible using the SharedSizeGroup
property of DefinitionBase
(the base class of RowDefinition
and ColumnDefinition
). This is just a string; if RowDefinition
or ColumnDefinition
objects from two separate grids have the same value for this property, they would maintain identical length. To make this work, the attached property Grid.IsSharedSizeScope
must be set to true
on a common parent of those Grid
instances.
This feature is most useful with DataTemplate
properties (which are discussed in detail in Chapter 6, Data Binding) when binding to an ItemsControl
control (or one of its derivatives).
Here's a quick example: A ListBox
defines a DataTemplate
for its items (ItemTemplate
property) for displaying information on Person
objects, defined as follows:
class Person { public string Name { get; set; } public int Age { get; set; } }
The ListBox
is defined with the following markup:
<ListBox ItemsSource="{Binding}" Grid.IsSharedSizeScope="True"> <ListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" SharedSizeGroup="abc" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBlock Text="{Binding Name}" FontSize="20" Margin="4"/> <TextBlock Grid.Column="1" FontSize="16" Text="{Binding Age, StringFormat=is {0} years old}" VerticalAlignment="Bottom" Margin="4"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
Note the attached property Grid.IsSharedSizeScope
is set to true
on a common parent of all involved Grid
instances (the ListBox
is an obvious choice) and the SharedSizeGroup
property of RowDefinition
is set to some string ("abc"). The string itself does not matter – what matters is that it's the same string for all relevant Grid
instances. Since this is part of a DataTemplate
, Grid
instances are created for every item in the ListBox
. Here's the result when binding to some collection of Person
objects:
Here's what happens if one of the above properties is missing (note the misalignments):
The complete project is named CH03.SharedGridSizeDemo
and is available in the downloadable source for this chapter.
If more than one element is placed in the same cell, they sit one on top of the other – by default, elements appearing later in the XAML are on top. We can change that by using the attached Panel.ZIndex
property (default is zero). Higher values make the element on top (negative values are accepted).
The Grid
is so flexible, that it can emulate most of the standard WPF panels, all except the WrapPanel
(which is too chaotic for the Grid
), namely the StackPanel
, Canvas
, and DockPanel
. This is why Visual Studio chooses the Grid
as the default root layout panel when a new window is generated.
If the Grid
can do almost anything, why use other panels at all? There are two reasons. The first is convenience: although a Grid
can emulate a StackPanel
, that's cumbersome and leads to increased markup with no real gains. The second reason is performance: the StackPanel
(for instance) has very little to worry about, while the Grid
has a lot. For a layout consisting of many elements the complexity of Grid
may degrade performance, so it's usually best to use the lightest panel possible that gets the job done.
Although Grid
instances are typically constructed in XAML and have a fixed number of columns and rows, that doesn't have to be the case. It's possible to add (or remove) rows/columns at runtime if so desired. Here's an example that adds three rows to an existing Grid
named _grid
:
RowDefinition[] rows = { new RowDefinition { Height = new GridLength(100) }, new RowDefinition { Height = GridLength.Auto }, new RowDefinition { Height = new GridLength(2, GridUnitType.Star) } }; Array.ForEach(rows, row => _grid.RowDefinitions.Add(row));
The first row has a fixed height of 100
, the second is auto-sized, and the third is a star
row with a factor of 2
.
There is a simpler grid in WPF, the UniformGrid
(in the System.Windows.Controls.Primitives
namespace). This grid has two properties to set the grid size: Rows
and Columns
(both default to 1). Every cell in a UniformGrid
is, well, uniform (same width and height). Every new element is placed in the next cell starting from the top left, moving left to right, and then down to the next row, starting from the left again.
The UniformGrid
may be used as a simple shortcut for a full-blown grid (it's more lightweight), if the need arises. A more creative use of the UniformGrid
is a custom panel hosting elements in an ItemsControl
.
Here's a quick example: A ListBox
holds a collection of numbers, displayed by default in a row layout (a VirtualizingStackPanel
, similar to a StackPanel
for our purposes, is the default panel for laying out items in an ListBox
). Let's change that to a UniformGrid
. Here's the complete ListBox
markup:
<ListBox ItemsSource="{Binding}" FontSize="25" SelectionMode="Multiple"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <UniformGrid Rows="4" Columns="4" /> </ItemsPanelTemplate> </ListBox.ItemsPanel> </ListBox>
Here's the ListBox
at runtime when bound to a list of numbers:
Despite appearances, this is still a regular ListBox
! The complete source is in the CH03.UniformGridLayout
project available in the downloadable source for this chapter.