导航

问题引入:
    有过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;
Parameters
wParam
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方法,如果您不知道怎么做,说明您没有仔细阅读我推荐的文章,没关系,我直接给您:

CreateWindowExW
[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的句柄:
Ctor

  1. public class WebBrowserEx : Control
  2.     {
  3.         private IntPtr m_hwnd;
  4.         public WebBrowserEx()
  5.         {
  6.             m_hwnd = CreateWindowExW(0, "DISPLAYCLASS",
  7.             null, 0x40000000 | 0x10000000,
  8.             0, 0, 0, 0,
  9.             this.Handle, 0, 0, 0);
  10.         }
  11.     }

 

貌似已经托管出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指针, 这个我不做解释了,文档里可以找到,我直接给代码:
      首先要托管几个个函数:
Win32API

  1. [DllImport("coredll.dll", EntryPoint = "GetModuleHandleW", SetLastError = true)]
  2.         public static extern IntPtr GetModuleHandle(string moduleName);
  3.         [DllImport("coredll.dll", EntryPoint = "LoadLibraryW", SetLastError = true)]
  4.         internal static extern IntPtr LoadLibraryCE(string lpszLib);
  5.         [DllImport("htmlview.dll", EntryPoint = "InitHTMLControl", SetLastError = true)]
  6.         private static extern int InitHTMLControl(IntPtr hinst);

 

构造函数改为:

  1. public class WebBrowserEx : Control
  2.     {
  3.         private IntPtr m_hwnd;
  4.         public WebBrowserEx()
  5.         {
  6.             IntPtr m_instance = GetModuleHandle(null);
  7.             IntPtr module = LoadLibraryCE("htmlview.dll");
  8.             int result = InitHTMLControl(m_instance);
  9.             m_hwnd = CreateWindowExW(0, "DISPLAYCLASS",
  10.             null, 0x40000000 | 0x10000000,
  11.             0, 0, 0, 0,
  12.             this.Handle, 0, 0, 0);
  13.         }
  14.     }

 

到此,您已经成果托管了属于自己的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方法,不会?不要紧,我直接给您代码:
SendMessage

  1. [DllImport("coredll.dll", SetLastError = true)]
  2.         public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref IntPtr lParam);

 

通过调用SendMessage传入一个IntPtr作为最后一个输出参数,执行成功后就会返回IPIEHTMLDocument接口的指针了,要把该指针转换为COM接口对象也很简单,直接调用Marshal.GetObjectForIUnknown方法即可。代码如下:
HtmlDocument Property

  1. public IPIEHTMLDocument3 HtmlDocument
  2.         {
  3.             get
  4.             {
  5.                 IntPtr buffer = IntPtr.Zero;
  6.                 var intprt = Win32Window.SendMessage(this.NativeHandle, (int)(1024 + 123), 0, ref buffer);
  7.                 return Marshal.GetObjectForIUnknown(buffer) as HtmlViewExport.IPIEHTMLDocument3;
  8.             }
  9.         }

 

至于IPIHTMLDocument2,IPIHTMLDocument3只不过是COM版本而已,根据您的需要可以随意转换,但处于IE兼容性考虑建议您使用IPIHTMLDocument2,到这里您已经学会如何在CF中和HTML互操作了,这个WebBrowser控件的其他属性,例如DocumentText您也可以通过Send一个AddText消息实现,这里就不再讨论了,OK,文章完。