EXPERT  
VISUAL PROGRAMMING

Develop Composite Controls With ATL 3.0

Visual C++ 6 adds features to ATL that make building composite controls easy and fast. by Ash Rofail

Reprinted with permission from Visual Basic Programmer's Journal, 11/98, Volume 8, Issue 13, Copyright 1998, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange at www.devx.com

In a continued increase of support and commitment to component development, Microsoft is investing in new features across the entire line of Visual Studio tools. Active Template Library (ATL) is no exception. With Visual C++ 6.0 comes a new release: ATL 3.0, which comes loaded with features and enhancements that make developing components easy and flexible. The new version of ATL supports the use of ClassWizards, which speed formerly time-consuming projects. ATL 3.0 not only improves building components in general, but it also boasts new features that support the building of new types of ActiveX controls, such as the composite control.

This article explores the capabilities of the composite control and walks you through building one. It also compares using composite controls to creating ActiveX controls in Visual Basic.

A composite control is a type of ActiveX control that hosts other ActiveX or Windows controls, much like a dialog box does. Once the composite control is built, it acts like a standalone ActiveX control, and can be inserted in any ActiveX-aware container (see Figure 1). Visual C++ 6.0 provides two tools: the ATL COM AppWizard and the ATL Object Wizard. Both automate the creating and implementing of a composite control project. Using them is similar to using the AppWizard to create a Microsoft Foundation Class (MFC) application framework. In this article, I'll show you how to build a composite control called AniProgress, which wraps a progress bar control and an animation control.

Figure 1 Using Composite Controls. To a VB form, you can add a composite control you've created using ATL 3.0. Composite controls let you share abilities across apps without the overhead of a DLL. The composite control shown, AniProgress, wraps a progress bar control and an animation control.

Although ATL 3.0 is loaded with new features, it still has a long way to go to compete with VB for ease of use. VB offers the ActiveX Control Interface Wizard to help create and expose needed interfaces. It automatically preselects standard properties, methods, and events that most controls support, while leaving least common or specific ones out. It allows you to add your own custom member properties, methods, and events. You can map the functionality of properties, methods, and events in your controls to members in constituent controls while setting the attributes of unmapped members.

VB might be friendlier, but ATL is more versatile. ATL allows you flexibility over interfaces of properties, methods, and events. For instance, when creating an ActiveX control in VB, the compiler throws in some properties that you cannot remove but that might cause a problem if the control is to run on a non-VB ActiveX container. One example of this is the PropertyBag. Additionally, when developing ActiveX controls that are to be hosted under Windows CE, all you need to do is turn on a few flags. ATL also allows you command over the control interfaces and marshaling. With VB ActiveX controls, you must ship the VB runtime library, which is about 20 times larger than the ATL runtime dynamic link library (DLL). If you want to build small, compact, and fast ActiveX controls, ATL is a better choice. The downside, of course, is the steep learning curve of C++.

Building a composite control is just like building a dialog box. Until now, if you wanted to share components that have a C++ interface with VB or any other RAD tool, you make a call to a function in C++ and the function does its processing and displays the dialog box for you. Or, the C++ developer can expose that dialog box through a COM interface; when you call from VB, the dialog shows up.

For the most part, that approach worked fine. The problem with it was its overhead. The VB application had to ship the C++ DLL or EXE just to reuse one dialog out of the entire C++ code. So if you have a single feature that you want to reuse in C++, you must create a new project and port that feature into it. The composite control allows you to create a capability as a standalone ActiveX control, with only the interfaces you want to use in VB. So if you have a feature that is better performed in C++ and requires an interface, packaging it in an ActiveX control is the best solution.

In the case of the composite control we're building, we had a small feature in a C++ program that we'd been calling from VB that performs file archiving routines. Both the C++ and the VB application use this feature. We did not want to re-create it in two development languages or carry around a large C++ DLL or EXE just to reuse these features. Instead, we took this functionality in C++ and moved it into a composite control, which both the C++ and VB applications can use. With this solution, we were able to reuse components and keep the COM gods happy.

