Using Rendering Behaviors

This tutorial explains how to implement a rendering behavior in C++ and how to use it in an HTML document or an application hosting MSHTML. In the course of explaining rendering behaviors, the tutorial provides some review of binary behavior implementation. This tutorial includes a sample rendering behavior and its source code. The discussion focuses on the overall structure of this program as a rendering behavior, rather than the details of this particular sample's behavior.

  • Prerequisites and Requirements
  • Overview of Rendering Behaviors
  • Sample Specifications
  • Implementing the Behavior
    • IElementBehaviorFactory
    • IElementBehavior
    • IObjectSafety
    • IEventSink
  • Implementing the Rendering Behavior
    • GetPainterInfo
    • Draw
    • HitTestPoint
    • OnResize
  • Handling Events
  • Rendering Behavior Support Interfaces
    • IHTMLPaintSite
  • Attaching Your Rendering Behavior to an Element
    • On a Web Page
    • In an Application Hosting MSHTML
  • Related topics

Prerequisites and Requirements

To understand and use this tutorial, you need:

  • A good understanding of C++, the Component Object Model (COM), and the Active Template Library (ATL).
  • Microsoft Internet Explorer 5.5 or later installed on your computer.
  • Header files for Internet Explorer 5.5 or later for use in your development environment; in particular, Mshtml.h and Mshtmdid.h.

Overview of Rendering Behaviors

Rendering behaviors enable you to perform custom drawing on a Web page, using the Windows Graphics Device Interface (GDI) or a drawing object such as Microsoft DirectDraw or Microsoft Direct3D. You might use rendering behaviors to provide a custom background or highlighting for elements, for instance, or to provide guidelines for laying out a page in a "what you see is what you get" (WYSIWYG) editor.

Rendering behaviors are extensions of binary behaviors. To use a rendering behavior, you must:

  • Implement the basic structure of a binary behavior.
  • Implement the rendering behavior within the binary behavior.
  • Attach the rendering behavior to an element on a page.

A binary behavior has two basic components: the binary behavior itself, and a class factory to create the binary behavior. Attach a binary behavior to an element by giving MSHTML a reference to the behavior's factory, which is an implementation of IElementBehaviorFactory. MSHTML then calls on the factory to create the IElementBehavior interface for the behavior itself. From IElementBehavior, MSHTML automatically calls QueryInterface, requesting an IHTMLPainter interface pointer to determine if the behavior is also a rendering behavior.

Once MSHTML has a pointer to IHTMLPainter, it calls IHTMLPainter::GetPainterInfo. IHTMLPainter::GetPainterInfo provides MSHTML with information about how to draw a rendering behavior, including:

  • How it is to be layered in relation to the element.
  • Whether it uses the GDI or another object (such as DirectDraw) for drawing.
  • Whether it is opaque or transparent.
  • Whether your behavior responds to events.
  • Whether your behavior draws outside the element.

After the call to IHTMLPainter::GetPainterInfo, MSHTML is ready to draw the behavior. When the system needs to paint or repaint the area of the screen occupied by the element to which the behavior is attached, MSHTML calls the IHTMLPainter::Draw method of your IHTMLPainter implementation. If your behavior responds to events, MSHTML calls IHTMLPainter::HitTestPoint whenever an event occurs in the rendering behavior's drawing area. MSHTML also calls IHTMLPainter::OnResize whenever the window containing your element is resized.

The first step in implementing a rendering behavior is to implement, at a minimum, the IElementBehavior and IElementBehaviorFactory interfaces needed to get a binary behavior up and running. If the behavior is to handle events, it also must implement an event sink, derived from IDispatch, to intercept and handle events occurring on the element. If a behavior will be used on a Web page, the factory should implement IObjectSafety, which defines the behavior's security level. A binary behavior used in an application hosting MSHTML would not need to implement IObjectSafety. See Security Considerations: Element Behaviors for more information on security issues relating to binary behaviors.

