Creating Win32 Control Panel Applets with Visual C++

Nigel Thompson
Microsoft Developer Network Technology Group

July 24, 1995

Click to open or copy the files in the NTCPLAPP sample application for this technical article.

Abstract

This article describes a simple step-by-step process for building Win32® Control Panel applets using Microsoft® Visual C++™ and a single C++ class module. The accompanying NTCPLAPP sample application was built using Microsoft Visual C++ version 2.2 and tested on Microsoft Windows NT™ version 3.51 and Microsoft Windows® 95.

Introduction

Creating Control Panel applets is not something most of us do very often. I found the need to create one while building a Microsoft® Windows NT™ service application. In order to manage my service, I needed some sort of user interface, and putting that in the Control Panel seemed the logical thing to do. Creating Control Panel applets, as they are called, does involve quite a bit of digging through the documentation to find out exactly what you need to do. I've been through that process and have built a single C++ class (CControlPanel) to do the grunt work for you. This article describes the steps you need to follow to build your own applet. I kept my sample very simple, providing only one applet in the sample. If you want to modify it to support multiple applets, you should find that relatively easy to do. One warning about the code in CControlPanel: Because the code sample uses a global C++ object pointer, you cannot have more than one object (CControlPanel) in the applet you create. This should not be a limitation because one CControlPanel object can support multiple applets.

The Steps

My application was created in C:\APPS\NTCPLAPP2 and the target applet was copied to C:\NT351\SYSTEM23. Obviously, your own development configuration might be different from mine, so bear this in mind while following these instructions.

  1. Start Visual C++™ and create a new project. I called mine MyCplApp. Set the project type to "MFC AppWizard (dll)" and choose the option "Use MFC in a static library." This will create a dynamic-link library (DLL) that is not dependent on any DLLs other than those that ship with every system.

  2. From the Project Settings menu, set the link options so that the output file is created in your <systempath>\SYSTEM32 directory. This prevents your having to copy the applet for testing every time you make a change. Set the output file extension to .CPL. My destination entry was: C:\NT351\SYSTEM32\MYCPLAPP.CPL.

  3. From the Project Settings menu, set the debug options as follows:

    Executable: <systempath>/SYSTEM32/CONTROL.EXE
    Working Directory: C:\APPS\NTCPLAPP2  (your source code directory)
    Additional DLLs: <systempath>/SYSTEM32/MYCPLAPP.CPL  (your executable)

    When you click the Run button, the Control Panel will start and then load your applet, so you can set breakpoints and debug it.

  4. Copy the CTRLPAN.CPP and CTRLPAN.H files from the NTCPLAPP sample to your new project. From the Project Files menu, add CTRLPAN.CPP to your project.

  5. Edit the main application header file (MYCPLAPP.H) and add the headers and a Control Panel object as shown in the code here:
    // MYCPLAPP.H : Main header file for MYCPLAPP.DLL
    //
    
    #ifndef __AFXWIN_H__
    #error include 'stdafx.h' before including this file for PCH
    #endif
    #ifdef IDC_STATIC      // <-- You add this.
    #undef IDC_STATIC      // <-- You add this.
    #endif                 // <-- You add this.
    #include "resource.h"       // <-- You add this.
    #include "mypanel.h"        // <-- You add this.
    #include "mydialog.h"       // <-- You add this.
    
    /////////////////////////////////////////////////////////////////////////////
    // CMyCplAppApp
    // See MYCPLAPP.CPP for the implementation of this class.
    //
    
    class CMyCplAppApp : public CWinApp
    {
    public:
    CMyCplAppApp();
    
    // Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CMyCplAppApp)
    //}}AFX_VIRTUAL
    
    //{{AFX_MSG(CMyCplAppApp)
    // NOTE - ClassWizard will add and remove member functions here.
    //    DO NOT EDIT what you see in these blocks of generated code!
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
    
    // local data         // <-- You add this.
    CMyPanel m_Control;   // <-- You add this.
    };
    
  6. Create a new class, CMyPanel, derived from CControlPanel in two new files: MYPANEL.H and MYPANEL.CPP. Add MYPANEL.CPP to the project. Here's MYPANEL.H from the NTCPLAPP sample:
    // MYPANEL.H
    
    #include "ctrlpan.h"
    
    class CMyPanel : public CControlPanel
    {
    public:
    virtual LONG OnInquire(UINT uAppNum, NEWCPLINFO* pInfo); 
    virtual LONG OnDblclk(HWND hwndCPl, UINT uAppNum, LONG lData); 
    };
    

    As you can see, it's very simple and overrides only two member functions from CMyPanel. We'll look at the implementation of MYPANEL.CPP later.

  7. Open the project's .DEF file (MYCPLAPP.DEF) and add an export statement for the CPlApplet function:
    ; MYCPLAPP.DEF : Declares the module parameters for the DLL.
    
    LIBRARY      MYCPLAPP
    DESCRIPTION  'MYCPLAPP Windows Dynamic Link Library'
    
    EXPORTS
    ; Explicit exports can go here
    CPlApplet
    
  8. Open the project's .RC file (MYCPLAPP.RC) and use the Resource - New menu to add a new icon. Create the icon for your Control Panel applet. I gave mine this ID: IDI_MYICON.

  9. Use the Resource - New menu again to add the dialog box for your applet. I gave mine this ID: IDD_MYDIALOG.

  10. Run ClassWizard and create a class for your dialog box. I called mine CMyDialog in the MYDIALOG.H and MYDIALOG.CPP files.

    Add member variables as needed. I added a single integer value, so there would be something simple to show in the sample.

  11. In your CMyPanel implementation file (MYPANEL.CPP), you must now implement the OnInquire and OnDblclk member functions. You may also want to use OnInit and OnStop. (See the discussion later in the "Implementing OnGetCount, OnInit, and OnStop" section for more details on these functions.)

  12. Update all the dependencies on your project and build it. If it builds OK, you should be able to click the Run button and see the Windows Control Panel start. The Control Panel will load all the applets, including yours. If you set breakpoints in your code, they should be hit now.

