4.4 Context Menu Handler Interfaces

only for RuBoard - do not distribute or recompile

4.4 Context Menu Handler Interfaces

The components we will write in this book will all implement any given number of system interfaces. "System" in this context (no pun intended) means that these interfaces have already been defined by Microsoft. They are documented, and you can read all about them in the Platform SDK (though the details may be a little murky sometimes).

You can think of an interface as a defined functionality. When a component implements an interface, it is really saying, "I support this functionality!" Consider a Triangle component. It implements the interface Shape . Shape defines two methods : Draw and Color . Therefore, you could expect to access the following functionality through Triangle:

 Triangle.Draw
Triangle.Color 

Because the Circle, Square, and Trapezoid components also implement Shape , you would expect these objects to have the same functionality as well. This is what it means to implement an interface.

The components in this book all implement some functionality that is required by the shell. This means that when the shell loads our components, it will be able to gain access to our component through a defined mechanism: an interface.

With that said, let's talk about the interfaces a context menu handler component needs to implement before it can be loaded by the shell.

4.4.1 IShellExtInit

IShellExtInit contains one method (besides the IUnknown portion of the interface), Initialize , as shown in Table 4.1.

Table4.1. IShellExtInit

Method

Description

Initialize

Initializes the shell extension

IShellExtInit::Initialize is the first method called by the shell after it loads the context menu handler; it is the context menu handler's equivalent of a class constructor in C++ programming or the Class_Initialize event procedure of a class in VB. Typically, this method is used by the context menu handler to determine which file objects are currently selected within Explorer. Initialize is defined as follows :

 HRESULT Initialize(LPCITEMIDLIST   pidlFolder   , 
		        IDataObject *   lpdobj   , 
		        HKEY   hkeyProgID   ); 

All three arguments are provided by the shell and passed to the context menu handler when it is invoked, which is indicated by the [in] notation in the following argument list. The three arguments are:

pidlFolder

[in] A pointer to an ITEMIDLIST structure (commonly referred to in shell parlance as a PIDL) with information about the folder containing the selected objects. If you want more information on PIDLs and what you can do with them, see Chapter 12. We are not going to use this member, and we are not even going to discuss it (yet), because the topic of PIDLs is a universe unto itself. All you need to know is that a PIDL provides a location of something (such as the path of a file or folder object) within the Windows namespace.

lpdobj

[in] A pointer to an IDataObject interface that provides information about the selected objects. The IDataObject interface is discussed in the following section.

hKeyProgID

[in] The handle of the registry key containing the programmatic identifier of the selected file. For instance, if a Word .doc file was right-clicked, hKeyProgID would be a handle to the HKEY_CLASSES_ROOT\Word.Document.8 key on systems with Office 2000 installed. Once the handle to this key is available, it is a trivial matter to find the host application that is responsible for dealing with this file type, which in the case of our example happens to be Microsoft Word. The context menu handler can then defer any operations to the host application, if necessary.

The only parameter in which we are interested is the second, lpdobj , which is a pointer to an IDataObject interface. Like the first parameter, IDataObject is also a world unto itself. Fortunately for us, we don't need to know too much about the interface at this juncture. In Chapter 8, when we create a data handler, we will put this interface under the knife , so to speak, but until then let's just cover what we need to know. The shell uses this interface to communicate to us the files that were clicked on in Explorer. We'll see how this works momentarily.

Now that we know a little bit about this interface, let's get on to how we are actually going to implement it. There are some problems ahead.

IShellExtInit , like most of the interfaces in this book, is a VB- unfriendly interface. An unfriendly interface contains datatypes that are not automation compatible. You can think of an automation-compatible type as basically anything that will fit into a Variant . Table 4.2 lists all of the datatypes that are considered OLE automation compatible.

Table4.2. OLE Automation-Compatible Types

Datatype

Description

 boolean 

Corresponds to the VB Boolean type

 unsigned char 

8-bit unsigned data item

 double 

64-bit IEEE floating-point number

 float 

32-bit IEEE floating-point number

 int 

Signed integer whose size is system-dependent

 long 

32-bit signed integer

 short 

16-bit signed integer

 BSTR 

Length-prefixed string; this is the String datatype in VB

 CURRENCY 

8-byte, fixed-point number

 DATE 

64-bit, floating-point fractional number of days since December 31, 1899

 SCODE 

Error code for 16-bit systems

 Typedef enum   myenum   

Signed integer whose size is system-dependent

 Interface IDispatch * 

