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.
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:
System Small
The icon displayed on the window's caption bar. Its size can be controlled by modifying the size of the window title bar in the Appearances tab of the Display Properties dialog box.
System Large
The icon displayed in the system's Alt-Tab dialog box that lists the icons of running applications. It is also used by applications. Its size is defined by the video driver and cannot be altered.
Shell Small
The icon displayed in Explorer windows, common dialog boxes like File Open and File Save, the Applications tab of Windows Task Manager, and possibly the Start menu. Currently, Windows is configured to use the System Small icon as the Shell Small icon.
Shell Large
The icon displayed on the desktop and possibly on the Start menu. Its size is defined by the Shell Icon Size value of the HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics Registry key.
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.
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. 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. 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.
|
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. 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.
|
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. 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
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. 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 |
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.