Exclusive Blog at WordPress.com
Speech WordPress Blog

Chapter 5. Stack and Wrap

Controls that derive from the ContentControl class (such as Window, Button, Label, and ToolTip) have a property named Content that you can set to almost any object. Commonly, this object is either a string or an instance of a class that derives from UIElement. The problem is you can set Content to only one object, which may be satisfactory for simple buttons, but is clearly inadequate for a window.

Fortunately, the Windows Presentation Foundation includes several classes designed specifically to alleviate this problem. These are collectively known as panels, and the art and science of putting controls and other elements on the panel is known as layout.

Panels derive from the Panel class. This partial class hierarchy shows the most important derivatives of Panel:

UIElement

    FrameworkElement

         Panel (abstract)

                Canvas

                DockPanel

                Grid

                StackPanel

                UniformGrid

                WrapPanel

The panel is a relatively recent concept in graphical windowing environments. Traditionally, a Windows program populated its windows and dialog boxes with controls by specifying their precise size and location. The Windows Presentation Foundation, however, has a strong commitment to dynamic layout (also known as automatic layout). The panels themselves are responsible for sizing and positioning elements based on different layout models. That's why a variety of classes derive from Panel: Each supports a different type of layout.

Panel defines a property named Children used to store the child elements. The Children property is an object of type UIElementCollection, which is a collection of UIElement objects. Thus, the children of a panel can be Image objects, Shape objects, TextBlock objects, and Control objects, just to mention the most popular candidates. The children of a panel can also include other panels. Just as you use a panel to host multiple elements in a window, you use a panel to host multiple elements in a button or any other ContentControl object.

In this chapter I'll discuss the StackPanel, which arranges child elements in a vertical or horizontal stack, and the WrapPanel, which is similar to the StackPanel except that child elements can wrap to the next column or row.

The next chapter will focus on the DockPanel, which automates the positioning of elements against the inside edges of their parents, and the Grid, which hosts children in a grid of rows and columns. The UniformGrid is similar to the Grid except that all the rows are equal height and all the columns are equal width.

It will then be time to look at the Canvas, which allows you to arrange elements by specifying their precise coordinate locations. Of course, the Canvas panel is closest to traditional layout and consequently is probably used least of these five options.

Although automatic layout is a crucial feature in the Windows Presentation Foundation, you can't use it in a carefree way. Almost always, you'll have to use your own aesthetic sense in tweaking certain properties of the elements, most commonly HorizontalAligment, VerticalAlignment, Margin, and Padding.

The following program sets the Content property of its window to a StackPanel and then creates 10 buttons that become children of the panel.

StackTenButtons.cs

[View full width]//------------------------------------------------ // StackTenButtons.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.StackTenButtons { class StackTenButtons : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new StackTenButtons()); } public StackTenButtons() { Title = "Stack Ten Buttons"; StackPanel stack = new StackPanel(); Content = stack; Random rand = new Random(); for (int i = 0; i < 10; i++) { Button btn = new Button(); btn.Name = ((char)('A' + i)) .ToString(); btn.FontSize += rand.Next(10); btn.Content = "Button " + btn .Name + " says 'Click me'"; btn.Click += ButtonOnClick; stack.Children.Add(btn); } } void ButtonOnClick(object sender, RoutedEventArgs args) { Button btn = args.Source as Button; MessageBox.Show("Button " + btn.Name + " has been clicked", "Button Click"); } } }


Notice that the program creates an object of type Random and then increases the FontSize property of each button with a small random number. Each button is added to the Children collection of the StackPanel with the following statement:

stack.Children.Add(btn);

I've given each button a slightly different FontSize to demonstrate how the StackPanel works with elements of different sizes. When you run the program, you'll see the buttons arranged from the top down in the order they were added to the Children collection. Each button gets a height that is suitable for displaying its content. The width of each button, however, extends to the width of the StackPanel, which itself fills the client area of the window.

When experimenting with this program, you may want to give the StackPanel a non-default Background brush so that you can see exactly how big it is:

stack.Background = Brushes.Aquamarine;

By default, the StackPanel arranges its children vertically. You can alter that behavior by setting the Orientation property:

stack.Orientation = Orientation.Horizontal;

Now you see that each button has a width that reflects its content, but the button heights extend to the full height of the panel. Depending on the width of your monitor size, you may or may not notice that the window will mercilessly truncate the display of some buttons if the window isn't large enough to fit them all.

For the remainder of this experiment, let's go back to a vertical orientation and just keep in mind that the following discussion applies to both orientations, with all references to horizontal and vertical swapped, of course.

It is unlikely that you want buttons extending the full width of the window. You can make them less wide by setting the HorizontalAlignment property of each button in the for loop of the program:

btn.HorizontalAlignment = HorizontalAlignment.Center;

The StackPanel still occupies the full width of the client area, but now each button is made a proper size. Alternatively, you can set the HorizontalAlignment of the StackPanel itself:

stack.HorizontalAlignment = HorizontalAlignment.Center;

Now the StackPanel becomes only large enough to fit the maximum width of the buttons.

The background color of the StackPanel illustrates a subtle difference between setting the horizontal alignment of the buttons and the panel: When you set the HorizontalAlignment of each button to Center, each button is made wide enough to fit its content. When you alternatively set the HorizontalAlignment property of the StackPanel to Center, the panel is made large enough to fit the widest button. Each button still has a HorizontalAlignment property of Stretch, so each button stretches to the width of the StackPanel. Result: All the buttons are the same width.

Perhaps the most satisfying solution is to forgo setting the HorizontalAlignment properties on either the panel or the buttons, and just size the window to the content:

SizeToContent = SizeToContent.WidthAndHeight; ResizeMode = ResizeMode.CanMinimize;

Now the window is exactly the size of its content, which is the size of the StackPanel, which now reflects the width of the widest button. You may like to have all the buttons the same width, or you may want the width of each button to reflect its particular content. That's an aesthetic decision, and you can impose your verdict by the way you set the HorizontalAlignment property of each button.

What you probably do not want, however, is for all the buttons to be jammed up against each other as they've been so far. You'll want to set a margin around each button:

btn.Margin = new Thickness(5);

This is much, much better. Each button now has a margin of five device-independent units (about 1/20 inch) on each side. Still, some sticklers (like me) may not be entirely satisfied. Because each button has about 1/20-inch margin on all sides, the distance between adjacent buttons is 1/10 inch. However, the margin between the buttons and the border of the window is still only 1/20 inch. You can fix that by setting a margin on the panel itself:

stack.Margin = new Thickness(5);

You can now remove the background brush for the StackPanel and you have a nice, attractive (albeit do-nothing) program.

You may have noticed that when giving each Button some Content text, I first assigned the Name property of the Button to the short text string "A", "B", "C", and so forth. You can use these Name strings to later obtain the objects they're associated with by calling the FindName property defined by FrameworkElement. For example, in some event handler or other method in the window class you can have the following code:

Button btn = FindName("E") as Button;

Although you're calling the FindName method of Window, the method will search recursively through the window Content and then the StackPanel.

You can also index the Children property of the panel. For example, the following expression returns the sixth element added to the Children collection:

stack.Children[5]

The UIElementCollection class has several methods that can help you deal with the child elements. If el is an element in the collection, the expression

stack.Children.IndexOf(el)

returns the index of that element in the collection, or 1 if the element is not part of the collection. The UIElementCollection also has methods to insert an element into the location at a particular index and to remove elements from the collection.

Event handlerssuch as ButtonOnClick in StackTenButtonsmust often obtain the object that generated the event. The technique I've been using so far is the traditional .NET approach. The first argument to the event handler (typically named sender) is cast to an object of the correct type:

Button btn = sender as Button;

However, the StackTenButtons program ignores the sender argument and instead uses a property of the RoutedEventArgs object, which is the second argument to the event handler:

Button btn = args.Source as Button;

In this particular program, it doesn't matter which technique is used. The source of the Click event obtained from the Source property of RoutedEventArgs is the same as the object sending the event: the object to which the event handler is attached and which is obtained from the sender argument. When an event handler is attached to the element originating the event, these two values are the same.

However, an alternative event-handling scenario causes these two values to be different. You can try this alternative by commenting out the statement in the for loop that assigns the handler to the Click event of each button and then inserting the following statement at the very end of the constructor outside of the for loop:

stack.AddHandler(Button.ClickEvent, new RoutedEventHandler(ButtonOnClick));

The AddHandler method is defined by UIElement. The first argument must be an object of type RoutedEvent, and it is. ClickEvent is a static read-only field of type RoutedEvent that is defined by ButtonBase and inherited by Button. The second argument indicates the event handler that you want to attach to this event. It must be specified in the form of a constructor of the delegate type of the event handler, in this case RoutedEventHandler.

The call to AddHandler instructs the StackPanel to monitor all its children for events of type Button.ClickEvent, and to use ButtonOnClick as the handler for those events. After you recompile, the program works the same except that slightly different information is coming through the event handler. The Source property of RoutedEventArgs still identifies the Button object, but the sender argument is now the StackPanel object.

You don't need to call AddHandler for the StackPanel. You could instead call AddHandler for the window itself:

AddHandler(Button.ClickEvent, new RoutedEventHandler(ButtonOnClick));

Now the same event handler will apply to all Button objects that are anywhere in the tree that descends from the window. The sender argument to the event handler is now the Window object.

I'll have more to say about routed events in Chapter 9.

Panels can be nested. Here's a program that displays 30 buttons in 3 columns of 10 buttons each. Four StackPanel objects are involved.

StackThirtyButtons.cs [View full width]//--------------------------------------------------- // StackThirtyButtons.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.StackThirtyButtons { class StackThirtyButtons : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new StackThirtyButtons()); } public StackThirtyButtons() { Title = "Stack Thirty Buttons"; SizeToContent = SizeToContent .WidthAndHeight; ResizeMode = ResizeMode.CanMinimize; AddHandler(Button.ClickEvent, new RoutedEventHandler(ButtonOnClick)); StackPanel stackMain = new StackPanel(); stackMain.Orientation = Orientation .Horizontal; stackMain.Margin = new Thickness(5); Content = stackMain; for (int i = 0; i < 3; i++) { StackPanel stackChild = new StackPanel(); stackMain.Children.Add(stackChild); for (int j = 0; j < 10; j++) { Button btn = new Button(); btn.Content = "Button No. " + (10 * i + j + 1); btn.Margin = new Thickness(5); stackChild.Children.Add(btn); } } } void ButtonOnClick(object sender, RoutedEventArgs args) { MessageBox.Show("You clicked the button labeled " + (args.Source as Button).Content); } } }


Notice the AddHandler call near the top of the constructor. All the buttons share the same Click event handler. The constructor creates one StackPanel with a horizontal orientation that fills the client area. This StackPanel is parent to 3 other StackPanel objects, each of which has a vertical orientation and is parent to 10 buttons. The buttons and the first StackPanel are given a Margin of five device-independent units, and the window is sized to fit the result.

Keep adding more and more buttons, and pretty soon you're going to cry, "Yikes, I have so many buttons they won't even fit on my screen." And at that point you need a scroll bar or two.

Rather than a scroll bar, you'd be better off in this instance with the ScrollViewer class. Like Window and ButtonBase, ScrollViewer inherits from ContentControl. The difference is that if the content of the ScrollViewer is too large to be displayed within the size of the control, ScrollViewer lets you scroll it.

Here's a program that sets the Content property of its window to an object of type ScrollViewer and sets the Content property of the ScrollViewer to a StackPanel that contains 50 buttons.

