This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


November 1999

Microsoft Systems Journal Homepage

C++ Q&A

Code for this article: Nov99CQA.exe (145KB)

Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and writer-at-large. He can be reached at askpd@pobox.com or http://pobox.com/~askpd.

Q I'm writing an MPEG player for Windows® 98 and Windows 2000 and I want to create a Control Panel applet to let the user change some basic settings. I know I have to create a DLL, but the interface looks like it was made for C. How can I do it with C++ and MFC?

A Never forget: C++ is C. So anything you do in C is, ipso facto, C++. But I don't suppose that's the answer you want, so I'll show you how you can encapsulate a Control Panel applet in C++. In fact, I wrote a miniature Control Panel framework that makes it easy.
     Amazingly, very little has changed in Control Panel extensions since Greg Keyser discussed this topic back in the November 1992 issue of MSJ. A Control Panel extension is just a DLL with a special function called CPlApplet that has a signature like this:


 LRESULT CPlApplet(HWND hwnd, UINT msg, 
                   LPARAM lp1, LPARAM lp2);
     Control Panel DLLs have the special file name extension .cpl, not .dll. When the Windows Control Panel (CONTROL.EXE) starts up, it looks for XXX.cpl in your Windows System directory. It loads each DLL and calls its CPlApplet function with various messages. For example, when the Control Panel first starts up, it calls your CPlApplet function with msg=CPL_INIT. When the user double-clicks your applet's icon, it calls CPlApplet with msg=CPL_DBLCLK, your applet's cue to display its dialog. Each Control Panel DLL can support more than one icon or applet. You tell the Control Panel how many applets you have by responding to CPL_GETCOUNT, and the Control Panel requests information about each one by sending CPL_INQUIRE or CPL_NEWINQUIRE. Figure 1 shows a TRACE dump that describes which messages the Control Panel sends when.
Figure 1 TRACE Dump
     Figure 1 TRACE Dump

     Most of the handshaking drudge work between the Control Panel and your DLL is so generic that it can be encapsulated in a framework. I wrote two classes, CControlPanelApp and CCPApplet, that handle this. To show how they work, I wrote my own Control Panel DLL called MyPanel. MyPanel implements two Control Panel applets (see Figure 2): one dialog (see Figure 3) and one property sheet (see Figure 4).
Figure 2 My Control Panel Applets
     Figure 2 My Control Panel Applets

     MyPanel.cpp (see Figure 5) looks a lot like a typical MFC doc/view app except the application class is derived from CControlPanelApp instead of CWinApp, and instead of overriding InitInstance to add document templates, it overrides a function called OnInit to create the two applets.

 BOOL CMyControlPanelApp::OnInit()
 {
     AddApplet(newCCPApplet(
         IDR_MYAPPLET1, 
         RUNTIME_CLASS(
         CMyDialog1)));
     AddApplet(new CCPApplet(
         IDR_MYAPPLET3, 
         RUNTIME_CLASS(
         CMyPropSheet)));
    return CControlPanelApp::OnInit();
 }
     The applet class, CCPApplet, is so generic that there's no need for MyPanel to even derive its own; CCPApplet works straight out of the box. The only real code you have to write is for the dialogs themselves. In this case, MyPanel implements a dialog (CMyDialog) and a property sheet (CMyPropSheet). Believe it or not, that's all there is. Just write your dialogs and override CControlPanelApp::OnInit to add them as shown above. The framework does everything else.
Figure 3 Dialog-based Applet
     Figure 3 Dialog-based Applet

     But wait a minute—where are the icons? Where's the description? Where's the CPlApplet function, and what about all those CPL messages? All that stuff is taken care of by CControlPanelApp and CCPApplet. CPanel.cpp has a CPlApplet function that translates CPL messages into virtual function calls. When the Control Panel calls CPlApplet with CPL_INIT, CPlApplet calls CControlPanelApp:: OnCplMsg, which in turn passes the buck to CControlPanelApp::OnInit. OnCplMsg is analogous to CWnd::WindowProc, and OnInit is analogous to a message handler like OnCreate. Some CPL messages, like CPL_INQUIRE and CPL_ DBLCLK, have as lParam1 the applet number (index) for which the message is destined. (Remember, a single Control Panel extension can implement more than one icon or applet.) In that case, CControlPanelApp::OnCplMsg routes the message to a virtual function in CCPApplet, not CControlPanelApp.
Figure 4 Property Sheet-based Applet
     Figure 4 Property Sheet-based Applet

     So far, so good. I've implemented a bunch of syntactic mumbo-jumbo that maps low-tech DLL function calls and message codes to high-tech C++ classes and virtual functions. Syntactic sugar is sweet—but the real benefit comes with the implementations. CControlPanelApp and CCPApplet implement all the handlers you need, using static information you give it. When you create a new applet, you give the constructor a resource ID and an MFC runtime class.

AddApplet(new CCPApplet(IDR_MYAPPLET3, 
     RUNTIME_CLASS(CMyPropSheet)));
This is all the information the framework needs to implement your applet. It adds your applet to its list, m_lsApplets. The default handler for CPL_ GETCOUNT then returns the number of applets in the list. When the Control Panel sends CPL_INQUIRE or CPL_ NEWINQUIRE, CCPApplet uses the resource ID to get the icon, name, and description. The name and description are parsed as substrings of the main resource string.

STRINGTABLE PRELOAD DISCARDABLE 
 BEGIN
   IDR_MYAPPLET3  "Intergalactic\n
     Intergalactic settings for space  
     cadets\n\n"
 END
     This is similar to the way MFC uses the IDR_MAINFRAME string to hold several newline-separated substrings for the app name, document type, COM ProgID, and so on. As long as you define your icons and resource strings using the convention, you don't have to implement OnInquire or OnNewInquire. By the way, CPL_NEWINQUIRE is the one innovation since the November 1992 MSJ article. Generally, you should only implement OnInquire. You only need to implement OnNewInquire if your applet information can change from session to session (which is a little weird). If so, you should set CCPApplet::m_bDynamic to TRUE; this tells the framework to return FAILED from CPL_INQUIRE, which makes the Control Panel try CPL_NEWINQUIRE instead. It's a little confusing, which is why it's a good thing you can ignore everything I just said and simply use the resource string.
     When the user double-clicks one of your applet icons in the Control Panel, Windows sends the CPL_DBLCK message. This maps to CCPApplet::OnLaunch, which uses your dialog or property sheet's runtime class to create an instance and call DoModal.

 LRESULT CCPApplet::OnLaunch(CWnd* pWndCpl, 
                            LPCSTR lpCmdLine)
 {
    CWnd* pw = (CWnd*)m_pDialogClass->CreateObject();
    if (pw) {
       if (pw->IsKindOf(RUNTIME_CLASS(CPropertySheet))) {
          CPropertySheet* ps = (CPropertySheet*)pw;
          ps->SetActivePage(lpCmdLine ? atoi(lpCmdLine) : 0);
          ps->DoModal();
       } else if (pw->IsKindOf(RUNTIME_CLASS(CDialog))) {
          CDialog* pd = (CDialog*)pw;
          pd->DoModal();
       }
    }
    return pw==NULL;
 }
     Make sure you declare your dialog or property sheet class with DECLARE_DYNCREATE. If you don't, Create will fail and you'll probably get a friendly TRACE diagnostic from MFC. Also, you must remember to override your dialog or property sheet's OnPostNcDestroy to "delete this". Normally, you create a dialog on the stack

   CMyDialog dlg;
   dlg.DoModal();
so there's no need to delete it. But since CCPApplet creates your dialog or property sheet on the heap, you must delete it after it's destroyed. Otherwise, you'll leak memory.
     In summary, writing a Control Panel extension with CControlPanelApp and CCPApplet is pretty brainless. For normal dialog or property sheet-based applets, the only function you have to override is CControlPanelApp::OnInit to add your applets. Then implement your dialogs. Figure 6 summarizes how the framework handles all the various CPL messages. Figure 7 shows the source.
     Before signing off, there are a few tricks and quirks I should relate. When you've built your Control Panel extension, don't forget to rename it to .cpl and copy it to the Windows System directory. You can add a Post-Build step in your Project Settings to do it. If you'd rather keep your DLL in your own directory, you or your install program can add a line to the MMCPL section of the Windows CONTROL.INI.

 [MMCPL]
 MyPanel=c:\utils\MyPanel\MyPanel.cpl
     Next, as you're building your Control Panel, you might run into the following problem: you add another applet or change the name or icon, but the changes don't appear in the Control Panel. That's because the Control Panel reads your information (CPL_INQUIRE) the first time it encounters your DLL, then caches the information on disk. The surest way to make the Control Panel reread the new information is to rename your DLL. You can also try pressing F5 (Refresh) in the Control Panel, but this didn't always work for me. During development, you can set CCPApplet::m_bDynamic to TRUE, which tells the framework to use CPL_NEWINQUIRE (information not cached) instead of CPL_INQUIRE (information cached). When you're finished debugging and you're ready to release, you can set m_bDynamic=FALSE (the default).
     And speaking of debugging, how do you debug a Control Panel app? There are two ways. You can either launch the Control Panel under the debugger or, if that's too tedious, you can use rundll32.

 rundll32 shell32.dll,Control_RunDLL mypanel.cpl
     You can type this at a command prompt or use it in your Visual C++® Debug Settings. rundll32.exe is a program that calls a function in a DLL; Control_RunDLL is a special function in shell32.dll that runs a Control Panel app. To run a specific applet in your DLL, type

  rundll32   
   shell32.dll,Control_RunDLL 
   mypanel.cpl,@n
where n is the zero-based index of your applet. If you add a string at the end, it gets passed to CPL_ STARTWPARAMS like a command line to a normal Windows app. Usually, this string is used to launch a property sheet-based Control Panel applet to a particular page in the property sheet. For example, to invoke your computer's Display Properties to the Settings tab, type:

   rundll32    
   shell32.dll,Control_RunDLL  
   desk.cpl,,3
     If you ever wondered how some programs launch particular Control Panel applets, now you know. If you use my framework, you don't have to write any code to parse the params; if your applet is a property sheet, CCPApplet automatically interprets the extra arg as a page number.

 // In CCPApplet::OnLaunch
 CWnd* pw = (CWnd*)m_pDialogClass->CreateObject();
 if (pw) {
    if (pw->IsKindOf(RUNTIME_CLASS(CPropertySheet))) {
       CPropertySheet* ps = (CPropertySheet*)pw;
       ps->SetActivePage(lpCmdLine ? 
                            atoi(lpCmdLine) : 0);
       ps->DoModal();
    }
 }
     I can't help but love Control Panel applets. INI files, a hardwired function name, and message codes....They're so retro!

Have a question about programming in C or C++? Send it to Paul DiLascia at askpd@pobox.com

From the November 1999 issue of Microsoft Systems Journal.