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.
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. 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.
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
via the CAxWindow member function
SetExternalUIHandler:
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.
|