Click here to Skip to main content
15,887,361 members
Articles / Desktop Programming / MFC
Article

Automated IE SaveAs MHTML

Rate me:
Please Sign up or sign in to vote.
5.00/5 (29 votes)
4 Sep 20023 min read 471.1K   7.9K   95   112
This article demonstrates how to automate IE's Save As functionality

Introduction

The purpose of this article is to show how to automate the fully fledged Save As HTML feature from Internet Explorer, which is normally hidden to those using the Internet Explorer API. Saving the current document as MHTML format is just one of the options available, including:

  • Save As MHTML (whole web page, images, ... in a single file)
  • Save As Full HTML (additional folder for images, ...)
  • Save HTML code only
  • Save As Text

Saving Silently as HTML Using the Internet Explorer API

In fact, the ability to save the current web page for storage without showing a single dialog box is already available to everyone under C++, using the following code, with an important restriction:

C++
LPDISPATCH lpDispatch = NULL;
IPersistFile *lpPersistFile = NULL;

// m_ctrl is an instance of the Web Browser control
lpDispatch = m_ctrl.get_Document();
lpDispatch->QueryInterface(IID_IPersistFile, (void**)&lpPersistFile);

lpPersistFile->Save(L"c:\\htmlpage.html",0);
lpPersistFile->Release();
lpDispatch->Release();

(caption for code above) Saving HTML code only, without dialog boxes

The restriction is that we are talking about the HTML code only, not the web page. Of course, what is interesting is to gain access to full HTML archives with images and so on.

Because there is no "public" or known way to ask for this feature without showing one or more dialog boxes from Internet Explorer, what we are going to do is hook the operating system to listen all window creations, including the dialog boxes. Then we'll ask Internet Explorer for the feature and override the file path from the dialog boxes without being seen. Finally, we'll mimic the user clicking on the Save button to validate the dialog box and unhook ourselves. That's done!

Image 1

Hooking Internet Explorer to Save As HTML without popping the dialog boxes

This was the short workflow, but there are a few tricks to get along and this article is a unique opportunity to go into detail. By the way, the code is rooted by an article from MS about how to customize Internet Explorer Printing by hooking the Print dialog boxes; see here or here. In our app, we have our own Save As feature:

C++
m_wbSaveAs.Config( CString("c:\\htmlpage.mhtml"), SAVETYPE_ARCHIVE );
m_wbSaveAs.SaveAs();

// where the second parameter is the type of HTML needed :
typedef enum _SaveType
{
    SAVETYPE_HTMLPAGE = 0,
    SAVETYPE_ARCHIVE,
    SAVETYPE_HTMLONLY,
    SAVETYPE_TXTONLY
} SaveType;

We start the SaveAs() implementation by installing the hook:

C++
// prepare SaveAs Dialog hook
//
g_hHook = SetWindowsHookEx(WH_CBT, CbtProc, NULL, GetCurrentThreadId());
if (!g_hHook)
    return false;

// make SaveAs Dialog appear
//
// cmd = OLECMDID_SAVEAS (see ./include/docobj.h)
g_bSuccess = false;
g_pWebBrowserSaveAs = this;
HRESULT hr = m_pWebBrowser->ExecWB(OLECMDID_SAVEAS, 
    OLECMDEXECOPT_PROMPTUSER, NULL, NULL);

// remove hook
UnhookWindowsHookEx(g_hHook);
g_pWebBrowserSaveAs = NULL;
g_hHook = NULL;

The hook callback procedure is just hardcore code; see for yourself:

C++
LRESULT CALLBACK CSaveAsWebbrowser::CbtProc(int nCode, 
    WPARAM wParam, LPARAM lParam) 
{  
    // the windows hook sees for each new window being created :
    // - HCBT_CREATEWND : when the window is about to be created
    //      we check out if it is a dialog box (classid = 0x00008002, 
    //      see Spy++)
    //      and we hide it, likely to be the IE SaveAs dialog
    // - HCBT_ACTIVATE : when the window itself gets activited
    //      we run a separate thread, and let IE do his own init steps in 
    //      the mean time
    switch (nCode)
    {
        case HCBT_CREATEWND:
        {
            HWND hWnd = (HWND)wParam;
            LPCBT_CREATEWND pcbt = (LPCBT_CREATEWND)lParam;
            LPCREATESTRUCT pcs = pcbt->lpcs;
            if ((DWORD)pcs->lpszClass == 0x00008002)
            {
                g_hWnd = hWnd;          // Get hwnd of SaveAs dialog
                pcs->x = -2 * pcs->cx;  // Move dialog off screen
            }
            break;
        }    
        case HCBT_ACTIVATE:
        {
            HWND hwnd = (HWND)wParam;
            if (hwnd == g_hWnd)
            {
                g_hWnd = NULL;
                g_bSuccess = true;

                if (g_pWebBrowserSaveAs->IsSaveAsEnabled())
                {
                    g_pWebBrowserSaveAs->SaveAsDisable();

                    CSaveAsThread *newthread = new CSaveAsThread();
                    newthread->SetKeyWnd(hwnd);
                    newthread->Config( g_pWebBrowserSaveAs->GetFilename(), 
                        g_pWebBrowserSaveAs->GetSaveAsType() );
                    newthread->StartThread();
                }
            }
            break;
        }
    }
    return CallNextHookEx(g_hHook, nCode, wParam, lParam); 
}

In our thread, we wait until the Internet Explorer Save As dialog is ready with filled data:

C++
switch(    ::WaitForSingleObject( m_hComponentReadyEvent, m_WaitTime) )
{
     ...
     if ( ::IsWindowVisible(m_keyhwnd) )
     {
         bSignaled = TRUE;
         bContinue = FALSE;
     }

     MSG msg ;
     while( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) )
     {
         if (msg.message == WM_QUIT)
         {
              bContinue = FALSE ;
              break ;
         }
         TranslateMessage(&msg);
         DispatchMessage(&msg);
     }
     ...
}

// relaunch our SaveAs class, but now everything is ready to play with
if (bSignaled)
{
    CSaveAsWebbrowser surrenderNow;
    surrenderNow.Config( GetFilename(), GetSaveAsType() );
    surrenderNow.UpdateSaveAs( m_keyhwnd );
}

// kill the thread, we don't care anymore about it
delete this;

We can now override the appropriate data:

C++
void CSaveAsWebbrowser::UpdateSaveAs(HWND hwnd)
{
    // editbox : filepath (control id = 0x047c)
    // dropdown combo : filetypes (options=complete page;
    //     archive;html only;txt) (control id = 0x0470)
    // save button : control id = 0x0001
    // cancel button : control id = 0x0002


    // select right item in the combobox
    SendMessage(GetDlgItem(hwnd, 0x0470), CB_SETCURSEL, 
        (WPARAM) m_nSaveType, 0);
    SendMessage(hwnd, WM_COMMAND, MAKEWPARAM(0x0470,CBN_CLOSEUP), 
        (LPARAM) GetDlgItem(hwnd, 0x0470));

    // set output filename
    SetWindowText(GetDlgItem(hwnd, 0x047c), m_szFilename);

    // Invoke Save button
    SendMessage(GetDlgItem(hwnd, 0x0001), BM_CLICK, 0, 0);  
}

In the code above, it is funny to remark that to select the kind of HTML we want (full HTML, archive, code only or text format), we not only select the adequate entry in the combo-box, we also send Internet Explorer a combo-box CloseUp notification. This is because that's what Internet Explorer has subscribed for to know we want this kind of HTML. This behavior is known by hints-and-trials.

Conclusion

This article describes a technique to gain access to the fully fledged Save As HTML feature exposed by Internet Explorer. I have never seen an article about this topic on the 'net, whereas it's easy to figure out that it is a compelling feature for developers building web applications. Files you may use from the source code provided are:

  • SaveAsWebBrowser.h, *.cpp: hook procedure; fill the dialog box data
  • SaveAsThread.h, *.cpp: auxiliary thread for synchronization with Internet Explorer

The application is just a simple MFC-based CHtmlView application embedding the web browser control.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
France France
Addicted to reverse engineering. At work, I am developing business intelligence software in a team of smart people (independent software vendor).

