Implementing Edit Designers 2: The Annotator Sample

This topic and its accompanying sample explain how to implement an edit designer that enables users to annotate a Web page. This topic also shows how to create a Web page with an editable region and attach the annotation component to it.

  • Prerequisites and Requirements
  • Introduction
  • The Sample
  • Design Considerations
  • Opening the Project
  • Preparing the Project Environment
  • Adding an Active X Control to the DLL
  • Roughing in IHTMLEditDesigner
  • Scripting Initialization and Support
  • Adding the Comment Glyph Image Resource
  • The Logic of PreHandleEvent
  • Opening the Comment Edit Box
  • Closing the Comment Edit Box
  • Adding Comments
  • Creating a Web Page for the Annotator
  • Running the Annotator
  • Related topics

Prerequisites and Requirements

To make best use of this tutorial and use its accompanying sample, you need:

Introduction

If you've read through the overview and first tutorial concerning edit designers, you already know quite a bit about edit designers and how to implement them. You know that they are COM components that you implement to customize the MSHTML Editor's behavior. You know that the main interface for edit designers is IHTMLEditDesigner, and that IHTMLEditDesigner consists of four methods:

These methods act as callback routines for the MSHTML Editor; that is, the Editor calls these methods at different points during the handling of events.

Now, take the knowledge you've acquired so far and put it to work building an annotation system for use with the MSHTML Editor. The Annotator is simple in principle. It gives the user access to the comments in a document, allowing the user to edit the comments or add new ones as desired. To build the Annotator, you'll use a few customization tools available for the MSHTML Editor. You'll use custom glyphs to show the location of comments in a document, and markup pointer, display pointer, and the IHTMLCaret interface to determine a comment's location on the screen so you can open a comment edit box next to it or add new comments.

The Sample

The sample for this tutorial implements an IHTMLEditDesigner interface that enables the user to read, modify, and add comments in a document. It demonstrates important features of edit designer implementation:

You can see the Annotator sample in action by clicking the Show Me button.

Code example: http://samples.msdn.microsoft.com/workshop/samples/browser/edannotator/annotator.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 common 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 IHTMLEditDesigner. The project source code can be downloaded at the Edit Designer Annotator Sample Source Page.

Structurally, the sample consists of one class, CAnnotator, that implements two interfaces. The interfaces are:

  • IHTMLEditDesigner: Only one method of this interface, IHTMLEditDesigner::PreHandleEvent, is implemented for the Annotator designer.
  • IAnnotator: This interface supports the Annotator designer. Its primary methods allow a designer to be attached to, or detached from, a document through script. IAnnotator also has a method to control the display of glyphs showing the location of comments in a document, and another that lets the user add comments.

The sample has been kept simple in order to focus on the implementation structure of edit designers. For this reason, the sample performs minimal error checking and no exception handling. A "real-world" application generally provides more robust error checking and exception handling.

Design Considerations

Here's a strategy for implementing the Annotator and making it work on a Web page:

  • The Annotator must provide a visual marker to show the locations of comments in a document. You can use a custom editing glyph for this purpose. The glyph is activated using the IOleCommandTarget::Exec method with the MSHTML Command Identifiers for controlling custom editing glyphs.
  • The Annotator must open a particular comment to give the user access to the element. There are a variety of ways to do this— the comments could be opened in a separate window or a separate area of the document, for instance. In this sample, the comments open in a pop-up box. The Annotator inserts a temporary, absolutely positioned, content-editable element into the document at the end of the body section, and positions it next to the comment glyph.
  • In order to place the pop-up window near the comment, the Annotator must determine the on-screen coordinates of the comment. You can use a display pointer, a markup pointer, and the ILineInfo interface for this.
  • The Annotator must provide a way to attach itself to a Web page through script. IHTMLEditServices::AddDesigner and IHTMLEditServices::RemoveDesigner are C++ methods, and are not ordinarily accessible through script. The Annotator therefore must include some scriptable methods that make the calls to IHTMLEditServices::AddDesigner and IHTMLEditServices::RemoveDesigner on behalf of a client using the Annotator. You can include these methods by implementing IDispatch. This is relatively easy with ATL's implementation IDispatchImpl. You can make turning the glyphs on and off accessible through script also.
  • For completeness, the Annotator should add a scriptable method that enables the user to add new comments to a document. To accomplish this, you can use the IHTMLCaret interface, a display pointer, and a markup pointer.

Opening the Project

Let's begin with the steps needed to start a project in Visual C++ 6.

  1. First, open Visual C++ 6.
  2. On the File menu, click New.
  3. Click the Projects tab and choose "ATL COM AppWizard." Give your project a name and choose a directory in which to put it. This tutorial calls the project "EDAnnotator" and put the project directory in the root directory, C:\EDAnnotator. Click OK.
  4. In Step 1 of the Wizard, accept all defaults (Server Type = DLL; all checkboxes unchecked).
  5. Click Finish.
  6. Click OK at the New Project Information box.

These steps create a bare-bones project with enough support to build a DLL. At this point, it's a good idea to build the project to make sure the project settings are all correct.

Preparing the Project Environment

This project uses interface definitions from Mshtml.idl in its project Interface Definition Language (IDL) file, EDAnnotator.idl. In order to import Mshtml.idl into EDAnnotator.idl, there are some special modifications to be made to Mshtml.idl. Instructions for these modifications are located in the MIDL and Mshtml.idl section of Implementing Edit Designers: The Basics. If you haven't already done so, make the modifications described there.

There's one other modification to make to the Annotator project. Later, you'll be adding code that uses C run-time string comparison functions. By default, ATL projects don't link to the C run-time startup code. To enable the use of the string comparison functions, you must remove the _ATL_MIN_CRT preprocessor definition from the project settings. Follow these steps:

  1. On the Project menu, click Settings...
  2. In the "Settings For:" drop-down list, select "Multiple Configurations."
  3. In the "Select project configuration(s) to modify" dialog box, select the check boxes for all release versions, then click OK.
  4. Click the C/C++ tab in the Project Settings dialog box, and then choose the General category.
  5. There are a number of preprocessor definitions in the "Preprocessor definitions" edit box, separated by commas. Remove the "_ATL_MIN_CRT" preprocessor definition.

