5.3 Creating an Icon Handler

only for RuBoard - do not distribute or recompile

5.3 Creating an Icon Handler

Our icon handler is going to be very impractical , but it is going to be a great example. We are going to create icons for several animal types, including dogs, cats, fish, cows, and armadillos. There will also be an icon representing an unknown animal. Our icon handler will allow us to determine which animal we are dealing with and display the appropriate icon in the shell. The icons will be stored in a resource file that we will include in the server. Each animal will have a 16 figs/u00d7.gif 16 icon that will be displayed by the shell for the file object and a 32 figs/u00d7.gif 32 icon that is used by the Property Page for the file. We have a little bit of work to do, so let's get started.

We aren't going to create a new project for the icon handler (although you may if you wish). We are simply going to continue with the project we started in Chapter 4. This brings up an interesting point. Our server can contain as many objects as we wish. There doesn't have to be a one-to-one relationship between the COM server and the server object (in this case a shell extension). In fact, every shell extension we create in the book will reside in one COM server. Each shell extension in the book will have its own class. But you don't have to implement separate classes, either. You could have just one class with all the appropriate interfaces implemented inside of it to support multiple shell extensions. It would be gargantuan and hard to get around in, but you could do it!

Let's begin the project by adding a new class module called clsExtractIcon. clsExtractIcon will implement three interfaces: IPersistFile , IExtractIconA , and IExtractIconW , as follows :

 Option Explicit

Implements IPersistFile
Implements IExtractIconA
Implements IExtractIconW 

5.3.1 Implementing IPersistFile

IPersistFile represents one of the few breaks we're going to get throughout the course of this book. First of all, IPersistFile contains five methods , but we only have to implement one of them, Load . Second, the implementation is the same every time we use it. Third, it's only two lines of code. It will not get this easy again! Example 5.4 contains the full listing for Load .

Example 5.4. Load Implementation
 Private m_sFile As String

Private Sub IPersistFile_Load( _ 
    ByVal pszFileName As VBShellLib.LPCOLESTR, _ 
    ByVal dwMode As VBShellLib.DWORD)

    m_sFile = Space(255)
    CopyMemory ByVal StrPtr(m_sFile), _ 
               ByVal pszFileName, _ 
               Len(m_sFile)
        
End Sub 

As stated earlier, pszFileName is a pointer. It contains an address of a filename. This is a Long value, not a String. We can't just assign pszFileName to m_sFile . That's a type mismatch. We need to copy the string from the location pointed to by pszFileName into our private variable, m_sFile . We can do that with a call to CopyMemory (See Appendix B). StrPtr is used to provide the address of our target, m_sFile . Notice that m_sFile has been preallocated with 255 spaces. Pointers to any local memory that are passed to CopyMemory must be preallocated. You cannot pass a pointer to a string that has no size . The shell will crash.

m_sFile will be used later by IExtractIcon::GetIconLocation to determine the icon that needs to be displayed for the given file. That's all there is to this method.

There is one more thing we need to do to finish our IPersistFile implementation. Remember, every method in an interface needs to be implemented even if the methods are not used. Fortunately for us, all we need to do is add one line of code to the remaining methods of IPersistFile :

 Err.Raise E_NOTIMPL 

Should the method be called (in our situation, it will not), the client will be informed that the method has not been implemented.

We can return OLE Automation error codes that begin with E_ in this manner, but this will not work for codes beginning with S_ , such as S_FALSE . Codes beginning with E_ specify an error condition. Therefore, raising an error would be valid in this circumstance. Codes beginning with S_ mean that the method was successful, but that the desired result was not achieved. To return SCODE values, we need to swap the function address of the desired method in the vtable with our own implementation.

5.3.2 Implementing IExtractIcon

IExtractIcon will provide the shell with the location of an icon to display for a single instance of a .rad file object. The icons that we use reside inside of our server in a resource file. GetIconLocation will provide the shell with all the information necessary to display our icons. Even so, the shell will still call Extract . Extract must return S_FALSE to indicate that it already has enough information to display the icon (as a result of the previous call to GetIconLocation ). We cannot directly return the HRESULT S_FALSE from Extract , so we will swap the class implementation with our own. (See the discussion of swapping routines in "Implementing IContextMenu" in Chapter 4.)

