问题引入:
有过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 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而非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的句柄:
貌似已经托管出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指针, 这个我不做解释了,文档里可以找到,我直接给代码:
首先要托管几个个函数:
构造函数改为:
到此,您已经成果托管了属于自己的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传入一个IntPtr作为最后一个输出参数,执行成功后就会返回IPIEHTMLDocument接口的指针了,要把该指针转换为COM接口对象也很简单,直接调用Marshal.GetObjectForIUnknown方法即可。代码如下:
至于IPIHTMLDocument2,IPIHTMLDocument3只不过是COM版本而已,根据您的需要可以随意转换,但处于IE兼容性考虑建议您使用IPIHTMLDocument2,到这里您已经学会如何在CF中和HTML互操作了,这个WebBrowser控件的其他属性,例如DocumentText您也可以通过Send一个AddText消息实现,这里就不再讨论了,OK,文章完。