I have an application that saves HTML from an IE windows using features of Interop.ShDocVw.dll. I have a need to save a PDF file, but the output goes to an iframe element which is not exposed in the interop. I found that I can trigger the save as option in IE using the IWebBrowser2.ExecWB(). I was thinking that it should be possible (maybe) to put a hook in the browser thread that would allow me to change the file name and path and click the Save button. While I can put a WH_CBT hook into the local process, the hook appears either NOT to be in the IE process, or I am getting a hook into something other than the Save As dialog. I am not sure how to check exactly what process I am in when I get a hook, that might help identify what I am doing wrong.
What I am seeing is that my CBTProc method does not fire a HCBT_CREATEWND code when I put the WH_CBT hook in. I do get HCBT_ACTIVATE codes firing in the hook, but without trapping the wParam on the HCBT_CREATEWND, I have no way to know that the window has been created so that I can try to SendMsg for the text change and button click.
What I am thinking is that you cannot do this due to the way windows security works, but I am not familiar with the way of the (Windows) force by any stretch of the imagination, so I don't really know. I know we can get at and save HTML, these PDF files have been quite elusive. It would seem that it should be possible considering what I do with HTML, but I need some guidance to determine this. It sure looks like it should work. My C# code provided should anyone care to point out something I am overlooking, or to tell me that it can't be done... (Hopefully not the latter.)
// This class is compiled into it's own DLL
public class CIeSaveAs
{
#region Unmanaged Hook Methods
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
private static extern bool UnhookWindowsHookEx(int idHook);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
private static extern int CallNextHookEx(int idHook, int nCode, IntPtr wParam, IntPtr lParam);
#endregion
#region Unmanaged calls to update Dialog
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr GetDlgItem(IntPtr hWnd, int nIDDlgItem);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern bool SetWindowText(IntPtr hWnd, string lpString);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool IsWindowVisible(IntPtr hWnd);
#endregion
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
#region Win32 structures requred to handle windows messages/state
[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;
}
#endregion
public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);
private IntPtr _hwndSaveAsDlg;
private string _filePath;
private HookProc _hookProcedure;
private int _hook = 0;
//The public interface to CIeSaveAs
public void SavePdf(IWebBrowser2 browser, string filePath)
{
_filePath = filePath;
_hookProcedure = new HookProc(SaveAsHookProc);
_hook = SetWindowsHookEx(5 /*WH_CBT*/, _hookProcedure, (IntPtr)0, (int)GetCurrentThreadId());
if (_hook == 0 || browser == null) return;
try
{
// open the browser Save As dialog
browser.ExecWB(OLECMDID.OLECMDID_SAVEAS, OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER);
}
finally
{
UnhookWindowsHookEx(_hook);
}
}
// The CBTProc method
private int SaveAsHookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
//Debug.WriteLine( string.Format( "nCode = {0}", nCode ) );
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)
{
_hwndSaveAsDlg = (IntPtr)wParam; // Get hwnd of SaveAs dialog
cs.x = -2 * cs.cx; // Move dialog off screen
}
break;
case 5: // HCBT_ACTIVATE
IntPtr hwnd = (IntPtr)wParam;
if (hwnd == _hwndSaveAsDlg && _hwndSaveAsDlg != (IntPtr)0)
{
//Prepare a thread to act as required messages source.Also set File path and its save type.
var tpok = new ThreadToPressOk(hwnd, _filePath);
_hwndSaveAsDlg = (IntPtr)0;
new Thread(tpok.ThreadProc).Start();
}
break;
}
//This is required to pass control to next hook if exists on this process.
return CallNextHookEx(_hook, nCode, wParam, lParam);
}
public class ThreadToPressOk
{
private IntPtr _hwndDialog;
private string _filePath;
private EnumBrowserFileSaveType _fileSaveType;
public ThreadToPressOk(IntPtr hwnd, string filePath)
{
_hwndDialog = hwnd;
_filePath = filePath;
_fileSaveType = EnumBrowserFileSaveType.SAVETYPE_ARCHIVE;
}
public void ThreadProc()
{
//To avoid race condition, we are forcing this thread to wait until Saveas dialog is displayed.
while (!IsWindowVisible(_hwndDialog))
{
Thread.Sleep(100);
Application.DoEvents();
}
Application.DoEvents();
//Get the handle to file path on the saveas dialog.
IntPtr nameB = GetDlgItem(_hwndDialog, 0x047c);
//Get the handle to saveas on the saveas dialog.
IntPtr saveBtn = GetDlgItem(_hwndDialog, 0x0001);
if (
((IntPtr)0 != nameB) &&
((IntPtr)0 != saveBtn) &&
IsWindowVisible(_hwndDialog))
{
SetWindowText(nameB, _filePath);
SendMessage(saveBtn, 0x00F5 /*BM_CLICK*/, 0, 0);
}
// Clean up GUI - we have clicked save button.
//GC is going to do that cleanup job, so we are OK
Application.DoEvents();
//Terminate the thread.
return;
}
}
}
I made a console app that gets a browser window with ShDocVw.dll that contains a PDF file, then call the above class from that app...
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Creating new CIeSaveAs class...");
var saveAs = new CIeSaveAs();
Console.WriteLine("Invoke SavePdf...");
var filePath = @"C:\test\a\b\test.pdf";
InternetExplorer browser = null;
var shell = new ShellWindows();
foreach (InternetExplorer ie in shell)
{
var file = Path.GetFileNameWithoutExtension(ie.FullName).ToLower();
if (file.Equals("iexplore"))
{
HTMLDocument document = ie.Document as HTMLDocument;
if (document != null)
{
var title = GetPropertyValue(document, "title").ToString();
if (title.StartsWith("CM/ECF"))
{
browser = ie;
break;
}
}
}
}
saveAs.SavePdf(browser, filePath);
Console.WriteLine();
Console.WriteLine("Press Enter to exit...");
Console.ReadKey();
}
private static object GetPropertyValue(object obj, string propertyName)
{
int dispId = 0;
DispatchUtility.TryGetDispId(obj, propertyName, out dispId);
if (dispId > 0)
return DispatchUtility.Invoke(obj, dispId, null);
return null;
}
}
Ok, here's the missing code that makes it compile...
public static class DispatchUtility
{
private const int S_OK = 0; //From WinError.h
private const int LOCALE_SYSTEM_DEFAULT = 2 << 10; //From WinNT.h == 2048 == 0x800
public static bool ImplementsIDispatch(object obj)
{
bool result = obj is IDispatchInfo;
return result;
}
public static Type GetType(object obj, bool throwIfNotFound)
{
RequireReference(obj, "obj");
Type result = GetType((IDispatchInfo)obj, throwIfNotFound);
return result;
}
public static bool TryGetDispId(object obj, string name, out int dispId)
{
RequireReference(obj, "obj");
bool result = TryGetDispId((IDispatchInfo)obj, name, out dispId);
return result;
}
public static object Invoke(object obj, int dispId, object[] args)
{
string memberName = "[DispId=" + dispId + "]";
object result = Invoke(obj, memberName, args);
return result;
}
public static object Invoke(object obj, string memberName, object[] args)
{
RequireReference(obj, "obj");
Type type = obj.GetType();
object result = type.InvokeMember(memberName,
BindingFlags.InvokeMethod | BindingFlags.GetProperty,
null, obj, args, null);
return result;
}
private static void RequireReference<T>(T value, string name) where T : class
{
if (value == null)
{
throw new ArgumentNullException(name);
}
}
private static Type GetType(IDispatchInfo dispatch, bool throwIfNotFound)
{
RequireReference(dispatch, "dispatch");
Type result = null;
int typeInfoCount;
int hr = dispatch.GetTypeInfoCount(out typeInfoCount);
if (hr == S_OK && typeInfoCount > 0)
{
dispatch.GetTypeInfo(0, LOCALE_SYSTEM_DEFAULT, out result);
}
if (result == null && throwIfNotFound)
{
// If the GetTypeInfoCount called failed, throw an exception for that.
Marshal.ThrowExceptionForHR(hr);
// Otherwise, throw the same exception that Type.GetType would throw.
throw new TypeLoadException();
}
return result;
}
private static bool TryGetDispId(IDispatchInfo dispatch, string name, out int dispId)
{
RequireReference(dispatch, "dispatch");
RequireReference(name, "name");
bool result = false;
Guid iidNull = Guid.Empty;
int hr = dispatch.GetDispId(ref iidNull, ref name, 1, LOCALE_SYSTEM_DEFAULT, out dispId);
const int DISP_E_UNKNOWNNAME = unchecked((int)0x80020006); //From WinError.h
const int DISPID_UNKNOWN = -1; //From OAIdl.idl
if (hr == S_OK)
{
result = true;
}
else if (hr == DISP_E_UNKNOWNNAME && dispId == DISPID_UNKNOWN)
{
result = false;
}
else
{
Marshal.ThrowExceptionForHR(hr);
}
return result;
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00020400-0000-0000-C000-000000000046")]
private interface IDispatchInfo
{
[PreserveSig]
int GetTypeInfoCount(out int typeInfoCount);
void GetTypeInfo(int typeInfoIndex, int lcid, [MarshalAs(UnmanagedType.CustomMarshaler,
MarshalTypeRef = typeof(System.Runtime.InteropServices.CustomMarshalers.TypeToTypeInfoMarshaler))] out Type typeInfo);
[PreserveSig]
int GetDispId(ref Guid riid, ref string name, int nameCount, int lcid, out int dispId);
// NOTE: The real IDispatch also has an Invoke method next, but we don't need it.
}
}
Any insight would be much appreciated,
Kent
User contributions licensed under CC BY-SA 3.0