Chapter 6. The Dock and the Grid


A traditional Windows program has a fairly standard layout. An application's menu almost always sits at the top of the main window's client area and extends to the full width of the window. The menu is said to be docked at the top of the client area. If the program has a toolbar, that too is docked at the top of the client area, but obviously only one control can be docked at the very edge. If the program has a status bar, it is docked at the bottom of the client area.

A program such as Windows Explorer displays a directory tree in a control docked at the left side of the client area. But the menu, toolbar, and status bar all have priority over the tree-view control because they extend to the full width of the client area while the tree-view control extends vertically only in the space left over by the other controls.

The Windows Presentation Foundation includes a DockPanel class to accommodate your docking needs. You create a DockPanel like so:

DockPanel dock = new DockPanel(); 


If you're creating this DockPanel in the constructor of a Window object, you'll probably set it to the window's Content property:

Content = dock; 


It is fairly common for window layout to begin with DockPanel and then (if necessary) for other types of panels to be children of the DockPanel. You add a particular control (named, perhaps, ctrl) or other element to the DockPanel using the same syntax as with other panels:

dock.Children.Add(ctrl); 


But now it gets a little strange, for you must indicate on which side of the DockPanel you want ctrl docked. To dock ctrl on the right side of the client area, for example, the code is:

DockPanel.SetDock(ctrl, Dock.Right); 


Don't misread this statement: it does not refer at all to the DockPanel object you've just created named dock. Instead, SetDock is a static method of the DockPanel class. The two arguments are the control (or element) you're docking, and a member of the Dock enumeration, either Dock.Left, Dock.Top, Dock.Right, or Dock.Bottom.

It doesn't matter if you call this DockPanel.SetDock method before or after you add the control to the Children collection of the DockPanel object. In fact, you can call DockPanel.SetDock for a particular control even if you've never created a DockPanel object and have no intention of ever doing so!

This strange SetDock call makes use of an attached property, which is something I'll discuss in more detail in Chapter 8. For now, you can perhaps get a better grasp of what's going on by knowing that the static SetDock call above is equivalent to the following code:

ctrl.SetValue(DockPanel.DockProperty, Dock.Right); 


The SetValue method is defined by the DependencyObject class (from which much of the Windows Presentation Foundation descends) and DockPanel.DockProperty is a static read-only field. This is the attached property, and this attached property and its setting (Dock.Right) are actually stored by the control. When performing layout, the DockPanel object can obtain the Dock setting of the control by calling:

Dock dck = DockPanel.GetDock(ctrl); 


which is actually equivalent to:

Dock dck = (Dock) ctrl.GetValue(DockPanel.DockProperty); 


The following program creates a DockPanel and 17 buttons as children of this DockPanel.

DockAroundTheBlock.cs

[View full width]

//--------------------------------------------------- // DockAroundTheBlock.cs (c) 2006 by Charles Petzold //--------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.DockAroundTheBlock { class DockAroundTheBlock : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new DockAroundTheBlock()); } public DockAroundTheBlock() { Title = "Dock Around the Block"; DockPanel dock = new DockPanel(); Content = dock; for (int i = 0; i < 17; i++) { Button btn = new Button(); btn.Content = "Button No. " + (i + 1); dock.Children.Add(btn); btn.SetValue(DockPanel .DockProperty, (Dock)(i % 4)); } } } }



In this program, the following statement

DockPanel.SetDock(btn, (Dock)(i % 4)); 


cleverly assigns each button a member of the Dock enumeration in a cycle: Dock.Left, Dock.Top, Dock.Right, and Dock.Bottom, which have the numeric values 0, 1, 2, and 3. You can easily see how the controls added earliest have priority in hugging the side of the DockPanel over those added latest. Each button has a content-appropriate size in one direction and is stretched in the other direction.

The control added last (which displays the string "Button No. 17") is not docked at all but occupies the leftover interior space. This behavior is by virtue of the default true setting of the DockPanel property LastChildFill. Although the program calls DockPanel.SetDock for that last button, the value is ignored. Try adding the statement

dock.LastChildFill = false; 


any time after the DockPanel is created to see the last button docked as well and the leftover space unfilled.

So, when using a DockPanel, you work from the outside in. The first children get priority against the edge of the parent; the subsequent children are further toward the center.

Children of the DockPanel normally expand to fill the full width (or height) of the panel because the HorizontalAlignment and VerticalAlignment properties of the children are both Stretch by default. Try inserting the following statement to see something strange:

btn.HorizontalAlignment = HorizontalAlignment.Center; 


The buttons docked on the top and bottom (as well as the last button in the center) are all reduced in width. Obviously, this is not standard user interface practice. Neither is setting a Margin property on docked controls.

Here's a program that uses a DockPanel in a more conventional way. It creates a Menu, ToolBar, StatusBar, ListBox, and TextBox controls, but because these controls are covered in more detail in later chapters, they remain mere skeletons of their potential. The TextBox is the last child, so it fills the space not required by the other controls.

MeetTheDockers.cs

[View full width]

