Chapter 13. ListBox Selection


Many of the controls presented so far in this book have derived from ContentControl. These controls include Window, Label, Button, ScrollViewer, and ToolTip. All these controls have a Content property that you set to a string or to another element. If you set the Content property to a panel, you can put multiple elements on that panel.

The GroupBoxcommonly used to hold radio buttonsalso derives from ContentControl but by way of HeaderedContentControl. The GroupBox has a Content property, and it also has a Header property for the text (or whatever) that appears at the top of the box.

The TextBox and RichTextBox controls do not derive from ContentControl. These controls derive from the abstract class TextBoxBase and allow the user to enter and edit text. The ScrollBar class also does not derive from ContentControl. It inherits from the abstract RangeBase class, a class characterized by maintaining a Value property of type double that ranges between Minimum and Maximum properties.

Beginning with this chapter, you will be introduced to another major branch of the Control hierarchy: The ItemsControl class, which derives directly from Control. Controls that derive from ItemsControl display multiple items, generally of the same sort, either in a hierarchy or a list. These controls include menus, toolbars, status bars, tree views, and list views.

This particular chapter focuses mostly on ListBox, which is one of three controls that derive from ItemsControl by way of Selector:

Control

       ItemsControl

                 Selector (abstract)

                             ComboBox

                             ListBox

                             TabControl