Our icon handler will implement the following methods of IExtractIcon :

GetIconLocation

The source for GetIconLocationW (NT Version) is shown in Example 5.5, and the source for GetIconLocationA (ANSI version) is shown in Example 5.6. These functions are almost identical. The differences will be noted in the listing. Basically, the string representing the pathname to the icon file must be converted from Unicode to ANSI in GetIconLocationA . This is similar to what we had to do with the IContextMenu::GetCommandString method (see Chapter 4). The string just needs to be converted from Unicode in the ANSI version. The difference between these two listings is only one line of code.

Here's how it works. The full path to the icon file (our server) will be built using the App object and our filename. This path is copied to the address pointed to by szIconFile , and the index of the icon is assigned to piIndex . The icons used for the handler will reside in a resource file in the server. The index passed back to the shell is the zero-based position of the icon in this resource file. pwFlags is used to tell the shell that each file of this type has its own icon ( GIL_PERINSTANCE ) and that it should not cache the icons ( GIL_DONTCACHE ).

Example 5.5. GetIconLocationW Listing
 Private Sub IExtractIconW_GetIconLocation( _ 
    ByVal uFlags As VBShellLib.UINT, _ 
    ByVal szIconFile As VBShellLib.LPWSTRVB, _ 
    ByVal cchMax As VBShellLib.UINT, _ 
    piIndex As Long, pwFlags As VBShellLib.GETICONLOCATIONRETURN)

    Dim szName As String
    Dim szType As String
    Dim sType As String
    Dim bszName(  ) As Byte
    
    szName = App.Path & "\" & App.Title & ".dll"
    szName = Left$(szName, cchMax) & vbNullChar
    bszName = szName
    CopyMemory ByVal szIconFile, ByVal StrPtr(szName), _
               UBound(bszName) + 1
    
    szType = Space(255)
    
    GetPrivateProfileString "Animal", _
                            "Type", _
                            "Unknown", _
                            szType, _
                            Len(szType), _
                            m_sFile
    
    sType = Trim(LCase(szType))
    sType = Left(sType, Len(sType) - 1)
    
    If sType = "armadillo" Then
        piIndex = 0
    ElseIf sType = "cat" Then
        piIndex = 1
    ElseIf sType = "cow" Then
        piIndex = 2
    ElseIf sType = "dog" Then
        piIndex = 3
    ElseIf sType = "fish" Then
        piIndex = 4
    Else
        piIndex = 5 'Unknown
    End If
           
    pwFlags = pwFlags Or GIL_PERINSTANCE Or GIL_DONTCACHE
    
End Sub 
Example 5.6. GetIconLocationA Listing
 Private Sub IExtractIconA_GetIconLocation( _ 
    ByVal uFlags As VBShellLib.UINT, _ 
    ByVal szIconFile As VBShellLib.LPSTRVB, _ 
    ByVal cchMax As VBShellLib.UINT, _ 
    piIndex As Long, pwFlags As VBShellLib.GETICONLOCATIONRETURN)

    Dim szName As String
    Dim szType As String
    Dim sType As String
    Dim bszName(  ) As Byte
    
    szName = App.Path & "\" & App.Title & ".dll"
    szName = Left$(szName, cchMax)
    bszName = StrConv(szName, vbFromUnicode) & vbNullChar
    CopyMemory ByVal szIconFile, bszName(0), UBound(bszName) + 1
    
    szType = Space(255)
    
    GetPrivateProfileString "Animal", _
                            "Type", _
                            "Unknown", _
                            szType, _
                            Len(szType), _
                            m_sFile
    
    sType = Trim(LCase(szType))
    sType = Left(sType, Len(sType) - 1)
    
    If sType = "armadillo" Then
        piIndex = 0
    ElseIf sType = "cat" Then
        piIndex = 1
    ElseIf sType = "cow" Then
        piIndex = 2
    ElseIf sType = "dog" Then
        piIndex = 3
    ElseIf sType = "fish" Then
        piIndex = 4
    Else
        piIndex = 5 'Unknown
    End If
           
    pwFlags = pwFlags Or GIL_PERINSTANCE Or GIL_DONTCACHE
    