Once the binary behavior framework is complete, implementing a rendering behavior involves implementing just one more interface, IHTMLPainter, over those that are necessary for a binary behavior. IHTMLPainter must be implemented on the same object that implements IElementBehavior, since MSHTML calls QueryInterface from the IElementBehavior interface to find it.

Sample Specifications

The sample for this tutorial provides a simple example of a rendering behavior. It demonstrates some important features that rendering behaviors provide and implementation details to consider:

  • How you can use the GDI to draw directly on a Web page
  • How a behavior can specify the z-order in which it is to be layered with the element
  • How a behavior can draw outside the element to which it is attached
  • How a rendering behavior can intercept and handle mouse events, including events outside the element itself

The specifications of this behavior are as follows:

  • The rendering behavior initially draws a blue rectangular polygon over an element with black circular handles centered on each corner of the element.
  • The handles can be dragged to reshape the polygon. The color of a handle changes to yellow while being dragged.
  • The rendering behavior draws outside the boundaries of the element by a small amount equal to the handle radius.
  • The rendering behavior restricts mouse movements to the confines of the element whenever a handle is dragged.

Code example: http://samples.msdn.microsoft.com/workshop/samples/browser/renderbehave/render.htm

The source code for this sample is included in a Visual C++ 6 workspace. It uses ATL to provide COM support, standard implementations of some of the standard interfaces, and "smart" interface pointers that handle their own reference counting. You can use this sample as a structure for building your own rendering behaviors. The project source code can be downloaded at the Rendering Behavior Sample Source Page.

Structurally, the sample rendering behavior consists of three classes:

The program need not be divided into these three classes. It could, for instance, be combined into a single class. This might be a good strategy for a very simple rendering behavior. (For an example of a rendering behavior implemented in a single class, see the sample for Implementing IHTMLEditHost.) Dividing the behavior into classes like these is a good object-oriented approach, and in this sample program helps to demonstrate how to set up the communication needed between different classes composing a rendering behavior.

Implementing the Behavior

This section provides a brief review of the methods of IElementBehaviorFactory, IElementBehavior, IObjectSafety, and an IDispatch interface for event handling.

IElementBehaviorFactory

IElementBehaviorFactory has one method, IElementBehaviorFactory::FindBehavior, which instantiates the object implementing the binary behavior and returns a pointer to its IElementBehavior interface. Since this sample implements only one behavior, its IElementBehaviorFactory::FindBehavior implementation is very simple.

STDMETHODIMP
CFactory::FindBehavior(BSTR bstrBehavior,
                       BSTR bstrBehaviorUrl,
                       IElementBehaviorSite* pSite,
                       IElementBehavior** ppBehavior)
{
   HRESULT hr;
   CComObject<CBehavior>* pBehavior;

   // Create a Behavior object.
   hr = CComObject<CBehavior>::CreateInstance(&pBehavior);

   if (SUCCEEDED(hr))
   {
        hr = pBehavior->QueryInterface(IID_IElementBehavior, (void**)ppBehavior );
   }

   return hr;
}

IElementBehavior