Using the ATL Object Wizard
Just in case this is your first attempt at using ATL, here are the steps to creating the framework. To create the ATL project, select New from the File menu, and then select the Projects tab. Choose the ATL COM AppWizard item and type the new name into the Project edit box. In the Location edit box, type the location of the new project. Click on OK to start the ATL COM AppWizard. In Step 1 of the ATL COM AppWizard, select Dynamic Link Library (DLL) from the Server Type group. Click on Finish. Click on OK to create the ATL project.

The newly created project is small and contains only a few of the necessary components needed for an ATL project. Currently, it contains H and CPP source files for the proxy initialization module and an IDL source file, which is used to generate the type library and to marshal code.

Now that we have created a framework, we're ready to add our composite control. To access the wizard and add the composite control framework, choose the New ATL Object option from the Insert menu or the ATL Object Wizard button from the ATL toolbar.

Once the ATL Object Wizard appears, choose the Controls option and select Composite Control. Next, you need to provide information on the properties of the composite control. Enter a value for the short name. All other entries are generated automatically, using the short name as the basis.

The Attributes tab page offers options related to the threading model interface and aggregation of the composite control. For our example, accept the default properties. The Stock Properties tab provides a list of common control properties that the composite control can support.

Once you make your selections, click on OK. The ATL Object Wizard then creates five components:

  1. A template for the dialog box that implements the composite control.
  2. A custom resource, REGISTRY, that automatically registers the composite control when invoked.
  3. A C++ class that implements the composite control.
  4. A COM interface, exposed by the composite control.
  5. An HTML test page containing the composite control.

At this point, your composite control project implements the necessary objects. The next step is to add any controls that the composite control will host. Once you insert the necessary controls into the composite control, you can add new abilities. This can go two ways. First, you can support additional interfaces and customize the behavior of the composite control with additional, specific features. Second, you can handle events from the contained controls. We will concentrate on event handling for now, since it is at the heart of making all the controls work together and understanding one another's events.

Handling Events
To handle events for each contained control, implement an event dispatch interface by deriving from the IDispEventImpl template class. You derive this interface in the declaration of the composite control using two parameters: the ID of the control, and the name of the composite control object:

public       
   IDispEventImpl<IDC_CO  NTROL1, 
   CMyCompositeCtrl>

After declaring the dispatch event classes for each contained control, add an event sink map to the composite control object. This relates a control ID to a dispatch interface ID, which the contained control uses for events. It also maps handler functions to events from a contained control. Several predefined macros help implement the event sink map:

  • BEGIN_SINK_MAP(CtrlName): Declares the beginning of the event sink map for the CtrlName composite control.
  • SINK_ENTRY(CtrlID, DispIID, dispID, EventHandler): Declares the handler function (EventHandler) for the specified event (dispID), of the dispatch interface DispIID, for the control identified by CtrlID.
  • END_SINK_MAP(): Declares the end of the event sink map for the CtrlName composite control.

This code example (located in the CPP file of the composite control) declares an event sink map for the CMyCompositeCtrl object. It declares two event handlers for two contained controls (IDC_CONTROL1 and IDC_CONTROL2). The Click event for both controls is handled by the OnClick_control1 and OnClick_control2 functions:

BEGIN_SINK_MAP(CAniProgress)
   //Make sure the Event Handlers have 
   //__stdcall calling convention
   SINK_ENTRY(IDC_PROGCTRL1, 
      DISPID_CLICK,    
      OnClickProgctrl1)
END_SINK_MAP()

Once you declare the event sink map, proceed to implement the event handlers for the composite control.

In the previous step, you added a new map that declared two event handlers for the two contained controls. It is now time to implement these handlers:

retVal __stdcall EventHandler (args)
{
   //handle event firing appropri-
   //ately
}

retVal is the return value of the event handler, EventHandler is the name of the function handler, and args is an optional listing of arguments for the event. This name must match one of the event handlers declared in the composite control's event sink map.

This code example defines an event handler (the Click event) for the contained control identified by IDC_CONTROL1:

void __stdcall OnClick_control1( )
{
   MessageBox("A Click event was fired", 
   "control1", MB_OK);
}

