PropertySheet Shell Extension AppWizard

PropertySheet Shell Extension AppWizard

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.

 [VC New dialog - 3K]

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.

 [AppWizard dialog - 10K]

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:

 
<FONT COLOR="red">#include </FONT><FONT COLOR="red">"shlwapi.h"</FONT>
...
STDMETHODIMP CShellExt::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
{
	CSamplePropPage* m_pPage;
	m_pPage = new CSamplePropPage;

	<FONT COLOR="red">// Add a small icon (assuming a resource identifier IDI_ICON)</FONT><FONT
COLOR="red">
	m_pPage->m_psp.pszIcon = MAKEINTRESOURCE(IDI_ICON);
	m_pPage->m_psp.dwFlags |= PSP_USEICONID;</FONT>

	<FONT COLOR="red">// Use the file name as the title</FONT><FONT COLOR="red">
	TCHAR szFile[_MAX_FNAME];
	lstrcpy(szFile, m_files[</FONT><FONT COLOR="red">0</FONT><FONT COLOR="red">]);
	::PathStripPath(szFile);</FONT>	

	m_pPage->SetTitle(<FONT COLOR="red">szFile</FONT>);
	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.

 [Property page dlg - 3K]

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 /*hWnd*/, LPARAM /*lParam*/)
{
	// Display the full path
	CStatic sPath;
	sPath.Attach(::GetDlgItem(m_hWnd, IDC_FILE));
	sPath.SetWindowText(m_file);
	sPath.Detach();

	// Get the file dates and times

	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);

	// Display the creation/modification/access date and time

	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);
...
// Message map
public:
BEGIN_MSG_MAP_EX(CSamplePropPage)
	CHAIN_MSG_MAP(CPropertyPageImpll<CSamplePropPage>)
	MSG_WM_INITDIALOG(OnInitDialog)
	<FONT COLOR="red">NOTIFY_RANGE_CODE_HANDLER_EX(IDC_CREATIONDATE, IDC_ACCESSDATE, DTN_DATETIMECHANGE, OnDateTimeChanged)</FONT>
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 /*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;

	// Get the new creation/modification/access date and time

	GetCombinedDateTime(m_hWnd, IDC_CREATIONDATE, IDC_CREATIONTIME, &creationTime);
	GetCombinedDateTime(m_hWnd, IDC_MODIFICATIONDATE, IDC_MODIFICATIONTIME, &modificationTime);
	GetCombinedDateTime(m_hWnd, IDC_ACCESSDATE, 0, &accessTime);

	// Set the file dates and times
	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.

posted @ 2022-12-13 14:09  小风风的博客  阅读(22)  评论(0编辑  收藏  举报