問題引入:
有過CF的項目經驗的朋友一定常常遇到與BS後台對接的問題,HTML在BS系統中有著得天獨厚的條件,他能夠直接被用作界面顯示,並且能夠被C#代碼和Javascript操作,因此在一些應用中BS系統可能采取在數據庫中存儲HTML表單的設計,例如一些表單可視化設計控件(Table Designer)生成的就是HTML,直接存儲HTML的好處在於繞過了解析HTML DOM的復雜性,可是在前端與之對接的Mobile應用程序中就帶來的問題,當我的.NET CF程序讀取到包含HTML的字段後就顯得很尴尬了,用正則表達式解析HTML生成WinFom界面顯然不切實際,然而.NET CF似乎只為我們提供了這麼一條路,因為如果用WebBroswer(以下簡稱WB)控件直接顯示HTML,很有可能因為HTML的規格不適合PDA屏幕而使得用戶體驗非常糟糕,一個常見的問題就是HTML FORM的寬度超出了Mobile設備屏幕范圍,而使得WB出現橫向滾動條,這還不是問題的關鍵,關鍵在於你將HTML交給了WB控件之後你就沒有任何控制權了,WebBrowser類為我們提供的唯一和HTML交互手段是一個叫DocumentText的屬性,遺憾的是該屬性是SetOnly的,也就是說只有Set訪問器,那麼獲取表單中輸入的數值就不可能了。這個問題在Full Framework(以下簡稱FF)中是不存在的,因為FF中的WebBroswer類為我們提供了Document屬性,它返回類型為HtmlDocument的HTML DOM結構,借助該屬性可以輕松的完成HTML交互任務。對於上述問題我的思路還是使用HTML DOM模型來和HTML交互,如果您使用過WebBroswer.Document屬性來操作HTML,那麼您應該知道HtmlDocument只不過是封裝了COM接口IHTMLDocument2的對象而已,他的大部分功能都是COM中提供的,那麼CF裡這個接口要怎麼引入呢?這就是本文要解決的問題.
解決之道:
解決上述問題並不需要造原子彈的技術,不過閱讀以下內容之前您需要具備Windows COM知識和.NET Compact Framework中P/Invoke基礎知識,這裡不需要您了解Data layout,但至少您要知道C++中的char*封送為string,不過作為CF開發者,我建議您還是深入學習P/Invoke,如果您沒有這些知識那麼我推薦您先閱讀以下文章:
http://msdn.microsoft.com/en-us/library/aa446529.aspx
http://msdn.microsoft.com/en-us/library/k3f1t3ct(VS.80).aspx
http://msdn.microsoft.com/en-us/library/aa446497.aspx
當您已經掌握了這些知識那麼恭喜您,您可能不需要把我的文章全部看完就能夠做我們要做的事了。
既然CF中的WebBrowser控件沒有提供我們對HtmlDocument的訪問,那麼我們來看看EVC中的HTML Control(以下簡稱HC)吧。
我一開始翻閱了大量Window Mobile SDK的文檔,甚至在幫助文檔中的HTML Control API Messages主題也找不到任何Message能夠取得HC內部的HTML,最後在一個國外的C++開發論壇中找到了一些線索,其實在Mobile API中其實是可以訪問HtmlDocument模型的,有這麼一個Message可以返回HC的HTML Document對象,那就是DTM_DOCUMENTDISPATCH,有趣的是我們在SDK文檔中輸入DTM_DOCUMENTDISPATCH可以找到幫助頁面,並且下方有HTML Control API Messages主題的鏈接,可是HTML Control API Messages幫助主題並沒有介紹DTM_DOCUMENTDISPATCH消息,我們來看看DTM_DOCUMENTDISPATCH的描述吧:
The DTM_DOCUMENTDISPATCH message is sent by an application to the HTML viewer control to request a reference to its IDispatch interface.
Syntax
DTM_DOCUMENTDISPATCH wParam = 0; lParam = (LPARAM)(IDispatch*) ppDisp;
ParameterswParam Not used. pDisp [out] Reference to the HTML viewer control's IDispatch interface. Return Values
Returns the HTML viewer control's IDispatch pointer. Use it to call QueryInterface(IID_IPIEHTMLDocument, (void**)&pHTMLDocument) to retrieve the HTML viewer control's IPIEHTMLDocument interface.
聰明的你一定看到了,原來FF中的IHtmlDocument2接口變成了IPIEHTMLDocument,看到IDispatch字樣您可以知道這也是一個COM接口,現在的問題在於我們如何在CF中使用C++中的HTML Control了,CF中的WebBrowser其實也只是對HTML Control進行了封裝,我們完全可以托管自己的WB,在開始托管HTML Control之前我先介紹一些CF中的控件的基礎知識。
在CF的System.Windows.Forms命名空間中的控件其實都包含了兩個層面,一是非托管代碼層,它包含在netcfagl2_0.dll中,該層直接關聯到底層的Windows CE 操作系統,這一層包含大量的用於GUI的邏輯代碼,例如對系統消息進行響應,二是托管代碼層,它包含在System.Windows.Forms.dll中,這也是我們CF最常用的,但是該層其實只包含了很少的GUI邏輯代碼,很多都只是以托管方式對底層邏輯進行的封裝,以暴露一些托管函數供我們使用。.NET運行時(Runningtime)負責對Control或者Component的托管狀態進行維護。
所有的托管控件需要使用GWL_USERDATA空間,該空間內存狀態都是由.NET運行時維護的,我們不能夠更改這些狀態。鑒於上面的原因,托管控件的任何屬性改變都是通過非托管層實現的,當然微軟為我們處理好了托管代碼和非托管代碼的互操作,然而CF為我們提供了與非托管層交互的入口,那就是Control.Handle屬性,他直接暴露了非托管代碼中的HWND。
所幸的是,.Net Compact Framework提供了大量的操作非托管代碼的函數,使得我們封裝非托管控件變得異常簡單,CF中提供了Delegate允許我們處理非托管控件的Wndproc, InteropServices.Marshal類為我們提供了在Object和Intptrs之間的封送處理。.NET CF2.0加入了對COM互操作的支持,使得我們托管ActiveX controls變得異常簡單,記得當年我在做CF1.0程序時還必須是使用Ordessy(名字可能拼錯了)提供的封送框架,該框架異常難用—_—!。
當然托管與非托管之間的交互還需要很多知識得您自己去學習,本文不是基礎補習,在此不過多介紹,下面我們看看如何托管Html Control,如果說了這麼多您還不是很明白其中的技術原理那跟我一起做就成。
首先打開HTML Control API Window Styles主題,我們可以看到關於Html Control的描述:
An HTML Viewer Control window is created by calling the Windows CE CreateWindow function with WC_HTML passed as the lpClassName parameter. WC_HTML is defined in htmlctrl.h as TEXT("DISPLAYCLASS").
The styles listed in the following table can be combined with other windows styles and are passed as the dwStyle parameter of CreateWindow.
既然我們要托管一個COM組件為一個System.Windows.Forms.Control自然需要創建一個繼承於Control的控件,看到幫助中的描述了把,需要托管CreateWindow方法,如果您不知道怎麼做,說明您沒有仔細閱讀我推薦的文章,沒關系,我直接給您:
[DllImport("coredll.dll")] public extern static IntPtr CreateWindowExW(uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hwndParent, int hMenu, int hInstance, int lpParam);
關於該函數的參數請您自己查幫助, 細心的朋友會發現,我用的是 CreateWindowExW而非CreateWindow,這是由於我要和FF保持一致,這又涉及到控件設計時問題,就不詳細解釋了,總之您明白這兩個函數效果是一樣的就行。我簡單介紹幾個參數的作用:
dwExStyle:表示窗體(這裡指非托管窗體)的樣式,例如是否要添加窗體OK按鈕等。具體定義在SDK的winuser.h頭文件中
lpClassName :已注冊類型的名稱。例如我們要托管的Html Control通過幫助文檔可以知道名稱為“DISPLAYCLASS”。
x,y:窗體在宿主中出現的位置,即左上角坐標。
nWidth,nHeight: 窗體在宿主中的寬和高。
dwStyle:這個很重要,他也是窗體樣式,但和第一個參數不同,他是一些基礎屬性,例如是否可見等。具體定義在SDK的winuser.h頭文件中
其他參數自行查文檔,上面所說的窗體和CF窗體概念不同,其實是指非托管控件,你可以理解為托管com組建就是把COM控件當作一個窗體放到CF的Control中。 dw的參數都能夠進行位運算,類似於位枚舉。
我們在自定義控件構造函數中直接獲得HTML Control的句柄:
public class WebBrowserEx : Control { private IntPtr m_hwnd; public WebBrowserEx() { m_hwnd = CreateWindowExW(0, "DISPLAYCLASS", null, 0x40000000 | 0x10000000, 0, 0, 0, 0, this.Handle, 0, 0, 0); } }
貌似已經托管出Html Control了,可是運行這些代碼會出現異常,其實大多數非托管組建都包含一個Init方法來初始化控件,我們打開幫助文檔HTML Control API Functions主題可以清楚的看到這個函數,描述為:
Initializes the HTML viewer control. Before calling InitHTMLControl, an application must load the HTML control library Htmlview.dll.
注意紅字部分,該函數調用必須加載Htmlview.dll,要加載這個DLL還需要獲得Module指針, 這個我不做解釋了,文檔裡可以找到,我直接給代碼:
首先要托管幾個個函數:
[DllImport("coredll.dll", EntryPoint = "GetModuleHandleW", SetLastError = true)]
public static extern IntPtr GetModuleHandle(string moduleName);
[DllImport("coredll.dll", EntryPoint = "LoadLibraryW", SetLastError = true)]
internal static extern IntPtr LoadLibraryCE(string lpszLib);
[DllImport("htmlview.dll", EntryPoint = "InitHTMLControl", SetLastError = true)]
private static extern int InitHTMLControl(IntPtr hinst);
構造函數改為:
public class WebBrowserEx : Control { private IntPtr m_hwnd; public WebBrowserEx() { IntPtr m_instance = GetModuleHandle(null); IntPtr module = LoadLibraryCE("htmlview.dll"); int result = InitHTMLControl(m_instance); m_hwnd = CreateWindowExW(0, "DISPLAYCLASS", null, 0x40000000 | 0x10000000, 0, 0, 0, 0, this.Handle, 0, 0, 0); } }
到此,您已經成果托管了屬於自己的WebBrowser了,下面說說獲得獲得COM接口IPIEHTMLDocument,這也是本文的重點。
在我們專攻代碼之前,我給您介紹一些COM互操作的基礎知識。如果您已經閱讀了我推薦你的第一篇文章,那麼或許您已經知道方法了,對於COM接口,.NET中使用Runtime Callable Wrapper (RCW) 對象來封裝一個COM對象,該對象能夠把native COM對象轉換為一個可以描述COM接口的托管接口。
有三種方法可以創建RCW:
1.使用tlbimp工具來生成互操作接口。
2.可以使用以下代碼獲得COM接口:
Type t = Type.GetTypeFromCLSID(new Guid("2CD19942-4103-4dcc-A75C-57DF5814C611") ); // GUID是COM接口注冊的GUID。
Object obj = Activator.CreateInstance(t);
該代碼將導致.NET調用CoCreateInstance方法創建COM對象。
3.您也可以通過marshaling方式獲得一個COM對象。
方法2和方法3會在調用COM接口方法時,還需要獲得Com Callable Wrappers (CCWs),不但開發效率低而且極易出錯,因此我推薦您使用方法1,在這裡我們也使用方法1完成任務。
Tlbimp工具允許您將.tlb,.dll形式的COM類型庫轉換為托管庫,並自動生成托管的DLL程序集。遺憾的是MobileSDK並沒有為我們提供包含IPIEHTMLDocument的類型庫,所幸的是,但是有一些技巧可以幫助我們避開這些困難,在SDK中包含了一個webvw.idl文件,裡面包含了IPIEHTMLDocument以及IPIEHTMLDocument2和IPIEHTMLDocument3接口定義,打開查看代碼,我們發現IPIEHTMLDocument接口方法中又引用了諸如IHtmlElement之類的接口,手動轉換這些代碼為C#代碼顯然不切實際,如果您決定做超人那也可以這樣做,有一種投機的方法可以從該文件創建一個類型庫。記得這個方法是在我很早以前看過的一篇MSDN文章中提到過的,原文我已經找不到了,但方法我還記得。
我們在VS命令行工具中使用midl工具來編譯webvw.idl文件。
midl /D UNDER_CE webwv.idl。
如果出現找不到Include文件的錯誤,您可以使用/I參數來指定查找目錄,注意/I的大小寫。MIDL 編譯器編譯生成可一個文件 webwv.tbl。如果您以前使用過COM組件那您一定想直接從VS中引用該文件了,抱歉,這個操作是錯誤的,因為該COM接口不是注冊在本機的,而是注冊在Mobile設備上的,所以您還不能直接引用,老老實實的用上面所說的方法1吧,也很簡單,打開VS命令行工具:
tlbimp webwv.tbl /out:webwv.dll
到這裡您已經生成了COM接口的托管程序集。接著我們看看如何調用的問題。
還記得前面提到過的DTM_DOCUMENTDISPATCH消息嗎?文檔中有一個示例語句
SendMessage(hwndHTML, DTM_DOCUMENTDISPATCH, 0, (LPARAM) &pDisp);
在看看pDisp 的描述:[out] Reference to the HTML viewer control's IDispatch interface. 顯然,這是一個輸出參數, Idispatch是一種COM接口類型,那麼我們還需要托管SendMessage方法,不會?不要緊,我直接給您代碼:
[DllImport("coredll.dll", SetLastError = true)]
public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref IntPtr lParam);
通過調用SendMessage傳入一個IntPtr作為最後一個輸出參數,執行成功後就會返回IPIEHTMLDocument接口的指針了,要把該指針轉換為COM接口對象也很簡單,直接調用Marshal.GetObjectForIUnknown方法即可。代碼如下:
public IPIEHTMLDocument3 HtmlDocument { get { IntPtr buffer = IntPtr.Zero; var intprt = Win32Window.SendMessage(this.NativeHandle, (int)(1024 + 123), 0, ref buffer); return Marshal.GetObjectForIUnknown(buffer) as HtmlViewExport.IPIEHTMLDocument3; } }
至於IPIHTMLDocument2,IPIHTMLDocument3只不過是COM版本而已,根據您的需要可以隨意轉換,但處於IE兼容性考慮建議您使用IPIHTMLDocument2,到這裡您已經學會如何在CF中和HTML互操作了,這個WebBrowser控件的其他屬性,例如DocumentText您也可以通過Send一個AddText消息實現,這裡就不再討論了,OK,文章完。