//----------------------------------------------- // MeetTheDockers.cs (c) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; namespace Petzold.MeetTheDockers { public class MeetTheDockers : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new MeetTheDockers()); } public MeetTheDockers() { Title = "Meet the Dockers"; DockPanel dock = new DockPanel(); Content = dock; // Create menu. Menu menu = new Menu(); MenuItem item = new MenuItem(); item.Header = "Menu"; menu.Items.Add(item); // Dock menu at top of panel. DockPanel.SetDock(menu, Dock.Top); dock.Children.Add(menu); // Create tool bar. ToolBar tool = new ToolBar(); tool.Header = "Toolbar"; // Dock tool bar at top of panel. DockPanel.SetDock(tool, Dock.Top); dock.Children.Add(tool); // Create status bar. StatusBar status = new StatusBar(); StatusBarItem statitem = new StatusBarItem(); statitem.Content = "Status"; status.Items.Add(statitem); // Dock status bar at bottom of panel. DockPanel.SetDock(status, Dock.Bottom); dock.Children.Add(status); // Create list box. ListBox lstbox = new ListBox(); lstbox.Items.Add("List Box Item"); // Dock list box at left of panel. DockPanel.SetDock(lstbox, Dock.Left); dock.Children.Add(lstbox); // Create text box. TextBox txtbox = new TextBox(); txtbox.AcceptsReturn = true; // Add text box to panel & give it input focus. dock.Children.Add(txtbox); txtbox.Focus(); } } }



This program is woefully incomplete, of course, because none of these controls actually does anything, but you may also feel that it's incomplete in a less obvious way. There should really be a splitter between the ListBox and the TextBox so that you can allocate the space between these two controls.

A splitter is implemented in the Grid panel, but that's not the only reason to use a Grid. The Grid panel displays its children in a collection of cells organized as rows and columns. It may at first seem possible to achieve rows and columns of controls by using a technique similar to the StackThirtyButtons program in the previous chapter. However, often when you arrange controls in rows and columns you want them to line up both horizontally and vertically. Each row of three buttons in StackThirtyButtons would not have lined up correctly if the buttons were of various sizes.

The Grid is perhaps the most useful layout panel, but it is also the most complex. Although it's possible to make Grid your single, all-purpose layout solution, you should probably be more flexible. The Windows Presentation Foundation supports different types of panels for a reason.

With that in mind, however, you start by creating an object of type Grid:

Grid grid = new Grid(); 


When experimenting with the Grid, you may want to display lines between the cells:

grid.ShowGridLines = true; 


Setting this property to true causes dotted lines to appear between the cells. There is no way to change the style or color or width of these lines. If you want lots of control over the lines between your rows and columns in a way that might be suitable in a printed document, you probably want to use a Table rather a Grid. The Table (defined in the System.Windows.Documents namespace) is more like a table you might find in HTML or a word processor. The Grid is strictly for layout.

You need to tell the Grid how many rows and columns you want, and how these rows and columns should be sized. The height of each row can be one of the following options:

  • Fixed in device-independent units

  • Based on the tallest child of that row

  • Based on leftover space (perhaps apportioned among other rows)

Similarly, the width of each column can also be one of these three options. These three options correspond to three members of the GridUnitType enumeration: Pixel, Auto, and Star. (The term star comes from the use of an asterisk in HTML tables to apportion leftover space.)

You specify the height of rows and the width of columns using a class named GridLength. Two constructors are available. If you just specify a value:

new GridLength(100) 


the value is assumed to be a dimension in device-independent units. That constructor is equivalent to:

new GridLength(100, GridUnitType.Pixel) 


You can also specify a value with the enumeration member GridUnitType.Star:

new GridLength(50, GridUnitType.Star) 


The value you specify is used for apportioning leftover space, but the numeric value has meaning only when taken in combination with other rows or columns that use GridUnitType.Star. You can also specify GridUnitType.Auto in a constructor:

new GridLength(0, GridUnitType.Auto) 


However, in this case, the numeric value is ignored, and you can alternatively use the static property

GridLength.Auto 


which returns an object of type GridLength.

Grid has two properties named RowDefinitions and ColumnDefinitions. These properties are of type RowDefinitionCollection and ColumnDefinitionCollection, which are collections of RowDefinition and ColumnDefinition objects.

You create a RowDefinition object for every row of the Grid. The crucial property is Height, which you set to a GridLength object. (RowDefinition also has MinHeight and MaxHeight properties that you can use to constrain the height, but these are optional.) The crucial property of ColumnDefinition is Width, which you also set to a GridLength object. ColumnDefinition also includes MinWidth and MaxWidth properties.

Unfortunately, the code to set the RowDefinitions and ColumnDefinitions collections is rather wordy. This code sets four rows:

RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); rowdef = new RowDefinition(); rowdef.Height = new GridLength(33, GridUnitType.Star); grid.RowDefinitions.Add(rowdef); rowdef = new RowDefinition(); rowdef.Height = new GridLength(150); grid.RowDefinitions.Add(rowdef); rowdef = new RowDefinition(); rowdef.Height = new GridLength(67, GridUnitType.Star); grid.RowDefinitions.Add(rowdef); 


