Hosting Control Panel Applets using C#/C++

What does the applet expect?

Now that we have covered a few tricky concepts, let's move back into some functionality discussions. At this point, we can load any applet we want, and call the CplApplet function to communicate with the applet. But what exactly are we going to pass to this function to get the results we want. There are several things that you'll notice Windows presents for applets, by looking at the Windows Control Panel in Windows Explorer. There are icons, and text for all the applets. How does it get those? Answer: By calling the CplApplet function with specific messages and getting specific results back.

Applets are designed (or should be designed) to provide a name and description as well as an icon to be displayed for the user. Let's look at how we can recreate this functionality and talk to our applets. A quick look at the docs on MSDN and we find a set of messages and several structures that are used to communicate with the applet. Another problem is coming, but it's not nearly as rough as any previous ones, but if you didn't know what to do, it could be really a world ending problem. Don't worry I will show you how to deal with it, but let's see what these messages and structures look like first.

To communicate with the applet we will send a message, and optionally a pointer to a structure to receive information back from the applet. The structures are where our last hurdle occurs. Here are the definitions in managed code:

/// <summary>
/// The standard Control Panel Applet Information structure
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct CPLINFO
{
        /// <summary>
        /// The resource Id of the icon the applet wishes to display
        /// </summary>
        public int IconResourceId;
        /// <summary>
        /// The resource Id of the name the applet wishes to display
        /// </summary>
        public int NameResourceId;   
        /// <summary>
        /// The resource Id of the information the applet wishes to display (aka. Description)
        /// </summary>
        public int InformationResourceId;
        /// <summary>
        /// A pointer to applet defined data
        /// </summary>
        public IntPtr AppletDefinedData;   
        /// <summary>
        /// A simple override to display some debugging information about the resource ids returned from each applet
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
                return string.Format(
"IconResourceId: {0},
NameResourceId: {1},
InformationResourceId: {2},
AppletDefinedData: {3}",
IconResourceId.ToString(),
NameResourceId.ToString(),
InformationResourceId.ToString(),
AppletDefinedData.ToInt32().ToString("X"));
        }
}

/// <summary>
/// The advanced Control Panel Applet Information structure
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct NEWCPLINFO
{
        /// <summary>
        /// The size of the NEWCPLINFO structure
        /// </summary>
        public int Size;
        /// <summary>
        /// This field is unused
        /// </summary>
        public int Flags;
        /// <summary>
        /// This field is unused
        /// </summary>
        public int HelpContext;
        /// <summary>
        /// A pointer to applet defined data
        /// </summary>
        public IntPtr AppletDefinedData;
        /// <summary>
        /// A handle to an icon that the applet wishes to display
        /// </summary>
        public IntPtr hIcon;
        /// <summary>
        /// An array of chars that contains the name that the applet wishes to display
        /// </summary>
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)]
        public string NameCharArray;
        /// <summary>
        /// An array of chars that contains the information that the applet wishes to display
        /// </summary>
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst=64)]
        public string InfoCharArray;
        /// <summary>
        /// An array of chars that contains the help file that the applet wishes to display for further help
        /// </summary>
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
        public string HelpFileCharArray;
}

There are really two kickers here. The first is in the CPLINFO structure. Once returned to us, it contains the integer IDs of resources in the applet's resource file that contain either the string for a name or description, or an icon. A quick glance at the docs, I realized we could extract this information using LoadString and LoadImage . However, it states clearly that you should use the MAKEINTRESOURCE macro on the resource ID before passing it to the LoadString or LoadImage functions. I dug through the header files and discovered a really nasty looking conversion. I won't even bring it up, because I think the Windows developers did it just to screw with the rest of the world, a.k.a. anyone not programming in C/C++! It's funknasty and I don't know how to convert it to C#, believe me I tried. Here is what it looks like in the header file:

#define IS_INTRESOURCE(_r) (((ULONG_PTR)(_r) >> 16) == 0)
#define MAKEINTRESOURCEA(i) (LPSTR)((ULONG_PTR)((WORD)(i)))
#define MAKEINTRESOURCEW(i) (LPWSTR)((ULONG_PTR)((WORD)(i)))
#ifdef UNICODE
#define MAKEINTRESOURCE  MAKEINTRESOURCEW
#else
#define MAKEINTRESOURCE  MAKEINTRESOURCEA
#endif // !UNICODE

Now if any of you can translate that to C#, again please clue me in. I'd love to know. I consider myself pretty good at translating, but again I may have just been up too late or had too much Mountain Dew to operate at the level required to translate this. So like my previous solution to the function pointers, I went back to my C DLL and made another wrapper function so that I could just use the real deal and be done with it. Here is what the wrapper functions look like:

HICON APIENTRY LoadAppletIcon(HINSTANCE hInstance, int resId)
{
        return ::LoadIcon(hInstance, MAKEINTRESOURCE(resId));
}

HANDLE APIENTRY LoadAppletImage(HINSTANCE hInstance, int resId, int width, int height)
{
        return ::LoadImage(hInstance, MAKEINTRESOURCE(resId), IMAGE_ICON, width, height, LR_DEFAULTCOLOR);
}

Ok, now that we can load the strings and icons from the resources of the applet, we hit the last of our hurdles. This probably caused me more trouble than any of them, but ended up being the easiest to solve once I understood how to tackle the problem. Like I stated before, the structures above will be used to pass information back to us when we call into the applet. The CPLINFO structure is really straight forward, but the NEWCPLINFO structure is kinda different. Some applets expose dynamic information, based on some changing resource for example. Something like wi-fi or some network or disk resource the description or icon might need to be changed. So we have to refresh the information each time using the NEWCPLINFO structure. As I started translating the structure to C# I discovered the following definition in the docs. A fixed length char[] of predefined length. As you may or may not know you cannot pre-initialize public fields in managed structures. And I didn't understand how to get my structure to map to the real one because of this limitation.

typedef struct tagNEWCPLINFO {
    DWORD dwSize;
    DWORD dwFlags;
    DWORD dwHelpContext;
    LONG_PTR lpData;
    HICON hIcon;
    TCHAR szName[32];
    TCHAR szInfo[64];
    TCHAR szHelpFile[128];
} NEWCPLINFO, *LPNEWCPLINFO;

Take the szName field for our example. How do we define a fixed length array of characters in our structure? The answer lies in the MarshalAs attribute. We can define our field as a string and let the P/Invoke services marshal the data as in a specific format. This is what we can do to get the desired marshalling to occur. We will define our field to be marshaled as an array of null terminated characters with a fixed array size.

[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)]
public string NameCharArray;

A small side note is in the string conversions. Do we use ANSI or Unicode? For the most part Unicode is the data type of choice for strings, but because we don't know ahead of time the real struct uses the TCHAR type which will map to the appropriate type when compiled based on the various #define s in the C/C++ header files. We don't have that luxury, but we do have a solution. We will apply the StructLayout attribute to our structures to define sequential layout of the fields, which will keep the fields in the order we specify, (the CLR tends to optimize structure fields to what it feels is optimal, which can really hose us when dealing with pointers and unmanaged code, so we need to tell it that it needs to leave our fields in the order we define) and also the character set to use for any strings it encounters. This is accomplished by the following:

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct NEWCPLINFO

Having piddled around with the various character sets, I discovered the strings are displayed correctly using the ANSI charset on my Windows XP pro box on down to my 98 test machine. I don't know if this is the right choice, hopefully someone will step up and put a final yes or no to this question. Again I'm completely open for suggestions as this was a learning experience for me as well. I hope to have all of my questions answered and sleep comfortable in my bed at some point in the next 30 years, but that's only going to happen when all of my Windows API questions have been answered. And at the rate I find questions, I'll either have to get a job at Microsoft or steal some of the source to get at the answers. I'm quite frankly open for both at this point. So many hours of my life have been wasted trying to understand the minds of the guys that made those APIs, but again I digress.

Ok, we're nearing the completion of our translations and definitions, so let's look at how to communicate with the applets. The applets expect a certain order of messages. The first call to CPlApplet should initialize the applet and let it know we are loading. The second call will tell us how many applets are actually contained in the .cpl file. This is a one to many relationship as one DLL could have an unlimited number of applets contained inside. The third and fourth calls will inquire the information from the applet. After getting the information we need, we can ask the applet to show its UI by sending it a double click message (don't stress this is normal, and I've made an enumeration for the messages. I renamed them to fit my .NET likings but if you are interested, find the cpl.h header file and look at the CPL_INIT , CPL_GETCOUNT , CPL_INQUIRE , CPL_NEWINQUIRE , CPL_DBLCLK , CPL_CLOSE , CPL_STOP messages for details). Here is the message enumeration that I translated from the MSDN docs:

public enum AppletMessages
{
        Initialize = 1,
        /*  This message is sent to indicate CPlApplet() was found. */
        /*  lParam1 and lParam2 are not defined. */
        /*  Return TRUE or FALSE indicating whether the control panel should proceed. */
        GetCount = 2,
        /*  This message is sent to determine the number of applets to be displayed. */
        /*  lParam1 and lParam2 are not defined. */
        /*  Return the number of applets you wish to display in the control */
        /*  panel window. */
        Inquire = 3,
        /*  This message is sent for information about each applet. */
        /*  A CPL SHOULD HANDLE BOTH THE CPL_INQUIRE AND CPL_NEWINQUIRE MESSAGES. */
        /*  The developer must not make any assumptions about the order or dependance */
        /*  of CPL inquiries. */
        /*  lParam1 is the applet number to register, a value from 0 to */
        /*  (CPL_GETCOUNT - 1).  lParam2 is a far ptr to a CPLINFO structure. */
        /*  Fill in CPLINFO's IconResourceId, NameResourceId, InformationResourceId and AppletDefinedData fields with */
        /*  the resource id for an icon to display, name and description string ids, */
        /*  and a long data item associated with applet #lParam1.  This information */
        /*  may be cached by the caller at runtime and/or across sessions. */
        /*  To prevent caching, see CPL_DYNAMIC_RES, above.  */
        Select = 4,
        /*  The CPL_SELECT message has been deleted. */
        DoubleClick = 5,
        /*  This message is sent when the applet's icon has been double-clicked */
        /*  upon.  lParam1 is the applet number which was selected.  lParam2 is the */
        /*  applet's AppletDefinedData value. */
        /*  This message should initiate the applet's dialog box. */
        Stop = 6,
        /*  This message is sent for each applet when the control panel is exiting. */
        /*  lParam1 is the applet number.  lParam2 is the applet's AppletDefinedData  value. */
        /*  Do applet specific cleaning up here. */
        Exit = 7,
        /*  This message is sent just before the control panel calls FreeLibrary. */
        /*  lParam1 and lParam2 are not defined. */
        /*  Do non-applet specific cleaning up here. */
        NewInquire = 8
        /* Same as CPL_INQUIRE execpt lParam2 is a pointer to a NEWCPLINFO struct. */
        /*  A CPL SHOULD HANDLE BOTH THE CPL_INQUIRE AND CPL_NEWINQUIRE MESSAGES. */
        /*  The developer must not make any assumptions about the order or dependance */
        /*  of CPL inquiries. */
}

There is a lot of stuff I'm skipping because it's kinda dull. I know you all are interested in seeing something running. But if you are curious, read through the comments in my code and the docs in MSDN to see the exact steps the applets are expecting.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“You can stand on the shoulders of giants OR a big enough pile of dwarfs, works either way.”