Note  You may have to "clean" the project before this setting change takes effect. On the Build menu, click Clean; alternatively, you can click Batch Build and then click the Clean button from the Batch Build dialog box.

 

For more information, see the Knowledge Base article Q165076, "LNK2001 on CRT Symbols in ATL Release Build."

Adding an Active X Control to the DLL

You now have a DLL, but it's only a shell of a DLL. The next process turns the DLL shell into a Microsoft ActiveX component shell.

  1. Right-click "EDAnnotator classes" in the ClassView pane and select New ATL Object as shown in the following screen shot.

  2. Select "Controls" in the Category window.

  3. Select "Lite Control" in the Objects window and click Next.

  4. Type "Annotator" in the Short Name box; the other boxes fill in automatically.

  5. Click the Attributes tab. You can see that the interface is a dual interface by default. This means that the control implements IDispatch and that you can create interfaces whose methods are accessible from script. Leave the default settings and click OK.

Note  If you've downloaded and used the sample project for this tutorial, or if you've gone through this tutorial more than once using the same class name, Visual C++ 6 displays a dialog box warning you that a class ID already exists in the registry for this class. It asks whether you want to use the existing class ID. It's typically best to choose No here, unless you want to substitute the project you're creating now for any previous ones.

 

Visual C++ 6 generates the skeleton of an ActiveX control. Since this control needs to work on a Web page, it's good to mark the control as safe for scripting. (The control can be marked as safe for scripting because it won't do anything that accesses the user's file system or registry. For more information, see Designing Secure ActiveX Controls.) In the ClassView pane, click the plus sign next to EDAnnotator to reveal the classes and interfaces in the project. Double-click "CAnnotator" to open the file Annotator.h, which contains the class declaration for the CAnnotator class. Add the following declaration for IObjectSafetyImpl to the inheritance tree.

class ATL_NO_VTABLE CAnnotator : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public IDispatchImpl<IAnnotator, &IID_IAnnotator, &LIBID_EDANNOTATORLib>,
    public CComControl<CAnnotator>,
    public IPersistStreamInitImpl<CAnnotator>,
    public IOleControlImpl<CAnnotator>,
    public IOleObjectImpl<CAnnotator>,
    public IOleInPlaceActiveObjectImpl<CAnnotator>,
    public IViewObjectExImpl<CAnnotator>,
    public IOleInPlaceObjectWindowlessImpl<CAnnotator>,
    public CComCoClass<CAnnotator, &CLSID_Annotator>,
    public IObjectSafetyImpl<CAnnotator,
                             INTERFACESAFE_FOR_UNTRUSTED_CALLER |
                             INTERFACESAFE_FOR_UNTRUSTED_DATA>

Note  Be sure to add a comma to the line before the IObjectSafetyImpl declaration.

 

IObjectSafetyImpl is the ATL implementation of the IObjectSafety interface, which is one technique for marking a control as safe for scripting. IObjectSafetyImpl is defined in Atlctl.h. If you look at the include directives at the top of this file (Annotator.h), you'll see that Atlctl.h has already been included by the ATL Object Wizard. Next, add IObjectSafety to the COM map just below the inheritance tree:

BEGIN_COM_MAP(CAnnotator)
    COM_INTERFACE_ENTRY(IAnnotator)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IViewObjectEx)
    COM_INTERFACE_ENTRY(IViewObject2)
    COM_INTERFACE_ENTRY(IViewObject)
    COM_INTERFACE_ENTRY(IOleInPlaceObjectWindowless)
    COM_INTERFACE_ENTRY(IOleInPlaceObject)
    COM_INTERFACE_ENTRY2(IOleWindow, IOleInPlaceObjectWindowless)
    COM_INTERFACE_ENTRY(IOleInPlaceActiveObject)
    COM_INTERFACE_ENTRY(IOleControl)
    COM_INTERFACE_ENTRY(IOleObject)
    COM_INTERFACE_ENTRY(IPersistStreamInit)
    COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
    COM_INTERFACE_ENTRY(IObjectSafety)
END_COM_MAP()

This entry exposes the IObjectSafety interface to clients of the Annotator DLL. In other words, this entry adds IObjectSafety to the list of interfaces that can be obtained by a call to QueryInterface from any one of the interfaces on the Annotator component.

Again, it's a good idea to build the project at this point just to make sure that all settings are in order.

Roughing in IHTMLEditDesigner

The next task is to rough in an implementation of IHTMLEditDesigner. You could use a wizard to do this, but adding the code by hand lets you see where it all goes. First, add an include directive to the top of Annotator.h for Mshtml.h. Mshtml.h is where IHTMLEditDesigner and most Windows Internet Explorer interfaces are declared.

// Annotator.h : Declaration of the CAnnotator

#ifndef __ANNOTATOR_H_
#define __ANNOTATOR_H_

#include "resource.h"       // main symbols
#include <atlctl.h>
#include "mshtml.h"

Next, add IHTMLEditDesigner to the inheritance list below IObjectSafety.

    .
    .
    .
    public CComCoClass<CAnnotator, &CLSID_Annotator>,
    public IObjectSafetyImpl<CAnnotator,
                             INTERFACESAFE_FOR_UNTRUSTED_CALLER |
                             INTERFACESAFE_FOR_UNTRUSTED_DATA>,
    public IHTMLEditDesigner

Note  Be sure to add a comma to the line before the IHTMLEditDesigner declaration.

 

Next, add IHTMLEditDesigner to the COM Map:

    .
    .
    .
    COM_INTERFACE_ENTRY(IObjectSafety)
    COM_INTERFACE_ENTRY(IHTMLEditDesigner)
END_COM_MAP()

Next it's necessary to add method declarations for the IHTMLEditDesigner methods to the class declaration. Add the following block somewhere in the header file—just before the comment "// IAnnotator" is a good place:

// IHTMLEditDesigner
STDMETHOD(PostEditorEventNotify)(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);
STDMETHOD(PostHandleEvent)(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);
STDMETHOD(PreHandleEvent)(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);
STDMETHOD(TranslateAccelerator)(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);

// IAnnotator

As a final step, add skeleton implementations of each of these methods to Annotator.cpp. Go to the FileView pane, click to expand the EDAnnotator files, then click to expand Source Files. Double-click Annotator.cpp and add the implementation skeletons for the IHTMLEditDesigner methods:

// Annotator.cpp : Implementation of CAnnotator

#include "stdafx.h"
#include "EDAnnotator.h"
#include "Annotator.h"

/////////////////////////////////////////////////////////////////////////////
// CAnnotator

// IHTMLEditDesigner
STDMETHODIMP
CAnnotator::PostEditorEventNotify(DISPID inEvtDispId, IHTMLEventObj *pIEventObj)
{
    return S_FALSE;
}

STDMETHODIMP
CAnnotator::PostHandleEvent(DISPID inEvtDispId, IHTMLEventObj *pIEventObj)
{
    return S_FALSE;
}

STDMETHODIMP
CAnnotator::PreHandleEvent(DISPID inEvtDispId, IHTMLEventObj *pIEventObj)
{
    return S_FALSE;
}

STDMETHODIMP
CAnnotator::TranslateAccelerator(DISPID inEvtDispId, IHTMLEventObj *pIEventObj)
{
    return S_FALSE;
}

Notice that each method returns S_FALSE.

Note  The alternative to adding the code for IHTMLEditDesigner by hand is to right-click CAnnotator in the ClassView pane, choose Implement Interface, and use the Implement Interface wizard. (Be sure you've compiled the project at least once for this procedure.) At first, you'll be presented with a dialog box saying the wizard "Could not find the appropriate interface in the type library." Click the button "Add Typelib..." A dialog box appears in which you can browse the type libraries for components registered on your system. Scroll down to "Microsoft HTML Object Library(4.0)" and select it. Click OK, and the wizard opens the Mshtml.tlb type library for you. Now you can scroll down and select IHTMLEditDesigner, click OK, and the wizard inserts the code you need to create a skeleton implementation of IHTMLEditDesigner. Be aware, however, that all the code is added to the header file. In particular, the wizard puts the method definitions in the .h file, rather than putting method declarations in the .h file and method definitions in the .cpp file.

 

If you were to build the project now, you would have a working implementation of IHTMLEditDesigner, though admittedly it wouldn't do anything. You would be able to use it in a C++ application hosting the MSHTML Editor. However, you would not be able to use it on a Web page yet because the IHTMLEditServices::AddDesigner method is not accessible through script. Making the component scriptable is the topic of the next section.

Scripting Initialization and Support

The next task is to provide the scripting support needed to add the Annotator designer to a Web page. You can do this by adding methods to the IAnnotator interface that was automatically declared by the ATL Object Wizard. The ATL Object Wizard also added IDispatchImpl to the inheritance tree for CAnnotator. IDispatchImpl provides a built-in framework to support scripting. To add scriptable methods, you need to add method definitions to the IDL file for the project, declarations to the header file, and definitions to the .cpp source file. Let's add four methods to IAnnotator:

  • One that attaches the Annotator designer to a document,
  • One that detaches the designer from a document,
  • One that turns the comment glyphs on and off, and
  • One that adds comments to a document.

Go to the FileView pane, expand the Source Files list, and double click EDAnnotator.idl. This file contains the interface and object definitions for the project. Add four lines to the definition for IAnnotator:

[
    object,
    uuid(24BFA480-21DF-41D2-8DAD-03FB1E0A3493),
    dual,
    helpstring("IAnnotator Interface"),
    pointer_default(unique)
]
interface IAnnotator : IDispatch
{
    [id(1), helpstring("method AttachAnnotator")] HRESULT AttachAnnotator(IHTMLDocument2* pDoc);
    [id(2), helpstring("method DetachAnnotator")] HRESULT DetachAnnotator();
    [id(3), helpstring("method ShowCommentGlyphs")] HRESULT ShowCommentGlyphs(BOOL bShow);
    [id(4), helpstring("method AddComment")] HRESULT AddComment();
};

It's also necessary to add an import directive at the head of this file to bring in Mshtml.idl:

// EDAnnotator.idl : IDL source for EDAnnotator.dll
//

// This file is processed by the MIDL tool to
// produce the type library (EDAnnotator.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";
import "mshtml.idl";
#include "olectl.h"

Now, open EDAnnotator.h and add the following method declarations just after the public statement that follows the comment "// IAnnotator":

// IAnnotator
public:
    STDMETHOD(AttachAnnotator)(IHTMLDocument2* pDoc);
    STDMETHOD(DetachAnnotator)();
    STDMETHOD(ShowCommentGlyphs)(BOOL bShow);
    STDMETHOD(AddComment)();

The next step is to add the method implementations for AttachAnnotator, DetachAnnotator, and ShowCommentGlyphs to Annotator.cpp. You add an AddComment skeleton at this point, so you can compile it, and add its implementation later.

The implementation of AttachAnnotator stores the IHTMLDocument2 interface pointer passed into it in a member variable in the class. It also creates an edit designer pointer that should be stored in a member variable. As a first step, then, add the member variables. Open Annotator.h by double-clicking "CAnnotator" in the Class view pane. You can add variable declarations in a number of different places. Scroll to the end and add the following three lines:

private:
    CComPtr<IHTMLDocument2> m_spDoc;
    CComPtr<IHTMLEditDesigner> m_spDesigner;
};

#endif //__ANNOTATOR_H_

Now that you've declared the pointers to store the IHTMLDocument2 and IHTMLEditDesigner pointers, look at the three method implementations one by one. AttachAnnotator first stores the IHTMLDocument2 pointer; then it queries the document for an IServiceProvider, from which it queries for an IHTMLEditServices interface. The Annotator queries itself for an IHTMLEditDesigner interface that it stores in m_spDesigner. With IHTMLEditServices::AddDesigner, the method can pass the edit designer pointer to add the annotator designer. Add the following code to Annotator.cpp:

// IAnnotator
STDMETHODIMP CAnnotator::AttachAnnotator(IHTMLDocument2* pDoc)
{
    CComPtr<IServiceProvider> spSP;
    CComPtr<IHTMLEditServices> spES;

    m_spDoc = pDoc; // IUnknown::AddRef is automatically called 
                    // on smart pointer m_spDoc

    m_spDoc->QueryInterface(IID_IServiceProvider, (void**)&spSP);

    spSP->QueryService(SID_SHTMLEditServices, 
                       IID_IHTMLEditServices, 
                       (void**)&spES);

    this->QueryInterface(IID_IHTMLEditDesigner, (void**)&m_spDesigner);

    spES->AddDesigner(m_spDesigner);

    return S_OK;
}

DetachAnnotator queries for IServiceProvider and IHTMLEditServices and removes the designer. DetachAnnotator then releases m_spDoc and m_spDesigner by assigning NULL to the variables. Add the following code to Annotator.cpp:

STDMETHODIMP CAnnotator::DetachAnnotator()
{
    CComPtr<IServiceProvider> spSP;
    CComPtr<IHTMLEditServices> spES;

    m_spDoc->QueryInterface(IID_IServiceProvider, (void**)&spSP);

    spSP->QueryService(SID_SHTMLEditServices, 
                       IID_IHTMLEditServices, 
                       (void**)&spES);

    spES->RemoveDesigner(m_spDesigner);

    m_spDesigner = (IHTMLEditDesigner*)NULL; // Assignment of NULL causes automatic
                                             // call to IUnknown::Release on the
                                             // m_spDesigner smart pointer.
    return S_OK;
}

ShowCommentGlyphs queries for an IOleCommandTarget interface. Depending on the value of the bShow Boolean passed into it, ShowCommentGlyphs then executes either the IDM_ADDTOGLYPHTABLE or IDM_EMPTYGLYPHTABLE command. Add the following code to Annotator.cpp:

STDMETHODIMP CAnnotator::ShowCommentGlyphs(BOOL bShow)
{
    CComVariant vGlyphTableEntry;
    CComPtr<IOleCommandTarget> pCmdTarg;

    m_spDoc->QueryInterface(IID_IOleCommandTarget, (void**)&pCmdTarg);

    if (bShow)
    {
        //Load Glyph Table Entry into VARIANT
        vGlyphTableEntry =
            "%%comment^^%%res://EDannotator.dll/comment.gif^^%%0^^%%3^^%%3^^%%4^^%%20^^%%15^^%%20^^%%15^^**";

        // Exec IDM_ADDTOGLYPHTABLE
        pCmdTarg->Exec(&CGID_MSHTML, 
                       IDM_ADDTOGLYPHTABLE,
                       OLECMDEXECOPT_DODEFAULT, 
                       &vGlyphTableEntry, 
                       NULL);
    }
    else
        pCmdTarg->Exec(&CGID_MSHTML, 
                       IDM_EMPTYGLYPHTABLE, 
                       OLECMDEXECOPT_DODEFAULT, 
                       NULL, 
                       NULL);

    return S_OK;
}

IOleCommandTarget is declared in Docobj.h; the command IDs are declared in Mshtmcid.h; and the command group GUID, CGID_MSHTML, is declared in Mshtmhst.h. Add the three include directives for these files to the top of Annotator.cpp:

// Annotator.cpp : Implementation of CAnnotator

#include "stdafx.h"
#include "EDAnnotator.h"
#include "Annotator.h"
#include "docobj.h"
#include "mshtmcid.h"
#include "mshtmhst.h"

Add the following AddComment method skeleton to Annotator.cpp:

STDMETHODIMP CAnnotator::AddComment()
{

}

At this point, do a test build of the Annotator control to make sure your code is correct to this point.

Note  You can also use a wizard to add interface method definitions and implementations. In the ClassView pane, right-click either of the IAnnotator listings. Choose Add Method. The Add Method to Interface dialog box appears, in which you can enter a method name and signature. When you're done, click OK. The wizard adds the method definition to the IDL file, a declaration to the header file, and a skeleton definition to the .cpp source file.

 

Adding the Comment Glyph Image Resource

The next step is to supply the Annotator designer with the comment image it displays in the ShowCommentGlyph method. There are a number of ways to do this—a reliable one is to add the image directly to the DLL as a resource contained in the DLL. To get the image, right-click the Comment.gif image , and select Save Picture As. Place the GIF in the Annotator project directory. Next, directly edit the resource file, EDAnnotator.rc. To do this, click Open from the File menu. In the Open dialog box, click the drop-down arrow on the Open as: box and change it to "Text." Select EDAnnotator.rc.

Tip: Make sure the Open dialog box is pointing to the correct project directory. It sometimes opens on a directory from a previous project.

Click Open. Scroll down to the Bitmap, REGISTRY, and String Table definitions. Add the following (above the Bitmap definitions is a good place):

/////////////////////////////////////////////////////////////////////////////
//
// HTML
//

comment.gif              HTML    DISCARDABLE     "comment.gif"

Warning  Attempting to import a GIF resource is not recommended (for example, choosing Resource from the Insert menu and clicking the Custom button, or right-clicking the EDAnnotator resources in the Resource pane and choosing Import...). The attempt can cause problems with Visual C++ 6.

 

The Logic of PreHandleEvent

All of the Annotator's designer implementation occurs in IHTMLEditDesigner::PreHandleEvent, with support from helper functions to open and close the edit box for a comment. The other methods, IHTMLEditDesigner::PostHandleEvent, IHTMLEditDesigner::TranslateAccelerator, and IHTMLEditDesigner::PostEditorEventNotify, are not implemented and return S_FALSE.

IHTMLEditDesigner::PreHandleEvent contains the logic to filter events. It uses a switch statement to filter for the mouseover, mouseout, and mouseup events using the DISPID parameter passed into IHTMLEditDesigner::PreHandleEvent. The mouseover handler changes the mouse pointer from a cursor to an arrow when the pointer moves over a comment element. The mouseout handler restores the cursor. The mouseup handler opens a comment edit box when a click occurs on a comment element. It closes the box when a click occurs anywhere but in an open comment edit box. The logic has been structured so that only one comment edit box can be open at a time.

Note  While it might seem that IHTMLEditDesigner::PreHandleEvent could intercept the onclick event, onclick is a synthesized event and does not cause a call the IHTMLEditDesigner::PreHandleEvent method, though it causes calls to IHTMLEditDesigner::PostHandleEvent and IHTMLEditDesigner::PostEditorEventNotify.

 

The first step toward implementing IHTMLEditDesigner::PreHandleEvent is to add member variables to the CAnnotator class. The implementation also needs two helper functions. The helper functions are added later, in the next two sections. Double-click CAnnotator in the ClassView pane to open Annotator.h. Scroll to the bottom and add three member variables to the class:

private:
    CComPtr<IHTMLDocument2> m_spDoc;
    CComPtr<IHTMLEditDesigner> m_spDesigner;
    CComPtr<IHTMLElement> m_spSrcElem;
    CComPtr<IHTMLElement> m_spCommentEditBox;
    HCURSOR m_hOldCursor;
};

#endif //__ANNOTATOR_H_

Now you can add the implementation of IHTMLEditDesigner::PreHandleEvent. Open Annotator.cpp, find the definition of IHTMLEditDesigner::PreHandleEvent, and add the following code to fill out the method. This code calls two new functions, OpenCommentEditBox and CloseCommentEditBox, which you declare and define in the following sections.

STDMETHODIMP
CAnnotator::PreHandleEvent(DISPID inEvtDispId, IHTMLEventObj *pIEventObj)
{
    USES_CONVERSION;

    CComQIPtr<IHTMLElement> spSrcElem;
    CComQIPtr<IHTMLElement3> spSrcElem3;
    VARIANT_BOOL bIsEditable;

    pIEventObj->get_srcElement(&spSrcElem);
    spSrcElem->QueryInterface(IID_IHTMLElement3, (void**)&spSrcElem3);
    spSrcElem3->get_isContentEditable(&bIsEditable);

    // Reject all events that aren't in content-editable areas
    if (bIsEditable == VARIANT_FALSE) return S_FALSE;

    CComQIPtr<IOleCommandTarget> spCmdTarg;
    CComBSTR bstrSrcTagName;
    CComVariant var;
    LPSTR szName;
    BOOL bAreObjectsEqual = FALSE;

    m_spDoc->QueryInterface(IID_IOleCommandTarget, (void**)&spCmdTarg);

    spSrcElem->get_tagName(&bstrSrcTagName);
    szName = OLE2A(bstrSrcTagName);

    switch (inEvtDispId)
    {
    case DISPID_HTMLELEMENTEVENTS2_ONMOUSEOVER:

        // Note: strcmp assumes its parameters to be null-terminated strings
        if (!strcmp(szName, "!") || !strcmp(szName, "COMMENT"))
        {
            // Set cursor over comment to arrow
            var = TRUE;
            spCmdTarg->Exec(&CGID_MSHTML, 
                            IDM_OVERRIDE_CURSOR,
                            OLECMDEXECOPT_DODEFAULT,
                            &var,
                            NULL);
            HCURSOR arrow = LoadCursor(NULL,IDC_ARROW);
            m_hOldCursor = SetCursor(arrow);
        }
        break;

    case DISPID_HTMLELEMENTEVENTS2_ONMOUSEOUT:

        if (!strcmp(szName, "!") || !strcmp(szName, "COMMENT"))
        {
            // Set cursor over comment back to auto
            var = FALSE;
            SetCursor(m_hOldCursor);
            spCmdTarg->Exec(&CGID_MSHTML, 
                            IDM_OVERRIDE_CURSOR, 
                            OLECMDEXECOPT_DODEFAULT, 
                            &var, 
                            NULL);
        }
        break;

    case DISPID_HTMLELEMENTEVENTS2_ONMOUSEUP:

        // Check if current src element is the same as any saved src element
        // (which would be the comment that is currently being edited)
        // If it is, close the comment edit box without opening a new one.
        if (m_spSrcElem)
            bAreObjectsEqual = spSrcElem.IsEqualObject(m_spSrcElem);

        // If there is an open comment edit box already and the mouse release
        // didn't occur in the edit box, close the comment edit box
        if (m_spCommentEditBox &&
            !spSrcElem.IsEqualObject(m_spCommentEditBox))
                CloseCommentEditBox();

        // If there isn't an open comment edit box already, and
        // if the click was not on a glyph to close the comment edit box, and
        // if the click occurred on a comment element,
        // then open a comment edit box
        if (!m_spCommentEditBox &&
            !bAreObjectsEqual &&
            (!(strcmp(szName, "!")) || !(strcmp(szName, "COMMENT"))))
                OpenCommentEditBox(spSrcElem);
        
        break;
    }

    return S_FALSE;
}

Add an include directive to the top of this file to bring in Mshtmdid.h (where the DISPIDs used in the switch statement are defined):

// Annotator.cpp : Implementation of CAnnotator
   .
   .
   .
#include "mshtmhst.h"
#include "mshtmdid.h"

Opening the Comment Edit Box

Now it's time to add the OpenCommentEditBox method. The implementation of CloseCommentEditBox is covered in the next section, but let's add the declarations for both methods now. Open Annotator.h and add these two declarations between the data member declarations and the private: label just above them:

// Helper Methods
private:
    STDMETHOD(OpenCommentEditBox)(IHTMLElement* pSrcElem);
    STDMETHOD(CloseCommentEditBox)();

