I’m writing up these notes in order to document what has been a long and painful process, involving much spelunking through MSHTML.DLL and IEFRAME.DLL to try and understand what Internet Explorer (or more accurately, the WebBrowser control) is doing and how to correctly use the semi-documented interfaces to provide full control over a print job.
The original requirement for this mini-project was to provide tray, collation, and duplex control for a HTML print job using IHTMLDocument2.execCommand(IDM_PRINT), with a custom print template. These functions had been supported through a 3rd party ActiveX component, but this component proved to be incompatible with Internet Explorer 9 (causing a blue screen would you believe!), and the company providing the component was defunct, so it fell to me to re-engineer the solution.
After considerable research, I found some sparse documentation on MSDN suggesting that one could pass a HTMLDLG_PRINT_TEMPLATE flag to ShowHTMLDialogExand thereby duplicate and extend the functionality of the print template. In particular, the __IE_CMD_Printer_Devmode property that could somehow be passed into this function would give us the ability to control anything we liked in terms of the printer settings.
Too easy. Much too easy. The first stumbling block was trying to discover the type of the pvarArgIn parameter to ShowHTMLDialogEx. A variant array seemed sensible but did not work. It turns out that this needs to be an IHTMLEventObj, which can be created with IHTMLDocument4.CreateEventObject. You can then use IHTMLEventObj2.setAttribute to set the various attributes for the object.
Then there were questions about what IMoniker magic was needed for the pMk parameter. And more questions about the most appropriate set of flags. Diving into the debugger to examine what Microsoft did answered both of these questions — it was a simple CreateURLMonikerEx call, no need to bind the moniker or other magic, and the flags that Microsoft used were HTMLDLG_ALLOW_UNKNOWN_THREAD or HTMLDLG_NOUI or HTMLDLG_MODELESS or HTMLDLG_PRINT_TEMPLATE for a print job, or HTMLDLG_ALLOW_UNKNOWN_THREAD or HTMLDLG_MODAL or HTMLDLG_MODELESS or HTMLDLG_PRINT_TEMPLATE for a print preview job. Yes, that is both HTMLDLG_MODAL and HTMLDLG_MODELESS!
Next, what variant type should the __IE_BrowseDocument attribute be? VT_DISPATCH or VT_UNKNOWN? The answer is VT_UNKNOWN — things just won’t work if you pass a VT_DISPATCH. I also came unstuck on the __IE_PrinterCmd_DevMode and __IE_PrinterCmd_DevNames attributes. These need to be a VT_I4 containing an unlocked HGLOBAL that references a DEVMODEW structure. I’ll leave the setup of the DEVMODEW structure to you: there are a lot of examples of that online.
However, even after overcoming these hurdles (with copious debugging to understand what MSHTML.DLL and IEFRAME.DLL were doing), there were other issues. First, the print template was unable to access the dialogArguments.__IE_BrowseDocument property, with an Access Denied error thrown. Also, HTC behaviors would fail to load as the WebBrowser component believed that they were being referenced in an insecure, cross-domain manner. And finally, JavaScript in the page being printed was failing to run — and this JavaScript was required to render some of the details of the page.
I knew that Microsoft actually pass a reference to a temporary file for printing in the __IE_ContentDocumentURL attribute. So I saved the file to a temporary file, which also required adding a BASE element to the header so that relative URLs in the document would resolve. But the problems had not gone away.
All three of these problems in reality stemmed from the same root cause. The security IDs for the various elements — the print template, the document being printed, and the HTC components — were not matching. So I embarked on an attempt to find out why. At first I wondered if we needed to bind the moniker to a bind context or storage. That was a no-go. Then I looked at the IInternetSecurityManager interface, which a developer can implement to provide custom security IDs, zones and more. Sounds logical, right? Only problem is that the ShowHTMLDialogEx function provides its own IInternetSecurityManager implementation, which you cannot override (and its GetSecurityID just returns INET_E_DEFAULTACTION for the relevant URLs). Yikes.
I was starting to run out of options. As far as I could tell, we were duplicating Microsoft’s functionality essentially identically, and I could not see any calls which changed the security for the document so that it would match security contexts.
Finally I noticed an undocumented attribute had been added to the HTML element in the temporary copy of the page: __IE_DisplayURL. And as soon as I added that to my file, referencing the original URL of the document, everything worked!
Now, this is all fun (and sounds straightforward in hindsight), but without some code it’s probably not terribly helpful. So here’s some code (in Delphi, translate to your favourite language as required). It all looks pretty straightforward now(!), but nearly every line involved blood, sweat and tears! This is really not a complete example and hence does not compile but just covers the bits necessary to complement the better documented aspects of custom printing with MSHTML. Please note that this example uses the TEmbeddedWB component for Delphi, and that temporary file cleanup has been excluded.
procedure THTMLPrintController.StartPrint(FPrint: Boolean); var FDeviceW, FDriverW, FPortW: WideString; FDevModeHandle, FDevNamesHandle: HGLOBAL; pEventObj2: IHTMLEventObj2; procedure SetTempFileName; begin FTempFileName := GetTempFileName('', '.htm'); end; { SaveToFile: Saves the current web document to a temporary file, adding the required BASE and HTML properties } procedure SaveToFile; var FElementCollection: IHTMLElementCollection; FHTMLElement: IHTMLElement; FBaseElement: IHTMLBaseElement; FString: WideString; begin FElementCollection := webBrowser.Doc3.getElementsByTagName('base'); if FElementCollection.length = 0 then begin FBaseElement := webBrowser.Doc2.createElement('base') as IHTMLBaseElement; FBaseElement.href := webBrowser.LocationURL; (webBrowser.Doc3.getElementsByTagName('head').item(0,0) as IHTMLElement2).insertAdjacentElement('afterBegin', FBaseElement as IHTMLElement); end else begin FBaseElement := FElementCollection.item(0,0) as IHTMLBaseElement; if FBaseElement.href = '' then FBaseElement.href := webBrowser.LocationURL; end; FElementCollection := webBrowser.Doc3.getElementsByTagName('html'); if FElementCollection.length > 0 then begin FHTMLElement := FElementCollection.item(0,0) as IHTMLElement; FHTMLElement.setAttribute( '__IE_DisplayURL', webBrowser.LocationURL, 0); end; with TFileStream.Create(FTempFileName, fmCreate) do try if webBrowser.Doc5.compatMode = 'CSS1Compat' then begin FString := ''; Write(PWideChar(FString)^, Length(FString)*2); end; FString := webBrowser.Doc3.documentElement.outerHTML; Write(PWideChar(FString)^, Length(FString)*2); finally Free; end; end; { Configured the printer, assuming we've already been passed an ANSI handle } procedure ConfigurePrinter; var FDevice, FDriver, FPort: array[0..255] of char; FDevModeHandle_Ansi: HGLOBAL; FPrinterHandle: THandle; FDevMode: PDeviceModeW; FDevNames: PDevNames; FSucceeded: Boolean; sz: Integer; Offset: PChar; begin Printer.GetPrinter(FDevice, FDriver, FPort, FDevModeHandle_Ansi); if FDevModeHandle_Ansi = 0 then RaiseLastOSError; FDeviceW := FDevice; FDriverW := FDriver; FPortW := FPort; { Setup the DEVMODE structure } FSucceeded := False; if not OpenPrinterW(PWideChar(FDeviceW), FPrinterHandle, nil) then RaiseLastOSError; try sz := DocumentPropertiesW(0, FPrinterHandle, PWideChar(FDeviceW), nil, nil, 0); if sz < 0 then RaiseLastOSError; FDevModeHandle := GlobalAlloc(GHND, sz); if FDevModeHandle = 0 then RaiseLastOSError; try FDevMode := GlobalLock(FDevModeHandle); if FDevMode = nil then RaiseLastOSError; try if DocumentPropertiesW(0, FPrinterHandle, PWidechar(FDeviceW), FDevMode, nil, DM_OUT_BUFFER) < 0 then RaiseLastOSError; FDevMode.dmFields := FDevMode.dmFields or DM_DEFAULTSOURCE or DM_DUPLEX or DM_COLLATE; FDevMode.dmDefaultSource := FTrayNumber; if FDuplex then FDevMode.dmDuplex := DMDUP_VERTICAL else FDevMode.dmDuplex := DMDUP_SIMPLEX; if FCollate then FDevMode.dmCollate := DMCOLLATE_TRUE else FDevMode.dmCollate := DMCOLLATE_FALSE; if DocumentPropertiesW(0, FPrinterHandle, PWideChar(FDeviceW), FDevMode, FDevMode, DM_OUT_BUFFER or DM_IN_BUFFER) < 0 then RaiseLastOSError; FSucceeded := True; finally GlobalUnlock(FDevModeHandle); end; finally if not FSucceeded then GlobalFree(FDevModeHandle); end; finally ClosePrinter(FPrinterHandle); end; Assert(FSucceeded); { Setup up the DEVNAMES structure } FSucceeded := False; FDevNamesHandle := GlobalAlloc(GHND, SizeOf(TDevNames) + (Length(FDeviceW) + Length(FDriverW) + Length(FPortW) + 3) * 2); if FDevNamesHandle = 0 then RaiseLastOSError; try FDevNames := PDevNames(GlobalLock(FDevNamesHandle)); if FDevNames = nil then RaiseLastOSError; try Offset := PChar(FDevNames) + SizeOf(TDevnames); with FDevNames^ do begin wDriverOffset := (Longint(Offset) - Longint(FDevNames)) div 2; Move(PWideChar(FDriverW)^, Offset^, Length(FDriverW) * 2 + 2); Inc(Offset, Length(FDriverW) * 2 + 2); wDeviceOffset := (Longint(Offset) - Longint(FDevNames)) div 2; Move(PWideChar(FDeviceW)^, Offset^, Length(FDeviceW) * 2 + 2); Inc(Offset, Length(FDeviceW) * 2 + 2); wOutputOffset := (Longint(Offset) - Longint(FDevNames)) div 2; Move(PWideChar(FPortW)^, Offset^, Length(FPortW) * 2 + 2); end; FSucceeded := True; finally GlobalUnlock(FDevNamesHandle); end; finally if not FSucceeded then GlobalFree(FDevNamesHandle); end; Assert(FSucceeded); end; { Creates the IHTMLEventObj2 and populates the attributes for printing } procedure CreateEventObject; var v: OleVariant; FShortFileName: WideString; FShortFileNameBuf: array[0..260] of widechar; begin v := EmptyParam; pEventObj2 := webBrowser.Doc4.CreateEventObject(v) as IHTMLEventObj2; pEventObj2.setAttribute('__IE_BaseLineScale', 2, 0); GetShortPathNameW(PWideChar(FTempFileName), FShortFileNameBuf, 260); FShortFileName := FShortFileNameBuf; v := webBrowser.Document as IUnknown; pEventObj2.setAttribute('__IE_BrowseDocument', v, 0); pEventObj2.setAttribute('__IE_ContentDocumentUrl', FShortFileName, 0); pEventObj2.setAttribute('__IE_ContentSelectionUrl', '', 0); // Empty as we never print selections pEventObj2.setAttribute('__IE_FooterString', '', 0); pEventObj2.setAttribute('__IE_HeaderString', '', 0); pEventObj2.setAttribute('__IE_ActiveFrame', 0, 0); pEventObj2.setAttribute('__IE_OutlookHeader', '', 0); pEventObj2.setAttribute('__IE_PrinterCMD_Device', FDeviceW, 0); pEventObj2.setAttribute('__IE_PrinterCMD_Port', FPortW, 0); pEventObj2.setAttribute('__IE_PrinterCMD_Printer', FDriverW, 0); pEventObj2.setAttribute('__IE_PrinterCmd_DevMode', FDevModeHandle, 0); pEventObj2.setAttribute('__IE_PrinterCmd_DevNames', FDevNamesHandle, 0); if FPrint then pEventObj2.setAttribute('__IE_PrintType', 'NoPrompt', 0) else pEventObj2.setAttribute('__IE_PrintType', 'Preview', 0); pEventObj2.setAttribute('__IE_TemplateUrl', GetPrintTemplateURL, 0); pEventObj2.setAttribute('__IE_uPrintFlags', 0, 0); v := VarArrayOf([FShortFileName]); pEventObj2.setAttribute('__IE_TemporaryFiles', v, 0); pEventObj2.setAttribute('__IE_ParentHWND', 0, 0); pEventObj2.setAttribute('__IE_HeaderString', webBrowser.Doc2.title, 0); pEventObj2.setAttribute('__IE_DisplayURL', webBrowser.LocationURL, 0); end; procedure InstantiateDialog; var FWindowParams, FMonikerURL: WideString; FMoniker: IMoniker; FDialogFlags: DWord; varArgIn, varArgOut: OleVariant; res: HRESULT; begin varArgIn := pEventObj2 as IUnknown; varArgOut := Null; FMonikerURL := GetPrintTemplateURL; OleCheck(CreateURLMonikerEx(nil, PWideChar(FMonikerURL), FMoniker, URL_MK_UNIFORM)); if FPrint then begin FWindowParams := ''; FDialogFlags := HTMLDLG_ALLOW_UNKNOWN_THREAD or HTMLDLG_NOUI or HTMLDLG_MODELESS or HTMLDLG_PRINT_TEMPLATE; end else begin FWindowParams := 'resizable=yes;'; FDialogFlags := HTMLDLG_ALLOW_UNKNOWN_THREAD or HTMLDLG_MODAL or HTMLDLG_MODELESS or HTMLDLG_PRINT_TEMPLATE; end; res := ShowHTMLDialogEx(0, FMoniker, FDialogFlags, varArgIn, PWideChar(FWindowParams), varArgOut); if res <> S_OK then raise EOSError.Create(SysErrorMessage(res)); end; begin SetTempFileName; SaveToFile; ConfigurePrinter; CreateEventObject; InstantiateDialog; end;
Update 14 July: This code is not our production code: I’ve stripped out bits and pieces and tried to keep the bits that are somewhat relevant. Don’t worry too much about the ConfigurePrinter details — the takeaway is the HGLOBAL. I must also apologise for the atrocity that is the SaveToFile function. That’s what you get when working with legacy versions of software. Internet Explorer also won’t reliably work with non-ASCII content there unless you toss a BOM into the start of the stream.
This exploration you’ve made is awesome! Your insight and sample code are priceless. I’ve tripped over print templates too and I never got to set the default printer settings. I’ve found printer setup dialog hacks that depend on the underlying Windows version, but this post is surely the best I’ve found on the topic!
Thanks Paulo 🙂
Since I published this post I have tweaked a couple of things:
* It’s safer to use UTF-8 and put a UTF-8 byte order mark on the temporary file in the SaveToFile function. This resolved some issues we experienced with Unicode characters.
* It does not appear to be necessary to use a short file name for the __IE_ContentDocumentUrl attribute.
You may also find the following post helpful: Problems with Internet Explorer 8, print templates and standards compliance
Thank you very much for this description. Do you know if it is possible to make it work without temporary html-file. I create the html document dynamically. So i have an IHTMLDocument2. If I pass this document as argument __IE_BrowseDocument without specifying a ContentDocumentUrl, Print preview has empty pages. I would expect that BrowseDocument specifies the document and there is no need for the URL.
As print template i tested with the templates of the printtemplate.exe examples of msdn.
I also tried with exec-command but it always creates a temporary copy of my html file.
I you have a suggestion please let me know.
Thank you very much.
Daniela
Daniela, I’ve not found any way to do this without a temporary html file: IE always creates a temp file when it is printing, so I followed its print method. Is there a reason why you can’t save the file to disk temporarily?
Hi Marc,
thanks for your reply. I would like to avoid saving to disk because actually there would be no need for that. I create the html source in my code and I don’t want to save this data in plain text to disk.
I don’t understand why this is necessary beacause with the argument “__IE_BrowseDocument” you have the document. If I test with template7 of the printtemplate.exe-Example (you know what I mean?), I cannot find a reason why this temporary file is necessary. But If I don’t set the argument __IE_ContentDocumentUrl, there is a blank page.
Thank you very much.
The save-to-disk behaviour emulates what Internet Explorer does. That gave us what we needed, and I don’t know the justification or reasoning behind the use of both the __IE_ContentDocumentUrl and __IE_BrowseDocument arguments. Not sure I can help more than that.
Just to let you know, I answered this question (How to set __IE_PrinterCmd_DevMode Property to a DEVMODE structure in print template in IE with Visual C++) at StackOverflow. I point to this blog post, as the credit for finding this out is all yours.
The new thing is that I found out is that you can set the properties of dialogArguments in JScript to affect the TemplatePrinter behavior objects created thereafter.
Thanks again Paulo! That’s a handy solution to avoid having to call ShowHTMLDialogEx.
Hi Marc,
Your DevMode setting analysis is very impressive. I tried to resolve this for years. The best I could do to change default printer before printing then change it back after printing. This is much simpler but not nearly as elegant as your solution.
Now I spotted another problem. When printing with custom template IE creates temporary file in TEMP folder. File is not deleted after printing completed. This is easily demonstrated even with the printtemplates.exe MS Sample – Template 2. Though when you prints from IE application with default template file cleaning is perfect.
To make it worse my pages include ActveX controls. Printing creates temporary .emf file per every ActiveX instance which also not cleaned after printing. When prints huge document it may leave 1000th .emf files in TEMP directory. Again standard IE with default template cleans files perfectly.
Can you please use your expertise to look at what is lost from default template functionality. Why custom template does not clean temporary files?
Thank you Arkady — it was certainly quite a long process to understand how it all worked. My experience with temp files has been mixed, but most important is making sure all COM references to IE are properly released before exiting the app — so destroy the control and release any interfaces, as this does do the cleanup in my experience.
Hi Marc,
I am trying to implement your solution in C#. However I can not figure out what GetPrintTemplateURL is doing. Help?
Thanks,
Brad
Brad, GetPrintTemplateURL is just returning the URL to our own print template — in our case, something like ‘http://localhost:port/printtemplate.html’
Thanks for the insights Marc. I think you are the right person who can solve my problem here.
http://stackoverflow.com/q/23293786/418175
Thanks in advance 🙂
I’m sorry, I’ve never used CHTMLView — not sure how the web view events are hooked up in MFC.
Hi Marc,
Do you have experiences with users which can’t print because print doesn’t start or it crashes completely. Other users with same IE-Version and same Windows version can print without any problems.
You know any reason for this issue?
Thanks,
Dani
Not something I can really diagnose via a blog comment but I’d be checking print drivers.
I need to try your solution, in VBA.
I spent 3 or 4 mos last year in MSACCESS trying to get Tray selection to work,
to no avail, and move the printing to another printer that did respond.
Your sleuthing is marvelous. What tools enabled you to find these answers?
tkx, Paul
Hi Paul, thanks for reading. Most of the digging was done with WinDBG and Procmon. A little bit of higher-level window message analysis with Spy++. I suspect that this may be hard to implement in VBA. Are you doing printing from a web control in MSAccess?
Hi Marc,
Two projects — last year MSACCESS pass-thru-query to SQLServer to get the data, and then format for either “pink” or “green” tray. This would not resolve on the OCE printer, and I needed to switch to the next room for the other KONICA MINOLTA C754SeriesPCL printer. This is the project I want to return to and try your code above.
This year/this month I am again using MSACCESS, & with WebBrowser control in Office 2010 (accdb) and am having problems printing. When going directly Firefox (or IE) to the web url (http://celdt.org/resources/scoring_tool/lst.aspx) , and populating the data, then the print button on the url works fine for alignment and spacing. But when I use access and the WBcontrol, then the printing of the H2 takes the first part and prints it with the prior div on the right 28% of the page. And the rest of the H2 prints centered on the proper next line (where the entire H2 should print).
I do not see the PrinterController helping in this instance. TMI… bye for now.
Hi Marc,
Are you wonder how to access the pages displayed in the preview window?
Sorry, I’m not sure I understand your question 🙂