IElementBehavior has three methods, IElementBehavior::Init, IElementBehavior::Notify, and IElementBehavior::Detach:

  • IElementBehavior::Init passes an IElementBehaviorSite interface pointer into the behavior for the behavior's use. The sample uses its one method, IElementBehaviorSite::GetElement, in its implementation of IElementBehavior::Notify once the document is complete and the document object model is available. The sample caches the IElementBehaviorSite pointer in its IElementBehavior::Init implementation. It also calls QueryInterface from IElementBehaviorSite to obtain and cache an IHTMLPaintSite interface pointer. IHTMLPaintSite will be very important later. It's used to force MSHTML to redraw as needed, or to change information for drawing your behavior.
  • IElementBehavior::Notify handles two events pertaining to the status of the document being downloaded. For this sample, the only relevent one is BEHAVIOREVENT_DOCUMENTREADY, because the sample needs access to the element, the document, the window, and the selection object at various points. These objects are available after the document is complete. The sample caches pointers to the interfaces corresponding to each of these objects for later use. Notice that the sample obtains the IHTMLElement interface pointer by calling IElementBehaviorSite::GetElement on the IElementBehaviorSite interface it cached during the IElementBehavior::Init method. The sample also caches the coordinates of the element for use when drawing later. Finally, the sample registers an event sink using the AtlAdvise function. Now any events that take place on the element can be handled by the behavior.
  • MSHTML calls IElementBehavior::Detach when the page is unloaded or the behavior is removed from the element to which it is attached. It provides an opportunity to release all resources. The sample uses IElementBehavior::Detach to release all interface pointers that aren't smart pointers.

IObjectSafety

IObjectSafety has two methods, GetInterfaceSafetyOptions and SetInterfaceSafetyOptions. ATL provides a default implementation of each when the class is declared in the inheritance tree for CFactory:

class ATL_NO_VTABLE CFactory :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CFactory, &CLSID_Factory>,
    public IObjectSafetyImpl<CFactory, INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA>,
    public IElementBehaviorFactory

This implementation is satisfactory for the sample's purposes, and will allow the rendering behavior to run on a Web page. If you declare your binary behavior safe, you need to be sure that it is safe on any Web page. The sample behavior is safe because it does not manipulate the file system in any way or support any scriptable access through its IDispatch implementation.

IEventSink

IEventSink is the sample's name for its implementation of the IDispatch interface. IEventSink uses the ATL template class IDispatchImpl to obtain a basic implementation of IDispatch. It then overrides the IDispatch::Invoke method to intercept specific mouse events on the rendering behavior's element. IEventSink is registered to handle element events for the rendering behavior in the behavior's IElementBehavior::Notify method (using AtlAdvise). Events that take place on the element will now automatically call IEventSink's IDispatch::Invoke implementation where the rendering behavior can respond to the mouse clicks and movements.

Implementing the Rendering Behavior

Implementing a rendering behavior involves implementing just one more interface, IHTMLPainter, over those that are necessary for a binary behavior. Remember that IHTMLPainter has to be implemented on the same object that implements IElementBehavior. IHTMLPainter has four methods:

  • IHTMLPainter::GetPainterInfo provides information to MSHTML about what a rendering behavior does and what resources it requires.
  • IHTMLPainter::Draw does the actual painting that a rendering behavior is to perform.
  • IHTMLPainter::HitTestPoint allows you to test for events on specific parts of a rendering behavior. It also allows you to signal to MSHTML that you want to handle an event that occurs within the rendering behavior's drawing area, but outside the element itself.
  • IHTMLPainter::OnResize handles any special processing needed if the window is resized.

GetPainterInfo

MSHTML calls this method to get information from you about the specifications and requirements of the rendering behavior. Your job is to initialize an HTML_PAINTER_INFO structure passed into the method through its pInfo argument. Let's look at the members of this structure one by one:

  • LONG lFlags: The allowed flags are members of the HTML_PAINTER enumeration. These flags can be combined using the bitwise OR operator (|). The sample sets the HTMLPAINTER_OPAQUE and HTMLPAINTER_HITTEST flags. The HTMLPAINTER_HITTEST is a very important flag to set if your rendering behavior is to handle events. It tells MSHTML to call your IHTMLPainter::HitTestPoint implementation. Without it, IHTMLPainter::HitTestPoint will not be called. If you want to use a drawing object, like DirectDraw or Direct3D, you must set HTMLPAINTER_SURFACE or HTMLPAINTER_3DSURFACE flags.

  • LONG lZOrder: Use this member to specify where you want your rendering behavior to be drawn in relation to the element's contents. Your drawing can replace the element's background, for instance. It can be placed above the background but below the element's contents. It can be placed above the element. It can even replace the element's contents if you so choose.

  • IID iidDrawObject: Use this member to request a certain drawing object, like DirectDraw, to use for drawing. If you're using the GDI only for drawing, leave this member alone or set it to 0 using memset.

    memset(&pInfo->iidDrawObject, 0, sizeof(IID));
    
  • RECT rcExpand: Allows you to draw outside the boundaries of your element by a specified amount. The rcExpand member is not a RECT per se. Its top, bottom, left, and right members are the amounts on each side by which you want to expand your element. If you don't want to draw outside the element's boundaries, you need to set rcExpand to zero. You can set it to 0 using memset, or set each of its members to 0 individually.

    pInfo->sPainterInfo.rcExpand.left = 0;
    pInfo->sPainterInfo.rcExpand.top = 0;
    pInfo->sPainterInfo.rcExpand.right = 0;
    pInfo->sPainterInfo.rcExpand.bottom = 0;
    

