65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.17/5 (21投票s)

2003年5月15日

11分钟阅读

viewsIcon

192941

downloadIcon

1871

本文介绍了一些关于如何编写具有 Web 外观的独立桌面应用程序的信息。它还提供了一个简化任务的框架。

引言

许多最新的程序都有 Web 外观。以 Windows XP 的登录屏幕、XP 上类别视图的控制面板、XP 上的用户账户管理器为例。或者 Visual Studio .NET 的开始页或其向导。它们看起来都像 HTML 页面(其中一些,例如最后两个,实际上就是)。原因是 HTML 页面可以看起来和感觉都很酷。比传统的对话框好得多。

这些应用程序是如何制作的?

有两种方法

  1. 使用标准的 Windows 控件并模仿网页的行为
  2. 将 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 接口来实现。请参见示例中的 AttachEventsOnClickHandler 函数。这是我之前提到的捕获 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。如果您对 IDocHostUIHandlerICustomDoc 接口感兴趣,请参阅 MSDN。要使用它们,您应该拥有互操作程序集 MsHtmHstInterop.dll,可以在示例中找到。 IDocHostUIHandler 是一个遗留的 COM 接口。它期望从其方法返回 HRESULT。通常 .NET 返回 S_OK。在 TranslateUrlShowContextMenu 的情况下,您需要返回 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 个函数:ResolvesAnswerGetResourceAsStream。当 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 页面。要使用它们,您应该重写 XsltServletPageBaseXsltServletPage 类(第一个类为您提供了一些额外的自由)。您应该实现 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 添加到动态解析器,请使用 AddPageAddAllPages 函数。第二个函数在传递的程序集中搜索标记有 ServletPage 属性的类,创建每个类的实例,然后通过 AddPage 函数将它们添加到解析器。

关于调试 servlet 的技巧

普通 servlet 的调试方式与调试程序相同。XSLT servlet 则不然。我还没有找到一个好的 XSL 转换调试器,所以我使用的技巧是在输出中写入消息。您最终可以通过编写一个带有打印消息到调试控制台的函数的 XSLT 扩展对象来使自己轻松一些,例如通过 Debug.Write。另一个问题是您看不到 getXML 函数生成的 XML。为了能够做到这一点,您可以将 XsltServletPageBase.XMLDumpPath 设置为某个路径(在硬盘上),服务器会将生成的 XML 转储到那里。将 XsltServletPage.DebugReload 设置为 true,以便服务器在每次转换之前重新加载您的 XSLT 文件。否则,它将只加载一次并保留在内存中。您可以使用独立的浏览器调试程序的部分(某些页面)。

缺点和可能的改进

尚未考虑的问题之一是安全性。在此版本中,任何计算机上的任何人都可以连接到正在运行的服务器。一个好的改进是添加 ASP 支持。最难的部分是从 asp 创建 servlet(源代码)。之后使用 .NET 提供的编译器进行编译很容易。

© . All rights reserved.