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 RichTextBox
es, 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 RichTextBox
es. 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
IExtenderProvider
s 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.
<ToolboxBitmap(GetType(NHunspellTextBoxExtender), "spellcheck.png"), _
ProvideProperty("SpellCheckEnabled", GetType(Control))> _
Public Class NHunspellTextBoxExtender
Inherits Component
Implements IExtenderProvider
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:
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.
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"
Public Sub AddText(ByVal Input As String, ByVal SelectionStart As Long)
Public Sub RemoveText(ByVal SelectionStart As Integer)
Public Sub SetText(ByVal Input As String)
#End Region
#Region "FindPositions"
Private Function FindFirstLetterOrDigitFromPosition_
(ByVal SelectionStart As Long) As Long
Private Function FindLastLetterOrDigitFromPosition_
(ByVal SelectionStart As Long) As Long
#End Region
#Region "Spelling Functions and Subs"
Public Sub AddRangeToIgnore(ByVal IgnoreRange As CharacterRange)
Public Sub ClearIgnoreRanges()
Public Sub DontResetIgnoreRanges(Optional ByVal DontReset As Boolean = True)
Public Function GetIgnoreRanges() As CharacterRange()
Public Function GetSpellingErrorRanges() As CharacterRange()
Public Function GetSpellingErrors() As String()
Public Function GetSuggestions(ByVal Word As String, _
ByVal NumberOfSuggestions As Integer) As String()
Public Function GetMisspelledWordAtPosition_
(ByVal CharIndex As Integer) As String
Public Function HasSpellingErrors() As Boolean
Public Function IsPartOfSpellingError(ByVal CharIndex As Integer) As Boolean
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.
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.
Public Sub SetSpellCheckEnabled(ByVal extendee As Control, ByVal Input As Boolean)
If myNHunspell Is Nothing Then
controlEnabled.Add(extendee, False)
Return
End If
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
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:
#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:
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:
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:
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:
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:
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
Dim editor_service As IWindowsFormsEditorService = _
CType(provider.GetService(GetType(IWindowsFormsEditorService)), _
IWindowsFormsEditorService)
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)
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.
CreateNewHunspell:
Try
myNHunspell = New Hunspell(USaff, USdic)
Catch ex As Exception
If TypeOf ex Is System.DllNotFoundException Then
Dim DLLpath As String = Trim(Strings.Mid(ex.Message, _
InStr(ex.Message, "DLL not found:") + 14))
Dim DLLName As String = Path.GetFileName(DLLpath)
If DLLName = "Hunspellx64.dll" Then
Try
File.WriteAllBytes(DLLpath, My.Resources.Hunspellx64)
Catch ex2 As Exception
MessageBox.Show("Error writing Hunspellx64.dll" & _
vbNewLine & ex2.Message)
End Try
GoTo CreateNewHunspell
ElseIf DLLName = "Hunspellx86.dll" Then
Try
File.WriteAllBytes(DLLpath, My.Resources.Hunspellx86)
Catch ex3 As Exception
MessageBox.Show("Error writing Hunspellx86.dll" & _
vbNewLine & ex3.Message)
End Try
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 TextBox
es 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:
Dim Position = GetScrollPos(currentTextBoxBase.Handle, SBS_VERT)
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:
SendMessage(currentTextBoxBase.Handle, WM_SETREDRAW, _
New IntPtr(CInt(False)),IntPtr.Zero)
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 RichTextBox
es, 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 RichTextBox
es 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, RichTextBox
es have a GetFirstCharIndexFromLine
method and a GetLineFromCharIndex
method. This proved to be immensely more efficient. The new code looks like:
Dim firstCharInLine, lastCharInLine, curCharLine as Long
curCharLine = tempRTB.GetLineFromCharIndex(startingIndex)
firstCharInLine = tempRTB.GetFirstCharIndexFromLine(curCharLine)
lastCharInLine = tempRTB.GetFirstCharIndexFromLine(curCharLine + 1)
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, RichTextBox
es 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