Click here to Skip to main content
15,899,013 members
Articles / Mobile Apps
Article

Syntax highlighting textbox written in C#

Rate me:
Please Sign up or sign in to vote.
4.78/5 (81 votes)
13 Jul 20053 min read 973.4K   12.4K   225   190
This article describes the code of a RichTextBox that knows how to highlight parts of the text and provide auto-completion.

Sample Image

Introduction

Hey. This code is a class derived from .Net's RichTextBox. It offers syntax highlighting and auto-completion features (like Intellisense, only a little dumber).

Background

Have you ever written an application that had its own script language? Ever needed to edit SQL/code in you application? Didn't you just want a control that was a little better than a TextBox with a fixed width font? Now you can have it :-)

This control was written when I needed to edit SQLs in an application I wrote. Then I thought:

"Even if I could find a control that knows to edit SQLs well, it still wouldn't highlight names of tables or stored procedures that I wrote. And I know I'll need a control that can highlight my own script language sometime..."

So I sat down and wrote it. Later I realized that syntax highlighting without an auto-completion is not worth much, so I added support for that too.

Using the code

First of all, I must apologise. I'm not much of a GUI programmer, so all of the manipulation is done using code, as I lack the knowledge to write custom editors for a PropertyGrid. With that out of the way, we may continue! The control has two inputs for highlighting:

  1. Separators: Accessed using the Seperators property.

    Each separator is a char, and what is later refferd to as "token" is a not-empty string between separators.

  2. Highlight Descriptor: You can use the (how surprising) HighlightDescriptor method to add a HighlightDescriptor.

    A HighlightDescriptor is an instance of a class describing a highlighting rule, which can be divided into token identification info and design info.

Highlighting Rules

A highlighting rule has 6 fields it's constructed with:

  • Token: A string that is later compared to the text.
  • DescriptorType: Sets the highlighting type. The options are in a single word, to the end of the line, and to the corresponding closing token.
  • DescriptorRecognition: Determines how the token is compared to the token from the text. The options are: the text is equal to the token, the text begins with the token, and the text contains the token.
  • Color: Sets the color of the token when highlighted.
  • Font: Sets the font on the token when highlighted. This field is optional.
  • UseForAutoComplete: Determines if the token will be used for auto completion.

Due to laziness, a HighlightDescriptor's values can only be set in the constructor. <SideNote> I really think that the readonly keyword is under appreciated.</SideNote>.

The HighlightDescriptor is as follows:

C#
public class HighlightDescriptor
{
    public HighlightDescriptor(string token,
           string closeToken, Color color, Font font,
           DescriptorType descriptorType, DescriptorRecognition dr,
           bool useForAutoComplete)
    {
        Color = color;
        Font = font;
        Token = token;
        DescriptorType = descriptorType;
        CloseToken = closeToken;
        DescriptorRecognition = dr;
        UseForAutoComplete = useForAutoComplete;
    }

    public readonly Color Color;
    public readonly Font Font;
    public readonly string Token;
    public readonly string CloseToken;
    public readonly DescriptorType DescriptorType;
    public readonly DescriptorRecognition DescriptorRecognition;
    public readonly bool UseForAutoComplete;
}

Points of Interest

