MSHTML Hosting – Editing

Did you know that you can turn WebBrowser into a decent WYSIWYG HTML editor? It’s true and it’s relatively simple. There are actually two ways to turn on the editor mode: contentEditable markup attribute, and IHTMLDocument2::designMode method. Since we are talking about embedding WebBrowser in an application, the IHTMLDocument2::designMode method is the most typical choice.

The basic steps are:

  1. Navigate to “about:blank”
  2. Wait for the blank page to load
  3. set designMode to “on”

At this point you should be able to type, select, cut, copy and paste into the WebBrowser. But more than that, the editor allows you to quickly add various kinds of HTML formatting markup, such as: Font (Name, Size and Style), Colors, Horizontal lines, Paragraph and line breaks, Justification, and Indenting. It does this through a nice little interface called IOleCommandTarget. The interface has only two methods: QueryStatus and Exec. The interface can be used with many different OLE servers, such as Microsoft Word and Excel, but it is especially useful with WebBrowser.

Using IOleCommandTarget

At the heart of IOleCommandTarget is command identifiers. All commands supported by an implementer of IOleCommandTarget must have an identifier. Here is a list of MSHTML commands. Getting an instance of IOleCommandTarget is pretty simple: QueryInterface IHTMLDocument2 for it.


IHTMLDocument2* pDoc = ...;

// Turn on editor mode
pDoc->put_designMode(CComBSTR(L"on"));

// Execute some commands
IOleCommandTarget* pCmdTarget = 0;
hr = pDoc->QueryInterface(IID_IOleCommandTarget, (void**)&pCmdTarget);
if (SUCCEEDED(hr)) {
  // do commands here
  // ...
  pCmdTarget->Release();
}

QueryStatus provides a way to get the current state of a command. It is of limited value for most commands. Most uses include retreiving the current value of formatting commands. So, if you want to know if a command is supported, QueryStatus does the job. In addition, QueryStatus can be used to determine whether some toggle-type commands, such as IDM_BOLD, are “on” or “off”. The following code checks to see if various Edit menu items should be enabled based on the current selection or clipboard contents:


OLECMD Cmnds[5];
::ZeroMemory(Cmnds, sizeof(OLECMD)*5);
Cmnds[0].cmdID = IDM_COPY;
Cmnds[1].cmdID = IDM_PASTE;
Cmnds[2].cmdID = IDM_CUT;
Cmnds[3].cmdID = IDM_DELETE;
Cmnds[4].cmdID = IDM_SELECTALL;

if (SUCCEEDED(pCmdTarget->QueryStatus(&CGID_MSHTML, 5, Cmnds, NULL)))
{
  bool bCanCopy = Cmnds[0].cmdf & OLECMDF_ENABLED;
  bool bCanPaste = Cmnds[1].cmdf & OLECMDF_ENABLED;
  bool bCanCut = Cmnds[2].cmdf & OLECMDF_ENABLED;
  bool bCanDelete = Cmnds[3].cmdf & OLECMDF_ENABLED;
  bool bCanSelectAll = Cmnds[4].cmdf & OLECMDF_ENABLED;
}

Exec is pretty simple. It provides the way to change the current value, insert a new item or perform an action. Exec allows commands to pass extra information or retrieve information via two VARIANT parameters. Some examples:


// Set current selection to bold
pCmdTarget->Exec(&CGID_MSHTML, IDM_BOLD,
                 OLECMDEXECOPT_DONTPROMPTUSER, NULL, NULL);

// Set current selection to tahoma
pCmdTarget->Exec(&CGID_MSHTML, IDM_FONTNAME,
                 OLECMDEXECOPT_DONTPROMPTUSER, CComVariant("Tahoma"), NULL);

// Set current selection to font size 2 (HTML font sizes, not points)
pCmdTarget->Exec(&CGID_MSHTML, IDM_FONTSIZE,
                 OLECMDEXECOPT_DONTPROMPTUSER, CComVariant(2), NULL);

// Copy current selection to clipboard
pCmdTarget->Exec(&CGID_MSHTML, IDM_COPY,
                 OLECMDEXECOPT_DONTPROMPTUSER, NULL, NULL);

MSHTML Hosting – IDocHostUIHandler

We can’t discuss embedding WebBrowser in an application without also discussing IDocHostUIHandler. The IDocHostUIHandler (and IDocHostShowUI) interface is the standard method provided by Microsoft for customizing how WebBrowser works when hosted in an application. Some of the things you can use IDocHostUIHandler to do include:

  • Disable standard right-click, context menu (or provide your own version)
  • Turn off the 3D border and scrollbars
  • Give the WebBrowser scripting engine access to your special purpose COM methods
  • Handle accelerator keys and URL’s before MSHTML gets them

IDocHostUIHandler is an interface that you need to implement. How you implement depends on your development language and environment. For most languages, it means deriving a class from IDocHostUIHandler, adding code to the methods you want and returning safe, default values from those you do not want.

The next step is giving MSHTML your implementation of IDocHostUIHandler. Again, this depends on your development language and environment. The easy way is the “ICustomDoc” method. The best way is the “IOleClientSite” method. There is a nice C# article on CodeProject that discusses both methods (as well as some other good WebBrowser information).

ICustomDoc

This method requires only IDocHostUIHandler to be implemented which keeps things simpler. However, you have to give the interface to MSHTML after each page is loaded. The best way to do it is in the OnDocumentComplete or OnNavigationComplete events. This is the method I currently use in my applications. Code looks something like this:


IHTMLDocument2* pDoc = ...;
ICustomDoc* pCustom = 0;
hr = pDoc->QueryInterface(IID_ICustomDoc, (void**)&pCustom);
if (SUCCEEDED(hr)) {
  pCustom->SetUIHandler(pMyDocHostUIHandler);
  pCustom->Release();
}

IOleClientSite

This method requires that you implement IOleClientSite, as well as IDocHostUIHandler. The benefit is that you only need to give the interfaces to MSHTML once, before any HTML is loaded. I have not used this method in my applications, so I have no code to show you. I am planning on using it soon. I’ll update this post when I am finished.

I am switching from ICustomDoc to IOleClientSite mainly because of a transient problem. When the WebBrowser control loads my custom HTML, it waits until it is completely loaded before getting my IDocHostUIHandler, so there is a split second where the borders and scrollbars are not displaying correctly. With IOleClientSite, WebBrowser asks for my IDocHostUIHandler before it loads the HTML.