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

VeryRichOutput 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (7投票s)

2011 年 8 月 1 日

CPOL

7分钟阅读

viewsIcon

19861

downloadIcon

382

通过子类化 WebBrowser 控件,在您的输出显示中利用 HTML 和 CSS 的强大功能。

引言

本文概述了如何编写一个基于 System.Windows.Forms.WebBrowser 的显示控件,该控件使用 HTML 和 CSS 进行富格式输出。功能包括:

  • 基于 HTML 的输出,允许使用表格和其他 HTML 功能
  • 完全可定制的 CSS,让设计者完全控制文本显示
  • 自动滚动到底部,以便新文本始终可见
  • “页面”队列,让您可以限制控件中显示的文本块数量

此外,演示代码还展示了如何实现自定义上下文菜单,以实现复制全选打印打印预览页面设置功能。

First.jpg

背景

在一阵怀旧情绪中,我开始了一个项目,旨在创建一个类似 Zork 的基于文本的游戏。然而,对于输出,我希望利用图形界面的丰富机会:文本命令将以一种格式回显,房间和物品描述将以另一种格式显示,等等。

我的第一次尝试是使用 RichTextbox 控件。不到一小时,我意识到 RTF 太笨重,对我没有任何帮助。我开始在网上搜索替代方案。

然后我突然明白了:我正在搜索 网络。Web 浏览器已经有能力显示丰富内容,而且功能远超 RTF。此外,编写 HTML 文档比编写富文本源更容易(用黄油刀给自己做疝气手术可能比编写富文本源更容易,但我跑题了)。

VeryRichOutput 类

.NET Framework 带有一个基本的浏览器控件 WebBrowser,它可以渲染带有 CSS 的 HTML 文档。我只需要添加一些零碎的东西来实现有用的功能,就完成了。

基于文本的游戏会产生大量的输出。为了不让它任意增长,我添加了“页面”的概念,即被视为单个单元的文本块。属性 MaxPages 定义了输出可以增长的程度,而 Queue(Of String) 存储了先进先出(first-in, first-out)的页面列表。方法 AddPage 管理队列;如果 MaxPages 设置为非零值(即分页已启用)并且队列有那么多页面,AddPage 将在将新页面添加到末尾之前删除队列头部的页面。

属性 Style 实现了一个 List(Of String),它允许我向页面添加 CSS 代码。用 Editor 属性标记该属性,可以告诉 IDE 使用属性网格窗口中的字符串集合编辑器,而不是通用列表编辑器。

protected 方法 GenerateDocument 将样式和页面组合成一个 HTML 文档,该文档会传递给从 WebBrowser 继承的 DocumentText 属性。基本控件布局并渲染文本到输出,然后触发 OnDocumentCompleted 事件,该事件已被重写以滚动到文档底部。

最后一步是添加 public 方法 OutputPage,它被调用以向控件发送文本块。

基本类看起来像这样:

Imports System.ComponentModel
Imports System.Drawing
Imports System.Text
Imports System.Windows.Forms

<DesignerCategory("code")> _
Public Class VeryRichOutput
    Inherits WebBrowser

#Region " Storage "

    Protected pMaxPages As Integer
    Protected pPages As Queue(Of String)
    Protected pStyles As List(Of String)

#End Region

#Region " Properties "

    <DefaultValue(0)> _
    <Description("The number of pages that will be displayed on a " + _
                 "first-in, first-out basis. Set to 0 for unlimited pages.")> _
    Public Property MaxPages() As Integer
        Get
            Return pMaxPages
        End Get
        Set(ByVal value As Integer)
            pMaxPages = value
        End Set
    End Property

    Protected ReadOnly Property Pages() As Queue(Of String)
        Get
            If pPages Is Nothing Then pPages = New Queue(Of String)
            Return pPages
        End Get
    End Property

    <Description("The list of styles available to the control. " + _
                 "Must be properly formatted CSS.")> _
    <Editor("System.Windows.Forms.Design.StringCollectionEditor, System.Design", _
            "System.Drawing.Design.UITypeEditor, System.Drawing")> _
    Public ReadOnly Property Styles() As List(Of String)
        Get
            If pStyles Is Nothing Then pStyles = New List(Of String)
            Return pStyles
        End Get
    End Property

#End Region

#Region " Constructors "

    Public Sub New()
        MaxPages = 0
        pPages = New Queue(Of String)
        pStyles = New List(Of String)
    End Sub

#End Region

#Region " Methods "

    Private Sub AddPage(ByVal Text As String)
        If MaxPages > 0 Then
            Do While Pages.Count >= MaxPages
                Pages.Dequeue()
            Loop
        End If

        Pages.Enqueue(Text)
    End Sub

    Protected Overridable Sub GenerateDocument()
        Dim SB As New StringBuilder

        SB.Append("<html><head><title></title>")

        SB.Append("<style type='text/css'>")
        For Each s As String In Styles
            SB.Append(s)
        Next
        SB.Append("</style></head>")

        SB.Append("<body>")
        For Each s As String In Pages
            SB.Append(s)
        Next
        SB.Append("</body></html>")

        Me.DocumentText = SB.ToString
    End Sub

    Protected Overrides Sub OnDocumentCompleted _
	(ByVal e As WebBrowserDocumentCompletedEventArgs)
        MyBase.OnDocumentCompleted(e)
        Document.Window.ScrollTo(0, Document.Body.ScrollRectangle.Height)
    End Sub

    Public Sub OutputPage(ByVal Text As String)
        AddPage(Text)
        GenerateDocument()
    End Sub

#End Region

End Class

子类继承其父类的设计器以及其代码。这意味着 VeryRichOutput 通常会继承附加到大多数 Control 控件的可视设计器。我发现这很烦人,所以我使用 DesignerCategory 属性来告诉 IDE 该文件应被视为普通代码而不是控件。当该控件本身被子类化时,子控件也继承“代码”指定。