End Sub 
Extract

Extract provides the shell with a means to get an icon from an alternate location, say an image list or perhaps a call to the Win32 LoadIcon function. Since a location has been provided by GetIconLocation , this method is simply supposed to return S_FALSE (1) . Here we run into the problem of not being able to specify an HRESULT to return. So, once again, a vtable entry must be swapped (see the discussion in "Implementing IContextMenu" in Chapter 4). We swap the vtable for both the ANSI and Unicode version of the interface. Since both versions of Extract just return S_FALSE , we can swap both versions of Extract with the same function. This is done in the icon handler's Class_Initialize event procedure, as Example 5.7 shows.

Example 5.7. The Class_Initialize Event Procedure
 Private Sub Class_Initialize(  )

    Dim pVtable1 As IExtractIconA
    Set pVtable1 = Me
    m_pOldExtractA = SwapVtableEntry(ObjPtr(pVtable1), 5, _ 
                                     AddressOf ExtractVB)
    
    Dim pVtable2 As IExtractIconW
    Set pVtable2 = Me
    m_pOldExtractW = SwapVtableEntry(ObjPtr(pVtable2), 5, _ 
                                     AddressOf ExtractVB)
    
End Sub 

The value 5 is passed to SwapVtableEntry because Extract is the second method defined in the interface ( IUnknown [3] + Extract [2] = 5). This can be verified by looking directly at the IDL listing for IExtractIcon or using OLE View.

Class_Terminate merely swaps the function addresses back in a similar fashion (see "Implementing IContextMenu" in Chapter 4). ExtractVB , which is shown in Example 5.8, simply returns S_FALSE .

Example 5.8. ExtractVB
 Public Function ExtractVB(ByVal this As IExtractIconA, _ 
    ByVal pszFile As LPCSTRVB, _ 
    ByVal nIconIndex As UINT, _ 
    phiconLarge As HICON, _ 
    phiconSmall As HICON, _ 
    ByVal nIconSize As UINT) As Long
    
    ExtractVB = S_FALSE

End Function 

Notice that the this pointer is of type IExtractIconA . Since it is not being used, we can get away with using this function for both versions of the interface. If we did need to use it, however, we would have to write two separate functions.

5.3.2.1 [in, out] parameters

If you look at the listing for IExtractIcon (see Example 5.3), you will notice that several method parameters have been marked as [in, out] . The [in] attribute means that the parameter is being passed from the client (the shell, in the case of a shell extension) to the called method. The [out] attribute says that the parameter is a pointer and that the client (our server) should use this pointer to return any values. Basically, it gives us the power to use the parameter directly and not have to mess around with CopyMemory . The pwFlags and piIndex parameters in GetIconLocation (see Example 5.5 and Example 5.6) are good examples of this technique. We use it just as we would a parameter passed ByRef . Consider the following code fragment. piIndex and pwFlags are defined as [in, out] parameters in the IExtractIcon IDL. You can think of this as being equivalent to the shell passing us an argument ByRef . We can use these values directly to return information back to the shell:

 Private Sub IExtractIconW_GetIconLocation(
    ByVal uFlags As VBShellLib.UINT, _ 
    ByVal szIconFile As VBShellLib.LPWSTRVB, _ 
    ByVal cchMax As VBShellLib.UINT, _ 
    piIndex As Long, _ 
    pwFlags As VBShellLib.GETICONLOCATIONRETURN)
.
.
.
    pwIndex = 5
    pwFlags = pwFlags Or GIL_PERINSTANCE Or GIL_DONTCACHE

End Sub 

5.3.3 Stubs

