Background
項目里要求將一個HTML頁面(支付結果)
生成pdf文檔。頁面有圖片,有表格,貌似開源的iTextSharp應付不了.
在一番搜索之后,找到了wkhtmltopdf,一個命令行的開源轉換工具,支持指定url或本地html file的路徑,試用后效果不錯,還特意用wkhtmltopdf寫了一個工具將博客園的帖子備份pdf到本地,后續有空把這個工具分享出來
But,發給客戶測試兩天運行效果不太理想,出現一些未知錯誤,而且奇怪的是在測試環境沒問題,正式環境卻頻繁出錯。最后客戶放棄這個方案
附上 WkhtmlToXSharp C# wrapper wrapper (using P/Invoke) for the excelent Html to PDF conversion library wkhtmltopdf library.
OK,來到正題,另類的解決方案:Hook
調用IE打印功能,使用XPS打印機,先將HTML文件生成xps文檔,再生成pdf
新建WinForm 項目,拖入WebBrowser控件,代碼指定Url到本地html文件路徑,等待文檔加載完成后 WebBrowser.Print(); OK,運行,會彈出選擇打印機的對話框,如圖一。點擊打印后,彈出另存為的對話框,輸入xps路徑后保存(圖二),即可得到一份xps文檔。
圖一:選擇打印機
圖二:輸入xps路徑
從上面可以看到,這里的打印需要與UI交互,人工點擊打印,輸入xps路徑保存才行。
接下來在網絡搜索:怎么不顯示對話框,直接打印生成xps文件,在stackoverflow,codeproject看了很多,沒找到辦法。后來偶然翻到園子前人的文章,采用hook方式,UI Automation來完成打印和保存的動作,覺得這個方案可行
接下來上代碼吧
//調用WebBrowser.Print的代碼就忽略了,直接看鈎子
IntPtr hwndDialog;
string pathFile;
EnumBrowserFileSaveType 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);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool IsWindowVisible(IntPtr hWnd);
//Win32 Api定義
[DllImport("user32.dll")]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfeter, string lpszClass, string lpszWindow);
[DllImport("user32.dll")]
static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, String lParam);
[DllImport("user32.dll")]
static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
//Win32消息定義
const uint WM_SETTEXT = 0x000c;
const uint WM_IME_KEYDOWN = 0x0290;
const uint WM_LBUTTONDOWN = 0x0201;
const uint WM_LBUTTONUP = 0x0202;
// The thread procedure performs the message loop and place the data
public void ThreadProc()
{
int maxRetry = 10;
int retry = 0;
IntPtr hWndPrint = FindWindow("#32770", "打印");
IntPtr hWnd = FindWindow("#32770", "文件另存為");
if (hWnd != IntPtr.Zero)
{
log.InfoFormat("got saveas dialog handle. Printer Dialog skipped.");
}
else
{
Thread.Sleep(200);
hWndPrint = FindWindow("#32770", "打印");
//這里有時候獲取不到window,所以加了Sleep,多試幾次
while (hWndPrint == IntPtr.Zero && retry < maxRetry)
{
Thread.Sleep(200);
log.InfoFormat("retry get Print dialog handle.retry:{0}", retry);
hWndPrint = FindWindow("#32770", "打印");
retry++;
}
if (hWndPrint == IntPtr.Zero)
{
//wait 1 second,retry again
Thread.Sleep(1000);
hWndPrint = FindWindow("#32770", "打印");
}
if (hWndPrint == IntPtr.Zero)
{
log.InfoFormat("Did not get Print dialog handle.retry:{0}", retry);
return;
}
log.InfoFormat("got Print dialog handle.retry:{0}", retry);
//select printer dialog
IntPtr hChildP;
hChildP = IntPtr.Zero;
hChildP = FindWindowEx(hWndPrint, IntPtr.Zero, "Button", "打印(&P)");
// 向保存按鈕發送2個消息,以模擬click消息,借此來按下保存按鈕
PostMessage(hChildP, WM_LBUTTONDOWN, IntPtr.Zero, IntPtr.Zero);
PostMessage(hChildP, WM_LBUTTONUP, IntPtr.Zero, IntPtr.Zero);
Application.DoEvents();
}
//hWnd = FindWindow("#32770", null);
hWnd = FindWindow("#32770", "文件另存為");
//To avoid race condition, we are forcing this thread to wait until Saveas dialog is displayed.
retry = 0;
while ((!IsWindowVisible(hWnd) || hWnd == IntPtr.Zero) && retry < maxRetry)
{
Thread.Sleep(200);
log.InfoFormat("retry get saveas dialog handle.retry:{0}", retry);
hWnd = FindWindow("#32770", null);
retry++;
Application.DoEvents();
}
log.InfoFormat("got saveas dialog handle.retry:{0}", retry);
if (hWnd == IntPtr.Zero)
{
//wait 1 second,retry again
Thread.Sleep(1000);
hWnd = FindWindow("#32770", "文件另存為");
}
if (hWnd == IntPtr.Zero)
{
return;
}
Application.DoEvents();
IntPtr hChild;
// 由於輸入框被多個控件嵌套,因此需要一級一級的往控件內找到輸入框
hChild = FindWindowEx(hWnd, IntPtr.Zero, "DUIViewWndClassName", String.Empty);
hChild = FindWindowEx(hChild, IntPtr.Zero, "DirectUIHWND", String.Empty);
hChild = FindWindowEx(hChild, IntPtr.Zero, "FloatNotifySink", String.Empty);
hChild = FindWindowEx(hChild, IntPtr.Zero, "ComboBox", String.Empty);
hChild = FindWindowEx(hChild, IntPtr.Zero, "Edit", String.Empty); // File name edit control
// 向輸入框發送消息,填充目標xps文件名
SendMessage(hChild, WM_SETTEXT, IntPtr.Zero, pathFile);
// 等待1秒鍾
System.Threading.Thread.Sleep(1000);
// 找到對話框內的保存按鈕
hChild = IntPtr.Zero;
hChild = FindWindowEx(hWnd, IntPtr.Zero, "Button", "保存(&S)");
// 向保存按鈕發送2個消息,以模擬click消息,借此來按下保存按鈕
PostMessage(hChild, WM_LBUTTONDOWN, IntPtr.Zero, IntPtr.Zero);
PostMessage(hChild, WM_LBUTTONUP, IntPtr.Zero, IntPtr.Zero);
// 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;
}
接下來有關xps轉pdf,使用了Spire.Pdf,官方有demo,這里不再說明
有圖有真相