Click here to Skip to main content
Click here to Skip to main content

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

, 24 May 2006
Rate this:
Please Sign up or sign in to vote.
A tutorial on writing a shell extension that provides custom drag and drop functionality.

Contents

Introduction

In Parts I and II of the Guide, I showed how to write context menu extensions that are invoked when the user right-clicks on certain types of files. In this part, I'll demonstrate a different type of context menu extension, the drag and drop handler, which adds items to the context menu displayed for a right-button drag and drop operation. I'll also give more examples of using MFC in an extension.

Part IV assumes that you understand the basics of shell extensions, and are familiar with MFC. This particular extension is a real utility that creates hard links on Windows 2000 and later, but you can still follow along even if you are using an older version of Windows.

As every power user knows (and few normal users know), you can drag and drop items in Explorer using the right mouse button. When you release the button, Explorer shows a context menu that lists all the available actions you can take. Normally, these are move, copy, and create shortcut:

 [Drag and drop menu - 4K]

Explorer lets us add items to this menu, by using a drag and drop handler. This type of extension is invoked when any right-drag and drop operation happens, and the extension can add menu items if it deems it should. An example of a drag and drop handler is in WinZip. Here's what WinZip adds to the context menu when you right-drag a compressed file:

 [WinZip menu - 6K]

WinZip's extension is invoked for any right-drag and drop operation, but it only adds it menu items if a compressed file is being dragged.

This article's sample extension will use an API added in Windows 2000, CreateHardLink(), to make hard links to files on NTFS volumes. We'll add an item for making links to the context menu, so the user can make hard links the same way as regular shortcuts.

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.

Using AppWizard to Get Started

Run the AppWizard and make a new ATL COM app. We'll call it HardLink. We are going to use MFC, so check the Support MFC checkbox, and then click Finish. To add a COM object to the DLL, go to the ClassView tree, right-click the HardLink classes item, and pick New ATL Object. (In VC 7, right-click the item and pick Add|Add Class.) As before, choose Simple Object in the wizard, and use the name HardLinkShlExt for the object. This will create a C++ class CHardLinkShlExt that will implement the extension.

The Initialization Interface

As with our earlier context menu extensions, Explorer initializes us using the IShellExtInit interface. We first need to add IShellExtInit to the list of interfaces that CHardLinkShlExt implements. Open HardLinkShlExt.h, and add the lines listed here in bold:

#include <comdef.h>
#include <shlobj.h>
 
class CHardLinkShlExt :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CHardLinkShlExt, &CLSID_HarkLinkShlExt>,
  public IShellExtInit<FONT COLOR="red">
</FONT>{
  BEGIN_COM_MAP(CHardLinkShlExt)
<FONT COLOR="red">    </FONT>COM_INTERFACE_ENTRY(IShellExtInit)
  END_COM_MAP()
 
public:
  // IShellExtInit
  STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);

We'll also need some variables to hold a bitmap and the names of the files being dragged:

protected:
  CBitmap     m_bitmap;
  TCHAR       m_szFolderDroppedIn[MAX_PATH];
  CStringList m_lsDroppedFiles;

Also, we'll need to add a some #defines to stdafx.h to make the CreateHardLink() and shlwapi.dll function prototypes visible:

#define WINVER 0x0500
#define _WIN32_WINNT 0x0500
#define _WIN32_IE 0x0400

Defining WINVER as 0x0500 enables features specific to Win 98 and 2000, and defining _WIN32_WINNT as 0x0500 enables NT features specific to Win 2000. Defining _WIN32_IE as 0x0400 enables features introduced with IE 4.

Now, on to the Initialize() method. This time, I'll show how to use MFC to access the list of files being dragged. MFC has a class, COleDataObject, that wraps the IDataObject interface. Previously, we had to call IDataObject methods directly. But fortunately, MFC makes the job a bit easier for us. Here's the prototype for Initialize(), to refresh your memory:

HRESULT IShellExtInit::Initialize (
  LPCITEMIDLIST pidlFolder,
  LPDATAOBJECT  pDataObj,
  HKEY          hProgID );

For drag and drop extensions, pidlFolder is the PIDL of the folder where the items were dropped. (I'll have more to say about the PIDL later.) pDataObj is an IDataObject interface with which we can enumerate all of the items that were dropped. hProgID is an open HKEY on our shell extension's key under HKEY_CLASSES_ROOT.

Our first step is to load a bitmap for our menu item. Then, we attach a COleDataObject variable to the IDataObject interface.

HRESULT CHardLinkShlExt::Initialize (
  LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj,
  HKEY hProgID )
{
  AFX_MANAGE_STATE(AfxGetStaticModuleState());  // init MFC
 
COleDataObject dataobj;
HGLOBAL        hglobal;
HDROP          hdrop;
TCHAR          szRoot[MAX_PATH];
TCHAR          szFileSystemName[256];
TCHAR          szFile[MAX_PATH];
UINT           uFile, uNumFiles;
 
  m_bitmap.LoadBitmap ( IDB_LINKBITMAP );
 
  dataobj.Attach ( pDataObj, FALSE );

Passing FALSE as the second parameter to Attach() means to not release the IDataObject interface when the dataobj variable is destructed. The next step is to get the directory where the items were dropped. We have the PIDL of this directory, but how do we get the path? Time for a little sidebar...

"PIDL" is an acronym for pointer to an ID list. A PIDL is a way of uniquely identifying any object within the hierarchy presented by Explorer. Every object in the shell, whether it's part of the file system or not, has a PIDL. The exact structure of a PIDL depends on where the object is, but unless you are writing your own namespace extension, you don't (and shouldn't) have to worry about the internal structure of a PIDL.

For our purposes, we can use the shell API SHGetPathFromIDList() to translate the PIDL into a conventional path. If the target folder isn't a directory in the file system (for example, the Control Panel), SHGetPathFromIDList() will fail and we can bail out.

  if ( !SHGetPathFromIDList(pidlFolder, m_szFolderDroppedIn) )
    return E_FAIL;

Next, we have to check if the target directory is on an NTFS volume. We get the root component of the path (for example, E:\), and get the info about that volume. If the file system is not NTFS, we can't make any links, so we can return.

  lstrcpy ( szRoot, m_szFolderDroppedIn );
  PathStripToRoot ( szRoot );
 
  if ( !GetVolumeInformation ( szRoot, NULL, 0, NULL, NULL, NULL, 
                               szFileSystemName, 256 ))
    {
    // Couldn't determine file system type.
    return E_FAIL;
    }
 
  if ( 0 != lstrcmpi ( szFileSystemName, _T("ntfs") ))
    {
    // The file system isn't NTFS, so it doesn't support hard links.
    return E_FAIL;
    }

Next, we get an HDROP handle from the data object, which we'll use to enumerate the files that were dropped. This is similar to the method in Part III, except we're using the MFC class to access the data. COleDataObject handles setting up the FORMATETC and STGMEDIUM structs for us.

  hglobal = dataobj.GetGlobalData ( CF_HDROP );
 
  if ( NULL == hglobal )
    return E_INVALIDARG;
 
  hdrop = (HDROP) GlobalLock ( hglobal );
 
  if ( NULL == hdrop )
    return E_INVALIDARG;

We then use the HDROP handle to enumerate the dropped files. For each one, we check if it is a directory. Directories cannot be linked to, so if we find a directory, we can return.

  uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );
 
  for ( uFile = 0; uFile < uNumFiles; uFile++ )
    {
    if ( DragQueryFile ( hdrop, uFile, szFile, MAX_PATH ) )
      {
      if ( PathIsDirectory ( szFile ) )
        {
        // We found a directory!  Bail out.
        m_lsDroppedFiles.RemoveAll();
        break;
        }

We also have to check that the dropped files reside on the same volume as the target directory. What I did was compare the root components of each file and the target directory, and return if they are different. This is not a complete solution, though, since on NTFS volumes, you can mount a volume in the middle of another. For example, you could have a C: volume, and mount another volume as C:\dev. This code will not reject an attempt to make a link from C:\dev to somewhere else on C:.

Here's the check that compares the root components:

      if ( !PathIsSameRoot(szFile, m_szFolderDroppedIn) )
        {
        // Dropped files came from a different volume - bail out.
        m_lsDroppedFiles.RemoveAll();
        break;
        }

If the file passes both checks, we add it to m_lsDroppedFiles, which is a CStringList (linked list of CStrings).

      // Add the file to our list of dropped files.
      m_lsDroppedFiles.AddTail ( szFile );
      }
    }   // end for

After the for loop, we release resources and return back to Explorer. If the string list contains any filenames, we return S_OK to indicate we need to modify the context menu. Otherwise, we return E_FAIL so that we won't be called again for this drag and drop event.

  GlobalUnlock ( hglobal );
 
  return (m_lsDroppedFiles.GetCount() > 0) ? S_OK : E_FAIL;
}

Modifying the Context Menu

Like other context menu extensions, a drag and drop handler implements the IContextMenu interface with which it interacts with the context menu. To add IContextMenu to our extension, open HardLinkShlExt.h again and add the lines listed in bold:

class CHardLinkShlExt :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CHardLinkShlExt, &CLSID_HardLinkShlExt>,
  public IShellExtInit,
  public IContextMenu
{
  BEGIN_COM_MAP(CHardLinkShlExt)
    COM_INTERFACE_ENTRY(IShellExtInit)
<FONT COLOR="red">    </FONT>COM_INTERFACE_ENTRY(IContextMenu)
  END_COM_MAP()
 
public:
<FONT COLOR="red">  </FONT>// IContextMenu
  STDMETHODIMP GetCommandString(UINT, UINT, UINT*, LPSTR, UINT)
                 { return E_NOTIMPL; }
  STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
  STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);

Note that we don't need any code in GetCommandString(), because that method is not called in drag and drop handlers.

Explorer calls our QueryContextMenu() function to let us modify the context menu. There's nothing here you haven't seen before; we just add one menu item and set its bitmap.

HRESULT CHardLinkShlExt::QueryContextMenu (
    HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd,
    UINT uidLastCmd, UINT uFlags )
{
  AFX_MANAGE_STATE(AfxGetStaticModuleState());  // init MFC
 
  // If the flags include CMF_DEFAULTONLY then we shouldn't do anything.
  if ( uFlags & CMF_DEFAULTONLY )
    return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
 
  // Add the hard link menu item.
  InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION,
               uidFirstCmd, _T("Create hard link(s) here") );
 
  if ( NULL != m_bitmap.GetSafeHandle() )
    {
    SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION,
                         (HBITMAP) m_bitmap.GetSafeHandle(), NULL );
    }
 
  // Return 1 to tell the shell that we added 1 top-level menu item.
  return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

