Click here to Skip to main content
13,861,260 members
Click here to Skip to main content
Add your own
alternative version


57 bookmarked
Posted 23 Jan 2015
Licenced CPOL

EXTENDED Version of Extended Rich Text Box (RichTextBoxEx)

, 25 Dec 2018
Rate this:
Please Sign up or sign in to vote.
I've created an enhanced version of the Extended RichTextBox created by Razi Syed.

Download RichTextBoxEx UserControl and sample application, with PrintRichTextBox class library and TextRuler UserControl


The RichTextBox of Visual Studio allows a lot of things; the problem is, a host program has to initiate those things in code. The following control, RichTextBoxEx, which features a toolbar and ruler, allows elaborate end-user-initiated functionality in a single WinForms control. Below is the RichTextBoxEx control, plus CheckBoxes to enable/disable spell-check and picture insertion and a multi-line TextBox to show the main control's width, scroll position, and character code for any selected text.


This UserControl is a WinForms RichTextBox with a Toolstrip and Ruler added to allow the user to do the following things with it (some updates are recent--as of December 4, 2018):

  1. Setting  (foreground and background),  attributes, text alignment
  2. Finding and replacing search text
  3. Spell-checking
  4. Picture-inserting
  5. Hyphenation/dehyphenation
  6. Defining custom characters and smart-character conversions
  7. Saving and loading text
  8. Printing (using class library)
  9. Tracking scroll-bar information (using class library)
  10. Keeping track of whether recent changes have been made
  11. Setting tabs indents and tabs via a ruler
  12. Text listing (non-nested bulleted, numbered, and lettered lists)
  13. Symbol-inserting (unicode characters)

This tip features the RichTextBoxEx control--an enhanced version of the "Extended RichTextBox" created by Razi Syed--along with Paul Welter's "NetSpell - Spell Checker for .NET" (DLL and dictionary files only)--plus a class library for printing rich text, getting scroll-bar info, and setting list-styles (standard or enhanced control), a ruler control specially designed for rich-text boxes, and a small demo program. This version has last been updated on December 4, 2018.