Need a fast Excel generation component? Try xlsgen.

Comments and Discussions

 
GeneralRe: How to save only one image ? Pin
twinssen14-Jan-06 13:36
twinssen14-Jan-06 13:36 
GeneralUsing in .Exe to get IE to save current page Pin
Neville Franks21-Mar-04 23:37
Neville Franks21-Mar-04 23:37 
GeneralRe: Using in .Exe to get IE to save current page Pin
Stephane Rodriguez.27-Mar-04 7:53
Stephane Rodriguez.27-Mar-04 7:53 
QuestionCan I get the Internet shortcuts in the IE history with other methods? Pin
xieweihua23-Feb-04 21:57
xieweihua23-Feb-04 21:57 
AnswerRe: Can I get the Internet shortcuts in the IE history with other methods? Pin
Stephane Rodriguez.24-Feb-04 2:12
Stephane Rodriguez.24-Feb-04 2:12 
QuestionHow to set URL of IE through program? Pin
shri_3118-Feb-04 0:44
shri_3118-Feb-04 0:44 
AnswerRe: How to set URL of IE through program? Pin
Stephane Rodriguez.18-Feb-04 1:48
Stephane Rodriguez.18-Feb-04 1:48 
GeneralC# code Pin
Ralf251113-Jan-04 2:07
Ralf251113-Jan-04 2:07 
Simple test the class in a form, e.g.
IWebBrowser2 wb2 = (IWebBrowser2) axWebBrowser1.GetOcx();
SaveAsWebbrowser saw = new SaveAsWebbrowser(wb2, "give_me_a_file_name", De.Fun.IESaveAsHack.SaveType.SAVETYPE_ARCHIVE);
saw.SaveAs();

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

using SHDocVw;

namespace De.Fun.IESaveAsHack
{
  /// <summary>
  /// IE has this file options in the "SaveAs" dialog
  /// </summary>
  public enum SaveType
  {
    SAVETYPE_HTMLPAGE = 0,
    SAVETYPE_ARCHIVE,
    SAVETYPE_HTMLONLY,
    SAVETYPE_TXTONLY
  };

  /// <summary>
  /// Because security reasons the IE always shows a "Save As" dialog box if you want to save a html page complete.
  /// This helper class automatically fills out the fields and presses ok.
  /// </summary>
  public class SaveAsWebbrowser  
  {
    /// <summary>
    /// Construction
    /// </summary>
    /// <param name="webbrowser">Webbrowser interface</param>
    /// <param name="pathFile">Path and filename</param>
    /// <param name="saveType">Type of file</param>
    public SaveAsWebbrowser(IWebBrowser2 webbrowser, string pathFile, SaveType saveType)
    {
      this.WebBrowser = webbrowser;
      this.PathFile = pathFile;
      this.SaveType = saveType;
    }

    IWebBrowser2 webBrowser;
    public IWebBrowser2 WebBrowser
    {
      get { return webBrowser; }
      set { webBrowser = value; }
    }

    string pathFile;
    public string PathFile
    {
      get { return pathFile; }
      set { pathFile = value; }
    }

    SaveType saveType;
    public SaveType SaveType
    {
      get { return saveType; }
      set { saveType = value; }
    }

    /// <summary>
    /// Call this function to save current page - after page is loaded complete
    /// </summary>
    /// <returns>true if successful</returns>
    public bool SaveAs()
    {
      if (0==pathFile.Length)
        pathFile = "untitled";
      // TODO check path file. If file exists, IE prompts again...

      if (null==webBrowser)
        return false;
      
      if (0!=hook)
        return false;

      HookProcedure = new HookProc(SaveAsHookProc);

      // prepare SaveAs dialog hook
      hook = SetWindowsHookEx(5 /*WH_CBT*/, HookProcedure, (IntPtr) 0, AppDomain.GetCurrentThreadId());
      if (0==hook)
        return false;

      // this will show the dialog
      saveaswebbrowser = this;
      object o = null;
      webBrowser.ExecWB(SHDocVw.OLECMDID.OLECMDID_SAVEAS,
        SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_PROMPTUSER, ref o, ref o);

      // remove hook
      UnhookWindowsHookEx(hook);
      saveaswebbrowser = null;
      hook = 0;
      return true;
    }