Here's what the new menu item looks like:

 [Hard link menu item - 11K]

Making the Link

Explorer calls InvokeCommand() when the user clicks our menu item. We'll create links to all the files that were dropped. The names of the links will be "Hard link to <filename>", or, if that name is already in use, "Hard link (2) to <filename>". The number will go up to 99, an arbitrary limit.

First, the locals and a check of the lpVerb parameter, which must be 0 since we only have 1 menu item.

HRESULT CHardLinkShlExt::InvokeCommand (
  LPCMINVOKECOMMANDINFO pInfo )
{
  AFX_MANAGE_STATE(AfxGetStaticModuleState());  // init MFC
 
TCHAR    szNewFilename[MAX_PATH+32];
CString  sSrcFile;
TCHAR    szSrcFileTitle[MAX_PATH];
CString  sMessage;
UINT     uLinkNum;
POSITION pos;
 
  // Double-check that we're getting called for our own menu item - lpVerb
  // must be 0.
  if ( 0 != pInfo->lpVerb )
    return E_INVALIDARG;

Next, we get a POSITION value pointing at the beginning of the string list. A POSITION is an opaque data type which you don't use directly, but instead you pass it to other methods of the CStringList class. To get the POSITION of the head of the list, we call GetHeadPosition():

  pos = m_lsDroppedFiles.GetHeadPosition();
  ASSERT ( NULL != pos );

pos will be NULL if the list is empty, but the list shouldn't be empty, ever, so I added an ASSERT to check for that case. Next up is the beginning of the loop that will iterate through the filenames in the list and make a link to each one.

  while ( NULL != pos )
    {
    // Get the next source filename.
    sSrcFile = m_lsDroppedFiles.GetNext ( pos );
 
    // Remove the path - this reduces "C:\xyz\foo\stuff.exe" to "stuff.exe"
    lstrcpy ( szSrcFileTitle, sSrcFile );
    PathStripPath ( szSrcFileTitle );
 
    // Make the filename for the hard link - we'll first try 
    // "Hard link to stuff.exe"
    wsprintf ( szNewFilename, _T("%sHard link to %s"), m_szFolderDroppedIn,
               szSrcFileTitle );

GetNext() returns the CString at the position indicated by pos, and increments pos to point at the next string. If pos was at the end of the list, pos becomes NULL (so that's how the while loop will end).

At this point, szNewFilename holds the full path of the hard link. We check if a file with this name exists, and if so, we'll try adding numbers 2 through 99, looking for a name that's not already in use. We also make sure the length of the link name (including the terminating null) doesn't exceed MAX_PATH characters.

    for ( uLinkNum = 2;
          PathFileExists(szNewFilename) && uLinkNum < 100; 
          uLinkNum++ )
      {
      // Try another filename for the link.
      wsprintf ( szNewFilename, _T("%sHard link (%u) to %s"),
                 m_szFolderDroppedIn, uLinkNum, szSrcFileTitle );
 
      // If the resulting filename is longer than MAX_PATH, show an 
      // error message.
      if ( lstrlen(szNewFilename) >= MAX_PATH )
        {
        sMessage.Format ( _T("Failed to make a link to %s.\nDo you want to continue making links?"),
                         (LPCTSTR) sSrcFile );
 
        if (IDNO == MessageBox ( pInfo->hwnd, sMessage, _T("Create Hard Links"),
                                 MB_ICONQUESTION | MB_YESNO) )
          break;
        else
          continue;
        }
      }  // end for

The message box lets you abort the entire operation if you want. Next, we check to see if we hit the limit of 99 links. Again, we let the user abort the whole operation.

    if ( 100 == uLinkNum )
      {
      sMessage.Format ( _T("Failed to make a link to %s.\nDo you want to continue making links?"),
                       (LPCTSTR) sSrcFile );
 
      if (IDNO == MessageBox ( pInfo->hwnd, sMessage, _T("Create Hard Links"),
                               MB_ICONQUESTION | MB_YESNO) )
        break;
      else
        continue;
      }

All that's left is to make the hard link. I've omitted the error handling code for clarity.

    CreateHardLink ( szNewFilename, sSrcFile, NULL );
    }  // end while
 
  return S_OK;
}

The hard link doesn't look any different in Explorer, it just looks like any other ordinary file. But if you modify one copy, the changes will be reflected in the other copy.

 [Hard link to the file - 4K]

Registering the Shell Extension

Registering a drag and drop handler is simpler than other context menu extensions. All handlers are registered under the HKCR\Directory key, since that's where the drop happens, in a directory. However, what the docs don't say is that registering under HKCR\Directory isn't enough to handle all cases. You also need to register under HKCR\Folder to handle drops on the desktop, and HKCR\Drive to handle drops in root directories.

Here is the RGS script to handle all three of the above situations:

HKCR
{
  NoRemove Directory
  {
    NoRemove shellex
    {
      NoRemove DragDropHandlers
      {
        ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
      }
    }
  }
  NoRemove Folder
  {
    NoRemove shellex
    {
      NoRemove DragDropHandlers
      {
        ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
      }
    }
  }
  NoRemove Drive
  {
    NoRemove shellex
    {
      NoRemove DragDropHandlers
      {
        ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
      }
    }
  }
}

As with our previous extensions, 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.

If You Don't Have Windows 2000/NTFS

You can still build and run the sample project on earlier versions of Windows, or if you don't have an NTFS volume available. Just open the stdafx.h file, and uncomment the line that reads:

// #define NOT_ON_WIN2K

That will make the extension skip the file system check (so it will run on anything, not just NTFS), and display message boxes instead of actually making links.

To Be Continued

Coming up in Part V, we'll see a new type of extension, the property sheet handler, which adds pages to the properties dialog for files.

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 3, 2000: Article first published.
June 6, 2000: Something updated. Wink | ;)
May 24, 2006: Updated to cover changes in VC 7.1, cleaned up code snippets.

