Using the ListView Control

Using the ListView Control

As well known as the TreeView control is the ListView control. Every file dialog box features a ListView control as its most important control. Windows Explorer and the Registry Editor use it, as does any application that wants to list data in various ways. Chances are good that you’ll want to use the ListView control in your own applications. The following subsections will guide you through the intricacies of the ListView control.

Implementing a ListView Control

Although there’s designer support for ListView too, this time we’ll start with writing code. The example used throughout this section is located in the folder ListviewDrives on the companion CD. It shows how a ListView control can be used to view the drives accessible on the local computer.

The Four Views

A ListView control supports four distinct ways to display its items: a small icons view, a large icons view, a list view, and a details view. Every user knows these views from Windows Explorer, and you can choose to support any or all of the views in your applications as you see fit. The following subsections describe what’s necessary to support the four views.

Working with small and large icons

The small icons and large icons views are very similar—both require an ImageList object for the icons to be displayed alongside the ListViewItem object’s text. The ImageList property for the small icons view is appropriately named SmallImageList, whereas the large icons view is controlled via the LargeImageList property. As with the TreeView control, you can set the ImageList properties in the Properties window at design time or programmatically at run time.

The process of creating an ImageList object from single images was shown earlier in this chapter, in the section “Designing a TreeView Control.” There’s another technique, however, that’s faster and more convenient—placing all icon images in a single image strip. An example image strip is shown in Figure 16-12.

Figure 16-12.
A single image strip can contain all ImageList images.

If you’ve used Visual C++ to write Microsoft Foundation Classes (MFC) applications, you know what this is all about because that’s how CToolbar works: you provide an image strip, a transparent color, and a size for the individual bitmaps in the image strip. Given that information, CToolbar extracts the button bitmaps and paints its toolbar. This is the way it works in ImageList objects too, as shown here:

lvDrives.LargeImageList = new ImageList();
Bitmap icoLarge = new Bitmap(GetType(), "LvIconsLarge.bmp");
icoLarge.MakeTransparent(Color.FromArgb(0xff, 0x00, 0xff));
Size sizeImages = new Size(32,32);
lvDrives.LargeImageList.ImageSize = sizeImages;
lvDrives.LargeImageList.Images.AddStrip(icoLarge);

You first create a new ImageList object for the view you’re customizing. Then you load the image strip into a new Bitmap object and define the transparent color using the MakeTransparentColor method. Because images in an ImageList object have a default Size property value, you have to assign this property the size of the bitmaps in the image strip. Now all that’s left to do is to assign the Bitmap object to the ImageList object, which is done by the AddStrip method.

When programming a ListView control that supports the small icons or large icons view, you typically load the ImageList objects in the constructor of your form or custom ListView control, or at the latest before you actually add items to the ListView control. You can add an item with an icon to your ListView control as follows:

ListViewItem lvi = new ListViewItem();
lvi.ImageIndex = 1;
lvi.Text = "Drive C:\\";
lvDrives.Items.Add(lvi);

The ImageIndex property is used for both the small icons and large icons views. To programmatically switch a ListView control to the small icons view, use the following code:

lvDrives.View = View.SmallIcon;

To switch to the large icons view, set the View property to View.LargeIcon.

Listing the contents

When you need to view a large number of items using multiple columns, the list view is what you’re looking for. Enabling the list view is as simple as enabling any other view, as shown here:

lvDrives.View = View.List;

To display a small icon alongside the text of an item, set the ImageIndex property for each ListViewItem object in the ListView object. If this property is omitted, the list view displays text only, which can be desirable in certain applications.

Viewing details

The details view of a ListView control looks a lot like a read-only data grid. The details view has multiple columns, and you can sort on every column; however, you can’t edit the data. Switching to the details view enables the user to display more detailed information about the properties of an item than might be available in one of the other views.

Before the details view works the way you want it to, you must do one thing: define the columns (their headers and sizes at least) that will be displayed. For example, to add two columns named Drive and Type, add the following code to the initialization of the ListView control:

lvDrives.Columns.Add("Drive", 100, HorizontalAlignment.Left);
lvDrives.Columns.Add("Type", 150, HorizontalAlignment.Left);

In this example, the second column is wider than the first one, and for both the text is left-aligned. (Other options are Center and Right.)

