Component Object Model API  
 

Another approach that can be used to create an instance of an ActiveX control in your C++ program is to use the COM API directly. Generally speaking this option should only be used if absolutely necessary; it is a more complex process and involves more coding than either using CWnd derived controls or the #import directive.

The first step is to create the header file for the interface defined in the control's type library. This will require two tools that are included with Visual C++ and the Microsoft Windows SDK; the COM Object Viewer, and the Microsoft Interface Definition Language (MIDL) compiler. These tools can also be downloaded from Microsoft from their MSDN resources section of the website.

To create the interface definition (IDL) file, start the COM Object Viewer, select the Control folder and then the control you are interested in. For purposes of this example, we'll use the File Transfer Protocol control. Right click on the control and select View Type Information. This will open the ITypeLib Viewer window which contains the interface definition. Select File | Save As and save it as csftpctl.idl in the project directory. Close the viewer window and exit the COM Object Viewer.

Once the IDL file has been created, open the IDL file in the editor and look for a series of enum typedefs which define the constants for the control:

typedef [public]
    _ftpOptionsConstants ftpOptionsConstants;

    typedef enum {
        ftpOptionHttpNocache = 1,
        ftpOptionFtpSecureAuth = 8192
    } _ftpOptionsConstants;

These declarations are a side-effect of how the COM Object Viewer generates the IDL file and it needs to be cleaned up a bit so that the MIDL compiler generates the correct header file. Remove the typedef [public] section before each typedef enum section in the file. In other words, you would want to remove each section that looks like:

typedef [public]
    _ftpOptionsConstants ftpOptionsConstants;

The actual enum typedefs can stay in the IDL so that they're included in the header file and can be used by the application. Note that if these extraneous typedefs aren't removed, the MIDL compiler will generate duplicate enums in the header file and will cause compiler errors.

Save the IDL file and then use the MIDL compiler to generate the header file which will be included with your project. From the command line, enter:

midl /Oicf /W1 /Zp8 /h csftpctl.h /iid csftpctl_i.c csftpctl.idl

This will create three files: csftpctl.h, csftpctl_i.c and csftpctl.tlb. The TLB is the compiled type library and isn't needed for this example. The csftpctl.h header file contains the interface definition for the control, and the csftpctl.c file is a C source file which defines the GUIDs used by the control. Both of these files should be included in your project, typically in the source module where the control will be used.

Now that the header file for the control interface has been created, the next step is to create an instance of the control. Define a member variable that is a pointer to the interface which looks like this:

IFtpClient *m_pIFtpClient;

Safe programming practices would also ensure that the pointer is initialized to NULL in the constructor to avoid potential errors when referencing the variable. As with the previous examples, the COM subsystem must be initialized. For MFC based applications, this can be done by calling AfxOleInit in the InitInstance function for the CWinApp derived application class. For other applications, CoInitializeEx should be called as:

HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if (FAILED(hr))
{
    // Unable to initialize COM subsystem
    return;
}

Then the following code can be used to create an instance of the control:

HRESULT hr;
BSTR bstrLicKey;
IClassFactory2 *pFactory = NULL;
IUnknown *pUnknown = NULL;
USES_CONVERSION;

m_pIFtpClient = NULL;

hr = CoGetClassObject(CLSID_FtpClient, 
                      CLSCTX_INPROC_SERVER,
                      NULL,
                      IID_IClassFactory2,
                      (LPVOID *)&pFactory);

if (FAILED(hr))
{
    // Unable to get the class factory interface for the
    // control, probably because it isn't registered
    EndDialog(0);
    return FALSE;

}

// Create the runtime license key defined in csrtkey10.h
bstrLicKey = SysAllocString(T2OLE(CSTOOLS10_LICENSE_KEY));

// Create an instance of the control
hr = pFactory->CreateInstanceLic(NULL, NULL, IID_IUnknown,
                                 bstrLicKey,
                                 (LPVOID *)&pUnknown);
pFactory->Release();

if (FAILED(hr))
{
    // Unable to create an instance of the control using
    // the specified license key
    EndDialog(0);
    return FALSE;
}

hr = pUnknown->QueryInterface(IID_IFtpClient,
                              (LPVOID *)&m_pIFtpClient);

if (FAILED(hr))
{
    // Unable to get the interface to the control
    EndDialog(0);
    return FALSE;
}

The CoGetClassObject function is used to get an interface pointer to the control's class factory, which actually does the work of creating an instance of the class. The CreateInstanceLic member function passes the runtime license key to the control, and an instance is created if the key is valid. Note that if a NULL value is passed as the license key, then the control will only be created if the system has a development license installed. The interface to the class factory is released and then QueryInterface is called on the returned pointer to obtain the interface to the control's properties and methods.

The code to use the interface is similar to the previous examples, however there are several significant differences:

COleVariant varServerName(m_strServerName);
COleVariant varServerPort(m_nServerPort);
COleVariant varUserName(m_strUserName);
COleVariant varPassword(m_strPassword);
COleVariant varAccount(m_strAccount);
COleVariant varTimeout(m_nTimeout);
COleVariant varLocalFile(m_strLocalFile);
COleVariant varRemoteFile(m_strRemoteFile);
COleVariant varOptions;
COleVariant varError;
HRESULT hr;

hr = m_pIFtpClient->Connect(varServerName,
                            varServerPort,
                            varUserName,
                            varPassword,
                            varAccount,
                            varTimeout,
                            varOptions,
                            &varError);

if (V_I4(&varError) != 0)
{
    CString strError;
    BSTR bstrError;
    USES_CONVERSION;

    hr = m_pIFtpClient->get_LastErrorString(&bstrError);
    if (FAILED(hr))
        return;

    strError.Format(_T("Unable to connect to %s\n%s"),
                    m_strServerName,
                    OLE2T(bstrError));

    AfxMessageBox(strError, MB_ICONEXCLAMATION, 0);
}
else
{
    CString strMessage;
    COleVariant varRestartOffset;
    LONG nBytes = 0;

    hr = m_pIFtpClient->GetFile(varLocalFile, 
                                varRemoteFile, 
                                varRestartOffset,
                                &varError);

    if (V_I4(&varError) != 0)
    {
        CString strError;
        BSTR bstrError;
         USES_CONVERSION;

        hr = m_pIFtpClient->get_LastErrorString(&bstrError);
        if (FAILED(hr))
            return;

        strError.Format(_T("Unable to download %s\n%s"),
                    m_strRemoteFile,
                    OLE2T(bstrError));

        AfxMessageBox(strError, MB_ICONEXCLAMATION, 0);
    }
    else
    {
        hr = m_pIFtpClient->get_TransferBytes(&nBytes);
        if (FAILED(hr))
            return;
        strMessage.Format(_T("Transferred %ld bytes of %s"), 
                           nBytes, m_strRemoteFile);
        AfxMessageBox(strMessage, MB_ICONINFORMATION, 0);
    }
    
    m_pIFtpClient->Disconnect(&varError);
    
    
}

As with the version of the code using the COM smart pointer, p_IFtpClient is a pointer to the interface, which requires that the -> operator be used to access its member functions. Property values are read using accessor functions that are prefixed with "get_", while those which set properties are prefixed with "put_". For example, to get the value of the TransferBytes property, the function name would be get_TransferBytes. Methods in the control are called using the same name.

Another difference is that all of the functions return HRESULT values, with the actual property value or return value from the method specified as a function parameter that is passed by reference. This is why the varError variable is passed as the last argument to the Connect method. If the HRESULT return value is non-zero, this typically will indicate an error. The error may be specific to the control, or it may be a general error coming from the COM subsystem.

Once the application is done using the control, the interface must be released with code like this:

if (m_pIFtpClient)
    m_pIFtpClient->Release();

Each control that is created has a reference count which is used to keep track of how many times one of its interfaces has been requested. When the reference count drops to zero, the control destroys itself and releases the memory that was allocated. Failing to release the interface will prevent the control from ever being destroyed and will result in a memory leak.