ScrollFiftyButtons.cs [View full width]//--------------------------------------------------- // ScrollFiftyButtons.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.ScrollFiftyButtons { class ScrollFiftyButtons : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ScrollFiftyButtons()); } public ScrollFiftyButtons() { Title = "Scroll Fifty Buttons"; SizeToContent = SizeToContent.Width; AddHandler(Button.ClickEvent, new RoutedEventHandler(ButtonOnClick)); ScrollViewer scroll = new ScrollViewer(); Content = scroll; StackPanel stack = new StackPanel(); stack.Margin = new Thickness(5); scroll.Content = stack; for (int i = 0; i < 50; i++) { Button btn = new Button(); btn.Name = "Button" + (i + 1); btn.Content = btn.Name + " says 'Click me'"; btn.Margin = new Thickness(5); stack.Children.Add(btn); } } void ButtonOnClick(object sender, RoutedEventArgs args) { Button btn = args.Source as Button; if (btn != null) MessageBox.Show(btn.Name + " has been clicked", "Button Click"); } } }


If the window (or your monitor) isn't large enough to display 50 buttons, you can use the vertical scroll bar to scroll the lower buttons into view.

The ScrollViewer has two properties that govern the visibility of the vertical and horizontal scroll bars. By default, the VerticalScrollBarVisibility property is the enumeration member ScrollBarVisibility.Visible, which means that the scroll bar is always visible, but it's inactive when it's not needed.

The default setting of the HorizontalScrollBarVisibility property is ScrollBarVisibility.Disabled, which means that the scroll bar is never visible.

Another option is ScrollBarVisibility.Auto, which means that the scroll bar appears only when it's needed. You might try setting the VerticalScrollBar property to this member and also decrease the number of buttons to something that can fit on your screen. As you make the window tall enough for the vertical scroll bar to disappear, you'll notice that all the buttons get wider because they have additional space in which to stretch! (Of course, you can avoid that effect by setting the HorizontalAlignment property of the buttons to HorizontalAlignment.Center.)

You can set the HorizontalScrollBarVisibility property to ScrollBarVisibility.Visible or ScrollBarVisibility.Auto. Now when you make the window too narrow for the buttons, a horizontal scroll bar lets you scroll the right sides into view. Without a horizontal scroll bar, when you make the window narrow, the ScrollViewer gives the StackPanel a narrower width. A very narrow StackPanel makes the buttons less wide, and button content is truncated. With a horizontal scroll bar, the StackPanel gets the width it needs.

You'll notice that the program sets the Margin property of each Button to five units on all four sides, and uses the same margin for the StackPanel. The Margin property on the ScrollViewer is not set, however, because it would cause the scroll bar to be offset from the edges of the window, which would look very strange.

Toward the beginning of the constructor, the SizeToContent property is set so that only the width of the window is sized to its content:

SizeToContent = SizeToContent.Width;

The customary setting of SizeToContent.WidthAndHeight causes the window to be sized to display all 50 buttons, so that doesn't make much sense. Moreover, the constructor doesn't set the ResizeMode property to ResizeMode.CanMinimize as I've often shown with windows that have been sized to their content. That option suppresses the sizing border and doesn't let you experiment with changing the width and height of the window.

You'll notice something a little different in the Click event handler. After casting the Source property of RoutedEventArgs to a Button object, the code checks if the result is null. The result will be null if Source is not actually a Button object. How can this be? Try it: comment out the if statement, recompile the program, and then click one of the arrows at the end of the scroll bar. The program bombs out with a Null Reference Exception.

If you investigate the problem, you'll find that the problem occurs when the Source property of RoutedEventArgs refers to the ScrollBar object. An object of type ScrollBar cannot be cast to an object of type Button. But if you look at the documentation for the ScrollBar control, you'll find that it doesn't even implement a Click event. So why is an object of type ScrollBar even showing up in this event handler?

The answer is revealed if you check another property of the RoutedEventArgs named OriginalSource. Sometimes controls are built up of other controls, and the ScrollBar is a good example. The two arrows at the ends are RepeatButton objects, and these objects generate Click events. The OriginalSource property of RoutedEventArgs indeed reveals an object of type RepeatButton.