To switch to the details view, use the following code:

lvDrives.View = View.Details;

Figure 16-13 shows the details view for our sample application ListViewDrives.

Figure 16-13.
The sample application ListViewDrives in details view.
Filling the ListView Control

A ListView control’s Items collection contains any number of ListViewItem objects, which define how an element is shown in each of the four view styles. In its most simple form, a ListViewItem object can be added with the following command:

lvDrives.Items.Add(new ListViewItem(@"Drive C:\"));

This item has no image associated with it, only text. It would appear as an empty icon in both the small icons and large icons views. If you want to display an icon, you have to set the ImageIndex property of the ListViewItem object prior to adding the item, as shown here:

ListViewItem lvi = new ListViewItem(@"Drive C:\");
lvi.ImageIndex = 1;
lvDrives.Items.Add(lvi);

You could also pass the ImageIndex property via the constructor, as follows:

ListViewItem lvi = new ListViewItem(@"Drive C:\", 1);

The ImageIndex property is zero-based, like all other arrays in the .NET Framework. When switching to the small icons or large icons view, the second image is taken from the assigned ImageList object.

To put everything together, take a look at the ListviewDrives application on the companion CD. This application enumerates all drives on the local computer, reads drive name and drive type, and displays this information in a ListView control that can be switched between its four views using menu commands.

The filling of the ListView control is handled by the ListDrives function, as shown here:

protected void ListDrives() 
{
    string[] drives = Directory.GetLogicalDrives();
    string strDriveType = "";
    for (int i=0; i < drives.Length; i++)
    {
        string strDriveName = Win32.GetVolumeName(drives[i]);
        ListViewItem lvi = new ListViewItem();

        // Note: Network drives show up as local.
        switch(Win32.GetDriveType(drives[i])) 
        {
            case Win32.DRIVE_CDROM:
                strDriveType = "Compact Disc";
                lvi.ImageIndex = 1;
                break;
            case Win32.DRIVE_FIXED:
                strDriveType = "Local Disc";
                lvi.ImageIndex = 0;
                break;
            case Win32.DRIVE_REMOVABLE:
                strDriveType = "Floppy";
                lvi.ImageIndex = 2;
                break;
            default:
                goto case Win32.DRIVE_FIXED;
        }
        if (0 == strDriveName.Length) strDriveName = strDriveType;
        lvi.Text = strDriveName + " (" + drives[i].Substring(0, 2) + ")";
        lvi.SubItems.Add(strDriveType);
        this.lvDrives.Items.Add(lvi);
    }
}

A list of available drives can be obtained by calling the static GetLogical­Drives method. This method provides you with the drive letters but doesn’t provide drive name or drive type. To get that information, you’ll have to dig a little deeper—that is, you’re forced to use platform invoke (P/Invoke) calls, which in this sample application are encapsulated in the Win32 class, shown in the following code:

public sealed class Win32
{
    public const uint DRIVE_UNKNOWN     = 0;
    public const uint DRIVE_NO_ROOT_DIR = 1;
    public const uint DRIVE_REMOVABLE   = 2;
    public const uint DRIVE_FIXED       = 3;
    public const uint DRIVE_REMOTE      = 4;
    public const uint DRIVE_CDROM       = 5;
    public const uint DRIVE_RAMDISK     = 6;

    [DllImport("kernel32.dll")]
    public static extern uint GetDriveType(
        string lpRootPathName   // Root directory
        );

    [DllImport("kernel32.dll")]
    private static extern bool GetVolumeInformation(
        string lpRootPathName,
        StringBuilder lpVolumeNameBuffer,
        int nVolumeNameSize,
        ref int lpVolumeSerialNumber,
        ref int lpMaximumComponentLength,
        ref int lpFileSystemFlags,
        StringBuilder lpFileSystemNameBuffer,
        int nFileSystemNameSize
        );

    public static string GetVolumeName(string strRootPath)
    {
        StringBuilder sbVolumeName = new StringBuilder(256);
        StringBuilder sbFileSystemName = new StringBuilder(256);
        int nVolSerial = 0;
        int nMaxCompLength = 0;
        int nFSFlags = 0;

        bool bResult = GetVolumeInformation(strRootPath,sbVolumeName,256,
            ref nVolSerial, ref nMaxCompLength, 
            ref nFSFlags, sbFileSystemName, 256);

        if (true == bResult)
        {
            return sbVolumeName.ToString();
        }
        else
        {
            return "";
        }
    }
}

Using the information obtained by GetVolumeName, the application sets the Text property of the ListViewItem object to the volume name plus the drive letter. The call to GetDriveType is used to determine the ImageIndex value, as well as the text to be shown in the Type column in the details view. Because the Text property refers to the first column, additional column text is added using the following code:

lvi.SubItems.Add(strDriveType);

You can create almost as many subitems as you want, but don’t go overboard with information. Instead, provide additional information to the user when an item is selected.

Handling Item Selection and Activation

The first action you as a developer are interested in is when the user selects or deselects an item in the ListView control. When this happens, the SelectedIndexChanged event is triggered. This event doesn’t pass the selected items as parameters, however. You have to retrieve the selected items from the Selected­Items collection, as shown here:

private void OnSelItemChanged(object sender, System.EventArgs e)
{
    string strSelected = "";
    foreach (ListViewItem lvi in lvDrives.SelectedItems)
    {
        strSelected += " " + lvi.Text;
    }
    MessageBox.Show(strSelected);
}

If the MultiSelect property of the ListView control is set to true, this code returns all items that were selected; it returns a single item if this property is set to false. To verify whether anything was selected, check lvDrives.SelectedItems.Count for a value greater than 0. When the selection is complete, you can act on the selected items, depending on your application.

One way to act on selected items is through the ItemActivate event. This event is triggered by either a single click or a double click, depending on the Activation property setting. Table 16-3 shows all possible ItemActivation members.

Remember, to find out which item was activated, use the SelectedItems collection, as described at the beginning of this section. The event arguments for the ItemActivate event don’t provide you with this information.

RegistryViewer—TreeView and ListView Combined

As a final example, we’ll create a registry viewer that combines the RegistryTreeClass class written earlier with a ListView control that displays the values for a given subkey. Figure 16-14 shows the completed application, which is located in the RegistryViewer folder on the companion CD.

Figure 16-14.
Using the RegistryViewer sample application to browse the local computer’s registry.
Creating the User Interface

First you’ll need to design the user interface. To do this, you need one TreeView control and one ListView control. Although you can’t see the effect in Figure 16?14, this user interface adjusts to window size changes. This adjustment is accomplished using various docking styles and a splitter.

The first element to be placed on the form is the TreeView control. Set its docking style to Left, and add the splitter, which must have the same docking style. Then add the ListView control, whose docking style must be set to Fill. Next you need to configure the ListView control: in the Properties window, select Columns, and in the ColumnHeader Collection Editor, add columns named Name and Data. The Forms Designer should now look like Figure 16-15.

Figure 16-15.
Designing the user interface of the RegistryViewer application.

One more thing can be taken care of immediately: changing the TreeView class to RegistryTreeClass. This class contains almost all of the application’s functionality. The sole exception is that no data will appear in the ListView control unless we put it there.

Updating the Contents of the ListView Control

As mentioned, the ListView control’s task is to show registry values (not keys) for the currently selected subkey. Therefore, to change the contents of the ListView control according to what’s selected in the TreeView control, we have to react to an event—but which one? BeforeExpand is obviously the wrong choice; AfterSelect is definitely what we’re looking for.

After you’ve wired up the AfterSelect event handler, you must open the subkey, just as you did earlier for the TreeView control. The following code shows the AfterSelect event handler, with the code to open the subkey omitted for clarity:

private void AfterSelect(object sender, TreeViewEventArgs e)
{
    
    

    string[] astrValueNames = regSubKey.GetValueNames();
    lvValues.Items.Clear();
    for (int j=0; j < astrValueNames.Length; j++)
    {
        object val = regSubKey.GetValue(astrValueNames[j]);
        ListViewItem lvi = new ListViewItem(astrValueNames[j]);
        lvi.SubItems.Add(val.ToString());
        lvValues.Items.Add(lvi);
    }
}

With this snippet of new code, we’re all set. When the user selects a node in the RegistryViewer application, this code will first clear the ListView control of all old values and then fill the control with the values it found for the current subkey.



Part III: Programming Windows Forms