Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

The Complete Idiot's Guide to Writing Shell Extensions - Part V

0.00/5 (No votes)
24 May 2006 5  
A tutorial on writing a shell extension that adds pages to the properties dialog of files.

Contents

Introduction

Here in Part V of the Guide, we'll venture into the world of property sheets. When you bring up the properties for a file system object, Explorer shows a property sheet with a tab labeled General. The shell lets us add pages to the property sheet by using a type of shell extension called a property sheet handler.

This article assumes that you understand the basics of shell extensions, and are familiar with the STL collection classes. If you need a refresher on STL, you should read Part II, since the same techniques will be used in this article.

Remember that VC 7 (and probably VC 8) users will need to change some settings before compiling. See the README section in Part I for the details.

Everyone is familiar with Explorer's properties dialogs. More specifically, they are property sheets that contain one or more pages. Each property sheet has a General tab that lists the full path, modified date, and other various stuff. Explorer lets us add our own pages to the property sheets, using a property sheet handler extension. A property sheet handler can also add or replace pages in certain Control Panel applets, but that topic will not be covered here. See my article Adding Custom Pages to Control Panel Applets to learn more about extending applets.

This article presents an extension that lets you modify the created, accessed, and modified times for a file right from its properties dialog. I will do all property page handling in straight SDK calls, without MFC or ATL. I haven't tried using an MFC or WTL property page object in an extension; doing so may be tricky because the shell expects to receive a handle to the sheet (an HPROPSHEETPAGE), and MFC hides this detail in the CPropertyPage implementation.

If you bring up the properties for a .URL file (an Internet shortcut), you can see property sheet handlers in action. The CodeProject tab is a sneak peek at this article's extension. The Web Document tab shows an extension installed by IE.

 [Built-in prop sheet handler - 21K ]

The Initialization Interface

You should be familiar with the set-up steps now, so I'll skip the instructions for going through the VC wizards. If you're following along in the wizards, make a new ATL COM app called FileTime, with a C++ implementation class CFileTimeShlExt.

Since a property sheet handler operates on all selected files at once, it uses IShellExtInit as its initialization interface. We'll need to add IShellExtInit to the list of interfaces that CFileTimeShlExt implements. Again, this should be familiar to you, so I will not repeat the steps here.

The class will also need a list of strings to hold the names of the selected files.

typedef list< basic_string<TCHAR> > string_list;
 
protected:
  string_list m_lsFiles;

The Initialize() method will do the same thing as Part II - read in the names of the selected file and store them in the string list. Here's the beginning of the function:

STDMETHODIMP CFileTimeShlExt::Initialize (
  LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj,
  HKEY hProgID )
{
TCHAR     szFile[MAX_PATH];
UINT      uNumFiles;
HDROP     hdrop;
FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg;
INITCOMMONCONTROLSEX iccex = { sizeof(INITCOMMONCONTROLSEX), ICC_DATE_CLASSES };
 
  // Init the common controls.

  InitCommonControlsEx ( &iccex );

We initialize the common controls because our page will use the date/time picker (DTP) control. Next we do all the mucking about with the IDataObject interface and get an HDROP handle for enumerating the selected files.

  // Read the list of items from the data object.  They're stored in HDROP

  // form, so just get the HDROP handle and then use the drag 'n' drop APIs

  // on it.

  if ( FAILED( pDataObj->GetData ( &etc, &stg ) ))
    return E_INVALIDARG;
 
  // Get an HDROP handle.

  hdrop = (HDROP) GlobalLock ( stg.hGlobal );
 
  if ( NULL == hdrop )
    {
    ReleaseStgMedium ( &stg );
    return E_INVALIDARG;
    }
 
  // Determine how many files are involved in this operation.

  uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );

Next comes the loop that actually enumerates through the selected files. This extension will only operate on files, not directories, so any directories we come across are ignored.

  for ( UINT uFile = 0; uFile < uNumFiles; uFile++ )
    {
    // Get the next filename.

    if ( 0 == DragQueryFile ( hdrop, uFile, szFile, MAX_PATH ) )
      continue;
 
    // Skip over directories.  We *could* handle directories, since they

    // keep the creation time/date, but I'm just choosing not to do so

    // in this example.

    if ( PathIsDirectory ( szFile ) )
      continue;
 
    // Add the filename to our list of files to act on.

    m_lsFiles.push_back ( szFile );
    }   // end for

 
  // Release resources.

  GlobalUnlock ( stg.hGlobal );
  ReleaseStgMedium ( &stg );

