previous page
next page

HTML Controls

Generating an HTML Control

You create an HTML control via the ATL Control Wizard. On the Options page, choose DHTML Control (Minimal is also an option, as described in Chapter 11). The wizard-generated code creates a control that derives from CComControl, sets m_bWindowOnly to TRUE, and provides a resource for the layout of the UI elements of your control. This is similar to the resource that results from running the Object Wizard for a composite control, except that instead of using a dialog resource, an HTML control uses an HTML resource. The same WebBrowser control that provides the UI for Internet Explorer provides the parsing for the HTML resource at runtime. This allows a control to use a declarative style of UI development, but with all the capabilities of the HTML engine in Internet Explorer. The following are a few of the advantages that HTML provides over a dialog resource:

  • Support for resizing via height and width attributes, both in absolute pixels and percentages

  • Support for scripting when using top-level initialization code, defining functions, and handling events

  • Support for extending the object model of the HTML document via an "external" object

  • Support for flowing of mixed text and graphics

  • Support for multiple font families, colors, sizes, and styles

In fact, pretty much everything you've ever seen on a web site can be performed using the HTML control.

By default, you get this HTML as a starting point in a wizard-generated string resource named IDH_<PROJECTNAME>:

<HTML>
<BODY id=theBody>
<BUTTON onclick='window.external.OnClick(theBody, "red");'>
Red
</BUTTON>
<BR>
<BR>
<BUTTON onclick='window.external.OnClick(theBody, "green");'>
Green
</BUTTON>
<BR>
<BR>
<BUTTON onclick='window.external.OnClick(theBody, "blue");'>
Blue
</BUTTON>
</BODY>
</HTML>

From here, you can start changing the HTML to do whatever you like.

HTML Control Creation

The magic of hooking up the WebBrowser control is performed in the OnCreate handler generated by the ATL Object Wizard:

LRESULT CSmartDartBoard::OnCreate(UINT, WPARAM, LPARAM, BOOL&) {
  // Wrap the control's window to use it to host control
  // (not an AtlAxWin80, so no CAxHostWindow yet created)
CAxWindow wnd(m_hWnd);                                          
                                                                
  // Create a CAxWinHost: It will subclass this window and
  // create a control based on an HTML resource.
  HRESULT hr = wnd.CreateControl(IDH_SMARTDARTBOARD);           
  ...                                                           
  return SUCCEEDED(hr) ? 0 : -1;                                
}                                                               

Because m_bWindowOnly is set to TRue, activating the HTML control creates a window. To give this window control-containment capabilities so that it can host an instance of the WebBrowser control, the HTML control's window must be subclassed and sent through the message map provided by CAxHostWindow, just like every other control container. However, because the HTML control's window is not an instance of the AtlAxWin80 class, the subclassing must be handled manually. Notice that the first thing the wizard-generated OnCreate function does is wrap an instance of CAxWindow around the control's HWND. The call to CreateControl creates an instance of CAxHostWindow. The CAxHostWindow object subclasses the HTML control's window, creates an instance of the WebBrowser control, and feeds it a URL of the form res://<module name>/<resource ID>, using CreateNormalizedControl. In effect, the HWND of the HTML control is a parent for the WebBrowser control, which then uses the HTML resource to manage the UI elements of the control. This is exactly analogous to the composite control, in which the HWND of the control was an instance of the dialog manager, using a dialog resource to manage the elements of the UI.

Element Layout

For example, the SmartDartBoard HTML control that is available with the source code of this book uses HTML to lay out its UI, just like the DartBoard composite control. However, in HTML, I can use the auto-layout capabilities of a <table> element to autosize the HTML control to whatever size the user wants, instead of limiting myself to a single size, as with the dialog resource. The following shows how the SmartDartBoard control's HTML resource lays out its elements:

<! Use all of the control's area >
<table width=100% height=100%>
<tr>
    <td colspan=2>
    <object id=objBullsEye width=100% height=100%
            classid="clsid:7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D">
    </object>
    </td>
</tr>
<tr height=1>
    <td>Score: <span id=spanScore>0</td>
    <td align=right>
        <input type=button id=cmdReset value="Reset">
    </td>
</tr>
</table>

