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)
{
.
.
.
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 , REFIID , LCID ,
WORD , DISPPARAMS * ,
VARIANT *pVarResult, EXCEPINFO * ,
UINT * )
{
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);
if (pSel != (IHTMLElement *) NULL)
{
pSel->get_tagName(&b);
if (_tcsicmp(_T("IMG"), W2A(b)) == 0)
{
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.