Click here to Skip to main content
15,867,835 members
Articles / Desktop Programming / Windows Forms
Article

Links with arbitrary text in a RichTextBox

Rate me:
Please Sign up or sign in to vote.
4.84/5 (80 votes)
1 Jan 2005CPOL4 min read 673.5K   11.9K   96   210
An extended RichTextBox that allows to enter links <i>not</i> starting with one of the standard protocols.

Sample Image - RichTextBoxLinks.gif

Introduction

From time to time, somebody asks in the forums if it is possible to add links to a RichTextBox that don't start with http://, ftp:// or another one of the 'standard' protocols.

Well, here's the solution.

Background

The standard RichTextBox has a quite handy property: DetectUrls.

When this property is set, every time the text in the RichTextBox is changed, the text is parsed for URLs and the matching text ranges are formatted as links (underlined, blue foreground by default).

The problem is, only links starting with one of the recognized protocols (http:, file:, mailto:, ftp:, https:, gopher:, nntp:, prospero:, telnet:, news:, wais:, outlook:) are recognized and reformatted. When you don't want such a reference, you're stumped, because the standard RichTextBox doesn't allow for manually setting the link style at all.

Fortunately, the .NET RichTextBox is only a wrapper around the Win32 RichEdit control, and so its functionality can be extended by adding the necessary wrappers to send the messages the RichEdit control needs.

How the RichEdit control manages style changes

The RichEdit control defines two sets of messages that control how part of the text is being rendered. One set EM_GETCHARFORMAT, EM_SETCHARFORMAT) is responsible for setting and querying formatting options for character ranges inside a paragraph, the other one (EM_GETPARAFORMAT, EM_SETPARAFORMAT) sets/queries formatting options for entire paragraphs (like alignment, for example).

We'll use the first one to set the desired style.

When you look up EM_SETCHARFORMAT's documentation, you'll see that a CHARFORMAT structure is used to transmit the formatting information. For recent versions of the RichEdit control (V2.0 and up), this structure is extended to a CHARFORMAT2 struct holding additional information.

The definition of this CHARFORMAT2 struct as taken from the platform SDK:

typedef struct _charformat2 {
    UINT cbSize;
    DWORD dwMask;
    DWORD dwEffects;
    LONG yHeight;
    LONG yOffset;
    COLORREF crTextColor;
    BYTE bCharSet;
    BYTE bPitchAndFamily;
    TCHAR szFaceName[LF_FACESIZE];
    WORD wWeight;
    SHORT sSpacing;
    COLORREF crBackColor;
    LCID lcid;
    DWORD dwReserved;
    SHORT sStyle;
    WORD wKerning;
    BYTE bUnderlineType;
    BYTE bAnimation;
    BYTE bRevAuthor;
    BYTE bReserved1;
} CHARFORMAT2;

Among other information, there are two members controlling formatting aspects that can be expressed as flags, i.e., part of the text has this formatting property set or not. These members are dwMask and dwEffects. dwMask is used to specify whether you want to set/query a given formatting option and dwEffect holds the actual value.

There's a flag CFE_LINK that does just what we want: give part of the text the link appearance and behaviour.

Wrapping up the structs and messages

In order to tell the RichEdit control that we want to assign a given character format to part of the text, you need to send the Windows message EM_SETCHARFORMAT to the control. If you're interested in how exactly the Win32 struct has been wrapped and how SendMessage() has been declared, please take a look at the source code.

Rewriting the struct declarations was quite straightforward, the only member that requires a little thought is szFaceName, because C# structs can't be declared to hold a character array of a given size. Microsoft accounted for this case, however, by supplying the SizeConst field to the MarshalAs attribute.

Using the class

When you use the extended RichTextBox, you'll have several new methods available:

C#
public void InsertLink(string text);
public void InsertLink(string text, int position);

to insert a link at a given position (or at the current insert position if not specified).

In case the link text is not suitable as a result of the LinkClicked event (for example, if you have several identical link text that you want to reference different hyperlinks), there are two additional methods where you can specify an additional hyperlink string:

C#
public void InsertLink(string text, string hyperlink);
public void InsertLink(string text, string hyperlink, int position);

They behave like the previous two methods, but after the link text itself, the hyperlink string is added invisibly, separated by a hash ('#'). For example, calling:

C#
InsertLink("online help", "myHelpFile.chm");

will give "online help#myHelpFile.chm" in the LinkClickedEventArgs. That way, you can manage link texts and hyperlinks independently.

The base methods to set or clear the link character format are:

C#
public void SetSelectionLink(bool link);
public int GetSelectionLink();

Both operate on the current selection. The return value of GetSelectionLink() has been made int instead of bool because the current selection can contain link - and regular parts and thus yield inconsistent results. This is reflected by returning -1, whereas consistent link style yields 1 and consistent non-link style yields 0.

Caveats