Every method of an interface must be implemented. The server will not compile otherwise . In this book, all methods not implemented will be marked as such with a comment:

 Private Sub IPersistFile_GetClassID(lpClassID As VBShellLib.CLSID)
'Not implemented
End Sub 

VB automatically returns an HRESULT of S_OK (0) in instances such as these. In COM programming (in C++), it is common to see methods like this return E_NOTIMPL (&H80004001) , S_FALSE (1), or E_FAIL (&H80004005)in other words, some indicator that the method is not actually implemented. Returning S_OK for an empty method (like we will be doing) is somewhat misleading. Someone calling the method may wonder why it is returning successfully when nothing is happening! Returning S_OK is not entirely clear, but swapping vtable entries for every function that we do not implement is simply too much work. So, we will take the easy way out and let VB do its thing.

5.3.4 Registration

The icon handler is a little easier to register because there can be only one per file type (as opposed to context menu handlers). Add an IconHandler key under the shellex key and assign its default value the CLSID of the component. Remember, the CLSID for the component can be found under its ProgID, RadEx.clsIconHandler :

 HKEY_CLASSES_ROOT
    radfile
        shellex
            IconHandler = {B3213FAC-EB84-11D2-9FD9-00550076E06F} 

Of course, the icon handler's CLSID must also be added to the list of approved shell extensions. That key is as follows:

 HKEY_LOCAL_MACHINE
    Software
        Microsoft
            Windows
                CurrentVersion
                    Shell Extensions
                        Approved 

After the component is registered, the icons for the individual .rad files will be displayed, as Figure 5.2 illustrates. You might need to refresh the shell's display by pressing F5, but other than that, everything is ready to go.

Figure 5.2. Each .rad file displays a different icon
figs/vshl.0502.gif

The following script registers the component that is included with this chapter:

 REGEDIT4

[HKEY_CLASSES_ROOT\.rad]
@ = "radfile"

[HKEY_CLASSES_ROOT\radfile]

[HKEY_CLASSES_ROOT\radfile]
@ = "Rudimentary Animal Data"

[HKEY_CLASSES_ROOT\radfile\shellex]

 [HKEY_CLASSES_ROOT\radfile\shellex\IconHandler]
@ = "{B3213FAC-EB84-11D2-9FD9-00550076E06F}"

 [HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved]
"{61E9A1D1-5985-11D3-BB7C-444553540000}" = "RAD file icon handler" 

Resource Files

The resource file in this book was created with Visual C++ and compiled using the resource compiler, RC.EXE . If you do not have a resource editor such as the one that is part of Visual C++, you can create these files by hand. RC.EXE does ship with Visual Basic, so compiling resource scripts should not be a problem. The resource script for the icon handler is very simple:

 #define IDI_ARMADILLO                   101
#define IDI_CAT                         102
#define IDI_COW                         103
#define IDI_DOG                         104
#define IDI_FISH                        105
#define IDI_UNKNOWN                     106

IDI_ARMADILLO ICON DISCARDABLE "armadill.ico"

IDI_CAT       ICON DISCARDABLE "cat.ico"
IDI_COW       ICON DISCARDABLE "cow.ico"
IDI_DOG       ICON DISCARDABLE "dog.ico"
IDI_FISH      ICON DISCARDABLE "fish.ico"
IDI_UNKNOWN   ICON DISCARDABLE "unknown.ico" 

Every resource is assigned a unique identifier, and this identifier is then mapped to the actual file containing the icon. Using the Visual C++ resource editor is the best option, because it will do everything for you. Writing your own resource scripts can be tedious . There is nothing to be ashamed of by using a tool to do it for you.

Visual Basic 6.0 does have an add-in called the VB Resource Editor. It will allow you to create a resource file of icons that can be used in this chapter. What it will not do is allow you to create dialog box templates. Dialog box templates will be necessary when we create property page extensions in the next chapter.

Your options are limited to getting a resource editor or coding the script by hand. Writing your own resource script is a little trickier where dialogs are concerned , but it is not out of the question. As you can see, the script for icons is very straightforward.

only for RuBoard - do not distribute or recompile