Pointer to the IDispatch interface

 Interface IUnknown * 

Any interface pointer that directly derives from IUnknown

 dispinterface   Typename *   

Pointer to an interface derived from IDispatch

 Co-class   Typename *   

Pointer to a co-class name

 [oleautomation] interface   Typename *   

Pointer to an interface that derives from IUnknown

 SAFEARRAY(   TypeName   ) 

Array of any of the preceding types

   TypeName   * 

Pointer to any of the preceding types

 Decimal 

96-bit unsigned binary integer scaled by a variable power of 10 that provides a size and scale for a number (as in coordinates)

Now, to implement IShellExtInit successfully, the interface will have to be redefined with automation-compatible types and made available through a type library. This interface contains one method, Initialize . Let's tear it apart to see what we need to do in order to make this interface work for us.

Consider the first parameter of the Initialize method, which is an LPCITEMIDLIST . The documentation for the interface states that this is an address of an ITEMIDLIST . (We'll talk about ITEMIDLIST in Chapter 11.) The structure is defined like this:

 typedef struct _ITEMIDLIST {
        SHITEMID   mkid   ;
    } ITEMIDLIST; 

As you can see, the one and only member of this structure is another structure called SHITEMID , which is not an automation-compatible type. This means we cannot define this parameter as a pointer to an ITEMIDLIST when we define the IShellExtInit interface. What can we do? Well, a pointer is four bytes wide, so the automation-compatible type that can be used in place of LPCITEMIDLIST is a long . When we create our type library, we will just redefine LPCITEMIDLIST to mean a long , like so:

 typedef [public] long LPCITEMIDLIST; 

When we actually define the Initialize method (see Example 4.1), we can still use LPCITEMIDLIST for the datatype of the first parameter. Then, when VB displays the parameters for the method via IntelliSense , rather than seeing long , we will see LPCITEMIDLIST . This acts as a reminder of what the original definition is supposed to be.

We'll do the same thing for the third parameter, which is an HKEY . An HKEY is a handle to a registry key. Handles to anything are four bytes, so a long works in this case, too:

 typedef [public] long HKEY; 

We don't have to redefine anything as far as the second parameter goes. It's an IDataObject interface pointer. And interface pointers that are derived from IUnknown or IDispatch are automation compatible, so this portion of the definition is fine as is.

Let's talk about these parameters we have redefined for a moment. As it turns out, we will not need the first or the third parameters of this method in order to implement a context menu handler. But what if we did? After all, these types have been redefined as long values. Well, an HKEY is really a void pointerthat is, a pointer that does not point to any specific datatype. As a long , you can use this value as is with any of the registry API functions that take HKEY s.

How do we access the pointer to the ITEMIDLIST when all we have is a long value? We can use the RtlMoveMemory API (a.k.a. CopyMemory ) to make a local copy of the UDT. This API call is defined like so:

 Public Declare Sub CopyMemory Lib "kernel32" _ 
    Alias "RtlMoveMemory" (pDest As Any, _
                           pSource As Any, _ 
                           ByVal ByteLen As Long) 

The code on the VB side would then look something like the following:

 Private Sub IShellExtInit_Initialize(_  ByVal pidlFolder As VBShellLib.LPCITEMIDLIST  , _ 
    ByVal pDataObj As VBShellLib.IDataObject, _ 
    ByVal hKeyProgID As VBShellLib.HKEY)

    Dim idlist As ITEMIDLIST

    CopyMemory idlist,  ByVal pidlFolder  , len(idlist) 

Notice, though, that the second parameter to CopyMemory (our ITEMIDLIST that has been redefined as a long) is passed to the function ByVal . This is because this long value represents a raw address. We'll talk more about this later, since we will use techniques similar to this throughout the course of this book.

Example 4.1 shows the modified definition for the IShellExtInit interface as it exists in our type library.

Example 4.1. IShellExtInit Interface
  typedef [public] long HKEY;
    typedef [public] long LPCITEMIDLIST;  [
		uuid(000214E8-0000-0000-C000-000000000046),
		helpstring("IShellExtInit Interface"),
		odl
	]
	interface IShellExtInit : IUnknown
	{
		[helpstring("Initialize")]
		HRESULT Initialize([in]  LPCITEMIDLIST  pidlFolder, 
                              [in] IDataObject   *pDataObj, 
                              [in]  HKEY  hKeyProgID);
	} 

Structures and IDL

We had to redefine the ITEMIDLIST pointer for the IShellExtInit::Initialize method because the structure contained a non-automation-compatible type. However, there are some circumstances in which we'll deal with pointers to structures that contain types whose members are all automation compatible. In situations like these, we don't have to redefine the pointer to the structure as a long like we had to for ITEMIDLIST .

Consider the POINT structure, which is defined as follows:

 typedef struct {
    long x;
    long y;
} POINT; 

This structure's members are all automation compatible.

Let's assume we have an interface (this is hypothetical, of course) that contains a GetCoordinate method. This method returns a pointer to POINT that specifies the location of some object. In IDL, its definition looks like this:

 HRESULT GetCoordinate([in] POINT *location) 

If we were to implement this method in VB, we could do the following:

 Private Sub IBattleShip_GetCoordinate(_
    location As POINT)

    location.x = 10
    location.y = 20

End Sub 

Instead of having to use CopyMemory to create a local copy of POINT , we can access the structure directly.

The [public] attribute used in Example 4.1 makes the typedef values available through the type library; otherwise , they would just be available for use inside of the library itself.

The [odl] attribute is required for all interfaces compiled with MKTYPLIB. MIDL supports this attribute as well, but only for the sake of backward compatibility. The attribute itself does absolutely nothing.

The [helpstring] attribute, as you can probably guess, denotes the text that will be displayed for a library or an interface from within Object Browser or the Project/References dialog.

The [in] attribute is known as a directional attribute. This indicates that the parameter is passed from the caller to the COM component. (In the case of our context menu handler, it indicates that the shell is passing our COM component a parameter.) Another attribute, [out] , specifies the exact opposite , which is a parameter that is passed from the component to the caller. All parameters to a method have a directional attribute. This is either [in] , [out] , or [in, out] . But VB cannot handle [out] -only parameters. Parameters designated as [out] usually require the caller to free memory. VB likes to shield responsibility from the programmer whenever possible, especially when it comes to memory management.

Look at the GUID for IShellExtInit , (000214E8-0000-0000-C000-000000000046) . This GUID comes straight from the registry. It has been defined by Microsoft as the GUID for IShellExtInit . It is important that you use the correct GUID for interfaces already defined by the system, because, after all, that is their true name. The GUID for the library block (see Appendix A ), on the other hand, can be anything since it's being defined by usbut not anything you can think of off the top of your head. Whenever you need to define your own GUID, you should use GUIDGEN (see Figure 4.4). GUIDGEN is a program used for generating GUIDs that guarantees them to be unique (theoretically) and copies them to the clipboard. GUIDGEN ships with Visual Studio, but if you don't have it, you can always make your own, as Example 4.2 demonstrates .

Figure 4.4. The GUIDGEN utility
figs/vshl.0404.gif
Example 4.2. Source Code for a Self-Created GUIDGEN Utility
 Option Explicit

Private Type GUID
    Data1 As Long
    Data2 As Integer
    Data3 As Integer
    Data4(7) As Byte
End Type

Private Declare Function CoCreateGuid Lib "ole32.dll" _
    (g As GUID) As Long
Private Declare Sub CopyMemory Lib "kernel32" Alias _ 
    "RtlMoveMemory" (pDst As Any, pSrc As Any, _
    ByVal ByteLen As Long)
Private Declare Function StringFromCLSID Lib "ole32.dll" _ 
    (pClsid As GUID, lpszProgID As Long) As Long

Private Sub StrFromPtrW(pOLESTR As Long, strOut As String)
    
    Dim ByteArray(255) As Byte
    Dim intTemp As Integer
    Dim intCount As Integer
    Dim i As Integer
    
    intTemp = 1
    
    'Walk the string and retrieve the first byte of each WORD.
    While intTemp <> 0
        CopyMemory intTemp, ByVal pOLESTR + i, 2
        ByteArray(intCount) = intTemp
        intCount = intCount + 1
        i = i + 2
    Wend
    
    'Copy the byte array to our string.
    CopyMemory ByVal strOut, ByteArray(0), intCount
    
End Sub

Private Sub Command1_Click(  )

    Dim g As GUID
    Dim lsGuid As Long
    Dim sGuid As String * 40
    
    If CoCreateGuid(g) = 0 Then
        StringFromCLSID g, lsGuid
        StrFromPtrW lsGuid, sGuid
    End If
    
    InputBox "This is your GUID!", "GUID", sGuid
    
End Sub 

Figuring out the details of this code is an exercise for you. However, this will be much easier to do after you have finished this book, since we will discuss all of the functions in this listing extensively.

4.4.2 IDataObject

IDataObject is not implemented by the context menu handler directly, but rather, it is a parameter to IShellExtInit::Initialize . Therefore, it has to be defined in the type library. IDataObject provides the means to determine which files have been right-clicked within the shell. IDataObject is a fairly complex interface that contains nine methods: GetData , GetDataHere , QueryData , GetCanonicalFormat , SetData , EnumFormatEtc , DAdvise , DUnadvise , and EnumDAdvise . This interface is the soul of OLE data transfers.

In regards to context menu handlers, there is only one method, GetData , that we will use to implement the extension. Its syntax is:

 HRESULT GetData(FORMATETC * pFormatetc, STGMEDIUM *   pmedium   ); 

Its parameters are:

pFormatetc

[in] Pointer to a FORMATETC structure. The FORMATETC structure represents a generalized clipboard format. It's defined like this:

 typedef struct {
        long  cfFormat;
        long  ptd;
        DWORD dwAspect;
        long  lindex;
        TYMED tymed;
} FORMATETC; 
pmedium

[in] Pointer to a STGMEDIUM structure. STGMEDIUM is a generalized global-memory handle used for data-transfer operations. It is defined like this:

 typedef struct tagSTGMEDIUM {
    DWORD tymed;
    union {
        HBITMAP hBitmap;
        HMETAFILEPICT hMetaFilePict;
        HENHMETAFILE hEnhMetaFile;
        HGLOBAL hGlobal;
        LPWSTR lpszFileName;
        IStream *pstm;
        IStorage *pstg;
    };
    IUnknown *pUnkForRelease;
}STGMEDIUM; 

Because VB does not support unions, our type library will contain a more generalized definition of this structure:

 typedef struct {
        TYMED    tymed;
        long     pData;
        IUnknown *pUnkForRelease;
    } STGMEDIUM; 

Admittedly, the discussion of FORMATETC and STGMEDIUM is rather cryptic here. This is intentional. When we implement IShellExtInit later in the chapter, just understand that the shell is using IDataObject to transfer a list of files to us. IDataObject is the primary interface involved in OLE data transfers. That's about all you need to know right now. We will learn much more about this interface in Chapter 8.

4.4.3 IContextMenu

As Table 4.3 shows, IContextMenu contains three methods: GetCommandString , InvokeCommand , and QueryContextMenu . This is the core of the context menu handler. The methods of this interface provide the means to add items to a file object's context menu, display help text in Explorer's status bar, and execute the selected command, respectively. We'll discuss each of these methods in turn .

Table4.3. IContextMenu

Method

Description

GetCommandString

Returns the help string that Explorer will display in the status bar.

InvokeCommand

Implements menu commands when the menu items are selected.

QueryContextMenu

Adds items to the context menu.

4.4.3.1 GetCommandString

GetCommandString allows the handler to specify the text that will be displayed in the status bar of Explorer. This occurs when a particular context menu item is selected. Its syntax is:

 HRESULT GetCommandString(
    UINT   idCmd   ,		
    UINT   uFlags   ,
    UINT *   pwReserved   ,
    LPSTR   pszName   ,
    UINT   cchMax   ); 

Its parameters are:

idCmd

The ordinal position of the selected menu item.

uFlags

A flag specifying the information to return.

pwReserved

Unused; handlers must ignore this parameter, which should be set to NULL .

pszName

A pointer to the string buffer that holds the null- terminated string to be displayed.

cchMax

Size of the buffer defined by pszName .

When the method is invoked by the shell, the shell passes the following items of information to the GetCommandString method:

  • The idCmd argument to indicate which menu item is selected.

  • The uFlags argument to indicate what string the method is expected to return. This can be one of the following values:

Constant

Description

GCS_HELPTEXT

Returns the Help text for the context menu item.

GCS_VALIDATE

Validates that the menu item exists.

GCS_VERB

Returns the language-independent command name for the menu item.

  • The cchMax argument to indicate how many bytes of memory have been allocated for the string that the method is to pass back to the shell.

The method can then place the desired string in the pszName buffer. As a general rule, the string should be 40 characters or less and should not exceed cchMax .

4.4.3.2 InvokeCommand

The shell calls this method to execute the command selected in the context menu. Its syntax is:

 HRESULT InvokeCommand(LPCMINVOKECOMMANDINFO   lpici   ); 

with the following parameter:

lpici

A pointer to a CMINVOKECOMMANDINFO structure that contains information about the command to execute when the menu item is selected.

The CMINVOKECOMMANDINFO structure is defined in the Platform SDK as follows:

 typedef struct _CMInvokeCommandInfo{ 
    DWORD cbSize; 
    DWORD fMask; 
    HWND hwnd; 
    LPCSTR lpVerb; 
    LPCSTR lpParameters; 
    LPCSTR lpDirectory; 
    int nShow; 
    DWORD dwHotKey; 
    HANDLE hIcon; 
} CMINVOKECOMMANDINFO, *LPCMINVOKECOMMANDINFO; 

Its members are:

cbSize

The size of the structure in bytes.

fMask

Zero, or one of the following values:

Constant

Description

CMIC_MASK_HOTKEY

The dwHotKey member is valid.

CMIC_MASK_ICON

The hIcon member is valid.

CMIC_MASK_FLAG_NO_UI

Tells the system to refrain from displaying user -interface elements, like error messages, while carrying out a command.

hwnd

The handle of the window that owns the context menu.

lpVerb

Contains the zero-based menu item offset in the low-order word.

lpParameters

Not used for shell extensions.

lpDirectory

Not used for shell extensions.

nShow

If the command opens a window, specifies whether it should be visible or not visible. Can be either SW_SHOW or SW_HIDE .

dwHotKey

fMask must contain CMIC_MASK_HOTKEY for this value to be valid. It contains an optional hot key to assign to the command.

hIcon

Icon to use for any application activated by the command.

4.4.3.3 QueryContextMenu

This method is called by the shell to allow the handler to add items to the context menu. Its syntax is:

 HRESULT QueryContextMenu(
    HMENU   hmenu   ,
    UINT   indexMenu   ,
    UINT   idCmdFirst   ,
    UINT   idCmdLast   ,
    UINT   uFlags   ); 

with the following parameters:

hmenu

Handle of the menu.

indexMenu

Zero-based position at which to insert the first menu item.

iCmdFirst

Minimum value that the handler can use for a menu-item identifier.

iCmdLast

Maximum value that the handler can use for a menu-item identifier.

uFlags

Flags specifying how the context menu can be changed. These flags are discussed later in this chapter.

In invoking the method, the shell provides the context menu handler with all of the information needed to customize the context menu. The QueryContextMenu method can then use this information when calling the Win32 InsertMenu function to modify the context menu.

The documentation for the interface states that QueryContextMenu should return the menu identifier of the last menu item added, plus one. This presents an interesting problem, because VB does not allow access to the HRESULT . Fortunately, there is a workaround. We will discuss this in detail when we actually implement the interface. The complete IDL listing for IContextMenu is shown in Example 4.3.

Example 4.3. IContextMenu
 typedef [public] long HMENU;
typedef [public] long LPCMINVOKECOMMANDINFO;
typedef [public] long LPSTRVB;
typedef [public] long UINT;

[
    uuid(000214e4-0000-0000-c000-000000000046),
    helpstring("IContextMenu Interface"),
    odl
]
interface IContextMenu : IUnknown
{
    HRESULT QueryContextMenu([in] HMENU hmenu, 
                             [in] UINT indexMenu, 
                             [in] UINT idCmdFirst, 
                             [in] UINT idCmdLast,
                             [in] QueryContextMenuFlags uFlags);

    HRESULT InvokeCommand([in] LPCMINVOKECOMMANDINFO lpcmi);

    HRESULT GetCommandString([in] UINT    idCmd,
                             [in] UINT    uType,
                             [in] UINT    pwReserved,
                             [in] LPSTRVB pszName,
                             [in] UINT    cchMax);    	
} 

Notice the last parameter of QueryContextMenu , which takes a type of QueryContextMenuFlags . This is actually an enumeration defined within the type library. Enumerations are a good way to restrict the range of values that can be accepted as a method parameter. We will define many such enumerations throughout the course of this book. This provides some type safety for this method, though not much. The enum does not require an attributes block, although you could add one if you wanted. QueryContextMenuFlags is defined as follows:

 typedef enum {
	CMF_NORMAL        = 0x00000000,
	CMF_DEFAULTONLY   = 0x00000001,
	CMF_VERBSONLY     = 0x00000002,
	CMF_EXPLORE       = 0x00000004,
	CMF_NOVERBS       = 0x00000008,
	CMF_CANRENAME     = 0x00000010,
	CMF_NODEFAULT     = 0x00000020,
	CMF_INCLUDESTATIC = 0x00000040,
	CMF_RESERVED      = 0xffff0000 
} QueryContextMenuFlags; 
only for RuBoard - do not distribute or recompile