Visual Basic Shell Programming
only for RuBoard - do not distribute or recompile |
11.6 The PIDL Manager
Before we continue, we really need to discuss pidlMgr.cls . This class is a helper class that we will use to manage functions involving PIDLs. These helper functions include things like creating, copying, and deleting PIDLs, getting the last PIDL in a list of PIDLs, getting the next PIDL in a list of PIDLs, as well as additional functions that are more specific to our particular namespace extension.
So add a new class to the project called pidlMgr.cls , and let's start implementing some of the functionality of this class.
11.6.1 Delete
We have used three functions from this class so far: Delete , Copy , and Create . Delete is by far the easiest of the functions to implement. It just wraps a call to IMalloc::Free . Delete looks like this:
'pidlMgr.cls Private m_ pMalloc As IMalloc Private Sub Class_Initialize( ) Set m_ pMalloc = GetMalloc End Sub Private Sub Class_Terminate( ) Set m_ pMalloc = Nothing End Sub Public Sub Delete(ByVal pidl As LPITEMIDLIST) m_ pMalloc.Free pidl End Sub
11.6.2 Copy
Copy is used to make a copy of the PIDL:
Public Function Copy(ByVal pidlSource As LPITEMIDLIST) As LPITEMIDLIST Dim pidlTarget As LPITEMIDLIST Dim cbSource As UINT Copy = 0 If (pidlSource = 0) Then Exit Function End If cbSource = GetSize(pidlSource) pidlTarget = m_pMalloc.Alloc(cbSource) If (pidlTarget > 0) Then CopyMemory ByVal pidlTarget, ByVal pidlSource, cbSource Copy = pidlTarget End If End Function
11.6.3 Create
This method is a little more involved, but the reason for this requires some background information. We have already discussed the format of the PIDL with which we will be dealing in this example. It looks like this:
size/type/level/index
Therefore, you might expect to create a UDT to represent this PIDL. Perhaps something like the following:
Type PIDL iSize As Integer pType As Long iLevel As Integer iIndex As Integer End Type
Unfortunately, we cannot do this. The data that comprises the PIDL must be sequential. That is, the PIDL data must immediately follow the two bytes indicating the size of the PIDL itself. The UDT that has been described will not work, because VB aligns UDTs on 4-byte boundaries (Long values). What does this mean exactly? Well, look at the UDT for a moment. The first member, iSize , is an Integer. In VB, for backward-compatibility reasons, that is a 2-byte value. When we talk about aligning data on 4-byte boundaries, this means VB will place two empty bytes immediately after iSize to pad the member to 4-bytes. The same is done with iLevel and iIndex . So, rather than having sequential data, we have data with holes in it, as Figure 11.8 illustrates.
Figure 11.8. PIDL aligned on 4-byte boundaries
The simple solution to this problem is to use CopyMemory and to build our PIDL in memory as if it were a 0-byte aligned structure. Let's step through Create a few lines at a time, starting with the beginning of the method in Example 11.19, and discuss the function.
Example 11.19. Create from pidlMgr
Public Enum PIDLTYPE PT_FOLDER = 0 PT_ITEM = 1 End Enum Public Function Create(ByVal pt As PIDLTYPE, ByVal iLevel As Integer, ByVal iIndex As Integer) As LPITEMIDLIST Dim iSize As Integer Dim pidl As LPITEMIDLIST 'pidl size bytes (2) + pt(4) + iLevel(2) + iIndex(2) iSize = 10 'Allocate memory for PIDL + a NULL ITEMIDLIST entry. pidl = m_pMalloc.Alloc(iSize + 2)
The first thing that happens is that the memory for the PIDL is allocated. The size of our PIDL is 10 bytes. That's 2 bytes for the size, 4 bytes for the type, 2 bytes for the "level," and 2 bytes for the "index." Therefore, we use IMalloc to allocate 10 bytes for our PIDL :
If (pidl > 0) Then CopyMemory ByVal pidl, iSize, 2
If the memory was allocated successfully (it is always prudent to check for this), then the size of the PIDL is copied into the first 2 bytes (the size) of the memory that will hold the PIDL. We can then use pointer arithmetic to copy the remaining values into the proper locations. Since we know our PIDL size is 2 bytes, we can skip 2 bytes forward in memory (PIDL + 2) and get the address for the PIDLTYPE . PIDLTYPE is 4 bytes, so jumping forward 6 bytes (PIDL + 6) will give us the address of the PIDLs level, and so on:
'Copy data into pidl CopyMemory ByVal pidl + 2, pt, 4 'PIDLTYPE CopyMemory ByVal pidl + 6, iLevel, 2 'PIDL level CopyMemory ByVal pidl + 8, iIndex, 2 'PIDL index
Finally, we need to terminate the PIDL with a NULL , ITEMIDLIST . We could just terminate the PIDL with 2 bytes containing 0s, specifying a PIDL of no size. But to be technically accurate, we will end the PIDL with 2 NULL bytes (refer back to the SHITEMID structure). We then return the PIDL back to the caller:
'Add empty ITEMIDLIST to end of PIDL CopyMemory ByVal pidl + iSize, 0, 2 End If Create = pidl End Function
Because we have created the PIDL in this manner, we will write several helper functions in PidlMgr to help us extract these values: GetPidlSize , GetPidlType , GetPidlLevel , and GetPidlIndex . Of the four functions, only one, GetPidlSize , can ever be used again. After all, the PIDL's size will always be the first 2 bytes of the structure. Even so, when you write your own namespace extensions, you will have to write similar functions to retrieve information from the PIDL. The rest are specific to the extension. These functions are shown in Example 11.20. They are self-explanatory.
Example 11.20. pidlMgr Helper Functions
'pidlMgr.cls Public Function GetPidlSize(ByVal pidl As LPITEMIDLIST) As Integer Dim iSize As Integer GetPidlSize = 0 If (pidl = 0) Then Exit Function End If 'Size is the first 2 bytes of the pidl CopyMemory iSize, ByVal pidl, 2 GetPidlSize = iSize End Function Public Function GetPidlType(ByVal pidl As LPITEMIDLIST) As PIDLTYPE Dim pt As Integer GetPidlType = 0 If (pidl = 0) Then Exit Function End If 'The "level" of the pidl is stored in bytes 3-6 CopyMemory pt, ByVal pidl + 2, 4 GetPidlType = pt End Function Public Function GetPidlLevel(ByVal pidl As LPITEMIDLIST) As Integer Dim iLevel As Integer GetPidlLevel = 0 If (pidl = 0) Then Exit Function End If 'The "level" of the pidl is stored in bytes 7-8. CopyMemory iLevel, ByVal pidl + 6, 2 GetPidlLevel = iLevel End Function Public Function GetPidlIndex(ByVal pidl As LPITEMIDLIST) As Integer Dim iIndex As Integer GetPidlIndex = 0 If (pidl = 0) Then Exit Function End If 'The "index" of the pidl is stored in bytes 9-10. CopyMemory iIndex, ByVal pidl + 8, 2 GetPidlIndex = iIndex End Function
We will add new functions to PidlMgr as needed. But for now let's see what happens after our list has been built and the shell has called IEnumIDList::Next .
11.6.4 GetAttributesOf
Now that the shell has one of our PIDLs (as a result of calling IEnumIDList::Next ) it needs to determine whether that PIDL is a folder or not. It does this by asking us, by way of a call to IShellFolder::GetAttributesOf . This method is defined as follows :
HRESULT GetAttributesOf(UINT cidl, LPCITEMIDLIST *apidl, ULONG *rgfInOut);
The first parameter, cidl , is the length of the PIDL array that is being pointed to by the second parameter. This is important. We are not getting a PIDL here. We are getting a pointer to an array of PIDLs. This method should be coded to handle more than one PIDL, even though it will not come into play in the example. It merely provides us the opportunity to write some really dangerous code involving pointer arithmetic. The second parameter, rgfInOut , is an [in, out] parameter that will be assigned one or more values from the SFGAO enumeration. We are only concerned with two of these values, SFGAO_FOLDER and SFGAO_HASSUBFOLDER . Let's look at the implementation, which begins at Example 11.21.
Example 11.21. IShellFolder::GetAttributesOf
Private Sub IShellFolder_GetAttributesOf( _ ByVal cidl As VBShellLib.UINT, _ aPidl As VBShellLib.LPCITEMIDLIST, _ rgfInOut As VBShellLib.ULONG) Dim i As UINT Dim dwAttribs As DWORD Dim pidl As LPITEMIDLIST dwAttribs = dwAttribs Or 0 rgfInOut = -1 For i = 0 To cidl - 1 CopyMemory pidl, aPidl + (i * 4), 4
Everything here is fairly straightforward until we get to the call to CopyMemory . This just increments the address of aPidl by 4 (the size of a pointer to an ITEMIDLIST which is a PIDL) every time we iterate the loop. This will happen cidl times:
If m_pidlMgr.GetPidlType(pidl) = PT_FOLDER Then dwAttribs = dwAttribs Or SFGAO_FOLDER
Now that we have the PIDL, we can call PidlMgr::GetPidlType to retrieve the type of the PIDL. If it is a folder, then we add SFGAO_FOLDER to our attributes DWORD :
If m_pidlMgr.GetPidlLevel(pidl) < g_nMaxLevels Then dwAttribs = dwAttribs Or SFGAO_HASSUBFOLDER End If End If Next i rgfInOut = rgfInOut And dwAttribs End Sub
Next, we'll get the PIDL level. If it's less than g_nMaxLevels , we know that is has subfolders beneath it. Therefore, we turn on the SFGAO_HASSUBFOLDER bits in the attributes DWORD . This will cause Explorer to draw the "+" node next to the folder.
When all is said and done, we return the PIDL attributes by way of rgfInOut .
Of course, this is all very specific to the example for the chapter. When you write your own namespace extensions, the format that you decide to create for your PIDL needs to take functions like GetAttributesOf into consideration. A good rule of thumb is that a PIDL should be able to describe its name and location. Go through the additional sample code that is provided with this chapter and see how this method is implemented in those examples. This should give you a better feel for writing extensions of your own.
11.6.5 GetDisplayNameOf
The shell also needs a way to determine the text to display for a PIDL. It does this by calling IShellFolder::GetDisplayNameOf . Its syntax is as follows:
HRESULT GetDisplayNameOf(LPCITEMIDLIST pidl, DWORD uFlags, LPSTRRET lpName);
The first parameter, pidl , is the PIDL for which the shell wants display information.
The second parameter, uFlags , is a value from the SHGDN enumeration. This value tells us how the shell is trying to display the name. Table 11.8 describes the values from this enumeration. Our chapter example does not use these values, but the additional examples do. If you want more details, check them out. Here, we're just going to return the same string no matter what the circumstances.
Table11.8. SHGDN Enumeration
Constant | Description |
---|---|
SHGDN_NORMAL | The name is a full name relative to the desktop and not to any specific folder. |
SHGDN_INFOLDER | The name is relative to the folder that is processing the name. |
SHGDN_NORMAL | The name will be used for generic display. |
SHGDN_FORADDRESSBAR | The name will be used for display in the address bar combo box. |
SHGDN_FORPARSING | The name will be used for parsing. It can be passed to ParseDisplayName . |
The last parameter, lpName , is the address of an STRRET structure, which is defined as follows:
Public Const STRRET_WSTR = &H0 Public Type STRRET uType As UINT pOLESTR As Long End Type
This parameter is how we'll return the display name to the shell.
Let's examine the implementation for GetDisplayNameOf , which is shown in Example 11.22.
Example 11.22. IShellFolder::GetDisplayNameOf
Private Sub IShellFolder_GetDisplayNameOf( _ ByVal pidl As VBShellLib.LPCITEMIDLIST, _ ByVal uFlags As VBShellLib.DWORD, _ ByVal lpName As VBShellLib.LPSTRRET) Dim pString As Long Dim szText As String Dim szID As String Dim dwFlags As DWORD dwFlags = uFlags And &HFF00 szText = m_ pidlMgr.GetPidlName(pidl) & vbNullChar pString = m_pMalloc.Alloc(LenB(szText)) If (pString) Then CopyMemory ByVal pString, ByVal StrPtr(szText), LenB(szText) End If Dim sret As STRRET sret.uType = STRRET_WSTR sret.pOLESTR = pString CopyMemory ByVal lpName, sret, Len(sret) End Sub
The first interesting thing that happens is that we call PidlMgr::GetPidlName to build the display name of the PIDL. This function merely uses the helper functions we talked about earlier ( GetPidlType , GetPidlLevel , GetPidlLevel , and GetPidlIndex ) to build a string in the following format:
Folder/Item Level-Index.
Next, memory for the string is allocated using IMalloc , and the contents of the string are copied into that location. This makes it "global" to the shell's address space. If we didn't do this, szText would go out of scope as soon as the method terminated .
Finally, we populate the STRRET structure. The first member is set to STRRET_WSTR to inform the shell that the pointer we are giving it is to a wide character string. Then the second member is assigned a string pointer.
There is something very important worth mentioning at this point. The lpName parameter is being used to return information back to the shell, yet the parameter is defined as [in] in the type library. This is done with a purpose in mind. If we had defined this as an [in, out] parameter, we could have coded the last part of this method like so:
Dim sret As STRRET sret.uType = STRRET_WSTR sret.pOLESTR = pString lpName = VarPtr(sret)
This sure looks better than the CopyMemory call, doesn't it? The problem is that the shell now has a pointer to a structure that is about to go out of scope. So the information contained in this structure is literally destroyed moments later. We could kludge this by declaring the structure as a private member to the shell folder class; then it would stay in scope for as long as the class was alive . But if we do this for every structure we need to pass back to the shell, things start to get confusing (as if they aren't confusing enough with us hacking the vtables of all these classes).
By defining this parameter as [in] , VB declares it as ByVal , which forces us to actually copy the contents of the structure to the address specified by lpName . Therefore, every time we have a pointer to a structure, we will always define it as [in] .
11.6.6 CompareIDs
IShellFolder::CompareIDs is called by the shell to give the namespace an opportunity to sort the PIDLs that are about to be displayed. If you look at a directory in Explorer, the Details view contains four columns : Name, Size, Type, and Modified. Explorer sorts the list based on the column you select. This is CompareIDs at work.
When CompareIDs is called, the shell passes us two PIDLs, and our implementation needs to determine which one should be displayed first:
-
If PIDL #1 is displayed first, we return 1.
-
If PIDL #2 is displayed first, we return a number > 0.
-
If the PIDLs are the same, we return 0.
Our implementation is simple. First, we make sure folders are displayed first, then we sort on the level, and finally we sort on the index (see Figure 11.6). Before we jump into the code, here is an important concept to remember. The PIDLs we will be getting here will not always contain one item. The PIDL really does represent a path back to the root of the extension. We will get PIDLs that look like this:
Folder 1-1/Folder 2-2/Folder 3-1/Folder 4-1/Item 1-1
This list contains five ITEMIDLIST s. The PIDL is the pointer to this list. This is an important concept (which is why we're going over it again and again). A PIDL is not one thing. It is a pointer to a list such as this.
What the shell will give us here is a pointer to the first item in the array of ITEMIDLIST sFolder 1-1. The value we want for sorting purposes, however, is Item 1-1. Therefore, CompareIDs will use a method in the PidlMgr class called GetLastItem to get this value. This method is defined in Example 11.23.
Example 11.23. The GetLastItem Method
Public Function GetLastItem(ByVal pidl As LPITEMIDLIST) _ As LPITEMIDLIST Dim pidlLast As LPITEMIDLIST Dim pidlTemp As LPITEMIDLIST pidlTemp = pidl Dim iSize As Integer iSize = GetPidlSize(pidlTemp) Do While (iSize > 0) pidlLast = pidlTemp pidlTemp = GetNextItem(pidlTemp) iSize = GetPidlSize(pidlTemp) Loop GetLastItem = pidlLast End Function
PidlMgr::GetLastItem makes use of a helper function, GetNextItem , which retrieves the next shell identifier in a PIDL. This function is defined in Example 11.24.
Example 11.24. The GetNextItem Method
Public Function GetNextItem(ByVal pidl As LPITEMIDLIST) _ As LPITEMIDLIST GetNextItem = 0 If (pidl > 0) Then Dim iSize As Integer iSize = GetPidlSize(pidl) 'Pointer arithmetic in BASIC (scary, huh?) GetNextItem = pidl + iSize End If End Function
And finally, here we get to CompareIDs. It has been swapped in the vtable because we need to return specialized values. Example 11.25 contains the implementation.
Example 11.25. CompareIDsX
'DemoSpace.bas Public Function CompareIDsX(ByVal this As IShellFolder, _ ByVal lParam As lParam, _ ByVal pidl1 As LPCITEMIDLIST, _ ByVal pidl2 As LPCITEMIDLIST) As Long Dim pidlMgr As pidlMgr Set pidlMgr = New pidlMgr Dim pidlTemp1 As LPITEMIDLIST Dim pidlTemp2 As LPITEMIDLIST Dim pt1 As PIDLTYPE Dim pt2 As PIDLTYPE Dim iLvl1 As Integer Dim iLvl2 As Integer Dim iIndex1 As Integer Dim iIndex2 As Integer 'Default - pidls are equal CompareIDsX = 0 pidlTemp1 = pidlMgr.GetLastItem(pidl1) pidlTemp2 = pidlMgr.GetLastItem(pidl2) pt1 = pidlMgr.GetPidlType(pidlTemp1) pt2 = pidlMgr.GetPidlType(pidlTemp2) If (pt1 <> pt2) Then CompareIDsX = pt1 - pt2 Exit Function End If iLvl1 = pidlMgr.GetPidlLevel(pidlTemp1) iLvl2 = pidlMgr.GetPidlLevel(pidlTemp2) If (iLvl1 <> iLvl2) Then CompareIDsX = iLvl1 - iLvl2 Exit Function End If iIndex1 = pidlMgr.GetPidlIndex(pidlTemp1) iIndex2 = pidlMgr.GetPidlIndex(pidlTemp2) CompareIDsX = iIndex1 - iIndex2 End Function
|
11.6.7 GetUIObjectOf
When the shell requires additional interfaces to carry out tasks for the namespace extension, it calls IShellFolder::GetUIObjectOf . For instance, when the shell needs to display icons for the namespace extension, it calls GetUIObjectOf and asks for an IExtractIcon interface. If the shell wants to display a context menu, it will ask for an IContextMenu interface. In this way, GetUIObjectOf is sort of a specialized QueryInterface. Its syntax is:
HRESULT GetUIObjectOf(HWND hwndOwner, UINT cidl, LPCITEMIDLIST *apidl, REFIID riid, UINT *prgfInOut, LPVOID *ppvOut);
The first parameter, hwndOwner , is the parent window to use if the extension needs to display a message box. This is of no consequence to the VB programmer, because the MsgBox function supplied by Visual Basic does not allow one to specify the parent.
The second parameter, cidl , is the count of the PIDLs in the third parameter, apidl . This method can be coded similarly to IShellFolder::GetAttributesOf to handle more than one PIDL if that functionality is needed. It is usually not, however.
The third parameter, apidl , is a pointer to a GUID that represents the interface the shell is interested in acquiring from us. We will write a function that will take this pointer and convert it into a string that represents this interface. This will allow us to use simple comparisons to figure out which interface the shell is asking for.
The fourth parameter, prgfInOut , is reserved and is not used.
The fifth parameter, ppvOut , is the address that will receive the interface pointer.
To implement this method, we will rely heavily on a function that we will write called GetIID . This function takes a pointer to a GUID and returns a string representation of the GUID. This function is shown Example 11.26.
Example 11.26. GetIID Function
Public Function GetIID(ByVal riid As REFIID) As String Dim aGuid As GUID CopyMemory aGuid, ByVal riid, Len(aGuid) Dim pClsid As Long StringFromCLSID aGuid, pClsid Dim strOut As String * 255 StrFromPtrW pClsid, strOut Dim iid As String GetIID = Left(strOut, InStr(strOut, vbNullChar) - 1) End Function
GetIID makes use of a function called StringFromCLSID , which is found in ole32.dll . The function is declared as follows:
Public Declare Function StringFromCLSID Lib "ole32.dll" _ (pClsid As GUID, lpszProgID As Long) As Long
The function doesn't return an actual string that can be used by VB, but rather a pointer to a string. Therefore, we need a function that will take this pointer and return us a string. This function is called StringFromPointer , and it is shown in Example 11.27.
Example 11.27. StringFromPointer Function
Public Sub StringFromPointer(pOLESTR As Long, strOut As String) Dim b(255) As Byte Dim iTemp As Integer Dim iCount As Integer Dim i As Integer iTemp = 1 'Walk the string and retrieve the first byte of each WORD. While iTemp <> 0 CopyMemory iTemp, ByVal pOLESTR + i, 2 b(iCount) = iTemp iCount = iCount + 1 i = i + 2 Wend 'Copy the byte array to our string. CopyMemory ByVal strOut, b(0), iCount End Sub
Working with a CLSID
There are four helper functions found in ole32.dll that can be very helpful when working with a CLSID. They are declared as follows: Public Declare Function CLSIDFromProgID Lib "ole32.dll" (ByVal lpszProgID As Long, pClsid As GUID) As Long Public Declare Function CLSIDFromString Lib "ole32.dll" (ByVal lpszProgID As Long, pClsid As GUID) As Long Private Declare Function ProgIDFromCLSID Lib "ole32.dll" (pCLSID As GUID, lpszProgID As Long) As Long Public Declare Function StringFromCLSID Lib "ole32.dll" (pClsid As GUID, lpszProgID As Long) As Long These four functions allow you to use CLSIDs in almost any circumstance. The functions all take pointers to strings as parameters, so you will have to use StrPtr to provide these values. For the functions that return pointers to strings, use the StrFromPtrW function shown in this chapter. |
Now we're ready to discuss GetUIObjectOf . The address for this method has been swapped in the vtable for a function called GetUIObjectOf X . This is done because we need to be able to return E_NOINTERFACE if the shell asks us for an interface that we cannot provide. Example 11.28 shows our implementation of GetUIObjectOf X .
Example 11.28. GetUIObjectOfX
'DemoSpace.bas Public Const IID_IExtractIconA = _ "{000214EB-0000-0000-C000-000000000046}" Public Const IID_IExtractIconW = _ "{000214FA-0000-0000-C000-000000000046}" Public Function GetUIObjectOfX(ByVal this As IShellFolder, _ ByVal hwndOwner As hWnd, ByVal cidl As UINT, _ aPidl As LPCITEMIDLIST, _ ByVal riid As REFIID, _ prgfInOut As UINT, ByVal ppvOut As LPVOID) As Long GetUIObjectOfX = E_NOINTERFACE Dim pUnk As IUnknownVB Dim pidl As LPITEMIDLIST Dim clsShellFolder As ShellFolder Dim szIID As String szIID = GetIID(riid)
The first thing we need to do is figure out which interface the shell is asking for by calling GetIID with the riid parameter. Next, we will check to see if the interface is IExtractIconA (Windows 98) or IExtractIconW (NT). All other requests will be ignored:
If (szIID = IID_IExtractIconA) Then Set clsShellFolder = this Dim extIconA As ExtractIcon Set extIconA = New ExtractIcon extIconA.pidl = aPidl Dim iExtIconA As IExtractIconA Set iExtIconA = extIconA Set pUnk = iExtIconA pUnk.AddRef CopyMemory ByVal ppvOut, iExtIconA, 4 GetUIObjectOfX = S_OK ElseIf (szIID = IID_IExtractIconW) Then
Once it has been determined that the shell is asking for IExtractIcon , we can declare an instance of our ExtractIcon class (which implements both IExtractIconA and IExtractIconW ) and pass it the PIDL that was given to us by the shell through the aPidl parameter:
Set clsShellFolder = this Dim extIconW As ExtractIcon Set extIconW = New ExtractIcon 'Pass pidl to IExtractIcon. extIconW.pidl = aPidl
Next, we'll query our ExtractIcon class for the IExtractIcon itself. But before we pass the interface back to the shell, we need to do an AddRef so the interface will still be valid once this method terminates. We'll do that using IUnknownVB . Once this has been accomplished, the interface is copied to the address ppvOut :
Dim iExtIconW As IExtractIconW Set iExtIconW = extIconW Set pUnk = iExtIconW pUnk.AddRef CopyMemory ByVal ppvOut, iExtIconW, 4 GetUIObjectOfX = S_OK End If End Function
If all is well, the method should return S_OK .
11.6.8 ExtractIcon
ExtractIcon is a class we will use to provide the shell with icons for our namespace extension. You should already be familiar with IExtractIcon (see Chapter 5), so we will not discuss these interfaces again. But we will implement the interface a little bit differently than we did in Chapter 5. For one thing, our icons are not located in a file this time around. They will be stored in an image list. Therefore, we will implement Extract to provide the icons from an image list. GetIconLocation will only be used to specify the index of the icon in the image list.
If you recall from Example 11.28, we passed a PIDL to ExtractIcon. This is done through a property in the class called PIDL. This is shown in Example 11.29.
Example 11.29. ExtractIcon
Implements IExtractIconA Implements IExtractIconW Private m_pidl As LPITEMIDLIST Private m_pidlMgr As pidlMgr Private m_pMalloc As IMalloc Private m_hWnd As Long Private Const ILD_TRANSPARENT = 1 Private Sub Class_Initialize( ) Set m_pMalloc = GetMalloc Set m_pidlMgr = New pidlMgr End Sub Private Sub Class_Terminate( ) If (m_pidl > 0) Then m_pidlMgr.Delete m_pidl End If Set m_pidlMgr = Nothing End Sub Public Property Let pidl(ByVal p As LPITEMIDLIST) m_ pidl = m_ pidlMgr.Copy(p) End Property
11.6.8.1 GetIconLocation
GetIconLocation will only provide the index for the icon, not its location. Because of this, the shell will expect Extract to provide the actual location of the icon. Let's take a peek at GetIconLocation , which is shown in Example 11.30. We'll implement this method first, and then we'll tackle Extract . We'll only look at the IExtractIconW functions, but this should not be a problem. If you remember from Chapter 5, the difference between IExtractIconW and IExtractIconA is how the string containing the path to the icon must be handled. We will not be providing the shell a path for the icon location, so both interfaces can be implemented with the same code.
Example 11.30. IExtract::GetIconLocation
Private Sub IExtractIconW_GetIconLocation( _ ByVal uFlags As VBShellLib.UINT, _ ByVal szIconFile As VBShellLib.LPWSTRVB, _ ByVal cchMax As VBShellLib.UINT, _ piIndex As Long, pwFlags As VBShellLib.GETICONLOCATIONRETURN) pwFlags = GIL_NOTFILENAME 'pidl is either a value or a folder Dim pidlTemp As LPITEMIDLIST pidlTemp = m_pidlMgr.GetLastItem(m_pidl) If m_pidlMgr.GetPidlType(pidlTemp) = PT_ITEM Then piIndex = ICON_ITEM Else If (uFlags And GIL_OPENICON) Then piIndex = ICON_OPEN Else piIndex = ICON_CLOSED End If End If End Sub
The first thing we need to do is get the last item in the PIDL. This makes sense if you think of the PIDL as a file path. You want to display an icon for the item at the end of the "path," not the beginning, right? Once we have the last item in the PIDL, we determine its type by calling PidlMgr::GetPidlType . Then we will know whether it is a "folder" or an "item."
If we have a folder, we need to check the uFlags parameter for GIL_OPENICON . This will tell us whether the folder is opened or closed.
The values denoted by ICON_ are merely constants describing the index of a particular icon in the image list that is located on frmView . They are simply:
Public Const ICON_OPEN = 0 Public Const ICON_CLOSED = 1 Public Const ICON_ITEM = 2
11.6.8.2 Extract
Extract is short and sweet. It is shown in Example 11.31.
Example 11.31. IExtractIcon::Extract
Private Sub IExtractIconW_Extract( _ ByVal pszFile As VBShellLib.LPWSTRVB, _ ByVal nIconIndex As VBShellLib.UINT, _ phiconLarge As VBShellLib.HICON, _ phiconSmall As VBShellLib.HICON, _ ByVal nIconSize As VBShellLib.UINT) Dim hImgList As Long hImgList = frmView.ImageList.hImageList phiconSmall = ImageList_GetIcon( _ hImgList, nIconIndex, ILD_TRANSPARENT) phiconLarge = ImageList_GetIcon( _ hImgList, nIconIndex, ILD_TRANSPARENT) End Sub
Since our view will never change, we only need to provide small icons to the shell. Therefore, phiconSmall and phiconLarge can both point to the same icons. The function ImageList_GetIcon , which actually returns the handle to the icons, is located in comctl32.dll and is declared as follows:
Public Declare Function ImageList_GetIcon Lib "comctl32.dll" _ (ByVal himl As Long, ByVal i As Integer, ByVal flags As UINT) _ As HICON
11.6.9 FillList
We need to get back to ShellView and finish it up. We have one function left that handles populating the view object with items, called FillList . Every namespace extension you write will have a FillList function, but as you can probably imagine, the implementation of this function is highly dependent on the format of the PIDL being used.
Here's how the function works. If you remember, we wrote a function called Initialize in ShellView which we used to pass in an instance of ShellFolder and a PIDL to the class. FillList relies on this instance of ShellFolder for calling EnumObjects. It will call EnumObjects, which will return an IEnumIDList interface. Once FillList has the IEnumIDList interface, it calls IEnumIDList::Next repeatedly in a loop until no more PIDLs are returned. As each PIDL is returned, we determine if the PIDL is a folder or an item and populate the list view accordingly . FillList is shown in Example 11.32. Let's go through it slowly.
Example 11.32. FillList
Public Sub FillList( ) Dim pEnumIDList As IEnumIDList Dim pShellFolder As IShellFolder Dim pidl As LPITEMIDLIST Dim dwFetched As DWORD Dim szPidlName As String Dim lIconIndex As Long Dim lv As ListView Set lv = m_frmView.ListView SendMessage lv.hWnd, LVM_DELETEALLITEMS, 0, 0
The first significant thing that happens is that all the items in the list view are cleared. This is done using SendMessage in this example, but it doesn't have to be done this way. You could also call lv.ListItems.Clear . It's just a matter of personal preference:
Dim hr As Long Set pShellFolder = m_parentFolder Set pEnumIDList = pShellFolder.EnumObjects _ (m_frmView.hWnd, SHCONTF_NONFOLDERS)
Then we call IShellFolder::EnumObjects to get an IEnumIDList interface. This call is a little misleading here. It looks like we are asking for only non-folders (items), but this is not the case. If you examine CreateEnumList (Example 11.14), which is the function that builds the list for us, you will see that folders will always be added to the list (this is an implementation decision, not something that has to be done this way). By specifying SHCONTF_NONFOLDERS , we are asking for items in addition to folders:
If (ObjPtr(pEnumIDList) > 0) Then 'Turn off listview redraw SendMessage lv.hWnd, WM_SETREDRAW, 0, 0 pEnumIDList.Next 1, pidl, dwFetched
After we have checked to make sure that our IEnumIDList interface is valid, SendMessage is used to turn off the drawing on the list view. This minimizes flickering when the list view is being populated with items. Once the drawing is off, we call IEnumIDList::Next for the first PIDL. IEnumIDList is interesting in this example, because it is the first (and only) time that we directly use an interface that has been implemented by us:
Do While (dwFetched > 0) Dim li As ListItem Dim pidlTemp As LPITEMIDLIST szPidlName = m_pidlMgr.GetPidlName(pidl) If (m_pidlMgr.GetPidlType(pidl) = PT_FOLDER) Then lIconIndex = 2 ElseIf (m_pidlMgr.GetPidlType(pidl) = PT_ITEM) Then lIconIndex = 3 End If
When a PIDL has been returned to us, we get its display name and type. We could have called IShellFolder::GetDisplayNameOf for the name, but this would have been overkill. GetDisplayNameOf calls PidlMgr::GetPidlName , so we can just get to the point and call it ourselves .
The icon index is determined by the type. An interesting point to make here is that we ourselves are determining which icon should be displayed. Technically, we could call IShellFolder::GetUIObjectOf to get an IExtractIcon interface, and call GetIconLocation for this index value. Our implementation is so simple in this example, though, that it's easier just to do it this way.
Another reason that we do this ourselves is that we would have to write additional code to determine under which version of Windows we are running so we could return the appropriate interface ( IExtactIconA or IExtractIconW ):
Set li = lv.ListItems.Add(, , szPidlName, , lIconIndex) li.Tag = Str(pidl) pEnumIDList.Next 1, pidl, dwFetched Loop
After the item is added to the list, we store the PIDL in the Tag property of the list item. This is done for one reason only. Our example will allow folder browsing from the view object. This means that when a folder item is double-clicked in the list view, navigation will take place. We need this PIDL so that we can determine whether a "folder" or an "item" was selected.
This happens repeatedly until there are no more PIDLs:
'Turn on listview redraw SendMessage lv.hWnd, WM_SETREDRAW, 1, 0 End If Set pEnumIDList = Nothing End Sub
After the list view has been filled, drawing is turned back on, and our IEnumIDList interface is released.
11.6.10 Finishing the View Object
We have one more detail left on the view object. We need to allow for the browsing of folders. When a folder is double-clicked in the list view, we should navigate to that folder. Fortunately for us, this is a simple process. Everything is handled by IShellBrowser . To add this functionality, we add code for the DblClick event in list view. This code is shown in Example 11.33.
Example 11.33. ListView_DblClick
Private Sub ListView_DblClick( ) Dim pidl As LPITEMIDLIST Dim li As ListItem Set li = ListView.SelectedItem pidl = CLng(li.Tag) If (m_pidlMgr.GetPidlType(pidl) = PT_FOLDER) Then m_pShellBrowser.BrowseObject pidl, SBSP_DEFBROWSER Or _ SBSP_RELATIVE End If Set li = Nothing End Sub
The PIDL is extracted from the Tag property of the list item that was double-clicked and converted back to a Long. Its type is then determined. If the list item is a folder, IShellBrowser::BrowseObject is called. This has the same effect as if we were to double-click on the item in the tree view ourselves.
11.6.11 BindToObject
We have one more method left in IShellFolder that we need to implement. This method, BindToObject , is called when a folder is opened in the tree view. Its syntax is as follows:
HRESULT BindToObject(LPCITEMIDLIST pidl, LPBC pbcReserved, REFIID riid,LPVOID *ppvOut);
BindToObject acts similarly to QueryInterface. The shell will ask for an interface via the riid parameter, and it will be our job to provide the interface through ppvOut . In fact, we'll just forward this request to IUnknownVB::QueryInterface . Under Windows 9x and Windows NT, the shell will always ask for IShellFolder . But not so for Windows 2000. In the event that the shell asks for another interface, we need to be prepared to return E_OUTOFMEMORY which is why this method has been swapped in the vtable with a replacement function. The remaining code for the ShellFolder class in shown in Example 11.34.
Example 11.34. IShellFolder::BindToObject
'Demospace.bas Public Const IID_IShellFolder = _ "{000214E6-0000-0000-C000-000000000046}" Public Function BindToObjectX(ByVal this As IShellFolder, _ ByVal pidl As LPCITEMIDLIST, _ ByVal pbcReserved As LPBC, _ ByVal riid As REFIID, _ ppvOut As LPVOID) As Long BindToObjectX = E_OUTOFMEMORY Dim iid As String iid = GetIID(riid) If iid = IID_IShellFolder Then 'Current shell folder. Dim pParentShellFolder As ShellFolder Set pParentShellFolder = this 'New child shell folder. Dim pShellFolder As ShellFolder Set pShellFolder = New ShellFolder 'Pass current pidl and folder reference. 'to child folder Set pShellFolder.Parent = pParentShellFolder pShellFolder.pidl = pidl 'Give new shell folder back to the shell. Dim pUnk As IUnknownVB Set pUnk = pShellFolder pUnk.QueryInterface riid, ppvOut Set pUnk = Nothing Set pShellFolder = Nothing Set pParentShellFolder = Nothing BindToObjectX = S_OK End If End Function 'ShellFolder.cls Public Property Set Parent(p As ShellFolder) Set m_parentFolder = p End Property Public Property Let pidl(ByVal p As LPITEMIDLIST) m_pidl = m_pidlMgr.Copy(p) Dim pidlTemp As LPITEMIDLIST pidlTemp = m_pidlMgr.GetLastItem(m_pidl) m_iLevel = m_pidlMgr.GetPidlLevel(pidlTemp) + 1 End Property
|
Basically, opening another folder repeats the entire process we have just walked through. When BindToObject is called, we create an instance of ShellFolder ourselves, passing it a reference to the current ShellFolder and the current PIDL. The IShellFolder interface is then given back to the shell, and the process begins again.
only for RuBoard - do not distribute or recompile |