Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WTL for MFC Programmers, Part VI - Hosting ActiveX Controls

0.00/5 (No votes)
10 Jan 2006 1  
A tutorial on hosting ActiveX controls in WTL dialogs.

Contents

Introduction

Here in Part VI, I'll cover ATL's support for hosting ActiveX controls in dialogs. Since ActiveX controls are ATL's specialty, there are no additional WTL classes involved. However, the ATL way of hosting is rather different from the MFC way, so this is an important topic to cover. I will cover how to host controls and sink events, and develop an application that loses no functionality compared to an MFC app written with ClassWizard. You can, naturally, use the ATL hosting support in the WTL apps that you write.

The sample project for this article demonstrates how to host the IE WebBrowser control. I chose the browser control for two good reasons:

  1. Everybody has it on their computers, and
  2. It has many methods and fires many events, so it's a good control to use for demonstration purposes.

I certainly can't compete with folks who have spent tons of time writing custom browsers around the WebBrowser control. However, after reading through this article, you'll know enough to start working on a custom browser of your own.

Starting with the AppWizard

Creating the project

The WTL AppWizard can create an app for us that is ready to host ActiveX controls. We'll start with a new project, this one will be called IEHost. We'll use a modeless dialog as in the last article, only this time check the Enable ActiveX Control Hosting checkbox as shown here:

 [AppWizard - 22K]

 [AppWizard - 25K]

Checking that box makes our main dialog derive from CAxDialogImpl so it can host ActiveX controls. In the VC 6 wizard, there is another checkbox on page 2 labeled Host ActiveX Controls, however checking that seems to have no effect on the resulting code, so you can click Finish from page 1.

The generated code

In this section, I'll cover some new pieces of code that we haven't seen before from the AppWizard. In the next section, I'll cover the ActiveX hosting classes in detail.

The first file to check out is stdafx.h, which has these includes:

#include <atlbase.h>

#include <atlapp.h>

 
extern CAppModule _Module;
 
#include <atlcom.h>

#include <atlhost.h>

#include <atlwin.h>

#include <atlctl.h>

// .. other WTL headers ...

atlcom.h and atlhost.h are the important ones. They contain the definitions of some COM-related classes (like the smart pointer CComPtr), and the window class used to host controls.

Next, look at the declaration of CMainDlg in maindlg.h:

class CMainDlg : public CAxDialogImpl<CMainDlg>,
                 public CUpdateUI<CMainDlg>,
                 public CMessageFilter, public CIdleHandler

CMainDlg is now derived from CAxDialogImpl, which is the first step in enabling the dialog to host ActiveX controls.

Finally, there's one new line in WinMain():

int WINAPI _tWinMain(...)
{
//...

    _Module.Init(NULL, hInstance);
 
    AtlAxWinInit();
 
    int nRet = Run(lpstrCmdLine, nCmdShow);
 
    _Module.Term();
    return nRet;
}

AtlAxWinInit() registers a window class called AtlAxWin. This is used by ATL when it creates the container window for an ActiveX control.

Due to a change in ATL 7, you have to pass a LIBID to _Module.Init(). Some folks on the forum suggested using this code on VC 7:

    _Module.Init(NULL, hInstance, &LIBID_ATLLib);

This change has worked fine for me.

Adding Controls with the Resource Editor

ATL lets you add ActiveX controls to a dialog using the resource editor, just as you can in an MFC app. First, right-click in the dialog editor and select Insert ActiveX control:

 [Insert menu - 8K]

VC will show a list of the controls installed on your system. Scroll down to Microsoft Web Browser and click OK to insert the control into the dialog. View the properties of the new control and set its ID to IDC_IE. The dialog should now look like this, with the control visible in the editor:

 [ IE control in editor - 6K]

If you compile and run the app now, you'll see the WebBrowser control in the dialog. However, it will be blank, since we haven't told it to navigate anywhere.

In the next section, I'll cover the ATL classes involved in creating and hosting ActiveX controls, and then we'll see how to use those classes to communicate with the browser.

ATL Classes for Control Hosting

When hosting ActiveX controls in a dialog, there are two classes that work together: CAxDialogImpl and CAxWindow. They handle all the interfaces that control containers have to implement, and provide some utility functions for common actions such as querying the control for a particular COM interface.

CAxDialogImpl