To test the UI of the HTML control without compiling, you can right-click the HTML file and choose Preview, which shows the HTML in the Internet Explorer. For example, Figures 12.9 and 12.10 show the control's UI resizing itself properly to fit into the space that it's given.

Figure 12.9. Small SmartDartBoard UI


Figure 12.10. Large SmartDartBoard UI


Accessing the HTML from the Control

When you create an instance of the WebBrowser control, you're actually creating two things: a WebBrowser control, which knows about URLs, and an HTML Document control, which knows about parsing and displaying HTML. The WebBrowser control forms the core of the logic of Internet Explorer and lives in shdocvw.dll. The WebBrowser control implements the IWebBrowser2 interface, with methods such as Navigate, GoBack, GoForward, and Stop. Because the CAxWindow object is merely used to bootstrap hosting the WebBrowser control, and you'd need to be able to access the WebBrowser control in other parts of your code, the OnCreate code generated by the wizard uses QueryControl to cache the IWebBrowser2 interface:

LRESULT CSmartDartBoard::OnCreate(UINT, WPARAM, LPARAM, BOOL&) {
...                                                             
  // Cache the IWebBrowser2 interface                           
  if (SUCCEEDED(hr))                                            
    hr = wnd.QueryControl(IID_IWebBrowser2,                     
        (void**)&m_spBrowser);                                  
                                                                
...                                                             
}                                                               

To display the HTML, the WebBrowser control creates an instance of the HTML Document control, which is implemented in mshtml.dll. The HTML Document represents the implementation of the Dynamic HTML object model (DHTML). This object model exposes each named element on a page of HTML as a COM object, each of which implements one or more COM interfaces and many of which fire events. Although the full scope and power of DHTML is beyond the scope of this book, the rest of this chapter is dedicated to showing you just the tip of the iceberg of functionality DHTML provides.

The HTML Document object implements the IHTMLDocument2 interface, whose most important property is the all property. Getting from the WebBrowser control to the HTML Document control is a matter of retrieving the IWebBrowser2 Document property and querying for the IHTMLDocument2 interface. Accessing any named object on the HTML pagethat is, any tag with an id attributeis done via the all property of the IHTMLDocument2 interface. Retrieving a named object on an HTML page in C++ can be accomplished with a helper function such as the GetHtmlElement helper shown here:

HRESULT CSmartDartBoard::GetHtmlElement(
  LPCOLESTR pszElementID,
  IHTMLElement** ppElement) {
  ATLASSERT(ppElement);
  *ppElement = 0;

  // Get the document from the browser
  HRESULT hr = E_FAIL;
  CComPtr<IDispatch> spdispDoc;
  hr = m_spBrowser->get_Document(&spdispDoc);
  if( FAILED(hr) ) return hr;

  CComQIPtr<IHTMLDocument2> spDoc = spdispDoc;
  if( !spDoc ) return E_NOINTERFACE;

  // Get the All collection from the document
  CComPtr<IHTMLElementCollection> spAll;
  hr = spDoc->get_all(&spAll);
  if( FAILED(hr) ) return hr;

  // Get the element from the All collection
  CComVariant varID = pszElementID;
  CComPtr<IDispatch> spdispItem;
  hr = spAll->item(varID, CComVariant(0), &spdispItem);

  // Return the IHTMLElement interface
  return spdispItem->QueryInterface(ppElement);
}

When you have the IHtmlElement interface, you can change just about anything about that element. For example, notice the <span> tag named spanScore in the SmartDartBoard HTML resource:

...
<td>Score: <span id=spanScore>0</span></td>
...

A span is just a range of HTML with a name so that it can be programmed against. As far as we're concerned, every named object in the HTML is a COM object that we can access from our control's C++ code. This span's job is to hold the control's current score, so after the WebBrowser control has been created, we need to set the span to the m_nScore property of the control. The SetScoreSpan helper function in the SmartDartBoard HTML control uses the GetHtmlElement helper and the IHTMLElement interface to set the innerText property of the span:

HRESULT CSmartDartBoard::SetScoreSpan() {
  // Convert score to VARIANT
  CComVariant varScore = m_nScore;
  HRESULT hr = varScore.ChangeType(VT_BSTR);
  if( FAILED(hr) ) return hr;

  // Find score span element
  CComPtr<IHTMLElement> speScore;
  hr = GetHtmlElement(OLESTR("spanScore"), &speScore);
  if( FAILED(hr) ) return hr;

  // Set the element's inner text
  return speScore->put_innerText(varScore.bstrVal);
}

Whenever the score changes, this function is used to update the contents of the HTML span object from the control's C++ code.

Sinking WebBrowser Events

You might be tempted to use the SetScoreSpan function right from the OnCreate handler to set the initial score value when the control is activated. Unfortunately, the architecture of the HTML Document object dictates that we wait for the document to be completely processed before the object model is exposed to us. To detect when that happens, we need to sink events on the DWebBrowserEvents2 interface. Specifically, we need to know about the OnDocumentComplete event. When we receive this event, we can access all the named objects on the page. Because DWebBrowserEvents2 is a dispinterface, sinking the events can be accomplished with IDispEventImpl and an entry in the sink map:

typedef IDispEventImpl< 1, CSmartDartBoard,
  &DIID_DWebBrowserEvents2, &LIBID_SHDocVw, 1, 0>
  BrowserEvents;

class ATL_NO_VTABLE CSmartDartBoard :
    public CComObjectRootEx<CComSingleThreadModel>,
    ...
    // Sink browser events
      public BrowserEvents {
    ...
BEGIN_SINK_MAP(CSmartDartBoard)
  SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2,
    0x00000103, OnDocumentComplete)
END_SINK_MAP()

     void __stdcall OnDocumentComplete(IDispatch*, VARIANT*);
    ...
};

The OnCreate handler and the OnDestroy handler are good places to establish and shut down the DWebBrowserEvent2 connection point:

LRESULT CSmartDartBoard::OnCreate(UINT, WPARAM, LPARAM, BOOL&) {
  ...
  // Set up connection point w/ the browser
  if( SUCCEEDED(hr) )
    hr = BrowserEvents::DispEventAdvise(m_spBrowser);
  ...
  return SUCCEEDED(hr) ? 0 : -1;
}
LRESULT CSmartDartBoard::OnDestroy(UINT, WPARAM, LPARAM, BOOL&) {
  DispEventUnadvise(m_spBrowser);
  return 0;
}

In the implementation of the OnDocumentComplete event handler, we can finally access the HTML object:

void __stdcall CSmartDartBoard::OnDocumentComplete(IDispatch*,
  VARIANT*) {
  // Set the spanScore object's inner HTML
  SetScoreSpan();
}

Accessing the Control from the HTML

In addition to accessing the named HTML objects from the control, you might find yourself wanting to access the control from the HTML. For this to happen, the HTML must have some hook into the control. This is provided with the window.external property. The script can expect this to be another dispinterface of the control itself. In fact, the ATL Object Wizard generates two dual interfaces on an HTML control. The first, I<ControlName>, is the default interface available to the control's clients. The second, I<ControlName>UI, is an interface given to the WebBrowser control via the SetExternalDispatch function on the CAxWindow object:

HRESULT CAxWindow::SetExternalDispatch(IDispatch* pDisp);

The wizard-generated implementation of OnCreate sets this interface as part of the initialization procedure:

LRESULT CSmartDartBoard::OnCreate(UINT, WPARAM, LPARAM, BOOL&) {        
...                                                                     
  if (SUCCEEDED(hr))                                                    
    hr = wnd.SetExternalDispatch(static_cast<ISmartDartBoardUI*>(this));
...                                                                     
  return SUCCEEDED(hr) ? 0 : -1;                                        
}                                                                       

In the SmartDartBoard example, the interface used by control containers is ISmartDartBoard:

[dual] interface ISmartDartBoard : IDispatch {
  [propget] HRESULT Score([out, retval] long *pVal);
  [propput] HRESULT Score([in] long newVal);
  HRESULT ResetScore();
};

On the other hand, the interface used by the HTML script code is ISmartDartBoardUI:

[dual] interface ISmartDartBoardUI : IDispatch {
  HRESULT AddToScore([in] long ringValue);
  HRESULT ResetScore();
};

This interface represents a bidirectional communication channel. A script block in the HTML can use this interface for whatever it wants. For example:

<table width=100% height=100%>
...
</table>