The height of the first row is based on the tallest element in that row. The height of the third row is 150 device-independent units. Depending on the height of the Grid itself, there may be some space left over. If so, that space is apportioned 33 percent to the second row and 67 percent for the forth row. Those values don't have to be specified in percentages, although the code is certainly clearer when they are. The Grid control simply adds up all the GridUnitType.Star values, and then divides each value by that sum to determine the allocation of space.

Column definitions are similar, except that you set the Width property. With any luck, you may be able to put some of them in a for loop.

The default Height and Width properties are GridUnitType.Star with a value of 1, so if you want to distribute space among the rows and columns equally, you can use these defaults. But you still need to add RowDefinition and ColumnDefinition objects to the collections for every row and column you want. The code reduces to statements like this:

grid.RowDefinitions.Add(new RowDefinition()); 


If you need only a single-column Grid, you don't need to add a ColumnDefinition. You won't need a RowDefinition for a single-row Grid, and you won't need either for a single-cell Grid.

You add a control to the Grid just as with any other panel:

grid.Children.Add(ctrl); 


You then need to specify a zero-based row and column where this control is to appear by using the static SetRow and SetColumn methods:

Grid.SetRow(ctrl, 2); Grid.SetColumn(ctrl, 5); 


That's the third row and the sixth column (the indices are zero-based). As with DockPanel.SetDock, these calls make use of attached properties. The defaults are rows and columns of 0. Multiple objects can go into a single cell, but often you won't see them all. If you don't see something you know you put in the grid, it's likely it received the same row and column as something else.

The HorizontalAlignment and VerticalAlignment properties of the element affect how the element is positioned within the cell. In general, if the cell is not sized to the element, the element will be sized to the cell.

The following program creates a Grid that occupies the entire client area. The window sizes itself to the size of the Grid and suppresses the window sizing border. The program gives the Grid three rows and two columns, all using GridLength.Auto. In the cells of this Grid the program positions four Label controls and two TextBox controls, into which you can enter dates. The program calculates a difference between the two dates.

I originally wrote a Win32 version of this program for a friend who was researching family genealogy and needed to calculate the years, months, and days between birth dates and death dates. I was able to use the DATETIMEPICK_CLASS for that program. Unfortunately, the initial release of the Windows Presentation Foundation has no equivalent control, so this program uses the static DateTime.TryParse method to convert text strings into a date. I also changed the names and labels when I realized most readers of this book will probably use the program to determine how long they've lived so far.

CalculateYourLife.cs

[View full width]

//-------------------------------------------------- // CalculateYourLife.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.CalculateYourLife { class CalculateYourLife : Window { TextBox txtboxBegin, txtboxEnd; Label lblLifeYears; [STAThread] public static void Main() { Application app = new Application(); app.Run(new CalculateYourLife()); } public CalculateYourLife() { Title = "Calculate Your Life"; SizeToContent = SizeToContent .WidthAndHeight; ResizeMode = ResizeMode.CanMinimize; // Create the grid. Grid grid = new Grid(); Content = grid; // Row and column definitions. for (int i = 0; i < 3; i++) { RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); } for (int i = 0; i < 2; i++) { ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = GridLength.Auto; grid.ColumnDefinitions.Add(coldef); } // First label. Label lbl = new Label(); lbl.Content = "Begin Date: "; grid.Children.Add(lbl); Grid.SetRow(lbl, 0); Grid.SetColumn(lbl, 0); // First TextBox. txtboxBegin = new TextBox(); txtboxBegin.Text = new DateTime(1980, 1, 1).ToShortDateString(); txtboxBegin.TextChanged += TextBoxOnTextChanged; grid.Children.Add(txtboxBegin); Grid.SetRow(txtboxBegin, 0); Grid.SetColumn(txtboxBegin, 1); // Second label. lbl = new Label(); lbl.Content = "End Date: "; grid.Children.Add(lbl); Grid.SetRow(lbl, 1); Grid.SetColumn(lbl, 0); // Second TextBox. txtboxEnd = new TextBox(); txtboxEnd.TextChanged += TextBoxOnTextChanged; grid.Children.Add(txtboxEnd); Grid.SetRow(txtboxEnd, 1); Grid.SetColumn(txtboxEnd, 1); // Third label. lbl = new Label(); lbl.Content = "Life Years: "; grid.Children.Add(lbl); Grid.SetRow(lbl, 2); Grid.SetColumn(lbl, 0); // Label for calculated result. lblLifeYears = new Label(); grid.Children.Add(lblLifeYears); Grid.SetRow(lblLifeYears, 2); Grid.SetColumn(lblLifeYears, 1); // Set margin for everybody. Thickness thick = new Thickness(5); // ~1/20 inch. grid.Margin = thick; foreach (Control ctrl in grid.Children) ctrl.Margin = thick; // Set focus and trigger the event handler. txtboxBegin.Focus(); txtboxEnd.Text = DateTime.Now .ToShortDateString(); } void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { DateTime dtBeg, dtEnd; if (DateTime.TryParse(txtboxBegin.Text , out dtBeg) && DateTime.TryParse(txtboxEnd.Text, out dtEnd)) { int iYears = dtEnd.Year - dtBeg.Year; int iMonths = dtEnd.Month - dtBeg .Month; int iDays = dtEnd.Day - dtBeg.Day; if (iDays < 0) { iDays += DateTime.DaysInMonth (dtEnd.Year, 1 + (dtEnd.Month + 10) % 12); iMonths -= 1; } if (iMonths < 0) { iMonths += 12; iYears -= 1; } lblLifeYears.Content = String.Format("{0} year{1}, {2 } month{3}, {4} day{5}", iYears, iYears == 1 ? "" : "s", iMonths, iMonths == 1 ? "" : "s", iDays, iDays == 1 ? "" : "s"); } else { lblLifeYears.Content = ""; } } } }



