Click here to Skip to main content
15,887,135 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 682.1K   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

 
GeneralRe: Restoring links in the RTF Pin
mav.northwind7-Oct-05 4:03
mav.northwind7-Oct-05 4:03 
GeneralRe: Restoring links in the RTF Pin
erking8-Oct-05 7:17
erking8-Oct-05 7:17 
GeneralRe: Restoring links in the RTF Pin
Paul Cookson17-Feb-06 22:33
Paul Cookson17-Feb-06 22:33 
GeneralRe: Restoring links in the RTF Pin
erking19-Feb-06 21:52
erking19-Feb-06 21:52 
GeneralRe: Restoring links in the RTF Pin
Paul Cookson21-Feb-06 22:18
Paul Cookson21-Feb-06 22:18 
GeneralRe: Restoring links in the RTF Pin
SaschaBaumgart6-Mar-06 5:20
SaschaBaumgart6-Mar-06 5:20 
GeneralRe: Restoring links in the RTF Pin
Benny Raymond29-Sep-06 11:36
Benny Raymond29-Sep-06 11:36 
GeneralRe: Restoring links in the RTF Pin
itshibu200627-Jul-09 3:34
itshibu200627-Jul-09 3:34 
Dear Sir,

I am Shibu Mathew. I checked your solution and it is really impressive. But unfortunately in that coding implementation it is not showing the link if I put
this.SelectedRtf = @"{\rtf1\ansi \v LINKSTART\v0" + text + @"\v #" + hyperlink + @"#LINKEND\v0}";

if it is
this.SelectedRtf = @"{\rtf1\ansi \vLINKSTART\v0" + text + @"\v #" + hyperlink + @"#LINKEND\v0}";
Then it showing link. Could you please send me any sample coding you made for showing the link.

Kindly send me in my email id : shibu.mathew@jgc.com.sa

I am using vb.net I converted the code into Vb.net

Imports System
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Runtime.InteropServices

Namespace RichTextBoxLinks
Public Class RichTextBoxExNew
Inherits RichTextBox
#Region "Interop-Defines"
&lt;StructLayout(LayoutKind.Sequential)&gt; _
Private Structure CHARFORMAT2_STRUCT
Public cbSize As UInt32
Public dwMask As UInt32
Public dwEffects As UInt32
Public yHeight As Int32
Public yOffset As Int32
Public crTextColor As Int32
Public bCharSet As Byte
Public bPitchAndFamily As Byte
&lt;MarshalAs(UnmanagedType.ByValArray, SizeConst:=32)&gt; _
Public szFaceName As Char()
Public wWeight As UInt16
Public sSpacing As UInt16
Public crBackColor As Integer
' Color.ToArgb() -&gt; int
Public lcid As Integer
Public dwReserved As Integer
Public sStyle As Int16
Public wKerning As Int16
Public bUnderlineType As Byte
Public bAnimation As Byte
Public bRevAuthor As Byte
Public bReserved1 As Byte
End Structure

&lt;DllImport("user32.dll", CharSet:=CharSet.Auto)&gt; _
Private Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
End Function

Private Const WM_USER As Integer = &amp;H400
Private Const EM_GETCHARFORMAT As Integer = WM_USER + 58
Private Const EM_SETCHARFORMAT As Integer = WM_USER + 68

Private Const SCF_SELECTION As Integer = &amp;H1
Private Const SCF_WORD As Integer = &amp;H2
Private Const SCF_ALL As Integer = &amp;H4

#Region "CHARFORMAT2 Flags"
Private Const CFE_BOLD As UInt32 = &amp;H1
Private Const CFE_ITALIC As UInt32 = &amp;H2
Private Const CFE_UNDERLINE As UInt32 = &amp;H4
Private Const CFE_STRIKEOUT As UInt32 = &amp;H8
Private Const CFE_PROTECTED As UInt32 = &amp;H10
Private Const CFE_LINK As UInt32 = &amp;H20
Private Const CFE_AUTOCOLOR As UInt32 = &amp;H40000000
Private Const CFE_SUBSCRIPT As UInt32 = &amp;H10000
' Superscript and subscript are
Private Const CFE_SUPERSCRIPT As UInt32 = &amp;H20000
' mutually exclusive

