Windows Forms Programming With C#
Windows Forms Programming With C#
Windows Forms Programming With C#
List controls
10.1 List boxes 315 10.4 Combo box edits 339
10.2 Multiselection list boxes 325 10.5 Owner-drawn lists 343
10.3 Combo boxes 333 10.6 Recap 352
This chapter continues our discussion of the Windows Forms controls available in the
.NET Framework. The controls we saw in chapter 9 each presented a single item, such
as a string of text or a button with associated text. In this chapter we will look at some
controls useful for presenting collections of items in Windows-based applications.
While it is certainly possible to use a multiline Textbox control to present a
scrollable list of items, this control does not allow the user to select and manipulate
individual items. This is where the ListBox and other list controls come in. These
controls present a scrollable list of objects that can be individually selected, high-
lighted, moved, and otherwise manipulated by your program. In this chapter we will
look at the ListBox and ComboBox controls in some detail. We will discuss the fol-
lowing topics:
• Presenting a collection of objects using the ListBox class.
• Supporting single and multiple selections in a list box.
• Drawing custom list items in a list box.
• Displaying a selection using the ComboBox class.
• Dynamically interacting with the items in a combo box.
314
Note that the ListView and TreeView classes can also be used with collections of
objects. These classes are covered in chapters 14 and 15.
We will take a slightly different approach to presenting the list controls here.
Rather than using the MyPhotos application we have come to know and love, this
chapter will build a new application for displaying the contents of an album, using the
existing MyPhotoAlbum.dll library. This will demonstrate how a library can be reused
to quickly build a different view of the same data. Our new application will be called
MyAlbumEditor, and is shown in figure 10.1.
Figure 10.1
The MyAlbumEditor appli-
cation does not include a
menu or status bar.
Let’s see how to use some of these members to display the list of photographs con-
tained in an album. The following steps create a new MyAlbumEditor application.
We will use this application throughout this chapter to demonstrate how various con-
trols are used. Here, we will open an album and display its contents in a ListBox
using some of the members inherited from ListControl.
Action Result
1 Create a new project called The new project appears in the Solution Explorer window,
“MyAlbumEditor.” with the default Form1 form shown in the designer
How-to window.
Use the File menu, or the
keyboard shortcut
Ctrl+Shift+N. Make sure you
close your existing solution, if
any.
These steps should be familiar to you if you have been following along from the begin-
ning of the book. Since we encapsulated the PhotoAlbum and Photograph classes
in a separate library in chapter 5, these objects, including the dialogs created in
chapter 9, are now available for use in our application. This is quite an important
point, so I will say it again. The proper encapsulation of our objects in the MyPhoto-
Action Result
Action Result
Our form is now ready. You can compile and run if you like. Before we talk about this
in any detail, we will add some code to make our new ListBox display the photo-
graphs in an album.
Some of the new code added by the following steps mimics code we provided for
our MyPhotos application. This is to be expected, since both interfaces operate on
photo album collections.
Action Result
Action Result
if (result == DialogResult.Yes)
{
_album.Save();
}
}
_album.Clear();
}
15 Add a Click handler for the Open button private void btnOpen_Click
to open an album and assign it to the (object sender, System.EventArgs e)
{
ListBox. CloseAlbum();
How-to
using (OpenFileDialog dlg
a. Close any previously open album. = new OpenFileDialog())
b. Use the OpenFileDialog class to {
allow the user to select an album. dlg.Title = "Open Album";
dlg.Filter = "abm files (*.abm)"
c. Use the PhotoAlbum.Open method + "|*.abm|All Files (*.*)|*.*";
to open the file. dlg.InitialDirectory
= PhotoAlbum.DefaultDir;
d. Assign the album’s file name to the
title bar of the form. try
e. Use a separate method for updating {
if (dlg.ShowDialog()
the contents of the list box. == DialogResult.OK)
{
_album.Open(dlg.FileName);
this.Text = _album.FileName;
UpdateList();
}
}
catch (Exception)
{
MessageBox.Show("Unable to open "
+ "album\n" + dlg.FileName,
"Open Album Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
}
Action Result
That’s it! No need to add individual photographs one by one or perform other com-
plicated steps to fill in the list box. Much of the code is similar to code we saw in pre-
vious chapters. The one exception, the UpdateList method, simply assigns the
DataSource property of the ListBox control to the current photo album.
protected void UpdateList()
{
lstPhotos.DataSource = _album;
}
The DataSource property is part of the data binding support in Windows Forms.
Data binding refers to the idea of assigning one or more values from some source of
data to the settings for one or more controls. A data source is basically any array of
objects, and in particular any class that supports the IList interface.1 Since the
PhotoAlbum class is based on IList, each item in the list, in this case each Pho-
tograph, is displayed by the control. By default, the ToString property for each
contained item is used as the display string. If you recall, we implemented this
method for the Photograph class in chapter 5 to return the file name associated
with the photo.
Compile and run your code to display your own album. An example of the out-
put is shown in figure 10.2. In the figure, an album called colors.abm is displayed,
with each photograph in the album named after a well-known color. Note how the
GroupBox controls display their keyboard access keys, namely Alt+A and Alt+P.
When activated, the focus is set to the first control in the group box, based on the
assigned tab order.
1
We will discuss data binding more generally in chapter 17.
You will also note that there is a lot of blank space in our application. Not to worry.
These spaces will fill up as we progress through the chapter.
TRY IT! The DisplayMember property for the ListBox class indicates the name
of the property to use for display purposes. In our program, since this prop-
erty is not set, the default ToString property inherited from the Object
class is used. Modify this property in the UpdateList method to a prop-
erty specific to the Photograph class, such as “FileName” or “Caption.”
Run the program again to see how this affects the displayed photographs.
The related property ValueMember specifies the value returned by
members such as the SelectedValue property. By default, this property
will return the object instance itself.
10.1.2 HANDLING SELECTED ITEMS
As you might expect, the ListBox class supports much more than the ability to display
a collection of objects. Particulars of this class are summarized in .NET Table 10.2. In
the MyAlbumEditor application, the list box is a single-selection, single-column list
corresponding to the contents of the current album. There are a number of different
features we will demonstrate in our application. For starters, let’s display the dialogs we
created in chapter 9.
The album dialog can be displayed using a normal button. For the PhotoEdit-
Dlg dialog, we would like to display the properties of the photograph that are cur-
rently selected in the list box. As you may recall, this dialog displays the photograph
at the current position within the album, which seemed quite reasonable for our
MyPhotos application. To make this work here, we will need to modify the current
position to correspond to the selected item.
SelectionMode Gets or sets how items are selected in the list box.
TopIndex Gets the index of the first visible item in the list.
BeginUpdate Prevents the control from painting its contents while
items are added to the list box.
FindString Returns the index of the first item with a display value
Public beginning with a given string.
Methods
GetSelected Indicates whether a specified item is selected.
Action Result
Action Result
In the code to display the Photograph Properties dialog, note how the SelectedIn-
dex property is used. If no items are selected, then SelectedIndex will contain the
value –1, and the current position in the album is not modified. When a photograph
is actually selected, the current position is updated to the selected index. This assign-
ment relies on the fact that the order of photographs in the ListBox control matches
the order of photographs in the album itself.
if (lstPhotos.SelectedIndex >= 0)
_album.CurrentPosition = lstPhotos.SelectedIndex;
For both dialogs, a C# using block ensures that any resources used by the dialog are
cleaned up when we are finished. We also call UpdateList to update our applica-
tion with any relevant changes made. In fact, neither property dialog permits any
changes that we would display at this time. Even so, updating the list is a good idea in
case we add such a change in the future.
Compile and run your application to ensure that the dialog boxes display cor-
rectly. Note how easily we reused these dialogs in our new application. Make some
changes and then reopen an album to verify that everything works as you expect.
One minor issue with our application occurs when the album is empty. When a
user clicks the photo’s Properties button, nothing happens. This is not the best user
interface design, and we will address this fact in the next section.
So far our application only allows a single item to be selected at a time. List boxes can
also permit multiple items to be selected simultaneously—a topic we will examine next.
Action Result
Action Result
3 Set the Enabled property for the four The code in the InitializeComponent method
buttons in the Photographs group box for all four buttons is modified so that their
to false. Enabled properties are set to false.
How-to btnMoveUp.Enabled = false;
. . .
a. Click the first button. btnMoveDown.Enabled = false;
b. Hold down the Ctrl key and click the . . .
other buttons so that all four buttons
are highlighted.
c. Display the Properties window.
d. Set the Enabled item to False.
Our next task will be to provide an implementation for these buttons. We will pick
up this topic in the next section.
10.2.2 HANDLING THE MOVE UP AND MOVE DOWN BUTTONS
Now that our list box allows multiple selections, we need to implement our three but-
tons that handle these selections from the list. This will permit us to discuss some col-
lection and list box methods that are often used when processing multiple selections
in a list.
We will look at the Move Up and Move Down buttons first. There are two prob-
lems we need to solve. The first is that our PhotoAlbum class does not currently pro-
vide an easy way to perform these actions. We will fix this by adding two methods to
our album class for this purpose.
The second problem is that if we move an item, then the index value of that item
changes. For example, if we want to move items 3 and 4 down, then item 3 should
move to position 4, and item 4 to position 5. As illustrated in figure 10.3, if we first
Figure 10.3 When the third item in the list is moved down, the original
fourth item moves into position 3.
The trick here, as you may realize, is to move item 4 first, and then move item 3. In
general terms, to move multiple items down, we must move the items starting from
the bottom. Conversely, to move multiple items up, we must start at the top.
We will begin with the new methods required in the PhotoAlbum class.
Set the version number of the MyPhotoAlbum library to 10.2.
Action Result
With these methods in place, we are ready to implement Click event handlers for our
Move Up and Move Down buttons. These handlers are shown in the following steps:
Action Result
_bAlbumChanged = true;
UpdateList();
_bAlbumChanged = true;
UpdateList();
Both of these methods employ a number of members of the ListBox class. Let’s
examine the Move Down button handler in detail as a way to discuss these changes.
Action Result
_bAlbumChanged = true;
UpdateList();
}
}
This code uses the SelectedItems property to retrieve the collection of selected
objects. This property is used to determine how many items are selected so that our
message to the user can include this information.
int n = lstPhotos.SelectedItems.Count;
Figure 10.4
The dropdown list for a Combo-
Box is hidden until the user
clicks on the small down arrow
to reduce the amount of space
required for the control on the
DropDownStyle Gets or sets the style used to display the edit and list
box controls in the combo box.
DropDownWidth Gets or sets the width of the list box portion of the
control.
SelectedText Gets or sets any text that is selected in the text box
portion of the control.
The steps required to create the combo box for our application are as follows:
Action Result
1 Delete the Open button in the The button and all related code added by Visual Studio
MainForm.cs [Design] window. are removed from the MainForm.cs source file. Any
nonempty event handlers, in this case btnOpen_Click,
remain in the file and must be removed manually.
2 Drag a ComboBox control into
the left side of the Albums group
box as shown in the graphic.
Settings
Property Value
(Name) cmbxAlbums
Anchor Top, Left, Right
DropDownStyle DropDownList
Sorted True
Feel free to compile and run your program if you like. The combo box will display
the available albums, without the ability to actually open an album. Opening an
album requires that we handle the SelectedItemChanged event for our combo
box, which is the topic of the next section.
10.3.2 HANDLING THE SELECTED ITEM
Our ComboBox currently displays a selected album, but it doesn’t actually open it.
The previous section replaced the Click handler for the now-deleted Open button
with an OpenAlbum method, so all we need to do here is recognize when a new
album is selected and open the corresponding album.
The one issue we must deal with is the case where an invalid album exists. While
we initialized our control to contain only album files ending with “.abm,” it is still pos-
sible that one of these album files contains an invalid version number or other problem
that prevents the album from loading. The following steps handle this case by dis-
abling the Properties button and ListBox control when such a problem occurs. An
appropriate error message is also displayed in the title bar.
Action Result
This code provides both text and visual cues on whether the selected album was suc-
cessfully opened. Note how the SelectedItem property is used to retrieve the cur-
rent selection. Even though we know this is a string, the framework provides us an
object instance, so ToString must be called to extract the actual text.
string albumPath = cmbxAlbums.SelectedItem.ToString();
When the selected album opens successfully, the ListBox background is painted the
normal window color as defined by the system and the Properties button in the
Albums group box is enabled. Figure 10.1 at the beginning of this chapter shows the
interface with a successfully opened album. When the album fails to open, the excep-
tion is caught and the title bar on the form is set to indicate this fact. In addition, the
ListBox background is painted the default background color for controls and the
Button control is disabled.
An example of this situation appears in figure 10.5. The specified album, badal-
bum.abm, could not be opened, and between the title bar and the window this fact
should be fairly clear.
Figure 10.5
When the selected album can-
not be loaded, only the Close
button remains active.
TRY IT! The ComboBox in our application does not allow the user to manually en-
ter a new album. This could be a problem if the user has created some al-
bums in other directories. To fix this, add a ContextMenu object to the
form and associate it with the Albums group box. Add a single menu item
called “Add Album…” to this menu and create a Click event handler to
allow the user to select additional album files to add to the combo box via
the OpenFileDialog class.
Note that you have to modify the ComboBox to add the albums from
the default directory manually within the OnLoad method. At present,
since the DataSource property is assigned, the Items collection cannot
be modified directly. Use BeginUpdate and EndUpdate to add a set of
albums via the Add method in the Items collection, both in the OnLoad
method and in the new Click event handler.
The next section provides an example of how to handle manual edits within a combo box.
Figure 10.6
Note how the dropdown for the ComboBox ex-
tends outside of the Panel control. This is per-
mitted even though the control is contained by
the panel.
We will add this control to the MyAlbumEditor application in two parts. First we will
create and initialize the contents of the control, and then we will support the addition
of new photographers by hand.
Action Result
1 In the PhotoEditDlg.cs [Design] The control is removed from the form, and the code
window, delete the TextBox control generated by Visual Studio is removed as well. The
associated with the Photographer subsequent steps modify the manually entered
label. code associated with this control.
2 Place a ComboBox control on the form The MaxDropDown property here specifies that the
where the text box used to be. list portion of the combo box displays at most four
items at a time, with any remaining items
Settings
accessible via the scroll bar.
Property Value
(Name) cmbxPhotographer
MaxDropDown 4
Sorted True
Text photographer
Action Result
return true;
}
Note how this code uses both the SelectedItem and Text properties for the Com-
boBox control. The SelectedItem property retrieves the object corresponding
to the item selected in the list box, while the Text property retrieves the string
entered into the text box. Typically these two values correspond to each other, but
this is not always true, especially when the user manipulates the text value directly, as
we shall see next.
10.4.2 UPDATING THE COMBO BOX DYNAMICALLY
With our control on the form, we now need to handle manual entries in the text box.
This is normally handled via events associated with the ComboBox control. The
Validated event, discussed in chapter 9, can be used to verify that a user-provided
entry is part of the list and also add it to the list if necessary. The TextChanged
event can be used to process the text while the user is typing.
We will handle both of these events in our code. First, let’s add a Validated
event handler, and then add code to auto-complete the entry as the user types.
Action Result
Our ComboBox is now updated whenever the user enters a new photographer, and
the new entry will be available to other photographs in the same album.
Another change that might be nice is if the dialog automatically completed a par-
tially entered photographer that is already on the list. For example, if the photographer
“Erik Brown” is already present, and the user types in “Er,” it would be nice to com-
plete the entry on the user’s behalf.
Of course, if the user is typing “Erin Smith,” then we would not want to prevent
the user from doing so. This can be done by causing the control to select the auto-
filled portion of the name as the user types. You will be able to experiment with this
behavior yourself after following the steps in the subsequent table.
Action Result
cmbxPhotographer.SelectionStart
= text.Length;
cmbxPhotographer.SelectionLength
= newText.Length - text.Length;
}
}
When a match is found, the text associated with this match is extracted from the list
and assigned to the text box portion of the control.
if (index >= 0)
{
// Found a match
string newText = cmbxPhotographer.Items[index].ToString();
cmbxPhotographer.Text = newText;
The additional text inserted into the text box is selected using the SelectionStart
and SelectionLength properties. The SelectionStart property sets the cursor
location, and the SelectionLength property sets the amount of text to select.
cmbxPhotographer.SelectionStart = text.Length;
cmbxPhotographer.SelectionLength = newText.Length - text.Length;
}
TRY IT! The list portion of the control can be forced to appear as the user types with
the DroppedDown property. Set this property to true in the Text-
Changed handler to display the list box when a match is found.
You may have realized that this handler introduces a slight problem with
the use of the backspace key. When text is selected and the user presses the
backspace key, the selected text is deleted rather than the previously typed
character as a user would normally expect. Fix this behavior by handling the
KeyPress event, discussed in chapters 9 and 12, to force the control to de-
lete the last character typed rather than the selected text.
Before leaving our discussion of ListControl objects, it is worth noting that the
controls we have discussed so far all contain textual strings. The .NET Framework
automatically handles the drawing of these text strings within the list window. It is
possible to perform custom drawing of the list elements, in a manner not too differ-
ent than the one we used for our owner-drawn status bar panel in chapter 4.
As a final example in this chapter, let’s take a look at how this is done.
Figure 10.7
The ListBox here shows both
the image and the caption for
each photograph. Note how
none of the items are selected
in this list.
We will permit the user to switch between the thumbnail and pure text display using
a context menu associated with the list box. This menu will be somewhat hidden,
since users will not know it exists until they right-click on the list control. A hidden
menu is not necessarily a good design idea, but it will suffice for our purposes. We
will begin our example by adding this new menu.
10.5.1 ADDING A CONTEXT MENU
Since we would like to dynamically switch between an owner-drawn and a frame-
work-drawn control, we need a way for the user to select the desired drawing method.
We will use a menu for this purpose, and include a check mark next to the menu
when the thumbnail images are shown. Context menus were discussed in chapter 3,
so the following steps should be somewhat familiar.
Action Result
The Click handler for our new menu simply toggles its Checked flag and sets the
drawing mode based on the new value. The DrawMode property is used for both the
ListBox and ComboBox controls to indicate how each item in the list will be drawn.
The possible values for this property are shown in .NET Table 10.6. Since the size of our
photographs in an album may vary, we allow the size of each element in the list to vary
as well. As a result, we use the DrawMode.OwnerDrawVariable setting in our code.
The ItemHeight property contains the default height for each item in the list.
When the DrawMode property is set to Normal, we set this property to the height of
the current font plus 2 pixels. For our owner-drawn list, the item height depends on
the size of the photograph we wish to draw. This requires that we assign the item
height dynamically, and this is our next topic.
The following steps implement the code required for the MeasureItem event. Fig-
ure 10.8 illustrates the various measurements used to determine the width and height
of the item.
Action Result
For the item’s height, this code uses the larger of the scaled item’s height and the
ListBox control’s font height, plus 2 pixels as padding between subsequent items in
the list.
e.ItemHeight = Math.Max(scaledRect.Height, lstPhotos.Font.Height) + 2;
For the item’s width, the width of the scaled image plus the width of the drawn string
is used, plus 2 pixels as padding between the image and the text. To do this, the
Our final task is to draw the actual items using the DrawItem event.
10.5.3 DRAWING THE LIST ITEMS
As you may recall, the DrawItem event and related DrawItemEventArgs class were
discussed in chapter 4. See .NET Table 4.4 on page 119 for an overview of the
DrawItemEventArgs class.
Before we look at how to draw the list items in our application, let’s make a small
change to the Photograph class to improve the performance of our drawing. Since we
may have to draw an item multiple times, it would be nice to avoid drawing the
thumbnail from the entire image each time. To avoid this, let’s create a Thumbnail
property in our Photograph class to obtain a more appropriately sized image.
Set the version number of the MyPhotoAlbum library to 10.5.
Action Result
if (_thumbnail != null)
_thumbnail.Dispose();
_bitmap = null;
_thumbnail = null;
}
3 Add a static constant to store the private const int ThumbSize = 90;
default width and height for a
thumbnail.
Action Result
_thumbnail = bm;
}
return _thumbnail;
}
}
This ensures that we will not have to load up and scale the full-size image every time
we draw an item. With this property in place, we have everything we need to draw
our list items.
Action Result
Action Result
How-to
a. Use DrawImage to paint
the thumbnail into the
rectangle.
b. Use DrawRectangle to
paint a black border
around the image.
Action Result
Well done! You’ve just created your first owner-drawn list box. This code provides a
number of features that should be useful in your own applications. It includes how to
draw the image as well as the string for the item, and how to handle selected and dese-
lected text. Compile and run the application. Click the Thumbnail context menu and
watch the list display thumbnails. Click it again and the list reverts to normal strings.
TRY IT! Our list box currently displays the file name for each photograph when
DrawMode is Normal, and the caption string when DrawMode is Owner-
DrawVariable. It would be nice if the user could select which string to
display in either mode.
Try implementing this change by adding additional entries to the
ListBox control’s context menu. Add a parent menu called “Display As,”
and a submenu to allow the user to select between “File Name,” “Caption,”
and “Photographer.” Based on their selection, set the DisplayMember
property for the list to the appropriate property string.
In normal draw mode, the framework picks up the DisplayMember
property automatically. For the DrawItem event, you will need to retrieve
the appropriate string based on the DisplayMember value. You can use
string comparisons to do this, or use the System.Reflection name-
space classes and types. This namespace is not discussed in detail in this
book, but the following code excerpt can be used at the end of your Draw-
Item event handler to dynamically determine the value associated with the
property corresponding to a given string.
PropertyInfo pi = typeof(Photograph).
GetProperty(lstPhotos.DisplayMember);
object propValue = pi.GetValue(p, null);
g.DrawString(propValue.ToString(), e.Font,
_textBrush, textRect);
10.6 RECAP
This chapter discussed the basic list classes in the .NET Framework, namely the
ListBox and ComboBox controls. We created a new application for this purpose,
the MyAlbumEditor application, and built this application from the ground up using
our existing MyPhotoAlbum library.
We began with a discussion of the common base class for list controls, namely the
ListControl class, followed by a discussion of both single and multiple selection in
the ListBox class. We saw how to enable and disable controls on the form based on
the number of items selected, and how to handle double clicks for quick access to a
common operation.
For the ComboBox class, we created a noneditable ComboBox to hold the list of
available album files. Modifying the selected value automatically closed the previous
album and opened the newly selected one. We then looked at an editable ComboBox
for our photographer setting in the PhotoEditDlg dialog box. We discussed how to
dynamically add new items to the list, and how to automatically select an existing item
as the user is typing.
We ended with a discussion of owner-drawn list items by providing the option
of displaying image thumbnails in our list box. We saw how to draw both images and
text, including selected text.
There are additional controls than those discussed in chapters 9 and 10, of course.
We will see some of these in the next chapter, and others as we continue our progres-
sion through the book. In chapter 11 we continue with our new MyAlbumEditor
application, and look at Tab pages as a way to organize large amounts of information
on a single form.