The program installs event handlers for the TextChanged events of the two TextBox controls. As the name suggests, this event is fired whenever the text changes. After converting the text strings to DateTime objects, the event handler calculates the difference between the two dates. Normally, if you subtract one DateTime object from another, you'll get a TimeSpan object, but that wasn't quite adequate. I wanted the program to calculate the difference between two dates much like a person would. For example, if the start date is the fifth day of the month, and the end date is the twentieth day of the same month or a different month, I wanted the difference to be 15 days.

If the start date is the twentieth day of the month (May, for example), and the end date is the fifth day of a month (such as September), I wanted the program to tally three full months from May 20 to August 20, and then a number of days from August 20 to September 5. That last calculation required taking into account the number of days in the month preceding the end date. The static DaysInMonth method of DateTime and some modulo arithmetic did the trick.

Let's look at another layout example using the Grid. Here's a layout that might be found in a data-entry form. The program creates two Grid panels as children of a StackPanel. The first Grid panel has two columns and five rows for labels and TextBox controls. The second Grid has one row and two columns for the OK and Cancel buttons.

EnterTheGrid.cs

[View full width]

//--------------------------------------------- // EnterTheGrid.cs (c) 2006 by Charles Petzold //--------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.EnterTheGrid { public class EnterTheGrid : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new EnterTheGrid()); } public EnterTheGrid() { Title = "Enter the Grid"; MinWidth = 300; SizeToContent = SizeToContent .WidthAndHeight; // Create StackPanel for window content. StackPanel stack = new StackPanel(); Content = stack; // Create Grid and add to StackPanel. Grid grid1 = new Grid(); grid1.Margin = new Thickness(5); stack.Children.Add(grid1); // Set row definitions. for (int i = 0; i < 5; i++) { RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid1.RowDefinitions.Add(rowdef); } // Set column definitions. ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = GridLength.Auto; grid1.ColumnDefinitions.Add(coldef); coldef = new ColumnDefinition(); coldef.Width = new GridLength(100, GridUnitType.Star); grid1.ColumnDefinitions.Add(coldef); // Create labels and text boxes. string[] strLabels = { "_First name:", "_Last name:", "_Social security number:", "_Credit card number:", "_Other personal stuff:" }; for(int i = 0; i < strLabels.Length; i++) { Label lbl = new Label(); lbl.Content = strLabels[i]; lbl.VerticalContentAlignment = VerticalAlignment.Center; grid1.Children.Add(lbl); Grid.SetRow(lbl, i); Grid.SetColumn(lbl, 0); TextBox txtbox = new TextBox(); txtbox.Margin = new Thickness(5); grid1.Children.Add(txtbox); Grid.SetRow(txtbox, i); Grid.SetColumn(txtbox, 1); } // Create second Grid and add to StackPanel. Grid grid2 = new Grid(); grid2.Margin = new Thickness(10); stack.Children.Add(grid2); // No row definitions needed for single row. // Default column definitions are "star". grid2.ColumnDefinitions.Add(new ColumnDefinition()); grid2.ColumnDefinitions.Add(new ColumnDefinition()); // Create buttons. Button btn = new Button(); btn.Content = "Submit"; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.IsDefault = true; btn.Click += delegate { Close(); }; grid2.Children.Add(btn); // Row & column are 0. btn = new Button(); btn.Content = "Cancel"; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.IsCancel = true; btn.Click += delegate { Close(); }; grid2.Children.Add(btn); Grid.SetColumn(btn, 1); // Row is 0. // Set focus to first text box. (stack.Children[0] as Panel) .Children[1].Focus(); } } }



This program relies on the default behavior of elements to fill their parents. The StackPanel occupies the full width of the window, as do the two Grid panels. In the top Grid panel, the first column (containing the labels) has a width of GridLength.Auto while the second column (with the TextBox controls) has a width of GridUnitType.Star, which means that they occupy all the remaining available space. The result: When you make the window wider, the TextBox controls expand in width. The TextBox controls (and the window) also expand when you type text in the control that exceeds its initial width. The window is given a minimum width of 300 units to prevent the TextBox controls from looking a little too small when the program starts up.

The height of the first five rows is based on the height of the TextBox controls. To force the Label controls to line up attractively with each TextBox, the program sets the VerticalContentAlignment property of each Label to Center.