Private Const CFM_SMALLCAPS As Integer = &amp;H40
' (*)
Private Const CFM_ALLCAPS As Integer = &amp;H80
' Displayed by 3.0
Private Const CFM_HIDDEN As Integer = &amp;H100
' Hidden by 3.0
Private Const CFM_OUTLINE As Integer = &amp;H200
' (*)
Private Const CFM_SHADOW As Integer = &amp;H400
' (*)
Private Const CFM_EMBOSS As Integer = &amp;H800
' (*)
Private Const CFM_IMPRINT As Integer = &amp;H1000
' (*)
Private Const CFM_DISABLED As Integer = &amp;H2000
Private Const CFM_REVISED As Integer = &amp;H4000

Private Const CFM_BACKCOLOR As Integer = &amp;H4000000
Private Const CFM_LCID As Integer = &amp;H2000000
Private Const CFM_UNDERLINETYPE As Integer = &amp;H800000
' Many displayed by 3.0
Private Const CFM_WEIGHT As Integer = &amp;H400000
Private Const CFM_SPACING As Integer = &amp;H200000
' Displayed by 3.0
Private Const CFM_KERNING As Integer = &amp;H100000
' (*)
Private Const CFM_STYLE As Integer = &amp;H80000
' (*)
Private Const CFM_ANIMATION As Integer = &amp;H40000
' (*)
Private Const CFM_REVAUTHOR As Integer = &amp;H8000


Private Const CFM_BOLD As UInt32 = &amp;H1
Private Const CFM_ITALIC As UInt32 = &amp;H2
Private Const CFM_UNDERLINE As UInt32 = &amp;H4
Private Const CFM_STRIKEOUT As UInt32 = &amp;H8
Private Const CFM_PROTECTED As UInt32 = &amp;H10
Private Const CFM_LINK As UInt32 = &amp;H20
Private Const CFM_SIZE As UInt32 = &amp;H8000000
Private Const CFM_COLOR As UInt32 = &amp;H40000000
Private Const CFM_FACE As UInt32 = &amp;H20000000
Private Const CFM_OFFSET As UInt32 = &amp;H10000000
Private Const CFM_CHARSET As UInt32 = &amp;H8000000
Private Const CFM_SUBSCRIPT As UInt32 = CFE_SUBSCRIPT Or CFE_SUPERSCRIPT
Private Const CFM_SUPERSCRIPT As UInt32 = CFM_SUBSCRIPT

Private Const CFU_UNDERLINENONE As Byte = &amp;H0
Private Const CFU_UNDERLINE As Byte = &amp;H1
Private Const CFU_UNDERLINEWORD As Byte = &amp;H2
' (*) displayed as ordinary underline
Private Const CFU_UNDERLINEDOUBLE As Byte = &amp;H3
' (*) displayed as ordinary underline
Private Const CFU_UNDERLINEDOTTED As Byte = &amp;H4
Private Const CFU_UNDERLINEDASH As Byte = &amp;H5
Private Const CFU_UNDERLINEDASHDOT As Byte = &amp;H6
Private Const CFU_UNDERLINEDASHDOTDOT As Byte = &amp;H7
Private Const CFU_UNDERLINEWAVE As Byte = &amp;H8
Private Const CFU_UNDERLINETHICK As Byte = &amp;H9
Private Const CFU_UNDERLINEHAIRLINE As Byte = &amp;HA
' (*) displayed as ordinary underline

#End Region

#End Region

Public Sub New()
Me.DetectUrls = False
End Sub

&lt;DefaultValue(False)&gt; _
Public Shadows Property DetectUrls() As Boolean
Get
Return MyBase.DetectUrls
End Get
Set(ByVal value As Boolean)
MyBase.DetectUrls = value
End Set
End Property

Public Sub InsertLink(ByVal text As String)
InsertLink(text, Me.SelectionStart)
End Sub

Public Sub InsertLink(ByVal text As String, ByVal position As Integer)
If position &lt; 0 OrElse position &gt; Me.Text.Length Then
Throw New ArgumentOutOfRangeException("position")
End If