There were a few interesting things during the writing:

  1. Scrollbars: Since every time the text changes, I actually change it again (as far as the base textbox knows), the scrollbars reset themselves to top and left most positions. To resolve this, I used the EM_GETSCROLLPOS and EM_GETSCROLLPOS Windows messages:
    C#
    #region Scrollbar positions functions
    /// <summary>
    /// Sends a win32 message to get the scrollbars' position.
    /// </summary>
    /// <returns>a POINT structre containing horizontal
    ///       and vertical scrollbar position.</returns>
    private unsafe Win32.POINT GetScrollPos()
    {
        Win32.POINT res = new Win32.POINT();
        IntPtr ptr = new IntPtr(&res);
        Win32.SendMessage(Handle, Win32.EM_GETSCROLLPOS, 0, ptr);
        return res;
    
    }
    
    /// <summary>
    /// Sends a win32 message to set scrollbars position.
    /// </summary>
    /// <param name="point">a POINT
    ///        conatining H/Vscrollbar scrollpos.</param>
    private unsafe void SetScrollPos(Win32.POINT point)
    {
        IntPtr ptr = new IntPtr(&point);
        Win32.SendMessage(Handle, Win32.EM_SETSCROLLPOS, 0, ptr);
    
    }
    #endregion
    
  2. Keyboard strokes: During my work on the auto complete feature, I found out that even if you override the OnKeyDown method and not call the base method, keystrokes of keys containing actual characters are still handled (they arrive using the WM_CHAR message). Due to that I decided to override the WndProc method and filter unwanted keystrokes at that level.
  3. RTF: I didn't really learn RTF for this project. I just took a regular RichTextBox and understood what I have to build for my needs. Hence my resulting RTF is probably not optimal. It probably has language limitations that can be solved fairly easily by manipulating the RTF header.

History

  • 05/29/2005: First release.
  • 04/06/2005: Fixed a few bugs when using the ToCloseToken DescriptorType.
  • 07/13/2005: Fixed a few bugs, added Undo/Redo functionality.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Israel Israel
Hey.
I've been programming since I first understood english at age 10, starting with QBASIC.
I've been developing production C# applications since .Net's first beta, and I enjoy every second of it, as I enjoy sailing, kitesurfing, and playing guitar.

Comments and Discussions

 
GeneralRe: Can you make the tab key work Pin
QuantumQuinn7-Mar-06 6:09
QuantumQuinn7-Mar-06 6:09 
GeneralRe: Can you make the tab key work Pin
Armano10-Apr-06 2:18
Armano10-Apr-06 2:18 
GeneralRe: Can you make the tab key work Pin
Samuel8426-Jul-06 17:23
Samuel8426-Jul-06 17:23 
GeneralRe: Can you make the tab key work Pin
Samuel8426-Jul-06 17:25
Samuel8426-Jul-06 17:25 
Questionsuggestion: syntax file? Pin
Huisheng Chen22-Jul-05 0:07
Huisheng Chen22-Jul-05 0:07 
AnswerRe: suggestion: syntax file? Pin
larma22-Jul-05 0:27
larma22-Jul-05 0:27 
GeneralRe: suggestion: syntax file? Pin
Iluha[ua]9-Nov-05 3:54
Iluha[ua]9-Nov-05 3:54 
GeneralScintilla Pin
stax7619-Jul-05 22:08
stax7619-Jul-05 22:08 
RichTextBox is generally broke with drag & drop and multible undo not really working. Implementing syntax highlighting in RichTextBox is a hack that hardly can work without being painful slow and flickering. I would rather use the rock solid Scintilla. With the help of scide which is very mature and Reflector looking at the WinForms code I made a simple Scintilla control, it lacks seriously features, currently I don't need more features for myself.

<br />
Imports System<br />
Imports System.Runtime.InteropServices<br />
Imports System.IO<br />
Imports System.ComponentModel<br />
Imports System.Text<br />
<br />
Imports Stax<br />
Imports Stax.UI<br />
Imports Stax.Win32<br />
<br />
Namespace UI<br />
    Public Class Scintilla<br />
        Inherits Control<br />
<br />
        Private ScintillaHandle As IntPtr<br />
        Private DirectPointer As Integer<br />
        Private Initialized As Boolean<br />
<br />
        Public Sub New()<br />
            Dim path As String = CommonDirs.Startup + "SciLexer.dll"<br />
<br />
            If File.Exists(path) Then<br />
                MyBase.SetStyle(ControlStyles.UserPaint, False)<br />
                MyBase.SetStyle(ControlStyles.UseTextForAccessibility, False)<br />
                MyBase.SetStyle((ControlStyles.StandardDoubleClick Or ControlStyles.StandardClick), False)<br />
<br />
                LoadLibrary("SciLexer.dll")<br />
<br />
                ScintillaHandle = CreateWindowEx(0, "Scintilla", "", _<br />
                    WS.CHILD Or WS.VISIBLE Or WS.TABSTOP, 0, 0, _<br />
                    Width, Height, Handle, 0, IntPtr.Zero, Nothing)<br />
<br />
                DirectPointer = CInt(SlowPerform(2185, 0, 0))<br />
                SPerform(2037, CType(65001, UInt32), 0) 'Unicode<br />
<br />
                Application.AddMessageFilter(New MessageFiler(AddressOf MessageCallback))<br />
<br />
                Initialized = True<br />
            Else<br />
                BackColor = Color.White<br />
            End If<br />
        End Sub<br />
<br />
        Private Sub MessageCallback(ByRef m As Message, ByRef cancel As Boolean)<br />
            If m.HWnd = ScintillaHandle Then<br />
                Select Case m.Msg<br />
                    Case WM.LBUTTONDOWN, WM.RBUTTONDOWN<br />
                        If Not Focused Then<br />
                            FindForm.ActiveControl = Me<br />
                        End If<br />
                    Case 256, 257, 258, 260, 261 'implements all key event<br />
                        If CType(GetType(Control).InvokeMember("ProcessKeyMessage", _<br />
                               Reflection.BindingFlags.Instance Or _<br />
                               Reflection.BindingFlags.InvokeMethod Or _<br />
                               Reflection.BindingFlags.NonPublic, _<br />
                               Nothing, Me, New Object() {m}), Boolean) Then<br />
<br />
                            cancel = True<br />
                        End If<br />
                End Select<br />
            End If<br />
        End Sub<br />
<br />
        Protected Overrides Function IsInputKey(ByVal keyData As Keys) As Boolean<br />
            If (keyData And Keys.Alt) <> Keys.Alt Then<br />
                Dim data As Keys = keyData And Keys.KeyCode<br />
<br />
                If data <> Keys.Tab Then<br />
                    Select Case data<br />
                        Case Keys.Prior, Keys.Next, Keys.End, Keys.Home, _<br />
                            Keys.Left, Keys.Up, Keys.Right, Keys.Down<br />
<br />
                            Return True<br />
                    End Select<br />
                Else<br />
                    Return ((keyData And Keys.Control) = Keys.None)<br />
                End If<br />
            End If<br />
<br />
            Return MyBase.IsInputKey(keyData)<br />
        End Function<br />
<br />
#Region " Properties "<br />
        Private ReadOnlyValue As Boolean<br />
<br />
        <Category("Scintilla"), DefaultValue(False)> _<br />
        Public Property [ReadOnly]() As Boolean<br />
            Get<br />
                If Initialized Then<br />
                    Return SPerform(2140, 0, 0) <> 0<br />
                Else<br />
                    Return ReadOnlyValue<br />
                End If<br />
            End Get<br />
            Set(ByVal value As Boolean)<br />
                ReadOnlyValue = value<br />
<br />
                If Initialized Then<br />
                    If value Then<br />
                        SPerform(2171, CType(1, System.UInt32), 0)<br />
                    Else<br />
                        SPerform(2171, CType(0, System.UInt32), 0)<br />
                    End If<br />
                End If<br />
            End Set<br />
        End Property<br />
<br />
        Public ReadOnly Property Length() As Integer<br />
            Get<br />
                Return CInt(SPerform(2006, 0, 0))<br />
            End Get<br />
        End Property<br />
<br />
        Private TextValue As String = ""<br />
<br />
        Public Overrides Property Text() As String<br />
            Get<br />
                If Initialized Then<br />
                    Dim buffer As Byte() = New Byte(Length) {}<br />
                    Dim ptr As IntPtr = Marshal.AllocHGlobal(buffer.Length)<br />
                    SPerform(2182, CType(buffer.Length, UInt32), CType(ptr, UInteger))<br />
                    Marshal.Copy(ptr, buffer, 0, buffer.Length)<br />
                    Marshal.FreeHGlobal(ptr)<br />
                    Return UTF8Encoding.UTF8.GetString(buffer, 0, buffer.Length)<br />
                End If<br />