private:
    CComPtr<IHTMLDocument2> m_spDoc;
    CComPtr<IHTMLEditDesigner> m_spDesigner;
    CComPtr<IHTMLElement> m_spSrcElem;
    CComPtr<IHTMLElement> m_spCommentEditBox;
    HCURSOR m_hOldCursor;
};

#endif //__ANNOTATOR_H_

Now open Annotator.cpp and add the OpenCommentEditBox method definition:

//Helper methods
STDMETHODIMP 
CAnnotator::OpenCommentEditBox(IHTMLElement* pSrcElem)
{
    USES_CONVERSION;

    CComPtr<IHTMLElement> spBody;
    CComPtr<IHTMLElement2> spBody2;
    CComPtr<IHTMLElement2> spSrcElem2;
    CComPtr<IHTMLElement2> spCommentEditBox2;
    CComPtr<IHTMLElement3> spCommentEditBox3;
    CComPtr<IHTMLStyle> spStyle;
    CComPtr<IHTMLStyle2> spStyle2;
    CComBSTR bstrOuterHTML;
    CComBSTR spanString;
    CComBSTR tagName;
    CComVariant vBoxLeft, vBoxTop, vBoxWidth, vBoxHeight, vBoxBGColor;
    LPTSTR szName;

    // Retrieve entire comment and tag name
    m_spSrcElem = pSrcElem;
    m_spSrcElem->get_outerHTML(&bstrOuterHTML);
    m_spSrcElem->get_tagName(&tagName);
    szName = OLE2T(tagName);

    // Extract text from comment depending on comment style
    LONG lCommentLength = bstrOuterHTML.Length();

    if (!_tcscmp(szName, _T("!")))
    {
        for (int i = 4; i < lCommentLength - 3; i++)
        {
            LPTSTR onechar = (LPTSTR)&bstrOuterHTML.m_str[i];
            spanString.Append(onechar);
        }
    }

    if (!_tcscmp(szName, _T("COMMENT")))
    {
        int endbracket = 8;
        LPTSTR onechar = (LPTSTR)&bstrOuterHTML.m_str[endbracket];

        while (_tcscmp(onechar, _T(">"))) 
            onechar = (LPTSTR)&bstrOuterHTML.m_str[++endbracket];

        for (int i = ++endbracket; i < lCommentLength - 10; i++)
        {
            LPTSTR onechar = (LPTSTR)&bstrOuterHTML.m_str[i];
            spanString.Append(onechar);
        }
    }

    // Disable resizing handles on while in comment edit mode
    CComPtr<IOleCommandTarget> spCmdTarg;
    m_spDoc->QueryInterface(IID_IOleCommandTarget, (void**)&spCmdTarg);
    spCmdTarg->Exec(&CGID_MSHTML, 
                    IDM_DISABLE_EDITFOCUS_UI, 
                    OLECMDEXECOPT_DODEFAULT, 
                    NULL, 
                    NULL);

    // Create the temporary comment edit box
    m_spDoc->createElement(L"span", &m_spCommentEditBox);
    m_spCommentEditBox->put_innerHTML(spanString);
    
    // insertAdjacentElement must be called after put_innerHTML 
    // or the undo command can erase the comment box's contents
    // insertAdjacentElement protects put_innerHTML from 
    // inclusion in undo stack
    m_spDoc->get_body(&spBody);
    spBody->QueryInterface(IID_IHTMLElement2, (void**)&spBody2);
    spBody2->insertAdjacentElement(L"beforeEnd", m_spCommentEditBox, NULL);

    CComPtr<IMarkupServices> spMUS;
    CComPtr<IMarkupPointer> spMUP;
    CComPtr<IDisplayServices> spDS;
    CComPtr<IDisplayPointer> spDP;
    CComPtr<ILineInfo> spLineInfo;
    POINT ptDPLocation;

    // Query for IMarkupServices and 
    // IDisplayServices interfaces
    m_spDoc->QueryInterface(IID_IMarkupServices, (void**)&spMUS);
    m_spDoc->QueryInterface(IID_IDisplayServices, (void**)&spDS);

    // Create markup pointer and move to end of source element
    spMUS->CreateMarkupPointer(&spMUP);
    spMUP->MoveAdjacentToElement(m_spSrcElem, ELEM_ADJ_AfterEnd);

    // Create display pointer and place at markup pointer
    spDS->CreateDisplayPointer(&spDP);
    spDP->MoveToMarkupPointer(spMUP, NULL);

    // Get ILineInfo interface from which to 
    // retrieve display pointer location
    spDP->GetLineInfo(&spLineInfo);
 
    // Get display pointer location and
    // transform to global coordinate system
    spLineInfo->get_x(&(ptDPLocation.x));
    spLineInfo->get_baseLine(&(ptDPLocation.y));

    spDS->TransformPoint(&ptDPLocation,
                         COORD_SYSTEM_CONTENT, 
                         COORD_SYSTEM_GLOBAL, 
                         m_spSrcElem);

    // Transfer position, size and color for box to
    // VARIANT structures
    vBoxLeft = ptDPLocation.x;
    vBoxTop = ptDPLocation.y;
    vBoxWidth = 225;
    vBoxHeight = 150;
    vBoxBGColor = "#ff6666";

    // Set positioning and styles for comment edit box
    m_spCommentEditBox->get_style(&spStyle);
    spStyle->QueryInterface(IID_IHTMLStyle2, (void**)&spStyle2);
    spStyle2->put_position(L"absolute");
    spStyle->put_top(vBoxTop);
    spStyle->put_left(vBoxLeft);
    spStyle->put_width(vBoxWidth);
    spStyle->put_height(vBoxHeight);
    spStyle->put_backgroundColor(vBoxBGColor);
    spStyle->put_padding(L"5");
    spStyle->put_border(L"thin solid #666666");

    // Make Comment Edit Box contentEditable
    m_spCommentEditBox->QueryInterface(IID_IHTMLElement3, 
                                       (void**)&spCommentEditBox3);
    spCommentEditBox3->put_contentEditable(L"true");

    // Place insertion point in box
    m_spCommentEditBox->QueryInterface(IID_IHTMLElement2, 
                                       (void**)&spCommentEditBox2);
    spCommentEditBox2->focus();

    return S_OK;
}