<script language=vbscript>
    sub objBullsEye_OnScoreChanged(ringValue)
       ' Access the ISmartDartBoardUI interface
        window.external.AddToScore(ringValue)
    end sub

    sub cmdReset_onClick
       ' Access the ISmartDartBoardUI interface
        window.external.ResetScore
    end sub
</script>

In this example, we're using the ISmartDartBoardUI interface as a way to raise events to the control from the HTML, but instead of using connection points, we're using the window's external interface, which is far easier to set up.

Sinking HTML Element Events in C++

Notice that the previous HTML code handled events from the objects in the HTML itself. We're using the object_event syntax of VBScript; for example, cmdReset_onClick is called when the cmdReset button is clicked. It probably doesn't surprise you to learn that all the HTML objects fire events on an interface established via the connection-point protocol. There's no reason we can't sink the events from the HTML objects in our control directly instead of using the window.external interface to forward the events. For example, the cmdReset button fires events on the HTMLInputTextElementEvents dispinterface. Handling these events is, again, a matter of deriving from IDispEventImpl and adding entries to the sink map:

typedef IDispEventImpl<2, CSmartDartBoard,
  &DIID_HTMLInputTextElementEvents,
  &LIBID_MSHTML, 4, 0>
  ButtonEvents;

class ATL_NO_VTABLE CSmartDartBoard :
    public CComObjectRootEx<CComSingleThreadModel>,
    ...
  // Sink events on the DHTML Reset button
  public ButtonEvents {
  ...
BEGIN_SINK_MAP(CSmartDartBoard)
  ...
  SINK_ENTRY_EX(2, DIID_HTMLInputTextElementEvents, DISPID_CLICK,
                OnClickReset)
END_SINK_MAP()
  VARIANT_BOOL __stdcall OnClickReset();
  ...
};

Because we need to have an interface on the cmdReset button, we need to wait until the OnDocumentComplete event to establish a connection point with the button:

void __stdcall CSmartDartBoard::OnDocumentComplete(IDispatch*,
  VARIANT*) {
  // Set the spanScore object's inner HTML
  SetScoreSpan();
  // Retrieve the Reset button
  HRESULT hr;
  CComPtr<IHTMLElement> speReset;
  hr = GetHtmlElement(OLESTR("cmdReset"), &speReset);
  if( FAILED(hr) ) return;

  // Set up the connection point w/ the button
  ButtonEvents::DispEventAdvise(speReset);
}

When we've established the connection with the Reset button, every time the user clicks on it, we get a callback in our OnClickReset event handler. This means that we no longer need the cmdReset_onClick handler in the script. However, from a larger perspective, because we program and handle events back and forth between the C++ and the HTML code, we have the flexibility to use whichever is more convenient when writing the code. This is quite a contrast from a dialog resource, in which the resource is good for laying out the elements of the UI (as long as the UI was a fixed size), but only our C++ code can provide any behavior.

Extended UI Handling

It turns out that the external dispatch is but one setting you can set on the CAxHostWindow that affects the HTML Document control. Several more options can be set via the IAxWinAmbientDispatch interface implemented by CAxHostWindow:

typedef enum tagDocHostUIFlagDispatch {                                  
  docHostUIFlagDIALOG            = 1,                                    
  docHostUIFlagDISABLE_HELP_MENU = 2,                                    
  docHostUIFlagNO3DBORDER        = 4,                                    
  docHostUIFlagSCROLL_NO         = 8,                                    
  docHostUIFlagDISABLE_SCRIPT_INACTIVE = 16,                             
  docHostUIFlagOPENNEWWIN        = 32,                                   
  docHostUIFlagDISABLE_OFFSCREEN = 64,                                   
  docHostUIFlagFLAT_SCROLLBAR = 128,                                     
  docHostUIFlagDIV_BLOCKDEFAULT = 256,                                   
  docHostUIFlagACTIVATE_CLIENTHIT_ONLY = 512,                            
} DocHostUIFlagDispatch;                                                 
                                                                         