<br />
                Return TextValue<br />
            End Get<br />
            Set(ByVal value As String)<br />
                TextValue = value<br />
<br />
                If Initialized Then<br />
                    If value Is Nothing Then<br />
                        value = ""<br />
                    End If<br />
<br />
                    Dim buffer As Byte() = UTF8Encoding.UTF8.GetBytes(value + ChrW(0))<br />
                    Dim ptr As IntPtr = Marshal.AllocHGlobal(buffer.Length)<br />
                    Marshal.Copy(buffer, 0, ptr, buffer.Length)<br />
                    SPerform(2181, 0, CType(ptr, UInteger))<br />
                    Marshal.FreeHGlobal(ptr)<br />
                End If<br />
            End Set<br />
        End Property<br />
<br />
        Protected Overrides ReadOnly Property CreateParams() As CreateParams<br />
            Get<br />
                Dim ret As CreateParams = MyBase.CreateParams<br />
                ret.ExStyle = ret.ExStyle Or CInt(WS.EX_CLIENTEDGE)<br />
                Return ret<br />
            End Get<br />
        End Property<br />
#End Region<br />
<br />
#Region " Focus Management "<br />
        Private Function GetScintillaFocus() As Boolean<br />
            'Debug.WriteLine("GetScintillaFocus")<br />
<br />
            If Initialized Then<br />
                Return SPerform(2381, 0, 0) <> 0<br />
            End If<br />
        End Function<br />
<br />
        Private Sub SetScintillaFocus(ByVal focus As Boolean)<br />
            If Initialized Then<br />
                If focus Then<br />
                    'Debug.WriteLine("SetScintillaFocus true")<br />
                    SPerform(2380, CType(1, System.UInt32), 0)<br />
                Else<br />
                    'Debug.WriteLine("SetScintillaFocus false")<br />
                    SPerform(2380, CType(0, System.UInt32), 0)<br />
                End If<br />
            End If<br />
        End Sub<br />
<br />
        Protected Overrides Sub OnGotFocus(ByVal e As EventArgs)<br />
            'Debug.WriteLine("OnGotFocus")<br />
            SetScintillaFocus(SetFocus(ScintillaHandle) <> IntPtr.Zero)<br />
        End Sub<br />
<br />
        Protected Overrides Sub OnLostFocus(ByVal e As EventArgs)<br />
            'Debug.WriteLine("OnLostFocus")<br />
            SetScintillaFocus(False)<br />
        End Sub<br />
<br />
        Public Overrides ReadOnly Property Focused() As Boolean<br />
            Get<br />
                'Debug.WriteLine("Focused")<br />
                Return GetScintillaFocus()<br />
            End Get<br />
        End Property<br />
#End Region<br />
<br />
#Region " Native Functions "<br />
        <DllImport("kernel32")> _<br />
        Private Shared Function LoadLibrary(ByVal lpLibFileName As String) As IntPtr<br />
        End Function<br />
<br />
        <CLSCompliant(False), DllImport("user32")> _<br />
        Private Shared Function CreateWindowEx(ByVal dwExStyle As UInteger, ByVal lpClassName As String, ByVal lpWindowName As String, ByVal dwStyle As UInteger, ByVal x As Integer, ByVal y As Integer, ByVal width As Integer, ByVal height As Integer, ByVal hWndParent As IntPtr, ByVal hMenu As Integer, ByVal hInstance As IntPtr, ByVal lpParam As String) As IntPtr<br />
        End Function<br />
<br />
        <CLSCompliant(False), DllImport("user32.dll")> _<br />
        Private Shared Function SendMessage(ByVal hWnd As Integer, ByVal msg As UInt32, ByVal wParam As Integer, ByVal lParam As Integer) As Integer<br />
        End Function<br />