    //Import for SetWindowsHookEx.
    [DllImport("user32.dll",CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
    public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);

    //Import for UnhookWindowsHookEx.
    [DllImport("user32.dll",CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
    public static extern bool UnhookWindowsHookEx(int idHook);

    //Import for CallNextHookEx.
    //Use this function to pass the hook information to next hook procedure in chain.
    [DllImport("user32.dll",CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
    public static extern int CallNextHookEx(int idHook, int nCode, IntPtr wParam, IntPtr lParam);

    // Hook variables
    public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);
    static int hook = 0;
    HookProc HookProcedure;        

    static IntPtr hwndDlg = (IntPtr) 0;
    static SaveAsWebbrowser saveaswebbrowser = null;

    [StructLayout(LayoutKind.Sequential)]
    public struct CBT_CREATEWND 
    { 
      public IntPtr   lpcs; 
      int             hwndInsertAfter; 
    }; 

    [StructLayout(LayoutKind.Sequential)]
    public struct CREATESTRUCT 
    {
      int             lpCreateParams;
      int             hInstance;
      int             hMenu;
      int             hwndParent;
      int             cy;
      public int      cx;
      int             y;
      public int      x;
      int             style;
      int             lpszName;
      public int      lpszClass;
      int             dwExStyle;
    } 

    /// <summary>
    /// The hook procedure for dialogs. Only called by windows.
    /// </summary>
    /// <param name="nCode">Action code</param>
    /// <param name="wParam">Depends on action code</param>
    /// <param name="lParam">Depends on action code</param>
    /// <returns></returns>
    public static int SaveAsHookProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
      switch(nCode)
      {
        case 3:  // HCBT_CREATEWND
          CBT_CREATEWND cw = (CBT_CREATEWND) Marshal.PtrToStructure(lParam, typeof(CBT_CREATEWND));
          CREATESTRUCT cs = (CREATESTRUCT) Marshal.PtrToStructure(cw.lpcs, typeof(CREATESTRUCT));
          if (cs.lpszClass == 0x00008002 && (IntPtr)0 == hwndDlg)
          {
            hwndDlg = (IntPtr) wParam;  // Get hwnd of SaveAs dialog
            cs.x = -2 * cs.cx;          // Move dialog off screen
          }
          break;
        case 5: // HCBT_ACTIVATE
          ThreadPressOk tpok = new ThreadPressOk(hwndDlg, saveaswebbrowser.PathFile, saveaswebbrowser.SaveType);
          hwndDlg = (IntPtr) 0;
          // Create a thread to execute the task, and then
          // start the thread.
          new Thread((new ThreadStart(tpok.ThreadProc))).Start();
          break;
      }
      return CallNextHookEx(hook, nCode, wParam, lParam); 
    }

    /// <summary>
    /// Helper thread class.
    /// </summary>
    class ThreadPressOk 
    {
      public ThreadPressOk(IntPtr hwndDlg, string pathFile, SaveType saveType)
      {
        this.hwndDlg = hwndDlg;
        this.pathFile = pathFile;
        this.saveType = saveType;
      }
      
      IntPtr    hwndDlg;
      string    pathFile;
      SaveType  saveType;

      // Imports of the User32 DLL.   
      [DllImport("user32.dll", CharSet=CharSet.Auto)]
      public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);

      [DllImport("user32.dll", CharSet=CharSet.Auto)]
      public static extern IntPtr GetDlgItem(IntPtr hWnd, int nIDDlgItem);

      [DllImport("user32.dll", CharSet=CharSet.Auto)]
      static extern private bool SetWindowText(IntPtr hWnd, string lpString);
         
      // The thread procedure performs the message loop and place the data
      public void ThreadProc() 
      {        
        // Create the dialog
        Application.DoEvents();
    
        //  Begin saving the webpage
        //  Note: these settings only work on win2k IE 6 SP1
        //  You can make them work the same way on other versions

        // editbox : filepath (control id = 0x047c)
        // dropdown combo : filetypes (options=complete page|archive|html only|txt) (control id = 0x0470)
        // save button : control id = 0x0001
        // cancel button : control id = 0x0002

        IntPtr typeB = GetDlgItem(hwndDlg, 0x0470);
        IntPtr nameB = GetDlgItem(hwndDlg, 0x047c);
        IntPtr saveBtn = GetDlgItem(hwndDlg, 0x0001);
        if (((IntPtr)0!=typeB) && ((IntPtr)0!=nameB) && ((IntPtr)0!=saveBtn))
        {
          //select save type
          SendMessage(typeB, 0x014E /*CB_SETCURSEL*/, (int)saveType, 0);
          SendMessage(hwndDlg, 0x0111 /*WM_COMMAND*/, 0x80470/*MAKEWPARAM(0x0470, CBN_CLOSEUP)*/, (int)typeB);
          // set output pathFile
          SetWindowText(nameB, pathFile);
          // Invoke Save button
          SendMessage(saveBtn, 0x00F5 /*BM_CLICK*/, 0, 0);
        }
        // Clean up GUI - we have clicked ok
        Application.DoEvents();

        // thread finish, garbage collection should do the clean up...
      }
    }
  }
}


Ralf2511
GeneralRe: C# code Pin
Stephane Rodriguez.13-Jan-04 2:30
Stephane Rodriguez.13-Jan-04 2:30 
GeneralRe: C# code Pin
Ralf251113-Jan-04 3:42
Ralf251113-Jan-04 3:42 
GeneralRegarding OLECMDID_ALLOWUILESSSAVEAS Pin
Amul6-Jan-04 3:39
Amul6-Jan-04 3:39 
GeneralModal Print Dialog Pin
kkaps26-Nov-03 5:18
kkaps26-Nov-03 5:18 
GeneralRe: Modal Print Dialog Pin
Steph Rodriguez26-Nov-03 5:42
sussSteph Rodriguez26-Nov-03 5:42 
GeneralRe: Modal Print Dialog Pin
kkaps26-Nov-03 9:25
kkaps26-Nov-03 9:25 
GeneralRe: Modal Print Dialog Pin
Steph Rodriguez27-Nov-03 0:02
sussSteph Rodriguez27-Nov-03 0:02 
GeneralRe: Modal Print Dialog Pin
kkaps27-Nov-03 18:46
kkaps27-Nov-03 18:46 
Generalwhy use thread? (Some Delphi code) Pin
kmicic17-Nov-03 18:49
kmicic17-Nov-03 18:49 
GeneralRe: why use thread? (Some Delphi code) Pin
Stephane Rodriguez.17-Nov-03 20:51
Stephane Rodriguez.17-Nov-03 20:51 
GeneralA few more comments Pin
kmicic8-Dec-03 8:17
kmicic8-Dec-03 8:17 
GeneralError Msg: &quot;The web page could not be saved to the selected location&quot; Pin
banxuegang23-Aug-03 17:14
banxuegang23-Aug-03 17:14 
GeneralRe: Error Msg: &amp;quot;The web page could not be saved to the selected location&amp;quot; Pin
Stephane Rodriguez.23-Aug-03 20:15
Stephane Rodriguez.23-Aug-03 20:15 
GeneralRe: Error Msg: &quot;The web page could not be saved to the selected location&quot; Pin
Anonymous24-Aug-03 17:35
Anonymous24-Aug-03 17:35 
GeneralRe: Error Msg: &quot;The web page could not be saved to the selected location&quot; Pin
Nelis Willers11-Oct-03 10:29
Nelis Willers11-Oct-03 10:29 
GeneralRe: Error Msg: &amp;quot;The web page could not be saved to the selected location&amp;quot; Pin
Stephane Rodriguez.11-Oct-03 10:50
Stephane Rodriguez.11-Oct-03 10:50 
GeneralRe: Error Msg: &quot;The web page could not be saved to the selected location&quot; Pin
exe2mht29-Mar-04 20:32
sussexe2mht29-Mar-04 20:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.