The code that enumerates the filenames is the same as before, but there's also something new here. A property sheet has a limit on the number of pages it can have, defined as the constant MAXPROPPAGES in prsht.h. Each file will get its own page, so if our list has more than MAXPROPPAGES files, it gets truncated so its size is MAXPROPPAGES. (Even though MAXPROPPAGES is currently 100, the property sheet will not display that many tabs. It maxes out at around 34.)

  // Check how many files were selected.  If the number is greater than the

  // maximum number of property pages, truncate our list.

  if ( m_lsFiles.size() > MAXPROPPAGES )
    m_lsFiles.resize ( MAXPROPPAGES );
 
  // If we found any files we can work with, return S_OK.  Otherwise,

  // return E_FAIL so we don't get called again for this right-click

  // operation.

  return (m_lsFiles.size() > 0) ? S_OK : E_FAIL;
}

Adding Property Pages

If Initialize() returns S_OK, Explorer queries for a new interface, IShellPropSheetExt. IShellPropSheetExt is quite simple, with only one method that requires an implementation. To add IShellPropSheetExt to our class, open FileTimeShlExt.h and add the lines listed here in bold:

class CFileTimeShlExt :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CFileTimeShlExt, &CLSID_FileTimeShlExt>,
  public IShellExtInit,
  public IShellPropSheetExt
{
  BEGIN_COM_MAP(CFileTimeShlExt)
    COM_INTERFACE_ENTRY(IShellExtInit)
    COM_INTERFACE_ENTRY(IShellPropSheetExt)
  END_COM_MAP()
 
public:
  // IShellPropSheetExt

  STDMETHODIMP AddPages(LPFNADDPROPSHEETPAGE, LPARAM);
  STDMETHODIMP ReplacePage(UINT, LPFNADDPROPSHEETPAGE, LPARAM)
      { return E_NOTIMPL; }

The AddPages() method is the one we'll implement. ReplacePage() is only used by extensions that replace pages in Control Panel applets, so we do not need to implement it here. Explorer calls our AddPages() function to let us add pages to the property sheet that Explorer sets up.

The parameters to AddPages() are a function pointer and an LPARAM, both of which are used only by the shell. lpfnAddPageProc points to a function inside the shell that we call to actually add the pages. lParam is some mysterious value that's important to the shell. We don't mess with it, we just pass it right back to the lpfnAddPageProc function.

STDMETHODIMP CFileTimeShlExt::AddPages (
  LPFNADDPROPSHEETPAGE lpfnAddPageProc,
  LPARAM lParam )
{
PROPSHEETPAGE  psp;
HPROPSHEETPAGE hPage;
TCHAR          szPageTitle [MAX_PATH];
string_list::const_iterator it, itEnd;
                                   
  for ( it = m_lsFiles.begin(), itEnd = m_lsFiles.end();
        it != itEnd; it++ )
    {
    // 'it' points at the next filename. Allocate a new copy of the string

    // that the page will own.

    LPCTSTR szFile = _tcsdup ( it->c_str() );

The first thing we do is make a copy of the filename. The reason for this is explained below.

The next step is to create a string to go in our page's tab. The string will be the filename, without the extension. Additionally, the string will be truncated if it's longer than 24 characters. This is totally arbitrary; I chose 24 because it looked good to me. There should be some limit, to prevent the name from running off the end of the tab.

    // Strip the path and extension from the filename - this will be the

    // page title.  The name is truncated at 24 chars so it fits on the tab.

    lstrcpyn ( szPageTitle, it->c_str(), MAX_PATH );
    PathStripPath ( szPageTitle );
    PathRemoveExtension ( szPageTitle );
    szPageTitle[24] = '\0';

Since we're using straight SDK calls to do the property page, we'll have to get our hands dirty with a PROPSHEETPAGE struct. Here's the setup for the struct:

    psp.dwSize      = sizeof(PROPSHEETPAGE);
    psp.dwFlags     = PSP_USEREFPARENT | PSP_USETITLE |
                        PSP_USEICONID | PSP_USECALLBACK;
    psp.hInstance   = _Module.GetResourceInstance();
    psp.pszTemplate = MAKEINTRESOURCE(IDD_FILETIME_PROPPAGE);
    psp.pszIcon     = MAKEINTRESOURCE(IDI_TAB_ICON);
    psp.pszTitle    = szPageTitle;
    psp.pfnDlgProc  = PropPageDlgProc;
    psp.lParam      = (LPARAM) szFile;
    psp.pfnCallback = PropPageCallbackProc;
    psp.pcRefParent = (UINT*) &_Module.m_nLockCnt;

There are a few important details here that we must pay attention to for the extension to work correctly:

  1. The pszIcon member is set to the resource ID of a 16x16 icon, which will be displayed in the tab. Having an icon is optional, of course, but I added an icon to make our page stand out.
  2. The pfnDlgProc member is set to the address of the dialog proc of our page.
  3. The lParam member is set to szFile, which is a copy of the filename the page is associated with.
  4. The pfnCallback member is set to the address of a callback function that gets called when the page is created and destroyed. The role of this function will be explained later.
  5. The pcRefParent member is set to the address of a member variable inherited from CComModule. This variable is the lock count of the DLL. The shell increments this count when the property sheet is displayed, to keep our DLL in memory while the sheet is open. The count will be decremented after the sheet is destroyed.

Having set up that struct, we call the API to create the property page.

    hPage = CreatePropertySheetPage ( &psp );

If that succeeds, we call the shell's callback function which adds the newly-created page to the property sheet. The callback returns a BOOL indicating success or failure. If it fails, we destroy the page.

    if ( NULL != hPage )
      {
      // Call the shell's callback function, so it adds the page to

      // the property sheet.

      if ( !lpfnAddPageProc ( hPage, lParam ) )
        DestroyPropertySheetPage ( hPage );
      }
    }   // end for

 
  return S_OK;
}

A Sticky Situation With Lifetimes of Objects

Time to deliver on my promise to explain about the duplicate string. The duplicate is needed because after AddPages() returns, the shell releases its IShellPropSheetExt interface, which in turn destroys the CFileTimeShlExt object. That means that the property page's dialog proc can't access the m_lsFiles member of CFileTimeShlExt.

My solution was to make a copy of each filename, and pass a pointer to that copy to the page. The page owns that memory, and is responsible for freeing it. If there is more than one selected file, each page gets a copy of the filename it is associated with. The memory is freed in the PropPageCallbackProc function, shown later. This line in AddPages():

  psp.lParam = (LPARAM) szFile;

is the important one. It stores the pointer in the PROPSHEETPAGE struct, and makes it available to the page's dialog proc.

The Property Page Callback Functions

Now, on to the property page itself. Here's what the new page looks like. Keep this picture in mind while you're reading over the explanation of how the page works.

 [Our new property page - 25K]

Notice there is no last accessed time control. FAT only keeps the last accessed date. Other file systems keep the time, but I have not implemented logic to check the file system. The time will always be stored as 12 midnight if the file system supports the last accessed time field.

The page has two callback functions and two message handlers. These prototypes go at the top of FileTimeShlExt.cpp:

BOOL CALLBACK PropPageDlgProc ( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam );
UINT CALLBACK PropPageCallbackProc ( HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp );
BOOL OnInitDialog ( HWND hwnd, LPARAM lParam );
BOOL OnApply ( HWND hwnd, PSHNOTIFY* phdr );

The dialog proc is pretty simple. It handles three messages: WM_INITDIALOG, PSN_APPLY, and DTN_DATETIMECHANGE. Here's the WM_INITDIALOG part:

BOOL CALLBACK PropPageDlgProc ( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
BOOL bRet = FALSE;
 
  switch ( uMsg )
    {
    case WM_INITDIALOG:
      bRet = OnInitDialog ( hwnd, lParam );
    break;

OnInitDialog() is explained later. Next up is PSN_APPLY, which is sent if the user clicks the OK or Apply button.

    case WM_NOTIFY:
      {
      NMHDR* phdr = (NMHDR*) lParam;
 
      switch ( phdr->code )
        {
        case PSN_APPLY:
          bRet = OnApply ( hwnd, (PSHNOTIFY*) phdr );
        break;

And finally, DTN_DATETIMECHANGE. This one is simple - we just enable the Apply button by sending a message to the property sheet (which is the parent window of our page).

        case DTN_DATETIMECHANGE:
          // If the user changes any of the DTP controls, enable

          // the Apply button.

          SendMessage ( GetParent(hwnd), PSM_CHANGED, (WPARAM) hwnd, 0 );
        break;
        }  // end switch

      }  // end case WM_NOTIFY

    break;
    }  // end switch

 
  return bRet;
}

So far, so good. The other callback function is called when the page is created or destroyed. We only care about the latter case, since it's when we can free the duplicate string that was created back in AddPages(). The ppsp parameter points at the PROPSHEETPAGE struct used to create the page, and the lParam member still points at the duplicate string which must be freed.

UINT CALLBACK PropPageCallbackProc ( HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp )
{
  if ( PSPCB_RELEASE == uMsg )
    free ( (void*) ppsp->lParam );
 
  return 1;
}

The function always returns 1 because when the function is called during the creation of the page, it can prevent the page from being created by returning 0. Returning 1 lets the page be created normally. The return value is ignored when the function is called when the page is destroyed.

The Property Page Message Handlers

A lot of important stuff happens in OnInitDialog(). The lParam parameter again points to the PROPSHEETPAGE struct used to create this page. Its lParam member points to that ever-present filename. Since we need to have access to that filename in the OnApply() function, we save the pointer using SetWindowLong().

BOOL OnInitDialog ( HWND hwnd, LPARAM lParam )
{        
PROPSHEETPAGE*  ppsp = (PROPSHEETPAGE*) lParam;
LPCTSTR         szFile = (LPCTSTR) ppsp->lParam;
HANDLE          hFind;
WIN32_FIND_DATA rFind;
 
  // Store the filename in this window's user data area, for later use.

  SetWindowLong ( hwnd, GWL_USERDATA, (LONG) szFile );

Next, we get the file's created, modified, and accessed times using FindFirstFile(). If that succeeds, the DTP controls are initialized with the right data.

  hFind = FindFirstFile ( szFile, &rFind );
 
  if ( INVALID_HANDLE_VALUE != hFind )
    {
    // Initialize the DTP controls.

    SetDTPCtrl ( hwnd, IDC_MODIFIED_DATE, IDC_MODIFIED_TIME,
                 &rFind.ftLastWriteTime );
 
    SetDTPCtrl ( hwnd, IDC_ACCESSED_DATE, 0,
                 &rFind.ftLastAccessTime );
 
    SetDTPCtrl ( hwnd, IDC_CREATED_DATE, IDC_CREATED_TIME,
                 &rFind.ftCreationTime );
 
    FindClose ( hFind );
    }

SetDTPCtrl() is a utility function that sets the contents of the DTP controls. You can find the code at the end of FileTimeShlExt.cpp.

As an added touch, the full path to the file is shown in the static control at the top of the page.

  PathSetDlgItemPath ( hwnd, IDC_FILENAME, szFile );
  return FALSE;
}

The OnApply() handler does the opposite - it reads the DTP controls and modifies the file's created, modified, and accessed times. The first step is to retrieve the filename pointer by using GetWindowLong() and open the file for writing.

BOOL OnApply ( HWND hwnd, PSHNOTIFY* phdr )
{
LPCTSTR  szFile = (LPCTSTR) GetWindowLong ( hwnd, GWL_USERDATA );
HANDLE   hFile;
FILETIME ftModified, ftAccessed, ftCreated;
 
  // Open the file.

  hFile = CreateFile ( szFile, GENERIC_WRITE, FILE_SHARE_READ, NULL,
                       OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );

If we can open the file, we read the DTP controls and write the times back to the file. ReadDTPCtrl() is the counterpart of SetDTPCtrl().

  if ( INVALID_HANDLE_VALUE != hFile )
    {
    // Retrieve the dates/times from the DTP controls.

    ReadDTPCtrl ( hwnd, IDC_MODIFIED_DATE, IDC_MODIFIED_TIME, &ftModified );
    ReadDTPCtrl ( hwnd, IDC_ACCESSED_DATE, 0, &ftAccessed );
    ReadDTPCtrl ( hwnd, IDC_CREATED_DATE, IDC_CREATED_TIME, &ftCreated );
 
    // Change the file's created, accessed, and last modified times.

    SetFileTime ( hFile, &ftCreated, &ftAccessed, &ftModified );
    CloseHandle ( hFile );
    }
  else
    // <<Error handling omitted>>

 
  // Return PSNRET_NOERROR to allow the sheet to close if the user clicked OK.

  SetWindowLong ( hwnd, DWL_MSGRESULT, PSNRET_NOERROR );
  return TRUE;
}

Registering the Shell Extension

Registering a drag and drop handler is similar to registering a context menu extension. The handler can be invoked for a particular file type, for example all text files. This extension works on any file, so we register it under the HKEY_CLASSES_ROOT\* key. Here's the RGS script to register the extension:

HKCR
{
  NoRemove *
  {
    NoRemove shellex
    {
      NoRemove PropertySheetHandlers
      {
        {3FCEF010-09A4-11D4-8D3B-D12F9D3D8B02}
      }
    }
  }
}

You might notice that the extension's GUID is the stored as the name of a registry key here, instead of a string value. The documentation and books I've looked at conflict on the correct naming convention, although during my brief testing, both ways worked. I have decided to go with the way Dino Esposito's book (Visual C++ Windows Shell Programming) does it, and put the GUID in the name of the registry key.

As always, on NT-based OSes, we need to add our extension to the list of "approved" extensions. The code to do this is in the DllRegisterServer() and DllUnregisterServer() functions in the sample project.

To Be Continued...

Coming up in Part VI, we'll see another new type of extension, the drop handler, which is invoked when shell objects are dropped onto a file.

Copyright and License

This article is copyrighted material, ©2000-2006 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.

The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not required.

Revision History

April 8, 2000: Article first published.
June 6, 2000: Something updated. ;)
May 25, 2006: Updated to cover changes in VC 7.1, cleaned up code snippets, sample project gets themed on XP.

Series Navigation: « Part IV | Part VI »

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here