Windows DevCenter    
 Published on Windows DevCenter (http://www.windowsdevcenter.com/)
 See this if you're having trouble printing code examples


Working with Icons in Visual Basic

by Ron Petrusha
08/17/2004

The presence of icons to represent either running or launchable applications is one of the features that differentiates a graphical operating system from a nongraphical one. It's also a feature that users come to value: The effective use of icons makes it easier to find an application on the desktop, to identify a minimized application on the status bar, and to locate an application on the Start menu. In this article, we'll look at the ways Visual Basic allows you to work with icons.

Icons in Windows

Windows icons can be of two types. One is a simple icon of a standard size and a particular number of colors. The varieties are 16 by 16 pixels and 16 colors, 32 by 32 pixels and 16 colors, and 48 by 48 pixels and 256 colors. The second icon type defines a single icon in multiple formats (sizes and colors), which allows the operating system to extract the icon in the appropriate size. When an icon is available in multiple formats, the operating system decides which format most closely meets its needs. It then loads that icon and reduces or enlarges it as needed.

The operating system itself is capable of using an icon in four different general formats:

Icons can exist in stand-alone icon files, which typically have a file extension of .ico. VB and Visual Studio come with a large number of icon files, which are found in the Common\Graphics\Icons subdirectory of the directory in which Visual Studio (or Visual Basic) is installed. Icons are also included as resources in executable files and dynamic link libraries. For more details, see the Extracting Windows Icons section.

Assigning an .ico File

The most straightforward way to give your application an icon is to assign an icon (.ico) file to the Icon property of one of its forms at design time. If you assign icons to multiple forms, you can designate the form that contains the application icon by selecting it in the Make tab of the Project Properties dialog box.

When you assign an icon to the Icon property, Visual Basic generates an .frx file, which contains data about binary properties of the form and its controls. The icon that is assigned to the form's Icon property is then assigned a label and stored inside the .frx file. This is apparent if we examine Figure 1, which shows FRX Split, a shareware .frx file analysis and extraction utility.

Figure 1
Figure 1. A view of an .frx file from FRX Split

Once this happens, Visual Basic no longer needs your icon file. Instead, when it needs to retrieve the icon, it extracts it from the icon file rather than loading the file itself. When you compile, the .frx file is used to generate the executable, rather than the icon file. And as Figure 2 shows, the icon file is not included with the other project files as part of the setup routine.

Figure 2
Figure 2. Files included by the Package and Deployment Wizard

The ability to assign an icon at design time is useful if we know in advance what icon we want to assign to a form and to our application as a whole. But what if we don't know this information at design time? Or what if we want our icon to represent the state of the application as well as the application itself? The Recycle Bin, for instance, uses a wastebasket icon to represent the application. But when the Recycle Bin holds one or more deleted files, an overflowing wastebasket icon is used. When it is empty, an empty wastebasket icon is used. The icon conveys not only the application, but also its state. It would seem that we should be able to do this at runtime with code like the following:

Private Sub Form_Load()
   Me.Icon = "icon02.ico"
End Sub

However, this code generates a type mismatch. The reason is that a form's Icon property is of type IPictureDisp rather than String; Visual Basic expects the property to be assigned an image, not the name of an icon file. Instead, we can use the LoadPicture function to load our icon dynamically. If we test the following code in the design-time environment, we find that it works as expected:

Private Sub Form_Load()

   Dim strPath As String

   strPath = App.Path & "\icon02.ico"

   Me.Icon = LoadPicture(strPath)

End Sub

Because we're assigning an icon at runtime, Visual Basic does not generate a binary form (.frx) file. If we compile the application and run it in the same directory, it works just fine. However, if we move the application to a different directory but fail to move our icon file, the application generates a File Not Found error. Similarly, if we build an installation routine using the Package and Deployment Wizard, we have to add the icon file, icon02.ico, to the set of installation files, or a runtime error results.

As long as you actually remember to do it, the need to include separate icon files is just a minor annoyance. The possibility that a user of your application might inadvertently delete the file, thereby causing the application to crash, is a more serious issue. Rather than providing stand-alone icon files, it's clearly preferable to include icons either in the executable itself or in DLLs.

Using the ImageList Control

The ImageList control is a service component that provides stored images to interface objects capable of displaying them. In order to use the control, you'll have to add it to your project. It is included in the Microsoft Windows Common Controls library (comctl32.ocx). The control has one major restriction: Each image the control stores must be of the same size. If you want to store images of different sizes, you must use a separate control for each one. Once you select a size and add the first image, the size becomes read-only. The control stores its images in a ListImages collection, which consists of zero, one, or more ListImage objects. Each stored image is represented by a ListImage object.

Once you've added the ImageList control to a form, you can populate it with the icons you'd like the form to use. Typically, this is done in the design-time environment by right-clicking on the control and selecting Properties from its pop-up menu to open the control's Property Pages dialog box. (The Property window shows a different set of properties.) The General tab allows you to define the size of the images that the control will store. You should set it first, before actually assigning any images to the control. The Images tab, which is shown in Figure 3, lets you add pictures as well as define the one-based index of the image in the control's ListImages collection and the key to be assigned to the image. At runtime, you can retrieve a control from the ListImages collection's Item property by passing it either the index or the key of the image you want. Once you add an image to the control at design time, it is added to the form's .frx file; you no longer have to include the original image file with your project.

Figure 3
Figure 3. The Images tab of the ImageList control's Property Pages dialog box

You can also add images to the control dynamically at runtime by calling the ListImages collection's Add method. Although this is possible, it has the same problems as dynamically assigning an icon file to a form's Icon property: A separate set of icon files that you might overlook and that users might inadvertently delete must be included with the application.

To assign an icon to the form's Icon or DragIcon property, you use the ListImage object's ExtractIcon method. The method has no parameters and returns an IPictureDisp object representing an icon, which you can then assign to the property.

You can, for instance, download the source code for a sample phases of the moon program, IconList1.exe, that, when minimized, shows the current phase of the moon. The code that determines which icon to display and then calls the ExtractIcon method is as follows:

Private Sub ShowMoonIcon(datPhase As Date)

   Dim intMonth As Integer, intCtr As Integer, intPhase As Phases
   Dim fIcon As Boolean
   Dim datArray As Variant, datNextArray As Variant

   intMonth = Month(datPhase) - 1

   ' Iterate array
   datArray = aDates(intMonth)
   datNextArray = aDates(intMonth + 1)

   For intCtr = 0 To 3
      If datPhase > datArray(intCtr) Then
         If intCtr <= 2 Then
            If datPhase < datArray(intCtr + 1) Then
               intPhase = intCtr
               fIcon = True
               Exit For
            End If
         Else
            If datPhase < datNextArray(0) Then
               intPhase = intCtr
               fIcon = True
            End If
         End If
      End If
   Next

   ' Iterate next array if not found
   If Not fIcon Then
      For intCtr = 0 To 2
         If datPhase > datNextArray(intCtr) Then
            If datPhase < datNextArray(intCtr + 1) Then
               intPhase = intCtr
               fIcon = True
               Exit For
            End If
         End If
      Next
   End If


   Me.Icon = ImageList1.ListImages.Item(intPhase + 1).ExtractIcon

End Sub

In addition to being able to extract images from the ImageList control, we can also form icons by overlaying one icon on another. Windows does this regularly, for instance, whenever we create a shortcut file, or whenever we copy a file using a drag-and-drop operation. Overlaid icons are formed by calling the ImageList control's Overlay method, which returns an overlaid icon in which one color of the overlaying icon can be defined as a mask color--that is, a transparent color that allows the overlaid icon's colors to show through. The syntax for Overlay is:

IPictureDisp = ImageList.Overlay(index1, index2)

where index1 is the index or key of the image to be overlaid, and index2 is the index or key of the overlaying image. Before calling the method and returning the image, you may want to define the mask color by setting the MaskColor property. You can assign it a color value constant--a value returned by the QBColor function or assigned by the RGB function.

There is one wrinkle to using the ImageList control to create an overlaid icon: Although the function returns an IPictureDisp object, the returned image can be directly assigned only to a picture control, not to an icon. In order to work around this difficulty, we have to dynamically add the returned image to an ImageList control, then call ExtractIcon to use it as an icon. The following code fragment illustrates how we might overlay the a custom icon with a Windows shortcut icon:

Set combIcon = ImageList1.Overlay("CUSTOMICON", "SHORTCUT")
ImageList1.ListImages.Add , "CUSTOMSHORT", combIcon
Me.Icon = ImageList1.ListImages("CUSTOMSHORT").ExtractIcon

Note the syntax of the ListImages collection's Add method. The first two arguments let you define the index of the new image (which we've omitted in this case, so that the image is added to the end of the collection) and the key of the new image. The third argument is an IPictureDisp object, in this case the image returned by the ImageList control's Overlay method.

