Implementing IHTMLEditHost

This tutorial explains how to use the IHTMLEditHost interface to customize how the MSHTML Editor behaves when elements on a Web page are resized or moved. It describes the architecture and relationship of MSHTML to IHTMLEditHost, and explains how to implement IHTMLEditHost.

This article is divided into the following sections:

  • Prerequisites and Requirements
  • IHTMLEditHost Overview
  • IHTMLEditHost Architecture
  • Sample Specifications
  • Implementing IServiceProvider
  • Implementing IHTMLEditHost
  • Understanding SnapRect
  • Activating and Deactivating IHTMLEditHost
  • Related topics

Prerequisites and Requirements

In order 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 greater installed.
  • Header files and libraries for Internet Explorer 5.5 or greater for use in your development environment; in particular, you need Mshtml.h.

IHTMLEditHost Overview

IHTMLEditHost provides you a way to control how elements are resized and moved when the user grabs and drags the handles on a control element. For instance, you could cause an element to resize or move by specific increments (a snap-to-grid feature), limit resizing to a minimum or maximum size, or constrain the area to which an element can be moved. The use of IHTMLEditHost is limited to applications hosting MSHTML or the WebBrowser control. IHTMLEditHost cannot be used with binary behaviors or rendering behaviors.

IHTMLEditHost Architecture

Whenever the MSHTML Editor is activated, it looks to see if an IHTMLEditHost is available. The Editor first asks the host application for an IServiceProvider interface. If it finds IServiceProvider, it will query for an IHTMLEditHost interface pointer.

IHTMLEditHost acts like a callback routine. The MSHTML Editor holds a pointer to an IHTMLEditHost interface (with which you provide it) and calls IHTMLEditHost's one method, IHTMLEditHost::SnapRect, whenever the user resizes or moves a resizable/moveable element in the editing environment. When the MSHTML Editor calls IHTMLEditHost::SnapRect, the method's arguments will tell you which element is being resized, the original size and position of the element, and which handle the user has grabbed. You have the ability to control the size and position of the element when the user releases the handle.

The IHTMLEditHost interface will remain active until the Editor is turned off or the WebBrowser navigates to a new page. At this point, the Editor releases its pointer to any IHTMLEditHost interface it holds.

Sample Specifications

The sample for this tutorial implements a snap-to-grid feature using IHTMLEditHost. It demonstrates the important features that IHTMLEditHost provides, and offers implementation details to consider:

  • How you can tell which element is being resized and moved, determine the selected element's current position and size, and know which handle the user has grabbed.
  • How you can change the element's current position and size.
  • How and when MSHTML calls IHTMLEditHost's SnapRect method.
  • How to implement the IServiceProvider interface needed to give the MSHTML Editor a pointer to your IHTMLEditHost interface implementaton.
  • How you can deactivate and reactivate the IHTMLEditHost interface.

This sample is a simple browser implementation with an address bar, five buttons, and a combo box. The specifications are as follows:

  • The first button turns the MSHTML Editor on and off.
  • The second button turns the snap-to-grid feature on and off.
  • The third button turns a red dashed grid on and off, so you can see how element resizing and movement is constrained to a specific increment when the snap-to-grid feature is turned on.
  • The right-hand combo box holds the snap and grid increment, which can be changed.
  • The fourth button turns two-dimensional positioning on and off.
  • The fifth button turns live resizing on and off.

Note  You must download the sample to your own computer to run it.

 

The source code for this sample is included in a Microsoft Visual C++ 6.0 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 implementations of IHTMLEditHost. The project source code can be downloaded at the EditHost Sample Source Page.

Structurally, the sample consists of four classes, implementing three interfaces. The classes are:

  • CExeModule: this class is the server for the executable file. This module exposes the CBrowserHost, CSnap and CGrid objects. For the purposes of this article, this class only provides support and won't be discussed further.
  • CBrowserHost: this class implements the application's window—which includes a WebBrowser control, the buttons, and the text boxes—and implements IBrowserHost to create, show, hide, and destroy the window. The two functions of interest here are CBrowserHost::QueryService and OnButton2. CBrowserHost::QueryService is the IServiceProvider implementation that MSHTML calls to create the sample's IHTMLEditHost implementation. OnButton2 sets a flag telling the IHTMLEditHost implementation whether to activate the snap-to-grid feature.
  • CSnap: this class implements IHTMLEditHost along with its own interface called ISnap, and is the focus of this article. Besides an implemention of IHTMLEditHost::SnapRect, this class includes properties that allow you to change the snap increment and turn snapping on or off. These properties are accessed by put_ and get_ methods in the ISnap interface.
  • CGrid: this class implements a rendering behavior for drawing a red dashed grid, which makes it easy to see how the snap-to-grid function works. IGrid is CGrid's interface. This class includes a property to set the grid size, which is accessed by put_ and get_ methods. For the purposes of this article, CGrid only provides support, and won't be discussed further.

Implementing IServiceProvider

To use IHTMLEditHost, you must implement both IHTMLEditHost and an IServiceProvider interface to pass a pointer for your IHTMLEditHost interface when the MSHTML Editor asks for it.

IServiceProvider must be implemented by the object that hosts the WebBrowser control or MSHTML. In the case of the sample, the IServiceProvider implementation is in the CBrowserHost class because CBrowserHost hosts the IWebBrowser2 interface.

IServiceProvider has one method, IServiceProvider::QueryService. Unlike QueryInterface, which should only return interface pointers from the same object, IServiceProvider::QueryService allows you to provide a caller with an interface implemented on a different object. This is how the sample implements IHTMLEditHost. (In this example, m_spSnap and m_lSnapIncrement are member variables of CBrowserHost that store an ISnap pointer and the snap increment.)

STDMETHODIMP CBrowserHost::QueryService(REFGUID guidService,
                                      REFIID riid,
                                      void **ppv)
{
    HRESULT hr = E_NOINTERFACE;

    if (guidService == SID_SHTMLEditHost && riid == IID_IHTMLEditHost)
    {
        // Create new CSnap object using ATL
        CComObject<CSnap>* pSnap;
        hr = CComObject<CSnap>::CreateInstance(&pSnap);
        
        // Query the new CSnap object for IHTMLEditHost interface
        hr = pSnap->QueryInterface(IID_IHTMLEditHost, ppv);

        // Cache a pointer to ISnap so you can tell the Snap behavior
        // when to snap and change the snap increment
        m_spSnap = (ISnap*)NULL; // Clear any previous pointers
        hr = pSnap->QueryInterface(IID_ISnap, (void**)&m_spSnap);
        
        // Set the snap increment
        hr = pSnap->put_SnapIncrement(m_lSnapIncrement);
    }

    return hr;
}

Notice that a service ID (SID_SHTMLEditHost) and an interface ID (IID_IHTMLEditHost) are defined for IHTMLEditHost. These definitions can be found in in Mshtml.h.

Implementing IHTMLEditHost

IHTMLEditHost consists of one method, IHTMLEditHost::SnapRect. This method has three arguments:

  • pIElement: a pointer to an IHTMLElement interface for the element that is being moved or resized.
  • prcNew: a RECT which initially contains the element's size and position. Use this RECT to control how MSHTML resizes or moves the element.
  • eHandle: an ELEMENT_CORNER enumeration value. The ELEMENT_CORNER enumeration value tells you which handle the user has grabbed to resize the element, if any. If no handle has been grabbed—if eHandle is set to ELEMENT_CORNER_NONE—the element is being moved rather than resized.

One way to implement IHTMLEditHost::SnapRect is to use a switch statement to process each of the nine ELEMENT_CORNER values. This is how the sample implements IHTMLEditHost::SnapRect. In abbreviated form, the implementation looks like this:

HRESULT
CSnap::SnapRect(IHTMLElement* pIElement,
                RECT* prcNew,
                ELEMENT_CORNER eHandle)
{
    switch (eHandle)
    {
        case ELEMENT_CORNER_NONE:
            // Code for moving the element
            break;
        case ELEMENT_CORNER_TOP:
            // Code for resizing the element
            break;
        case ELEMENT_CORNER_LEFT:
            // Code for resizing the element
            break;
        case ELEMENT_CORNER_BOTTOM:
            // Code for resizing the element
            break;
        case ELEMENT_CORNER_RIGHT:
            // Code for resizing the element
            break;
        case ELEMENT_CORNER_TOPLEFT:
            // Code for resizing the element
            break;
        case ELEMENT_CORNER_TOPRIGHT:
            // Code for resizing the element
            break;
        case ELEMENT_CORNER_BOTTOMLEFT:
            // Code for resizing the element
            break;
        case ELEMENT_CORNER_BOTTOMRIGHT:
            // Code for resizing the element
            break;
    }

    return S_OK;
}

Understanding SnapRect

It's important to know when the MSHTML Editor calls IHTMLEditHost::SnapRect, because this method behaves differently depending on whether 2-D positioning and live resizing are on.

  • In its default state, when first activated, the MSHTML Editor only calls IHTMLEditHost::SnapRect when the user releases the mouse button after resizing an element.
  • When 2-D positioning is on, by issuing the IDM_2D_POSITION command with IOleCommandTarget::Exec, the Editor calls IHTMLEditHost::SnapRect during element moves and during resizing. During a move, IHTMLEditHost::SnapRect is called continously, from the time the user depresses the mouse button to click the element's handle to the time the button is released. In other words, every small mouse movement causes a call to IHTMLEditHost::SnapRect. During a resize, IHTMLEditHost::SnapRect is called once at the end of the resize when the user releases the mouse button.
  • When live resizing is on, IHTMLEditHost::SnapRect is called continously for both element moves and resizes.

The coordinate origin for the rectangle passed into IHTMLEditHost::SnapRect is the top left corner of the browser window. This rectangle, even during continous calls before the mouse button is released, will be the original rectangle for the element, adjusted for the mouse movement up to that point. For example, assume you have an element whose rectangle is (top, bottom, left, right)=(50, 100, 150, 200). If the user grabs the bottom middle handle and pulls it down 20 pixels, the rectangle passed to IHTMLEditHost::SnapRect will be the original element rectangle with 20 pixels added on the bottom, (50, 120, 150, 200). If you move the element in some direction—for instance, 30 up and 20 to the right—the rectangle passed into IHTMLEditHost::SnapRect will be the original element rectangle (with the same width and height) offset 20 pixels in that direction (20, 70, 170, 220).

When implementing IHTMLEditHost::SnapRect, you always know the element rectangle's size and position as if it could be released at that moment. In the sample, this makes implementing the snap-to-grid feature in CSnap straightforward. For instance, to calculate the new bottom position in the handler for ELEMENT_CORNER_BOTTOM, half of m_lSnapIncrement is added to the bottom coordinate as passed into prcNew. This sum is then divided by the increment. Since the variables are all integers, any decimal part is truncated by rounding down. The truncated value is then multiplied by the increment to determine the final bottom coordinate value.

case ELEMENT_CORNER_BOTTOM:
    prcNew->bottom = ((prcNew->bottom + (m_lSnapIncrement/2))/m_lSnapIncrement) * m_lSnapIncrement + 1; 
    break;

The handlers for all the other ELEMENT_CORNER values work in a similar way. In the case of ELEMENT_CORNER_NONE, there is a small complication. Since the whole element moves, all the prcNew member values need to be recalculated. In order to preserve the dimensions of the element, the sample stores the height and width of the element, calculates the new top and left, and then adds the height and width to the top and left to obtain the bottom and right position values.

case ELEMENT_CORNER_NONE:
    LONG lWidth = prcNew->right - prcNew->left;
    LONG lHeight = prcNew->bottom - prcNew->top;

    prcNew->top = ((prcNew->top + (m_lSnapIncrement/2))/m_lSnapIncrement) * m_lSnapIncrement - 2;
    prcNew->left = ((prcNew->left + (m_lSnapIncrement/2))/m_lSnapIncrement) * m_lSnapIncrement - 2;
    prcNew->bottom = prcNew->top + lHeight;
    prcNew->right = prcNew->left + lWidth;
    break;

The plus 1 and minus 2 values appended to each coordinate calculation are empirically discovered values necessary for resizes and moves to conform to the grid in this program.

Activating and Deactivating IHTMLEditHost

When design mode is activated and the MSHTML Editor finds an IHTMLEditHost, the Editor will use the IHTMLEditHost implementation for the course of the design mode session. IHTMLEditHost can't be "turned off" during the session. The MSHTML Editor releases its pointer to IHTMLEditHost only when design mode is deactivated or when the WebBrowser navigates to a new page. After navigating, the WebBrowser will reactivate the MSHTML Editor and the IHTMLEditHost interface will be re-initialized.

Since IHTMLEditHost can't be turned on and off at will, EdHost.exe allows the user to turn the snap-to-grid feature on and off by changing a property on the ISnap interface that disables the IHTMLEditHost::SnapRect method.

hr = m_spSnap->put_SnapOn(FALSE);

The put_SnapOn method changes a Boolean member variable in CSnap, m_bSnapOn. IHTMLEditHost::SnapRect checks m_bSnapOn and exits without doing anything if it's FALSE. This effectively disables IHTMLEditHost::SnapRect when the user does not want the snap-to-grid feature active.

CSnap::SnapRect(IHTMLElement *pIElement, 
                RECT *prcNew, 
                ELEMENT_CORNER eHandle)
{
    if (!m_bSnapOn) return S_OK; // Disables snap-to-grid when m_bSnapOn is FALSE

    switch (eHandle)
    .
    .
    .

Conceptual

Introduction to MSHTML Editing

Activating the MSHTML Editor

Modifying Documents in Edit Mode

Using the MSHTML Editor's Extra Features

Using Editing Glyphs

About Edit Designers