Introduction
This article outlines how to write a display control based on System.Windows.Forms.WebBrowser
that uses HTML and CSS for richly formatted output. Features include:
- HTML based output that allows for tables and other HTML functionality
- Fully customizable CSS that gives the designer complete control over the display of text
- Auto-scrolling to the bottom so that new text is always visible
- A “page” queue that lets you limit how many blocks of text appear in the control
In addition, the demo code shows how to implement a custom context menu to implement Copy, Select All, Print, Print Preview and Page Setup functionality.
Background
In a fit of nostalgia, I started on a project to create a text-based game along the lines of Zork. With the output, though, I wanted to take advantage of the rich opportunities of a graphical interface: the text commands would be echoed in one format, room and item descriptions would be displayed in a different format, and so on.
My first attempt was to use the RichTextbox
control. Within an hour, I realized that RTF was just too cumbersome to do me any good. I started searching the web for an alternative.
Then it hit me: I was searching the WEB. Web browsers already had the ability to display rich content, and with far more bells and whistles than RTF could manage. Plus, writing an HTML document was easier than writing rich text source (doing hernia surgery on yourself using a butter knife is probably easier than writing rich text source, but I digress.)
Class VeryRichOutput
The .NET Framework comes with a basic browser control, WebBrowser
, which can render an HTML document with CSS. All I needed to do was add a few bits and pieces to implement helpful functionality, and I was done.
Text-based games produce a lot of output. Rather than let this grow arbitrarily large, I added the idea of “pages”, blocks of text that would be treated as a single unit. The property MaxPages
defines how big the output can grow, and a Queue(Of String)
stores the first-in, first-out list of pages. The method AddPage
manages the queue; if MaxPages
is set to a non-zero value (i.e., paging is on) and the queue has that many pages, AddPage
will drop the page at the head of the queue before adding the new page to the end.
The property Style
implements a List(Of String)
which lets me add CSS code to the page. Flagging the property with the Editor
attribute lets me tell the IDE to use the string collection editor from the Property Grid window rather than the generic list editor.
The protected
method GenerateDocument
assembles the styles and pages into a single HTML document, which gets passed to the DocumentText
property inherited from WebBrowser
. The base control lays out and renders the text to the output, then fires the OnDocumentCompleted
event which has been overridden to scroll to the bottom of the document.
The last piece was to add the public
method OutputPage
, which gets called to send a block of text to the control.
The basic class looks like this:
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
A subclass inherits its parent’s designer as well as its code. That means VeryRichOutput
would normally inherit the visual designer attached to most Control
s. I find this annoying, so I use the DesignerCategory
attribute to tell the IDE that the file should be treated as ordinary code and not as a control. When this control is itself subclassed, the child control also inherits the “code” designation.
WebBrowser
implements a number of properties that I want to either hide or change. For example, the property AllowNavigation
must be True
in order to alter the base DocumentText
property. To prevent this from accidentally being changed, I set the base property in the class’s constructor, then shadow the property to apply the <Browsable(False)>
and <EditorBrowsable(EditorBrowsableState.Never)>
attributes. I also hid the AllowWebBrowserDrop
, ScriptErrorsSuppressed
, Url
and WebBrowserShortcutsEnabled
properties, and set the IsWebBrowserContextMenuEnabled
property to default to False
(it is still available, though; more on that later.) The details can be seen in the source code.
Using the Control
Using the control is very simple. First, I need to add the CSS. Note that, as with any HTML document, I can modify the layout of the entire document by styling the body
tag.
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
Next, I need to format the text before it is given to the control. I use an instance of PersonalDataClass
(see project source for definition) and wrap everything up in styled HTML tags. In this code, BaseControl
is the name of the VeryRichOutput
control being written to.
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>")
SB.Append("</div>")
BaseControl.OutputPage(SB.ToString)
Here is what the output looks after a few “Add text” clicks with the left control set to MaxPages = 3
. Let’s see RichTextBox
do this:
Subclassing the Control
VeryRichOutput
, as written, is pretty basic. If you are using the class for structured data -- say, to display a look around a room or an alert that the Grue is sneaking up behind you -- you can make coding easier by subclassing it do the formatting for you.
SubclassedVeryRichOutput
inherits from VeryRichOutput
to implement a few additional features. It fills Styles
with CSS on its own. It implements the method OutputContactInfo
which takes a PersonalDataClass
object, extracts the data, wraps it in HTML tags and sends it to OutputPage
. It overrides the GenerateDocument
method to add text to the <title>
tag of the source document. (Kind of useless, but it illustrates how you can change the way the document source is created.) And lastly, it implements a custom context menu. The source code for all this can be found in the download.
What about "View Source" and "Find"?
I really, really would like to have these features, but Microsoft did not see fit to make them available. The base WebBrowser
provides methods for print, print preview and printer setup, and the HtmlDocument
property has the ExecCommand
method which lets me select all text and copy selected text, but the view source and find dialogs are completely buried. Supposedly, you can use undocumented COM routines to force your way in, but I could not get those to work. If nothing else, you can view the document source by dumping DocumentText
to a TextBox
, or maybe add your own view source dialog.
For debugging, go ahead and set the IsWebBrowserContextMenuEnabled
property to True
. This will enable the standard browser context menu with the standard View Source item. It will also display a lot of other menu options you may not want to give users access to, and it will also disable any custom context menu: use with caution.
To Infinity and Beyond...
The WebBrowser
control is a fully functional web browser, so there is no reason why you could not write external links or grab images or stylesheets off of the web. This approach is probably a Bad Idea: you cannot be sure that your users will have internet access, and most guardian programs get twitchy when apps suddenly start downloading things. If you want to give your users an actual web browser, give them an actual web browser.
That said, there is no reason you cannot write internal links, with anchor tags pointing to elsewhere in your output. I don’t expect there would be problems if you used the file://
protocol to import images, sound and other resources, and the availability of JavaScript opens several interesting avenues of exploration. If you experiment with this control, please post a comment and let everyone know what you learned.
In Conclusion
By harnessing the power of HTML, VeryRichOutput
allows you to display richly formatted text very easily. It is important to remember that your output will have all the weaknesses of HTML as well as the strength; keep this in mind when designing your styles. If you can figure out how to implement the native View Source and Find functionality, I would really be interested in seeing your code.
History
- Version 1 - 2011-08-01 - Initial release