Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

NHunspellTextBoxExtender - A Spellchecking IExtenderProvider for TextBoxes using Hunspell for .NET

4.84/5 (36 votes)
2 Nov 2010CPOL16 min read 367.4K   9.8K  
Extends controls inheriting TextBoxBase to provide off-line spell checking functionality

Image 1

Table of Contents

Introduction

With many applications, spell checking can be a vital aspect to include. Most people are accustomed to the spell checking capabilities of products like Microsoft Word or OpenOffice. There are products available for purchase that can add spell checking capability, such as SharpSpell that can cost hundreds of dollars. Unfortunately, there is a lack of Open Source, freely available tools that can provide the functionality of Microsoft Word. That is why I began to work on a spell checking IExtenderProvider that could extend any control that inherits TextBoxBase (both TextBox and RichTextBox inherit TextBoxBase).

Background

As part of my normal duties, I was asked to develop a database for storing Salmon Recovery Actions for inclusion in Salmon Recovery Plans. To provide the highest level of functionality that I could, I also developed a stand-alone application that provides all of the GUIs for interacting with the database. However, I soon found out that the lack of spell checking resulted in a number of spelling errors within the database. The only way to remove those spelling errors was to go into Access and use Access' spell checking capabilities. That meant that the responsibility fell on me as I was the only person who was allowed to use the database directly.

To avoid this, I wanted to provide spell checking capability to my GUI application. There were plenty of ways to spell check text, from using NetSpell, to programmatically using Microsoft Word's spell checker in a way similar to this article. Not every user could be assured to have Microsoft Word, and opening a new Word application can take some time and uses resources, so I wanted to stay away from that. I chose instead to use NHunspell. A useful article on it can be found here.

I also wanted to provide a visual cue to the user that there was a spelling error. With RichTextBoxes, this could be done through simple underlining, such as in this article. My problem was that I had written a lot of code using simple textboxes, and I didn't want to change all of them to RichTextBoxes. Instead, I wanted to use the IExtenderProvider to extend any textbox with spell checking capabilities. To be honest, I had no clue where to even begin to do that. That is, until I found this SharpSpell blog that describes exactly how to draw that wavy red line on any textbox. With that base code, I was able to use NHunspell to determine where to draw the line.

Exploring the Code

IExtenderProviders can be very useful, and most of us that code use them regularly, maybe even without knowing them. The simplest example is the ToolTip. When you add a ToolTip to a form, it doesn't show up as a control directly on the form. And, while it has its own properties, it also adds properties to other controls. The IExtenderProvider is the basis of my control. There is a very good article describing the IExtenderProvider here.

The class declaration is shown below. The class inherits Component and implements IExtenderProvider. When IExtenderProvider is being implemented, properties are generally provided to other controls. This is done through the ProvideProperty declaration before the class declaration.

VB.NET
<ToolboxBitmap(GetType(NHunspellTextBoxExtender), "spellcheck.png"), _
 ProvideProperty("SpellCheckEnabled", GetType(Control))> _
Public Class NHunspellTextBoxExtender
    Inherits Component
    Implements IExtenderProvider

    '
    'Rest of the code here
    '
End Class

However, the core of the class is still not complete. Whenever IExtenderProvider is being implemented, the IExtenderProvider.CanExtend function must be implemented. In my case, it is implemented as shown below:

VB.NET
Public Function CanExtend(ByVal extendee As Object) As Boolean _
       Implements System.ComponentModel.IExtenderProvider.CanExtend
    Return (TypeOf extendee Is TextBoxBase) And (Not myNHunspell Is Nothing)
End Function

This control extends any control that inherits TextBoxBase. I also wanted to make sure that I could create the NHunspell object before I allowed it to extend. If the NHunspell object could not be created, then nothing else would work to begin with.

Before I go further, I want to describe the custom classes that I created. The first is the SpellCheckControl class. A new instance of this class is created for each control. It is used to store the text of the control, parse it, determine if there are spelling errors, where the spelling errors are, and suggestions for misspelled words. The class declaration, along with its Sub and Function declarations, is included below. The full code can be found in the source code download.

VB.NET
Public Class SpellCheckControl

#Region "Variables"
    Private FullText As String
    Private _Text(,) As String
    Public myNHunspell As Hunspell = Nothing
    Private _spellingErrors() As String
    Private _spellingErrorRanges() As CharacterRange
    Private _setTextCalledFirst As Boolean
    Private _ignoreRange() As CharacterRange
    Private _dontResetIgnoreRanges As Boolean
#End Region

#Region "New"
     Public Sub New(ByRef NHunspellObject As Hunspell)

#End Region

#Region "Adding or Removing Text"
    'Adds text given a starting position
    Public Sub AddText(ByVal Input As String, ByVal SelectionStart As Long)
    'Removes one character after a starting position
    Public Sub RemoveText(ByVal SelectionStart As Integer)
    'Resets the text using the input
    Public Sub SetText(ByVal Input As String)

#End Region

#Region "FindPositions"
    'Returns the index of the first letter in the word containing the current point
    Private Function FindFirstLetterOrDigitFromPosition_
    (ByVal SelectionStart As Long) As Long
    'Returns the index of the last letter in the word containing the current point
    Private Function FindLastLetterOrDigitFromPosition_
    (ByVal SelectionStart As Long) As Long
        
#End Region

#Region "Spelling Functions and Subs"
    'Adds a range of characters (a word) to ignore once
    Public Sub AddRangeToIgnore(ByVal IgnoreRange As CharacterRange)
    'Clears all of the ranges to be ignored once
    Public Sub ClearIgnoreRanges()
    'Tells this class not to reset the ignored ranges
    Public Sub DontResetIgnoreRanges(Optional ByVal DontReset As Boolean = True)
    'Returns the ranges to be ignored
    Public Function GetIgnoreRanges() As CharacterRange()
    'Returns the ranges of all of the misspelled words
    Public Function GetSpellingErrorRanges() As CharacterRange()
    'Returns the misspelled words
    Public Function GetSpellingErrors() As String()
    'Returns suggestions for a misspelled word
    Public Function GetSuggestions(ByVal Word As String, _
           ByVal NumberOfSuggestions As Integer) As String()
    'Given an index, returns the misspelled word containing that letter
    Public Function GetMisspelledWordAtPosition_
        (ByVal CharIndex As Integer) As String
    'Returns whether this control has spelling errors
    Public Function HasSpellingErrors() As Boolean
    'Returns whether the given char is part of a misspelled word
    Public Function IsPartOfSpellingError(ByVal CharIndex As Integer) As Boolean
    'Resets the ranges of misspelled words
    Public Sub SetSpellingErrorRanges()
      
#End Region

End Class

The second custom class that I created is the class that handles the custom painting. Much of the code from this class is taken from the SharpSpell blog mentioned above. Basically, this class waits until the control is given the WM_PAINT message, and then it goes into effect. This class will go through all of the misspelled ranges and determine if that word is visible. It will also make sure that it's not a word that is supposed to be ignored. If it is visible and not to be ignored, then it determines where that word is, and draws the red, wavy line underneath it.

VB.NET
Private Class CustomPaintTextBox _
        Inherits NativeWindow

    Private parentTextBox As TextBoxBase
    Private myBitmap As Bitmap
    Private textBoxGraphics As Graphics
    Private bufferGraphics As Graphics
    Private mySpellCheckControl As SpellCheckControl

    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)

    Public Sub New(ByRef CallingTextBox As TextBoxBase, _
                   ByRef ThisSpellCheckControl As SpellCheckControl)

    Private Sub CustomPaint()

    Public Sub ForcePaint()

    Private Sub DrawWave(ByVal StartOfLine As Point, ByVal EndOfLine As Point)

    Private Sub TextBoxBase_HandleCreated(ByVal sender As Object, _
                                          ByVal e As System.EventArgs)
End Class

A new instance of each of these classes is created for each control that has spell checking enabled. This is done through the provided property. The default value for this is to disable spell checking for each control. If the default value is True, then the SetEnabled property never fires. It is also through this Set property that the hashtables and event handlers are set up. The code is shown below. If this is the first time this Sub is called, it will add a new value to the hashtables. The first is whether or not the control is enabled. The Sub then creates a new SpellCheckControl and a new CustomPaintTextBox, and adds them to their hashtables. It is also through this Sub that the ContextMenuStrip is set up. If the control has a ContextMenuStrip already associated with it, then we just grab its ContextMenuStrip; otherwise, we create a new ContextMenuStrip. After all of the hashtables are set up, we then set up the event handlers. We care about when a user is typing within the textbox and when the mouse is moving. We care about the latter so that we can determine where the mouse was when the ContextMenuStrip was opened.

VB.NET
Public Sub SetSpellCheckEnabled(ByVal extendee As Control, ByVal Input As Boolean)
    If myNHunspell Is Nothing Then
        controlEnabled.Add(extendee, False)
        Return
    End If

    'Set the hashtables
    If controlEnabled(extendee) Is Nothing Then
        controlEnabled.Add(extendee, (Input And (Not myNHunspell Is Nothing)))

        mySpellCheckers.Add(extendee, New SpellCheckControl(myNHunspell))
        myCustomPaintingTextBoxes.Add(extendee, _
           New CustomPaintTextBox(CType(extendee, TextBoxBase), _
           CType(mySpellCheckers(extendee), SpellCheckControl)))

        If (CType(extendee, TextBoxBase).ContextMenuStrip) Is Nothing Then
            CType(extendee, TextBoxBase).ContextMenuStrip = New ContextMenuStrip
        End If

        AddHandler CType(extendee, TextBoxBase).ContextMenuStrip.Opening, _
                         AddressOf ContextMenu_Opening
        AddHandler CType(extendee, TextBoxBase).ContextMenuStrip.Closed, _
                         AddressOf ContextMenu_Closed

        myContextMenus.Add(extendee, _
                           CType(extendee, TextBoxBase).ContextMenuStrip)

        ReDim Preserve myControls(UBound(myControls) + 1)
        myControls(UBound(myControls)) = extendee
    Else
        controlEnabled(extendee) = (Input And (Not myNHunspell Is Nothing))
    End If

    'Get the handlers
    If Input = True And Not myNHunspell Is Nothing Then
        AddHandler CType(extendee, TextBoxBase).TextChanged, _
        AddressOf TextBox_TextChanged
        AddHandler CType(extendee, TextBoxBase).KeyDown, AddressOf TextBox_KeyDown
        AddHandler CType(extendee, TextBoxBase).KeyPress, AddressOf TextBox_KeyPress
        AddHandler CType(extendee, TextBoxBase).MouseMove, _
                    AddressOf TextBox_MouseMove
    Else
        RemoveHandler CType(extendee, TextBoxBase).TextChanged, _
        AddressOf TextBox_TextChanged
        RemoveHandler CType(extendee, TextBoxBase).KeyDown, AddressOf TextBox_KeyDown
        RemoveHandler CType(extendee, TextBoxBase).KeyPress, _
                    AddressOf TextBox_KeyPress
        RemoveHandler CType(extendee, TextBoxBase).MouseMove, _
                    AddressOf TextBox_MouseMove
    End If
End Sub

Language Support

To Top

Inherently, this Extender supports the English language. Included with the DLL are the English dic and aff files. However, providing inherent support for other languages would increase the DLL by around 2 MB per language. Instead of doing this, I wanted to allow for dynamic selection of language files. This will also allow for updating the original English files as well.

This is all done through a series of methods:

VB.NET
#Region "Change Language"
    Public Function GetAvailableLanguages() As String()

    Public Sub SetLanguage(ByVal NewLanguage As String)

    Public Function AddNewLanguage() As Boolean

    Public Sub RemoveLanguage(ByVal LanguageToRemove As String)

    Private Sub ResetLanguages()
 
    Public Sub UpdateLanguageFiles(ByVal LanguageToUpdate As String, _
                                   ByVal NewAffFileLocation As String, _
                                   ByVal NewDicFileLocation As String, _
                                   Optional ByVal OverwriteExistingFiles _
                    As Boolean = False, _
                                   Optional ByVal RemoveOlderFiles As Boolean = False)
#End Region

It is up to the designer as to how to present this functionality to the user, be it through menu items, context menus, etc... The only UI functionality that was implemented is within the AddNewLanguage method. This method will open up a selection form requiring the user to name the language and provide the location of the Aff and Dic files.

The example project uses menus. I set it to dynamically create the menus to allow for languages to be added. I did this through the DropDownOpening option. The code looks like this:

VB.NET
Private Sub LanguagesToolStripMenuItem_DropDownOpening_
    (ByVal sender As Object, ByVal e As System.EventArgs) _
            Handles LanguagesToolStripMenuItem.DropDownOpening
    LanguagesToolStripMenuItem.DropDownItems.Clear()

    If NHunspellTextBoxExtender1 IsNot Nothing Then
        Dim AddLanguage As New ToolStripMenuItem("Add New Language")
        AddHandler AddLanguage.Click, AddressOf AddLanguage_Click

        Dim RemoveLanguage As New ToolStripMenuItem("Remove Language")
        AddHandler RemoveLanguage.Click, AddressOf RemoveLanguage_Click

        Dim UpdateLanguage As New ToolStripMenuItem("Update Language")

        LanguagesToolStripMenuItem.DropDownItems.Add(AddLanguage)
        LanguagesToolStripMenuItem.DropDownItems.Add(UpdateLanguage)
        LanguagesToolStripMenuItem.DropDownItems.Add(RemoveLanguage)
        LanguagesToolStripMenuItem.DropDownItems.Add(New ToolStripSeparator)

        For Each lang As String In NHunspellTextBoxExtender1.GetAvailableLanguages
            Dim newMenuItem As New ToolStripMenuItem(lang)
            newMenuItem.Checked = True
            If lang = NHunspellTextBoxExtender1.Language Then
                newMenuItem.CheckState = CheckState.Checked
            Else
                newMenuItem.CheckState = CheckState.Unchecked
            End If
            AddHandler newMenuItem.Click, AddressOf ToolStripMenuItem_Click

            LanguagesToolStripMenuItem.DropDownItems.Add(newMenuItem)
        Next
    End If
End Sub

To add a new language is simple because the Extender handles the UI for this:

VB.NET
Private Sub AddLanguage_Click(ByVal sender As Object, ByVal e As EventArgs)
    NHunspellTextBoxExtender1.AddNewLanguage()
End Sub

To remove a language and to update a language, I made simple UIs. For removing a language, it simply consists of a ComboBox and a Button. Then, I show the forms, and if the form returned the necessary information, I made the call to the extender. For example, this is the code I used for updating the language:

VB.NET
Private Sub UpdateLanguage_Click(ByVal sender As Object, ByVal e As EventArgs)
    Dim newUpdateLangForm As New UpdateLanguageForm_
    (NHunspellTextBoxExtender1.GetAvailableLanguages())

    newUpdateLangForm.ShowDialog()

    If newUpdateLangForm.Result = Windows.Forms.DialogResult.Cancel Then Return

    Try
        With newUpdateLangForm
            NHunspellTextBoxExtender1.UpdateLanguageFiles(.LanguageSelection, _
                                                          .AffFileLocation, _
                                                          .DicFileLocation, _
                                                          .OverwriteExistingFiles, _
                                                          .RemoveOlderFiles)
        End With
    Catch ex As Exception
        MessageBox.Show(ex.Message)
    End Try
End Sub

Then, to set a new language, as you can see above, I loaded each language option as a new ToolStripMenuItem and made them checked if they were the default language. The code for this is also pretty simple:

VB.NET
Private Sub ToolStripMenuItem_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs)
    Try
        NHunspellTextBoxExtender1.SetLanguage(CType(sender, ToolStripMenuItem).Text)
    Catch ex As Exception
        MessageBox.Show(ex.Message)
    End Try

    For i = 0 To LanguagesToolStripMenuItem.DropDownItems.Count - 1
        If TypeOf LanguagesToolStripMenuItem.DropDownItems(i) Is ToolStripMenuItem Then
            If CType(LanguagesToolStripMenuItem.DropDownItems(i), _
        ToolStripMenuItem).Checked Then
                CType(LanguagesToolStripMenuItem.DropDownItems(i), _
        ToolStripMenuItem).CheckState = CheckState.Unchecked
            End If
        End If
    Next

    CType(sender, ToolStripMenuItem).CheckState = CheckState.Checked
End Sub

I also added a couple of properties to the extender for access during designing. The first is called MaintainUserChoice. The default value is True, but if set to False, then every time the application starts up, it will default to the designer's choice of languages. There is nothing special about this property, it is simply a Boolean value.

The fun came in with the Language property. I wanted to give the designer the ability to choose from the loaded languages. However, this list had to be created dynamically. This meant creating a UITypeEditor and a custom ListBox class.

One of the attributes that can be set for a property is the EditorAttribute. This is where I use my custom UITypeEditor class. This class is not very large, and only contains two methods: GetEditStyle and EditValue. This class is implemented like this:

VB.NET
Imports System.Drawing.Design
Imports System.Windows.Forms.Design

Public Class LanguageEditor
    Inherits System.Drawing.Design.UITypeEditor

    Public Overloads Overrides Function GetEditStyle(ByVal context _
    As System.ComponentModel.ITypeDescriptorContext) As UITypeEditorEditStyle
        Return UITypeEditorEditStyle.DropDown
    End Function

    Public Overloads Overrides Function EditValue(ByVal context _
    As System.ComponentModel.ITypeDescriptorContext, _
    ByVal provider As System.IServiceProvider, ByVal value As Object) As Object
        ' Get an IWindowsFormsEditorService.
        Dim editor_service As IWindowsFormsEditorService = _
            CType(provider.GetService(GetType(IWindowsFormsEditorService)),  _
                IWindowsFormsEditorService)

        ' If we failed to get the editor service, return the value.
        If editor_service Is Nothing Then Return value

        Dim strValue As String = TryCast(value, String)

        If strValue Is Nothing Then Return value

        Dim newListBox As New LanguageListBox(editor_service, strValue)

        editor_service.DropDownControl(newListBox)

        'Add the Item to the registry
        Dim regKey As Microsoft.Win32.RegistryKey
        regKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey_
        ("SOFTWARE\NHunspellTextBoxExtender\Languages", True)

        regKey.SetValue("Default", newListBox.SelectedItem)

        regKey.Close()
        regKey.Dispose()

        Return newListBox.SelectedItem
    End Function
End Class

When the designer goes to edit a value, it first calls the GetEditStyle method. This sets up the editor_service. We then just have to tell the editor_service what to use as the drop down. So, we create our custom class and pass it the editor_service along with the currently selected value. We have to pass the editor_service to the control so that when a new selection is made, the control can tell it to close. Once it has been closed, we then just update the Registry.

The custom ListBox class is basic. When it is created, we load all of the available languages from the Registry and select the currently selected item along with an option for adding a new language. Then, when a new selection is made, we first check to see if the designer wants to add a new language. If he/she does, we then open up our AddLanguage form which we talked about previously. If however, we get any other selection, we simply tell the editor_service to close this class.

Through all of this, I was able to implement both design-time language support and run-time language support to provide better functionality and adaptability.

Points of Interest

To Top

For the most part, this was straightforward coding. However, there were some interesting problems that I came across while working on this. The first was that the NHunspell DLL requires the x86 or x64 DLLs that come with NHunspell. However, I wanted a single DLL that I could provide to people. It still requires the NHunspell.dll file to be in the same directory as my Extender DLL, but I was able to embed the x86 and x64 files into the Extender DLL, and when I try to create a new Hunspell object, if it doesn't work, it tries to find out why. To do that, I try to create the object and check if the error was a DllNotFoundException. If it was, I get the name of the DLL not found, along with where it was supposed to be. I then add it to that location and try again. I haven't figured out how to include the NHunspell.dll file as well; if anyone has any suggestions, please let me know. It needs that file in place before I ever get to the New call. I'm guessing it's because of the global variable declarations that include a Hunspell object.

VB.NET
CreateNewHunspell:
    Try
        myNHunspell = New Hunspell(USaff, USdic)
    Catch ex As Exception
        If TypeOf ex Is System.DllNotFoundException Then
            'Get where the DLL is supposed to be
            Dim DLLpath As String = Trim(Strings.Mid(ex.Message, _
                                    InStr(ex.Message, "DLL not found:") + 14))
            Dim DLLName As String = Path.GetFileName(DLLpath)

            'Find out which DLL is missing
            If DLLName = "Hunspellx64.dll" Then
                'Copy the dll to the directory
                Try

                    File.WriteAllBytes(DLLpath, My.Resources.Hunspellx64)
                Catch ex2 As Exception
                    MessageBox.Show("Error writing Hunspellx64.dll" & _
                                    vbNewLine & ex2.Message)
                End Try

                'Try again
                GoTo CreateNewHunspell
            ElseIf DLLName = "Hunspellx86.dll" Then 'x86 dll
                'Copy the dll to the directory
                Try
                    File.WriteAllBytes(DLLpath, My.Resources.Hunspellx86)
                Catch ex3 As Exception
                    MessageBox.Show("Error writing Hunspellx86.dll" & _
                                    vbNewLine & ex3.Message)
                End Try

                'Try again
                GoTo CreateNewHunspell
            ElseIf DLLName = "NHunspell.dll" Then
                Try
                    File.WriteAllBytes(DLLpath, My.Resources.NHunspell)
                Catch ex4 As Exception
                    MessageBox.Show("Error writing NHunspell.dll" & _
                                    vbNewLine & ex4.Message)
                End Try
            Else
                MessageBox.Show(ex.Message & ex.StackTrace)
            End If
        Else
            MessageBox.Show("SpellChecker cannot be created." & _
                            vbNewLine & "Spell checking will be disabled." & _
                            vbNewLine & vbNewLine & ex.Message & ex.StackTrace)
            myNHunspell = Nothing
        End If
    End Try

I also had several bugs in the code. Finding these bugs took the component being used in a variety of situations that I have never used it in. Some of these bugs required interesting solutions. One of them focused on scrolling within the TextBoxBase. Using the ContextMenu, the user can add a word to the dictionary, ignore a word, or replace with one of five suggestions. However, in order to update the wavy, red lines, I had to reset the text of each TextBoxBase each time. This, however, would reset not only the scroll position, but also the caret. Fixing the caret was easy enough. Before resetting the text, I simply grabbed the SelectionStart and SelectionLength values and then reset them once the control had been updated.

However, this would change the scroll position each time, which does not look great to the user (especially if there are a lot of TextBoxes on the screen). So, I had to look into a way to reset the scroll position. A little research led me to an article here on CP called Controlling scrolling with the API[^]. I basically followed the code on that article to the letter. I grab the scroll position before I change the TextBox, and then after updating it, I reset it. It looks like:

VB.NET
'Get Scroll Position
Dim Position = GetScrollPos(currentTextBoxBase.Handle, SBS_VERT)