Implementing OnInquire and OnDblclk

To implement even the simplest applet, you must override CControlPanel::OnInquire and CControlPanel::OnDblclk. OnInquire is called to get information about your icon, caption, and so on. Here's how I implemented it for the sample:

LONG CMyPanel::OnInquire(UINT uAppNum, NEWCPLINFO* pInfo) 
{
    // Fill in the data.
    pInfo->dwSize = sizeof(NEWCPLINFO); // Important
    pInfo->dwFlags = 0;
    pInfo->dwHelpContext = 0;
    pInfo->lData = 0;
    pInfo->hIcon = ::LoadIcon(AfxGetResourceHandle(),
                              MAKEINTRESOURCE(IDI_MYICON));
    strcpy(pInfo->szName, "My Applet");
    strcpy(pInfo->szInfo, "My Control Panel Applet");
    strcpy(pInfo->szHelpFile, "");
    return 0; // OK (Don't send CPL_INQUIRE msg)
}

The entries you might change are the icon ID (if yours isn't IDI_MYICON), the name, and the information string that appears in the Control Panel status bar when your applet is selected. You might also create a Help file, in which case you need to supply its filename (and possibly its path, if it isn't in the same location as the applet) in the szHelpFile field.

The implementation of OnDblclk is a bit longer because it really does all the work of the applet. It essentially does three things:

  1. Loads the current state. (I saved mine in the registry.)

  2. Shows the dialog box with the current state and allows the user to make changes.

  3. Saves or discards the new information (OK or Cancel operations).

Here's how I did this for the sample:

LONG CMyPanel::OnDblclk(HWND hwndCPl, UINT uAppNum, LONG lData)
{
    // Create the dialog box using the parent window handle.
    CMyDialog dlg(CWnd::FromHandle(hwndCPl));

    // Set the default value.
    dlg.m_iMyValue = 0;

    // Read the current state from the registry.
    // Try opening the registry key:
    // HKEY_CURRENT_USER\Control Panel\<AppName>
    HKEY hcpl;
    if (RegOpenKeyEx(HKEY_CURRENT_USER,
                     "Control Panel",
                     0,
                     KEY_QUERY_VALUE,
                     &hcpl) == ERROR_SUCCESS) {
        HKEY happ;
        if (RegOpenKeyEx(hcpl,
                         "MyPanel",
                         0,
                         KEY_QUERY_VALUE,
                         &happ) == ERROR_SUCCESS) {
            // Yes we are installed
            DWORD dwType = 0;
            DWORD dwSize = sizeof(dlg.m_iMyValue);
            RegQueryValueEx(happ,
                            "MyValue",
                            NULL,
                            &dwType,
                            (BYTE*)&dlg.m_iMyValue,
                            &dwSize);
            RegCloseKey(happ);
        }
        RegCloseKey(hcpl);
    }

    // Show the dialog box.
    if (dlg.DoModal() != IDOK) return 0;

    // Update the registry.
    // Try creating/opening the registry key.
    if (RegOpenKeyEx(HKEY_CURRENT_USER,
                     "Control Panel",
                     0,
                     KEY_WRITE,
                     &hcpl) == ERROR_SUCCESS) {
        HKEY happ;
        DWORD dwDisp;
        if (RegCreateKeyEx(hcpl,
                           "MyPanel",
                           0,
                           "",
                           REG_OPTION_NON_VOLATILE,
                           KEY_WRITE,
                           NULL,
                           &happ,
                           &dwDisp) == ERROR_SUCCESS) {
            // Set the value.
            RegSetValueEx(happ,
                          "MyValue",
                          0,
                          REG_DWORD,
                          (BYTE*)&dlg.m_iMyValue,
                          sizeof(dlg.m_iMyValue));

            // Finished with keys.
            RegCloseKey(happ);
        }
        RegCloseKey(hcpl);
    }

    return 0;
}

This looks a bit complex, but most of it is dealing with opening registry keys. For this sample, I chose to keep the data in the registry under HKEY_CURRENT_USER\Control Panel\<AppName> and used a key called MyValue to store the single value the dialog box allows the user to vary. So this sample stores data on a per-user basis. In other words, different users could use this dialog box to set personal preferences. If your need is for something user-independent, you probably want to store your data under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\<yourservice>.

Implementing OnGetCount, OnInit, and OnStop

If your Control Panel utility contains more than one applet, you'll need to override OnGetCount to report how many applets you have, and your OnInquire function will need to respond once for each applet. You may also have some global memory allocation to do. If so, do this in the OnInit function, because this is the only place your applet can fail and have the Control Panel respond sensibly (that is, by not running the applet). Use the OnStop function to clean up any global allocations.

You should note that the Control Panel will run your applet once when the Control Panel initially starts up in order to get a count of the applets and the icons for each applet. It will then run your applet again if the user double-clicks its icon. That's why my sample does all its state reading in the OnDblclk function. It's redundant to do it in the OnInit function because this means doing all the work reading the state information just to show the icon, as well as showing the dialog box if the applet is actually run.

Reference Material

The Control Panel uses the single entry point, CPlApplet, to communicate with your applet. It sends CPL_DBLCLK, CPL_EXIT, CPL_GETCOUNT, CPL_INIT, CPL_INQUIRE, CPL_NEWINQUIRE, CPL_SELECT and CPL_STOP messages to this function. For more information, see the Win32® SDK documentation for these messages.