Keep this example in mind when you attach event handlers to a parent object. You may need to perform some additional checks in the event handler. Another solution in this particular program is to call AddHandler for the StackPanel rather than for the window.

Scrollbars represent the traditional solution for fitting more elements than space allows. The Windows Presentation Foundation offers another solution called the Viewbox. To try it out, first comment out the three lines of code in ScrollFiftyButtons that deal with the ScrollViewer. Instead, create an object of type Viewbox and set its Child property to the StackPanel:

Viewbox view = new Viewbox(); Content = view; view.Child = stack;

Now the entire StackPanel and its fifty buttons are reduced in size to fit in the window. While this is not quite the best solution for buttons with text on them, keep it in mind for other space problems.

I've been focusing on getting multiple buttons in a window. The StackPanel is also useful in putting multiple elements into buttons. The following program creates a single button, but the button contains a StackPanel that has four children: an Image, a Label, and two Polyline objects. (Polyline inherits from Shape.)

DesignAButton.cs [View full width]//---------------------------------------------- // DesignAButton.cs (c) 2006 by Charles Petzold //---------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace Petzold.DesignAButton { public class DesignAButton : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new DesignAButton()); } public DesignAButton() { Title = "Design a Button"; // Create a Button as content of the Window. Button btn = new Button(); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += ButtonOnClick; Content = btn; // Create a StackPanel as content of the Button. StackPanel stack = new StackPanel(); btn.Content = stack; // Add a Polyline to the StackPanel. stack.Children.Add(ZigZag(10)); // Add an Image to the StackPanel. Uri uri = new Uri("pack://application: ,,/BOOK06.ICO"); // 32-pixels BitmapImage bitmap = new BitmapImage (uri); Image img = new Image(); img.Margin = new Thickness(0, 10, 0, 0); img.Source = bitmap; img.Stretch = Stretch.None; stack.Children.Add(img); // Add a Label to the StackPanel. Label lbl = new Label(); lbl.Content = "_Read books!"; lbl.HorizontalContentAlignment = HorizontalAlignment.Center; stack.Children.Add(lbl); // Add another Polyline to the StackPanel. stack.Children.Add(ZigZag(0)); } Polyline ZigZag(int offset) { Polyline poly = new Polyline(); poly.Stroke = SystemColors .ControlTextBrush; poly.Points = new PointCollection(); for (int x = 0; x <= 100; x += 10) poly.Points.Add(new Point(x, (x + offset) % 20)); return poly; } void ButtonOnClick(object sender, RoutedEventArgs args) { MessageBox.Show("The button has been clicked", Title); } } }


The two Polyline objects are created in the ZigZag method. These are the same images (a jagged line) except that the second is inverted. The Image element is a little picture of a book that I found in the collection of icons and bitmaps distributed with Visual Studio 2005. I gave the Image element a Margin of 10 units, but only on the top to prevent it from being jammed against the Polyline.

The Label control contains the text "Read books!"; notice the underline character preceding the letter R. Even though this Label control is one of four elements on the button, and it isn't even the first one, this underline on its text content is enough to enable an Alt+R keyboard interface for the button.

Although you may have been skeptical that you'd ever create a stack of 50, 30, or even 10 buttons, there's one type of button that almost always appears in a stack, and that's the radio button.

Traditionally, a group of mutually exclusive radio buttons are children of a group box, which is a control with a simple outline border and a text heading. In the Windows Presentation Foundation, this is the GroupBox control, which is one of three classes that descend from HeaderedContentControl:

Control

    ContentControl

          HeaderedContentControl

                Expander

                GroupBox

                TabItem

Because HeaderedContentControl derives from ContentControl, these controls have a Content property. The controls also have a Header property, which (like Content) is of type object. GroupBox adds nothing to HeaderedContentControl. Use the Header property to set the heading at the top of the GroupBox. Although this heading is customarily text, you can really make it whatever you want. Use Content for the interior of the GroupBox. You'll probably want to put a StackPanel in there.