The program could have used a horizontal StackPanel rather than a single-row Grid for the OK and Cancel buttons, but notice that the column widths both are GridUnitType.Star, so the buttons maintain the same relative spacing as the window becomes wider.

It is possible for an element in a Grid to occupy multiple adjacent columns or multiple adjacent rows, or both. You specify the number of columns or rows you want an element to occupy through use of the static Grid.SetRowSpan and Grid.SetColumnSpan methods. Here's some sample code:

grid.Children.Add(ctrl); Grid.SetRow(ctrl, 3); Grid.SetRowSpan(ctrl, 2); Grid.SetColumn(ctrl, 5); Grid.SetColumnSpan(ctrl, 3); 


The control named ctrl will occupy the area encompassed by the six cells in zero-based rows 3 and 4, and columns 5, 6, and 7.

The following program has roughly the same layout of controls as EnterTheGrid but uses just one Grid panel with six rows and four columns. The first five rows contain the labels and text boxes; the last row has the buttons. The first column is for the labels. The rightmost two columns are for the buttons. The text boxes span the second, third, and fourth columns.

SpanTheCells.cs

[View full width]

//--------------------------------------------- // SpanTheCells.cs (c) 2006 by Charles Petzold //--------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.SpanTheCells { public class SpanTheCells : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new SpanTheCells()); } public SpanTheCells() { Title = "Span the Cells"; SizeToContent = SizeToContent .WidthAndHeight; // Create Grid. Grid grid = new Grid(); grid.Margin = new Thickness(5); Content = grid; // Set row definitions. for (int i = 0; i < 6; i++) { RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); } // Set column definitions. for (int i = 0; i < 4; i++) { ColumnDefinition coldef = new ColumnDefinition(); if (i == 1) coldef.Width = new GridLength (100, GridUnitType.Star); else coldef.Width = GridLength.Auto; grid.ColumnDefinitions.Add(coldef); } // Create labels and text boxes. string[] astrLabel = { "_First name:", "_Last name:", "_Social security number:", "_Credit card number:", "_Other personal stuff:" }; for(int i = 0; i < astrLabel.Length; i++) { Label lbl = new Label(); lbl.Content = astrLabel[i]; lbl.VerticalContentAlignment = VerticalAlignment.Center; grid.Children.Add(lbl); Grid.SetRow(lbl, i); Grid.SetColumn(lbl, 0); TextBox txtbox = new TextBox(); txtbox.Margin = new Thickness(5); grid.Children.Add(txtbox); Grid.SetRow(txtbox, i); Grid.SetColumn(txtbox, 1); Grid.SetColumnSpan(txtbox, 3); } // Create buttons. Button btn = new Button(); btn.Content = "Submit"; btn.Margin = new Thickness(5); btn.IsDefault = true; btn.Click += delegate { Close(); }; grid.Children.Add(btn); Grid.SetRow(btn, 5); Grid.SetColumn(btn, 2); btn = new Button(); btn.Content = "Cancel"; btn.Margin = new Thickness(5); btn.IsCancel = true; btn.Click += delegate { Close(); }; grid.Children.Add(btn); Grid.SetRow(btn, 5); Grid.SetColumn(btn, 3); // Set focus to first text box. grid.Children[1].Focus(); } } }



Notice that the program gives all six rows heights of GridLength.Auto, and all four columns except the second heights of GridLength.Auto. The second column has a GridLengthType.Star, so it uses all leftover space.

As you make the window wider, the text boxes expand to fill the space because one of the columns they occupy is set for GridLengthType.Star. The two buttons maintain their positions at the far right of the window. Also, the window automatically expands as the user types long text into the text boxes.

The Grid panel also comes to the rescue when you need a horizontal or vertical splitter, which is a thin bar the user can move to apportion space between two areas of the window.

The GridSplitter class derives from Control by way of the Thumb class. The GridSplitter must be a child of a Grid panel, and a program assigns the GridSplitter row and column positions (and optional cell spanning) just like any other child of Grid:

GridSplitter split = new GridSplitter(); split.Width = 6; grid.Children.Add(split); Grid.SetRow(split, 3); Grid.SetColumn(split, 2); 


One oddity, however: The GridSplitter can share a cell with another element, and it's possible that the GridSplitter will partially obscure the element in that cell or the element in that cell will completely obscure the GridSplitter. If you want GridSplitter to share a cell with another element, you can avoid these problems by giving the element in the cell a little margin where the GridSplitter appears, or adding the GridSplitter to the child collection after the element in that cell so that it appears in the foreground.

By default, the GridSplitter has a HorizontalAlignment of Right and a VerticalAlignment of Stretch, which causes the splitter to appear as a vertical bar at the right edge of the cell. The default width of a GridSplitter is 0, so assign the Width property to a small amount (perhaps 1/16th inch or 6 units). You can then move the splitter back and forth to change the width of the column the splitter is in, and the width of the column to the right of the splitter. Here's a small program that creates nine buttons in a 3 x 3 grid, and puts a splitter in the center cell:

SplitNine.cs

[View full width]