Using a Resource File

Although C and C++ programmers are long accustomed to using resource files, Visual Basic programmers are not. And while their primary use in the Visual Basic environment is to support localized applications (by storing an application's strings in a variety of supported languages in the resource file), they can also be used to store cursors, icons, bitmaps, and custom resources.

Unless you prefer to use an external resource editor like ResEdit, you have to add the VB 6 Resource Editor add-in to the Visual Basic environment in order to use resources. You do this by opening the Add-In Manager dialog box (selecting Add-Ins -> Add-In Manager from Visual Basic's main menu), selecting the VB 6 Resource Editor in the Available Add-ins list box, and making sure that the Loaded/Unloaded box is checked. You can then add a resource file to your project by right-clicking in the Project Explorer, selecting Add -> Resource File from the pop-up menu, and entering the name of the resource file you'd like to create in the Open a Resource File dialog box. (The dialog box is a bit confusing, since it either creates a new resource file that it adds to your project or opens an existing resource file that it adds to your project.)

Once you've added the resource file, you can add icons to it by clicking on the Add Icon button on the Resource Editor's toolbar. As you add each icon, the editor automatically assigns it a resource identifier. You can change this, however, by double-clicking on the resource identifier to open the Edit Icon Properties dialog box, as shown in Figure 4.

Figure 4
Figure 4. The Edit Icon Properties dialog box

It's generally a good idea to add a set of constants that indicate the icon identifiers in your project. Even if you don't use the constants (as we haven't in our sample phases of the moon application), they provide documentation that reminds you what the icons' identifiers are. For example, in our sample application we've defined the following enumeration that indicates the icons' identifiers:

Private Enum Phases        ' IDs in Resource File
   NEWMOON = 101
   FIRSTMOON = 102
   FULLMOON = 103
   LASTMOON = 104
End Enum

Instead of adding the icons to an .frx file, as it did when we assigned an icon to a form's Icon property or when we stored icons in an ImageList control, Visual Basic creates a compiled resource (.res) file. The effect is the same as if the icons were stored in an .frx file: Visual Basic now retrieves the icons from the resource file and no longer needs the icon files to be physically present. When you compile your project, the resource file is included in the executable. It can be examined using any standard resource editor.

To retrieve an icon from the resource file, you use the LoadResPicture function, which takes as arguments the icon's identifier and the vbResIcon constant, which tells the function that you want to retrieve an icon rather than a bitmap (indicated by the vbResBitmap constant) or a cursor (indicated by the vbResCursor constant). The following code from our phases of the moon application is very similar to the previous code, except that it uses a resource file instead of the ImageList control:

Private Sub ShowMoonIcon(datPhase As Date)

   Dim intMonth As Integer, intCtr As Integer, intPhase As Phases
   Dim fIcon As Boolean
   Dim datArray As Variant, datNextArray As Variant

   intMonth = Month(datPhase) - 1

   ' Iterate array
   datArray = aDates(intMonth)
   datNextArray = aDates(intMonth + 1)
 
   For intCtr = 0 To 3
      If datPhase > datArray(intCtr) Then
         If intCtr <= 2 Then
            If datPhase < datArray(intCtr + 1) Then
               intPhase = intCtr
               fIcon = True
               Exit For
            End If
         Else
            If datPhase < datNextArray(0) Then
               intPhase = intCtr
               fIcon = True
            End If
         End If
      End If
   Next

   ' Iterate next array if not found
   If Not fIcon Then
      For intCtr = 0 To 2
         If datPhase > datNextArray(intCtr) Then
            If datPhase < datNextArray(intCtr + 1) Then
               intPhase = intCtr
               fIcon = True
               Exit For
            End If
         End If
      Next
   End If


   Me.Icon = LoadResPicture(intPhase + 101, vbResIcon)

End Sub

Extracting Windows Icons

There are times when you may not want to create your own icon. Instead you may simply want to use an existing icon that expresses the general function of your application. Windows itself provides hundreds of icons for this purpose, as suggested in Table 1, which lists some major files and the number of icons they contain. Stored inside of dynamic link libraries and executables, these icons are there for the taking as long as you know how to find them and extract them.

Table 1. Some system files containing icons

Filename Number of Icons
compstui.dll 99
comres.dll 38
cryptui.dll 20
csc.dll 22
dsuiext.dll 35
explorer.exe 18
iexplore.exe 23
inetcpl.cpl 34
inetcpl.dll 14
mmcndmgr.dll 129
mmsys.cpl 40
moricons.dll 140
netshell.dll 157
ntbackup.exe 26
pfmgr.dll 38
progman.exe 48
setupapi.dll 37
SHDOCVW.DLL 35
shell32.dll 238
stobject.dll 31
wiashext.dll 23
wmploc.dll 60
xpsp2res.dll 19

To discover what icons are available, a good icon creation and extraction program is invaluable. For instance, Figure 5 shows ArtIcons Pro, a utility from Aha-Soft, displaying the first 35 of the 238 icons available in Shell32.dll. Notice that the status bar numbers each icon based on its ordinal position in the DLL. This is important, since we'll need to know the position of the icon we'd like to extract.

Figure 5
Figure 5. The icons in Shell32.dll

Extracting the icon from a file is easy enough; just call the Win32 ExtractIcon function, which has the syntax:

Private Declare Function ExtractIcon Lib "shell32.dll" _
    Alias "ExtractIconA" _
    (ByVal hInst As Long, _
    ByVal lpszExeFileName As String, _
    ByVal nIconIndex As Long) As Long

For simplicity, we're using ExtractIcon here. You may prefer, though, to use ExtractIconEx, which lets you retrieve an array of handles to large and small icons. This allows you to assign a new icon in the most appropriate format, rather than allowing Windows to resize an icon in whatever format ExtractIcon retrieves.

where hInst is the instance handle of the application making the function call (for our application, its value is simply App.hInstance), lpszExeFileName is the name of the executable from which we wish to extract the icon, and nIconIndex is the zero-based index of the icon to extract. If nIconIndex is -1, the function returns the total number of icons in lpszExeFileName. Otherwise, the function returns a handle to the extracted icon, a 1 (if lpszExeFileName is not a DLL, executable, or icon file), or a NULL (if no icons were found in lpszExeFileName).

These parameters are all fairly straightforward, except for lpszExeFileName. Although you should know the name of the file from which you want to extract the icon, you can't necessarily be confident of its location on the user's system. Most of the folders with which you're likely to work when extracting icons, however, are system folders, so you can call the SHGetFolderPath method and pass it a constant representing the folder whose path and name you'd like to retrieve. Once it returns the complete path, you don't have to deal with trimming it, possibly adding a path separator character, and appending our filename; you can simply call the Win32 PathAppend method, which handles all of the details for us.

Whenever you call ExtractIcon to extract an icon, you must eventually destroy it by calling the DestroyIcon function once you're finished with it. Its syntax is:

Private Declare Function DestroyIcon Lib "user32" _
   (ByVal hIcon As Long) As Long

where hIcon is the icon handle returned by an icon extraction or retrieval function.

Retrieving a handle to the icon we want seems straightforward, and indeed it is. The difficulty arises, however, when we try to use our icon as if it were an icon. Most code samples (actually, all of the code samples that I've seen) show how you can write an icon to a PictureBox control using the Win32 DrawIcon function. The problem, though, is that PictureBox is a COM component, and it expects to be provided an object of type StdPicture to display. DrawIcon is a non-COM function, and it is able to draw our icon in the PictureBox control only because the control provides a handle to a device context. But once the icon is written, there's nothing further that you can do with it. Because COM was circumvented when the icon was drawn, the PictureBox control doesn't even know it's there. Attempts to access it produce a syntax error.

Fortunately, the COM Automation library provides a solution. It lets us convert our icon handle into an object of type StdPicture and then directly assign it to a form's Icon property. To do this, we use the OleCreatePictureIndirect method, which has the following syntax:

Public Declare Sub OleCreatePictureIndirect Lib "oleaut32.dll" ( _
       ByRef lpPictDesc As PictDesc, _
       ByVal riid As Guid, _
       ByVal fOwn As Long, _
       ByRef lplpvObj As StdPicture)

The members of the PICTDESC structure describe the picture. The structure is defined as follows:

Public Type PictDesc
   cbSizeofStruct As Long           ' Set to Len(PictDesc)
   picType As Long                  ' Set to vbPicTypeIcon
   hImage As Long                   ' Set to hIcon
   xExt As Long                     ' Horizontal size in twips                     
   yExt As Long                     ' Vertical size in twips
End Type

We can supply arbitrary values for the size members.

The second parameter to the OleCreatePictureIndirect method is an IID, or a globally unique identifier (GUID) that identifies the StdPicture object's interface, named IPicture. The IID of the IPicture interface is {7BF80980-BF32-101A-8BBB-00AA00300CAB}. We can supply it to a GUID data structure, which breaks up the GUID into distinct numeric components and is defined as follows:

Public Type Guid
   Data1 As Long
   Data2 As Integer
   Data3 As Integer
   Data4(0 To 7) As Byte
End Type

The function's third parameter is a Boolean value that indicates whether the application is to own the GDI picture handle (or in our case the icon handle). We should set this to True and remember to call DestroyIcon once when we are finished with our icon, either before we assign a new icon or when our form closes.

The final parameter is a pointer to a StdPicture object. This is an out parameter; we pass the method an uninitialized picture object and receive back a StdPicture object representing the icon we've extracted. To simplify the method call, we can wrap the OleCreatePictureIndirect method in the following function:

Public Function ConvertIconHandle(hIcon As Long) As StdPicture

   Dim iid As Guid
   Dim icondesc As PictDesc
   Dim icn As StdPicture
   
   iid.Data1 = &H7BF80980
   iid.Data2 = &HBF32
   iid.Data3 = &H101A
   iid.Data4(0) = &H8B
   iid.Data4(1) = &HBB
   iid.Data4(2) = &H0
   iid.Data4(3) = &HAA
   iid.Data4(4) = &H0
   iid.Data4(5) = &H30
   iid.Data4(6) = &HC
   iid.Data4(7) = &HAB
   
   OleCreatePictureIndirect icondesc, iid, False, icn
   
   Set ConvertIconHandle = icn

End Function

We can then assign the icon dynamically with code like the following, which prompts the user for the index to an icon resource in Shell32.dll and then assigns that icon to the form's Icon property:

Private Sub cmdChangeIcon_Click()
   
   Dim ndxIcon As Integer
   Dim hIcon As Long, hOldIcon As Long
   Dim hwnd As Long, hWndCur As Long
   Dim strPath As String, strIndex As String
   Dim gd As Guid
   Dim icn As StdPicture
   
   strPath = Space(MAX_PATH + 1)

   ' Get folder location and form path/filename
   SHGetFolderPath Me.hwnd, CSIDL_SYSTEM, vbNull, SHGFP_TYPE_CURRENT, strPath
   PathAppend strPath, "shell32.dll"

   strIndex = InputBox("Enter icon index: ", "Icon in Shell32.dll", 0)
   If strIndex = vbNullString Or Not IsNumeric(strIndex) Then Exit Sub
   
   If nHandleCtr > 0 Then DestroyIconHandles
   
   ndxIcon = CInt(strIndex)

   ' Retrieve Icon
   hIcon = ExtractIcon(App.hInstance, strPath, ndxIcon)
   If hIcon = 0 Or hIcon = vbNull Then
      MsgBox "Invalid icon"
      Exit Sub
   Else
      IconHandles(nHandleCtr) = hIcon
      nHandleCtr = nHandleCtr + 1
   End If
   
   ' Display icon in picture box
   'DrawIcon Me.picIcon.hDC, 0, 0, hIcon
   'Me.picIcon.Refresh
   
  Set icn = ConvertIconHandle(hIcon)
  Set Me.Icon = icn
   
   ' Assign icon
   hOldIcon = SetClassLong(Me.hwnd, GCL_HICONSM, hIcon)
   
   If hOldIcon = 0 Then
      Dim errCode As Long
      Dim sBuffer As String

      sBuffer = Space(256)
      errCode = GetLastError()
      If errCode > 0 Then
         MsgBox FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, ByVal 0&, _
                              errCode, 0&, sBuffer, Len(sBuffer), ByVal 0)
      End If
   Else
      DestroyIcon hOldIcon
   End If
   
   ' Force redrawing of frame
   SetWindowPos Me.hwnd, HWND_TOP, 0, 0, 0, 0, _
                SWP_NOMOVE Or SWP_NOSIZE Or SWP_NOZORDER Or SWP_FRAMECHANGED

   ' Get top-level (hidden) window handle
    hWndCur = Me.hwnd
    Do
       hWndCur = GetWindowLong(hWndCur, GWL_HWNDPARENT)
       If hWndCur > 0 Then hwnd = hWndCur
    Loop While hWndCur > 0
    hOldIcon = SetClassLong(hwnd, GCL_HICONSM, hIcon)
    If hOldIcon > 0 Then DestroyIcon hOldIcon

   ' Notify of change in large, small icons
   SendMessage hwnd, WM_SETICON, ICON_SMALL, hIcon
   SendMessage hwnd, WM_SETICON, ICON_BIG, hIcon

End Sub

In order to dynamically change the window icon, we must do more than simply assign a new StdPicture object representing our icon to the window's Icon property. In the call to SetClassLong, we notify Windows that the icon has changed, and then we call SetWindowPos to force a repainting of the nonclient area. This takes care of changing our System Small icon. But when you're working with Visual Basic, more is involved in changing the application icon.

Visual Basic has a hidden top-level application window that provides the icon to the taskbar, the Alt-Tab dialog box, and the Windows shell. In changing our window icon, we haven't changed these icons. In order to do that, we have to retrieve the handle of the application's top-level window by calling GetWindowLong until the function returns a 0. We can then call SendMessage to notify Windows that the large and the small icons for the top-level window have changed.

Ron Petrusha is the author and coauthor of many books, including "VBScript in a Nutshell."


Return to WindowsDevCenter.com.

Copyright © 2009 O'Reilly Media, Inc.