During the course of your program, you can change the information provided by IHTMLPainter::GetPainterInfo in the HTML_PAINTER_INFO structure (though don't do so in the IHTMLPainter::Draw method). For instance, you could change the size of the expanded region into which you want to draw. To do this, call IHTMLPaintSite::InvalidatePainterInfo to force MSHTML to call your IHTMLPainter::GetPainterInfo implementation again and get the new information.

The sample does not change the HTML_PAINTER_INFO information during its execution. It sets the HTML_PAINTER_INFO structure information directly in its IHTMLPainter::GetPainterInfo implementation. If you want to change the HTML_PAINTER_INFO information during the execution of your rendering behavior, allocate your own member HTML_PAINTER_INFO structure and initialize it during the IElementBehavior::Init call. Your implementation of IHTMLPainter::GetPainterInfo copies your member structure to the method's pInfo argument.

Note  Initializing your member HTML_PAINTER_INFO structure in the IElementBehavior::Notify method does not work because IElementBehavior::Notify is called after IHTMLPainter::GetPainterInfo.

 

Draw

You have a choice of using the GDI or a drawing object, such as DirectDraw or Direct3D, for the drawing you do on your element. The drawing area is restricted to the boundaries of the element to which the behavior is attached, plus a margin around the element that you specify in the rcExpand member of the HTML_PAINTER_INFO. This area is passed into the IHTMLPainter::Draw method as the rcBounds rectangle parameter.

Drawing should be done within the rcBounds rectangle. In fact, drawing must be done within this rectangle if you've set the HTMLPAINTER_NOPHYSICALCLIP flag in the HTML_PAINTER enumeration in the HTML_PAINTER_INFO structure. The coordinates of the rcBounds rectangle are given with respect to the device context's coordinate system. Because this coordinate system will not have the same origin each time IHTMLPainter::Draw is called, you need to keep track of drawing coordinates in some other way, perhaps with respect to the element's top left corner, and then adjust them to correspond to the coordinate system of the rcBounds rectangle.

Note  A rendering behavior does not render itself all at once. It renders itself in multiple horizontal bands or sections, using multiple calls to IHTMLPainter::Draw. The top left corner of each band is the origin of the device context, which means that the top value of rcBounds is frequently negative.

 

It might not be necessary to redraw the entire rcBounds area when MSHTML calls IHTMLPainter::Draw. The rcUpdate parameter is the actual area that needs to be updated in any call to IHTMLPainter::Draw. Its coordinates are given with respect to the device context, just like the rcBounds rectangle. You can gain efficiency by limiting your drawing to this update area.

HitTestPoint

If your rendering behavior participates in event handling, you might want to identify certain areas of your behavior as special. For instance, if your behavior draws grab handles at the corners of the element, like the sample does, you'll want to test whether mouse clicks are occurring on the handles. IHTMLPainter::HitTestPoint allows you do do this. MSHTML calls IHTMLPainter::HitTestPoint whenever an event occurs concerning your element or its expanded region, a mouse move, mouse up, mouse down, or mouse click event, for instance. One of the IHTMLPainter::HitTestPoint parameters is lPartID, which you can use for a part identification number. You can set it to any value you want, using any system you devise. This value is then stored as an attribute in the event object for the event. For a behavior like the sample, this part ID value is not necessary, since the behavior can store and access the part ID within itself. The lPartID passed through IHTMLPainter::HitTestPoint is really meant for a situation where an application is hosting the browser. The application might need to know what part of a rendering behavior is receiving an event, and might not have access to the behavior's data members.

If your behavior is making use of an expanded area, IHTMLPainter::HitTestPoint is the only way for your rendering behavior to check for mouse clicks and other events that occur outside the boundaries of the element. IDispatch::Invoke normally handles only those events that take place on the element, not the expanded region. However, whenever you set the pbHit argument in IHTMLPainter::HitTestPoint to TRUE, MSHTML fires an event that in turn calls your IDispatch::Invoke implementation for event handling.

If your rendering behavior does not participate in events, the implementation of IHTMLPainter::HitTestPoint can be a single line as follows:

return E_NOTIMPL;

And of course, you would NOT set the HTMLPAINTER_HITTEST flag during the IHTMLPainter::GetPainterInfo call.

OnResize

MSHTML calls IHTMLPainter::OnResize whenever the window is resized that contains the element with your attached rendering behavior. Typically, an element changes size and location when the window is resized, and you must implement the method to adjust your rendering behavior variable so the behavior remains within its new boundaries. The sample uses IHTMLPainter::OnResize to proportionally preserve any reshaping of the polygon by the user.

Handling Events

As mentioned earlier, a rendering behavior's event handling takes place in both the IHTMLPainter::HitTestPoint and IDispatch::Invoke methods implemented by the rendering behavior. IHTMLPainter::HitTestPoint gives you the means to identify specific areas of your rendering behavior. It also allows you to signal a call to your IDispatch::Invoke implementation when events occur in the expanded region of your rendering behavior—events that would not ordinarily prompt a call to IDispatch::Invoke.

Your IDispatch::Invoke implementation will probably need to prompt calls to IHTMLPainter::Draw, and might need to signal changes in the drawing requirements and parameters set in the IHTMLPainter::GetPainterInfo method. Also, you might need to translate coordinates between the local and global coordinate systems. The IHTMLPaintSite interface pointer cached during the IElementBehavior::Init method gives you the means to perform these actions. For a discussion of IHTMLPaintSite, see the following section on Rendering Behavior Support Interfaces.

Rendering Behavior Support Interfaces

IHTMLPaintSite

The IHTMLPaintSite interface provides services to your rendering behavior. Its most important method is IHTMLPaintSite::InvalidateRect. When you call this method, you ask the system to redraw your rendering behavior, or a portion of it, as specified by its prcInvalid argument. Passing NULL for this argument redraws the entire area of your rendering behavior.

In addition to IHTMLPaintSite::InvalidateRect, IHTMLPaintSite offers a means for finer control of the area that needs to be redrawn using its IHTMLPaintSite::InvalidateRegion method. This method allows you to specify a CRgn class for redrawing, thereby defining a specific shape or multiple shapes for the area to be redrawn. If you use regions, use the IHTMLPaintSite::GetDrawInfo method in your IHTMLPainter::Draw implementation to retrieve the region class defining the area that needs to be redrawn. When you call IHTMLPaintSite::GetDrawInfo for region information, you specify that you want region information by using the HTMLPAINT_DRAWINFO_UPDATEREGION flag in the lFlags argument.

IHTMLPaintSite::GetDrawInfo gives you access to two other kinds of information that might be important to your IHTMLPainter::Draw implementation. If your behavior supports transformations—which are important for print preview, for instance—you can access the necessary transformation matrices. When you call IHTMLPaintSite::GetDrawInfo for tranformation information, you specify that you want the transformation matrices in the lFlags argument with the HTMLPAINT_DRAWINFO_XFORM flag.

The last kind of information available through IHTMLPaintSite::GetDrawInfo is a RECT, rcViewport, that contains the location of a scrolling area's visible area in relation to a scrolling area's document. For information on this RECT, see the HTML_PAINT_DRAW_INFO reference.

The final method of IHTMLPaintSite is IHTMLPaintSite::InvalidatePainterInfo. As previously mentioned, this method offers a means for changing the drawing specifications and requirements of your rendering behavior during the execution of your program. If you change the information that IHTMLPainter::GetPainterInfo provides to MSHTML, call IHTMLPaintSite::InvalidatePainterInfo to force MSHTML to update it.

Attaching Your Rendering Behavior to an Element

On a Web Page

For best performance, package the DLL containing your rendering behavior in a cabinet file (.cab) and digitally sign it. This lets users run your behavior on their computers with a minimum of security warnings. For information on cabinet files and digitally signing a cabinet file, see Packaging ActiveX Controls.

To use a rendering behavior on a Web page, use the object tag to attach the behavior to the page, using the class identifier (CLSID) for the object that implements IElementBehaviorFactory for your rendering behavior. In the sample, this is the CFactory coclass listed in the Render.idl interface definition file for the project. Its CLSID is B44A7019-AD20-412C-B38D-EE9E1D89BA4F.

<HTML>
<HEAD>
<OBJECT ID=behave
 CLASSID="clsid:B44A7019-AD20-412C-B38D-EE9E1D89BA4F"
 CODEBASE="render.cab"></OBJECT>
</HEAD>

<BODY>

<P ID="target" STYLE="behavior: url(#behave);">This paragraph element has a Rendering Behavior attached to it.</P>

</BODY>
</HTML>

If the object tag is not placed in the header, the behavior won't print.

The behavior is added to the element through a style definition. Notice how the object tag is given an IHTMLElement::id attribute and how it is used in the STYLE attribute syntax to attach the behavior to a specific element.

In an Application Hosting MSHTML

Rendering behaviors can also be used from C++ in an application hosting MSHTML or the WebBrowser. To add a behavior to an element, use the IHTMLElement2::addBehavior method for that element to point it to the behavior's factory. In the following example, the actual call to IHTMLElement2::addBehavior occurs at the very end. The rest of the code shows the preparatory steps necessary to instantiate the factory interface and convert it to the VARIANT format the IHTMLElement2::addBehavior method takes for its parameter.

// Assume pElement is a valid IHTMLElement2 interface pointer
HRESULT hr;
long lBehaviorID;
IElementBehaviorFactory* pRBFactory;
VARIANT vBehavior;

// Get the behavior factory interface for
// the rendering behavior
hr = CoCreateInstance(CLSID_RenderingBehavior,
                      NULL,
                      CLSCTX_INPROC_SERVER,
                      IID_IElementBehaviorFactory, (void**)&pRBFactory);

// Convert pRBFactory to the proper VARIANT data type
// for IHTMLElement2::AddBehavior
V_VT(&vBehavior) = VT_UNKNOWN;
V_UNKNOWN(&vBehavior) = pRBFactory;

// AddRef because the pRBFactory pointer has been copied
V_UNKNOWN(&vBehavior)->AddRef();

// Or Alternatively
//vBehavior.vt = VT_UNKNOWN;
//vBehavior.punkVal = pSnapperFactory;
//vBehavior.punkVal->AddRef();

// Add behavior
hr = pElement->addBehavior(NULL,
                           &vBehavior,
                           &lBehaviorID);

lBehaviorID is a cookie that you could use to remove the behavior later, if you wanted to, using IHTMLElement2::removeBehavior.

Implementing IHTMLEditHost