//------------------------------------------ // SplitNine.cs (c) 2006 by Charles Petzold //------------------------------------------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.SplitNine { public class SplitNine : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new SplitNine()); } public SplitNine() { Title = "Split Nine"; Grid grid = new Grid(); Content = grid; // Set row and column definitions. for (int i = 0; i < 3; i++) { grid.ColumnDefinitions.Add(new ColumnDefinition()); grid.RowDefinitions.Add(new RowDefinition()); } // Create 9 buttons. for (int x = 0; x < 3; x++) for (int y = 0; y < 3; y++) { Button btn = new Button(); btn.Content = "Row " + y + " and Column " + x; grid.Children.Add(btn); Grid.SetRow(btn, y); Grid.SetColumn(btn, x); } // Create splitter. GridSplitter split = new GridSplitter(); split.Width = 6; grid.Children.Add(split); Grid.SetRow(split, 1); Grid.SetColumn(split, 1); } } }



When you run this program, you'll see the splitter on the right side of the center cell, obscuring the right edge of the button. You can give the buttons a little margin to make the splitter stand out:

btn.Margin = new Thickness(10); 


But that looks a little odd as well.

Of course, even if the splitter appears in just one cell, the column widths in all the rows change as you move the splitter. It would make more sense for the splitter to span the full height of the grid:

Grid.SetRow(split, 0); Grid.SetColumn(split, 1); Grid.SetRowSpan(split, 3); 


But let's keep the splitter in one cell for experimental purposes. The crucial properties for the GridSplitter are HorizontalAlignment and VerticalAlignment. These properties govern whether the splitter is horizontal or vertical, where the splitter appears in the cell, and what rows or columns are affected. With default settings (HorizontalAlignment of Right and VerticalAlignment of Stretch) the GridSplitter sits on the right edge of the cell. Moving the splitter changes the apportionment of width between the column the splitter is in and the column to the right of the splitter.

You can change HorizontalAlignment to Left by inserting the following code:

split.HorizontalAlignment = HorizontalAlignment.Left; 


Now the splitter apportions space between the column that it's in and the column to the left of the splitter column. Try this:

split.HorizontalAlignment = HorizontalAlignment.Center: 


Now the splitter sits in the center of the middle button, and moving it affects not the width of the center column, but the widths of the two columns on either side of the splitter. This may seem like the most bizarre use of a splitter ever, but it's actually key to using GridSplitter sanely. If the cells in that center column were actually empty of buttons, and the width of that cell was set to GridLength.Auto, the splitter would look and act with some degree of normalcy.

You have just seen how the splitter can affect the widths of two different pairs of column depending on the HorizontalAlignment setting of Right (the default), Left, or Center. You can override that behavior with the ResizeBehavior property of GridSplitter. You set this property to a member of the GridResizeBehavior enumeration. The members of this enumeration refer to which columns are affected by the splitter, referring to the column the splitter is located in as current. The members are CurrentAndNext (the behavior normally encountered when the splitter is right-aligned), PreviousAndCurrent (which happens when the splitter is left-aligned), and PreviousAndNext (when the splitter is centered). The fourth member of the GridResizeBehavior enumeration is the default: BasedOnAlignment, which doesn't override the behavior normally associated with the alignment of the splitter.

You can make the splitter horizontal by setting the HorizontalAlignment to Stretch and the VerticalAlignment to Top, Center, or Bottom. These settings cause the splitter to appear at the top of the cell:

split.HorizontalAlignment = HorizontalAlignment.Stretch; split.VerticalAlignment = VerticalAlignment.Top; split.Height = 6; 


As you move the splitter, you apportion space between the row in which the splitter appears and the row above it.

As you've seen, the GridSplitter is vertical if VerticalAlignment is set to Stretch and horizontal if HorizontalAlignment is set to Stretch. A vertical splitter affects column widths; a horizontal splitter affects row heights. You can change this particular association between the appearance of the splitter and its functionality with the ResizeDirection property. You set it to a member of the GridResizeDirection enumeration: either Columns, Rows, or Auto (the default). Try this:

split.HorizontalAlignment = HorizontalAlignment.Stretch; split.VerticalAlignment = VerticalAlignment.Top; split.ResizeDirection = GridResizeDirection.Columns; 


Now the splitter is horizontal but you can't move it up and down. You can only move it back and forth to change the column widths.

