A Pictorial Tutorial
After you’ve written your first couple of sidebar gadgets, you may find yourself wanting to access system functionality that isn’t exposed by the gadget APIs. If you know how to do what you want using C++, then all you need is an ActiveX control in order to expose that functionality to your gadget. This blog post will show you how.
If you missed the first parts of this tutorial, you can find them here:
Ok, you’re right – an “Echo” function isn’t really interesting – you could do that with no ActiveX controls at all. What we really want is a function that does something you just can’t do from javascript, or using the built-in gadget APIs. So we’re going to do something else. We are going to add a function that returns a list of all the software installed on the machine.
Let’s get started…
First, there is some C++ book-keeping we have to do. If you’re an old pro with C++, just bear with me while I cover the basics.
Open up the project properties:
Make sure the Platform SDK include files are in our include file path; in particular, we need to make sure we can access MSI.H. You can see that my include files are at “D:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\Include”, but your location may be different, depending on where you installed VS and/or the Platform SDK:

Add a preprocessor definition (“_WIN32_MSI=300”) telling the compiler that we want to support a reasonably-current version of the MSI APIs:

Add the library MSI.LIB to the list of libraries that get linked into your code. Note that the path you give to MSI.LIB will likely be different from my example in the picture, depending on where you installed VS and/or the Platform SDK:

Ok, C++ book-keeping is complete; let’s get back to the code.
Go back to your “ITestControl” interface in the class viewer, right-click on it, and add another method. Call it “GetProducts”, and give it a [retval] parameter of type “VARIANT *”, as follows:

The “VARIANT” type is another one of the tools that you’ll use all the time if you’re writing scriptable activeX controls – you will want to be familiar with them. Unfortunately, I’ve found the material available on the web to be a bit scattered, dated, and otherwise difficult to understand, so feel free to go to our forums at http://www.microsoftgadgets.com/forums if you have questions that the docs don’t answer.
Now go to the implementation stub of the GetProducts function, and fill it in as shown below. Note that this code also demonstrates how to return an array (in particular, a SAFEARRAY), back to the calling script. Unfortunately, this support for arrays is not completely transparent to the script, as well see next.
#include "msi.h"
#define ERROR_CLEANUP(hrError, description) \
{\
AtlReportError(CLSID_TestControl, L"GetProducts: " description); \
hr = hrError; \
goto cleanup; \
}
#define MAX_PRODUCTS 1000
STDMETHODIMP CTestControl::GetProducts(VARIANT* products)
{
WCHAR productCode[300]; // needs to be big enough to hold a GUID in string form: {00000319-0000-0000-C000-000000000046}
LPWSTR productNames[MAX_PRODUCTS];
unsigned int count = 0;
int allocatedProductNameBuffers = 0;
HRESULT hr = E_FAIL;
SAFEARRAY * psa = NULL;
MSIINSTALLCONTEXT context;
UINT enumStatus = MsiEnumProductsEx(
NULL,
NULL,
MSIINSTALLCONTEXT_USERMANAGED | MSIINSTALLCONTEXT_USERUNMANAGED | MSIINSTALLCONTEXT_MACHINE,
0,
productCode,
&context,
NULL,
NULL);
if (enumStatus != ERROR_SUCCESS)
{
return AtlReportError(
CLSID_TestControl,
L"First attempted enumeration of installed products failed",
GUID_NULL, // system-defined error code
HRESULT_FROM_WIN32(enumStatus));
}
while (enumStatus == ERROR_SUCCESS)
{
DWORD productNameLength = 0;
UINT infoStatus = MsiGetProductInfoEx(
productCode,
NULL,
context,
INSTALLPROPERTY_INSTALLEDPRODUCTNAME,
NULL,
&productNameLength);
if (infoStatus != ERROR_SUCCESS)
{
ERROR_CLEANUP(HRESULT_FROM_WIN32(infoStatus), L"failed to get the length of a product name");
}
productNameLength++; // make room for the trailing NULL
WCHAR * productNameBuffer = new WCHAR[productNameLength + 1];
if (productNameBuffer == NULL)
{
ERROR_CLEANUP(E_OUTOFMEMORY, L"failed to allocate product name buffer");
}
productNames[allocatedProductNameBuffers++] = productNameBuffer;
infoStatus = MsiGetProductInfoEx(
productCode,
NULL,
context,
INSTALLPROPERTY_INSTALLEDPRODUCTNAME,
productNameBuffer,
&productNameLength);
if (infoStatus != ERROR_SUCCESS)
{
ERROR_CLEANUP(HRESULT_FROM_WIN32(infoStatus), L"error returned while querying a product name");
}
count++;
if (count == MAX_PRODUCTS)
{
ERROR_CLEANUP(E_FAIL, L"too many products found; couldn't list them all");
}
enumStatus = MsiEnumProductsEx(
NULL,
NULL,
MSIINSTALLCONTEXT_USERMANAGED | MSIINSTALLCONTEXT_USERUNMANAGED | MSIINSTALLCONTEXT_MACHINE,
count,
productCode,
&context,
NULL,
NULL);
}
if (enumStatus != ERROR_NO_MORE_ITEMS)
{
ERROR_CLEANUP(HRESULT_FROM_WIN32(enumStatus), L"failed to enumerate the next product in the product list");
}
psa = SafeArrayCreateVector(VT_VARIANT, 0, count);
if (psa == NULL)
{
ERROR_CLEANUP(E_OUTOFMEMORY, L"could not allocate safe array with SafeArrayCreateVector");
}
VARIANT * v = (VARIANT *)psa->pvData;
// not entirely sure if I need this next loop, but lets be safe
for (unsigned int i = 0; i < count; i++)
{
VariantInit(&v[i]);
}
for (unsigned int i = 0; i < count; i++)
{
v[i].bstrVal = SysAllocString(productNames[i]);
if (v[i].bstrVal == NULL)
{
ERROR_CLEANUP(E_OUTOFMEMORY, L"could not allocate a BSTR for a product name in the return array");
}
v[i].vt = VT_BSTR;
}
products->vt = VT_ARRAY | VT_VARIANT;
products->parray = psa;
hr = S_OK;
cleanup:
for (int i = 0; i < allocatedProductNameBuffers; i++)
{
delete [] productNames[i];
}
if (FAILED(hr))
{
if (psa)
{
SafeArrayDestroy(psa);
}
}
return hr;
}
And there you have it – a fully-functional ActiveX control, callable from script, that actually does something interesting.
What About The Gadget? - Part II
Last but not least, we need to actually call our new function from our gadget, and display the result.
By the way, remember I said that the support for returning arrays from an ActiveX control was less than perfect? Here is where we see why. The array is returned as a SAFEARRAY. The javascript can convert that into something called a VBArray:
var vbArray = new VBArray(safeArray);
And now, we can convert a VBArray into a native javascript array using the toArray() method:
var nativeArray = vbArray.toArray();
To make my life easier, I’ve gone ahead and folded this into a helper function:
function arrayFromSafeArray(safeArray)
{
var vbArray = new VBArray(safeArray);
return vbArray.toArray();
}
So, go ahead and add that helper function to your gadget script, right after where the out() function is defined. Also, replace these lines:
var myControl = new ActiveXObject(“TestActiveX.TestControl”);
out(myControl.Echo(“Bruce Williams”));
With these lines:
var myControl = new ActiveXObject("TestActiveX.TestControl");
var productArray = arrayFromSafeArray(myControl.GetProducts());
for (var i in productArray)
{
out(productArray[i]);
}
And we’re done! Go ahead and add your newly-updated gadget to your sidebar:

Deployment
There is one little detail that I have very cleverly not dealt with in this article, and that is deployment. When you build an ActiveX control in Visual Studio, VS will go to the trouble of registering that control for you – that’s how the gadget knows what to load when you say “new ActiveXObject(‘TestActiveX.TestControl’)”. If you want anyone else to use your gadget, though, you’ve got to get the control installed on their machine, as well as getting the gadget installed. I’m not going to go into the details of how to construct a robust control installer program, but I’ll give you the simple manual steps that I use for testing purposes:
- If VS or the redistributable libraries aren’t already installed on the target machine, you’ll need to install them. This is necessary because our ActiveX control uses the ATL library, so we need to make sure the ATL library is installed. On my machine, the redistributable installer is located at D:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\BootStrapper\Packages\vcredist_x86\vcredist_x86.exe
- After copying the control (TestActiveX.dll) to the target machine, register it as follows; from an elevated command window run: “regsvr32.exe TestActiveX.dll”
- Copy your gadget files (gadget.xml, gadget.html) into a compressed ZIP file named “TestActiveX.zip”, then rename the ZIP file to “TestActiveX.gadget”. Copy this gadget file to the target machine, and double-click on it to install the gadget.
Conclusion
I hope you enjoyed this tutorial, and found it useful. I welcome any feedback and corrections – send them to Bruce.Williams@microsoft.com.