Introduction
After writing my tutorial on property sheet shell
extensions, I had some folks ask me how to go about customizing pages in Control Panel applets. The procedure
is very similar to writing a "normal" property sheet extension, so I have written this article that summarizes
how to do it.
I classified this article as intermediate level, as you should already be familiar with property sheet extensions,
the COM interfaces involved and their methods, and how to code property pages in straight SDK calls. If you need
a refresher, see my tutorial.
Also keep in mind that customizing Control Panel applets shouldn't be done lightly. If you're writing custom
driver software (e.g., something like Microsoft's IntelliPoint or IntelliType), then it's perfectly reasonable.
But if you just have a little wallpaper-switching program, you'd be better served to write your own configuration
app instead of adding to the already-crowded Display Control Panel.
The sample project for this article is a simple extension that adds two custom pages to the Display applet.
The Extension Interfaces
As with a normal property sheet extension, a Control Panel extension implements two interfaces: IShellExtInit
and IShellPropSheetExt
. The sequence of method calls is a bit different from normal property sheet
extensions. The sequence goes like this:
IShellExtInit::Initialize()
is called first, but since there are no files to be selected, the
pidlFolder
and lpdobj
parameters are both NULL. You can perform any one-time initialization
in Initialize()
.
IShellPropSheetExt::ReplacePage()
is called once for each page that is replaceable, if the applet
allows pages to be replaced. You can replace one page in each ReplacePage()
call; I'll explain this
below.
IShellPropSheetExt::AddPages()
is called once. You can add as many pages as you like in AddPages()
.
The big difference is the ReplacePage()
method, which went unused in normal property sheet extensions.
Certain Control Panel applets let extensions replace pages in the property sheet. You can find the exact pages
and applets in the cplext.h
file, but I've summarized them here:
- Display applet: You can replace the Background page.
- Keyboard applet: You can replace the Speed page.
- Mouse applet: You can replace the Buttons and Motion pages.
The initialization interface
As I mentioned above, IShellExtInit::Initialize()
is called, but since there is no selection in
a Control Panel applet, the parameters are meaningless. If you have any one-time initialization to do, Initialize()
is a good place to do it.
The property sheet interface
The Display applet allows extensions to replace one page (Background), so the IShellPropSheetExt::ReplacePage()
method is called once. An extension can just return S_OK
if it doesn't want to replace the page. If
the extension does want to replace the page, it creates a new property page and the applet displays it in place
of the built-in Background page.
The prototype for ReplacePage()
is:
HRESULT IShellPropSheetExt::ReplacePage (
UINT uPageID,
LPFNADDPROPSHEETPAGE lpfnReplaceWith,
LPARAM lParam );
The parameters are:
uPageID
- A constant from
cplext.h
that indicates which page the applet is querying for. For example, the
Display applet calls ReplacePage()
with uPageID
set to CPLPAGE_DISPLAY_BACKGROUND
to indicate the extension can replace the Background page. In applets that allow more than one page to be replaced
(such as the Mouse applet), ReplacePage()
is called once for each page.
lpfnReplaceWith
, lParam
lpfnReplaceWith
is a function pointer that the extension calls to actually replace the page. lParam
is a value that's meaningful to the shell and gets passed to the lpfnReplaceWith
function.
The process for replacing a page is pretty much the same as adding a page. Our extension fills in a PROPSHEETPAGE
struct, creates a page, and calls the lpfnReplaceWith
function to replace the Background page. The
sample page has no controls on it; the purpose is just to show how to get pages in the Display applet. I'm assuming
that you're able to code a dialog proc for the page; if you need help with this, check out other articles on property
sheets at CodeProject.
Here's the code for our ReplacePage()
method. We first verify that uPageID
is a value
we expect.
#include <cplext.h>
STDMETHODIMP CDisplayCplExt::ReplacePage ( UINT uPageID,
LPFNADDPROPSHEETPAGE lpfnReplaceWith,
LPARAM lParam )
{
if ( CPLPAGE_DISPLAY_BACKGROUND != uPageID )
return S_OK;
We then fill in a PROPSHEETPAGE
struct. The code here references the dialog proc and a callback
function, which I'll get to in a bit.
PROPSHEETPAGE psp;
HPROPSHEETPAGE hPage;
ZeroMemory ( &psp, sizeof(PROPSHEETPAGE) );
psp.dwSize = sizeof(PROPSHEETPAGE);
psp.dwFlags = PSP_USEREFPARENT | PSP_DEFAULT | PSP_USECALLBACK;
psp.hInstance = _Module.GetResourceInstance();
psp.pszTemplate = MAKEINTRESOURCE(IDD_REPLACEPAGE);
psp.pfnDlgProc = ReplacementPageDlgProc;
psp.pfnCallback = ReplacementPageCallbackProc;
psp.pcRefParent = (UINT*) &_Module.m_nLockCnt;
We then create a page and pass it to the lpfnReplaceWith
function.
hPage = CreatePropertySheetPage ( &psp );
if ( NULL != hPage )
{
if ( !lpfnReplaceWith ( hPage, lParam ))
{
DestroyPropertySheetPage ( hPage );
}
}
return S_OK;
}
There are two additional functions, ReplacementPageDlgProc
and ReplacementPageCallbackProc
.
You can check out the code in the sample project; they are just skeletons since the page has no controls.
Note that shell version 4.71 and later includes a Display extension that has a Background page replacement,
so if you use the above code without disabling the shell's own extension, you'll still see a Background tab, as
shown here:
An extension can also add any number of pages to the property sheet from its AddPages()
method.
This is done just as in normal property sheet extensions, and the code is almost identical to the ReplacePage()
code above. Check out the sample project if you're dying to see the code.
The sample extension adds one page to the Display applet in AddPages()
, and here are the final
results:
Registering the Extension
The Control Panel applets that can be customized have their own area of the registry in HKEY_LOCAL_MACHINE
.
Unfortunately, the registry key used to extend the main Display property sheet is different on 9x and 2000, so
we can't register our extension with an RGS script. On Windows 9x, it's HKLM\Software\Microsoft\Windows\CurrentVersion\Controls
Folder\Display\shellex\PropertySheetHandlers
, and on Windows 2000 it's HKLM\Software\Microsoft\Windows\CurrentVersion\Controls
Folder\Desk\shellex\PropertySheetHandlers
. We create a new key under PropertySheetHandlers
for our extension in DllRegisterServer()
, and remove the key in DllUnregisterServer()
.
I don't have an NT 4 system handy to check which registry key it uses, so for the time being the registration
code treats NT 4 the same as Win 2000. I'd appreciate it if a kind reader could check on NT 4 for me, and let
me know if the code is using the wrong key.
Debugging Control Panel Extensions
It's easy to debug a Control Panel extension in the Visual C debugger, once you know what debug settings to
use. Microsoft Knowledge Base articles Q166168
and Q135068 have a good description
of how to do it, but I'll give a quick summary here.
In your project settings, go to the Debug tab and set the executable to "rundll32.exe". The program
arguments should be "shell32.dll,Control_RunDLL" followed by the full path to the CPL file that contains
the applet. (Note that the "Control_RunDLL" part is case-sensitive.) It's not always obvious which CPL
file to use, so here are some common ones:
Applet
|
Arguments (change "C:\windows\system" if you have Windows installed in some other directory)
|
Display
|
shell32.dll,Control_RunDLL C:\windows\system\desk.cpl
|
Mouse
|
shell32.dll,Control_RunDLL C:\windows\system\main.cpl
|
Keyboard
|
shell32.dll,Control_RunDLL C:\windows\system\main.cpl,@1
|
The "@1" part indicates which applet in main.cpl
to run. (CPL files can contain more
than one applet; adding @n runs applet number n, where n is a 0-based count.)
Here's a screen shot that demonstrates the settings, in case things still are a bit unclear:
When you start debugging, rundll32 will call the Control_RunDLL
function in the shell, which in
turn will launch the Control Panel applet. To end debugging, just close the applet's window.