The GridSplitter is even more versatile than that. You can set both HorizontalAlignment and VerticalAlignment to Stretch to make the splitter fill the cell. (Don't set Width or Height.) Or, you can avoid using Stretch with either HorizontalAlignment or VerticalAlignment and make the splitter appear as a box whose size is governed by Width and Height.

My recommendations are simple: Don't put a GridSplitter in a cell occupied by another element, and devote an entire Grid to splitting functionality. If you want a vertical splitter, create a Grid with three columns. Give the middle column a Width of GridLength.Auto, and put the splitter in that column. If you want a horizontal splitter, create a Grid with three rows. Give the middle row a Height of GridLength.Auto, and put the splitter in that row. In either case, you can put whatever you want in the two outer columns or rows, including other Grid panels.

Here's a program that creates a three-column Grid that hosts a button, a vertical splitter, and a three-row Grid. The three-row Grid has another button, a horizontal splitter, and a third button.

SplitTheClient.cs

[View full width]

//----------------------------------------------- // SplitTheClient.cs (c) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; class SplitTheClient : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new SplitTheClient()); } public SplitTheClient() { Title = "Split the Client"; // Grid with vertical splitter. Grid grid1 = new Grid(); grid1.ColumnDefinitions.Add(new ColumnDefinition()); grid1.ColumnDefinitions.Add(new ColumnDefinition()); grid1.ColumnDefinitions.Add(new ColumnDefinition()); grid1.ColumnDefinitions[1].Width = GridLength.Auto; Content = grid1; // Button at left of vertical splitter. Button btn = new Button(); btn.Content = "Button No. 1"; grid1.Children.Add(btn); Grid.SetRow(btn, 0); Grid.SetColumn(btn, 0); // Vertical splitter. GridSplitter split = new GridSplitter(); split.ShowsPreview = true; split.HorizontalAlignment = HorizontalAlignment.Center; split.VerticalAlignment = VerticalAlignment.Stretch; split.Width = 6; grid1.Children.Add(split); Grid.SetRow(split, 0); Grid.SetColumn(split, 1); // Grid with horizontal splitter. Grid grid2 = new Grid(); grid2.RowDefinitions.Add(new RowDefinition()); grid2.RowDefinitions.Add(new RowDefinition()); grid2.RowDefinitions.Add(new RowDefinition()); grid2.RowDefinitions[1].Height = GridLength.Auto; grid1.Children.Add(grid2); Grid.SetRow(grid2, 0); Grid.SetColumn(grid2, 2); // Button at top of horizontal splitter. btn = new Button(); btn.Content = "Button No. 2"; grid2.Children.Add(btn); Grid.SetRow(btn, 0); Grid.SetColumn(btn, 0); // Horizontal splitter. split = new GridSplitter(); split.ShowsPreview = true; split.HorizontalAlignment = HorizontalAlignment.Stretch; split.VerticalAlignment = VerticalAlignment.Center; split.Height = 6; grid2.Children.Add(split); Grid.SetRow(split, 1); Grid.SetColumn(split, 0); // Bottom at bottom of horizontal splitter. btn = new Button(); btn.Content = "Button No. 3"; grid2.Children.Add(btn); Grid.SetRow(btn, 2); Grid.SetColumn(btn, 0); } }



This program sets the ShowsPreview property of GridSplitter to true. When you move the splitter, the cells don't actually change in size until you release the mouse button. Notice that you can use the Tab key to give the splitter input focus and move it with the cursor keys. If the response is not to your liking, check out the KeyboardIncrement property.

As you may have noticed, when you changed the size of the SplitNine or SplitTheClient windows, the row heights and column heights changed proportionally. It's possible you may want one row or column to remain fixed in size while the other one changes. (You can see this behavior in Windows Explorer: As you change the size of the window, the left side of the splitter remains fixed in size.) The equal redistribution of space occurs when the rows or columns have a Height or Width of GridLengthType.Star. To keep a row or column fixed as you change the size of the window, use GridLengthType.Pixel or (undoubtedly more commonly) GridLength.Auto.

The next program demonstrates this technique and a few others. I wrote the first version of this program for Windows 1.0 and it appeared in the May 1987 issue of Microsoft Systems Journal. In retrospect, the program was a pioneering study in automatic layout. The original program consisted of six labels and three scrollbars that let you select levels of red, green, and blue primaries to see the result. As you made the program window larger or smaller, the program resized and relocated the labels and scrollbars within the window. Calculations were involved, and ugly ones at that.

The Windows Presentation Foundation ScrollBar derives from Control by way of RangeBase. (RangeBase is an abstract class that is also parent to ProgressBar and Slider.) Scrollbars can be positioned vertically or horizontally based on the setting of the Orientation property. The Value property can range from the value of the Minimum property to the Maximum property. Clicking the arrows on the ends of the scrollbar change Value by the amount specified in the SmallChange property. Clicking the area on the side of the thumbs changes the value by LargeChange. All these numbers are double values.

Let me repeat that: All these properties of the ScrollBar are double values, including the Value property itself, and don't be surprised to see it take on non-integral values. If you need integral values, convert what the ScrollBar hands you.

The ScrollBar class inherits a ValueChanged event from RangeBase and defines a Scroll event as well. The Scroll event is delivered with additional information in the form of a member of the ScrollEventType enumeration that lets you know what part of the scrollbar the user was manipulating. For example, if your program can't keep up with thumb movement, it might ignore all events of type ScrollEventType.ThumbTrack and get a final thumb location in the event of type ScrollEventType.EndScroll.

The ScrollCustomColors program creates two Grid panels. The first (called gridMain) exists solely to implement a vertical splitter. The first cell contains a second Grid panel (simply called grid) that contains the six labels and three scrollbars. The center cell of GridMain contains the GridSplitter and the last cell contains a StackPanel used solely to display its Background color.

This program sets the initial size of the window to 500 device-independent units. When defining the three columns of gridMain, it gives the cell containing the scrollbars and labels a width of 200 units, while the cell containing the StackPanel has a width based on GridUnitType.Star. When you make the window wider or small, only the size of the StackPanel is affected.

ScrollCustomColors.cs

[View full width]

//--------------------------------------------------- // ScrollCustomColors.cs (c) 2006 by Charles Petzold //--------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; class ColorScroll : Window { ScrollBar[] scrolls = new ScrollBar[3]; TextBlock[] txtValue = new TextBlock[3]; Panel pnlColor; [STAThread] public static void Main() { Application app = new Application(); app.Run(new ColorScroll()); } public ColorScroll() { Title = "Color Scroll"; Width = 500; Height = 300; // GridMain contains a vertical splitter. Grid gridMain = new Grid(); Content = gridMain; // GridMain column definitions. ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = new GridLength(200, GridUnitType.Pixel); gridMain.ColumnDefinitions.Add(coldef); coldef = new ColumnDefinition(); coldef.Width = GridLength.Auto; gridMain.ColumnDefinitions.Add(coldef); coldef = new ColumnDefinition(); coldef.Width = new GridLength(100, GridUnitType.Star); gridMain.ColumnDefinitions.Add(coldef); // Vertical splitter. GridSplitter split = new GridSplitter(); split.HorizontalAlignment = HorizontalAlignment.Center; split.VerticalAlignment = VerticalAlignment.Stretch; split.Width = 6; gridMain.Children.Add(split); Grid.SetRow(split, 0); Grid.SetColumn(split, 1); // Panel on right side of splitter to display color. pnlColor = new StackPanel(); pnlColor.Background = new SolidColorBrush (SystemColors.WindowColor); gridMain.Children.Add(pnlColor); Grid.SetRow(pnlColor, 0); Grid.SetColumn(pnlColor, 2); // Secondary grid at left of splitter. Grid grid = new Grid(); gridMain.Children.Add(grid); Grid.SetRow(grid, 0); Grid.SetColumn(grid, 0); // Three rows for label, scroll, and label. RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); rowdef = new RowDefinition(); rowdef.Height = new GridLength(100, GridUnitType.Star); grid.RowDefinitions.Add(rowdef); rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); // Three columns for Red, Green, and Blue. for (int i = 0; i < 3; i++) { coldef = new ColumnDefinition(); coldef.Width = new GridLength(33, GridUnitType.Star); grid.ColumnDefinitions.Add(coldef); } for (int i = 0; i < 3; i++) { Label lbl = new Label(); lbl.Content = new string[] { "Red", "Green", "Blue" }[i]; lbl.HorizontalAlignment = HorizontalAlignment.Center; grid.Children.Add(lbl); Grid.SetRow(lbl, 0); Grid.SetColumn(lbl, i); scrolls[i] = new ScrollBar(); scrolls[i].Focusable = true; scrolls[i].Orientation = Orientation .Vertical; scrolls[i].Minimum = 0; scrolls[i].Maximum = 255; scrolls[i].SmallChange = 1; scrolls[i].LargeChange = 16; scrolls[i].ValueChanged += ScrollOnValueChanged; grid.Children.Add(scrolls[i]); Grid.SetRow(scrolls[i], 1); Grid.SetColumn(scrolls[i], i); txtValue[i] = new TextBlock(); txtValue[i].TextAlignment = TextAlignment.Center; txtValue[i].HorizontalAlignment = HorizontalAlignment.Center; txtValue[i].Margin = new Thickness(5); grid.Children.Add(txtValue[i]); Grid.SetRow(txtValue[i], 2); Grid.SetColumn(txtValue[i], i); } // Initialize scroll bars. Color clr = (pnlColor.Background as SolidColorBrush).Color; scrolls[0].Value = clr.R; scrolls[1].Value = clr.G; scrolls[2].Value = clr.B; // Set initial focus. scrolls[0].Focus(); } void ScrollOnValueChanged(object sender, RoutedEventArgs args) { ScrollBar scroll = sender as ScrollBar; Panel pnl = scroll.Parent as Panel; TextBlock txt = pnl.Children[1 + pnl.Children .IndexOf(scroll)] as TextBlock; txt.Text = String.Format("{0}\n0x{0:X2}", (int)scroll.Value); pnlColor.Background = new SolidColorBrush( Color.FromRgb((byte) scrolls[0].Value, (byte) scrolls[1] .Value,(byte) scrolls[2].Value)); } }



The ValueChanged event handler updates the TextBlock associated with the ScrollBar whose value has changed, and then recalculates a new Color based on the settings of all three scrollbars.

Similar to the Grid panel is the UniformGrid, a grid whose columns are always the same width and rows are always the same height. There are no RowDefinition or ColumnDefinition objects associated with a UniformGrid. Instead, you simply specify the number of rows and columns with the Rows and Columns properties. There are no attached properties, either. As you add children to the UniformGrid, they sequentially occupy the cells in the first row, then the second row, and so forth. One or both of the Rows and Columns properties can be zero, and the UniformGrid will figure out a suitable value from the number of children.

You'll see an example of UniformGrid in the next chapter, where I use it to implement the famous 14-15 puzzle, otherwise known as Jeu de Tacquin.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net