让桌面应用程序拥有 Web 外观






4.17/5 (21投票s)
2003年5月15日
11分钟阅读

192941

1871
本文介绍了一些关于如何编写具有 Web 外观的独立桌面应用程序的信息。它还提供了一个简化任务的框架。
引言
许多最新的程序都有 Web 外观。以 Windows XP 的登录屏幕、XP 上类别视图的控制面板、XP 上的用户账户管理器为例。或者 Visual Studio .NET 的开始页或其向导。它们看起来都像 HTML 页面(其中一些,例如最后两个,实际上就是)。原因是 HTML 页面可以看起来和感觉都很酷。比传统的对话框好得多。
这些应用程序是如何制作的?
有两种方法
- 使用标准的 Windows 控件并模仿网页的行为
- 将 Internet Explorer(以下简称 IE)用作嵌入式 ActiveX 控件
第一种方法一点好处都没有。事实证明,您大部分时间都花在编写代码来对齐控件、图片等。用户界面的微小更改会导致手工进行的源代码发生巨大变化(当您自己对齐控件时没有设计器可用)。Microsoft 试图通过锚定和停靠来解决这个问题。恕我直言,他们失败了。
因此,这使我们选择了第二种选择——将 IE 用作嵌入您应用程序中的 ActiveX 控件。
使用 IE 控件
要使用 IE 控件,我们需要解决 2 个任务:如何用 HTML 填充它,以及如何处理来自网页的事件。Microsoft 为这两者提供了解决方案,称为 HTML 对话框。不幸的是,它有很多缺点。首先,它仅适用于 C++ 和 MFC。其次,网页及其资源位于 exe/DLL 的 Win32 资源中。.Net 中不容易实现这一点。另一个问题是事件处理并不总是有效。有时您会按下按钮但它不会生成任何事件。此外,对话框的外观是固定的。您在设计时创建它们,它们就不会再改变了。总之:最好找其他方法。
用 HTML 填充控件
稍后我将讨论生成 HTML 的问题。现在我将假设 HTML 已经以某种方式生成了对话框。要用 HTML 填充控件,只需获取其 Document
对象,然后使用其 Write
函数即可。像这样(参见 WriteToDocument
函数)
using System;
using System.Windows.Forms;
using AxSHDocVw;
using mshtml;
namespace ShowHowToWriteToDocument
{
public class ShowHowToWriteToDocument {
AxWebBrowser m_webBrowser = null;
// Here goes the code that initializes the form and so on
// ............................
//
public void WriteToDocument(string aString)
{
IHTMLDocument2 doc = m_webBrowser.Document as IHTMLDocument2;
doc.write(aString);
}
void ClearContent()
{
IHTMLDocument2 doc = m_webBrowser.Document as IHTMLDocument2;
doc.write("");
doc.close();
doc.write("");
}
bool OnClickHandler(IHTMLEventObj o)
{
MessageBox.Show("click");
return true;
}
public void AttachEvents()
{
IHTMLDocument2 doc = m_webBrowser.Document as IHTMLDocument2;
doc.writeln("<body><button name='button1'>button1</button>"
+ "<button name='button1'>button2</button></body>");
object button = doc.all.item("button1", 0);
HTMLButtonElementEvents2_Event buttonEvents =
(HTMLButtonElementEvents2_Event)button;
buttonEvents.onclick += new
HTMLButtonElementEvents2_onclickEventHandler(OnClickHandler);
}
}
}
注意:在使用文档的 write
函数之前,必须导航到 about:blank。否则 Document
对象将为 null
。要清除文档内容,请使用 ClearContent
。
处理事件
更加复杂。需要将 .NET 代码片段附加到网页上控件的事件。这可以通过 mshtml.IHTMLDocument2
接口来实现。请参见示例中的 AttachEvents
和 OnClickHandler
函数。这是我之前提到的捕获 HTML 对话框事件的机制。它有一些严重的缺点。我注意到附加第一个事件处理程序需要很长时间(在我机器上接近 2 秒)。广泛的测试表明 MSHTML 库中存在一个错误,有时事件没有被附加。另一件事是您必须通过名称引用元素。通常这个名称是硬编码的,一旦您编写了安装事件处理程序的例程,您就无法轻松更改页面上 HTML 元素的名称 - 您还必须在 C# 代码中更改它。幸运的是,还有另一种方法。那就是 window.external
对象。Microsoft 提供了一种方法,让 HTML 页面中的 JavaScript 代码调用托管应用程序(托管 IE 控件的应用程序)的方法。有了它,程序员就不必在程序中硬编码控件的名称了。相反,他们向 window.external
对象提供函数,然后由附加到控件事件的 JavaScript 调用它们。请参见下面的示例。
using System;
using System.Windows.Forms;
using AxSHDocVw;
using mshtml;
using MsHtmHstInterop;
using System.Runtime.InteropServices;
namespace ShowHowToUseWindowExternal
{
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface TheWindowExternalInterface
{
[DispId(0)]
void HandleSomeEvent(string someInformation);
[DispId(1)]
bool HandleAnotherEvent(int someInformation);
}
public class TheWindowExternalImplementation : TheWindowExternalInterface
{
public TheWindowExternalImplementation() {}
public void HandleSomeEvent(string someInformation)
{
//Do something here
}
public bool HandleAnotherEvent(int someInformation)
{
//Do something here
return true;
}
}
public class DemonstrateWindowExternal
{
public class DocHostUIHandlerImpl : IDocHostUIHandler
{
object m_external;
public DocHostUIHandlerImpl(object aExternal)
{
m_external = aExternal;
}
public void EnableModeless(int fEnable) { }
public void GetOptionKeyPath(out string pchKey, uint dw)
{ pchKey = null; }
public void TranslateAccelerator(ref MsHtmHstInterop.tagMSG lpmsg,
ref System.Guid pguidCmdGroup, uint nCmdID) { }
public void FilterDataObject(MsHtmHstInterop.IDataObject pDO,
out MsHtmHstInterop.IDataObject ppDORet) { ppDORet = null; }
public void OnFrameWindowActivate(int fActivate) { }
public void UpdateUI() { }
public void ShowContextMenu(uint dwID,
ref MsHtmHstInterop.tagPOINT ppt,
bject pcmdtReserved, object pdispReserved)
{
throw new COMException("", 1);
}
public void TranslateUrl(uint dwTranslate, ref ushort pchURLIn,
System.IntPtr ppchURLOut)
{
throw new COMException("", 1);
}
public void ShowUI(uint dwID,
MsHtmHstInterop.IOleInPlaceActiveObject pActiveObject,
MsHtmHstInterop.IOleCommandTarget pCommandTarget,
MsHtmHstInterop.IOleInPlaceFrame pFrame,
MsHtmHstInterop.IOleInPlaceUIWindow pDoc) { }
public void GetExternal(out object ppDispatch)
{
ppDispatch = m_external;
}
public void ResizeBorder(ref MsHtmHstInterop.tagRECT prcBorder,
MsHtmHstInterop.IOleInPlaceUIWindow pUIWindow, int fRameWindow)
{ }
public void GetDropTarget(MsHtmHstInterop.IDropTarget pDropTarget,
out MsHtmHstInterop.IDropTarget ppDropTarget)
{ ppDropTarget = null; }
public void GetHostInfo(ref
MsHtmHstInterop._DOCHOSTUIINFO pInfo) { }
public void HideUI() { }
public void OnDocWindowActivate(int fActivate) { }
}
AxWebBrowser m_webBrowser = null;
// Here goes the code that initializes the form and so on
// ............................
//
public void InstallWindowExternal()
{
TheWindowExternalImplementation exObj;
exObj = new TheWindowExternalImplementation();
ICustomDoc custDoc = (ICustomDoc)m_webBrowser.Document;
custDoc.SetUIHandler(new DocHostUIHandlerImpl(exObj));
}
}
}
同样,要使用 InstallWindowExternal
函数,您必须先导航到某个网页,例如 about:blank。如果您对 IDocHostUIHandler
和 ICustomDoc
接口感兴趣,请参阅 MSDN。要使用它们,您应该拥有互操作程序集 MsHtmHstInterop.dll,可以在示例中找到。 IDocHostUIHandler
是一个遗留的 COM 接口。它期望从其方法返回 HRESULT
。通常 .NET 返回 S_OK
。在 TranslateUrl
和 ShowContextMenu
的情况下,您需要返回 S_FALSE
。这可以通过抛出 COMException
来完成。下面是如何从 HTML 页面使用 window.external
对象的一个示例。
<html>
<body>
<a href="javascript:window.external.HandleSomeEvent('someInfo')">
Invoke window.external.HandleSomeEvent('someInfo')
</a>
<a href="javascript:if(window.external.HandleAnotherEvent(5))
alert('true');">
Invoke window.external.HandleAnotherEvent(5)
</a>
</body>
</html>
一些未解决的问题
到目前为止,我们还没有讨论 HTML 生成。一种方法是将它存储在某个地方,读取它,并在需要时填充它。这对于某些应用程序有效,但这样显示的 HTML 页面将是静态的。使用某种框架来生成动态 Web 页面会很好。其他一些问题
- HTML 中的所有链接都必须是绝对的,因为页面实际上并不存在。这意味着您的程序必须在填充 HTML 之前修复所有链接。
- 您的所有内容(图片、样式...)都必须物理存在(在某个文件系统上)。您将无法动态生成图片或显示应用程序嵌入式资源中的资源。
- 您无法使用浏览器的历史记录功能。
解决方案
此类库提供的解决方案是一个小型 Web 服务器。它解决了上述所有问题。
服务器架构
Web 服务器具有插件架构。服务器充当容器,由您提供插件。所有工作都由它们完成。插件应提供 3 个函数:Resolves
、Answer
和 GetResourceAsStream
。当 Web 服务器收到请求时,它会开始遍历所有已安装的插件。它使用收到的请求作为参数调用它们的 Resolves
函数。如果该函数返回 true,Web 服务器将调用插件的 Answer
函数,该函数负责生成(或从某处读取)HTML 并将其发送回客户端。这些插件还应该实现一个函数——GetResourceAsStream
。此函数用于使用它们的虚拟地址访问资源(例如 /view/stf.html 是一个虚拟地址)。 GetResourceAsStream
仅用于静态内容 - 那些仅取决于文件名而不取决于请求参数的内容。它有点类似于 ASP 中的 Server.MapPath
。如果您在应用程序中需要某些资源,可以通过 Web 服务器的 GetResourceAsStream
函数访问它。它通过遍历所有已安装的插件并调用它们的 GetResourceAsStream
函数,直到其中一个返回非 null
的值。从现在开始,我将这些插件称为 **解析器**。
预定义的解析器
您可以使用的解析器有一些。它们分为两类:动态内容解析器和静态内容解析器。静态解析器用于为静态内容提供 HTML 服务器。它们的工作是将虚拟地址映射到物理地址。库中有 2 个静态解析器:一个用于位于物理文件系统上的资源,另一个用于嵌入在应用程序中的资源。只有一个动态内容解析器。它充当用户定义的 servlet 的容器。我稍后会回来讨论它。下面是如何启动服务器并添加 2 个解析器的一个示例
using System;
using System.Reflection;
using Vitamin.Research.WebFramework;
namespace ShowHowToUseServer
{
class ShowHowToUseServer
{
WebServer m_server;
public void Start()
{
m_server = new WebServer(8080);
ContentLocationResolver clr = new
ContentLocationResolver("c:\temp", "phys");
m_server.AddResolver(clr, 10, -1);
EmbeddedLocationResolver elr;
elr = new EmbeddedLocationResolver(
Assembly.GetExecutingAssembly(),
"stf.res", "emb");
m_server.AddResolver(elr, 10, 20);
m_server.Start();
}
}
}
执行 Start
方法后,您将拥有一个正在运行的 Web 服务器。您可以启动 IE 并连接到服务器。例如,可以这样:https://:8080/phys/stf.html。假设 temp 有一个名为 view 的子目录,并且其中有一个名为 stf.html 的文件。如果您输入 https://:8080/phys/view/stf.html,该文件将在浏览器中显示。请求将由第一个解析器处理。如果您调用 m_server.GetResourceAsStream("/phys/view/stf.html"),您将获得一个指向 c:\temp\view\stf.html 的只读流。现在假设您有一个名为 stf.res.anotherview.file.html 的嵌入式资源。如果您请求 https://:8080/emb/anotherview/file.html,该资源将在浏览器中显示。在上面的示例中,当将解析器添加到服务器时,您会指定 2 个数字。它们是线程池的数量和最大线程数。服务器是多线程的。一旦找到可以处理请求的解析器,它就会在另一个线程中启动其 Answer
函数。服务器启动时会创建一些线程并使它们休眠。我称它们为线程池。当请求到来时,服务器会唤醒其中一个线程并将其传递给请求。如果它们当前都在忙(没有休眠),服务器会创建一个新线程并在其中处理请求。之后,线程将被销毁。线程池的数量 + 空闲线程的数量不能超过最大线程数。如果所有线程池都已满并且服务器无法创建任何额外线程,则请求将被排队,并在线程可用时进行处理。如果您想运行某个解析器为单线程,请为 AddResolver
指定 1, 1 作为参数。
动态解析器和 servlet
框架中目前只有一个动态解析器。它充当 servlet 的容器。由程序员编写它们。Servlet 是实现 IServletPage
接口的类。通常,您从 ServletPageBase
抽象类派生您的 servlet 类。编写 servlet 时,您应该实现 Address
属性和 Answer
方法。从 servlet 编写 HTML 通常很麻烦。这就是为什么 ASP 页面被使用,例如。此引擎不支持 ASP 页面,但它为您提供了一个替代方案:XSLT servlet 页面。要使用它们,您应该重写 XsltServletPageBase
或 XsltServletPage
类(第一个类为您提供了一些额外的自由)。您应该实现 getXML
函数并提供转换的文件名(作为虚拟地址)。当用户尝试查看您的页面时,服务器将首先通过 getXML
函数获取 XML,然后使用提供的 XSL 转换将其转换为 HTML。优点是将数据与可视化完全分离。您首先编写并测试 getXML
函数,在确认它正常工作后,再编写 XSLT。另一个优点是您可以在服务器运行时更改 XSLT 并看到更改,而无需重新启动它。下面是 2 个示例:一个用于普通 servlet,一个用于 XSLT servlet。
普通 servlet
using System;
using System.Reflection;
using Vitamin.Research.WebFramework;
namespace PlainServletExample
{
class PlainServletExample : ServletPageBase
{
public PlainServletExample() {}
public override string Address
{
get
{
return "/test/test.sfrm";
}
}
public override void Answer
(Vitamin.Research.WebFramework.WebRequest aRequest)
{
aRequest.Response.WriteLine("<html><body>"
+ "A plain servlet example</body></html>")
}
}
}
XSLT servlet
using System;
using System.Xml;
using System.Reflection;
using Vitamin.Research.WebFramework;
namespace XsltServletExample
{
class XsltServletExample : XsltServletPage
{
public XsltServletExample() {}
public override string XslTransformName
{
get
{
return "/view/XsltTest.xslt";
}
}
public override XmlDocument getXML(WebRequest aRequest)
{
XmlDocument xdoc = new XmlDocument();
XmlElement elPage = xdoc.CreateElement("page");
xdoc.AppendChild(elPage);
for(int i = 0; i < 100; i++)
{
XmlElement el = xdoc.CreateElement("number");
XmlAttribute attr = xdoc.CreateAttribute("value");
attr.Value = i.ToString();
el.Attributes.Append(attr);
elPage.AppendChild(el);
}
return xdoc;
}
public override string Address
{
get
{
return "/view/XsltTest.xfrm";
}
}
}
}
XsltTest.xslt 文件
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html>
<body>
<xsl:for-each select="page/number">
<xsl:value-of select="@value"/>
<br/>
</xsl:for-each>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
请注意,为了使上述示例正常工作,您应该有一个静态解析器将 /view/XsltTest.xslt 解析到上面的文件。要将 servlet 添加到动态解析器,请使用 AddPage
和 AddAllPages
函数。第二个函数在传递的程序集中搜索标记有 ServletPage
属性的类,创建每个类的实例,然后通过 AddPage
函数将它们添加到解析器。
关于调试 servlet 的技巧
普通 servlet 的调试方式与调试程序相同。XSLT servlet 则不然。我还没有找到一个好的 XSL 转换调试器,所以我使用的技巧是在输出中写入消息。您最终可以通过编写一个带有打印消息到调试控制台的函数的 XSLT 扩展对象来使自己轻松一些,例如通过 Debug.Write
。另一个问题是您看不到 getXML
函数生成的 XML。为了能够做到这一点,您可以将 XsltServletPageBase.XMLDumpPath
设置为某个路径(在硬盘上),服务器会将生成的 XML 转储到那里。将 XsltServletPage.DebugReload
设置为 true,以便服务器在每次转换之前重新加载您的 XSLT 文件。否则,它将只加载一次并保留在内存中。您可以使用独立的浏览器调试程序的部分(某些页面)。
缺点和可能的改进
尚未考虑的问题之一是安全性。在此版本中,任何计算机上的任何人都可以连接到正在运行的服务器。一个好的改进是添加 ASP 支持。最难的部分是从 asp 创建 servlet(源代码)。之后使用 .NET 提供的编译器进行编译很容易。