Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using IHTMLEditDesigner

0.00/5 (No votes)
24 Apr 2004 1  
Using IHTMLEditDesigner to modify IE's editing behaviour

Introduction

My current project requires the ability to do WYSIWYG HTML editing. A quick look at the MFC class heirarchy revealed CHtmlEditView. An even quicker session with the MFC AppWizard and I had an SDI WYSIWYG HTML editor up and running, using the MSHTML COM object that ships as part of Internet Explorer.

My application, however, doesn't create arbitrary web pages. To create a new page you select a template HTML file and alter existing content. It was important that the layout of the page remain substantially unaltered. This meant that, for example, it should be possible to replace a placeholder image with a real image, resize it to fit the allotted space but not be able to drag the image to a different location on the page.

Solving this little problem turned out to be quite an interesting exercise.

Background

I used CHtmlEditView, one of the new classes introduced in MFC 7. It's a CHtmlView derived class and one of the very very few MFC classes that uses multiple inheritance. It's derived from both the CHtmlView and the CHtmlEditCtrlBase classes. The view inheritance lets it be used in document/view applications. The CHtmlEditCtrlBase adds a whole bunch of capabilities related specifically to HTML editing.

We're not really going to be discussing either of those base classes. However, the CHtmlView class contains a CWnd member which becomes the MSHTML COM object hosted within the class, and a pointer to an IWebBrowser2 COM interface. That interface, in turn, contains methods to navigate to new pages, refresh the current page and so on. We're not interested, for the purpose of this article, in that interface because it's primarly oriented toward browsing.

MSHTML

This is the COM object hiding behind Microsoft Internet Explorer. Internet Explorer itself is little more than a wrapper around MSHTML. This is actually pretty cool from our perspective because it means that our software can host MSHTML and obtain, almost for free, HTML display and editing capabilities. MSHTML includes a complete WYSIWYG HTML editor. All we have to do is host the MSHTML object and provide a user interface. All?

Well there's the little matter of understanding the Document Object Model (DOM) and making sense of a couple of hundred COM interfaces.

I don't intend to exhaustively discuss either the DOM or the many COM interfaces. This article is focussed on demonstrating how to modify the editors behaviour.

Edit Designers

As Internet Explorer evolves so too does MSHTML evolve, providing us with more and more ways to interrogate and control HTML display. Version 5.5 introduced Edit Designers and the IHTMLEditDesigner interface. This interface is essentially a collection of 4 callbacks which MSHTML makes to code we control. Each callback handles MSHTML editing events at various points in the lifetime of the event. The lifetime points are.
  • PreHandleEvent
  • PostHandleEvent
  • TranslateAccelerator
  • PostEditorEventNotify
Of these callbacks PreHandleEvent() is probably the most useful. MSHTML calls into our code to notify us that it's about to do something and we have the chance to modify that behaviour or to cancel it entirely.

So, inspired by what you've read so far, you eagerly fire up your copy of VS .NET, go to the help index and type in IHTMLEditDesigner. You read the description of the PreHandleEvent() method and see that the prototype for the method is.

HRESULT PreHandleEvent(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);
And the comments say this:

'The DISPID parameter provides the most efficient way for an IHTMLEditDesigner method to determine what type of event triggered the method call. The DISPID_HTMLELEMENTEVENTS2 identifiers are defined in Mshtmdid.h.' (Direct quote from MSDN help in VS .NET 2003).

Aha! Let's go and look at the DISPID_HTMLELEMENTEVENTS2 constants. Here's a short selection.