The first class is CAxDialogImpl. When you write your dialog class, you derive it from CAxDialogImpl, instead of CDialogImpl, to enable control hosting. CAxDialogImpl has overrides for Create() and DoModal(), which call through to the global functions AtlAxCreateDialog() and AtlAxDialogBox() respectively. Since the IEHost dialog is created with Create(), we'll take a closer look at AtlAxCreateDialog().

AtlAxCreateDialog() loads the dialog resource and uses the helper class _DialogSplitHelper to iterate through the controls and look for special entries created by the resource editor that indicate an ActiveX control needs to be created. For example, here is the entry in the IEHost.rc file for the WebBrowser control:

CONTROL "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}",
        WS_TABSTOP,7,7,116,85

The first parameter is the window text (an empty string), the second is the control ID, and the third is the window class name. _DialogSplitHelper::SplitDialogTemplate() sees that the window class begins with '{' and knows it's an ActiveX control entry. It creates a new dialog template in memory that has those special CONTROL entries replaced by ones that create AtlAxWin windows instead. The new entry is the in-memory equivalent of:

CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}", 
         IDC_IE, "AtlAxWin", WS_TABSTOP, 7, 7, 116, 85

The result is that an AtlAxWin window will be created with the same ID, and its window text will be the GUID of the ActiveX control. So if you call GetDlgItem(IDC_IE), the return value is the HWND of the AtlAxWin window, not the ActiveX control itself.

Once SplitDialogTemplate() returns, AtlAxCreateDialog() calls CreateDialogIndirectParam() to create the dialog using the modified template.

AtlAxWin and CAxWindow

As mentioned above, an AtlAxWin is used as the container window for an ActiveX control. There is a special window interface class that you use with an AtlAxWin called CAxWindow. When an AtlAxWin is created from a dialog template, the AtlAxWin window procedure, AtlAxWindowProc(), handles WM_CREATE and creates the ActiveX control in response to that message. An ActiveX control can also be created at runtime, without being in the dialog template, but we'll cover that case later.

The WM_CREATE handler calls the global AtlAxCreateControl(), passing it the AtlAxWin's window text. Recall that this was set to the GUID of the WebBrowser control. AtlAxCreateControl() calls a couple more functions, but eventually the code reaches CreateNormalizedObject(), which converts the window text to a GUID, and finally calls CoCreateInstance() to create the ActiveX control.

Since the ActiveX control is a child of the AtlAxWin, the dialog can't directly access the control. However, CAxWindow has methods for communicating with the control. The one you'll use most often is QueryControl(), which calls QueryInterface() on the control. For example, you can use QueryControl() to get an IWebBrowser2 interface from the WebBrowser control, and use that interface to navigate the browser to a URL.

Calling the Methods of a Control

Now that our dialog has a WebBrowser in it, we can use its COM interfaces to interact with it. The first thing we'll do is make it navigate to a new URL using its IWebBrowser2 interface. In the OnInitDialog() handler, we can attach a CAxWindow variable to the AtlAxWin that is hosting the browser.

