VeryRichOutput 控件






4.67/5 (7投票s)
通过子类化 WebBrowser 控件,在您的输出显示中利用 HTML 和 CSS 的强大功能。
引言
本文概述了如何编写一个基于 System.Windows.Forms.WebBrowser
的显示控件,该控件使用 HTML 和 CSS 进行富格式输出。功能包括:
- 基于 HTML 的输出,允许使用表格和其他 HTML 功能
- 完全可定制的 CSS,让设计者完全控制文本显示
- 自动滚动到底部,以便新文本始终可见
- “页面”队列,让您可以限制控件中显示的文本块数量
此外,演示代码还展示了如何实现自定义上下文菜单,以实现复制、全选、打印、打印预览和页面设置功能。

背景
在一阵怀旧情绪中,我开始了一个项目,旨在创建一个类似 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)>
属性。我还隐藏了 AllowWebBrowserDrop
、ScriptErrorsSuppressed
、Url
和 WebBrowserShortcutsEnabled
属性,并将 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
是怎么做到的。

子类化控件
正如所写,VeryRichOutput
相当基础。如果您将该类用于结构化数据——例如,显示房间周围的视图或 Grue 正在悄悄靠近您的警报——您可以通过子类化它来为您进行格式化,从而简化编码。
SubclassedVeryRichOutput
继承自 VeryRichOutput
,以实现一些附加功能。它自己填充 Styles
中的 CSS。它实现了 OutputContactInfo
方法,该方法接收 PersonalDataClass
对象,提取数据,将其包装在 HTML 标签中并发送到 OutputPage
。它重写 GenerateDocument
方法,将文本添加到源文档的 <title>
标签中。(有点无用,但它说明了如何更改文档源的创建方式。)最后,它实现了一个自定义上下文菜单。所有这些的源代码都可以在下载中找到。

那“查看源代码”和“查找”呢?
我真的非常想拥有这些功能,但微软认为没有必要提供它们。基础 WebBrowser
提供了打印、打印预览和打印机设置的方法,并且 HtmlDocument
属性有一个 ExecCommand
方法,允许我选择所有文本并复制选定的文本,但是查看源代码和查找对话框被完全隐藏了。据说,您可以使用未记录的 COM 例程强行进入,但我无法让它们工作。如果实在不行,您可以将 DocumentText
倾倒到 TextBox
中来查看文档源代码,或者添加您自己的查看源代码对话框。
为了调试,请继续将 IsWebBrowserContextMenuEnabled
属性设置为 True
。这将启用带有标准查看源代码项的标准浏览器上下文菜单。它还会显示许多您可能不想让用户访问的其他菜单选项,并且还会禁用任何自定义上下文菜单:请谨慎使用。
飞向无限,超越无限……
WebBrowser
控件是一个功能齐全的网页浏览器,因此没有理由不能编写外部链接或从网络上抓取图像或样式表。这种方法可能是一个坏主意:您无法确定用户是否能访问互联网,而且大多数守护程序在应用程序突然开始下载东西时会变得神经质。如果您想给用户提供一个真正的网页浏览器,就给他们一个真正的网页浏览器。
话虽如此,您完全可以编写内部链接,使用锚点标签指向输出中的其他位置。如果使用 file://
协议导入图像、声音和其他资源,我认为不会有问题,并且 JavaScript 的可用性开辟了几条有趣的探索途径。如果您尝试使用此控件,请发表评论,让大家知道您学到了什么。
结论
通过利用 HTML 的强大功能,VeryRichOutput
让您能够非常轻松地显示富文本。请务必记住,您的输出将同时拥有 HTML 的优点和缺点;在设计样式时请牢记这一点。如果您能想出如何实现原生的查看源代码和查找功能,我将非常乐意看到您的代码。
历史
- 版本 1 - 2011-08-01 - 初次发布