Closing the Comment Edit Box

Add the CloseCommentEditBox method implementation immediately following the OpenCommentEditBox method implementation:

STDMETHODIMP 
CAnnotator::CloseCommentEditBox()
{
    USES_CONVERSION;

    CComBSTR tagName;
    CComBSTR commentString;
    CComPtr<IHTMLWindow2> spWin;
    CComPtr<IHTMLElement> spBodyElem;
    CComPtr<IHTMLElement> spParElem;
    CComPtr<IHTMLDOMNode> spParElemNode;
    CComPtr<IHTMLDOMNode> spTempCommentBoxNode;
    CComPtr<IMarkupServices> spMUServ;
    CComPtr<IMarkupPointer> spMPSrcBeg;
    CComPtr<IMarkupPointer> spMPSrcEnd;
    CComPtr<IHTMLCommentElement> spCommentElement;
    CComPtr<IOleCommandTarget> spCmdTarg;

    m_spSrcElem->get_tagName(&tagName);
    LPSTR szName = OLE2A(tagName);

    // Format new comment from comment edit box inner text
    // Note: two different styles of comment
    if (!strcmp(szName, "!"))
        commentString = "<!--";

    CComBSTR spanString;
    m_spCommentEditBox->get_innerText(&spanString);
    commentString.AppendBSTR(spanString);

    if (!strcmp(szName, "!"))
        commentString.Append(_T("-->"));

    // Place new text in original comment
    m_spSrcElem->QueryInterface(IID_IHTMLCommentElement, 
                                (void**)&spCommentElement);
    spCommentElement->put_text(commentString);

    // Remove comment edit box
    m_spCommentEditBox->get_parentElement(&spParElem);

    m_spCommentEditBox->QueryInterface(IID_IHTMLDOMNode, 
                                       (void**)&spTempCommentBoxNode);

    spParElem->QueryInterface(IID_IHTMLDOMNode, 
                              (void**)&spParElemNode);

    spParElemNode->removeChild(spTempCommentBoxNode, NULL);

    // Release interface pointers; assignment of NULL causes automatic
    // call to IUnknown::Release on CComPtr smart pointers
    m_spCommentEditBox = (IHTMLElement*)NULL;
    m_spSrcElem = (IHTMLElement*)NULL;

    // Reactivate resizing handles when leaving comment edit mode
    m_spDoc->QueryInterface(IID_IOleCommandTarget, (void**)&spCmdTarg);
    spCmdTarg->Exec(&CGID_MSHTML, 
                    IDM_DISABLE_EDITFOCUS_UI, 
                    OLECMDEXECOPT_DODEFAULT, 
                    NULL, 
                    NULL);

    return S_OK;
}

Adding Comments

Now that all parts of the IHTMLEditDesigner implementation are in place, go back and add the implementation of AddComment. Add the following method implementation to Annotator.cpp in the skeleton of AddComment you created earlier:

STDMETHODIMP
CAnnotator::AddComment()
{
    CComPtr<IDisplayServices> spDS;
    CComPtr<IHTMLCaret> spCaret;
    CComPtr<IDisplayPointer> spDP;
    CComPtr<IHTMLElement> spNewComment;
    CComPtr<IHTMLElement> spFElem;
    CComPtr<IHTMLElement3> spFElem3;
    CComPtr<IHTMLWindow2> spWin;
    CComPtr<IMarkupServices> spMS;
    CComPtr<IMarkupPointer> spMP;
    VARIANT_BOOL bIsEditable;

    // Retrieve interface for insertion point,
    // place display pointer at its location, and retrieve the
    // flow element containing the display pointer
    m_spDoc->QueryInterface(IID_IDisplayServices, (void**)&spDS);
    spDS->CreateDisplayPointer(&spDP);
    spDS->GetCaret(&spCaret);
    spCaret->MoveDisplayPointerToCaret(spDP);
    spDP->GetFlowElement(&spFElem);

    // Check if display pointer is in content-editable area
    spFElem->QueryInterface(IID_IHTMLElement3, (void**)&spFElem3);
    spFElem3->get_isContentEditable(&bIsEditable);
    if (bIsEditable != VARIANT_TRUE) return S_OK;

    // Check to see if comment is open;
    if (m_spCommentEditBox)
    {
        // If so, check to see if caret is in comment edit box
        spDP->GetFlowElement(&spFElem);

        if (spFElem.IsEqualObject(m_spCommentEditBox))
        {
            // If so, alert that comment can't be added to comment and return
            m_spDoc->get_parentWindow(&spWin);
            spWin->alert(L"Cannot add comment to comment.");
            return S_OK;
        }
        else
            CloseCommentEditBox();
    }

    // Add the new comment and open the comment edit box for it
    m_spDoc->QueryInterface(IID_IMarkupServices, (void**)&spMS);

    spMS->CreateElement(TAGID_COMMENT,
                        NULL,
                        &spNewComment);

    spMS->CreateMarkupPointer(&spMP);
    spDP->PositionMarkupPointer(spMP);

    spMS->InsertElement(spNewComment, spMP, NULL);
    OpenCommentEditBox(spNewComment);

    return S_OK;
}

Creating a Web Page for the Annotator

At this point, you have a working Annotator designer. Now all you need is a Web page on which to run it. Visual C++ 6 creates a test Web page as part of an ActiveX control project. You can start building your Web page from this test page. On the File menu, click Open; click the drop-down arrow on the Files of type: box and choose "Web Files (.htm;.html;.htx;.asp;.stm;.shtml)." Select Annotator.htm and click Open. You can see that the test page already has an object tag with the correct IHTMLObjectElement::classid for the Annotator component. First, move the object tag from the document's body to its head.

<HTML>
<HEAD>
<TITLE>ATL 3.0 test page for object Annotator</TITLE>
<OBJECT ID="Annotator" CLASSID="CLSID:9790C4C2-7BFF-49C1-9439-068EE3B8DC22"></OBJECT>
</HEAD>
<BODY>

</BODY>
</HTML>

This step is necessary because the Annotator doesn't have a user interface. If the object tag is left in the body, the page displays a blank box showing the location in the document of the ActiveX control.

Note  If you look at your copy of EDAnnotator.idl, you'll notice that the CLASSID in the object tag in the Annotator test page is taken from the coclass definition in the library block. (The following example shows the location of the class ID in the IDL, not the actual value of the ID. The class ID in your project will be different.)

library EDANNOTATORLib
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");

    [
        uuid(9790C4C2-7BFF-49C1-9439-068EE3B8DC22),
        helpstring("Annotator Class")
    ]
    coclass Annotator
    {
        [default] interface IAnnotator;
    };
};

 

Copy the following code snippet to the body of Annotator.htm. It adds some buttons that show and hide the comment glyphs, and adds a button to create new comments. The buttons' onclick event handlers call into the Annotator component to execute the scriptable methods created earlier in this tutorial. The code snippet also adds a content-editable area with some pre-established text and comments.

<DIV STYLE="position:absolute;top:10;left:20;padding:10">

<SPAN CLASS="button" UNSELECTABLE="on" STYLE="cursor:hand" 
ONCLICK="document.all.Annotator.ShowCommentGlyphs(true)">Show Comment Glyphs</SPAN>
<BR><BR>
<SPAN CLASS="button" UNSELECTABLE="on" STYLE="cursor:hand" 
ONCLICK="document.all.Annotator.ShowCommentGlyphs(false)">Hide Comment Glyphs</SPAN>
<BR><BR>
<SPAN CLASS="button" UNSELECTABLE="on" STYLE="cursor:hand" 
ONCLICK="document.all.Annotator.AddComment()">Add Comment</SPAN>
</DIV>

<DIV ID="editregion" CONTENTEDITABLE="true">
<P>The Annotator page demonstrates an edit designer implementation
that lets you add, remove, and modify comments in a Web page. Individual 
comments are marked with custom glyphs, so it's easy to locate them. You might 
use comments like this to keep track of ideas in a document in progress, make 
notes for later markup changes, or explain parts of your text to other browsing 
users without the need to include your explanation in the page text.</P>

<P>Click the
<!-- This comment uses left-bracket/exclamation-point tags. -->
Show Comment Glyphs button above.</P>

<P>Now you can see glyphs that mark the location of comments in the 
content-editable region.
Click any glyph to edit the comment it denotes.<COMMENT ID="pook">This comment 
uses comment tags.</COMMENT> 
When you click again anywhere in the page other than the comment edit 
window, the revised comment is saved and the comment edit window closes.</P>

<P>Add a comment to the page in the same way.  Click the Add Comment button, add 
text to the comment edit window, and click outside the edit window to save and 
close the comment.</P>
<!-- This comment is located at the end of the content-editable region. -->
</DIV>

Add the following two style definitions—for the buttons and the editable region—to the HEAD element:

<STYLE>
.button {
    background-color:#009999;
    border:thin outset;
    padding:3;
    color:#ffffff;
    font-weight:bold;
}
#editregion {
    position:absolute;
    top:150;
    left:30;
    width:400;
    height:300;
    overflow:auto;
    border:ridge;
    background-color:#ffffff;
    padding:5;
}
</STYLE>

Finally, add an onload event handler to the BODY tag to attach the Annotator once the document is loaded. The following snippet also includes a background color definition:

<BODY ONLOAD="document.all.Annotator.AttachAnnotator(document)" BGCOLOR="#cc9999">

Running the Annotator

If all has gone well, you can browse to the Annotator.htm test page using Internet Explorer 5.5 or later, and the Annotator component runs. You can also run the component through the Visual C++ 6 debugger. On the Project menu, click Settings and go to the Debug tab. Click the arrow to the right of the Executable for debug session box and choose Default Web Browser.

Note  Be sure that Internet Explorer is your default browser.

 

In the Program arguments box, type the path to the sample page—for example, "C:\EDAnnotator\Annotator.htm." Click OK. Now, in the Visual C++ 6 window, click the red ! button (the Execute Program button) on the debug toolbar to run the component on the Annotator.htm page. Alternatively, choose Execute IEXPLORE.EXE from the Build menu to run the sample page. You can also run the Annotator component in debug mode by choosing Go after the Start Debug command from the Build menu, or by clicking the Go button on the debug toolbar.

Deploying the Annotator component to the Web is beyond the scope of this article. However, let's consider some of the steps necessary for deployment to the Web. The first consideration is registering the Annotator on other computers. If you were to copy the Annotator.htm and EDAnnotator.dll to another computer directly, the Annotator would not work. The Annotator works on the computer on which it was built because Visual C++ 6 registers the COM component with Windows as part of the build process. A standard way to deploy ActiveX components to the Web is to place the DLL in a cabinet file along with an .inf file that causes the control to be registered as it is downloaded. You also need to add a IHTMLObjectElement4::codeBase attribute to the object tag giving the URL to the cabinet file. A search of Msdn.microsoft.com provides you many resources on creating cabinet files and .inf files for ActiveX controls.

It's also important to consider security when downloading controls to other computers. Most default installations of Internet Explorer will not run an ActiveX control unless the cabinet file containing a control has been digitally signed with a certificate that identifies the maker of the control and verifies that the control is safe. This verification enhances the security afforded by the implementation of IObjectSafety that you added to the control. For more information on digital signature, see Using Cryptography. For a general discussion on how to design secure ActiveX controls, see Designing Secure ActiveX Controls.

Conceptual

Introduction to MSHTML Editing

Activating the MSHTML Editor

Modifying Documents in Edit Mode

Using the MSHTML Editor's Extra Features

Using Editing Glyphs

Implementing IHTMLEditHost

About Edit Designers

Implementing Edit Designers: The Basics