ou create an “owner-draw” ListBox or ComboBox when you want to bypass the control’s automatic item display in order to do something special, such as display an image for each item or display a list in which the items aren’t all the same size. The .NET framework makes it simple to create these custom item lists. In this article, you’ll see how to populate list and combobox controls with items you draw yourself.
The only thing you need to do to create an owner-drawn list or combo box is to set the DrawMode property to either OwnerDrawFixed, or OwnerDrawVariable. The DrawMode property has three possible settings: Normal (the default), in which the system handles displaying the items automatically; OwnerDrawFixed, which you should use when you want to draw the items yourself, but they’re all the same height and width; and OwnerDrawVariable, which you use to draw items that vary in height or width. When you select the OwnerDrawFixed setting, you must implement a DrawItem method, which the ListBox calls whenever it needs to draw an item. When you select the OwnerDrawVariable setting, you must implement both the DrawItem and a MeasureItem method. The MeasureItem method lets you set the size of the item to be drawn. When you select the Normal setting, the system does not fire either the MeasureItem or the DrawItem methods.
There are a couple of restrictions. You can’t create variable-height items for multicolumn ListBoxes, and CheckedListBoxes don’t support either of the owner-drawn DrawMode settings.
Example 1: Listing Files and Folders
Suppose you want to list the files and directories in a folder along with the associated system icons appropriate to the type of file. There are several steps required to accomplish the task:
- Validate the requested directory path
- Retrieve the files and subfolders from a directory
- Iterate through them, retrieving their types and names
- Find the appropriate icon for each file type
- Draw the items for the ListBox containing the appropriate icon and text
|Figure 1: The ListFilesAndFolders form contains an owner-draw ListBox that displays folder and file names, along with their system-associated icons.
Figure 1 depicts a Web form with the finished ListBox control that displays all the files in a specified directory along with their corresponding system icons. To use the example, enter a directory path in the first text field. The form ensures that the entered path is valid, and then follows the steps listed above to fill a ListBox shown in a separate dialog. The user can double-click an item in the list, or select an item and click OK. The constructor for the dialog form (ListFilesAndFolders) requires a path string.
The first step in the application’s logic is to validate the path string the user enters into the main form. The System.IO.DirectoryInfo class has an Exists method that returns True if the directory exists:
Dim di As DirectoryInfo di = New DirectoryInfo(Me.txtPath.Text) If Not di.Exists Then txtPath.ForeColor = System.Drawing.Color.Red Beep() End If
The code turns the TextBox text red and plays a warning sound if the entered path is invalid; otherwise, it creates a new instance of the ListFilesAndFolders form, passing the validated path string to its constructor.
Dim frmFiles As New _ ListFilesAndFolders(Me.txtPath.Text)
The ListFilesAndFolders form contains a ListBox, an OK button, and a Cancel button.
The ListFilesAndFolders form constructor calls a FillList method that retrieves the files and folders in the specified path and fills a ListBox control with the icons and names, suspending the control’s display until the method completes.
Sub FillList(ByVal aPath As String) Dim fsi As FileSystemInfo lstFiles.BeginUpdate() lstFiles.Items.Clear() files = New DirectoryInfo(aPath).GetFileSystemInfos For Each fsi In files lstFiles.Items.Add(fsi) Next lstFiles.EndUpdate() End Sub
The DirectoryInfo.GetFileSystemInfos method returns an array of FileSystemInfo objects. The code iterates through the returned array and adds each item to the ListBox’s Items collection.
The act of adding the items to the ListBox doesn’t cause the ListBox to draw them; instead, the ListBox fires a DrawItem event whenever the ListBox needs to display an item. In this case, you want to draw an icon and the name of a FileSystemInfo object that represents a file or folder. Because this is an owner-draw control, you need to create the DrawItem method to create the item display.
Private Sub lstFiles_DrawItem( _ ByVal sender As Object, _ ByVal e As System.Windows.Forms.DrawItemEventArgs) _ Handles lstFiles.DrawItem ' the system sometimes calls this method with ' an index of -1. If that happens, exit. If e.Index
In the DrawItem method, the code calls a shared GetSmallIcon method exposed by the IconExtractor class (see Listing 1), which, when passed a FileSystemInfo object, calls the Win32 SHGetFileInfo API to extract the icon for the file type represented by that object. The IconExtractor class exposes two public shared methods, GetLargeIcon and GetSmallIcon, both of which simply call a private GetIcon method that returns the large (32 x 32) and small (16 x 16) icon versions, respectively.
Public Shared Function GetSmallIcon( _ ByVal fsi As FileSystemInfo) As Icon Return IconExtractor.GetIcon _ (fsi, SHGFI_SMALLICON) End Function Public Shared Function GetLargeIcon( _ ByVal fsi As FileSystemInfo) As Icon Return IconExtractor.GetIcon _ (fsi, SHGFI_LARGEICON) End Function Private Shared Function GetIcon( _ ByVal fsi As FileSystemInfo, _ ByVal anIconSize As Integer) As Icon Dim aSHFileInfo As New SHFILEINFO() Dim cbFileInfo As Integer = _ Marshal.SizeOf(aSHFileInfo) Dim uflags As Integer = SHGFI_ICON Or _ SHGFI_USEFILEATTRIBUTES Or anIconSize Try SHGetFileInfo(fsi.FullName, fsi.Attributes, _ aSHFileInfo, cbFileInfo, uflags) Return Icon.FromHandle(aSHFileInfo.hIcon) Catch ex As Exception Return Nothing End Try End Function
The GetSmallIcon and GetLargeIcon methods both accept a FileSystemInfo object. Internally, the GetIcon method uses the FileSystemInfo object to pass the file name and file attributes to the SHGetFileInfo API call. After drawing the icon, the DrawItem event handler calls the Graphics.DrawString method to place the file name on the image next to the icon. The ListBox calls the DrawItem method repeatedly, for each item in its Items collection.
The DrawItemEventArgs argument to the DrawItem event handler exposes an Index property whose value is the index of the item to be drawn. Watch out! The system raises the DrawItem event with an index value of -1 when the Items collection is empty. When that happens you should call the DrawItemEventArgs.DrawBackground() and DrawFocusRectangle() methods and then exit. The purpose of raising the event is to let the control draw a focus rectangle so that users can tell it has the focus, even when there are no items present. The code traps for that condition, calls the two methods, and then exits the handler immediately.
Users can select an item and close the ListFilesAndFolders form either by selecting an item and then clicking the OK button, or by double-clicking an item. Either way, the form sets a public property called SelectedItem, sets another public property called Cancel and then closes. The main form then displays the filename of the selected item in the Result field.
Example 2: Drawing Items with Variable Width and Height
Here's another example. The items you create don't all have to be the same width and height. To create an owner-drawn list or combo box with variable item heights and widths, set the DrawMode property to OwnerDrawVariable. Then, implement a method that handles the MeasureItem event, which accepts a sender (Object), and a System.Windows.Forms.MeasureItemEventArgs argument. The sample form frmColorListBox displays all the known system colors and their names in a combo box. The items themselves vary between 20 and 40 pixels in height. It's contrived and ugly (see Figure 2), but serves to illustrate the point.