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.
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
, and MF_SEPARATOR
.
Therefore, the library will contain all of the MF_
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.odl
from 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 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,
IShellExtInit
and IContextMenu
can be implemented as follows:
Option Explicit Implements IContextMenu Implements IShellExtInit
Let’s implement
IShellExtInit::Initialize
first. Notice that, in
the code shell that Visual Basic creates for the
Initialize
method, 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 IDataObject
reference 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 use IDataObject
to get the selected files
from IShellExtInit::Initialize
immediately, 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 the
IDataObject
reference is not necessary. Rather
than hold a reference to IDataObject
, 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 the
Initialize
method is shown in Example 4.4.
Example 4-4. Implementing IShellExtInit::Initialize
'handler.bas Public 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 Long Public Declare Function ReleaseStgMedium Lib "ole32.dll" _ (pMedium As STGMEDIUM) As Long 'clsContextMenu.cls Option Explicit Implements IContextMenu Implements IShellExtInit Private m_sFiles( ) As String Public m_nFiles As Byte Private Sub IShellExtInit_Initialize( _ ByVal pidlFolder As VBShellLib.LPCITEMIDLIST, _ ByVal pDataObj As VBShellLib.IDataObject, _ ByVal hKeyProgID As VBShellLib.HKEY) Dim FmtEtc As FORMATETC Dim pMedium As STGMEDIUM Dim i As Long Dim lresult As Long Dim sTemp As String With FmtEtc .cfFormat = CF_HDROP .ptd = 0 .dwAspect = DVASPECT_CONTENT .lindex = -1 .TYMED = TYMED_HGLOBAL End With pDataObj.GetData FmtEtc, pMedium m_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) Then m_sFiles(i) = Left$(sTemp, lresult) End If Next ReleaseStgMedium pMedium End Sub
There’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 a FORMATETC
structure, and an
[in,
out]
parameter that
returns a pointer to a STGMEDIUM
structure. The
function is called like so:
pDataObj.GetData FmtEtc, pMedium
The parameters are as follows:
- FORMATETC
FORMATETC
is 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_HGLOBAL End 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 byptd
) is 0, because we don’t care about its value; it’s actually device-independent.DVASPECT_CONTENT
means 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_HGLOBAL
flag, which means the transfer will take place using global memory (as opposed to a file or structured storage objects). TheTYMED
member specifies which member of theSTGMEDIUM
union will be valid.
- STGMEDIUM
The second parameter to GetData is a pointer to a
STGMEDIUM
union. The union is based on the type of medium, which in this case isTYMED_HGLOBAL
(specified byFORMATETC
). Therefore, under normal circumstances, the union memberhGlobal
would contain the handle to the drop structure. However, since this structure has been redefined, thepData
member will always point to the data. This handle can be passed directly to the Win32DragQueryFile
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&HFFFFFFFF
tells it that we want the number of files selected. We can also pass it a number between 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 haveIContextMenu::InvokeCommand
loop through this array and process the context menu command for every valid file that is selected.Here’s one last detail: the
STGMEDIUM
structure 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 toReleaseStgMedium
(a routine found inOle32.dll
) is doing.
The
IContextMenu
interface 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
GetCommandString
method is shown in Example 4.5.GetCommandString
is 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 touType
.) A buffer for the help string is provided through thepszName
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 String Dim bszName( ) As Byte Dim sMenuHelp As String Select Case idCmd Case 0 'Noise szName = "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 Select szName = Left$(szName, cchMax) & vbNullChar Select Case uType Case GCS_VERBA, GCS_HELPTEXTA, GCS_VALIDATEA If (szName <> "") Then bszName = StrConv(szName, vbFromUnicode) CopyMemory ByVal pszName, _ bszName(0), _ UBound(bszName) + 1 End If Case GCS_VERBW, GCS_HELPTEXTW, GCS_VALIDATEW If (szName <> "") Then bszName = szName CopyMemory ByVal pszName, _ bszName(0), _ UBound(bszName) + 1 End If End Select End Sub
- InvokeCommand
InvokeCommand
is 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 theCMINVOKECOMMANDINFO
structure that is passed in by the shell.CMINVOKECOMMANDINFO
is 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 oflpVerb
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 toGetPrivateProfileString
, and to display the appropriate information.Example 4-6. InvokeCommand Listing
Private Sub IContextMenu_InvokeCommand(ByVal lpcmi As VBShellLib.LPCMINVOKECOMMANDINFO) Dim cmi As CMINVOKECOMMANDINFO CopyMemory cmi, ByVal lpcmi, Len(cmi) Dim i As Long Dim sNoise As String sNoise = Space(255) If LOWORD(cmi.lpVerb) = 0 Then For i = 0 To m_nFiles - 1 GetPrivateProfileString "Animal", _ "Noise", _ "Unknown", _ sNoise, _ Len(sNoise), _ m_sFiles(i) MsgBox Trim(sNoise), vbOKOnly, "Animal Noise" Next i End If End Sub
The
LOWORD
function is defined inhandler.bas
. There is also aHIWORD
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
QueryContextMenu
is used to add menu items to a file object’s context menu. ImplementingIContextMenu::QueryContextMenu
is 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 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
QueryContextMenu
and put it in a code module located in the project. Then we will find the vtable entry forQueryContextMenu
in our object (see Chapter 1). We will use theAddressOf
operator, in conjunction withCopyMemory
, 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 Long Private Sub Class_Initialize( ) Dim pVtable As IContextMenu Set pVtable = Me m_pOldQueryCtxMenu = SwapVtableEntry(ObjPtr(pVtable), _ 4, AddressOf QueryContextMenuVB) End Sub
A variable of type
IContextMenu
is set toMe
. This gives us a pointer to theIContextMenu
portion of the vtable. This memory location is copied intopVtable
, effectively giving us a pointer to theIContextMenu
portion 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 whereIContextMenu
begins), 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 inSwapVtableEntry
is the call toVirtualProtect
. 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 Long Dim lOldAddr As Long Dim lpVtableHead As Long Dim lpfnAddr As Long Dim lOldProtect As Long CopyMemory lpVtableHead, ByVal pObj, 4 lpfnAddr = lpVtableHead + (EntryNumber - 1) * 4 CopyMemory lOldAddr, ByVal lpfnAddr, 4 Call VirtualProtect(lpfnAddr, 4, _ PAGE_EXECUTE_READWRITE, _ lOldProtect) CopyMemory ByVal lpfnAddr, lpfn, 4 Call VirtualProtect(lpfnAddr, 4, lOldProtect, lOldProtect) SwapVtableEntry = lOldAddr End Function
How do we know where
QueryContextMenu
is 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
IContextMenu
interface definition in this manner, you will see thatQueryContextMenu
is the first method listed in the interface. Taking into consideration that the interface is derived fromIUnknown
, which contains three methods,QueryContextMenu
is the fourth method. Thus, we pass 4 toSwapVtableEntry
.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.
- QueryContextMenuVB
QueryContextMenuVB
gives 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 thethis
pointer. VB can use this pointer to resolve all references back to the object.QueryContextMenuVB
must 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 ownthis
pointer to the parameter list. Example 4.10 shows how we can then define a local copy of clsContextMenu and use thethis
pointer 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.bas Public 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 Long Dim ctxMenu As clsContextMenu Set 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 theuFlags
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 If
Once 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 Explorer If ctxMenu.FileCount > 1 Then sMenuItem = "Bunches o' Animal noises" Else GetPrivateProfileString "Animal", _ "Type", _ "Unknown", _ sAnimal, _ Len(sAnimal), _ ctxMenu.FileName sAnimal = Trim(sAnimal) sAnimal = Left$(sAnimal, Len(sAnimal) - 1) sMenuItem = sAnimal & "Noise" End If Call InsertMenu(hMenu, _ indexMenu, _ MF_STRING Or MF_BYPOSITION, _ idCmd, _ sMenuItem) idCmd = idCmd + 1 indexMenu = 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
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. 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_ROOT
for 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.
Get VB Shell Programming now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.