Me.SelectionStart = position
Me.SelectedText = text
Me.[Select](position, text.Length)
Me.SetSelectionLink(True)
Me.[Select](position + text.Length, 0)
End Sub

Public Sub InsertLink(ByVal text As String, ByVal hyperlink As String)
InsertLink(text, hyperlink, Me.SelectionStart)
End Sub

'Public Sub InsertLink(ByVal text As String, ByVal hyperlink As String, ByVal position As Integer)
' If position &lt; 0 OrElse position &gt; Me.Text.Length Then
' Throw New ArgumentOutOfRangeException("position")
' End If

' Me.SelectionStart = position
' Me.SelectedRtf = ("{\rtf1\ansi " &amp; text &amp; "\v #") + hyperlink &amp; "\v0}"
' 'Me.SelectedRtf = "{\rtf1\ansi\ansicpg" &amp; System.Text.Encoding.Default.CodePage &amp; " " &amp; text &amp; "\v #" &amp; hyperlink &amp; "\v0}"
' Me.[Select](position, text.Length + hyperlink.Length + 1)
' Me.SetSelectionLink(True)
' Me.[Select](position + text.Length + hyperlink.Length + 1, 0)
'End Sub
Public Sub InsertLink(ByVal text As String, ByVal hyperlink As String, ByVal position As Integer)
If position &lt; 0 OrElse position &gt; Me.Text.Length Then
Throw New ArgumentOutOfRangeException("position")
End If

Dim startLinkStr As String = "LINKSTART"
Dim endLinkStr As String = "#LINKEND"

Dim linkStartPos As Integer = position - startLinkStr.Length

Me.SelectionStart = position
Me.SelectedRtf = ("{\rtf1\ansi \v LINKSTART\v0" &amp; text &amp; "\v #") + hyperlink &amp; "#LINKEND\v0}"
Me.[Select](position, text.Length + hyperlink.Length + 1 + endLinkStr.Length + startLinkStr.Length)
Me.SetSelectionLink(True)
Me.[Select](position + text.Length + hyperlink.Length + 1, 0)
End Sub
Public Sub ShowLinks()
Dim curPos As Integer = 0

Dim startTextStr As String = "LINKSTART"
Dim endTextStr As String = "#"
Dim endLinkStr As String = "#LINKEND"

While RtfHasMoreLinks(curPos)
' Find hidden link text
Dim sPos As Integer = Me.Text.IndexOf(startTextStr, curPos)
Dim hStartIndex As Integer = sPos

sPos += startTextStr.Length
Dim ePos As Integer = Me.Text.IndexOf(endTextStr, sPos)

' Now we have the text of link
Dim linkText As String = Me.Text.Substring(sPos, (ePos - sPos))

' Get the start position of link
Dim sLinkPos As Integer = ePos + endTextStr.Length
Dim eLinkPos As Integer = Me.Text.IndexOf(endLinkStr, sPos)

' The link
Dim hLink As String = Me.Text.Substring(sLinkPos, (eLinkPos - sLinkPos))

Dim hEndIndex As Integer = eLinkPos + endLinkStr.Length

' Show the link in box
ShowDistinctLink(hStartIndex, hEndIndex, linkText, hLink)

' Save the current end post at current position
curPos = eLinkPos
End While
End Sub
Private Function RtfHasMoreLinks(ByVal curPos As Integer) As Boolean
Return (If(Me.Text.IndexOf("LINKSTART", curPos) &gt; curPos, True, False))
End Function

Private Sub ShowDistinctLink(ByVal sPos As Integer, ByVal ePos As Integer, ByVal displayText As String, ByVal hyperlink As String)
If sPos &lt; 0 OrElse sPos &gt; ePos Then
Throw New ArgumentOutOfRangeException("position")
End If

Me.[Select](sPos, (ePos - sPos))
Me.SetSelectionLink(True)
Me.[Select](sPos + displayText.Length + hyperlink.Length + 1, 0)
End Sub
Public Sub SetSelectionLink(ByVal link As Boolean)
SetSelectionStyle(CFM_LINK, IIf(link, CFE_LINK, 0))
End Sub