TuneTheRadio shows how to put four radio buttons in a group box by way of a StackPanel. The radio buttons let you dynamically change the WindowStyle property of the window.

TuneTheRadio.cs [View full width]//--------------------------------------------- // TuneTheRadio.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.TuneTheRadio { public class TuneTheRadio : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new TuneTheRadio()); } public TuneTheRadio() { Title = "Tune the Radio"; SizeToContent = SizeToContent .WidthAndHeight; GroupBox group = new GroupBox(); group.Header = "Window Style"; group.Margin = new Thickness(96); group.Padding = new Thickness(5); Content = group; StackPanel stack = new StackPanel(); group.Content = stack; stack.Children.Add( CreateRadioButton("No border or caption", WindowStyle.None)); stack.Children.Add( CreateRadioButton("Single-border window", WindowStyle .SingleBorderWindow)); stack.Children.Add( CreateRadioButton("3D-border window", WindowStyle .ThreeDBorderWindow)); stack.Children.Add( CreateRadioButton("Tool window", WindowStyle .ToolWindow)); AddHandler(RadioButton.CheckedEvent, new RoutedEventHandler (RadioOnChecked)); } RadioButton CreateRadioButton(string strText, WindowStyle winstyle) { RadioButton radio = new RadioButton(); radio.Content = strText; radio.Tag = winstyle; radio.Margin = new Thickness(5); radio.IsChecked = (winstyle == WindowStyle); return radio; } void RadioOnChecked(object sender, RoutedEventArgs args) { RadioButton radio = args.Source as RadioButton; WindowStyle = (WindowStyle)radio.Tag; } } }


I gave the GroupBox a 1-inch margin so that it stands out a bit more from the window background. GroupBox is also given a padding of about 1/20 inch, which is the same value used for the margin of each radio button. (Alternatively, you can set the Margin property of the interior StackPanel to the same value.) The radio buttons are then separated from each other and the inside of the GroupBox by 1/10 inch. The CreateRadioButton method gives the Content of each button a text string and assigns the Tag property the corresponding member of the WindowStyle enumeration. Notice that CreateRadioButton also sets the IsChecked property of the radio button that corresponds to the current setting of the window's WindowStyle property.

The checking and unchecking of sibling radio buttons happens automatically from either the mouse or keyboard. When you're using the keyboard, the cursor movement keys change the input focus among the sibling radio buttons. The space bar checks the currently focused button. All the program needs to do is monitor which button is being checked. (Some programs might also need to know when a button is losing its check mark.) It is usually most convenient to use the same event handler for sibling radio buttons. When writing code for the event handler, keep in mind that the IsChecked property has already been set.

It could be that you have a bunch of sibling radio buttons that comprise two or more mutually exclusive groups. To distinguish between these groups, use the GroupName property defined by RadioButton. Set GroupName to a unique string for each group of mutually exclusive buttons, and the multiple groups will be automatically checked and unchecked correctly. You can use the same GroupName property in your event handler to determine which group of buttons is affected.

The panel most similar to StackPanel is WrapPanel. During the years that the Windows Presentation Foundation was being designed and created, the WrapPanel actually preceded the StackPanel. The WrapPanel displays rows or columns of elements and automatically wraps to the next row (or column) if there's not enough room. It sounds quite useful, but it turned out that developers were mostly using WrapPanel without the wrapping. These developers didn't know it at the time, but they really wanted a StackPanel.

WrapPanel is useful when you need to display an unknown number of items in a two-dimensional area. (Think of it as Windows Explorer in non-detail view.) It's likely that all these items will be the same size. WrapPanel doesn't require that, but it has ItemHeight and ItemWidth properties that you can use to enforce uniform height or width. The only other property that WrapPanel defines is Orientation, which you use the same way as for StackPanel.

It is difficult to imagine an application of WrapPanel that doesn't also involve ScrollViewer. Let's use WrapPanel and ScrollViewer and some buttons to build a crude imitation of the right side of Windows Explorer. The program is called ExploreDirectories and it has two classes. Here's the first.

ExploreDirectories.cs [View full width]//--------------------------------------------------- // ExploreDirectories.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.ExploreDirectories { public class ExploreDirectories : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ExploreDirectories()); } public ExploreDirectories() { Title = "Explore Directories"; ScrollViewer scroll = new ScrollViewer(); Content = scroll; WrapPanel wrap = new WrapPanel(); scroll.Content = wrap; wrap.Children.Add(new FileSystemInfoButton()); } } }


Well, isn't this simple? The content of the window is a ScrollViewer, and the content of the ScrollViewer is a WrapPanel, and WrapPanel has one childan object of type FileSystemInfoButton. Apparently this button is really hot stuff.

I named FileSystemInfoButton for the FileSystemInfo object in System.IO. The two classes that derive from FileSystemInfo are FileInfo and DirectoryInfo. If you have a particular object of type FileSystemInfo, you can determine whether it refers to a file or to a directory by using the is operator.

The FileSystemInfoButton class derives from Button and stores a FileSystemInfo object as a field. The class has three constructors. The single-argument constructor is the one used most. It requires an argument of type FileSystemInfo, which it stores as a field. The constructor sets the button's Content to the Name property of this object. That's either the name of a directory or the name of a file. If this object is a DirectoryInfo, the text is bolded.

The parameterless constructor is used only when the program starts up and the window adds the first child to the WrapPanel. The parameterless constructor passes a DirectoryInfo object for the My Documents directory to the single-argument constructor.

FileSystemInfoButton.cs [View full width]//--------- -------------------------------------------- // FileSystemInfoButton.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.Diagnostics; // For the Process class using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ExploreDirectories { public class FileSystemInfoButton : Button { FileSystemInfo info; // Parameterless constructor make "My Documents" button. public FileSystemInfoButton() : this(new DirectoryInfo( Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments))) { } // Single-argument constructor makes directory or file button. public FileSystemInfoButton(FileSystemInfo info) { this.info = info; Content = info.Name; if (info is DirectoryInfo) FontWeight = FontWeights.Bold; Margin = new Thickness(10); } // Two-argument constructor makes "Parent Directory" button. public FileSystemInfoButton(FileSystemInfo info, string str) : this(info) { Content = str; } // OnClick override does everything else. protected override void OnClick() { if (info is FileInfo) { Process.Start(info.FullName); } else if (info is DirectoryInfo) { DirectoryInfo dir = info as DirectoryInfo; Application.Current.MainWindow .Title = dir.FullName; Panel pnl = Parent as Panel; pnl.Children.Clear(); if (dir.Parent != null) pnl.Children.Add(new FileSystemInfoButton(dir.Parent, "..")); foreach (FileSystemInfo inf in dir .GetFileSystemInfos()) pnl.Children.Add(new FileSystemInfoButton(inf)); } base.OnClick(); } } }


When you run the program, you'll see a single button labeled My Documents. You can click that button to see the contents of that directory. If you click a button showing the name of a file, the application associated with that file is launched. Most directories listed after the first will also contain a button with two periods ("..") that you can use to navigate to the parent directory. When that button doesn't appear, you're at the root of the disk drive and you can't go back any further.

The OnClick override handles all of this logic. It first checks whether the info object stored as a field is an object of type FileInfo. If so, it uses the static Process.Start method (located in the System.Diagnostics namespace) to launch the application.

If the info object refers to a directory, the new directory name is displayed as the window Title. The OnClick method then clears the current children from the WrapPanel. If the new directory has a parent, the two-element constructor creates the button with two periods. The OnClick method concludes by obtaining the contents of the directory from GetFileSystemInfos and creating FileSystemInfoButton objects for each.

Of course, you'll want to experiment with navigating to a directory containing many files and examining how the ScrollViewer scrolls the rest of the WrapPanel into view.


Previous Page Next Page