WebBrowser 实现了一些我想要隐藏或更改的属性。例如,属性 AllowNavigation 必须为 True 才能更改基本 DocumentText 属性。为了防止意外更改此属性,我在类的构造函数中设置了基本属性,然后遮蔽该属性以应用 <Browsable(False)><EditorBrowsable(EditorBrowsableState.Never)> 属性。我还隐藏了 AllowWebBrowserDropScriptErrorsSuppressedUrlWebBrowserShortcutsEnabled 属性,并将 IsWebBrowserContextMenuEnabled 属性默认设置为 False(尽管它仍然可用;稍后会详细介绍)。详细信息可以在源代码中查看。

使用控件

使用该控件非常简单。首先,我需要添加 CSS。请注意,与任何 HTML 文档一样,我可以通过设置 body 标签的样式来修改整个文档的布局。

With BaseControl.Styles
    .Add("body {background-color:#EED;font-family:Times New Roman,serif;padding:1em;}")
    .Add(".Person {border-left:solid 3px #077;
	border-top:solid 3px #077;margin-bottom:1.5em;padding-left:0.5em;}")
    .Add(".Name {font-size:1.5em;font-weight:bold;}")
    .Add(".Addr {color:#700;}")
    .Add(".Country {color:#007;font-weight:bold;}")
End With

接下来,我需要格式化文本,然后才能将其提供给控件。我使用 PersonalDataClass 的实例(定义请参见项目源代码)并将所有内容包装在样式化的 HTML 标签中。在此代码中,BaseControl 是正在写入的 VeryRichOutput 控件的名称。

Dim SB As New StringBuilder
SB.Append("<div class='Person'>")
SB.AppendFormat("<div class='Name'>{0} {1}</div>", PDC.FirstName, PDC.LastName)
SB.AppendFormat("<div class='Addr'>{0}<br />", PDC.Address1)
If Not String.IsNullOrEmpty(PDC.Address2) Then
    SB.AppendFormat("{0}<br />", PDC.Address2)
End If
SB.Append(PDC.City)
If Not String.IsNullOrEmpty(PDC.StateProvince) Then
    SB.AppendFormat(", {0}", PDC.StateProvince)
End If
If Not String.IsNullOrEmpty(PDC.PostalCode) Then
    SB.AppendFormat(" {0}", PDC.PostalCode)
End If
If Not String.IsNullOrEmpty(PDC.Country) Then
    SB.AppendFormat("<br /><span class='Country'>{0}</span>", PDC.Country)
End If
SB.Append("</div>") 'End Addr

SB.Append("</div>") 'End Person

BaseControl.OutputPage(SB.ToString)

以下是几次“添加文本”点击后,左侧控件设置为 MaxPages = 3 的输出。让我们看看 RichTextBox 是怎么做到的。

MaxPages.jpg

子类化控件

正如所写,VeryRichOutput 相当基础。如果您将该类用于结构化数据——例如,显示房间周围的视图或 Grue 正在悄悄靠近您的警报——您可以通过子类化它来为您进行格式化,从而简化编码。

SubclassedVeryRichOutput 继承自 VeryRichOutput,以实现一些附加功能。它自己填充 Styles 中的 CSS。它实现了 OutputContactInfo 方法,该方法接收 PersonalDataClass 对象,提取数据,将其包装在 HTML 标签中并发送到 OutputPage。它重写 GenerateDocument 方法,将文本添加到源文档的 <title> 标签中。(有点无用,但它说明了如何更改文档源的创建方式。)最后,它实现了一个自定义上下文菜单。所有这些的源代码都可以在下载中找到。

WithMenu.jpg

那“查看源代码”和“查找”呢?

我真的非常想拥有这些功能,但微软认为没有必要提供它们。基础 WebBrowser 提供了打印、打印预览和打印机设置的方法,并且 HtmlDocument 属性有一个 ExecCommand 方法,允许我选择所有文本并复制选定的文本,但是查看源代码和查找对话框被完全隐藏了。据说,您可以使用未记录的 COM 例程强行进入,但我无法让它们工作。如果实在不行,您可以将 DocumentText 倾倒到 TextBox 中来查看文档源代码,或者添加您自己的查看源代码对话框。

为了调试,请继续将 IsWebBrowserContextMenuEnabled 属性设置为 True。这将启用带有标准查看源代码项的标准浏览器上下文菜单。它还会显示许多您可能不想让用户访问的其他菜单选项,并且还会禁用任何自定义上下文菜单:请谨慎使用。

飞向无限,超越无限……

WebBrowser 控件是一个功能齐全的网页浏览器,因此没有理由不能编写外部链接或从网络上抓取图像或样式表。这种方法可能是一个坏主意:您无法确定用户是否能访问互联网,而且大多数守护程序在应用程序突然开始下载东西时会变得神经质。如果您想给用户提供一个真正的网页浏览器,就给他们一个真正的网页浏览器。

话虽如此,您完全可以编写内部链接,使用锚点标签指向输出中的其他位置。如果使用 file:// 协议导入图像、声音和其他资源,我认为不会有问题,并且 JavaScript 的可用性开辟了几条有趣的探索途径。如果您尝试使用此控件,请发表评论,让大家知道您学到了什么。

结论

通过利用 HTML 的强大功能,VeryRichOutput 让您能够非常轻松地显示富文本。请务必记住,您的输出将同时拥有 HTML 的优点和缺点;在设计样式时请牢记这一点。如果您能想出如何实现原生的查看源代码查找功能,我将非常乐意看到您的代码。

历史

  • 版本 1 - 2011-08-01 - 初次发布
© . All rights reserved.