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.
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 };
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.
if ( FAILED( pDataObj->GetData ( &etc, &stg ) ))
return E_INVALIDARG;
hdrop = (HDROP) GlobalLock ( stg.hGlobal );
if ( NULL == hdrop )
{
ReleaseStgMedium ( &stg );
return E_INVALIDARG;
}
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++ )
{
if ( 0 == DragQueryFile ( hdrop, uFile, szFile, MAX_PATH ) )
continue;
if ( PathIsDirectory ( szFile ) )
continue;
m_lsFiles.push_back ( szFile );
}
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.)
if ( m_lsFiles.size() > MAXPROPPAGES )
m_lsFiles.resize ( MAXPROPPAGES );
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:
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++ )
{
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.
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:
- 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.
- The
pfnDlgProc
member is set to the address of the dialog proc of our page.
- The
lParam
member is set to szFile
, which is a copy of the filename the page is associated
with.
- 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.
- 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 )
{
if ( !lpfnAddPageProc ( hPage, lParam ) )
DestroyPropertySheetPage ( hPage );
}
}
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.
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:
SendMessage ( GetParent(hwnd), PSM_CHANGED, (WPARAM) hwnd, 0 );
break;
}
}
break;
}
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;
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 )
{
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;
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 )
{
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 );
SetFileTime ( hFile, &ftCreated, &ftAccessed, &ftModified );
CloseHandle ( hFile );
}
else
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 »