There's one detail to take care of. By default, the DetectUrls property of the standard RichTextBox is set, so whatever you type is reformatted automatically.

My extension turns this property off by default because it can interfere with links that have been added programmatically. When the DetectUrls property is set to true and you modify the text adjacent to one of your links, the link formatting will be lost. This does not happen when DetectUrls is set to false, so I recommend you leave it switched off.

Possible extensions

Just like I added support for CFE_LINK, it should be a breeze to add support for other formatting flags as well (for example, CFE_SUBSCRIPT or CFE_SUPERSCRIPT) to add new formatting options not available out of the box.

The necessary flag definitions are included in the source code already so that you don't have to look them up in the platform SDK anymore.

Modification History

  • 02.01.2005

    Initial release.

  • 03.01.2005

    Added support for invisible hyperlink strings.

License

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


Written By
Software Developer (Senior) 4voice AG
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
SuggestionFix for multiple links becoming one link (run on links) Pin
JarrettOberg13-Jan-24 14:03
JarrettOberg13-Jan-24 14:03 
QuestionSupport for .net 4.7, works with save and load Pin
Søren B13-Sep-22 1:55
Søren B13-Sep-22 1:55 
BugTrying to delete a link is problematic Pin
Robert Gustafson23-Jan-19 20:13
Robert Gustafson23-Jan-19 20:13 
QuestionThe link formatting are lost in version .Net Framework 4.7 and and higher Pin
vl_saf11-Aug-18 1:41
vl_saf11-Aug-18 1:41 
AnswerRe: The link formatting are lost in version .Net Framework 4.7 and and higher Pin
rinaldin3-Jul-19 22:03
rinaldin3-Jul-19 22:03 
SuggestionWorkaround to DetectUrls Pin
WillOmae15-Jan-17 4:05
WillOmae15-Jan-17 4:05 
QuestionCan we change the link color? Pin
33deepak6-Feb-16 3:48
33deepak6-Feb-16 3:48 
QuestionA simple solution with no Win32? Pin
Kalkidas17-Jul-15 3:11
Kalkidas17-Jul-15 3:11 
AnswerRe: A simple solution with no Win32? Pin
Marcus Mangelsdorf25-Jan-16 23:17
Marcus Mangelsdorf25-Jan-16 23:17 
GeneralRe: A simple solution with no Win32? Pin
Kalkidas14-Feb-16 2:48
Kalkidas14-Feb-16 2:48 
GeneralRe: A simple solution with no Win32? Pin
Marcus Mangelsdorf14-Feb-16 3:37
Marcus Mangelsdorf14-Feb-16 3:37 
GeneralRe: A simple solution with no Win32? Pin
mav.northwind14-Feb-16 4:15
mav.northwind14-Feb-16 4:15 
GeneralRe: A simple solution with no Win32? Pin
Kalkidas14-Feb-16 6:47
Kalkidas14-Feb-16 6:47 
GeneralRe: A simple solution with no Win32? Pin
Marcus Mangelsdorf13-May-16 6:59
Marcus Mangelsdorf13-May-16 6:59 
GeneralRe: A simple solution with no Win32? Pin
Robert Gustafson23-Jan-19 21:05
Robert Gustafson23-Jan-19 21:05 
QuestionLinkClicked Event in datagridView Pin
Member 1059577925-May-15 8:12
Member 1059577925-May-15 8:12 
Questionadded Unicode support and backslash escaping Pin
RonOnCode4-Feb-15 19:15
RonOnCode4-Feb-15 19:15 
GeneralMy vote of 5 Pin
Maxwolf Goodliffe12-Dec-14 7:14
Maxwolf Goodliffe12-Dec-14 7:14 
BugMDI Child Window issue => All links disappear Pin
jucKi18-Nov-14 0:01
jucKi18-Nov-14 0:01 
GeneralRe: MDI Child Window issue => All links disappear Pin
rinaldin9-May-15 6:07
rinaldin9-May-15 6:07 
QuestionRe: MDI Child Window issue => All links disappear Pin
Robert Gustafson23-Jan-19 20:58
Robert Gustafson23-Jan-19 20:58 
QuestionHelp Pin
Emmery Chrisco28-Sep-14 8:53
Emmery Chrisco28-Sep-14 8:53 
This is great. It's amazing nothing like has been done elsewhere considering this thread is ancient. Is there anyway to add support for a link hover event?
Also, it was stated that <pre>it should be a breeze to add support for other formatting flags as well (for example, CFE_SUBSCRIPT or CFE_SUPERSCRIPT) to add new formatting options not available out of the box.</pre> Can you provide some code to do that?
Btw, I program in vb, and have a hard time following C#.

I would love to have this control merged with this:
<pre lang="vb"><pre>Imports System
Imports System.Windows.Forms
Imports System.Drawing
Imports System.Runtime.InteropServices
Imports System.Drawing.Printing