typedef enum tagDOCHOSTUIDBLCLKDispatch {                                
  docHostUIDblClkDEFAULT         = 0,                                    
  docHostUIDblClkSHOWPROPERTIES  = 1,                                    
  docHostUIDblClkSHOWCODE        = 2,                                    
} DOCHOSTUIDBLCLKDispatch;                                               
interface IAxWinAmbientDispatch : IDispatch {                            
  ...                                                                    
  // IDocHostUIHandler Defaults                                          
  [propput, helpstring("Set the DOCHOSTUIFLAG flags")]                   
  HRESULT DocHostFlags([in]DWORD dwDocHostFlags);                        
  [propget, helpstring("Get the DOCHOSTUIFLAG flags")]                   
  HRESULT DocHostFlags([out,retval]DWORD* pdwDocHostFlags);              
                                                                         
  [propput, helpstring("Set the DOCHOSTUIDBLCLK flags")]                 
  HRESULT DocHostDoubleClickFlags([in]DWORD dwFlags);                    
  [propget, helpstring("Get the DOCHOSTUIDBLCLK flags")]                 
  HRESULT DocHostDoubleClickFlags([out,retval]DWORD* pdwFlags);          
                                                                         
  [propput, helpstring("Enable or disable context menus")]               
  HRESULT AllowContextMenu([in]VARIANT_BOOL bAllowContextMenu);          
  [propget, helpstring("Are context menus enabled")]                     
  HRESULT AllowContextMenu([out,retval]VARIANT_BOOL* pbAllowContextMenu);
                                                                         
  [propput, helpstring("Enable or disable UI")]                          
  HRESULT AllowShowUI([in]VARIANT_BOOL bAllowShowUI);                    
  [propget, helpstring("Is UI enabled")]                                 
  HRESULT AllowShowUI([out,retval]VARIANT_BOOL* pbAllowShowUI);          
                                                                         
  [propput, helpstring("Set the option key path")]                       
  HRESULT OptionKeyPath([in]BSTR bstrOptionKeyPath);                     
  [propget, helpstring("Get the option key path")]                       
  HRESULT OptionKeyPath([out,retval]BSTR* pbstrOptionKeyPath);           
};                                                                       

The DocHostFlags property can be any combination of DocHostUIFlagDispatch flags. The DocHostDoubleClickFlags property can be any one of the DOCHOSTUIDBLCLKDispatch flags. The AllowContextMenu property enables you to shut off the context menu when the user right-clicks on the HTML control.[10] The AllowShowUI property controls whether the host will be replacing the IE menus and toolbars. The OptionKeyPath property tells the HTML document where in the Registry to read and write its settings.

[10] There's no reason for the user to know you're just bootlegging IE functionality, is there?

All these settings affect the CAxWindowHost object's implementation of IDocHost-UIHandler, an interface that the HTML Document control expects from its host to fine tune its behavior. If you want to control this interaction even more, CAxWindowHost enables you to set your own IDocHostUIHandlerDispatch interface[11] via the CAxWindow member function SetExternalUIHandler:

[11] This interface is defined by ATL in atliface.idl.

HRESULT CAxWindow::SetExternalUIHandler(
    IDocHostUIHandlerDispatch* pHandler);

The IDocHostUIHandlerDispatch interface is pretty much a one-to-one mapping to IDocHostUIHandler, but in dual interface form. When a CAxHostWindow object gets a call from the HTML Document control on IDocHostUIHandler, the call is forwarded if there is an implementation of IDocHostUIHandlerDispatch set. For example, if you want to replace the context menu of the HTML control instead of just turn it off, you have to expose the IDocHostUIHandlerDispatch interface, call SetExternalUIHandler during the OnCreate handler, and implement the ShowContextMenu member function. When there is no implementation of IDocHostUIHandlerDispatch set, the CAxHostWindow object uses the properties set via the IAxWinAmbientDispatch interface.

Hosting HTML Is Not Limited to the HTML Control

By hosting the WebBrowser control or the HTML Document control, you've given yourself a lot more than just hosting HTML. The HTML Document control represents a flexible COM UI framework called Dynamic HTML. The combination of declarative statements to lay out the UI elements and the capability to control each element's behavior, presentation style, and events at runtime gives you a great deal of flexibility.

Nor is this functionality limited to an HTML control. You can achieve the same effect by creating a CAxWindow object that hosts either the WebBrowser or the HTML Document control in a window, a dialog, or a composite control, as well as in an HTML control.[12]

[12] You can build entire applications using ATL as the glue code that initialized a first-tier thin client built entirely from HTML, but that's beyond the scope of this book.


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)