BOOL CMainDlg::OnInitDialog()
{
CAxWindow wndIE = GetDlgItem(IDC_IE);

Next, we declare an IWebBrowser2 interface pointer and query the browser control for that interface, using CAxWindow::QueryControl():

CComPtr<IWebBrowser2> pWB2;
HRESULT hr;
 
  hr = wndIE.QueryControl ( &pWB2 );

QueryControl() calls QueryInterface() on the WebBrowser, and if that succeeds, the IWebBrowser2 interface is returned to us. We can then call Navigate():

  if ( pWB2 )
    {
    CComVariant v;  // empty variant

 
    pWB2->Navigate ( CComBSTR("http://www.codeproject.com/"), 
                     &v, &v, &v, &v );
    }

Sinking Events Fired by a Control

Getting an interface from the WebBrowser is pretty simple, and it lets us communicate in one direction - to the control. There is also a lot of communication from the control, in the form of events. ATL has classes that encapsulate connection points and event sinking, so that we can receive the events fired by the browser. To use this support, we need to do four things:

  1. Add IDispEventImpl to CMainDlg's inheritance list
  2. Write an event sink map that indicates which events we want to handle
  3. Write handlers for those events
  4. Connect the control to the sink map (a process called advising)

The VC IDE helps out greatly in this process - it will make the changes to CMainDlg for you, as well as query the ActiveX control's type library to show a list of the events that the control fires. Since the UI for adding handlers is different in VC 6 and VC 7, I'll cover each separately.

Adding handlers in VC 6

There are two ways to get to the UI for adding handlers:

  1. In the ClassView pane, right-click CMainDlg and choose Add Windows Message Handler on the menu.
  2. When viewing the CMainDlg code or the associated dialog in the resource editor, click the dropdown arrow on the WizardBar action button, then choose Add Windows Message Handler on the menu.

After choosing that command, VC shows a dialog with a list of controls, labeled Class or object to handle. Select IDC_IE in that list, and VC will fill in the New Windows messages/events list with the events that the WebBrowser control fires.

 [Event list in VC6 - 21K]

We'll add a handler for the DownloadBegin event, so select that event and click the Add and Edit button. VC will prompt you for the method name:

 [Setting method name - 8K]

The first time you add an event handler, VC will make a few changes to CMainDlg, enabling it to be an event sink. The changes are a bit spread out over the header file; they are summarized in the snippet below:

#import "C:\WINNT\System32\shdocvw.dll"
 
class CMainDlg : public CAxDialogImpl<CMainDlg>,
                 public CUpdateUI<CMainDlg>,
                 public CMessageFilter, public CIdleHandler,
                 public IDispEventImpl<IDC_IE, CMainDlg>
{
// ...

public:
  BEGIN_SINK_MAP(CMainDlg)
    SINK_ENTRY(IDC_IE, 0x6a, OnDownloadBegin)
  END_SINK_MAP()
 
  void __stdcall OnDownloadBegin()
    {
    // TODO : Add Code for event handler.

    }
};

The #import statement is a compiler directive to read the type library in shdocvw.dll (the file that has the implementation of the WebBrowser ActiveX control) and create wrapper classes for using the coclasses and interfaces in that control. You would normally move this directive to stdafx.h, however in this case we don't need it at all since the Platform SDK already has header files that contain the WebBrowser interfaces and methods.

The inheritance list now has IDispEventImpl, which has two template parameters. The first is the ID that we assigned to the ActiveX control, IDC_IE, and the second is the name of the class deriving from IDispEventImpl.

The sink map is delimited by the BEGIN_SINK_MAP and END_SINK_MAP macros. Each SINK_ENTRY macro lists one event that CMainDlg will handle. The parameters to the macro are the control ID (IDC_IE again), the dispatch ID of the event, and the name of the function to call when the event is received. VC reads the dispatch ID from the ActiveX control's type library, so you don't have to worry about figuring out what the numbers are. (If you look in the exdispid.h header file, which lists IDs for various events sent by IE and Explorer, you'll see that 0x6A corresponds to the constant DISPID_DOWNLOADBEGIN.)

Finally, there is the new method OnDownloadBegin(). Some events have parameters; for those that do, VC will set up the method to have the proper prototype. All event handlers have the __stdcall calling convention since they are COM methods.

Adding handlers in VC 7

There are again two ways to add an event handler. You can right-click the ActiveX control in the dialog editor and pick Add Event Handler on the menu. This brings up a dialog where you select the event name and set the name of the handler.

 [Event list in VC6 - 24K]

Clicking the Add and Edit button will add the handler, make necessary changes to CMainDlg, and open the maindlg.cpp file with the new handler highlighted.

The other way is to view the property page for CMainDlg, and expand the Controls node, then the IDC_IE node. Under IDC_IE, you'll find the events that control fires.

 [Adding event handler thru properties list - 12K]

You can click the arrow next to an event name and pick <Add> [MethodName] on the menu to add the handler. You can modify the handler's name later by changing it right in the property page.

VC 7 makes almost the same changes to CMainDlg as VC 6, the exception being that no #import directive is added.

Advising for events

The last step is to advise the control that CMainDlg wants to sink (that is, receive) events fired by the WebBrowser control. Again, this process differs in VC 6 and VC 7, so I will cover each separately. In both cases, advising occurs in OnInitDialog(), and unadvising occurs in OnDestroy().

Advising in VC 6

ATL in VC 6 has a global function AtlAdviseSinkMap(). The function takes a pointer to a C++ object that has a sink map (which will usually be this), and a boolean. If the boolean is true, the object wants to start receiving events. If it's false, the object wants to stop receiving them. AtlAdviseSinkMap() advises all controls in the dialog to start or stop sending events to the C++ object.

To use this function, add handlers for WM_INITDIALOG and WM_DESTROY, then call AtlAdviseSinkMap() like this:

BOOL CMainDlg::OnInitDialog(...)
{
  // Begin sinking events

  AtlAdviseSinkMap ( this, true );
}
 
void CMainDlg::OnDestroy()
{
  // Stop sinking events

  AtlAdviseSinkMap ( this, false );
}

AtlAdviseSinkMap() returns an HRESULT that indicates whether the advising succeeded. If AtlAdviseSinkMap() fails in OnInitDialog(), then you won't get events from some (or all) of the ActiveX controls.

Advising in VC 7

In VC 7, CAxDialogImpl has a method called AdviseSinkMap() that is a wrapper for AtlAdviseSinkMap(). AdviseSinkMap() takes one boolean parameter, which has the same meaning as the second parameter to AtlAdviseSinkMap(). AdviseSinkMap() checks that the class has a sink map, then calls AtlAdviseSinkMap().

The big difference compared to VC 6 is that CAxDialogImpl has handlers for WM_INITDIALOG and WM_DESTROY that call AdviseSinkMap() for you. To take advantage of this feature, add a CHAIN_MSG_MAP macro at the beginning of the CMainDlg message map, like this:

  BEGIN_MSG_MAP(CMainDlg)
    CHAIN_MSG_MAP(CAxDialogImpl<CMainDlg>)
    // rest of the message map...

  END_MSG_MAP()

Overview of the Sample Project

Now that we've seen how event sinking works, let's check out the complete IEHost project. It hosts the WebBrowser control, as we've been discussing, and handles six events. It also shows a list of the events, so you can get a feel for how custom browsers can use them to provide progress UI. The program handles the following events:

  • BeforeNavigate2 and NavigateComplete2: These events let the app watch as the WebBrowser navigates to URLs. You can cancel navigation if you want in response to BeforeNavigate2.
  • DownloadBegin and DownloadComplete: The app uses these events to control the "wait" message, which indicates that the browser is working. A more polished app could use an animation, similar to what IE itself uses.
  • CommandStateChange: This event tells the app when the Back and Forward navigation commands are available. The app enables or disables the back and forward buttons accordingly.
  • StatusTextChange: This event is fired in several cases, for example when the cursor moves over a hyperlink. This event sends a string, and the app responds by showing that string in a static control under the browser window.

The app also has four buttons for controlling the browser: back, forward, stop, and reload. These call the corresponding IWebBrowser2 methods.

The events and the data accompanying the events are all logged to a list control, so you can see the events as they are fired. You can also turn off logging of any events so you can watch just one or two. To demonstrate some non-trivial event handling, the BeforeNavigate2 handler checks the URL, and if it contains "doubleclick.net", the navigation is cancelled. Ad and popup blockers that run as IE plug-ins, instead of HTTP proxies, use this method. Here is the code that does this check.

void __stdcall CMainDlg::OnBeforeNavigate2 (
    IDispatch* pDisp, VARIANT* URL, VARIANT* Flags, 
    VARIANT* TargetFrameName, VARIANT* PostData, 
    VARIANT* Headers, VARIANT_BOOL* Cancel )
{
CString sURL = URL->bstrVal;
 
    // You can set *Cancel to VARIANT_TRUE to stop the 

    // navigation from happening. For example, to stop 

    // navigates to evil tracking companies like doubleclick.net:

    if ( sURL.Find ( _T("doubleclick.net") ) > 0 )
        *Cancel = VARIANT_TRUE;
}

Here's what the app looks like while viewing the Lounge:

 [Sample app - 33K]

IEHost demonstrates several other WTL features that have been covered in earlier articles: CBitmapButton (for the browser control buttons), CListViewCtrl (for the event logging), DDX (to keep track of the checkbox states), and CDialogResize.

Creating an ActiveX Control at Run Time

It is also possible to create an ActiveX control at run time, instead of in the resource editor. The About box demonstrates this technique. The dialog resource contains a placeholder group box that marks where the WebBrowser control will go:

 [About box in editor - 5K]

In OnInitDialog(), we use CAxWindow to create a new AtlAxWin in the same RECT as the placeholder (which is then destroyed):

LRESULT CAboutDlg::OnInitDialog(...)
{
CWindow wndPlaceholder = GetDlgItem ( IDC_IE_PLACEHOLDER );
CRect rc;
CAxWindow wndIE;
 
    // Get the rect of the placeholder group box, then destroy 

    // that window because we don't need it anymore.

    wndPlaceholder.GetWindowRect ( rc );
    ScreenToClient ( rc );
    wndPlaceholder.DestroyWindow();
 
    // Create the AX host window.

    wndIE.Create ( *this, rc, _T(""), 
                   WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN );

Next, we use a CAxWindow method to create the ActiveX control. The two methods we can choose from are CreateControl() and CreateControlEx(). CreateControlEx() has additional parameters that can return interface pointers, so you don't have to subsequently call QueryControl(). The two parameters we're interested in are the first, which is the string version of the WebBrowser control's GUID; and the fourth, which is a pointer to an IUnknown*. This pointer will be filled in with the IUnknown of the ActiveX control. After creating the control, we query for an IWebBrowser2 interface, just as before, and navigate the control to a URL.

CComPtr<IUnknown> punkCtrl;
CComQIPtr<IWebBrowser2> pWB2;
CComVariant v;    // empty VARIANT

 
    // Create the browser control using its GUID.

    wndIE.CreateControlEx ( L"{8856F961-340A-11D0-A96B-00C04FD705A2}", 
                            NULL, NULL, &punkCtrl );
 
    // Get an IWebBrowser2 interface on the control and navigate to a page.

    pWB2 = punkCtrl;
    pWB2->Navigate ( CComBSTR("about:mozilla"), &v, &v, &v, &v );
}

For ActiveX controls that have ProgIDs, you can pass the ProgID to CreateControlEx() instead of the GUID. For example, we could create the WebBrowser control with this call:

    // Use the control's ProgID, Shell.Explorer:

    wndIE.CreateControlEx ( L"Shell.Explorer", NULL,
                            NULL, &punkCtrl );

CreateControl() and CreateControlEx() also have overloads that are used specifically with WebBrowser controls. If your app contains a web page as an HTML resource, you can pass its resource ID as the first parameter. ATL will create a WebBrowser control and navigate it to the resource. IEHost contains a page whose ID is IDR_ABOUTPAGE, so we can show it in the about box with this code:

    wndIE.CreateControl ( IDR_ABOUTPAGE );

Here's the result:

 [About box browser ctrl - 12K]

The sample project contains code for all three of the techniques described above. Check out CAboutDlg::OnInitDialog() and comment/uncomment code to see each one in action.

Keyboard Handling

One final, but very important, detail is keyboard messages. Keyboard handling with ActiveX controls is rather complicated, since the host and the control have to work together to make sure the control sees the messages it's interested in. For example, the WebBrowser control lets you move through links with the TAB key. MFC handles all this itself, so you may never have realized the amount of work required to get keyboard support perfectly right.

Unfortunately, the AppWizard doesn't generate keyboard handling code for a dialog-based app. However, if you make an SDI app that uses a form view as the view window, you'll see the necessary code in PreTranslateMessage(). When a mouse or keyboard message is read from the message queue, the code gets the control with the focus and forwards the message to the control using the special ATL message WM_FORWARDMSG. Normally, when a window receives WM_FORWARDMSG, it does nothing, since it doesn't know about that message. However, when an ActiveX control has the focus, the WM_FORWARDMSG ends up being sent to the AtlAxWin that hosts the control. AtlAxWin recognizes WM_FORWARDMSG and takes the necessary steps to see if the control wants to handle the message itself.

If the window with the focus does not recognize WM_FORWARDMSG, then PreTranslateMessage() calls IsDialogMessage() so that the standard dialog navigation keys like TAB work properly.

The sample project contains the necessary code in CMainDlg::PreTranslateMessage(). Since PreTranslateMessage() works only in modeless dialogs, your dialog-based app must use a modeless dialog if you want proper keyboard handling.

Up Next

In the next article, we'll return to frame windows and cover the topic of using splitter windows.

Copyright and license

This article is copyrighted material, (c)2003-2006 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.

The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefiting from my code) but is not required. Attribution in your own source code is also appreciated but not required.

Revision History

  • May 20, 2003: Article first published.
  • January 5, 2006: Rewrote the section on sink maps, event handlers, and advising. The old code was more complex than necessary.

Series Navigation: � Part V (Advanced Dialog UI Classes) | � Part VII (Splitter Windows)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here