This article aims at making implementation of a property sheet shell extension easier. It follows my first article
dealing with context menu shell extension and was inspired
by Michael Dunn's most excellent series of tutorials
about writing shell extensions. I strongly encourage you to read through it in order to have a good understanding
of what follows.
This time, we will tackle a solution to implementing a property sheet shell extension. We will reuse material
found on my previous article and add a few lines of code. All this is packaged in a neat PropertySheet Shell Extension
AppWizard.
Installing and running the Wizard
The Property Sheet Shell Extension Wizard comes in a self-contained propsheetapp.awx
file that
needs to be copied in your template folder. The template folder can be found under your main Microsoft Visual
Studio installation folder, under the \Common\MSDev98\Template folder.
The wizard is activated by using the File|New menu item that triggers a New Projects dialog box.
Selecting the Property Sheet Shell Extension item brings the following wizard dialog box. This step allows
to specify the type of files the shell extension will be registered against, as well as the C++ class name that
will be generated by the wizard.
What does the Wizard give you?
The wizard produces a Visual Studio project similar to those created by the ATL/COM AppWizard.
The main difference is that the project already contains a class implementing the skeleton of a property sheet
shell extension.
Recall from Michael Dunn's tutorial that a property sheet shell extension is a COM object that should implement
the IShellExtInit
and IShellPropSheetExt
interfaces. The class generated by the wizard
already provides all that is required to support these implementations in the form of one header file atlshellex.h
that is identical to the one used in my previous article, albeit updated, and a few extra lines of code to flesh
out the implementation of the IShellPropSheetExt
interface.
The Wizard produces code that make use of the WTL extensions to ATL. These are really convenient and there is
no obvious reason not to use them. These extensions simplify programming the property page itself and the dialog
controls. A initial property page class has been generated by the wizard. It already knows how to react to WM_INITDIALOG
and PSM_APPLY
messages.
Let's start with a sample
Let's attempt to write a shell extension similar to the one described in the tutorial. It's not as easy this
time, because a property sheet is somewhat complicated and you'll have to do most of the work yourself. However,
all the grunge work to actually create and display the property page is already generated by the wizard.
From the screen shot above, notice that we choose to hook our shell extension to .TXT files, just like Michael's.
Notice also that the class in which the extension will be implemented in our example is called CShellExt
.
If you look at the implementation of the AddPages()
method in CShellExt
, you will
see those lines:
STDMETHODIMP CShellExt::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
{
CSamplePropPage* m_pPage;
m_pPage = new CSamplePropPage;
m_pPage->SetTitle(_T("Sample"));
m_pPage->SetFile(m_files[0]);
HPROPSHEETPAGE hPage = m_pPage->Create();
if (!lpfnAddPage(hPage, lParam)) delete m_pPage;
return S_OK;
}
This code, generated by the wizard, actually creates an instance of a property page and assigns it a dummy title
based on your project name. It also passes the name of the first selected file to the property sheet, and calls
the shell's callback function that performs page addition to the property sheet. Please, notice that the property
page class name is synthesized based upon your project name, in our case, the project is Sample, hence
the CSamplePropPage
class.
If we need to add more pages, like Michael is doing in his example, that's the place to put them. I won't do
that for my example here. Suffice it to say that you just need to iterate over the m_files
array and
create additional instances of the property page for each of them.
Suprisingly enough, the WTL implementation of property pages doesn't provide a way to set the small icon in
the property page tab. So we will need to add it ourselves. Besides, we would like the filename to be displayed
in the tab as well. The changes needed are outlined below:
#include "shlwapi.h"
...
STDMETHODIMP CShellExt::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
{
CSamplePropPage* m_pPage;
m_pPage = new CSamplePropPage;
COLOR="red">
m_pPage->m_psp.pszIcon = MAKEINTRESOURCE(IDI_ICON);
m_pPage->m_psp.dwFlags |= PSP_USEICONID;
TCHAR szFile[_MAX_FNAME];
lstrcpy(szFile, m_files[0]);
::PathStripPath(szFile);
m_pPage->SetTitle(szFile);
m_pPage->SetFile(m_files[0]);
HPROPSHEETPAGE hPage = m_pPage->Create();
if (!lpfnAddPage(hPage, lParam)) delete m_pPage;
return S_OK;
}
Be sure to add shlwapi.lib
to the list of libraries in your project settings.
Now that we have the basics of our property sheet up and running, we need to add more interesting features to
the CSamplePropPage
class. We will add five Date and Time Picker controls to our dialog template in
order to display the various date and times, and an extra static control to display the complete file path.
To make our life simpler, we will make use of the SetCombinedDatetime()
and GetCombinedDateTime()
helper functions which, respectively set and get the date and time portions from a couple of Date and Time Picker
controls. The code for these functions is adapted from Michael's project:
void GetCombinedDateTime ( HWND hwnd, UINT idcDatePicker, UINT idcTimePicker,
FILETIME* pFiletime )
{
SYSTEMTIME st = {0}, stDate = {0}, stTime = {0};
FILETIME ftLocal;
CDateTimePickerCtrl dtControl;
dtControl.Attach(::GetDlgItem(hwnd, idcDatePicker));
dtControl.GetSystemTime(&stDate);
if (idcTimePicker != 0) {
dtControl.Attach(::GetDlgItem(hwnd, idcTimePicker));
dtControl.GetSystemTime(&stTime);
}
st.wMonth = stDate.wMonth;
st.wDay = stDate.wDay;
st.wYear = stDate.wYear;
st.wHour = stTime.wHour;
st.wMinute = stTime.wMinute;
st.wSecond = stTime.wSecond;
::SystemTimeToFileTime (&st, &ftLocal);
::LocalFileTimeToFileTime (&ftLocal, pFiletime);
dtControl.Detach();
}
void SetCombinedDateTime ( HWND hwnd, UINT idcDatePicker, UINT idcTimePicker,
const FILETIME* pFiletime )
{
SYSTEMTIME st;
FILETIME ftLocal;
::FileTimeToLocalFileTime (pFiletime, &ftLocal);
::FileTimeToSystemTime (&ftLocal, &st);
CDateTimePickerCtrl dtControl;
dtControl.Attach(::GetDlgItem(hwnd, idcDatePicker));
dtControl.SetSystemTime(GDT_VALID, &st);
if (idcTimePicker != 0) {
dtControl.Attach(::GetDlgItem(hwnd, idcTimePicker));
dtControl.SetSystemTime(GDT_VALID, &st);
}
dtControl.Detach();
}
The code to update these controls is pretty standard WTL control programming and happens in the OnInitDialog()
function. This function was generated by the Wizard, so here are the lines you need to add:
LRESULT CSamplePropPage::OnInitDialog(HWND , LPARAM )
{
CStatic sPath;
sPath.Attach(::GetDlgItem(m_hWnd, IDC_FILE));
sPath.SetWindowText(m_file);
sPath.Detach();
FILETIME creationTime;
FILETIME accessTime;
FILETIME modificationTime;
HANDLE hFile;
hFile = ::CreateFile(m_file, GENERIC_READ,
FILE_SHARE_READ, 0,
OPEN_EXISTING, 0, 0);
ATLASSERT(hFile != INVALID_HANDLE_VALUE);
::GetFileTime(hFile, &creationTime, &accessTime, &modificationTime);
::CloseHandle(hFile);
SetCombinedDateTime(m_hWnd, IDC_CREATIONDATE, IDC_CREATIONTIME, &creationTime);
SetCombinedDateTime(m_hWnd, IDC_MODIFICATIONDATE, IDC_MODIFICATIONTIME, &modificationTime);
SetCombinedDateTime(m_hWnd, IDC_ACCESSDATE, 0, &accessTime);
return 0L;
}
To enable the Apply button, we need to capture the notifications sent from the Date and Time Picker controls.
These send a DTN_DATETIMECHANGE
notification to the property sheet. We just need to add a macro to
our message map and a corresponding message handler in the class to get the job done. Notice that since we are
using WTL, I use the _EX
versions of the message map macros which perform message cracking, but you
can always use the one supplied with ATL if you prefer.
class CSamplePropPage : public CPropertyPageImpl<CSamplePropPage>
{
...
LRESULT OnDateTimeChanged(LPNMHDR lpnmhdr);
...
public:
BEGIN_MSG_MAP_EX(CSamplePropPage)
CHAIN_MSG_MAP(CPropertyPageImpll<CSamplePropPage>)
MSG_WM_INITDIALOG(OnInitDialog)
NOTIFY_RANGE_CODE_HANDLER_EX(IDC_CREATIONDATE, IDC_ACCESSDATE, DTN_DATETIMECHANGE, OnDateTimeChanged)
END_MSG_MAP()
};
I'm assuming that the resource identifiers of the Date and Time Picker controls are sequential, so that I can
use the range handler to route notifications from all these controls to one single message handler. The implementation
of the OnDateTimeChanged()
function is straightforward. Just enable the Apply button...
LRESULT CSamplePropPage::OnDateTimeChanged(LPNMHDR )
{
SetModified(TRUE);
return 0L;
}
Hitting the Apply button will now trigger the OnApply()
function, that was generated by the Wizard.
This is an overridden function that gets called whenever the user hits the OK or Apply button. We need to apply
the date and time modifications that were made back to the file.
BOOL CSamplePropPage::OnApply(void)
{
FILETIME creationTime;
FILETIME accessTime;
FILETIME modificationTime;
GetCombinedDateTime(m_hWnd, IDC_CREATIONDATE, IDC_CREATIONTIME, &creationTime);
GetCombinedDateTime(m_hWnd, IDC_MODIFICATIONDATE, IDC_MODIFICATIONTIME, &modificationTime);
GetCombinedDateTime(m_hWnd, IDC_ACCESSDATE, 0, &accessTime);
HANDLE hFile;
hFile = ::CreateFile(m_file, GENERIC_WRITE,
0, 0, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, 0);
ATLASSERT(hFile != INVALID_HANDLE_VALUE);
::SetFileTime(hFile, &creationTime, &accessTime, &modificationTime);
::CloseHandle(hFile);
return TRUE;
}
That's it! The project is now complete.
Conclusion
As you have seen, implementing a context menu extension is now faster and easier. Besides, you can reuse the
provided atlshellex.h
files in your own projects. This file, atlshellex.h
implements
the IShellExtInit
interface in a way that is also directly relevant to implementing Context Menu Shell
Extension as outlined in my previous article.
I hope this article is clear enough. Please let me know if you have some trouble.