Considered abstractly, a ListBox allows the user to select one item (or, optionally, multiple items) from a collection of items. In its default form, the ListBox is plain and austere. The items are presented in a vertical list and scroll bars are automatically provided if the list is too long or the items too wide. The ListBox highlights the selected item and provides a keyboard and mouse interface. (The ComboBox is similar to the ListBox but the list of items is not permanently displayed. I'll discuss ComboBox in Chapter 15.)

The crucial property of ListBox is Items, which the class inherits from ItemsControl. Items is of type ItemCollection, which is a collection of items of type object, which implies that just about any object can go into a ListBox. Although a ListBoxItem class exists specifically for list box items, you aren't required to use it. The simplest approach involves putting strings in the ListBox.

A program creates a ListBox object in the expected manner:

ListBox lstbox = new ListBox(); 


The ListBox is a collection of items, and these items must be added to the Items collection in a process called "filling the ListBox":

lstbox.Items.Add("Sunday"); lstbox.Items.Add("Monday"); lstbox.Items.Add("Tuesday"); lstbox.Items.Add("Wednesday"); ... 


Of course, list boxes are almost never filled with individual Add statements. Very often an array is involved:

string[] strDayNames = { "Sunday", "Monday", "Tuesday", "Wednesday",                          "Thursday", "Friday", "Saturday" }; foreach(string str in strDayNames)     list.Items.Add(str); 


And it's always nice to find a class like DateTimeFormatInfo in the System.Globalization namespace that provides these names for you in the user's own language:

string[] strDayNames = DateTimeFormatInfo.CurrentInfo.DayNames; 


or a "culture-independent" language (that is, English):

string[] strDayNames = DateTimeFormatInfo.InvariantInfo.DayNames; 


Another way to fill up the ListBox is by setting the ItemsSource property defined by ItemsControl. You can set ItemsSource to any object that implements the IEnumerable interface, which means that you can set ItemsSource to any collection that you might also use with foreach. For example:

lstbox.ItemsSource = DateTimeFormatInfo.CurrentInfo.DayNames; 


These two approaches to filling the ListBoxadding items through the Add method of the Items property and setting ItemsSourceare mutually exclusive.

Both the Items and ItemsSource properties are defined by the ItemsControl class. Two other useful ListBox properties are defined by Selector. These are SelectedIndex and SelectedItem, which indicate the item currently selected in the ListBox and which the ListBox displays with a special colored background (by default, blue).

By default, SelectedIndex equals 1 and SelectedItem equals null, which means that no item is selected. The ListBox doesn't automatically decide that a particular item from the Items collection should be selected. If your program doesn't deliberately set a selected item, SelectedIndex remains 1 and SelectedItem remains null until the user gets hold of the control.

If your program wants to set a selected item before the user sees the ListBox, it can do so by setting either SelectedIndex or SelectedItem. For example,

lstbox.SelectedItem = 5; 


selects the sixth item in the Items collection. SelectedIndex must be an integer ranging from 1 to one less than the number of items in the Items collection. If an item is selected, then

lstbox.SelectedItem 


is equivalent to

lstbox.Items[lstbox.SelectedIndex] 


When the selection changes, the ListBox fires the SelectionChanged event defined by Selector.

As a case study, let's try to design a ListBox that lets the user choose a color. This is a fairly common example, but certainly a useful one. (Someday, someone will write a book entitled A Brief History of Color Selection Controls and it will be 1,700 pages long.)

As you already know, the System.Windows.Media namespace has a class named Colors that contains 141 static properties with names that range from AliceBlue to YellowGreen. These static properties return objects of type Color. You could define an array like this and then fill up the list box from this array:

Color[] clrs = { Colors.AliceBlue, Colors.AntiqueWhite, ... }; 


But before you type 141 color names, there's a better approach, made possible by reflection. As you learned in Chapter 2, a program can use reflection to obtain all the Color objects defined in Colors. The expression typeof(Colors) returns an object of type Type, which defines a method named GetProperties that returns an array of PropertyInfo objects, each one corresponding to a property of the Colors class. You can then use the Name property of PropertyInfo to obtain the name of the property and the GetValue method to obtain the actual Color value.

The eventual goal here is to display the actual colors in the ListBox in some fashion. But let's start out simply with just the color names.

ListColorNames.cs

[View full width]

//----------------------------------------------- // ListColorNames.cs (c) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ListColorNames { class ListColorNames : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ListColorNames()); } public ListColorNames() { Title = "List Color Names"; // Create ListBox as content of window. ListBox lstbox = new ListBox(); lstbox.Width = 150; lstbox.Height = 150; lstbox.SelectionChanged += ListBoxOnSelectionChanged; Content = lstbox; // Fill ListBox with Color names. PropertyInfo[] props = typeof(Colors) .GetProperties(); foreach (PropertyInfo prop in props) lstbox.Items.Add(prop.Name); } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ListBox lstbox = sender as ListBox; string str = lstbox.SelectedItem as string; if (str != null) { Color clr = (Color)typeof(Colors) .GetProperty(str).GetValue(null, null); Background = new SolidColorBrush(clr); } } } }



You might benefit from getting a feel for the user interface of the ListBox before exploring the programmatic interface.

This program does not set input focus to the ListBox, and there is no initially selected item. You can fix that by clicking one of the items in the ListBox with the mouse. The background of the window changes to that color. You can continue to use the mouse to click different items. You can scroll the items by clicking the scroll bar. The currently selected ListBox item is always displayed with a special background color (blue by default). Notice also that the color of the text of the selected item changes from black to white to contrast with that blue background.

Of course, you can control the ListBox entirely from the keyboard. When you first run the program, press the Tab key to give the ListBox input focus. You'll see a dotted focus rectangle surrounding the first item. At this point, there is no selected item. You can press the spacebar to select the item indicated by the focus rectangle. Press the Up or Down arrow key to change the selected item. The ListBox also responds to Page Up, Page Down, Home, and End. You can type letters to move to items that begin with that letter.

If you hold the Ctrl key down and click the currently selected item with the mouse, the item will become unselected. (The color of the window background remains the same because the program is ignoring that little anomaly.) If you hold the Ctrl key down while pressing the Up and Down arrow keys, you can move the focus rectangle without changing the selection. You can then select an item or clear a selection with the spacebar. Again, it is possible to put the ListBox into a state where no item is currently selected.

Let's look at the code. The window constructor begins by creating a ListBox and giving it an explicit size in device-independent units. It is common to give a ListBox a specific size. The dimensions you choose will depend on the contents of the ListBox and its context.

If you don't give the ListBox a specific size, the control will fill the entire space allowed for it. If you instead set HorizontalAlignment and VerticalAlignment to Center, the ListBox will still stretch itself vertically to display as many items as possible, and the width will be based on the currently visible items. As you scroll through the items, it's possible that you will then witness the ListBox becoming narrower and wider depending on the particular items it displays.

The program fills the ListBox with color names and sets a handler for the SelectionChanged event. The event handler obtains the selected item from the ListBox using the SelectedItem property. (Notice that the event handler checks for a null value indicating no currently selected item and doesn't do anything in that case.) In this program, the ListBox item is a string, so it needs to be converted into a Color object. Again, reflection comes to the rescue. The GetProperty call obtains the PropertyInfo object associated with the static property in Colors, and GetValue obtains the Color object.

If you'd prefer that the ListBox have input focus and display a selection when the program starts up, you can add code to the bottom of the window constructor like this:

lstbox.SelectedItem = "Magenta"; lstbox.ScrollIntoView(lstbox.SelectedItem); lstbox.Focus(); 


Notice the ScrollIntoView call to scroll the ListBox so that the selected item is visible.

Although the Selector class implements only single-selection logic, the ListBox class itself allows the selection of multiple items. You set the SelectionMode property to a member of the SelectionMode enumeration: Single, Extended, or Multiple.

With SelectionMode.Multiple, you select and clear individual items with a click of the left mouse button. Or, you can use the arrow keys to move a focus rectangle and select or clear individual items with the spacebar.

With SelectionMode.Extended, you can select and clear individual items with the mouse only when the Ctrl key is pressed. Or, you can select a range of items by holding down the Shift key and clicking each item. If neither the Shift key nor the Ctrl key is down, a mouse click causes all previous selections to be unselected. When using the keyboard rather than the mouse, you must hold the Ctrl key to move the focus rectangle, and you select individual items with the spacebar. Or you can hold down the Shift key and use the arrow keys to select a group of consecutive items.

When using ListBox in a multiple-selection mode, you'll make use of the SelectedItems property (notice the plural), which is a collection of multiple items. Although the ListBox class has the necessary support of multiple-selection boxes, the SelectionChanged event defined by the Selector class also helps out. This event comes with an object of type SelectionChangedEventArgs with two properties named AddedItems and RemovedItems. These are both lists of items that have been added to or removed from the collection of selected items. If you're working with a single-selection ListBox, you usually don't need to bother with these properties.

The Items collection of ListBox is a collection of objects of type object, so it's possible to put Color objects directly in the ListBox. Let's try that.

ListColorValues.cs

[View full width]

//------------------------------------------------ // ListColorValues.cs (c) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Reflection; using System.Windows; using System.Windows.Input; using System.Windows.Controls; using System.Windows.Media; namespace Petzold.ListColorValues { class ListColorValues : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ListColorValues()); } public ListColorValues() { Title = "List Color Values"; // Create ListBox as content of window. ListBox lstbox = new ListBox(); lstbox.Width = 150; lstbox.Height = 150; lstbox.SelectionChanged += ListBoxOnSelectionChanged; Content = lstbox; // Fill ListBox with Color values. PropertyInfo[] props = typeof(Colors) .GetProperties(); foreach (PropertyInfo prop in props) lstbox.Items.Add(prop.GetValue (null, null)); } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ListBox lstbox = sender as ListBox; if (lstbox.SelectedIndex != -1) { Color clr = (Color)lstbox .SelectedItem; Background = new SolidColorBrush(clr); } } } }



This approach simplifies the event handler because the SelectedItem need only be cast to an object of type Color. Otherwise, it's a disaster. The ListBox displays the string returned from the ToString method of Color, and that turns out to be strings like "#FFF0F8FF." Even programmers with eight fingers on each hand don't like seeing hexadecimal color values in list boxes. The problem is that the Color structure knows nothing about the names in the Colors class. I wouldn't be surprised if the entire Colors class was implemented with properties like this:

public static Color AliceBlue {     get { return Color.FromRgb(0xF0, 0xF8, 0xFF); } } 


That's my guess, anyway.

Color is not the most conducive type of object to go into a ListBox. Much better are those objects that have a ToString method that returns exactly what you want to display to the user. Of course, that's not always possible, and there are some who would argue that ToString should return a string representation that might be human-readable, but which is primarily formatted for easy computer readability. Regardless, in the case of Color, major changes would need to be made so that ToString returned the color name.

Here's a class called NamedColor that illustrates a somewhat more desirable definition of a class you might use with a ListBox.

NamedColor.cs

[View full width]

//------------------------------------------- // NamedColor.cs (c) 2006 by Charles Petzold //------------------------------------------- using System; using System.Reflection; using System.Windows.Media; namespace Petzold.ListNamedColors { class NamedColor { static NamedColor[] nclrs; Color clr; string str; // Static constructor. static NamedColor() { PropertyInfo[] props = typeof(Colors) .GetProperties(); nclrs = new NamedColor[props.Length]; for (int i = 0; i < props.Length; i++) nclrs[i] = new NamedColor(props[i] .Name, (Color)props[i].GetValue(null, null)); } // Private constructor. private NamedColor(string str, Color clr) { this.str = str; this.clr = clr; } // Static read-only property. public static NamedColor[] All { get { return nclrs; } } // Read-only properties. public Color Color { get { return clr; } } public string Name { get { string strSpaced = str[0].ToString(); for (int i = 1; i < str.Length; i++) strSpaced += (char.IsUpper (str[i]) ? " " : "") + str[i] .ToString(); return strSpaced; } } // Override of ToString method. public override string ToString() { return str; } } }



The NamedColor class has two private instance fields named clr and str that store a Color value and a string with the color name (such as "AliceBlue"). These fields are set in the private NamedColor constructor. The fields are accessible from the public Color property and the override of the ToString property.

The Name property is similar to ToString except that it inserts spaces into the color names to make them actual words. For example, "AliceBlue" becomes "Alice Blue".

The static constructor is crucial to the functionality of this class. This static constructor uses reflection to access the Colors class. For each property in that class, the static NamedColor constructor uses the private instance constructor to create an object of type NamedColor. The array of resultant NamedColor objects is stored as a field and is publicly accessible through the static All method.

So, we could use this class instead of Colors to fill up a ListBox with code as simple as this:

foreach (NamedColor nclr in NamedColor.All)     lstbox.Items.Add(nclr); 


The ListBox uses the string returned from ToString to display the items. In the event handler you'd cast the SelectedItem to an object of type NamedColor and then access the Color property:

Color clr = (lstbox.SelectedItem as NamedColor).Color; 


That's one way to do it, but there's an easier alternative. Rather than filling up the ListBox with a foreach loop, you can instead pass the array of NamedColor values directly to the ItemsSource property of the ListBox:

lstbox.ItemsSource = NamedColor.All; 


This statement replaces the foreach loop, and the program works the same way.

Regardless of whether you use the foreach loop or ItemsSource, all the items in the ListBox are of the same type, and that type is NamedColor. This little fact allows you to use three other ListBox properties: DisplayMemberPath, SelectedValuePath, and SelectedValue.

The DisplayMemberPath property is defined by ItemsControl. You optionally set this property to a string containing the name of a property that you want the ListBox to use to display the items. For items of type NamedColor, that property is Name:

lstbox.DisplayMemberPath = "Name"; 


Now, instead of using ToString to display each item, the ListBox will use the Name property of each item and the colors will appear as "Alice Blue" rather than "AliceBlue".

The other two properties you can use are defined by the Selector class and are related. You use the SelectedValuePath property to indicate the name of the property that represents the value of the item. For the NamedColor class, that property is Color, so you can set SelectedValuePath like this:

lstbox.SelectedValuePath = "Color"; 


Now, rather than using SelectedIndex or SelectedItem in your event handler to obtain the currently selected item, you can instead use SelectedValue. The SelectedValue property returns the Color property of the selected item. Casting is still required but the code is definitely simpler:

Color clr = (Color) lstbox.SelectedValue; 


In summary, the strings you assign to DisplayMemberPath and SelectedValuePath must be the names of properties defined by the class (or structure) of the type of the ListBox items. DisplayMemberPath tells the ListBox which property to use for displaying the items in the ListBox. SelectedValuePath tells the ListBox which property to use for returning a value from SelectedValue. (DisplayMemberPath and SelectedValuePath are "paths" because the properties could be nested in other properties, and you would specify the entire path by separating the property names with periods.)

Here's a complete program that uses the NamedColor class to fill its ListBox.

ListNamedColors.cs

[View full width]

//------------------------------------------------ // ListNamedColors.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.ListNamedColors { class ListNamedColors : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ListNamedColors()); } public ListNamedColors() { Title = "List Named Colors"; // Create ListBox as content of window. ListBox lstbox = new ListBox(); lstbox.Width = 150; lstbox.Height = 150; lstbox.SelectionChanged += ListBoxOnSelectionChanged; Content = lstbox; // Set the items and the property paths. lstbox.ItemsSource = NamedColor.All; lstbox.DisplayMemberPath = "Name"; lstbox.SelectedValuePath = "Color"; } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ListBox lstbox = sender as ListBox; if (lstbox.SelectedValue != null) { Color clr = (Color)lstbox .SelectedValue; Background = new SolidColorBrush(clr); } } } }



Creating a whole class solely for filling a ListBox may seem excessive, but as you can see, the code that uses that class is cleaned up considerably.

But can we clean it up even more? Well, it's too bad that the ListBox is returning a SelectedValue of type Color, when what we really need to set the Background property of the window is an object of type Brush. If ListBox were instead storing objects with a Brush property, we could actually bind the SelectedValue property of the ListBox with the Background property of the window.

Let's try it. This class is virtually identical to NamedColor except that it replaces references to Color and Colors with Brush and Brushes.

NamedBrush.cs

[View full width]

//------------------------------------------- // NamedBrush.cs (c) 2006 by Charles Petzold //------------------------------------------- using System; using System.Reflection; using System.Windows.Media; namespace Petzold.ListNamedBrushes { public class NamedBrush { static NamedBrush[] nbrushes; Brush brush; string str; // Static constructor. static NamedBrush() { PropertyInfo[] props = typeof(Brushes) .GetProperties(); nbrushes = new NamedBrush[props.Length]; for (int i = 0; i < props.Length; i++) nbrushes[i] = new NamedBrush (props[i].Name, (Brush)props[i].GetValue(null, null)); } // Private constructor. private NamedBrush(string str, Brush brush) { this.str = str; this.brush = brush; } // Static read-only property. public static NamedBrush[] All { get { return nbrushes; } } // Read-only properties. public Brush Brush { get { return brush; } } public string Name { get { string strSpaced = str[0].ToString(); for (int i = 1; i < str.Length; i++) strSpaced += (char.IsUpper (str[i]) ? " " : "") + str[i] .ToString(); return strSpaced; } } // Override of ToString method. public override string ToString() { return str; } } }



If you put NamedBrush objects in a ListBox, you'll set SelectedValuePath to "Brush". If you choose to have an event handler, you could set the Background property from the SelectedValue simply with a little casting:

Background = (Brush)lstbox.SelectedValue; 


However, when the event handler gets this easy, it can be dispensed with altogether and replaced with a data binding:

lstbox.SetBinding(ListBox.SelectedValueProperty, "Background"); lstbox.DataContext = this; 


This binding indicates that the SelectedValue property of the ListBox object is to be bound with the Background property of the object this (which is the window). Here's the complete program that uses NamedBrush:

ListNamedBrushes.cs

[View full width]

//------------------------------------------------- // ListNamedBrushes.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.ListNamedBrushes { public class ListNamedBrushes : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ListNamedBrushes()); } public ListNamedBrushes() { Title = "List Named Brushes"; // Create ListBox as content of window. ListBox lstbox = new ListBox(); lstbox.Width = 150; lstbox.Height = 150; Content = lstbox; // Set the items and the property paths. lstbox.ItemsSource = NamedBrush.All; lstbox.DisplayMemberPath = "Name"; lstbox.SelectedValuePath = "Brush"; // Bind the SelectedValue to window Background. lstbox.SetBinding(ListBox .SelectedValueProperty, "Background"); lstbox.DataContext = this; } } }



What's nice about this program is that it no longer has any event handlers. I always think of event handlers as moving parts in a machine. Get rid of them, and you're coding with no moving parts. (Of course, there are moving parts behind the scenes, but those aren't our problem.)

Fortunately, if you put the ListBox in a state where there is no selected item, the window doesn't care if the Background property is set to null. It just turns the client area black and awaits further instruction.

I want to focus now on showing actual colors in the ListBox. It will be preferable to show both the color and the name, but we'll look at some simpler alternatives first. So far, the only objects appearing as ListBox items have been text strings. It is possible to put Shape objects into the ListBox instead. This program shows how.

ListColorShapes.cs

[View full width]

//------------------------------------------------ // ListColorShapes.cs (c) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.ListColorShapes { class ListColorShapes : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ListColorShapes()); } public ListColorShapes() { Title = "List Color Shapes"; // Create ListBox as content of window. ListBox lstbox = new ListBox(); lstbox.Width = 150; lstbox.Height = 150; lstbox.SelectionChanged += ListBoxOnSelectionChanged; Content = lstbox; // Fill ListBox with Ellipse objects. PropertyInfo[] props = typeof(Brushes) .GetProperties(); foreach (PropertyInfo prop in props) { Ellipse ellip = new Ellipse(); ellip.Width = 100; ellip.Height = 25; ellip.Margin = new Thickness(10, 5 , 0, 5); ellip.Fill = prop.GetValue(null, null) as Brush; lstbox.Items.Add(ellip); } } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ListBox lstbox = sender as ListBox; if (lstbox.SelectedIndex != -1) Background = (lstbox.SelectedItem as Shape).Fill; } } }



The constructor of the window creates an Ellipse object for each property of the Brushes class, and assigns that Brush object to the Fill property of the shape. The event handler is fairly simple: The Fill property of the SelectedItem is simply assigned to the Background property of the window.

When you run this program, you'll see why I used Ellipse rather than Rectangle, and why I gave each item a healthy margin. ListBox denotes the currently selected item with a background color (blue by default), and this technique relies on the items themselves not entirely obscuring the background.

You can also use binding with this program. Rather than attaching an event handler, you could specify that the selected value should be the Fill property of the item, and that this selected value should be bound to the Background property of the window:

lstbox.SelectedValuePath = "Fill"; lstbox.SetBinding(ListBox.SelectedValueProperty, "Background"); lstbox.DataContext = this; 


What if we want a color and a name? One approach is to use Label controls as the ListBox items. The Label text could provide the color name and the background of the label could provide the color. Of course, we'll need to make sure that the foreground color of the text displayed by the label does not blend in with the background, and that there's a sufficient margin around the label so that the ListBox can display the selection color.

ListColoredLabels.cs

[View full width]

//-------------------------------------------------- // ListColoredLabels.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ListColoredLabels { class ListColoredLabels : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ListColoredLabels()); } public ListColoredLabels() { Title = "List Colored Labels"; // Create ListBox as content of window. ListBox lstbox = new ListBox(); lstbox.Height = 150; lstbox.Width = 150; lstbox.SelectionChanged += ListBoxOnSelectionChanged; Content = lstbox; // Fill ListBox with label controls. PropertyInfo[] props = typeof(Colors) .GetProperties(); foreach (PropertyInfo prop in props) { Color clr = (Color)prop.GetValue (null, null); bool isBlack = .222 * clr.R + .707 * clr.G + .071 * clr.B > 128; Label lbl = new Label(); lbl.Content = prop.Name; lbl.Background = new SolidColorBrush(clr); lbl.Foreground = isBlack ? Brushes .Black : Brushes.White; lbl.Width = 100; lbl.Margin = new Thickness(15, 0, 0, 0); lbl.Tag = clr; lstbox.Items.Add(lbl); } } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ListBox lstbox = sender as ListBox; Label lbl = lstbox.SelectedItem as Label; if (lbl != null) { Color clr = (Color)lbl.Tag; Background = new SolidColorBrush(clr); } } } }



Notice that the program calculates the Boolean isBlack value based on the luminance of the color, which is a standard weighted average of the three primaries. This value determines whether the text is displayed in white or black against the colored background.

The event handler could have set the window Background brush from the Background property of the Label, but I decided to use the Tag property instead just for a little variety. (The program could alternatively set the SelectedValuePath to "Background" and set the window Background from the SelectedValue, or use binding.)

This program works, but I'm sure you'll agree that it doesn't look very good. To show the blue highlight, the labels are given a width of 100, with a margin of 15 on the left, and some extra space on the right from the width of the ListBox itself. It's not quite as elegant as it might be.

I mentioned earlier in this chapter that there exists a class named ListBoxItem that derives from ContentControl. Obviously, you're not required to use that class in your list boxes, as has been well demonstrated. And if you substitute ListBoxItem for Label in the previous program, you'll get something that becomes very awkward. The ListBox uses the Background property of the ListBoxItem for highlighting the selected item, so the selection doesn't stand out as it should.

Still, who says we need to use standard highlighting? We could come up with an alternative for indicating the selected item. The following program takes a rather unconventional approach. It uses ListBoxItem objects under the assumption that something more is needed to highlight the selected item.

ListWithListBoxItems.cs

[View full width]

//--------- -------------------------------------------- // ListWithListBoxItems.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; class ListWithListBoxItems : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ListWithListBoxItems()); } public ListWithListBoxItems() { Title = "List with ListBoxItem"; // Create ListBox as content of window. ListBox lstbox = new ListBox(); lstbox.Height = 150; lstbox.Width = 150; lstbox.SelectionChanged += ListBoxOnSelectionChanged; Content = lstbox; // Fill ListBox with ListBoxItem objects. PropertyInfo[] props = typeof(Colors) .GetProperties(); foreach (PropertyInfo prop in props) { Color clr = (Color)prop.GetValue(null, null); bool isBlack = .222 * clr.R + .707 * clr.G + .071 * clr.B > 128; ListBoxItem item = new ListBoxItem(); item.Content = prop.Name; item.Background = new SolidColorBrush (clr); item.Foreground = isBlack ? Brushes .Black : Brushes.White; item.HorizontalContentAlignment = HorizontalAlignment.Center; item.Padding = new Thickness(2); lstbox.Items.Add(item); } } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ListBox lstbox = sender as ListBox; ListBoxItem item; if (args.RemovedItems.Count > 0) { item = args.RemovedItems[0] as ListBoxItem; String str = item.Content as String; item.Content = str.Substring(2, str .Length - 4); item.FontWeight = FontWeights.Regular; } if (args.AddedItems.Count > 0) { item = args.AddedItems[0] as ListBoxItem; String str = item.Content as String; item.Content = "[ " + str + " ]"; item.FontWeight = FontWeights.Bold; } item = lstbox.SelectedItem as ListBoxItem; if (item != null) Background = item.Background; } }



The SelectionChanged event handler not only sets the window Background brush from the Background of the selected ListBoxItem, but also alters the text in that item. It surrounds the text with square brackets and gives it a bolded font. Of course, every item becoming unselected has to be undone. The item that has just been selected and the item that is unselected are both available from the AddedItems and RemovedItems properties of SelectionChangedEventArgs. (These two properties are plural to account for multiple-selection list boxes.)

If giving the selected item a bolded font and brackets isn't enough to make it stand out, you could use the actual font size. Add the following statement to the AddedItem sections:

item.FontSize *= 2; 


Undo the increase with this statement in the RemovedItems section:

item.FontSize = lstbox.FontSize; 


You'll want to make the ListBox itself wider to allow for this increase in item font sizes.

ListBoxItem itself has two events named Selected and Unselected, and two methods named OnSelected and OnUnselected. A class that inherits from ListBoxItem could override these methods to implement the text-changing and font-changing logic a bit more smoothly.

The following class inherits from ListBoxItem and overrides the OnSelected and OnUnselected methods. But what it really demonstrates is the use of a StackPanel to display both a Rectangle object colored with the color and a TextBlock object with the color name.

ColorListBoxItem.cs

[View full width]

//------------------------------------------------- // ColorListBoxItem.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.Shapes; namespace Petzold.ListColorsElegantly { class ColorListBoxItem : ListBoxItem { string str; Rectangle rect; TextBlock text; public ColorListBoxItem() { // Create StackPanel for Rectangle and TextBlock. StackPanel stack = new StackPanel(); stack.Orientation = Orientation .Horizontal; Content = stack; // Create Rectangle to display color. rect = new Rectangle(); rect.Width = 16; rect.Height = 16; rect.Margin = new Thickness(2); rect.Stroke = SystemColors .WindowTextBrush; stack.Children.Add(rect); // Create TextBlock to display color name. text = new TextBlock(); text.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(text); } // Text property becomes Text property of TextBlock. public string Text { set { str = value; string strSpaced = str[0].ToString(); for (int i = 1; i < str.Length; i++) strSpaced += (char.IsUpper (str[i]) ? " " : "") + str[i].ToString(); text.Text = strSpaced; } get { return str; } } // Color property becomes Brush property of Rectangle. public Color Color { set { rect.Fill = new SolidColorBrush (value); } get { SolidColorBrush brush = rect.Fill as SolidColorBrush; return brush == null ? Colors .Transparent : brush.Color; } } // Make font bold when item is selected. protected override void OnSelected (RoutedEventArgs args) { base.OnSelected(args); text.FontWeight = FontWeights.Bold; } protected override void OnUnselected (RoutedEventArgs args) { base.OnUnselected(args); text.FontWeight = FontWeights.Regular; } // Implement ToString for keyboard letter interface. public override string ToString() { return str; } } }



The ColorListBoxItem class creates the StackPanel (with a horizontal orientation), Rectangle, and TextBlock in its constructor. The class defines a Text property that sets the Text property of the TextBlock, and a Color property associated with the Brush used for the Rectangle. The class overrides the OnSelected and OnUnselected methods to make the text of the selected item bold.

The ColorListBox class inherits from ListBox and uses reflection to fill itself with objects of type ColorListBoxItem. It also defines a new property named SelectedColor that simply refers to SelectedValue.

ColorListBox.cs

[View full width]

//--------------------------------------------- // ColorListBox.cs (c) 2006 by Charles Petzold //--------------------------------------------- using System; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ListColorsElegantly { class ColorListBox : ListBox { public ColorListBox() { PropertyInfo[] props = typeof(Colors) .GetProperties(); foreach (PropertyInfo prop in props) { ColorListBoxItem item = new ColorListBoxItem(); item.Text = prop.Name; item.Color = (Color)prop.GetValue (null, null); Items.Add(item); } SelectedValuePath = "Color"; } public Color SelectedColor { set { SelectedValue = value; } get { return (Color)SelectedValue; } } } }



Once the ColorListBoxItem and ColorListBox classes are defined, the code to create such a ListBox becomes very straightforward and elegant. Elegant too is the display of the colored rectangles and text string in the ListBox.

ListColorsElegantly.cs

[View full width]

//---------------------------------------------------- // ListColorsElegantly.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.ListColorsElegantly { public class ListColorsElegantly : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ListColorsElegantly()); } public ListColorsElegantly() { Title = "List Colors Elegantly"; ColorListBox lstbox = new ColorListBox(); lstbox.Height = 150; lstbox.Width = 150; lstbox.SelectionChanged += ListBoxOnSelectionChanged; Content = lstbox; // Initialize SelectedColor. lstbox.SelectedColor = SystemColors .WindowColor; } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ColorListBox lstbox = sender as ColorListBox; Background = new SolidColorBrush (lstbox.SelectedColor); } } }



But still the question nags: With the Windows Presentation Foundation, is it possible to do something similar even elegantlier?

Yes, it is. You probably recall the BuildButtonFactory program in Chapter 11 that redefined the look of the button by setting the Template property defined by Control. The ListBox inherits that property, but ItemsControl also defines two more template-related properties: The ItemTemplate property (of type DataTemplate) lets you define the appearance of the items and bindings between the properties of the items and the data source. The ItemsPanel property (of type ItemsPanelTemplate) lets you substitute a different panel for displaying the items.

The ListColorsEvenElegantlier program demonstrates the use of the ItemTemplate property. The program also requires the NamedBrush class presented earlier in this chapter.

ListColorsEvenElegantlier.cs

[View full width]

//--------- ------------------------------------------------- // ListColorsEvenElegantlier.cs (c) 2006 by Charles Petzold //------------------------------------------------ ---------- using Petzold.ListNamedBrushes; using System; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.ListColorsEvenElegantlier { public class ListColorsEvenElegantlier : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ListColorsEvenElegantlier()); } public ListColorsEvenElegantlier() { Title = "List Colors Even Elegantlier"; // Create a DataTemplate for the items. DataTemplate template = new DataTemplate(typeof(NamedBrush)); // Create a FrameworkElementFactory based on StackPanel. FrameworkElementFactory factoryStack = new FrameworkElementFactory(typeof(StackPanel)); factoryStack.SetValue(StackPanel .OrientationProperty, Orientation.Horizontal); // Make that the root of the DataTemplate visual tree. template.VisualTree = factoryStack; // Create a FrameworkElementFactory based on Rectangle. FrameworkElementFactory factoryRectangle = new FrameworkElementFactory(typeof(Rectangle)); factoryRectangle.SetValue(Rectangle .WidthProperty, 16.0); factoryRectangle.SetValue(Rectangle .HeightProperty, 16.0); factoryRectangle.SetValue(Rectangle .MarginProperty, new Thickness(2)); factoryRectangle.SetValue(Rectangle .StrokeProperty, SystemColors.WindowTextBrush); factoryRectangle.SetBinding(Rectangle .FillProperty, new Binding("Brush")); // Add it to the StackPanel. factoryStack.AppendChild (factoryRectangle); // Create a FrameworkElementFactory based on TextBlock. FrameworkElementFactory factoryTextBlock = new FrameworkElementFactory(typeof(TextBlock)); factoryTextBlock.SetValue(TextBlock .VerticalAlignmentProperty, VerticalAlignment.Center); factoryTextBlock.SetValue(TextBlock .TextProperty, new Binding("Name")); // Add it to the StackPanel. factoryStack.AppendChild (factoryTextBlock); // Create ListBox as content of window. ListBox lstbox = new ListBox(); lstbox.Width = 150; lstbox.Height = 150; Content = lstbox; // Set the ItemTemplate property to the template created above. lstbox.ItemTemplate = template; // Set the ItemsSource to the array of NamedBrush objects. lstbox.ItemsSource = NamedBrush.All; // Bind the SelectedValue to window Background. lstbox.SelectedValuePath = "Brush"; lstbox.SetBinding(ListBox .SelectedValueProperty, "Background"); lstbox.DataContext = this; } } }



The window constructor begins by creating an object of type DataTemplate. (Towards the end of the constructor, this object is assigned to the ItemTemplate property of a ListBox.) Notice that the DataTemplate constructor is passed the type of the NamedBrush class, indicating that the ListBox will be filled with items of type NamedBrush.

The program next proceeds to construct a visual tree to display these items. At the root of the visual tree is a StackPanel. A FrameworkElementFactory object for a StackPanel is assigned to the VisualTree property of the DataTemplate. Next are created two more FrameworkElementFactory objects for Rectangle and TextBlock, and these are added to the children collection of the FrameworkElementFactory object for the StackPanel. Notice that the Fill property of the Rectangle element is bound to the Brush property of NamedBrush and the Text property of the TextBlock element is bound to the Name property of NamedBrush.

It is now time to create the ListBox. The code that concludes the program should look familiar, except for the assignment of the DataTemplate object just created to the ItemTemplate property of ListBox. ListBox will now display NamedBrush items with the visual tree of this template.

The constructor wraps up by filling the list box using ItemsSource and the static All property of NamedBrush. The SelectedValuePath is the Brush property of the NamedBrush item, and this is bound with the Background property of the window.

I said at the outset of this chapter that ListBox is a control for selecting one item out of many. Back in Chapter 11, the ColorGrid control displayed 40 ColorCell elements on a UniformGrid and supplied an extensive keyboard and mouse interface for selecting a color from this grid.

It is possible to use a ListBox for this control and avoid duplicating a lot of keyboard and mouse code already in ListBox. All that's really necessary is to persuade ListBox to use a UniformGrid rather than a StackPanel for presenting the items, and that is accomplished in the following class with three statements. The ColorGridBox class inherits from ListBox and nearly achieves the full functionality of the ColorGrid control from Chapter 11. (And what it doesn't duplicate is fairly minor.)

ColorGridBox.cs

[View full width]

//--------------------------------------------- // ColorGridBox.cs (c) 2006 by Charles Petzold //--------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.SelectColorFromGrid { class ColorGridBox : ListBox { // The colors to be displayed. string[] strColors = { "Black", "Brown", "DarkGreen", "MidnightBlue", "Navy", "DarkBlue", "Indigo", "DimGray", "DarkRed", "OrangeRed", "Olive", "Green", "Teal", "Blue", "SlateGray", "Gray", "Red", "Orange", "YellowGreen", "SeaGreen", "Aqua", "LightBlue", "Violet", "DarkGray", "Pink", "Gold", "Yellow", "Lime", "Turquoise", "SkyBlue", "Plum", "LightGray", "LightPink", "Tan", "LightYellow", "LightGreen", "LightCyan", "LightSkyBlue", "Lavender", "White" }; public ColorGridBox() { // Define the ItemsPanel template. FrameworkElementFactory factoryUnigrid = new FrameworkElementFactory(typeof(UniformGrid)); factoryUnigrid.SetValue(UniformGrid .ColumnsProperty, 8); ItemsPanel = new ItemsPanelTemplate (factoryUnigrid); // Add items to the ListBox. foreach (string strColor in strColors) { // Create Rectangle and add to ListBox. Rectangle rect = new Rectangle(); rect.Width = 12; rect.Height = 12; rect.Margin = new Thickness(4); rect.Fill = (Brush) typeof(Brushes).GetProperty (strColor).GetValue(null, null); Items.Add(rect); // Create ToolTip for Rectangle. ToolTip tip = new ToolTip(); tip.Content = strColor; rect.ToolTip = tip; } // Indicate that SelectedValue is Fill property of Rectangle item. SelectedValuePath = "Fill"; } } }



The constructor begins by creating a FrameworkElementFactory object for the UniformGrid panel, and assigning the Columns property a value of 8. The ItemsPanelTemplate constructor accepts this factory directly, and the ItemsPanelTemplate object is assigned to the ItemsPanel property of the ListBox. That's all that's necessary to get a ListBox to use an alternative to StackPanel.

Most of the rest of the constructor is devoted to filling up the ListBox with Rectangle objects, each with a ToolTip indicating the name of the color. The constructor concludes by assigning the SelectedValuePath property of the ListBox to the Fill property of the ListBox items, which refers to the Fill property of the Rectangle.

The SelectColorFromGrid program is very similar to the SelectColor program used in Chapter 11 to test the new control, except that this program binds the SelectedValue property of the ColorGridBox control with the Background property of the window.

SelectColorFromGrid.cs

[View full width]

//---------------------------------------------------- // SelectColorFromGrid.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.SelectColorFromGrid { public class SelectColorFromGrid : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new SelectColorFromGrid()); } public SelectColorFromGrid() { Title = "Select Color from Grid"; SizeToContent = SizeToContent .WidthAndHeight; // Create StackPanel as content of window. StackPanel stack = new StackPanel(); stack.Orientation = Orientation .Horizontal; Content = stack; // Create do-nothing button to test tabbing. Button btn = new Button(); btn.Content = "Do-nothing button\nto test tabbing"; btn.Margin = new Thickness(24); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(btn); // Create ColorGridBox control. ColorGridBox clrgrid = new ColorGridBox(); clrgrid.Margin = new Thickness(24); clrgrid.HorizontalAlignment = HorizontalAlignment.Center; clrgrid.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(clrgrid); // Bind Background of window to selected value of ColorGridBox. clrgrid.SetBinding(ColorGridBox .SelectedValueProperty, "Background"); clrgrid.DataContext = this; // Create another do-nothing button. btn = new Button(); btn.Content = "Do-nothing button\nto test tabbing"; btn.Margin = new Thickness(24); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(btn); } } }



In particular, I want you to test the keyboard interface of ColorGridBox. You can navigate horizontally with the Left and Right arrow keys, and vertically with the Up and Down arrow keys. (Unlike the custom ColorGrid control, ColorGridBox does not wrap from one column or row to another. Nor does it jump to the next control when it gets to the beginning or the end. These are minor concerns, I think.) Page Up and Page Down go to the top and bottom of a column. Home goes to the first item, and End to the last.

I am convinced that ListBox implements a generalized keyboard navigation logic that is based on the relative position of items as they are displayed on the screen. This becomes more evident in the following program, which substitutes the RadialPanel from Chapter 12 in the ListBox and uses it to display Rectangle elements for all 141 colors in the Brushes class.

This class is similar to ColorGridBox in that it inherits from ListBox, and the constructor begins by setting the ItemsPanel property of ListBox to an ItemsPanelTemplate based on a FrameworkElementFactory for an alternative panel.

ColorWheel.cs

[View full width]

//------------------------------------------- // ColorWheel.cs (c) 2006 by Charles Petzold //------------------------------------------- using Petzold.CircleTheButtons; using System; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.SelectColorFromWheel { class ColorWheel : ListBox { public ColorWheel() { // Define the ItemsPanel template. FrameworkElementFactory factoryRadialPanel = new FrameworkElementFactory(typeof(RadialPanel)); ItemsPanel = new ItemsPanelTemplate (factoryRadialPanel); // Create the DataTemplate for the items. DataTemplate template = new DataTemplate(typeof(Brush)); ItemTemplate = template; // Create a FrameworkElementFactory based on Rectangle. FrameworkElementFactory elRectangle = new FrameworkElementFactory(typeof(Rectangle)); elRectangle.SetValue(Rectangle .WidthProperty, 4.0); elRectangle.SetValue(Rectangle .HeightProperty, 12.0); elRectangle.SetValue(Rectangle .MarginProperty, new Thickness(1, 8, 1, 8)); elRectangle.SetBinding(Rectangle .FillProperty, new Binding("")); // Use that factory for the visual tree. template.VisualTree = elRectangle; // Set the items in the ListBox. PropertyInfo[] props = typeof(Brushes) .GetProperties(); foreach (PropertyInfo prop in props) Items.Add((Brush)prop.GetValue (null, null)); } } }



After setting the ItemsPanel property, the constructor then goes on to define a DataTemplate object that it assigns to the ItemTemplate property of Listbox. The DataTemplate indicates that the ListBox items are of type Brush. The visual tree consists of a single element of type Rectangle, and the Fill property is bound with the item itself (indicated by an empty string argument to the Binding constructor).

The constructor concludes by filling up the ListBox with all the static properties of the Brushes class. It is not necessary to set SelectedValuePath for the ListBox because the items in the ListBox are actually Brush items. Without the custom ItemTemplate, they would be displayed as hexadecimal RGB values.

The SelectColorFromWheel program, which is almost identical to SelectColorFromGrid, lets you experiment with the ColorWheel control.

SelectColorFromWheel.cs

[View full width]

//--------- -------------------------------------------- // SelectColorFromWheel.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.SelectColorFromWheel { public class SelectColorFromWheel : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new SelectColorFromWheel()); } public SelectColorFromWheel() { Title = "Select Color from Wheel"; SizeToContent = SizeToContent .WidthAndHeight; // Create StackPanel as content of window. StackPanel stack = new StackPanel(); stack.Orientation = Orientation .Horizontal; Content = stack; // Create do-nothing button to test tabbing. Button btn = new Button(); btn.Content = "Do-nothing button\nto test tabbing"; btn.Margin = new Thickness(24); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(btn); // Create ColorWheel control. ColorWheel clrwheel = new ColorWheel(); clrwheel.Margin = new Thickness(24); clrwheel.HorizontalAlignment = HorizontalAlignment.Center; clrwheel.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(clrwheel); // Bind Background of window to selected value of ColorWheel. clrwheel.SetBinding(ColorWheel .SelectedValueProperty, "Background"); clrwheel.DataContext = this; // Create another do-nothing button. btn = new Button(); btn.Content = "Do-nothing button\nto test tabbing"; btn.Margin = new Thickness(24); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(btn); } } }



The keyboard interface is very interesting. The Up and Down arrow keys only move the selected color on the left half or the right half of the circle. They can't move the selected color from one side to the other. Similarly, the Left and Right arrow keys work only on the top half or the bottom half. To move the selected color all the way around (clockwise, for example), you need to switch from the Right arrow to the Down arrow in the upper-right quadrant, and then from the Down arrow to the Left arrow on the lower-right quadrant, and so forth.

Giving a ListBox a whole new look is a very powerful technique. It is why you should think of ListBox any time you need the user to select one item out of many. Think first of that abstract nature of the ListBox and then how you can alter the control to make it look and behave exactly the way you want.




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