Public Function GetSelectionLink() As Integer
Return GetSelectionStyle(CFM_LINK, CFE_LINK)
End Function


Private Sub SetSelectionStyle(ByVal mask As UInt32, ByVal effect As UInt32)
Dim cf As New CHARFORMAT2_STRUCT()
cf.cbSize = Convert.ToUInt32(Marshal.SizeOf(cf))
cf.dwMask = mask
cf.dwEffects = effect

Dim wpar As New IntPtr(SCF_SELECTION)
Dim lpar As IntPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(cf))
Marshal.StructureToPtr(cf, lpar, False)

Dim res As IntPtr = SendMessage(Handle, EM_SETCHARFORMAT, wpar, lpar)

Marshal.FreeCoTaskMem(lpar)
End Sub

Private Function GetSelectionStyle(ByVal mask As UInt32, ByVal effect As UInt32) As Integer
Dim cf As New CHARFORMAT2_STRUCT()
cf.cbSize = Convert.ToUInt32(Marshal.SizeOf(cf))
cf.szFaceName = New Char(31) {}

Dim wpar As New IntPtr(SCF_SELECTION)
Dim lpar As IntPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(cf))
Marshal.StructureToPtr(cf, lpar, False)

Dim res As IntPtr = SendMessage(Handle, EM_GETCHARFORMAT, wpar, lpar)

cf = DirectCast(Marshal.PtrToStructure(lpar, GetType(CHARFORMAT2_STRUCT)), CHARFORMAT2_STRUCT)

Dim state As Integer
If (cf.dwMask And mask) = mask Then
If (cf.dwEffects And effect) = effect Then
state = 1
Else
state = 0
End If
Else
state = -1
End If

Marshal.FreeCoTaskMem(lpar)
Return state
End Function
End Class
End Namespace


Thanks in advance
GeneralLink lost when minimize to systray Pin
Labero25-Aug-05 11:31
Labero25-Aug-05 11:31 
AnswerRe: Link lost when minimize to systray Pin
mav.northwind25-Aug-05 20:22
mav.northwind25-Aug-05 20:22 
GeneralClear all text Pin
Labero24-Aug-05 19:18
Labero24-Aug-05 19:18 
GeneralRe: Clear all text Pin
mav.northwind24-Aug-05 19:49
mav.northwind24-Aug-05 19:49 
GeneralRe: Clear all text Pin
Labero25-Aug-05 7:38
Labero25-Aug-05 7:38 
General.Rtf Link Pin
zx2c426-May-05 10:19
zx2c426-May-05 10:19 
GeneralRe: .Rtf Link Pin
mav.northwind24-Aug-05 19:54
mav.northwind24-Aug-05 19:54 
GeneralLinks Lost When Saving Pin
jazzfan16-May-05 14:43
jazzfan16-May-05 14:43 
GeneralRe: Links Lost When Saving Pin
mav.northwind16-May-05 19:44
mav.northwind16-May-05 19:44 
GeneralRe: Links Lost When Saving Pin
pal118329-Sep-06 23:23
pal118329-Sep-06 23:23 
GeneralImage EMF and Link Pin
nhooge11-May-05 1:01
sussnhooge11-May-05 1:01 
Generalutf-8 strings Pin
Mahdi.Jabbari8-May-05 1:38
Mahdi.Jabbari8-May-05 1:38 
GeneralRe: utf-8 strings Pin
Mahdi.Jabbari8-May-05 4:14
Mahdi.Jabbari8-May-05 4:14 
GeneralRe: utf-8 strings Pin
fethigurcan24-Feb-06 3:48
fethigurcan24-Feb-06 3:48 
GeneralRe: utf-8 strings Pin
nesaver8514-Oct-07 18:48
nesaver8514-Oct-07 18:48 
GeneralVB.NET Code Issue Pin
scriptsure@comcast.net3-May-05 10:33
scriptsure@comcast.net3-May-05 10:33 
GeneralPainting Issue Pin
alineurope12-Apr-05 11:38
alineurope12-Apr-05 11:38 

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.