#define DISPID_HTMLELEMENTEVENTS2_ONMOUSEMOVE  DISPID_EVMETH_ONMOUSEMOVE
#define DISPID_HTMLELEMENTEVENTS2_ONMOUSEDOWN  DISPID_EVMETH_ONMOUSEDOWN
#define DISPID_HTMLELEMENTEVENTS2_ONMOUSEUP    DISPID_EVMETH_ONMOUSEUP
#define DISPID_HTMLELEMENTEVENTS2_ONBLUR       DISPID_EVMETH_ONBLUR
#define DISPID_HTMLELEMENTEVENTS2_ONRESIZE     DISPID_EVMETH_ONRESIZE
#define DISPID_HTMLELEMENTEVENTS2_ONDRAG       DISPID_EVMETH_ONDRAG
#define DISPID_HTMLELEMENTEVENTS2_ONDRAGEND    DISPID_EVMETH_ONDRAGEND
#define DISPID_HTMLELEMENTEVENTS2_ONDRAGENTER  DISPID_EVMETH_ONDRAGENTER
A definite hint that if one goes to the trouble of working out how to define and implement an IHTMLEditDesigner interface, and then attach it to the HTML editor one will be notified when these events (among many others) occurs.

So let's code it and see how it works. This is the header for my CMSHTMLDisableDragHTMLEditDesigner class derived from IHTMLEditDesigner. (Can I lay a claim to the longest classname on CP?).

class CMSHTMLDisableDragHTMLEditDesigner : public IHTMLEditDesigner
{
public:
    virtual HRESULT STDMETHODCALLTYPE 
            QueryInterface(REFIID riid, void __RPC_FAR *__RPC_FAR *ppvObject);
    virtual ULONG   STDMETHODCALLTYPE
            AddRef(void);
    virtual ULONG   STDMETHODCALLTYPE
            Release(void);

    virtual HRESULT STDMETHODCALLTYPE
            PreHandleEvent(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);
    virtual HRESULT STDMETHODCALLTYPE
            PostHandleEvent(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);
    virtual HRESULT STDMETHODCALLTYPE
            TranslateAccelerator(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);
    virtual HRESULT STDMETHODCALLTYPE
            PostEditorEventNotify(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);

                    CMSHTMLDisableDragHTMLEditDesigner();

    BOOL            Attach(IHTMLDocument2 *pDoc);
    void            Detach();

private:
    IHTMLEditServices *m_pServices;
    UINT            m_uRefCount;
    CMSHTMLDisableDragIDispatch m_dp;
};
IHTMLEditDesigner is derived from IUnknown so we must provide those standard methods.

Adding an Event Designer to the editor

Let's be very clear on this. MSHTML would probably (if it were sentient) rather we didn't go messing about with its event handling. So it's not about to go and create an IHTMLEditDesigner instance just because we wrote one. It doesn't even know our Edit Designer exists! We (the application writer hosting MSHTML in our application) want to modify MSHTML's behaviour, so we're the ones who have to create an instance of our IHTMLEditDesigner derived object and tell MSHTML to use it. Because we're the ones creating the object we get to decide if it's going to live in a COM DLL or in our exe file (the one that's hosting MSHTML). If it's in our exe file there's no need for CoCreateInstance(). Just do a m_designer = new CMSHTMLDisableDragHTMLEditDesigner or embed an instance of the object in your view and let c++ instantiation take care of the rest.

The documentation is rather less than explicit on the question of the lifetime of an IHTMLEditDesigner connection, so I've assumed that it lasts just as long as the currently loaded HTML document (probably a reasonable assumption given that we're attaching our Edit Designer to a HTML Document object). So I attach the Edit Designer in the views OnDownloadComplete() event. The code looks like this.

void CMyHTMLEditView::OnDownloadComplete(LPCTSTR lpszURL)
{
    // other code that's irrelevant to this discussion

    .
    .
    .
    CHtmlEditView::OnDownloadComplete(lpszURL);
    m_pDoc = (IHTMLDocument2 *) GetHtmlDocument();
    m_designer.Detach();
    m_designer.Attach(m_pDoc);
}
m_pDoc is a pointer to the DOM for the current HTML page and m_designer is an embedded instance of CMSHTMLDisableDragHTMLEditDesigner in the view. Just to play safe I do a Detach() and then reattach m_designer to the IHTMLDocument2 interface. If the designer wasn't already attached to a document the detach does nothing - otherwise it removes itself from that documents list of IHTMLEditDesigner instances. This way I don't have to keep track of whether an Edit Designer has already been attached or not.

Attach() looks like this.

BOOL CMSHTMLDisableDragHTMLEditDesigner::Attach(IHTMLDocument2 *pDoc)
{
    if (m_pServices != (IHTMLEditServices *) NULL)
        m_pServices->Release();

    IServiceProvider *pTemp;

    if (pDoc == (IHTMLDocument2 *) NULL)
        return FALSE;

    pDoc->QueryInterface(IID_IServiceProvider, (void **) &pTemp);

    if (pTemp != (IServiceProvider *) NULL)
    {
        pTemp->QueryService(SID_SHTMLEditServices, IID_IHTMLEditServices, 
                                (void **) &m_pServices);

        if (m_pServices != (IHTMLEditServices *) NULL)
        {
            m_pServices->AddDesigner(this);
            return TRUE;
        }
    }

    return FALSE;
}
This queries the document COM object for an IServiceProvider interface from the document and then requests a IHTMLEditServices interface from the IServiceProvider interface. We then add ourselves, as a designer, to the IHTMLEditServices interface. If all of this succeeds MSHTML will call the various methods in our IHTMLEditServices derived object whenever anything interesting happens.

Detach() looks like this.

void CMSHTMLDisableDragHTMLEditDesigner::Detach()
{
    if (m_pServices != (IHTMLEditServices *) NULL)
        m_pServices->RemoveDesigner(this);
}
The code within the PreHandleEvent() method might look like this.
CMSHTMLDisableDragHTMLEditDesigner::PreHandleEvent(DISPID inEvtDispId, 
                                         IHTMLEventObj *pIEventObj)
{
    if (inEvetDispId == DISPID_HTMLELEMENTEVENTS2_ONDRAG)
        pIEventObj->Cancel();
}
given that the purpose of the class is to disable dragging. (The Cancel() function isn't really there, I'm trying to keep it simple for illustration). Looks good. We're confident that we've read and understood the docs so let's compile it and give it a run.

It doesn't bloody work!!! It doesn't crash but it certainly doesn't see a DISPID_HTMLELEMENTEVENTS2_ONDRAG event. Our end user can click on an image in our WYSIWYG HTML editor and move that image around until their arms fall off and there's nothing we can do about it!

So what went wrong?

Let's go back over what we've done to be sure we didn't miss anything. We implemented a class derived from IHTMLEditDesigner. We followed the MSDN documentation that tells us how to add our IHTMLEditDesigner object. And if we were to add a trace statement before the if test in CMSHTMLDisableDragHTMLEditDesigner::PreHandleEvent we'd see many many calls to the function. So what went wrong?

The documentation doesn't match the behaviour! I should state that this is what I've seen using Internet Explorer 6 with all current security updates applied, on Windows 2000 Service Pack 4 and all current hotfixes. The only notifications I see in my PreHandleEvent() are the raw mouse events, that is, DISPID_HTMLELEMENTEVENTS2_ONMOUSEDOWN, DISPID_HTMLELEMENTEVENTS2_ONMOUSEMOVE and DISPID_HTMLELEMENTEVENTS2_ONMOUSEUP.

Now if you've followed this far you've probably guessed that there is a solution to the problem. If there weren't I'd have probably posted a few questions on various message boards and given up.

The Solution

Lies in interpreting the data we're sent during a callback. In addition to the event identifier we get a pointer to an IHTMLEventObj interface which lets us query various things about the event. One of the things we can query is the srcElement which gives us a pointer to an IHTMLElement interface. If we look at that interface we see there's an onDragStart method which allows us to substitute an event handler which will be called when the user initiates a drag operation. We could set the event handler when we see a DISPID_HTMLELEMENTEVENTS2_ONMOUSEDOWN event.

The event handler

The event handler we provide to the onDragStart() method needs to be a IDispatch pointer with a default function that takes no parameters. I'd show you the class definition but it's literally just an IDispatch interface. Nothing special there.

CMSHTMLDisableDragDispatch::GetTypeInfoCount() returns 0, indicating there are no type information interfaces. CMSHTMLDisableDragDispatch::GetTypeInfo() returns DISP_E_BADINDEX no matter what parameters you pass and CMSHTMLDisableDragDispatch::GetIDsOfNames() returns DISP_E_UNKNOWNNAME whatever the requested ID's. So far it's a pretty minimal implementation of IDispatch. The real work (and the only purpose the class has) is in the Invoke() method.

HRESULT STDMETHODCALLTYPE CMSHTMLDisableDragDispatch::Invoke(
    DISPID /*dispIdMember*/, REFIID /*riid*/, LCID /*lcid*/, 
    WORD /*wFlags*/, DISPPARAMS * /*pDispParams*/,
    VARIANT *pVarResult, EXCEPINFO * /*pExcepInfo*/, 
    UINT * /*puArgErr*/)
{
    //  If we were installed it means we should disable

    //  dragging. So set the return value to false

    pVarResult->vt = VT_BOOL;
    pVarResult->boolVal = false;
    return S_FALSE;
}
Even though there are a bunch of parameters passed to the function the only one we're interested in is the pVarResult one. Invoke() sets it to a false boolean and returns. Setting it to false cancels the event. Bingo, dragging is disabled!

Installing the event handler

is done in the IHTMLEditDesigner::PreHandleEvent() function thusly.
HRESULT STDMETHODCALLTYPE 
    CMSHTMLDisableDragHTMLEditDesigner::PreHandleEvent(
        DISPID inEvtDispId, IHTMLEventObj *pIEventObj)
{
    USES_CONVERSION;
    IHTMLElement  *pSel;
    BSTR          b = BSTR(NULL);

    if (inEvtDispId == DISPID_HTMLELEMENTEVENTS2_ONMOUSEDOWN)
    {
        if (pIEventObj != (IHTMLEventObj *) NULL)
        {
            pIEventObj->get_srcElement(&pSel);

            //  We've got our source element, get its tag

            if (pSel != (IHTMLElement *) NULL)
            {
                pSel->get_tagName(&b);

                if (_tcsicmp(_T("IMG"), W2A(b)) == 0)
                {
                    //  We only install the ondragstart handler if the

                    //  element is an IMG.

                    VARIANT v;

                    v.vt = VT_DISPATCH;
                    v.pdispVal = &m_dp;

                    pSel->put_ondragstart(v);
                }
            }
        }
    }

    return S_FALSE;
}
m_dp is an instance of CMSHTMLDisableDragDispatch embedded in our Edit Designer.

You'll notice there's a test on the elements Tag property. That's because I only wanted to disable dragging of images. If we install the event handler on any srcElement we disable dragging for all objects on the page. By checking for an IMG tag we ensure that we're installing the event handler only for image objects.

You'll notice the demo project uses CodeProject as the HTML document (what else would it use?). You'll also notice that some images are still dragable. Bob for instance. That's because Bob's tag is AREA not IMG. You can see where this is going...

Using the code

To use the Edit Designer in your own projects you need to add the source files in the source download. Then, in your edit view you add two data members
IHTMLDocument2  *m_pDoc;
CMSHTMLDisableDragHTMLEditDesigner m_designer;
Add an OnDownloadComplete() function to your view (the wizard can do this for you) and add the following lines in the function
m_pDoc = (IHTMLDocument2 *) GetHtmlDocument();
m_designer.Detach();
m_designer.Attach(m_pDoc);
Voila, you're done!

History

28 March 2004 - Initial version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here