|
|
|
|
Visual Basic Shell ProgrammingIntegrating Applications with the Windows ShellBy J. P. Hamilton1st Edition July 2000 1-56592-670-6, Order Number: 6706 396 pages, $29.95 |
Chapter 4
Context Menu HandlersThe shell displays a context menu for a file object when it is clicked with the right mouse button. This context menu allows various operations to be performed on the file object from within the shell, like printing it or opening it with another program. For example, Figure 4-1 shows the context menu that's displayed when the user clicks on a file in Windows Explorer.
Figure 4-1. A context menu
![]()
The items on context menus fall into two categories: static and dynamic. Static context menu items are always the same for every file object of a given type. They can be associated with a file object with just a few registry entries and require no shell extension handlers. The "handler" in this circumstance--that is, the object that performs some action on the file object when that particular context menu item is selected--is usually a normal executable that is passed the name of the file as a command-line parameter. Dynamic context menus, on the other hand, are created with the help of a shell extension handler, which, as we discussed earlier, is a COM component that runs in-process to Explorer. This handler provides the means to display different context menu items for file objects of the same type. The exact appearance of the context menu typically is determined by some state internal to the file itself. Static menus warrant a brief discussion, but the main focus of this chapter will be on dynamic context menus.
Static Context Menus
Static context menu items are listed under the application identifier key under a subkey called
shell(as opposed to theshellexkey). These entries remain constant for every instance of the file object and require no implementation code.Figure 4-2 illustrates how to add an Open context menu item to the .rad file. The subkey of
shell(in this caseopen) is the verb value for the command. There are seven verbs, called canonical verbs, whose meaning is automatically recognized by the shell:open,find,explore,printto,openas, andproperties. (Theprinttokey is never shown in a context menu, but allows a file to be dragged to a printer object for printing.)
Figure 4-2. Registry entry for static "Open" context menu
![]()
The default value of the verb key contains the text for the context menu; in the case of Figure 4-2, the open verb is described in the context menu as "Open." The verb key's subkey is the
commandkey, whose default value contains the path of the file that will be used to carry out the command. The%1portion of this string in Figure 4-2 denotes the file that was selected within the shell. Whatever file is selected will be passed to notepad.exe on the command line. Of course, this only works because notepad.exe accepts command-line arguments.However, don't believe for a second that you are limited to these seven canonical verbs. You can actually add you own commands to the context menu and call them anything you want. For example, let's add Register and Unregister commands to the context menu for DLLs. This will provide us with a convenient way to register and unregister components.
To accomplish this, we need to locate the application identifier key for a DLL, which happens to be
dllfile. Then, under theshellsubkey, we add two other keys:RegisterandUnregister. Figure 4-3 shows how the relevant portion of the registry should appear in order to support these two static commands.
Figure 4-3. Static menu handlers to register and unregister DLLs
![]()
As you can see from Figure 4-2, we must also add an additional subkey named
command. The default value for this key will contain the command that we actually want to execute. The following script, DLLRegister.reg, will do everything for you:REGEDIT4[HKEY_CLASSES_ROOT\dllfile\shell\Unregister\command]@="regsvr32.exe /u %1"[HKEY_CLASSES_ROOT\dllfile\shell\Register\command]@="regsvr32.exe %1"Static Context Menus in IE 5.0
With the release of Internet Explorer 5.0, Microsoft has made it possible for you to define your own static context menu items. It's as simple as adding a new registry key at the following location:
HKEY_CURRENT_USERSOFTWAREMicrosoftInternet ExplorerMenuExtThe default value for the key can be either a URL or a program. An additional key called
contextsmust also be present. This key contains a binary value that determines to which context menu (Internet Explorer provides several, depending on the circumstances) you want to add the new menu item. The values are:
Context Menu
Value
Default
0x01
Image
0x02
ActiveX Control
0x04
Table
0x08
Selected Text
0x10
Hyperlink
0x20
Dynamic Context Menus
Static context menus are limited because they are the same for every file object of a given type. Also, the number of files that can be processed through a static menu is limited by the program that is used to carry out the command. What if you need to process 20 files? What if you need different processing options based on the state of the file itself? There are also situations where you might need one context menu for a group of files and another for a single file. This is where dynamic context menus come into play.
A context menu handler is an ActiveX DLL that implements two interfaces:
IShellExtInitandIContextMenu. A third interface,IDataObject, is required to implementIShellExtInit. It is not implemented by the object itself but exists as a method parameter inIShellExtInit. We'll explore these interfaces in greater depth after we examine how the shell uses a context menu handler to assemble a context menu.The process begins when one or more files is right-clicked in Explorer. When this occurs, the shell checks the
shellexkey under the application identifier key to see if a context menu handler has been defined for the selected file type. In the case of the .rad file, the shell would look under the following key:HKEY_CLASSES_ROOT/radfile/shellex/ContextMenuHandlers/If you select 15 files that are of all different types, there is still only one file with active focus: the last file selected in the group. It is this file for which the shell attempts to find an associated context menu handler.
If a context menu handler exists, the shell loads the handler and calls
IShellExtInit::Initialize. One of the parameters ofInitializeis a reference toIDataObject. The shell usesIDataObjectto tell us how many files are selected and what their names happen to be. This gives us the opportunity (as the implementors ofIShellExtInit) to save the filenames and the number of selected files for later use. This information can be stored in private member variables within the class. Later, when a command is actually selected from the context menu, the array of files can be referenced and processing decisions can be made.Next, the shell calls
IContextMenu::QueryContextMenu. This method is responsible for adding items to the context menu. The shell passes into the method a handle to the context menu, called anHMENU. An index representing a valid insertion point for the menu item is also passed in. Adding the menu item is simply a matter of calling the InsertMenu API.You might want different menu items displayed based on whether one or multiple files have been selected. Since the number of files selected can be determined in
IShellExtInit::Initialize, this becomes a trivial matter. You also have the ability to base the menu item on the file itself. In addition to the number of files selected, you would also already know the filenames in question. This means you could open the file, retrieve information, and base the menu item on actual data. Or you could examine some other attribute of the file (such as its creation date, its size, or its read-only status) and base the menu item on that information as well.At this point, the shell displays the context menu with the additional menu items. Once the context menu is displayed, the shell attempts to call
IContextMenu::GetCommandStringwhenever the mouse is moved over the new context menu item. This allows you to provide a help string that will be displayed in the status bar of Explorer when the context menu item is highlighted.When the command is actually selected, the shell calls
IContextMenu::InvokeCommandon the handler. The method allows you to determine which context menu item has been selected, and as a result your handler can carry out the appropriate actions.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.Shapedefines two methods:DrawandColor. Therefore, you could expect to access the following functionality through Triangle:Triangle.DrawTriangle.ColorBecause 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.
IShellExtInit
IShellExtInitcontains one method (besides theIUnknownportion of the interface),Initialize, as shown in Table 4-1.
Table 4-1: IShellExtInit Method
Description
InitializeInitializes the shell extension
IShellExtInit::Initializeis 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 anITEMIDLISTstructure (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, Browser Extensions. 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 anIDataObjectinterface that provides information about the selected objects. TheIDataObjectinterface 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 theHKEY_CLASSES_ROOT\Word.Document.8key 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
IDataObjectinterface. Like the first parameter,IDataObjectis 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, Data Handlers, 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 aVariant. Table 4-2 lists all of the datatypes that are considered OLE automation compatible.
Table 4-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
IDispatchinterfaceInterface IUnknown *
Any interface pointer that directly derives from
IUnknowndispinterface Typename *
Pointer to an interface derived from
IDispatchCo-class Typename *
Pointer to a co-class name
[oleautomation] interface Typename *
Pointer to an interface that derives from
IUnknownSAFEARRAY(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
IShellExtInitsuccessfully, 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
Initializemethod, which is anLPCITEMIDLIST. The documentation for the interface states that this is an address of anITEMIDLIST. (We'll talk aboutITEMIDLISTin Chapter 11, Namespace Extensions.) 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 anITEMIDLISTwhen we define theIShellExtInitinterface. What can we do? Well, a pointer is four bytes wide, so the automation-compatible type that can be used in place ofLPCITEMIDLISTis along. When we create our type library, we will just redefineLPCITEMIDLISTto mean along, like so:typedef [public] long LPCITEMIDLIST;When we actually define the
Initializemethod (see Example 4-1), we can still useLPCITEMIDLISTfor the datatype of the first parameter. Then, when VB displays the parameters for the method viaIntelliSense, rather than seeinglong, we will seeLPCITEMIDLIST. 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. AnHKEYis a handle to a registry key. Handles to anything are four bytes, so alongworks 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
IDataObjectinterface pointer.Andinterface pointers that are derived fromIUnknownorIDispatchare 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
HKEYis really a void pointer--that is, a pointer that does not point to any specific datatype. As along, you can use this value as is with any of the registry API functions that takeHKEYs.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 ITEMIDLISTCopyMemory idlist, ByVal pidlFolder, len(idlist)Notice, though, that the second parameter to CopyMemory (our
ITEMIDLISTthat has been redefined as a long) is passed to the functionByVal. 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
IShellExtInitinterface 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);}
The
[public]attribute used in Example 4-1 makes thetypedefvalues 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 forIShellExtInit. 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, VBShell Library Listing ), on the other hand, can be anything since it's being defined by us--but 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
![]()
Example 4-2: Source Code for a Self-Created GUIDGEN Utility Option ExplicitPrivate Type GUIDData1 As LongData2 As IntegerData3 As IntegerData4(7) As ByteEnd TypePrivate Declare Function CoCreateGuid Lib "ole32.dll" _(g As GUID) As LongPrivate 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 LongPrivate Sub StrFromPtrW(pOLESTR As Long, strOut As String)Dim ByteArray(255) As ByteDim intTemp As IntegerDim intCount As IntegerDim i As IntegerintTemp = 1'Walk the string and retrieve the first byte of each WORD.While intTemp <> 0CopyMemory intTemp, ByVal pOLESTR + i, 2ByteArray(intCount) = intTempintCount = intCount + 1i = i + 2Wend'Copy the byte array to our string.CopyMemory ByVal strOut, ByteArray(0), intCountEnd SubPrivate Sub Command1_Click( )Dim g As GUIDDim lsGuid As LongDim sGuid As String * 40If CoCreateGuid(g) = 0 ThenStringFromCLSID g, lsGuidStrFromPtrW lsGuid, sGuidEnd IfInputBox "This is your GUID!", "GUID", sGuidEnd SubFiguring 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.
IDataObject
IDataObjectis not implemented by the context menu handler directly, but rather, it is a parameter toIShellExtInit::Initialize. Therefore, it has to be defined in the type library.IDataObjectprovides the means to determine which files have been right-clicked within the shell.IDataObjectis a fairly complex interface that contains nine methods:GetData,GetDataHere,QueryData,GetCanonicalFormat,SetData,EnumFormatEtc,DAdvise,DUnadvise, andEnumDAdvise. 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 aFORMATETCstructure. TheFORMATETCstructure 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 aSTGMEDIUMstructure.STGMEDIUMis 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
FORMATETCandSTGMEDIUMis rather cryptic here. This is intentional. When we implementIShellExtInitlater in the chapter, just understand that the shell is usingIDataObjectto transfer a list of files to us.IDataObjectis 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.IContextMenu
As Table 4-3 shows,
IContextMenucontains three methods:GetCommandString,InvokeCommand, andQueryContextMenu. 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.
Table 4-3: IContextMenu Method
Description
GetCommandStringReturns the help string that Explorer will display in the status bar.
InvokeCommandImplements menu commands when the menu items are selected.
QueryContextMenuAdds items to the context menu.
GetCommandString
GetCommandStringallows 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
GetCommandStringmethod:
- 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_HELPTEXTReturns the Help text for the context menu item.
GCS_VALIDATEValidates that the menu item exists.
GCS_VERBReturns 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.
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
CMINVOKECOMMANDINFOstructure that contains information about the command to execute when the menu item is selected.
The
CMINVOKECOMMANDINFOstructure 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_HOTKEYThe dwHotKey member is valid.
CMIC_MASK_ICONThe hIcon member is valid.
CMIC_MASK_FLAG_NO_UITells 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_SHOWorSW_HIDE.
- dwHotKey
- fMask must contain
CMIC_MASK_HOTKEYfor 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.
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
QueryContextMenumethod can then use this information when calling the Win32 InsertMenu function to modify the context menu.The documentation for the interface states that
QueryContextMenushould return the menu identifier of the last menu item added, plus one. This presents an interesting problem, because VB does not allow access to theHRESULT. 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 ofQueryContextMenuFlags. 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.QueryContextMenuFlagsis 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;Creating a Context Menu Handler
Let's put all of this into action and actually implement a context menu handler for the .rad file. We'll add a context menu item that displays the noise an animal makes in a message box. The menu item itself will be displayed in the format (Animal Name)
Noise. Animal Name will be determined from the .rad file in question. Let's begin.Type Library
The first step to creating the .rad file context menu handler is to compile the type library containing the interface definitions and constants that will be needed from VB. Constants and UDTs will also be put into the type library with their associated interfaces. But only the groups of constants that are needed will be put in the library. For instance, we need the menu constants
MF_BYPOSITION,MF_STRING, andMF_SEPARATOR. Therefore, the library will contain all of theMF_constants. We don't need any of the menu state constants (MFS_), so they will not be included with the library.The complete listing for the type library that will be used throughout the course of this book can be found in Appendix A. To compile the library, you need to have MKTYPLIB in your path. MKTYPLIB takes one argument on the command line, the name of the ODL file containing the type library definition. To compile, simply type:
mktyplib vbshell.odlfrom the command line. If everything is in order, this should produce a file named vbshell.tlb. This is the type library.
To use this library from Visual Basic, you should select Project
References . . . from the main menu. You should then browse to the location of the .tlb file and select it. This will do two things. First, it will register the type library at that location; second, it will make it available to the References dialog for all future projects.
The Project
The context menu handler begins life as an ActiveX DLL project called RadEx. Our first step is to register the type library so that interface definitions are available for us to implement. That is done by selecting Project
References from VB and then Browse (the library is not registered, so it will not be in the list). Navigate to the library that is associated with this chapter and add the reference. The library will be available in the References list box from this point on.
Next, add the class that will implement the handler to the project. Call this class clsContextMenu. With the class added to the project,
IShellExtInitandIContextMenucan be implemented as follows:Option ExplicitImplements IContextMenuImplements IShellExtInitImplementing IShellExtInit
Let's implement
IShellExtInit::Initializefirst. Notice that, in the code shell that Visual Basic creates for theInitializemethod, the parameters are prefixed with the name of the library in which their definitions are located:Private Sub IShellExtInit_Initialize( _ByVal pidlFolder As VBShellLib.LPCITEMIDLIST, _ByVal pDataObj As VBShellLib.IDataObject, _ByVal hKeyProgID As VBShellLib.HKEY)In some cases, you might want to add a private variable to your class to hold the
IDataObjectreference passed in by the shell, since from it you can determine how many files are selected in the user interface and what the names of those files happen to be. We will useIDataObjectto get the selected files fromIShellExtInit::Initializeimmediately, but it may be preferable to wait until a menu item is actually selected before the selected files are determined (possibly for performance reasons). In this particular case, saving theIDataObjectreference is not necessary. Rather than hold a reference toIDataObject, we will use pDataObj directly and build an array containing the names of the selected files. This array will be kept as private data. The entire listing for theInitializemethod is shown in Example 4-4.
Example 4-4: Implementing IShellExtInit::Initialize 'handler.basPublic Declare Function DragQueryFile Lib "shell32.dll" _Alias "DragQueryFileA" (ByVal HDROP As Long, _ByVal pUINT As Long, ByVal lpStr As String, _ByVal ch As Long) As LongPublic Declare Function ReleaseStgMedium Lib "ole32.dll" _(pMedium As STGMEDIUM) As Long'clsContextMenu.clsOption ExplicitImplements IContextMenuImplements IShellExtInitPrivate m_sFiles( ) As StringPublic m_nFiles As BytePrivate Sub IShellExtInit_Initialize( _ByVal pidlFolder As VBShellLib.LPCITEMIDLIST, _ByVal pDataObj As VBShellLib.IDataObject, _ByVal hKeyProgID As VBShellLib.HKEY)Dim FmtEtc As FORMATETCDim pMedium As STGMEDIUMDim i As LongDim lresult As LongDim sTemp As StringWith FmtEtc.cfFormat = CF_HDROP.ptd = 0.dwAspect = DVASPECT_CONTENT.lindex = -1.TYMED = TYMED_HGLOBALEnd WithpDataObj.GetData FmtEtc, pMediumm_nFiles = DragQueryFile(pMedium.pData, &HFFFFFFFF, _vbNullString, 0)ReDim m_sFiles(m_nFiles - 1)For i = 0 To (m_nFiles - 1)sTemp = String(1024, 0)lresult = DragQueryFile(pMedium.pData, i, sTemp, _Len(sTemp))If (lresult > 0) Thenm_sFiles(i) = Left$(sTemp, lresult)End IfNextReleaseStgMedium pMediumEnd SubThere's quite a bit going here, so let's just take it from the top, starting with the call to GetData. GetData takes two parameters: an
[in]parameter containing a pointer to aFORMATETCstructure, and an[in,out]parameter that returns a pointer to aSTGMEDIUMstructure. The function is called like so:pDataObj.GetData FmtEtc, pMediumThe parameters are as follows:
- FORMATETC
FORMATETCis a generalized clipboard format used by OLE wherever data format information is required. The structure contains the clipboard format, a pointer to a target device, the view of the data, how much of the data should be transferred, and the medium used to transfer the data. The members of the structure are assigned values in the following code fragment from Example 4-4:
With FmtEtc.cfFormat = CF_HDROP.ptd = 0.dwAspect = DVASPECT_CONTENT.lindex = -1.TYMED = TYMED_HGLOBALEnd With- In this case, the data transferred will be a handle to a drop structure (our list of files) specified by
CF_HDROP. The target device (specified by ptd ) is 0, because we don't care about its value; it's actually device-independent.DVASPECT_CONTENTmeans we want the actual data. A clipboard format can support more than one aspect or view. Here, we don't need a view, we just need the data. lindex is unimportant to the discussion. Last is theTYMED_HGLOBALflag, which means the transfer will take place using global memory (as opposed to a file or structured storage objects). TheTYMEDmember specifies which member of theSTGMEDIUMunion will be valid.
- STGMEDIUM
- The second parameter to GetData is a pointer to a
STGMEDIUMunion. The union is based on the type of medium, which in this case isTYMED_HGLOBAL(specified byFORMATETC). Therefore, under normal circumstances, the union member hGlobal would contain the handle to the drop structure. However, since this structure has been redefined, the pData member will always point to the data. This handle can be passed directly to the Win32 DragQueryFile function, which then allows us to find out how many files have been selected:
nFiles = DragQueryFile(pMedium.pData, &HFFFFFFFF, vbNullString, 0)
- Passing DragQueryFile the value
&HFFFFFFFFtells it that we want the number of files selected. We can also pass it a number between 0 and the total number of files selected to get the name of the file itself.
- The value for nFiles allows us to redimension our file array. DragQueryFile can then be called in a loop with the index of the requested file supplied as the second argument to the function. The filename (which is written to the buffer that passed as the third argument to the function) is retrieved and stored in the file array. If multiple files of different types are selected and the file with primary focus is a .rad file, our handler will still be called. But we have to filter these extraneous types if necessary. To do this, we can have
IContextMenu::InvokeCommandloop through this array and process the context menu command for every valid file that is selected.
- Here's one last detail: the
STGMEDIUMstructure has been allocated by the call to GetData. It is common to see this structure populated by a "provider" outside of the code in which it is being used, as is the case in Example 4-4. This means freeing the memory is our responsibility, and that is what the final call to ReleaseStgMedium (a routine found in Ole32.dll ) is doing.
Implementing IContextMenu
The
IContextMenuinterface is responsible for displaying the text of the menu item, for showing help text associated with the menu item, and for defining the action to be performed if the menu item is selected. In this section, we'll examine the code for the methods responsible for those operations.
- GetCommandString
- The source code for the
GetCommandStringmethod is shown in Example 4-5.GetCommandStringis called by the shell for the purpose of retrieving help text for a context menu item. This help text is then displayed in the status bar. This method is notable in that this is the first time we have to worry about implementing a method that will run under both Windows 98 and Windows NT. As you might guess, this has to do with how both platforms deal with strings. Windows 98 uses ANSI strings internally; Windows NT uses Unicode. VB uses Unicode strings internally, regardless of what platform is being used. Confusing, to say the least.
- The menu item in question is determined by the idCmd parameter passed in by the shell. uType indicates the flags that inform us of the information being requested. We will return the same string regardless of these flags. The only distinction we are interested in is whether the values should be ANSI or Unicode. (There are separate ANSI and Unicode versions of each constant stored to uType.) A buffer for the help string is provided through the pszName parameter. cchMax is the size of this buffer.
- The ANSI portion of the listing uses StrConv to convert the string from Unicode to ANSI. From this point forward, a common tactic is used. The string is copied into a byte array, and its starting address is copied to the memory location provided by the shell.
Example 4-5: GetCommandString Listing Private Sub IContextMenu_GetCommandString( _ByVal idCmd As VBShellLib.UINT, _ByVal uType As VBShellLib.UINT, _ByVal pwReserved As VBShellLib.UINT, _ByVal pszName As VBShellLib.LPSTRVB, _ByVal cchMax As VBShellLib.UINT)Dim szName As StringDim bszName( ) As ByteDim sMenuHelp As StringSelect Case idCmdCase 0 'NoiseszName = "Display Animal Noise"'Other menu items would be added like so:'Case 1 'Menu item 2' szName = "Menu Item 2"'Case 2 'Menu item 3' szName = "Menu Item 3"End SelectszName = Left$(szName, cchMax) & vbNullCharSelect Case uTypeCase GCS_VERBA, GCS_HELPTEXTA, GCS_VALIDATEAIf (szName <> "") ThenbszName = StrConv(szName, vbFromUnicode)CopyMemory ByVal pszName, _bszName(0), _UBound(bszName) + 1End IfCase GCS_VERBW, GCS_HELPTEXTW, GCS_VALIDATEWIf (szName <> "") ThenbszName = szNameCopyMemory ByVal pszName, _bszName(0), _UBound(bszName) + 1End IfEnd SelectEnd Sub
- InvokeCommand
InvokeCommandis called when the shell is ready to execute the context menu command. Its source code is shown in Example 4-6. The implementation of this method is fairly straightforward. Of interest is the pointer to theCMINVOKECOMMANDINFOstructure that is passed in by the shell.CMINVOKECOMMANDINFOis one of those structures that mean something different depending on the context in which it is used. Check the Platform SDK for full details on this one.
- This structure, while weighty as far as information goes, contains only one member that is of interest to us: lpVerb. The low-order word of lpVerb contains the menu identifier of the command being invoked.
- By the time the shell calls
InvokeCommand, we already have an array of the selected files stored as private data within our component. This allows us to grab every file in a loop, to find out the animal type of the file with a call to GetPrivateProfileString, and to display the appropriate information.
Example 4-6: InvokeCommand Listing Private Sub IContextMenu_InvokeCommand(ByVal lpcmi As VBShellLib.LPCMINVOKECOMMANDINFO)Dim cmi As CMINVOKECOMMANDINFOCopyMemory cmi, ByVal lpcmi, Len(cmi)Dim i As LongDim sNoise As StringsNoise = Space(255)If LOWORD(cmi.lpVerb) = 0 ThenFor i = 0 To m_nFiles - 1GetPrivateProfileString "Animal", _"Noise", _"Unknown", _sNoise, _Len(sNoise), _m_sFiles(i)MsgBox Trim(sNoise), vbOKOnly, "Animal Noise"Next iEnd IfEnd Sub
- The LOWORD function is defined in handler.bas. There is also a HIWORD function thrown in for good measure. The two functions look like this:
Public Function LOWORD(ByVal lVal As Long) As Integer
LOWORD = lVal And &HFFFF&
End Function
Public Function HIWORD(ByVal lVal As Long) As Integer
HIWORD = 0
If lVal Then
HIWORD = lVal \ &H10000 And &HFFFF&
End If
End Function- QueryContextMenu
QueryContextMenuis used to add menu items to a file object's context menu. ImplementingIContextMenu::QueryContextMenuis going to be a tricky process. The Platform SDK states that this method must return a positive integer representing the menu identifier of the last menu item added plus one. You might have noticed that these interface methods are implemented as subs, not functions. Even though we are dealing with a sub, VB still returns a value for each of these methods: a 0 if everything is okay or an error code that is available through the Err object. We have no direct access to the value returned from these methods.
- There is a solution to this dilemma. We will write a replacement function for
QueryContextMenuand put it in a code module located in the project. Then we will find the vtable entry forQueryContextMenuin our object (see Chapter 1, Introduction). We will use theAddressOfoperator, in conjunction with CopyMemory, and swap the two addresses. Our new function, QueryContextMenuVB, will be called instead of the class implementation. Of course, QueryContextMenuVB will be a function, and we can return any value we want. When the object is released, the two addresses will be swapped back for posterity's sake. Our troubles are solved.
- The addresses of the two functions need to be swapped as quickly as possible. Therefore, the Initialize and Terminate events (which are shown in Example 4-7 and Example 4-9, respectively) of the context menu handler class are used for this purpose.
Example 4-7: Swapping vtable Entries Private m_pOldQueryCtxMenu As LongPrivate Sub Class_Initialize( )Dim pVtable As IContextMenuSet pVtable = Mem_pOldQueryCtxMenu = SwapVtableEntry(ObjPtr(pVtable), _4, AddressOf QueryContextMenuVB)End Sub- A variable of type
IContextMenuis set toMe. This gives us a pointer to theIContextMenuportion of the vtable. This memory location is copied into pVtable, effectively giving us a pointer to theIContextMenuportion of our object's vtable. Then, SwapVtableEntry (shown in Example 4-8) is called with the address of the first method ofIContextMenu(this is the portion of the vtable whereIContextMenubegins), the relative position in the vtable of the method we want to replace (in this case, 4--we'll see why in a few moments), and the address of the new function. One thing of interest in SwapVtableEntry is the call to VirtualProtect. VB has marked the object memory as protected. This call changes the access permissions, allowing us to swap the addresses.
Example 4-8: SwapVtableEntry Listing Public Function SwapVtableEntry(pObj As Long, _EntryNumber As Integer, _ByVal lpfn As Long) As LongDim lOldAddr As LongDim lpVtableHead As LongDim lpfnAddr As LongDim lOldProtect As LongCopyMemory lpVtableHead, ByVal pObj, 4lpfnAddr = lpVtableHead + (EntryNumber - 1) * 4CopyMemory lOldAddr, ByVal lpfnAddr, 4Call VirtualProtect(lpfnAddr, 4, _PAGE_EXECUTE_READWRITE, _lOldProtect)CopyMemory ByVal lpfnAddr, lpfn, 4Call VirtualProtect(lpfnAddr, 4, lOldProtect, lOldProtect)SwapVtableEntry = lOldAddrEnd Function
- How do we know where
QueryContextMenuis located in relation to this address? Well, we can't look at our class file for clues, because VB just displays all of the implemented methods in alphabetical order. This is not an accurate representation of our object.
- To determine the vtable order of the method in question, look at the ODL listing. The methods are listed in the order in which they appear in the vtable. You can also use OLE View to get this information (should ODL be unavailable). Object Browser, however, does not provide it; it just lists the methods in alphabetical order. If you examine the
IContextMenuinterface definition in this manner, you will see thatQueryContextMenuis the first method listed in the interface. Taking into consideration that the interface is derived fromIUnknown, which contains three methods,QueryContextMenuis the fourth method. Thus, we pass 4 to SwapVtableEntry.
- When the object terminates, the addresses can be switched back in the same manner, as shown in the class Terminate event handler in Example 4-9.
Example 4-9: Restoring vtable Entries Private Sub Class_Terminate( )Dim pVtable As IContextMenuSet pVtable = Mem_pOldQueryCtxMenu = SwapVtableEntry(ObjPtr(pVtable), _4, m_pOldQueryCtxMenu)End Sub
- QueryContextMenuVB
QueryContextMenuVBgives us some insight into just how a class works. We already know that a class keeps track of its member functions with the vtable. But once we are inside one of those member functions, how is it that we can have access back to the class? To the other methods? To Private and Public data members? Well, when a member function is called, a pointer to the class is also passed with the parameters to the function. VB (also C++) handles this behind the scenes, making everything look nice and smooth. C++ programmers refer to the parameter as thethispointer. VB can use this pointer to resolve all references back to the object.
QueryContextMenuVBmust make allowances for this parameter, because it is not a part of a class; it is a function defined in a code module. This means we have to add our ownthispointer to the parameter list. Example 4-10 shows how we can then define a local copy of clsContextMenu and use thethispointer to get a reference back to our class. This is really cool, because we don't have to use a global variable to get at our class now.
Example 4-10: QueryContextMenuVB Implementation 'ContextMenu.basPublic Function QueryContextMenuVB (ByVal this As IContextMenu, _ByVal hMenu As Long, _ByVal indexMenu As Long, _ByVal idCmdFirst As Long, _ByVal idCmdLast As Long, _ByVal uFlags As Long) As LongDim ctxMenu As clsContextMenuSet ctxMenu = this
- The main task of
QueryContextMenuVB(which we seem to have ignored for a while) is to add menu items to the context menu. First, the circumstance in which the context menu is activated needs to be determined. This is accomplished with the uFlags parameter that is passed in by the shell. The following code fragment shows the various situations in which the context menu can be activated. The flag we are primarily interested in isCMF_EXPLORE:
If (uFlags And &HF) = CMF_NORMAL Then
'Implement this for Drag-and-Drop handler.
ElseIf (uFlags And CMF_VERBSONLY) Then
'This is a context menu for a shortcut item.
ElseIf (uFlags And CMF_EXPLORE) Then
'Right-click on file in Explorer.
'This is what we are interested in for our context
'menu.
ElseIf (uFlags And CMF_DEFAULTONLY) Then
'Indicates a default action is being performed (typically a
'user is double-clicking on the file).
End IfOnce it has been determined that files have been right-clicked in Explorer, the context menu item can be added accordingly. The menu item added is based on the number of files selected and the type of animal represented by the file. The animal type is determined with a call to GetPrivateProfileString:
ElseIf (uFlags And CMF_EXPLORE) Then'Right-click on file in ExplorerIf ctxMenu.FileCount > 1 ThensMenuItem = "Bunches o' Animal noises"ElseGetPrivateProfileString "Animal", _"Type", _"Unknown", _sAnimal, _Len(sAnimal), _ctxMenu.FileNamesAnimal = Trim(sAnimal)sAnimal = Left$(sAnimal, Len(sAnimal) - 1)sMenuItem = sAnimal & "Noise"End IfCall InsertMenu(hMenu, _indexMenu, _MF_STRING Or MF_BYPOSITION, _idCmd, _sMenuItem)idCmd = idCmd + 1indexMenu = indexMenu + 1'If you want to add another item just repeat the following code.''sMenuItem = "Animal Name"'Call InsertMenu(hMenu, _' indexMenu, _' MF_STRING Or MF_BYPOSITION, _' idCmd, _' sMenuItem)''idCmd = idCmd + 1'indexMenu = indexMenu + 1'etc. , etc., etc.''Do not increment idCmd for separators!'IndexMenu is always incremented.Set ctxMenu = Nothing'Lastly, the number of menu items added + 1 is returned.QueryContextMenuVB = indexMenu
Figure 4-5. Defining a drag-and-drop handler in the registry
![]()
Registration and Operation
Last but not least, the handler needs to be registered. As always, the file rad.reg that is included with this chapter's downloadable code contains the appropriate registry entries. Example 4-11 contains the entire listing. Note that items in square brackets must exist on the same line (the listing is formatted to fit on the page).
Example 4-11: rad.reg 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\ContextMenuHandlers]@ = "RadFileMenu"[HKEY_CLASSES_ROOT\radfile\shellex\ContextMenuHandlers\RadFileMenu]@ = "{D4F9CECF-E84E-11D2-BB7C-444553540000}"[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved]"{D4F9CECF-E84E-11D2-BB7C-444553540000}" = "RAD file context menu extension"These are the same entries that were discussed in Chapter 3, Shell Extensions. Review the entries until you become familiar with them. It shouldn't take too long. If you want to register the extension by hand, you will need to find the CLSID for the object. The easiest way to do that is to search under
HKEY_CLASSES_ROOTfor the programmatic identifier, or ProgID, of the extension object. The ProgID is formed by appending the class name to the project name with a period in the middle. So look for "RadEx.clsContextMenu," and there should be a CLSID subkey with the needed value.After you have registered the handler, kill off any instances of Explorer you might have running. The handler, which is shown in Figure 4-6, will be available with the next instance you run. There are sample .rad files included with the source of this book that you can use to test the handler.
Figure 4-6. Context menu in action
![]()
Back to: Visual Basic Shell Programming
© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com