Series Navigation: « Part III | Part V »

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

Share

About the Author

Michael Dunn
Software Developer (Senior) VMware
United States United States
Michael lives in sunny Mountain View, California. He started programming with an Apple //e in 4th grade, graduated from UCLA with a math degree in 1994, and immediately landed a job as a QA engineer at Symantec, working on the Norton AntiVirus team. He pretty much taught himself Windows and MFC programming, and in 1999 he designed and coded a new interface for Norton AntiVirus 2000.
Mike has been a a developer at Napster and at his own lil' startup, Zabersoft, a development company he co-founded with offices in Los Angeles and Odense, Denmark. Mike is now a senior engineer at VMware.

He also enjoys his hobbies of playing pinball, bike riding, photography, and Domion on Friday nights (current favorite combo: Village + double Pirate Ship). He would get his own snooker table too if they weren't so darn big! He is also sad that he's forgotten the languages he's studied: French, Mandarin Chinese, and Japanese.
 
Mike was a VC MVP from 2005 to 2009.

Comments and Discussions

 
QuestionEquivalent to QueryContextMenu? PinmemberRobert97431-Mar-11 4:42 
Generalsome wrong on 2t disk simulator Pinmembernhchmg15-Dec-10 23:55 
QuestionThis is possible, right? PinmemberSebastien Mongrain18-Jan-10 12:06 
AnswerRe: This is possible, right? [edited] PinmemberGarth J Lancaster18-Jan-10 14:18 
GeneralRe: This is possible, right? [edited] PinmemberSebastien Mongrain19-Jan-10 6:02 
GeneralHardLink is not woking in Vista PinmemberGrantFei1-Feb-09 15:52 
GeneralRe: HardLink is not woking in Vista PinmemberGrantFei2-Feb-09 3:57 
GeneralHandle taskbar Contextmenu for lock/unlock toolbars. Pinmemberknareshkumar15-Dec-08 0:43 
Questionis there a way to have a shell extension not on right-drag-and-drop but on usual left-drag-and drop ? Pinmembercarabutnicolae123415-Oct-07 3:09 
AnswerRe: is there a way to have a shell extension not on right-drag-and-drop but on usual left-drag-and drop ? PinmvpMichael Dunn21-Oct-07 15:01 
GeneralRe: is there a way to have a shell extension not on right-drag-and-drop but on usual left-drag-and drop ? Pinmembercarabutnicolae123426-Oct-07 0:55 
QuestionIContexMenu::InvokeCommand not called on Vista Pinmembersathink25-Jul-07 2:48 
AnswerRe: IContexMenu::InvokeCommand not called on Vista PinmvpMichael Dunn28-Jul-07 10:28 
AnswerRe: IContexMenu::InvokeCommand not called on Vista PinmemberHugo Caldeira14-Apr-09 0:07 
QuestionAsync Transfers in a Namespace Extension Pinmemberaltronome13-Apr-07 14:53 
GeneralShel Drag Drop-ICopyHook::copyCallback() Not Called Pinmember16141113-Nov-06 2:55 
GeneralRe: Shel Drag Drop-ICopyHook::copyCallback() Not Called PinsitebuilderMichael Dunn18-Nov-06 16:08 
Generalany body help me Pinmembersyriast31-Jul-06 3:40 
GeneralFailed to build in Visual Studio .NET Pinmemberjonas.pettersson@sverige.nu29-Aug-05 9:02 
GeneralRe: Failed to build in Visual Studio .NET PinsitebuilderMichael Dunn29-Aug-05 12:07 
GeneralRe: Failed to build in Visual Studio .NET Pinmemberbelgab15-Jun-06 7:21 
GeneralRe: Failed to build in Visual Studio .NET Pinmemberrockryan23-Jul-06 21:32 
GeneralFolder Files being linked from Pinmembervaughandaly31-Jan-05 0:29 
GeneralRe: Folder Files being linked from PinsitebuilderMichael Dunn1-Feb-05 12:53 
GeneralRe: Folder Files being linked from Pinmembervaughandaly7-Feb-05 2:11 
QuestionHow to share the folder from program?? PinmemberJigar Mehta8-Oct-04 0:42 
GeneralFailing when a directory is dropped on a file PinmemberLars [Large] Werner19-Sep-04 2:32 
QuestionHardlink on Folders instead on files? PinmemberProxy4NT3-Sep-04 1:33 
AnswerRe: Hardlink on Folders instead on files? PinmemberRasqual Twilight16-Sep-04 11:51 
GeneralRe: Hardlink on Folders instead on files? PinmemberProxy4NT17-Sep-04 2:46 
AnswerRe: Hardlink on Folders instead on files? Pinmemberlsqgljzqhgjzqrhlgkjhz24-Jan-07 0:14 
GeneralFreeing the HGLOBAL PinmemberHenk Devos8-Jul-04 8:11 
GeneralRe: Freeing the HGLOBAL PinsitebuilderMichael Dunn8-Jul-04 8:36 
Questionhave you tested it on shortcuts ? PinsussAnonymous2-Jul-04 5:43 
GeneralIs there a way to get Network folder props Pinmemberbtourist17-Jul-03 10:38 
GeneralAdding a tab view control to the IShellView pane PinmemberShridhar Kamat27-Apr-03 22:09 
GeneralWon't work?! PinmemberSemlo12-Feb-03 0:34 
GeneralImportant note PinmemberBrett Beebe8-Feb-02 5:47 
GeneralRe: Important note Pinmemberlsqgljzqhgjzqrhlgkjhz24-Jan-07 0:29 
GeneralTranslation on Russian PinmemberOdissey8-Aug-01 2:52 
GeneralRe: Translation on Russian PinmemberChika7-Nov-01 15:22 
GeneralMaking the operation fail... PinmemberMickael Pointier20-Jun-01 8:15 
GeneralRe: Making the operation fail... PinmemberMichael Dunn24-Jun-01 21:09 
GeneralRe: Making the operation fail... PinmemberMickael Pointier24-Jun-01 22:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141220.1 | Last Updated 24 May 2006
Article Copyright 2000 by Michael Dunn
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid