|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Contents
IntroductionNamespace Extension (abbr. NSE) is actually a COM server which implements some required shell interfaces. Explorer uses those COM interfaces to let the user access and manipulate internal namespace data and display it in a graphical form. How to develop an NSE is a big topic, in this article, I will focus on how to implement subfolders in NSE. After struggling with NSE for about half a year, I finally found my way out. The available articles on NSE are so poor that I had to read the source code line by line to find solutions. I learned ATL myself by reading the MSDN's sample project RegView (it was written in pure C++). Thanks to Bjarke Viksoe, who contributed the source code of AdfView which teaches a lot of important skills to deal with NSE. Here, I want to share some tips in developing NSE. I will focus on how to solve problems that you may encounter and things you will be interested to know while writing your own NSE. Because writing an article to cover all these aspects is almost impossible (God knows how long such an article will be), I plan for a series of articles which will cover implementation of subfolder, delete and create folder, drag and drop. This article assumes you know C++, ATL, COM and are familiar with basic knowledge of NSE. Before reading this article, read "The Complete Idiot's Guide to Writing Namespace Extensions - Part I" by Michael Dunn, as the terms used in it are used in this article as well. In this article, the emphasis is on implementing subfolder. The NSE implemented in the provided sample project does the simple work: read the data which describes the structure of the NSE's root folder (with subfolders) from a configuration file and make the root folder act in Explorer like a real folder that has subfolders. Of course, you know this folder is not a part of the file system, it's a virtual folder. The sample project is created using ATL COM AppWizard. Manage NSE's dataConfiguration fileFollowing is the content of our sample NSE's configuration file, the file's format refers to initialization file's format: [ROOT]
dir=src;doc
[src]
dir=VB;VC
file=note1.txt;note2.txt
[src\VB]
file=vb.vbp
[src\VC]
file=test.cpp
Notes:
All the folders and files listed out in this file will be created into the corresponding objects in our sample NSE as the above image shows. CComModule derived classIf you are using the ATL COM AppWizard to create your NSE, the wizard will automatically generate Data members that manage global resourcesCShellMalloc m_Allocator; //class instance to manage // Shell's IMalloc interface // pointer CShellImageLists m_ImageLists; //class instance to manage NSE // used image/icon resources LPITEMIDLIST m_pidlNSFROOT; //the PIDL uniquely specifies our // NSE's root folder all the // across the entire namespace TCHAR m_szInstallPath[MAX_PATH]; //the absolute path of NSE Dll, // use to locate the cfg file If you have defined your PIDL management classEach namespace object in our NSE is uniquely identified by its PIDL- a pointer to its item ID (which is actually a The PIDLs covered in this article are mostly relative PIDLs, divided into two sub-types:
Our PIDL dataThe data that will be stored in PIDL is totally determined by the NSE's implementer. For our NSE's purpose, to simply creating a folder which holds objects including both files and subfolders, a flag to distinguish folder or file and a relative name to his parent folder is enough. The following typedef enum tagITEM_TYPE { NWS_FOLDER = 0x00000001, NWS_FILE = 0x00000002, }ITEM_TYPE; typedef struct tagPIDLDATA { ITEM_TYPE type; //used to mark folder or file TCHAR szName[1]; //store the object's name // (relative to its parent folder) }PIDLDATA, FAR *LPPIDLDATA; Because implementing our NSE involves in a lot of PIDL operations such as: create, delete, concatenate, copy and also needs some methods to fetch info from PIDL, a PIDL management class to do all these things is needed. class CNWSPidlMgr { public: LPITEMIDLIST Create(ITEM_TYPE iItemType,LPTSTR szName); LPITEMIDLIST GetNextItem(LPCITEMIDLIST pidl); LPITEMIDLIST Concatenate(LPCITEMIDLIST, LPCITEMIDLIST); //get current pidl item's name HRESULT GetName(LPCITEMIDLIST,LPTSTR); //concat the name of the pidl(from the //pointer to the list end) together HRESULT GetFullName(LPCITEMIDLIST pidl, LPTSTR szFullName,DWORD *pdwLen); ITEM_TYPE GetItemType(LPCITEMIDLIST pidl); //Decide whether current folder has subfolder BOOL HasSubFolder(LPTSTR pszPath); .... //Other public methods private: LPPIDLDATA GetDataPointer(LPCITEMIDLIST); }; I will cover some important methods to support subfolders, for others please refer to my sample project code and its comments. Create a new PIDLAs previously described, each Item ID's value in namespace consists of two parts: item type and name. Here we have created a simple PIDL. LPITEMIDLIST CNWSPidlMgr::Create(ITEM_TYPE iItemType,
LPTSTR pszName)
{
USHORT TotalSize = sizeof(ITEMIDLIST) +
sizeof(ITEM_TYPE) +
(_tcslen(pszName)+1)*sizeof(TCHAR);
// Also allocate memory for the final null SHITEMID.
LPITEMIDLIST pidlNew =
(LPITEMIDLIST) _Module.m_Allocator.Alloc(
TotalSize + sizeof(ITEMIDLIST));
if (pidlNew)
{
::ZeroMemory(pidlNew,TotalSize + sizeof(ITEMIDLIST));
LPITEMIDLIST pidlTemp = pidlNew;
// Prepares the PIDL to be filled with actual data
pidlTemp->mkid.cb = (USHORT)TotalSize;
LPPIDLDATA pData;
pData = GetDataPointer(pidlTemp); //viz. pidlTemp->mkid.abID
// Fill the PIDL
pData->type = iItemType;
::CopyMemory(pData->szName, pszName,
(_tcslen(pszName)+1) * sizeof(TCHAR));
// Set an empty PIDL at the end and
// set the NULL terminator to 0
pidlTemp = GetNextItem(pidlTemp);
pidlTemp->mkid.cb = 0;
pidlTemp->mkid.abID[0] = 0;
}
return pidlNew;
}
Get PIDL's typeThis function is very useful for our NSE to realize subfolders. In the other part of this article - Retrieve the object's attribute in the folder, this method is called to determine the type PIDL represents, folder or file, before returning to Explorer. ITEM_TYPE CNWSPidlMgr::GetItemType(LPCITEMIDLIST pidl)
{
LPITEMIDLIST pidlTemp = GetLastItem(pidl); //it is the last item
//determine the pidl
//represent what type
LPPIDLDATA pData = GetDataPointer(pidlTemp);
return pData->type;
}
Has subfolders?If the current folder has subfolder, NSE should tell the Explorer ( BOOL CNWSPidlMgr::HasSubFolder(LPCITEMIDLIST pidl)
{
TCHAR szPath[MAX_PATH]=_TEXT("");
TCHAR tmpStr[MAX_PATH]=_TEXT("");
TCHAR szCfgFile[MAX_PATH]=_TEXT("");
DWORD dwLen=MAX_PATH;
GetFullName(pidl,szPath,&dwLen);
if( dwLen>0)
{
dwLen = MAX_PATH;
_tcscpy(szCfgFile,g_szInstallPath);
_tcscat(szCfgFile,_T("\\NSExtWithSubFld.cfg"));
GetPrivateProfileString( szPath,
_T("dir"),_T("NotFound"),tmpStr,
dwLen, szCfgFile);
if( (_tcscmp(tmpStr,_T("NotFound"))==0 ) ||
(_tcslen(tmpStr)==0 ) )
return FALSE;
else
return TRUE;
}
}
Concatenate PIDLsIf NSE has multi-level folder, sometimes you need to combine an object's simple PIDL with either its parent folder's complex PIDL to get a qualified PIDL relative to the NSE's root or its parent folder's full PIDL to get a full PIDL relative to the desktop folder. LPITEMIDLIST CNWSPidlMgr::Concatenate(
LPCITEMIDLIST pidl1, LPCITEMIDLIST pidl2)
{
LPITEMIDLIST pidlNew;
UINT cb1 = 0, cb2 = 0;
//are both of these NULL?
if(!pidl1 && !pidl2)
return NULL;
//if pidl1 is NULL, just return a copy of pidl2
if(!pidl1)
{
pidlNew = Copy(pidl2);
return pidlNew;
}
//if pidl2 is NULL, just return a copy of pidl1
if(!pidl2)
{
pidlNew = Copy(pidl1);
return pidlNew;
}
cb1 = GetByteSize(pidl1) - sizeof(ITEMIDLIST);
cb2 = GetByteSize(pidl2);
//create the new PIDL
pidlNew =
(LPITEMIDLIST)_Module.m_Allocator.Alloc(cb1 + cb2);
if(pidlNew)
{
::ZeroMemory(pidlNew,cb1+cb2);
//copy the first PIDL
::CopyMemory(pidlNew, pidl1, cb1);
//copy the second PIDL
::CopyMemory(((LPBYTE)pidlNew) + cb1, pidl2, cb2);
}
return pidlNew;
}
Interfaces interact with ExplorerIEnumIDListTo implement subfoldering NSE, you must provide an enumerator for the subfolders, which are responsible for enumerating the contents for all NSE's folders, from top (NSE's root folder) to bottom (the NSE's deepest subfolders). When a folder's In NSE, common uses of
In our NSE, to create an enumerator for a folder to enumerate its contents, we refer to the configuration file and find the corresponding section for this folder, and create PIDL (which will be the item of enum list) for each subfolder and file object according to its "dir" and "file" key values and add them to an Sometimes Self-defined structure and data memberstypedef struct tagENUMLIST { struct tagENUMLIST *pNext; LPITEMIDLIST pidl; }ENUMLIST, FAR *LPENUMLIST; LPENUMLIST m_pFirst; //pointer to the head // of the enum list LPENUMLIST m_pLast; //pointer to the tail // of the enum list LPENUMLIST m_pCurrent; //pointer to the current // pidl in the enum list CNWSPidlMgr m_PidlMgr; //class instance use to // handle pidl operations Member functions//member function of IEnumIDList interface, //standard method STDMETHOD (Next) (DWORD, LPITEMIDLIST*, LPDWORD); STDMETHOD (Skip) (DWORD); STDMETHOD (Reset) (void); STDMETHOD (Clone) (LPENUMIDLIST*); //Self-defined functions BOOL DeleteList(void); //called when the // instance be deleted BOOL AddToEnumList(LPITEMIDLIST pidl); //add a pidl // to the enum list //called when init the enumlist HRESULT _Init(LPCITEMIDLIST pidlRoot,DWORD dwFlags); HRESULT _AddPidls(ITEM_TYPE iItemType, LPTSTR pszPath); Create the enum listIn initialize the HRESULT CPidlEnum::_Init(LPCITEMIDLIST pidlRoot,DWORD dwFlags)
{
TCHAR tmpPath[MAX_PATH]=_TEXT("");
DWORD dwLen=MAX_PATH;
if((NULL==pidlRoot)||(0 == pidlRoot->mkid.cb))//Current folder
//is NSE's root
_tcscpy(tmpPath,_T("ROOT"));
else //Current folder is NSE's subfolder
{
//Fetch pidlRoot correspond path name
//relative to NSE's root
m_PidlMgr.GetFullName(pidlRoot,tmpPath,&dwLen);
}
if(dwFlags & SHCONTF_FOLDERS)//add subfolders objects
_AddPidls(NWS_FOLDER,tmpPath);
if(dwFlags & SHCONTF_NONFOLDERS)//add files objects
_AddPidls(NWS_FILE,tmpPath);
Reset();
return S_OK;
}
HRESULT CPidlEnum::_AddPidls(ITEM_TYPE iItemType,
LPTSTR pszPath)
{
TCHAR tmpStr[MAX_PATH]=_TEXT("");
TCHAR tmpName[MAX_PATH]=_TEXT("");
TCHAR szCfgFile[MAX_PATH]=_TEXT("");
DWORD dwLen=MAX_PATH;
LPITEMIDLIST pidl=NULL;
_tcscpy(szCfgFile,g_szInstallPath);
_tcscat(szCfgFile,T("\\NSExtWithSubFld.cfg"));
if( iItemType == NWS_FOLDER )
GetPrivateProfileString( pszPath,_T("dir"),
_T(""),tmpStr,dwLen, szCfgFile);
else if( iItemType == NWS_FILE )
GetPrivateProfileString( pszPath,_T("file"),
_T(""),tmpStr,dwLen, szCfgFile);
if( _tcslen(tmpStr)==0 )
return S_OK;
TCHAR *pChr,*pszHandle;
pszHandle=tmpStr;
pChr=pszHandle;
while( ( pChr = _tcschr(pszHandle,_T(';') ) )!=NULL)
{
_tcsnset(tmpName,0,MAX_PATH);
_tcsncpy(tmpName,pszHandle,pChr-pszHandle);
//create relative pidl
pidl=m_PidlMgr.Create(iItemType,tmpName);
if(pidl)
{
if(!AddToEnumList(pidl))
return E_FAIL;
}
else
return E_FAIL;
if(pszHandle[0] == _T('\0'))
break;
pszHandle = pChr+1;
}
_tcsnset(tmpName,0,MAX_PATH);
_tcscpy(tmpName,pszHandle);
if(_tcslen(tmpName)==0)
return E_FAIL;
//create pidl for the last item
pidl=m_PidlMgr.Create(iItemType,tmpName);
if(pidl)
{
if(!AddToEnumList(pidl))
return E_FAIL;
}
else
return E_FAIL;
return S_OK;
}
BOOL CPidlEnum::AddToEnumList(LPITEMIDLIST pidl)
{
LPENUMLIST pNew =
(LPENUMLIST)_Module.m_Allocator.Alloc(sizeof(ENUMLIST));
if(pNew)
{
//set the next pointer
pNew->pNext = NULL;
pNew->pidl = pidl;
//is this the first item in the list?
if(!m_pFirst)
{
m_pFirst = pNew;
m_pCurrent = m_pFirst;
}
if(m_pLast)
m_pLast->pNext = pNew; //add the new item
//to the end of the list
//update the last item pointer
m_pLast = pNew;
return TRUE;
}
return FALSE;
}
Deal with NSE's folderAs you known, NSE is actually a COM server which implements some required Shell interface. The Explorer works like a COM client to cooperate with NSE. Following are the initial steps performed by the Explorer when you click our NSE root folder's (has subfolders) icon in Tree view:
As you click the subfoldered entries under our NSE’s root folder, a new instance of the folder object for each subfolder will be created in our DLL, by calling our IPersistFolderFolder initializationThe full PIDL of our NSE's root folder which is mentioned above must be saved if we want to provide more complex UI functions such as delete or create a new folder in our NSE and want our NSE to act correctly both in Tree view and Shell View. When we implement our NSE, we use Because the Explorer uses IShellFolderThis interface is used to manage folders in our NSE. It is a required interface when implementing a NSE. OLE interfaces that can be used to carry out actions on the specified file objects or folders (such as drag and drop) implemented by our NSE will be retrieved through this interface by calling the function Self-defined data membersCNWSPidlMgr m_PidlMgr; //class instance use // to handle pidl operations LPITEMIDLIST m_pidlPath; //store the complex PIDL // of current folder object Subfolder related member functionsThe subfolder related methods in this interface include:
Bind to the subfolderSTDMETHODIMP CMyVirtualFolder::BindToObject(LPCITEMIDLIST pidl,
LPBC pbcReserved,
REFIID riid,
LPVOID *ppRetVal)
{
*ppRetVal = NULL;
HRESULT hr = S_OK;
if( riid != IID_IShellFolder)
return E_NOINTERFACE;
CComObject<CMyVirtualFolder> *pVFObj=0;
hr = CComObject<CMyVirtualFolder>::CreateInstance(&pVFObj);
if(FAILED(hr))
return hr;
pVFObj->AddRef();
//cause relative pidl doesn't include path info,
//so use Concatenate to make a absolute pidl
LPITEMIDLIST pidlNew = m_PidlMgr.Concatenate(m_pidlPath,pidl);
pVFObj->m_pidlPath = m_PidlMgr.Copy(pidlNew);
m_PidlMgr.Delete(pidlNew);
hr = pVFObj->QueryInterface(riid, ppRetVal);
ATLASSERT(pVFObj);
if(pVFObj == NULL)
{
MessageBox(NULL,
_T("CMyVirtualFolder::BindToObject() pVFObj=NULL"),
_T("NSF"),MB_OK);
return E_FAIL;
}
pVFObj->Release();
return hr;
}
Get the folder's enumerate interfaceIf you have no idea about STDMETHODIMP CMyVirtualFolder::EnumObjects(HWND hWnd,
DWORD grfFlags,
LPENUMIDLIST* ppEnumIDList)
{
HRESULT Hr;
CComObject<CPidlEnum> *pEnum;
Hr = CComObject<CPidlEnum>::CreateInstance(&pEnum);
if (SUCCEEDED(Hr))
{
// AddRef() the object while we're using it.
pEnum->AddRef();
// Init the enumerator. create all the items in
// this folder and add it to the enumerator's list.
Hr = pEnum->_Init(m_pidlPath, grfFlags); //grfFlags will passed in
//as the initialization
//condition
// Return an IEnumIDList interface to the caller.
if (SUCCEEDED(Hr))
Hr = pEnum->QueryInterface(IID_IEnumIDList,
(void**)ppEnumIDList);
pEnum->Release();
}
return Hr;
}
Retrieve the object's attribute in the folderFolder objects should return attributes including The Explorer will call this function to decide how to show objects of the current folder in Tree view (If it is a folder object, show it in Tree view. If the folder object has subfolders, add a '+' sign beside its folder icon in Tree view). STDMETHODIMP CMyVirtualFolder::GetAttributesOf(UINT uCount,
LPCITEMIDLIST pidls[],
LPDWORD pdwAttribs)
{
HRESULT Hr;
*pdwAttribs = (DWORD)-1;
if(( uCount==0 )||(pidls[0]->mkid.cb == 0))
{
// Can happen on Win95. Queries the view folder.
DWORD dwAttr;
dwAttr |= SFGAO_FOLDER | SFGAO_HASSUBFOLDER
|SFGAO_HASPROPSHEET |SFGAO_DROPTARGET;
*pdwAttribs &= dwAttr;
}
else
{
TCHAR szText[MAX_PATH]=_TEXT("");
TCHAR szTmp[MAX_PATH]=_TEXT("");
DWORD dwLen=MAX_PATH;
for( UINT i=0; i<uCount; i++ )
{
DWORD dwAttr = 0;
switch( m_PidlMgr.GetItemType(pidls[i]) )
{
case NWS_FOLDER:
dwAttr |=SFGAO_FOLDER;
//get the full pidl
LPITEMIDLIST tmpPidl;
tmpPidl = m_PidlMgr.Concatenate(m_pidlPath, pidls[i]);
_tcsnset(szText,0,MAX_PATH);
_tcsnset(szTmp,0,MAX_PATH);
m_PidlMgr.GetFullName(tmpPidl,szTmp,&dwLen);
if( dwLen >0 )
_tcscat(szText,szTmp);
m_PidlMgr.Delete(tmpPidl);
//if current folder include subfolder, in
//Explorer's left treeView panel this folder
//has + can to expand
if( TRUE == m_PidlMgr.HasSubFolder(szText) )
dwAttr |= SFGAO_HASSUBFOLDER;
break;
}
*pdwAttribs &= dwAttr;
}
}
return S_OK;
}
IShellViewOur implementation of view object is to create a simple ListView window and handle a part of our user interface messages which helps to realize subfoldering (has a window procedure to handle messages). In fact, when you select a folder in system namespace, then the Explorer’s internal view object will be responsible for creating and managing a ListView pane. To make our NSE support subfolder, that is, when the user double clicks a folder in our ListView, this subfolder should be open, and in the Explorer Tree view, the clicked subfolder should be focused and expanded. To realize this, we should do the following:
For simplicity, we do nothing when the user double clicks the file object in our ListView. Self-defined main data memberspublic: HWND m_hWnd; //handle of right panel //window (Shell view window) HWND m_hwndList; //handle of listview //keep the IShellBrowser interface pointer to //allow communication with the Windows Explorer //window. LPSHELLBROWSER m_pShellBrowser; protected: CMyVirtualFolder *m_pFolder; //pointer to current // folder object LPITEMIDLIST m_pidlRoot; //complex PIDL of // current folder CNWSPidlMgr m_PidlMgr; Fill the ListViewWe fill the ListView while handling HRESULT CNSFShellView::_FillListView()
{
DWORD dwFlags = SHCONTF_NONFOLDERS | SHCONTF_FOLDERS;
LPENUMIDLIST pEnumIDList;
HRESULT Hr;
HR(m_pFolder->EnumObjects(m_hWnd,
dwFlags, &pEnumIDList));
{
// Turn the listview's redrawing off
::SendMessage(m_hwndList, WM_SETREDRAW, FALSE, 0L);
DWORD dwFetched = 0;
LPITEMIDLIST pidl = NULL;
while((S_OK == pEnumIDList->Next(1, &pidl, &dwFetched))
&& (dwFetched!=0))
{
LV_ITEM lvi = { 0 };
//set the mask
lvi.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM;
//item will be add to the end of the list
lvi.iItem = ListView_GetItemCount(m_hwndList);
//set the object's simple PIDL to lvi.lParam
lvi.lParam = (LPARAM)m_PidlMgr.Copy(pidl);
//set icon
if( m_PidlMgr.GetItemType(pidl) == NWS_FOLDER)
lvi.iImage = ICON_INDEX_FOLDER;
else
lvi.iImage = ICON_INDEX_FILE;
//Column 0:Name
TCHAR szName[MAX_PATH]=TEXT("");
m_PidlMgr.GetName(pidl,szName);
lvi.pszText = szName;
ListView_InsertItem(m_hwndList, &lvi);
//Set Column 1:type text
TCHAR szTemp[MAX_PATH]=TEXT("");
HR(m_PidlMgr.GetItemAttributes(pidl,ATTR_TYPE,szTemp));
ListView_SetItemText(m_hwndList,lvi.iItem,
COL_TYPE,szTemp);
}
pEnumIDList->Release();
CListSortInfo sort = { m_pFolder, COL_NAME, TRUE };
ListView_SortItems(m_hwndList, ListViewSortFuncCB,
(LPARAM) &sort );
// Turn the listview's redrawing back
// on and force it to draw
::SendMessage(m_hwndList, WM_SETREDRAW, TRUE, 0L);
::InvalidateRect(m_hwndList, NULL, TRUE);
::UpdateWindow(m_hwndList);
}
return S_OK;
}
Handle LVN_ITEMACTIVATE notifyFirst, you must declare a handler function for BEGIN_MSG_MAP(CNSFShellView)
...... //declare other msg handler functions
NOTIFY_CODE_HANDLER(LVN_ITEMACTIVATE, OnItemActivated)
...... //declare other msg handler functions
END_MSG_MAP()
Following is what we do to browse into a folder: LRESULT CNSFShellView::OnItemActivated(UINT CtlID,
LPNMHDR lpnmh, BOOL& bHandled)
{
LV_ITEM lvItem;
ZeroMemory(&lvItem, sizeof(lvItem));
lvItem.mask = LVIF_PARAM;
LPNMLISTVIEW lpnmlv = (LPNMLISTVIEW)lpnmh;
lvItem.iItem = lpnmlv->iItem;
if(ListView_GetItem(m_hwndList, &lvItem))
{
//folders to be activated
if(NWS_FOLDER ==
m_PidlMgr.GetItemType((LPITEMIDLIST)lvItem.lParam))
{
//Tells Windows Explorer to browse to another folder
m_pShellBrowser->BrowseObject(
(LPITEMIDLIST)lvItem.lParam,
SBSP_DEFBROWSER | SBSP_RELATIVE);
}
}
return 0;
}
Save the IShellBrowser pointer passed from shellSTDMETHODIMP CNSFShellView::CreateViewWindow(
LPSHELLVIEW lpPrevView,
LPCFOLDERSETTINGS lpFS,
LPSHELLBROWSER pSB,
LPRECT prcView,
HWND* phWnd)
{
......
m_pShellBrowser = pSB;
......
}
How to debugTo debug your extension, you need to execute the Shell from the debugger. Follow these steps:
ConclusionWriting on NSE is still a complex job. The aim of this article is to help you develop a NSE with subfolders. In my next article, I will to tell you how to create or delete a subfolder in your own NSE and make Explorer's Tree view and Shell view reflect these changes in a synchronized way. | ||||||||||||||||||||