|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Contents
IntroductionHere 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 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 InterfaceYou 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 Since a property sheet handler operates on all selected files at once, it uses 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 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 // 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 // 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 PagesIf 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 The parameters to 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 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:
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 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 ObjectsTime to deliver on my promise to explain about the duplicate string. The duplicate is needed because after 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 psp.lParam = (LPARAM) szFile; is the important one. It stores the pointer in the The Property Page Callback FunctionsNow, 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: BOOL CALLBACK PropPageDlgProc ( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
BOOL bRet = FALSE;
switch ( uMsg )
{
case WM_INITDIALOG:
bRet = OnInitDialog ( hwnd, lParam );
break;
case WM_NOTIFY: { NMHDR* phdr = (NMHDR*) lParam; switch ( phdr->code ) { case PSN_APPLY: bRet = OnApply ( hwnd, (PSHNOTIFY*) phdr ); break; And finally, 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 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 HandlersA lot of important stuff happens in 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 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 ); }
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 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. 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 ExtensionRegistering 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 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 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 LicenseThis 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 HistoryApril 8, 2000: Article first published. | ||||||||||||||||||||