<br />
        <DllImport("user32.dll")> _<br />
        Private Shared Function SetWindowPos(ByVal hWnd As IntPtr, ByVal hWndInsertAfter As Integer, ByVal X As Integer, ByVal Y As Integer, ByVal cx As Integer, ByVal cy As Integer, ByVal uFlags As Integer) As Integer<br />
        End Function<br />
<br />
        <CLSCompliant(False), DllImport("scilexer.dll", EntryPoint:="Scintilla_DirectFunction")> _<br />
        Private Shared Function Perform(ByVal directPointer As Integer, ByVal message As UInt32, ByVal wParam As UInt32, ByVal lParam As UInt32) As Integer<br />
        End Function<br />
<br />
        <CLSCompliant(False)> _<br />
        Private Function SlowPerform(ByVal message As UInt32, ByVal wParam As UInt32, ByVal lParam As UInt32) As UInt32<br />
            Return CType(SendMessage(CInt(ScintillaHandle), message, CInt(wParam), CInt(lParam)), UInt32)<br />
        End Function<br />
<br />
        <CLSCompliant(False)> _<br />
        Private Function SPerform(ByVal message As UInt32, ByVal wParam As UInt32, ByVal lParam As UInt32) As UInt32<br />
            Return CType(Perform(DirectPointer, message, wParam, lParam), UInt32)<br />
        End Function<br />
<br />
        <DllImport("user32.dll")> _<br />
        Public Shared Function SetFocus(ByVal hwnd As IntPtr) As IntPtr<br />
        End Function<br />
#End Region<br />
<br />
#Region " Event Handlers "<br />
        Private Sub Scintilla_Resize(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Resize<br />
            If Initialized Then<br />
                SetWindowPos(ScintillaHandle, 0, ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height, 0)<br />
            End If<br />
        End Sub<br />
#End Region<br />
<br />
    End Class<br />
End Namespace<br />

GeneralRe: Scintilla Pin
larma22-Jul-05 1:47
larma22-Jul-05 1:47 
GeneralRe: Scintilla Pin
stax7622-Jul-05 1:55
stax7622-Jul-05 1:55 
GeneralRe: Scintilla Pin
Kastellanos Nikos12-Dec-05 2:11
Kastellanos Nikos12-Dec-05 2:11 
GeneralRe: bitSorry for nOOb Pin
Uri Guy15-Jul-05 10:43
Uri Guy15-Jul-05 10:43 
GeneralString literals Pin
DotDue8-Jul-05 0:46
DotDue8-Jul-05 0:46 
GeneralRe: String literals Pin
Uri Guy9-Jul-05 8:21
Uri Guy9-Jul-05 8:21 
GeneralRe: String literals Pin
DotDue1-Aug-05 0:57
DotDue1-Aug-05 0:57 
GeneralRe: String literals Pin
KenBeckett13-Aug-05 18:22
KenBeckett13-Aug-05 18:22 
GeneralPerformance test Pin
larma18-Jun-05 4:53
larma18-Jun-05 4:53 
GeneralRe: Performance test Pin
Uri Guy9-Jul-05 8:20
Uri Guy9-Jul-05 8:20 
GeneralRe: Performance test Pin
Huisheng Chen21-Jul-05 18:03
Huisheng Chen21-Jul-05 18:03 
GeneralRe: Performance test Pin
Huisheng Chen22-Jul-05 0:09
Huisheng Chen22-Jul-05 0:09 
Question&quot;-- ..\n\r&quot; bug ? Pin
larma18-Jun-05 4:18
larma18-Jun-05 4:18 
QuestionRichTextBox to TextBox ? Pin
93Current16-Jun-05 1:31
93Current16-Jun-05 1:31 
AnswerRe: RichTextBox to TextBox ? Pin
Uri Guy16-Jun-05 9:13
Uri Guy16-Jun-05 9:13 
GeneralRe: RichTextBox to TextBox ? Pin
Anonymous20-Jun-05 0:58
Anonymous20-Jun-05 0:58 
GeneralBug with DescriptorType.ToCloseToken Pin
lomegah15-Jun-05 6:38
lomegah15-Jun-05 6: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.