Add event handlers for the remaining entries in the event sink map of your composite control. The ATL Events Wizard will perform these tasks for you, but you should understand how they work so you know how to manipulate them. Invoke the wizard with a right-mouse click on the composite control, and select Events. The wizard will display all the ActiveX control events. Select the events you want to support and the wizard will generate the template code.

Before the composite control becomes fully active, each contained control whose events you are handling is queried for outgoing interfaces, such as an event dispatch interface. If the wizard finds any interfaces, it establishes connections and uses a reference to the outgoing interface to handle events from the control. This procedure is referred to as "advising the control." The composite control object does it automatically, using the event sink map, when the composite control initializes.

After you dismiss the dialog box, the wizard cleans up all stored references to outgoing interfaces from contained controls. This is referred to as "unadvising the control" and is also done automatically by the composite control.

Testing Your Composite Control
One of the initial components of the project is a default HTML page, which hosts your new composite control and resides in the root directory of the project. After you finish modifying the composite control, you need to compile it. After the project successfully builds, load the HTML page into Internet Explorer and test your control's functionality. You can also test your composite control using the Test Container tool, or in any other application that can host an ActiveX control, such as Visual Basic.

Now that it's running, take a look under the hood. The main driving force behind the composite control is a function called GetControls (see Listing 1). GetControls obtains a window handle to the controls contained in the composite control; this handle gets a dispatch pointer to the interfaces of that control. We repeated this process for every control contained in the constituent control. This exposed four properties and three methods that provided us with the functionality we want to reuse (see Listing 2).

Listing 1
// AniProgress.cpp : Implementation of CAniProgress

#include "stdafx.h"
#include "CompCntrl.h"
#include "AniProgress.h"

#define ARCHIVE_UTIL "winzip32.exe "
//CAniProgress


STDMETHODIMP CAniProgress::SetRange(float fMin, float 
   fMax)
{
   // TODO: Add your implementation code here
   m_Progress.SetRange(fMin,fMax);
   return S_OK;
}

STDMETHODIMP CAniProgress::put_AVIFile(BSTR newVal)
{
   // TODO: Add your implementation code here
   m_Animate.SetFileName(newVal);
   return S_OK;
}

STDMETHODIMP CAniProgress::Animate(BOOL bStart)
{
   // TODO: Add your implementation code here
   if(bStart)
      m_Animate.Play();
   else
      m_Animate.Stop();
   return S_OK;
}
STDMETHODIMP CAniProgress::put_Progress(float newVal)
{
   // TODO: Add your implementation code here
   m_Progress.SetValue(newVal);
   return S_OK;
}
void CAniProgress::GetControls()
{
   //gets the IDispatch interface for each of the 
   //constituent controls
   HRESULT hresult=0;
   m_Progress.m_hWnd=GetDlgItem(IDC_PROGCTRL1);
   CAxWindow AxWind(m_Progress.m_hWnd);
   hresult=AxWind.QueryControl(&(m_Progress.m_IDispatch))
      ;

   m_Animate.m_hWnd=GetDlgItem(IDC_ANIMATION1);
   CAxWindow AxAnimWind(m_Animate.m_hWnd);
   hresult=AxAnimWind.QueryControl(&(m_Animate.m_
      IDispatch));
}

STDMETHODIMP CAniProgress::put_Source(BSTR newVal)
{

   WideCharToMultiByte(CP_ACP, 0, newVal, 
      wcslen(newVal)+1, m_szSrc,
      sizeof(m_szSrc), NULL, NULL);

   return S_OK;
}

STDMETHODIMP CAniProgress::put_Dest(BSTR newVal)
{
   WideCharToMultiByte(CP_ACP, 0, newVal, 
      wcslen(newVal)+1, m_szDest,
      sizeof(m_szDest), NULL, NULL);
   return S_OK;
}

STDMETHODIMP CAniProgress::StartArchive()
{
   // TODO: Add your implementation code here
   m_Progress.SetRange(0,100);
   m_Progress.SetValue(0);
   //Launch Archive Utility
   LaunchArchive();
   return S_OK;
}

void CAniProgress::LaunchArchive()
{

   char szCmdLine[260];
   memset(szCmdLine,0,sizeof(szCmdLine));
   if(strlen(m_szSrc)+strlen(m_szDest)>sizeof(szCmdLine))
   {
      MessageBox("Paths are too long", 
         "Error running archive",MB_OK); return;
   }

   strcpy(szCmdLine,ARCHIVE_UTIL);
   strcat(szCmdLine,(LPCSTR)m_szDest);
   strcat(szCmdLine," ");
   strcat(szCmdLine,(LPCSTR)m_szSrc);

   STARTUPINFO si;
   PROCESS_INFORMATION pi;   
   ZeroMemory( &si, sizeof(si) );
   si.cb = sizeof(si);   
   if( !CreateProcess( NULL,(LPSTR) szCmdLine,NULL,NULL, 
      FALSE, 0,NULL, NULL, &si,&pi ) )   
   {
      MessageBox("Could not run archive utility", 
         "Error running archive",MB_OK); return;
   }
   
   m_Animate.Play();
   
   DWORD ExitCode=0;
   GetExitCodeProcess(pi.hProcess,&ExitCode);
   float fProgress=10.0f;
   while(STILL_ACTIVE == ExitCode)
   {
      if(fProgress > 100)
         fProgress=0;
      m_Progress.SetValue(fProgress);
      fProgress+=10.0f;
      Sleep(1000); //wait 1 second
      GetExitCodeProcess(pi.hProcess,&ExitCode);
   }

   CloseHandle( pi.hProcess );

   m_Animate.Stop();
}

Listing 2
// CompCntrl.idl : IDL source for CompCntrl.dll
//

// This file will be processed by the MIDL tool to
// produce the type library (CompCntrl.tlb) and 
//marshalling code.

import "oaidl.idl";
import "ocidl.idl";
#include "olectl.h"
   

   [
      object,
      uuid(98F50A3D-18FF-11D2-8E83-00104B9B15F0),
      dual,
      helpstring("IAniProgress Interface"),
      pointer_default(unique)
   ]
   interface IAniProgress : IDispatch
   {
      [id(1), helpstring("method SetRange")] HRESULT 
         SetRange(float fMin, float fMax);
      [id(2), helpstring("method Animate")] HRESULT 
         Animate(BOOL bStart);
      [propput, id(3), helpstring("property Progress")] 
         HRESULT Progress([in] float newVal);
      [propput, id(4), helpstring("property AVIFile")] 
         HRESULT AVIFile([in] BSTR newVal);
      [propput, id(5), helpstring("property Source")] 
         HRESULT Source([in] BSTR newVal);
      [id(7), helpstring("method StartArchive")] HRESULT 
         StartArchive();
      [propput, id(8), helpstring("property Dest")] 
         HRESULT Dest([in] BSTR newVal);
   };

[
   uuid(98F50A31-18FF-11D2-8E83-00104B9B15F0),
   version(1.0),
   helpstring("CompCntrl 1.0 Type Library")
]
library COMPCNTRLLib
{
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");

   [
      uuid(98F50A3E-18FF-11D2-8E83-00104B9B15F0),
      helpstring("AniProgress Class")
   ]
   coclass AniProgress
   {
      [default] interface IAniProgress;
   };
};
The first method is Animate, which is a Boolean property that indicates whether we enabled animation during the archiving process. For example, with animation enabled, the end user will see files flying between folders while copying them from one place to another. The second method is SetRange, which sets the range for the progress bar. The final method is StartArchive, which calls up the local WinZip applications. The properties that are exposed for the controls are Source and Dest, which designate a path for the source file and the destination of the archived file.

As you can see, creating a composite control rather than a dialog box can be easy and straightforward. So the next time you need abilities from Visual C++, provide it as an ActiveX composite control.


Ash Rofail is a principal software engineer and UI architect at Best Software. A frequent contributor to VBPJ, he specializes in VB, C++, SQL Server, and Java. Ash is currently writing a book about developing apps in VB and COM. You can reach him at Ash_Rofail@BestSoftware.com.