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.
<pre lang=c++>
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.
<pre lang=c++>
#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.

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.
<pre lang=c++>
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.

Detach() looks like this.
<pre lang=c++>
void CMSHTMLDisableDragHTMLEditDesigner::Detach()
{
if (m_pServices != (IHTMLEditServices *) NULL)
m_pServices->RemoveDesigner(this);
}
The code within the PreHandleEvent() method might look like this.
<pre lang=c++>
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.
<pre lang=c++>
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!

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
<pre lang=c++>
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
<pre lang=c++>
m_pDoc = (IHTMLDocument2 *) GetHtmlDocument();
m_designer.Detach();
m_designer.Attach(m_pDoc);
Voila, you're done!

Share

About the Author

I've been programming for 35 years - started in machine language on the National Semiconductor SC/MP chip, moved via the 8080 to the Z80 - graduated through HP Rocky Mountain Basic and HPL - then to C and C++ and now C#.

I used (30 or so years ago when I worked for Hewlett Packard) to repair HP Oscilloscopes and Spectrum Analysers - for a while there I was the one repairing DC to daylight SpecAns in the Asia Pacific area.

Afterward I was the fourth team member added to the Australia Post EPOS project at Unisys Australia. We grew to become an A$400 million project. I wrote a few device drivers for the project under Microsoft OS/2 v 1.3 - did hardware qualification and was part of the rollout team dealing directly with the customer.

Born and bred in Melbourne Australia, now living in Scottsdale Arizona USA, became a US Citizen on September 29th, 2006.

I work for a medical insurance broker, learning how to create ASP.NET websites in VB.Net and C#. It's all good.