''' &lt;summary&gt;
''' The rich text box print control class was developed by Microsoft, information about
''' this control can be found in your help files at:
''' ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.KB.v10.en/enu_kbvbnetkb/vbnetkb/811401.htm
''' In general, their intent was to create a rich text box control with print capability
''' embedded into the control.
''' &lt;/summary&gt;
''' &lt;remarks&gt;This control class replaces the use of the regular RichTextBox control; the
''' purpose of this extension was specifically to facilitate printing the contents
''' of a rich text box control.&lt;/remarks&gt;

Public Class RichTextBoxPrintCtrl
Inherits RichTextBox
' Convert the unit that is used by the .NET framework (1/100 inch)
' and the unit that is used by Win32 API calls (twips 1/1440 inch)
Private Const AnInch As Double = 14.4

&lt;StructLayout(LayoutKind.Sequential)&gt; _
Private Structure RECT
Public Left As Integer
Public Top As Integer
Public Right As Integer
Public Bottom As Integer
End Structure

&lt;StructLayout(LayoutKind.Sequential)&gt; _
Private Structure CHARRANGE
Public cpMin As Integer ' First character of range (0 for start of doc)
Public cpMax As Integer ' Last character of range (-1 for end of doc)
End Structure

&lt;StructLayout(LayoutKind.Sequential)&gt; _
Private Structure FORMATRANGE
Public hdc As IntPtr ' Actual DC to draw on
Public hdcTarget As IntPtr ' Target DC for determining text formatting
Public rc As Rect ' Region of the DC to draw to (in twips)
Public rcPage As Rect ' Region of the whole DC (page size) (in twips)
Public chrg As CHARRANGE ' Range of text to draw (see above declaration)
End Structure

Private Const WM_USER As Integer = &amp;H400
Private Const EM_FORMATRANGE As Integer = WM_USER + 57

Private Declare Function SendMessage Lib "USER32" Alias "SendMessageA" (ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wp As IntPtr, ByVal lp As IntPtr) As IntPtr

' Render the contents of the RichTextBox for printing
' Return the last character printed + 1 (printing start from this point for next page)
Public Function Print(ByVal charFrom As Integer, ByVal charTo As Integer, ByVal e As PrintPageEventArgs) As Integer

' Mark starting and ending character
Dim cRange As CHARRANGE
cRange.cpMin = charFrom
cRange.cpMax = charTo

' Calculate the area to render and print
Dim rectToPrint As RECT
rectToPrint.Top = e.MarginBounds.Top * AnInch
rectToPrint.Bottom = e.MarginBounds.Bottom * AnInch
rectToPrint.Left = e.MarginBounds.Left * AnInch
rectToPrint.Right = e.MarginBounds.Right * AnInch

' Calculate the size of the page
Dim rectPage As RECT
rectPage.Top = e.PageBounds.Top * AnInch
rectPage.Bottom = e.PageBounds.Bottom * AnInch
rectPage.Left = e.PageBounds.Left * AnInch
rectPage.Right = e.PageBounds.Right * AnInch

Dim hdc As IntPtr = e.Graphics.GetHdc()

Dim fmtRange As FORMATRANGE
fmtRange.chrg = cRange ' Indicate character from to character to
fmtRange.hdc = hdc ' Use the same DC for measuring and rendering
fmtRange.hdcTarget = hdc ' Point at printer hDC
fmtRange.rc = rectToPrint ' Indicate the area on page to print
fmtRange.rcPage = rectPage ' Indicate whole size of page

Dim res As IntPtr = IntPtr.Zero

Dim wparam As IntPtr = IntPtr.Zero
wparam = New IntPtr(1)

' Move the pointer to the FORMATRANGE structure in memory
Dim lparam As IntPtr = IntPtr.Zero
lparam = Marshal.AllocCoTaskMem(Marshal.SizeOf(fmtRange))
Marshal.StructureToPtr(fmtRange, lparam, False)

' Send the rendered data for printing
res = SendMessage(Handle, EM_FORMATRANGE, wparam, lparam)

' Free the block of memory allocated
Marshal.FreeCoTaskMem(lparam)

' Release the device context handle obtained by a previous call
e.Graphics.ReleaseHdc(hdc)

' Return last + 1 character printer
Return res.ToInt32()
End Function
End Class</pre></pre>
Thank you so much.
EDIT: The above code comes from the project found here: Building a Simple Word Processor Around an Extended RichTextBox Control[^]

QuestionSave and Load Pin
ilkerxx6-Jul-14 18:53
ilkerxx6-Jul-14 18:53 
AnswerRe: Save and Load Pin
ilkerxx7-Jul-14 0:59
ilkerxx7-Jul-14 0:59 
GeneralRe: Save and Load Pin
Member 110477105-Sep-14 0:03
Member 110477105-Sep-14 0:03 

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.