This version of RichTextBoxEx has the following enhancements to Razi Syed's version:

  1. Buttons for Background Color, Italic, Insert Picture, Find/Replace, Find Next, Hyphenate, and Insert Symbol--and combo boxes for  face and size--have been added to the toolstrip. Also, when the control is resized, the constituent controls--toolstrip and text box--are resized to fill it.
  2. The underlying RichTextBox, "rtb", is declared Public so that its properties, methods, and events can be accessed; just be sure to include "rtb" in the object expression.
  3. The underlying "rtb" control has its AllowDrop property set to True, so that rich text can easily be dragged and dropped back and forth with other rich-text apps.HideSelection is set to False so selected text can be viewed while find, replace, and hyphenation dialogs are showing. ShowSelectionMargin is set to True so that the user can easily select whole lines of text.
  4. A context menu with shortcut keys is assigned to the "rtb" control.
  5. The constituent rich-text control's internal KeyDown event defines several "custom characters"--and raises an event to allow you to define your own custom characters or shortcuts; its internal KeyPress event defines "smart characters"--and raises an event to allow you to define your own smart-character replacements. (The text boxes for the Find/Replace dialog box recognize the default custom-character keystrokes now.)
  6. There are top-level properties for allowing/disallowing spell-check/bullets/picture-insertion/insert-text/smart-text by the user, showing/hiding the toolstrip (items' shortcut keys and any custom characters still work), allow/disallow setting text  and  together--and, as previously mentioned, an event to allow custom key sequences to generate RTF characters/strings.
  7. To search for text, use Find/Replace (Ctrl + F / Ctrl + H) to pull up a dialog box for entering search text, search direction (up or down), and whether or not to make search case-sensitive and/or whole word. If found in the rich-text, the word is selected. If opting to replace, you can replace one occurrence or all occurrences from the current position in the specified direction. To exit the Find or Replace dialog, hit Cancel (ESC). To find and select subsequent occurrences of the same text (using the same direction and criteria), use Find Next (F3).
  8. Pictures can be added. When the Insert Picture button (or the equivalent context-menu item) is selected, a file dialog allows the user to select a picture file. The picture is copied to the clipboard and pasted into the control at the current caret position (replacing any pre-highlighted text).
  9. The control can search for hyphenateable wrapped text; when such a word is found, a dialog box allows one to position the breaking point, and either hyphenate the word, skip it (leave it alone), or quit. (The word must be a series of alphanumeric characters with enough length/space to leave 2+ characters at the end of the first line and 3+ characters at the beginning of the second line after breaking.)
  10. The control can dehyphenate text: The options to hyphenate text, remove all hyphens, or remove only "hidden" hyphens (hyphens not displayed because text doesn't wrap after them) are in the "Hyphenate" context menu.
  11. A class library is added to facilitate printing of rich text, and tracking of the text box's scroll-bar info. The first 5 extension methods can be used to find page breaks within the text box, and to print or preview one, some, or all pages; the other 3 methods can be used to get or set the scroll-bars' positions or retrieve detailed information about either scroll bar.
  12. Three methods are created: 1 for saving RTF text, 1 for loading RTF text, and 1 for inserting pictures
  13. A ruler can be displayed to allow the user to set first-line/hanging/right indents and tabs.
  14. Lists can be specified as bulletted, Arabic-numbered, upper-case/lower-case Roman numeraled, or upper-case/lower-case lettered. (Lists cannot be "nested"--i.e., for outlining. Unfortunately.) 
  15. A dialog exists for finding an inserting symbols that can be represented by printable Unicode characters in various fonts.

Using the Code


Control's Reference Dependencies

To use the RichTextBoxEx UserControl in a project/solution, you must first reference RichTextBoxEx.dll, and, if you want to allow spell-checking, NetSpell.SpellChecker.dll.  Also, if you plan to use the extension methods of the PrintRichTextBox class library in the host application's code, then you need to Import RichTextBoxEx or Import RichTextBoxEx.PrintRichTextBox in any code files using the methods.

This version of RichTextBox.dll incorporates the main control (RichTextBoxEx), together with the TextRuler constituent control, and the PrintRichTextBox library, all into 1 single project--eliminating the need for as many references as before.

Each of these supporting libraries--PrintRichTextBox, TextRuler, and NetSpell--can be used on their own; to find out how to use the TextRuler, see the article General-purpose RULER Control for Use with RICH-TEXT CONTROLS by yours truly.

Member Changes

Also, if you already are using this control, note that the AllowBullets has been replaced by AllowLists. If you want your pre-existing programs to run or even display the (new version of the) control on the projects' forms, you must change all references to "AllowBullets" to "AllowLists" in all code, including the forms' "designer-code" files! (Simply use the Replace text option with the scope set to "Entire Solution"; or display Error box and click on errors about this to get to and fix the antiquated statements one at a time.)

Underlying Text Box Events

The underlying RichTextBox, property rtb, is defined as public so you can access its properties, methods, and events. To catch events for the underlying rtb, use AddHandler and RemoveHandler to hook procedures to them; using WithEvents and Handles doesn't work with constituent controls! The last line of the code sample below shows an example.

Just In Case...

If you see 2 entries in the Error List window saying, for instance, "TextRuler is not a member of RichTextBoxEx" popping up, simply enter the designer file RichTextBoxEx.Designer.vb, remove "RichTextBoxEx." from the object expressions in the offending statements (just click on the error messages to get to the statements), and Save. (This tends to happen when I display the UserControl and then open up the main code file; I don't know why. It's a minor irritant when editing the source code.)

Properties for RichTextBoxEx

  • MaintainSelection -- Boolean for whether highlighting should be restored to selected text after a replace, hyphenation, or spell-check operation
  • IsTextChanged --Boolean for whether text has been changed since it was saved, loaded, or last set to False (set to True to assume changes, False to indicate no recent changes). It is similar to the underlying rich-text box's Modfied property except that it is set to True, and the ChangesMade event is fired,when the Text property is set (or Rtf's value is changed), and not when "temporary" changes (like trial hyphenations) are made.
  • SetColorWithFont -- Boolean for whether it should be possible to set text  from within the Font dialog
  • ShowToolstrip -- Boolean for whether to show Toolstrip on top of RichTextBox
  • ShowRuler -- Boolean for whether to show a ruler for setting indents and tabs
  • AllowTabs -- Boolean for whether the user is able to set tabs for paragraphs (only meaningful if ShowRuler is True)
  • AllowSpellCheck -- Boolean for whether control should provide for spell-checking
  • AllowDefaultInsertText -- Boolean for whether control should provide automatic default custom-character insertions along with any user-defined ones
  • AllowDefaultSmartText -- Boolean for whether control should provide automatic default smart-character replacments along with any user-defined ones
  • AllowLists (NEW!) -- Boolean for whether control should allow list bulleting (replaces AllowBullets; see Note above)
  • AllowHyphenation -- Boolean for whether control should support hyphenation searches
  • AllowPictures -- Boolean for whether control should allow pictures to be inserted

  • AllowSymbols (NEW!) -- Boolean for whether control should allow custom symbols to be inserted using Insert Symbol dialog

  • UnitsForRuler -- TextRuler.Units enumeration for whether Inches or Centimeters should be used for the ruler (Import RichTextBoxEx or Import RichTextBoxEx.TextRuler into any code files that use this property!)

  • RightMargin  -- maximum printable width (rtb.RightMargin) for text; the ruler display is updated to reflect the change
  • FilePath -- String for directory the file dialog should start with (when saving text, loading text, or inserting pictures)
  • rtb -- constituent RichTextBox control
  • Text -- String for plain-text contents of text box (rtb.Text); IsTextChanged and rtb.Modified are set to True and ChangesMade event is fired always
  • Rtf -- String for RTF contents of text box (rtb.Rtf); IsTextChanged and rtb.Modified are set, and ChangesMade is fired, only if new value is different from existing value

Methods for RichTextBoxEx

  • booleanvalueSaveFile([filename]) -- saves context of rich-text box to an RTF-format file. If filename is null or omitted, then a file dialog is invoked with FilePath as the initial directory. Returns True if text was saved, False if dialog was canceled. Sets IsTextChanged to False if successful.
  • booleanvalue = LoadFile([filename]) -- loads the contents of an RTF-format file into the rich-text box. If filename is null or omitted, then a file dialog is invoked with FilePath as the initial directory. Returns True if text was loaded, False if dialog was canceled. Sets IsTextChanged to False if successful.
  • booleanvalue = InsertPicture([filename]) -- inserts a picture into the rich-text box at the caret (in place of any selected text). If filename is null or omitted, then a file dialog is invoked with FilePath as the initial directory. Returns True if picture was inserted, False if dialog was canceled. Sets IsTextChanged to True, and fires ChangesMade event, if successful.

Events for RichTextBoxEx

InsertRtfText -- allows host program to define custom (RTF) characters or functionality for various keystrokes.

  • INPUT:
    • e.KeyEventArgs -- information about keys being pressed (from "rtb"'s underlying KeyDown event)
    • e.RtfText -- custom text to be inserted (or replaced over selected text); format is RTF so that program can add functions and characters that are supported by the text box only through RTF codes (as opposed to properties and methods)

SmartRtfText -- allows host program to define smart (RTF) character-replacements for raw "dumb characters" as they are typed in.

  • INPUT:
    • e.KeyPressEventArgs -- information about keys being pressed (from "rtb"'s underlying KeyPress event)
    • e.RtfText -- custom text to be take the place of the incoming character, and possibly immediately preceding characters before the caret; format is RTF so that program can add functions and characters that are supported by the text box only through RTF codes (as opposed to properties and methods);
    • e.PrecedingCharacterCount -- number of existing characters, precedding the incoming character, which are to be removed before adding smart character(s).

ChangesMade -- fires when changes are made to contents of rich-text box; it differs from rtb_TextChanged in that it doesn't fire when "temporary" changes--like when the control does trial hyphenation in order to figure where hyphens can be inserted--take place. IsTextChanged will always be True when this event fires.

Extension Methods for PrintRichTextBox

NOTE: These methods are designed work for a standard RichTextBox, so that they work even if you're not using the RichTextBoxEx control. (When using the RichTextBoxEx control, use its rtb property to reference the underlying RichTextBox. Don't forget to Import RichTextBoxEx or Import RichTextBoxEx.PrintRichTextBox in any code file using the methods! When not using the UserControl, simply reference and Import PrintRichTextBox to use with the standard control.)

Extension methods for printout:

  • richtextbox.Print(PrintDocument) -- prints text. The PrinterSettings.PrintRange property of the PrintDocument instance determines whether the current page (at the caret), pages with selected text, a range of pages, or the entire text is printed.
  • dialogresult = richtextbox.PrintPreview(PrintPreviewDialog) -- previews text. What's previewed depends, once again, on the PrinterSettings.PrintRange property of the PrintDocument assigned to the dialog.
  • integerarray() = richtextbox.PageIndexes(PrintDocument) -- returns an array with the start positions within the text box for each successive page. This can be used to determine what text would be printed on what page.
  • richtextBox.SetRightMarginToPageWidth(PageSettings) -- sets richtextbox.RightMargin property so that text box wraps text at the same horizontal position as the printed page's width (within left and right margins). This method always sets control's WordWrap property to False.
  • richtextbox.SetPageWidthToRightMargin(PageSettings) -- sets printed page's right margin so that it wraps text at the same horizontal position as indicated by richtextbox.RightMargin property. This is SetRightMarginToPageWidth in reverse. If the RightMargin property is 0, then no changes are made.

Extension methods for tracking scroll bars:

  • scrollposition = richtextbox.GetScrollPosition() -- gets current horizontal and vertical scroll-bar positions in pixels to a System.Drawing.Point structure.
  • richtextbox.SetScrollPosition(scrollposition) -- sets the horizontal and vertical scroll-bars to pixel positions specified in a System.Drawing.Point structure.
  • scrollinfo = richtextbox.GetScrollBarInfo(type[, mask]) -- returns the range, page-size, position, and/or track-position info about a scroll bar to a PrintRichTextBox.ScrollInfo structure. type is a PrintRichTextBox.ScrollBarType enum value specifying Horizontal or Vertical; mask is an optional bit-flag PrintRichTextBox.ScrollBarMask enum value specifying what parameters to retrieve (by default, all 4 items).
  • textwidth = richtextbox.GetMaximumWidth() -- gets maximum horizontal width in pixels for any text. If control's RightMargin property is non-zero, then that is used; otherwise, if WordWrap is True, then control's client-area width is used; otherwise, the width of the widest line in the text is used. It also accounts for whether ShowSelectionMargin is True or False.

Extension method for making lists (NEW!):

  • richtextbox.SetListStyle(liststyle) -- makes the selected text in the rich box a style specified; liststyle is a PrintRichTextBox.RTBListStyle enum value specifying no list, bullets, Arabic numbers, lowercase letters, uppercase letters, lowercase Roman numerals, or uppercase Roman numerals for item headings. This method uses the rich-text box's SelectionBullet property to turn on/off listing, then uses SendKeys to set the specific list type.

A few code examples are given below:

Imports RichTextBoxEx
Imports RichTextBoxEx.PrintRichTextBox
Imports RichTextBoxEx.TextRulerControl

'   using a property or method

RichTextBoxEx1.AllowSpellCheck = True : RichTextBoxEx1.UnitsForRuler = TextRuler.Inches

RichTextBoxEx1.SaveFile("My Text.rtf")

'   using extension methods on underlying text box

Dim ScrollPosition As Point = RichTextBoxEx1.rtb.GetScrollPosition()



Dim Pages() As Integer = RichTextBoxEx1.rtb.PageIndexes(PrintDocument1)

RichTextBoxEx1.rtb.Select(Pages(2), Pages(3) - Pages(2)) ' select a page
RichTextBoxEX1.rtb.SetListStyle(RTBListStyle.LowercaseLetters) 'list as a, b, c, ...

'    catching events

AddHandler RichTextBoxEx1_InsertRtfText, RichTextBoxEx1.InsertRtfText

AddHandler RTB_DoubleClick, RichTextBoxEx1.rtb.DoubleClick ' event for underlying text box

Shortcut keys are keystroke-defined similar to Word. For instance, Ctrl+X is Cut, Ctrl + I is Italic, Ctrl + F is Find, Ctrl + H is Replace, F3 is Find Next, F7 is Spell-Check, etc. Simply click on the control's constituent component, ContextMenuStrip1, to see them all. (At run time, the menus can, of course, be invoked by right-clicking the text box. If the ruler is shown, then the user can switch between inches and centimeters by right-clicking the ruler in order to show its menu,)

As for the pre-defined custom characters, here is the list of keystrokes:


Key Sequence

Optional (syllable) hyphen
(- ; displays only when breaking
a word at the end of a line)

[Ctrl] + [-]

Em dash (—)

[Ctrl] + [Alt] + [-]

Left single quote (‘)

[Ctrl] + [`]

Left double quote (“)

[Ctrl] + [Shift] + [~]

Right single quote (’)

[Ctrl] + [']

Right double quote (”)

[Ctrl] + [Shift] + ["]

Copyright (©)

[Ctrl] + [Alt] + [C]

Registered trademark (®)

[Ctrl] + [Alt] + [R]

Trademark (™)

[Ctrl] + [Alt] + [T]


These custom-character-keystrokes can also be used for specifying special search and/or replacement text in the Find/Replace dialog; however, you can't customize their values, add additional custom characters, or specify "special actions" there like you can for the main control.

As for default smart-character conversions:

  1. A regular double quote ("), when typed, is changed to a left double quote (“) if tit occurs at the beginning of a line, or after a space, regular dash, em dash, tab, or left single quote; otherwise it is changed to a right double quote (”).
  2. A regular single quote ('), when typed, is changed to a left single quote (‘) if it occurs at the beginning of a line, or after a space, regular dash, em dash, tab, or left double quote; otherwise it is changed to a right single quote (’).
  3. A regular dash (-), when typed immediately after an existing regular dash is replaced (along with its preceding dash) with an em dash (—).

Demo Program

Features include the following:

  1. A Page Setup Dialog--invoked at the beginning of the program--to allow page size and margins to be set.
  2. Check boxes to enable/disable Spell-Check and Insert-Picture for the RichTextBoxEx control
  3. Displaying the UTF-32 Unicode values for the characters of the RichTextBoxEx's selected text in a (vertically-scrollable) standard TextBox--useful for determining which character codes do what in a RichTextBox--and showing the text box's current scroll position.
  4. Invoking the Print Dialog, followed by Print Preview and Print, when the text box is double-clicked or [Ctrl] + [P] is pressed.
  5. Saving or loading the text when [Ctrl] + [S] or [Ctrl] + [O], respectively, are pressed. Also, when exiting the application, the user is given the option to save text if it's been modified.
  6. The following custom characters (in addition to the default ones above):


Key Sequence

One-fourth (¼)

[Ctrl] + [4]

One-half (½)

[Ctrl] + [2]

Three-fourths (¾)

[Ctrl] + [3]

Start new page (RTF code: \page ;
displays in text box only as carriage return,
but will affect printout of pages)

[Ctrl] + [Shift] + [Enter]

Save text

[Ctrl] + [S]

Load (open) text

[Ctrl] + [O]



It also converts "1/4", "1/2", "3/4"--when they are typed in--into "¼", "½", and "¾", respectively.

Points of Interest

  1. A word about the hyphenation and spell-checker tools: They regard all optional/syllable hyphens--visible and invisible alike--as word separators. Therefore, if text is already hyphenated, they may confuse part of a word for a whole word. One solution is to dehyphenate the selected text (using the context menu), do the spell-check, then re-hyphenate it (using the button or context menu). (Another is going to the "NetSpell" article with the spell-check utility and modifying it to handle hyphenated words [hyphen character: ChrW(173)]. It could also then be designed to "auto-hyphenate" known words.)
  2. The Find/Replace, Hyphenate, and Insert Symbol dialog boxes are implemented modally, so as to have greater control over any selected text and to simplify my coding. (Hit Cancel ([ESC]) to exit find or replace dialog, or to exit hyphenation early.) The scope of text subject to search/modification for these features--and Spell-check--is the whole text unless a region of text is highlighted, in which case only the selected text is worked with. Replace, Hyphenation, and Spell-check restore the highlighting if MaintainSelection is True; Find (and find/replace if the last operation is a simple find) does not. Invoking Find through Ctrl + F displays a "find only" dialog (no replacement options). Finally, Find Next (F3) is not limited in scope to highlighted text; it looks through the full text for a subsequent occurrence.
  3. There is ruler-bar for the control, the link to the article explaining it is above in the part about "dependencies".
  4. I fixed the bug which occassionally causes event cascades when setting font face and size using the combo boxes in the toolbar. The key is the SettingFont Boolean, which allows events to guard against redundant setting of font or combo box entries. Consider it a Christmas present (posted December 25, 2018)!


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


About the Author

Robert Gustafson
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

Suggestionhyperlink click event? Pin
andremoraes4-Feb-19 11:44
memberandremoraes4-Feb-19 11:44 
QuestionYou should give credit to the source of the Ruler Pin
BillWoodruff11-Jan-19 3:51
mveBillWoodruff11-Jan-19 3:51 
SuggestionSave as Pin
Reggie Van Wyk26-Dec-18 14:32
professionalReggie Van Wyk26-Dec-18 14:32 
PraiseGreat control - I like it!! Pin
Jalapeno Bob9-Dec-18 14:07
professionalJalapeno Bob9-Dec-18 14:07 
QuestionRichTextBoxEx without TextRuler Pin
Paw Writer19-Jul-18 6:55
memberPaw Writer19-Jul-18 6:55 
AnswerRe: RichTextBoxEx without TextRuler Pin
Robert Gustafson4-Dec-18 13:34
memberRobert Gustafson4-Dec-18 13:34 
QuestionHow do get the RichTextBoxEx on my form? Pin
Paw Writer15-Jul-18 14:53
memberPaw Writer15-Jul-18 14:53 
AnswerRe: How do get the RichTextBoxEx on my form? Pin
Robert Gustafson4-Dec-18 14:03
memberRobert Gustafson4-Dec-18 14:03 
QuestionLanguage Pin
Price Stiffler30-Jun-18 7:08
memberPrice Stiffler30-Jun-18 7:08 
AnswerRe: Language Pin
Robert Gustafson4-Dec-18 14:20
memberRobert Gustafson4-Dec-18 14:20 
QuestionVersion differences Pin
Member 1266404625-Jun-18 4:00
memberMember 1266404625-Jun-18 4:00 
AnswerRe: Version differences Pin
Member 1266404625-Jun-18 5:27
memberMember 1266404625-Jun-18 5:27 
GeneralRe: Version differences Pin
Robert Gustafson4-Dec-18 14:16
memberRobert Gustafson4-Dec-18 14:16 
QuestionTable Insertion and Equation Editor Pin
gbhsk9-Mar-18 21:55
membergbhsk9-Mar-18 21:55 
AnswerRe: Table Insertion and Equation Editor Pin
Robert Gustafson21-Apr-18 15:48
memberRobert Gustafson21-Apr-18 15:48 
GeneralRe: Table Insertion and Equation Editor Pin
Robert Gustafson4-Dec-18 14:09
memberRobert Gustafson4-Dec-18 14:09 
QuestionIs it just me ...!? Pin
Member 80348605-Feb-18 22:27
memberMember 80348605-Feb-18 22:27 
AnswerRe: Is it just me ...!? Pin
Robert Gustafson21-Apr-18 15:49
memberRobert Gustafson21-Apr-18 15:49 
GeneralMy vote of 5 Pin
Member 1236439020-Dec-17 1:26
memberMember 1236439020-Dec-17 1:26 
QuestionSuggestion Pin
Gary Strunk19-Dec-17 15:42
memberGary Strunk19-Dec-17 15:42 
GeneralMy vote of 5 Pin
BillWoodruff30-Nov-17 7:53
mveBillWoodruff30-Nov-17 7:53 
QuestionSigned version Pin
MeziLu17-Aug-16 15:58
memberMeziLu17-Aug-16 15:58 
AnswerRe: Signed version Pin
Robert Gustafson3-Dec-17 14:04
memberRobert Gustafson3-Dec-17 14:04 
GeneralRe: Signed version Pin
MeziLu4-Dec-17 6:08
memberMeziLu4-Dec-17 6:08 
QuestionError: Index outside the bounds of the array Pin
ChpeS30-Jun-16 22:15
memberChpeS30-Jun-16 22:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web03 | 2.8.190214.1 | Last Updated 25 Dec 2018
Article Copyright 2015 by Robert Gustafson
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid