Currently I am working on embedding a HTML editor into a C++ WinAPI application using the MSHTML component.
I got everything set up (activating editing mode, changing font face, etc.), but now I have to support inserting images. MSHTML already has support for it built in, but this support is - to my knowledge - not enough.
Somehow I need a way to intercept the insertion of images into the HTML-editor, since I have to create a list of images in the UI of our application. So, whenever the user uses the default-dialog of the MSHTML-component to insert an image or updates its source (e.g. from file://xyz.jpg to file://abc.jpg), I want my code to be notified.
I already looked at the conecpt of "Edit Designers", the implementation of IHTMLEditHost, or the DWebBrowserEvents2 interface. But nothing seems to do the trick.
Perhaps someone can give me a hint?
Okay,
it looks like you cannot explicitly subscribe for specific changes of the document. What you can do is to create a so-called "Change Sink".
Everytime you change the document, either by user input or programmatically, you can get a notification that "something" changed in your document. This can be done by implementing the IHTMLChangeSink interface and attaching it to the IMarkupContainer2 of the IHTMLDocument2.
Example code (not complete):
class CHTMLChangeSink : public IHTMLChangeSink
{
public:
// Implement IUnknown: QueryInterface, AddRef, Release
// Implement IHTMLChangeSink:
HRESULT STDMETHODCALLTYPE Notify()
{
// Notify() is called everytime the document changes
return S_OK;
}
};
CHTMLChangeSink * pChangeSink = new CHTMLChangeSink();
IHTMLDocument2 * pDoc; // lets suppose we already have it
IMarkupContainer2 * pMarkupContainer = nullptr;
if (pDoc->QueryInterface(IID_IMarkupContainer2, reinterpret_cast<void **>(&pMarkupContainer)) == S_OK) {
DWORD dwCookie = 1;
// registration is done here
pMarkupContainer->RegisterForDirtyRange(pChangeSink, &dwCookie);
}
Please be aware, that the document has to be loaded completely (register for DIID_DWebBrowserEvents2::DocumentComplete).
From now on, whenever a change in the document occurs, your Notify-method will be called and you can do further processing to find out what has changed (e.g. process the list of images in the document).
Have fun!
Related
I have inherited some legacy code that i'm required to integrate with a modern c# GUI. The codebase is a MFC MDI application, that creates a type library and registers a COM component to expose the application API for external applications and scripting.
Throughout the MFC application (henceforth I will refer to the MFC code as "the application") there is a function that checks for an active MDI document and returns it, or null. This method is called from a "MainFrame" class which inherits CMDIFrameWnd. it looks something like this:
CMDIChildWnd * pChildFrame = MDIGetActive();
if (pChildFrame)
{
CDocument *pDoc = (CDocument *) pChildFrame->GetActiveDocument();
if (pDoc)
{
return(pDoc);
} else {
return NULL;
}
I have created a test c# console application and can successfully import the COM component and access the API, and make calls to it. The problem is that whenever I call something that requires an active document via the API, MDIGetActive() returns null. For example, I can open a document via the API, and I can visually confirm it opens in the running MFC application. But if I call the API method to save this file, the active document is null. But if I call the same method via the application GUI, this works fine. It is the same function call, the exposed API method is just a wrapper.
Strangely enough when I open a file via the API method, it eventually executes the same check for an active document which succeeds. After getting the active document, it calls CDocument::UpdateAllViews() to update the UI. Any calls made after this via the API will result in no active document.
I'm at a loss here, I can't understand why the active document is null. I'm still working my way through MFC documentation but I haven't found anything that would suggest why this is the case. Does anyone know?
Another way to avoid any GetActiveWhatever() method is the following code, which can be called from your CYourApp class:
POSITION posDoc, pos = GetFirstDocTemplatePosition();
while (NULL != pos)
{
CDocTemplate* pDocTemplate = (CDocTemplate*)GetNextDocTemplate(pos);
posDoc = pDocTemplate->GetFirstDocPosition();
while(NULL != posDoc)
{
CDocument* pDoc = pDocTemplate->GetNextDoc(posDoc);
if(NULL != pDoc)
pDoc->UpdateAllViews(pSender, lHint, pHint);
}
}
Of course, once you have the document, you'll have any view attached from that document.
Does anyone know which relation may exist between registration-free COM and drag/drop functionality?
Specifically, we have a huge C++ CAD/CAM application comprising a number of EXEs and several hundreds DLLs. Many of them serve as COM servers (both in-proc and out-of-proc) and/or clients, and also implement ActiveX controls.
The most of ActiveX controls and the main CMDIFrameWnd-based window of one of EXEs implement drag/drop functionality. ActiveX controls implement the both drop source and drop target, and the main window is only drop target, in particular, for files from Windows Explorer.
The drag/drop implementation is pretty standard and based on two data members derived from COleDataSource and COleDropTarget for drop source and drop target respectively. The COleDropTarget-derived member is registered with respective window in the window's OnCreate method. It also overrides OnDragEnter, OnDragOver and OnDrop methods in a similar way. Namely, the system-supplied COleDataObject parameter is asked for specific format (in particular, CF_HDROP), and in the case of positive answer, the data (e.g., file path) is extracted from the clipboard. The code looks like the following:
static FORMATETC g_FileFmt = {CF_HDROP, 0, DVASPECT_CONTENT, 0, TYMED_HGLOBAL};
....
// Inside OnDragEnter, OnDragOver or OnDrop method
STGMEDIUM stgmedium = {0,0,0};
if (pDataObject->IsDataAvailable(g_FileFmt.cfFormat))
{
HRESULT hr = pDataObject->GetData(g_FileFmt.cfFormat, &stgmedium);
HDROP hdrop = (HDROP)GlobalLock(stgmedium.hGlobal);
if (hdrop != 0)
{
int FilesCount = DragQueryFile(hdrop, (UINT)-1, 0, 0);
if (FilesCount != 0)
{
TCHAR FileName[_MAX_PATH];
DragQueryFile(hdrop, 0, FileName, _MAX_PATH);
// Check file extension and store the file name for farther use.
}
GlobalUnlock(hdrop);
}
}
The drop source implementation is also straightforward and looks like the following:
void CDmDocListCtrl::OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
if (pNMListView->iItem != -1 && m_pOleDataSource && prv_BeginDrag())
{
DROPEFFECT DE = m_pOleDataSource->DoDragDrop(
DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK, 0);
}
*pResult = 0;
}
where prv_BeginDrag() function collects dragged data, packs it and puts on the clipboard by calling SetData method from the m_pOleDataSource object's IDataObject interface.
The all this stuff worked perfectly until it was decided to make the whole application registration-free. It took me three months to force the application run isolated (without registration of COM components) by embedding manifests, launching out-of-proc COM servers on demand and altering CLSID of some classes in order to separate instances of the same server launched from different folders. At last it begins to work - but without drag/drop functionality, despite it wasn't even touched by my changes.
On the drop target side, when I drag file from Windows Explorer, depicted above call to COleDataObject::IsDataAvailable returns false, although before my changes returned true. At the same time, if I add a single line of code "DragAcceptFiles();" to the main window's OnCreate method, drag/drop begins working via the standard CFrameWnd's WM_DROPFILE message handler.
On the drop source side, the dragged data are successfully packed and placed on the clipboard, but COleDataSource::DoDragDrop method fails, because a call to ::DoDragDrop API inside MFC implementation returns REGDB_E_CLASSNOTREG "Class not registered" result.
It means, that COM activation changes somehow influence drag/drop behavior. How?
P.S. 1) The EXE, to which I drag files from Windows Explorer, has in its project properties "UAC Execution Level = asInvoker". As far as I understand, it tells that the EXE will run at the same UAC level as Windows Explorer when launched by double-click on the file.
2) Quite surprisingly, although drag/drop stopped working with symptoms described above, Copy/Paste continues work well, despite the both technologies have similar implementation.
3) I believe, that if find out when ::DoDragDrop API returns "Class not registered" error, and which class it is looking for, it would be possible to solve the problem.
Thanks for help,
Ilia.
Following to MartinBa advice, I solved the problem with the help of Process Monitor. The Process Monitor showed me that while I drag an item in the ActiveX control (mentioned in the question), the system unsuccessfully tries get access to a class ID in the Registry. Looking for that ID, I found that it is really not class ID, but IDataObject interface ID. It was referenced in one of my manifest files.
The most of manifests I have written by hand, but a few, especially at the beginning of the project having no experience in the area, I generated automatically by Visual Studio from existing type library. In one of them Studio included the comInterfaceExternalProxyStub statement for a couple of system interfaces, in which proxyStubClsid32 element was (erroneously) equal to the interface ID.
I'm still not sure whether those system interfaces should present in the manifest; for example, the IDataObject is only mentioned as a method's parameter in one of IDL definitions. Anyway, I corrected only the proxyStubClsid32 value, and the problem disappeared...
The moral of this very painful for me story is to always check output of automatic tools...
Update: Answer: Two normal lines of code required. Thanks Noseratio!
I banged my head on the keyboard for more hours than I would have cared to trying to simulate IEs Ctrl+N behavior in my hosted Browser control app. Unfortunately, due to complications which I've abstracted out of my code examples below, I can't just let IE do Ctlr+N itself. So I have to do it manually.
Keep in mind that I am running a hosted browser. So typically, opening links in new windows will actuall open it within a new "tab" within my application (it's not really a tab, but another window... but appearance-wise it's a tab). However, Ctrl+N is different -- here, it is expected a fully-fledged IE window will launch when pressed.
I think my problem is that of framing the questions -- admittedly I am new to WebBrowser control and I find it to be a lot of yucky. Regardless, I've scoured the Internet for the past day and couldn't come up with an elegant solution.
Basically, the ideal solution would be to call a "NewWindow" function within WebBrowser control or its affiliate libraries; however, all I was able to find where the *On*NewWindow methods, which were event handlers, not event signallers. Which I understand that most of the time, the user will be creating the events... but what about programmatic simulation?
I tried looking into an SENDMESSAGE approach where I could use the IDs that the OnNewWindow events use... that ended up in nothing than crashes. Perhaps I could go back to get it work, but I'd like confirmation is that approach is even worth my time.
The next approach, which should have been the most elegeant, but sadly didn't pan out, was like the following:
Navigate2(GetLocationURL().GetBuffer(), BrowserNavConstants::navOpenInNewWindow);
It would have worked marvelously if it weren't for the fact that the new window would open in the background, blinking in the taskbar. needing clicking to bring it to the front.
I tried to get around the limitation in a myriad of ways, including getting the dispatcher of the current context, then calling OnNewWindow2 with that IDispatch object. Then I would invoke QueryInterface on the dispatch object for an IWebBrowser control. The webBrowser control (presumably under the control of the new window) could then navigate to the page of the original context. However... this too was a pretty messy solution and in the end would cause crashes.
Finally, I resorted to manually invoking JavaScript to get the desired behavior. Really?? Was there really no more elegant a solution to my problem than the below mess of code?
if ((pMsg->wParam == 'N') && (GetKeyState(VK_CONTROL) & 0x8000) && !(GetKeyState(VK_SHIFT) & 0x8000) && !(GetKeyState(VK_MENU) & 0x8000))
{
LPDISPATCH pDisp = CHtmlView::GetHtmlDocument();
IHTMLDocument2 *pDoc;
if (SUCCEEDED(pDisp->QueryInterface(IID_IHTMLDocument2, (void **)&pDoc)))
{
IHTMLWindow2* pWnd;
pDoc->get_parentWindow(&pWnd);
BSTR bStrLang = ::SysAllocString(L"JavaScript");
CString sCode(L"window.open(\"");
sCode.Append(GetLocationURL().GetBuffer());
sCode.Append(L"\");");
BSTR bStrCode = sCode.AllocSysString();
COleVariant retVal;
pWnd->execScript(bStrCode, bStrLang, retVal);
::SysFreeString(bStrLang);
::SysFreeString(bStrCode);
pDoc->Release();
}
pDisp->Release();
I find it hard to believe that I must resort to such hackery as this to get something as simple as opening a new window when the user presses Ctrl+N.
Please stackoverflow, please point out the clearly obvious thing I overlooked.
Ctrl-N in IE starts a new window on the same session. In your case, window.open or webBrowser.Navigate2 will create a window on a new session, because it will be run by iexplore.exe process which is separate from your app. The session is shared per-process, this is how the underlying UrlMon library works. So you'll loose all cookies and authentication cache for the new window. On the other hand, when you create a new window which hosts WebBrowser control within your own app process, you'll keep the session.
If such behavior is OK for your needs, try first your initial Navigate2 approach, precededing it with AllowSetForegroundWindow(ASFW_ANY) call. If the new window still doesn't receive the focus correctly, you can try creating an instance of InternetExplorer.Application out-of-proc COM object, and use the same IWebBrowser2 interface to automate it. Below is a simple C# app which works OK for me, the new window is correctly brought to the foreground, no focus issues. It should not be a problem to do the same with MFC.
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace IeApp
{
public partial class MainForm : Form
{
// get the underlying WebBrowser ActiveX object;
// this code depends on SHDocVw.dll COM interop assembly,
// generate SHDocVw.dll: "tlbimp.exe ieframe.dll",
// and add as a reference to the project
public MainForm()
{
InitializeComponent();
}
private void NewWindow_Click(object sender, EventArgs e)
{
AllowSetForegroundWindow(ASFW_ANY);
// could do: var ie = new SHDocVw.InternetExplorer()
var ie = (SHDocVw.InternetExplorer)Activator.CreateInstance(Type.GetTypeFromProgID("InternetExplorer.Application"));
ie.Visible = true;
ie.Navigate("http://www.example.com");
}
const int ASFW_ANY = -1;
[DllImport("user32.dll")]
static extern bool AllowSetForegroundWindow(int dwProcessId);
}
}
The MSDN documentation on WebBrowser Customization explains how to prevent new windows from being opened and how to cancel navigation. In my case, my application is hosting an IWebBrowser2 but I don't want the user to navigate to new pages within my app. Instead, I'd like to open all links in a new IE window. The desired behavior is: user clicks a link, and a new window opens with that URL.
A similar question was asked and answered here and rather than pollute that answered post, it was suggested I open a new discussion.
The members on the related post suggested I should be able to do this by trapping DISPID_BEFORENAVIGATE2, setting the cancel flag, and writing code to open a new window, but I've found out that the browser control gets lots of BeforeNavigate2 events that seem to be initiated by scripts on the main page. For example, amazon.com fires BeforeNavigate2 events like crazy, and they are not a result of link invocation.
Replies appreciated!
What I ended up doing was using IHTMLDocument directly rather than IWebBrowser. IWebBrowser is a superset of IHTMLDocument, and the navigation model implemented by IWebBrowser isn't customizable to the degree I wanted.
I actually got MS Developer Support involved and this approach was their recommendation. They say this is what Outlook uses for HTML-based email, which is the user experience I wanted to emulate. They also confirmed that there's no reliable way to filter the OnBeforeNavigate events that result from user action from those that result from script activity.
Hope this helps anybody facing the same issues. It wasn't too hard to port the code to use IHTMLDocument. If you end up doing this, you may also find yourself looking for a way to figure out when the document is done loading. To do that, hook HTMLDocumentEvents instead of DWebBrowserEvents, and look for the DISPID_HTMLDOCUMENTEVENTS_ONREADYSTATECHANGE event. It doesn't tell you what the ready state is; you need to call IHTMLDocument::get_readyState and parse the resulting string. Goofy, but there you go.
You can bind to onclick event before document is complete while creating browser in OnCreate() using IHTMLDocument2::put_onclick():
#include <comutil.h>
ClickEvents<RootFrame> clickEvents;
_variant_t clickDispatch;
clickDispatch.vt = VT_DISPATCH;
clickDispatch.pdispVal = &clickEvents;
CComQIPtr<IDispatch> dispatch;
hr = webBrowser2->get_Document(&dispatch);
ASSERT_EXIT(SUCCEEDED(hr), "webBrowser->get_Document(&dispatch)");
CComQIPtr<IHTMLDocument2> htmlDocument2;
hr = dispatch->QueryInterface(IID_IHTMLDocument2, (void**) &htmlDocument2);
ASSERT_EXIT(SUCCEEDED(hr), "dispatch->QueryInterface(&htmlDocument2)");
htmlDocument2->put_onclick(clickDispatch);
ClickEvents class implements IDispatch, you only need to implement Invoke method, in rest return E_NOTIMPL:
HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags,
DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
HRESULT hr;
CComQIPtr<IWebBrowser2> webBrowser2;
hr = rootFrame->GetDlgControl(rootFrame->rootview.GetDlgCtrlID(), IID_IWebBrowser2, (void**) &webBrowser2);
ASSERT_EXIT(SUCCEEDED(hr), "rootframe->GetDlgControl(IID_IWebBrowser2) failed");
CComQIPtr<IDispatch> dispatch;
hr = webBrowser2->get_Document(&dispatch);
ASSERT_EXIT(SUCCEEDED(hr), "webBrowser2->get_Document(&dispatch)");
CComQIPtr<IHTMLDocument2> htmlDocument2;
hr = dispatch->QueryInterface(IID_IHTMLDocument2, (void**) &htmlDocument2);
ASSERT_EXIT(SUCCEEDED(hr), "dispatch->QueryInterface(&htmlDocument2)");
CComQIPtr<IHTMLWindow2> htmlWindow2;
hr = htmlDocument2->get_parentWindow((IHTMLWindow2**) &htmlWindow2);
ASSERT_EXIT(SUCCEEDED(hr), "htmlDocument2->get_parentWindow(&htmlWindow2)");
CComQIPtr<IHTMLEventObj> htmlEvent;
hr = htmlWindow2->get_event(&htmlEvent);
ASSERT_EXIT(SUCCEEDED(hr), "htmlWindow2->get_event(&htmlEvent)");
CComQIPtr<IHTMLElement> htmlElement;
hr = htmlEvent->get_srcElement(&htmlElement);
ASSERT_EXIT(SUCCEEDED(hr), "htmlEvent->get_srcElement(&htmlElement)");
CComBSTR hrefAttr(L"href");
VARIANT attrValue;
VariantInit(&attrValue);
hr = htmlElement->getAttribute(hrefAttr, 0 | 2, &attrValue); // 0 = case insensitive, 2 = return BSTR
ASSERT_EXIT(SUCCEEDED(hr), "htmlElement->getAttribute()");
wchar_t href[2084]; // maximum url length in IE, http://support.microsoft.com/kb/208427
wcsncpy_s(href, _countof(href), attrValue.bstrVal, _TRUNCATE);
if (!rootFrame->IsURLAllowed(href)) {
VARIANT variant;
variant.vt = VT_BOOL;
variant.boolVal = VARIANT_FALSE;
htmlEvent->put_returnValue(variant);
ShellExecute(0, L"open", href, 0, 0, SW_SHOWNORMAL);
}
return S_OK;
}
As you can see after querying some interfaces I finally have the element that got clicked, then I call IsURLAllowed() defined in my root frame to check whether to allow opening url in current webbrowser window or whether to open it using default browser on user's computer.
This handles all links even if they were appended to document using javascript.
The same should be done with "onsubmit" events for forms.
I also think I have a solution for "window.location" redirects in javascript, I haven't tested it yet, but I will soon test it and I will update this answer then. You could use a combination of "onunload" and "onbeforeunload" events along with DWebBrowserEvents2::BeforeNavigate2(), after onunload/onbeforeunload are called you will know that user is leaving current page so now in BeforeNavigate2() you can cancel it. You can attach unload events using IHTMLWindow2::put_onunload() and IHTMLWindow2::put_onbeforeunload().
See sources of a complete solution for the "onclick" below.
AttachClickEvents in BrowserFrame:
http://code.google.com/p/phpdesktop/source/browse/phpdesktop-msie/msie/browser_frame.h?r=709d00b991b5#125
Invoke in ClickEvents(IDispatch):
http://code.google.com/p/phpdesktop/source/browse/phpdesktop-msie/msie/click_events.h?r=a5b0b350c933#132
I'm hypothesising here but yet another approach could be to maintain a count of navigation events, incrementing the counter on DISPID_BEFORENAVIGATE2 and decrementing it on occurrences of DISPID_NAVIGATECOMPLETE2 and DISPID_NAVIGATEERROR. With that in place, you could speculate that whenever you get DISPID_BEFORENAVIGATE2 and your counter is at zero, it is actual user navigation / link invocation.
I have no idea whether this approach would work, or whether those are the right events you'd need to make it work, but it could be worth investigating.
You could try a different approach instead and physically add the attribute target="_blank"
to all <a> tags in the rendered document.
This approach would involve waiting for DISPID_DOCUMENTCOMPLETE and then using IHTMLDocument3::getElementsByTagName() to fetch all of the anchor elements. You would then use IHTMLElement::setAttribute() to set target="_blank" on each of them.
It seems to me, that it you want "to open all links in a new IE window", it means that you want that the opening of new windows must be done in another process. The easiest way to do so: using CreateObject("InternetExplorer.Application") way (see another question which solve a problem, which is opposite to your question: InternetExplorer.Application object and cookie container). With this way you will receive the best isolation from your application and the user who clicks on the link receive all possibilities which exist in IE. You should of cause continue usage of BeforeNavigate2 events to find out the moment when "a new IE window" should be opened.
When the user creates a new document in my SDI-application, I need to present a dialog specifying details on the document to be created (think: resolution, bit-depth, etc.) I initially put the code for displaying this dialog in OnNewDocument() (I don't need it when opening an existing document), but putting user-interface code in the document-class just doesn't feel right (also, I don't have any CWnd* to use as a parent for the dialog).
Is there a better place to do this in MFC?
You're right, the document class is no good place for UI.
CDocTemplate::[OpenDocumentFile][1](pszPath) looks like a better candidate:
pszPath==NULL means 'create a new document'.
The method is virtual -> Just derive CMySingleDocTemplate from CSingleDocTemplate and use an instance of this class in CMyWinApp::InitInstance().
This class is responsible for creating docs, frames and views, hence I think it's a good place to put a UI operation.
BOOL CMyWinApp::InitInstance()
{
...
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CMySingleDocTemplate( // <--Derives from CSingleDocTemplate
IDR_MAINFRAME,
RUNTIME_CLASS(CMyDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CMyView));
AddDocTemplate(pDocTemplate);
...
}
CDocument* CMySingleDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,
BOOL bMakeVisible)
{
CDocument *pDoc =
CSingleDocTemplate::OpenDocumentFile(lpszPathName, bMakeVisible);
if (lpszPathName==NULL)
{
// GUI to get user info
// update doc
m_pOnlyDoc->Blah(input);
// update view
m_pOnlyDoc->UpdateAllViews(NULL,...,...);
}
}
This might not be ideal though: In SDI, there is one and only doc object. It's re-used accross File/Load and File/New operation.
This function will then be called a first time before the initial mainframe is created. You may not want to have a dialog presented to user before the frame is created. Ouch! It's a little more complicated:
Instead of popping up a GUI in in OpenDocumentFile(NULL) as above, just post a custom message/command to the main frame. Then add a handler that will react by the sequence pop up GUI/update doc/update views. That way, the main frame will be displayed before the GUI is popped up and your user will be happier.
This also solves your problem where you don't have a CWnd parent: the main frame is already created and your dialog will use it byt default.
BTW, another solution consists in adding a command handler for ID_FILE_NEW in your CMyWinApp's message map and add your own override of OnFileNew(). But when you write OnFileNew(), I believe you'll quickly find out that it's an ugly solution :-(