'Set Scroll Position
If (SetScrollPos(currentTextBoxBase.Handle, SBS_VERT, Position, True <> -1) Then
    PostMessageA(currentTextBoxBase.Handle, WM_VSCROLL, _
                 SB_THUMBPOSITION + &H10000 * Position, Nothing)
End If

This worked as expected in that it did preserve the scroll position. However, whenever I would reset the TextBox, the caret would move to the first char, which would move the scroll bar. Then I would reset the scroll bar, which would cause the control to move again. All of this was visible to the user. SuspendLayout doesn't actually stop re-painting, so I needed to find a way to tell the control to pause re-painting. I found a solution written by Herfried K. Wagner at Preventing controls from redrawing[^]. It implements a very simple solution using the SendMessage function within "user32.dll". It looks like this:

VB.NET
'Disable Drawing
SendMessage(currentTextBoxBase.Handle, WM_SETREDRAW, _
            New IntPtr(CInt(False)),IntPtr.Zero)

'Enable Drawing
SendMessage(currentTextBoxBase.Handle, WM_SETREDRAW, _
            New IntPtr(CInt(True)),IntPtr.Zero)

Optimizations

To Top

While not technically a bug, users were complaining that with larger RichTextBoxes, it would often take a great deal of time to display the wavy red lines. Part of this is a direct result of the fact that the RichTextBox can have different sized fonts on the same line. However, some of it was caused by inefficiencies within my code.

As a result, I went through the code and tried to determine what was taking so long to draw. One of the inefficiencies was found within the GetOffests method of the CustomPaintTextBox class. This method needed to find out what the tallest font on a given line was if it was a RichTextBox. To do this, I had to cycle the SelectionStart property of the RichTextBox. However, if I did this to the original, it would cause all kinds of problems. So, I was making a copy of the original and using that copy to determine the font heights.

This was all well and good, until you had many errors that were being shown. For each error, a new "temporary" RichTextBox was being created. As it turns out, it takes approximately 15 milliseconds to do that alone. So, if there were 22 errors, this alone took 330 milliseconds. To avoid this, I simply create a temporary RichTextBox at the beginning of the CustomPaint method and then pass it to GetOffsets each time. By doing this, I could decrease the time it took to draw by approximately (15ms * (# of errors - 1)).

There were also a couple other minor modifications that I made that reduced the number of calls that needed to be made to the RichTextBox, which helped speed up the drawing. All in all, I was able to reduce the time it took to draw by approximately half. However, it was still losing time determining the tallest font on a line, and I wasn't sure that there was a way around that.

Through working out a few other bugs, I realized that one of the approaches I was taking to determine the font height was terribly inefficient. This approach was used with RichTextBoxes when opening the ContextMenu and when drawing the wavy, red lines. Because of the ability to change font sizes, I needed to determine the largest font on the line in question. To start, I had to find the first and last char on the line. When I initially started writing this, my brilliant (said with sarcasm) idea was to start at 0 and go char by char to determine this. Unbeknownst to me at the time, RichTextBoxes have a GetFirstCharIndexFromLine method and a GetLineFromCharIndex method. This proved to be immensely more efficient. The new code looks like:

VB.NET
'Need to get FirstChar, LastChar, and the Line number
Dim firstCharInLine, lastCharInLine, curCharLine as Long
curCharLine = tempRTB.GetLineFromCharIndex(startingIndex)
firstCharInLine = tempRTB.GetFirstCharIndexFromLine(curCharLine)
lastCharInLine = tempRTB.GetFirstCharIndexFromLine(curCharLine + 1)

'If the current char index is on the last line, lastCharInLine will be -1
'So we can just change it to the last char in the box
If lastCharInLine = -1 then lastCharInLine = curTextBox.TextLength

This simple fix sped up the drawing of the control and the opening of the ContextMenu dramatically.

I also added a custom Event into the extender called CustomPaintComplete that returns the TextBoxBase that finished drawing and the total time (in milliseconds) that it took to draw. This allows me to show the difference between a RichTextBox and a standard TextBox. To show the differences and the problems relating to the RichTextBox, I copied some of the text from this article and pasted it into the example project. With 33 spelling errors and a size of 690 x 522, the RichTextBox took 891 milliseconds to draw, while the standard TextBox only took 47 milliseconds to draw. This shows the inefficiencies of the RichTextBox.

How To Use It

To Top

Download the NHunspellTextBoxExtenderDLL.zip file above and unzip it to any folder. Within Visual Studio, right-click on any Toolbox and select "Choose Items...". Use the Browse option and select the NHunspellTextBoxExtender.dll file. (Make sure that the NHunspell.dll file is in the same directory.) Once it has been added, you can add it to your form the same way as any other control.

Future Work

To Top

The next project that I plan to tackle related to this is to figure out a way to visually inform the user that there is a spelling error within a ListView, or a ListBox item. My initial thought is to try to modify a ToolTip to show spelling errors in much the same way as this project.

Update: I have completed the spell checking ToolTip which can be found here.

History

To Top

  • February 3, 2010: Article created
  • February 8, 2010: Article updated
  • February 12, 2010: Added new download for .NET 3.5 Framework
  • February 19, 2010: Fixed a bug with the SpellCheckForm and updated the downloadable files
  • March 22, 2010: Fixed a bug with enabling/disabling spellchecking and with the speed of display, and updated source code
  • March 23, 2010: Optimized the speed of drawing the oh-so-important wavy red line
  • March 25, 2010: Fixed another bug with enabling spellchecking
  • March 30, 2010: Fixed bugs with the SpellCheckForm (apparently, RichTextBoxes strip out carriage returns, which was throwing off the numbering if the base control was a standard TextBox; also, while it allowed the user to change more than the misspelled word, it was having problems updating the text when this happened)
  • May 6, 2010: Several bugs fixed and a bit of optimization done
  • May 20, 2010: Added language support
  • May 27, 2010: Because of recent updates, the tool would no longer work within Visual Studio 2008. I re-coded a few things to make them 2008 compliant and included them in the 3.5 DLL.
  • July 29, 2010: Added IDisposable interface, changed Registry entries from LocalMachine to CurrentUser, fixed a few bugs
  • October 29, 2010: Updated download files
  • November 1, 2010: Updated 2 download files

License

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