Mirror

Control Panel Applets (Views: 792)


Problem/Question/Abstract:

How to develop control panel applets

Answer:

Integrating Configuration Programs with Windows

Control Panel applets are the small programs that are visible in, and run from, Windows Control Panel. They are typically used to configure hardware, the operating system, utility programs, and application software. This article shows you how to create and install your own Control Panel applet.

Why create your own custom Control Panel applet? Many programs you develop require configuration. You probably store the configuration parameters in an .INI file, the registry, or a database. Some programmers add code to their main programs to allow users to display, change, and save configuration parameters, perhaps making it accessible through an Options menu choice.

However, there are many reasons why you should consider placing this code in a separate Delphi project. Placing the configuration code in a separate Delphi project not only makes it more modular, and thus easier to debug, it also makes it more amenable to parallel development within a team of programmers. The separate Delphi project will also exhibit high cohesion and low coupling.

By placing the configuration code in a separate Delphi project, you can more easily prevent end-user access to the code. You may want just an administrator to be able to change the configuration parameters. If this is the case, you could install the compiled project on just the administrator's machine or, if it is on a file server, you could modify the file's execute permission so that only administrators can execute it.

Placing the configuration code in a separate Delphi project allows you to convert it to a Control Panel applet, making it appear more professional and integrated with Windows. Users and administrators are used to looking in Control Panel when they want to configure something. Why should it be any different when it comes to your program?

A Control Panel applet is a special .DLL that resides in the Windows system directory. In this article, we'll discuss implementing a simple dialog box run from an .EXE, converting the dialog box to a .DLL, and converting the .DLL into a special .DLL with the file extension .CPL. (All three projects are available for download; see end of article for details.)

Simple Dialog Box Executable

The first Delphi project builds an executable and uses only one unit/form: AlMnDlg.pas/AlMnDlg.dfm (see Figure 1). The project uses an icon that is different from the default Delphi torch-and-flame icon. The form's name is MainDlg, its BorderStyle property is set to bsDialog, and it contains two buttons. Each button's ModalResult property is set to something other than mrNone. When we're done, this dialog box will be what appears when you activate the applet from the Control Panel.


Figure 1: The Control Panel applet's main dialog box.

Dynamic Link Library

The second Delphi project builds a .DLL and uses the same form as the first project. It also adds a second unit, AlMain.pas, that implements the Execute procedure. The unit's interface section contains the Execute procedure's header so it can be used outside the unit. The procedure uses the stdcall calling convention, because it will be exported from the .DLL. Execute simply creates the dialog box, shows it modally, and destroys it. Its definition is shown here:

procedure Execute; stdcall;
begin
  AlMnDlg.MainDlg := AlMnDlg.TMainDlg.Create(nil);
  try
    AlMnDlg.MainDlg.ShowModal;
  finally
    AlMnDlg.MainDlg.Free;
    AlMnDlg.MainDlg := nil;
  end;
end;

It was easy enough to test the first Delphi project's compiled executable, Applet.exe. All we had to do was run it. To test the second Delphi project's compiled executable, Applet.DLL, we'll have to build a program whose sole purpose is to exercise the .DLL. This is done in AlDriver.dpr. It contains a single form named MainDlg that resides in ADMain.pas/ADMain.dfm, and has a single Execute button (see Figure 2).


Figure 2: The driver executable's main dialog box.

The button's OnClick event handler calls the Execute procedure exported by Applet.DLL:

procedure TMainDlg.ExecuteButtonClick(
  Sender: System.TObject);
begin
  ADMain.Execute;
end;

For the Applet DLL's Execute procedure to be visible in the ADMain unit, it must be imported from the .DLL. Once it's imported, it appears to the rest of the code to actually exist in the unit at the location of the import statement. (This explains why the fully-qualified procedure name, ADMain.Execute, works.) The procedure is statically imported using the external directive:

procedure Execute; external 'Applet.DLL';

Because no path is specified, Windows uses an algorithm to search for the .DLL. One of the directories searched is the Windows system directory. Another is the directory from which the application loaded. In fact, that directory is searched first, and is the preferred directory to use. The search algorithm is documented in win32.hlp (on the Index tab, search on "LoadLibrary").

It's simple to go into AlDriver.dpr's Project | Options menu choice, change to the Directories/Conditionals page, and type the directory path to where Applet.DLL is located in the Output directory combo box. If you do so in your driver project, it will automatically be saved in the same directory as the .DLL whenever your driver executable is built.

To test Applet.DLL, place AlDriver.exe into Applet.DLL's directory if it's not already there. Run AlDriver.exe and click the Execute button. You should see the original dialog box on screen. Because it's shown modally, and both buttons have modal results set to something other than mrNone, clicking either one of them closes (and frees) the dialog box. When you're done testing the .DLL, close AlDriver.exe.

Congratulations! You now know how to place a form in a .DLL. Only a few more steps are required to convert the .DLL into a Control Panel applet.

Control Panel Applet

To test the second Delphi project, we had to build a driver program to load the .DLL, import a subroutine, and execute the subroutine. To test the final Delphi project (the one that builds the Control Panel applet), we won't have to build a driver program. Why? Windows itself will be the driver program. This controlling application is usually Control Panel itself, or the Control Panel folder in Windows Explorer. Like our driver program, the controlling application will have to load the applet, import a subroutine, and execute the subroutine.

To load the Control Panel applet, the controlling application must first find it. The simplest way to allow the controlling application to find the applet is to copy it to the Windows system directory. How does it distinguish between Control Panel applet .DLLs and regular .DLLs? Control Panel applet .DLLs have the extension .CPL - not .DLL. You can make your project automatically use the .CPL extension instead of the .DLL extension. To do so, select Project | Options. On the Application page, type cpl in the Target file extension edit box.

To import a subroutine from a .DLL, the controlling application must use the subroutine's name or index. Windows uses the name, and looks for a function named CPlApplet with the following signature:

LONG APIENTRY CPlApplet(
  HWND hwndCPl, // Handle to Control Panel window.
  UINT uMsg, // Message.
  LONG lParam1, // First message parameter.
  LONG lParam2 // Second message parameter.
  );

The code is shown in C because it's from the Microsoft Help file win32.hlp (refer to the "References" section at the end of this article for more information). The Object Pascal equivalent is:

function CPlApplet(hwndCPl: Windows.THandle;
  uMsg: Windows.DWORD; lParam1, lParam2: System.Longint):
  System.Longint; stdcall;

This function header is declared in cpl.pas. If you have the Professional or Client/Server versions of Delphi, you have access to the source for the cpl.pas unit. You don't need it to create Control Panel applets, but it's heavily commented, and therefore provides good documentation.

Unlike our Execute procedure, the CPlApplet function is called many times and performs multiple functions, depending on what parameter values are passed. The table in Figure 3 shows the possible values for the uMsg parameter. (The information found in this table comes mostly from win32.hlp.)

What
When
Why
CPL.CPL_INIT
Called immediately after the .CPL containing the applet is loaded.
The CPlApplet function should perform initialization procedures, e.g. memory allocation if necessary. If it can't complete the initialization, it should return zero, directing the controlling application to terminate communication, and release the .CPL. If it can complete the initialization, it should return any non-zero value.
CPL.CPL_GETCOUNT
Called after the CPL_INIT function call returns any non-zero value.
The CPlApplet function should return the number of dialog boxes it implements.
CPL.CPL_INQUIRE
Called after the CPL_GETCOUNT function call returns a count greater than, or equal to, 1. The CPlApplet function will be called once for each dialog box, indicating which dialog box with its 0-based index placed in lParam1.
The CPlApplet function should provide information about a specified dialog box. The lParam2 parameter points to a CPLINFO record. The CPlApplet function uses this record to tell the controlling application the applet's name, description, and icon.
CPL.CPL_DBLCLK
Called after the user has chosen the icon associated with a given dialog box.
The CPlApplet function should display the corresponding dialog box, and carry out any user-specified tasks.
CPL.CPL_STOP
Called once for each dialog box before the controlling application closes, indicating which dialog box with its 0-based index placed in lParam1.
The CPlApplet function should free any resources associated with the given dialog box.
CPL.CPL_EXIT
Called after the last CPL_STOP function call and immediately before the controlling application uses the FreeLibrary function to free the .CPL containing the applet.
The CPlApplet function should free any remaining resources, and prepare to close.

Figure 3: Possible values for the CPlApplet parameter, uMsg.

Each CPL_XXX constant is defined in the CPL unit. The example project's CPlApplet function uses these constants (see Figure 4).

function CPlApplet(hwndCPl: Windows.THandle;
  uMsg: Windows.DWORD; lParam1, lParam2: System.Longint):
  System.Longint; stdcall;
const
  NonZeroValue = 1;
begin
  case uMsg of
    CPL.CPL_INIT: Result := NonZeroValue;
    CPL.CPL_GETCOUNT: Result := 1;
    CPL.CPL_INQUIRE:
      case lParam1 of
        0:
          begin
            Result := NonZeroValue;
            CPL.PCPLInfo(lParam2)^.idIcon := AlConst.IIcon;
            CPL.PCPLInfo(lParam2)^.idName := AlConst.SName;
            CPL.PCPLInfo(lParam2)^.idInfo := AlConst.SInfo;
            Result := 0;
          end;
      else
      end;
    CPL.CPL_DBLCLK:
      begin
        Result := NonZeroValue;
        AlMnDlg.MainDlg := AlMnDlg.TMainDlg.Create(nil);
        try
          AlMnDlg.MainDlg.ShowModal;
        finally
          AlMnDlg.MainDlg.Free;
          AlMnDlg.MainDlg := nil;
        end;
        Result := 0;
      end;
    CPL.CPL_STOP: Result := 0;
    CPL.CPL_EXIT: Result := 0;
  end;
end;
Figure 4: An implementation of the applet's exported function CPlApplet.

As the description for CPL_GETCOUNT indicates, it's possible to implement multiple Control Panel applets (i.e. dialog boxes) per .CPL. The example project, however, implements only one.

After you tell Windows how many dialog boxes your .CPL implements, it calls the CPlApplet function again with uMsg equal to CPL_INQUIRE once for each dialog box. The lParam1 parameter tells you which dialog box the function call is for. It will be numbered from 0 to NumberOfDialogBoxes - 1. Because the example project only implements one applet, the CPlApplet function will only be called once so it doesn't handle the cases where lParam1 is other than 0.

The CPLINFO record is defined in win32.hlp as:

typedef struct tagCPLINFO { // cpli
int idIcon;
int idName;
int idInfo;
LONG lData;
} CPLINFO;
  
and in cpl.pas as:

PCPLInfo = ^TCPLInfo;
tagCPLINFO = packed record
  idIcon: System.Integer; // Icon resource id.
  idName: System.Integer; // Name string res. id.
  idInfo: System.Integer; // Info string res. id.
  lData: System.Longint; // User defined data.
end;
CPLINFO = tagCPLINFO;
TCPLInfo = tagCPLINFO;
  
The controlling application allocates memory for this record, and passes your CPlApplet function a pointer to it in the lParam2 parameter. All your function has to do is dereference the pointer, fill in its fields, and return zero. But what should the function fill the record with?

The controlling application needs three things from your applet in order to display it inside the Control Panel properly: an icon, a name, and a description. These three things must be resources linked into your executable with unique identifiers. The record is filled with the resource identifiers. How do you link resources into and use them from your executable? There are five things you must do:

find a suitable icon,
create a text resource file,
compile the text resource file into a binary resource file,
link the binary resource file into your executable, and
use the resources in your Object Pascal code.

The example project uses one of the icons that comes with Delphi, but renames it to Applet.ico. The text resource file, Applet.rc, is shown here:

#include "AlConst.pas"
  
STRINGTABLE
{
  SName, "Applet",
  SInfo, "Test applet"
}
  
IIcon ICON ..\Applet.ico

There are two kinds of resources in this resource file: a string resource (STRINGTABLE), and an icon (ICON) resource. Each string resource has a pair of values: its identifier and its value. The value is shown in double quotes. The identifier is a constant that represents an integer. The constants are defined in the unit AlConst.pas (see Figure 5), which is included within Applet.rc by using the #include directive.

unit AlConst;

interface

const
  SName = 1;
  SInfo = 2;
  IIcon = 3;

implementation

end.
Figure 5: The AlConst.pas file.

The icon resource also has a pair of values: its identifier and the file that contains the icon. The identifier comes from the AlConst unit, just like the string resource identifiers. The file name shown (..\Applet.ico) includes path information because Applet.ico isn't in the same directory as Applet.rc. Now, two of the five tasks required to link in and use resources are finished: finding a suitable icon, and creating a text resource file. What remains is to compile the text resource file into a binary resource file, link the binary resource file into the executable, and use the resources in Object Pascal code.

To compile the text resource file into a binary resource file, use brcc32.exe. This command-line utility comes with Delphi and can be found in the Delphi \Bin directory. Change to the directory that contains Applet.rc and use the following command:

brcc32.exe Applet.rc

This creates an output file in the same directory, and with the same name as the input file Applet.rc, but with the extension .RES. Applet.RES is the binary resource file. You can inspect the file by opening it with the Delphi Image Editor (from the Tools menu).

Linking the binary resource file into the executable is a simple matter of adding a compiler directive to Applet.dpr:

{$R ..\Applet.RES}
  
In the sample project, the Applet.RES file generated from Applet.rc is in the directory immediately above Applet.dpr, hence the ..\ path information in front of the file name. It's a good thing, too, because Delphi automatically generates another Applet.res file in the same directory as the .dpr. This explains the directive you always see in Delphi project files:

{$R *.RES}

The asterisk here means "the same file name as the .dpr," not "any file name."

Now that the binary resource file will be linked into your executable the next time it's recompiled, how do you go about using the resources in Object Pascal? All you have to do now is include the AlConst unit in the uses clause of the unit that needs access to the resource identifiers. In the example project, this is the AlMain unit.

The only other uMsg parameter values that need explanation are CPL_STOP and CPL_EXIT. Because the sample project allocates and deallocates needed memory from within the CPL_DLBCLK case statement block, the CPL_STOP and CPL_EXIT case statements don't have to do anything except indicate success by returning 0.

Conclusion

Windows' open architecture, and Delphi's combination of ease and power, allow you to locate configuration code in custom Control Panel applets. Using custom Control Panel applets makes your application look more professional, polished, and integrated with Windows.

References

The win32.hlp file is part of Microsoft's Windows software development kit. It comes with Delphi, and if you accepted the default locations when you installed Delphi, it can be located at either C:\Program Files\Common Files\Borland Shared\MSHelp if you have Delphi 4 or 5, or at C:\Program Files\Borland\Delphi 3\HELP if you have Delphi 3. Open the file, make sure the Contents tab is selected, and scroll down until you see Control Panel Applications.

The CPL unit found in cpl.pas is a port of cpl.h. It comes with Delphi, and if you accepted the default locations when you installed Delphi, it can be found at either C:\Program Files\Borland\Delphi5\Source\Rtl\Win\cpl.pas if you have Delphi 5 (substitute 4 for 5 if you're using Delphi 4), or at C:\Program Files\Borland\Delphi 3\Source\Rtl\Win\cpl.pas if you have Delphi 3. Another reference from Inprise can be found at http://www.borland.com/devsupport/delphi/faq/FAQ1043D.html, although it seems to be old code (Delphi 2), because it isn't aware of the CPL unit added in Delphi 3. A reference from Microsoft can be found at http: //support.microsoft.com/support/kb/articles/q149/6/48.asp.

<< Back to main page