Background
As programmers, we are often asked to develop an interface to allow easier access to databases. In my case, I was dealing with a database structured using a snowflake schema. Certain tables held indices relating to other tables that contained the data. As an example, there were three levels of "Actions": "Objectives", "Recovery Actions", and "Action Steps". Any one of these could contain information on Priorities, Cost, Duration, Recovery Partners, and comments, along with the text of the action. To the human eye, the database was unreadable.
In developing the GUI for the user to access, it was unnecessary for the user to be able to modify all of the information from one screen. Instead, I wanted to provide a snapshot of the data to the user and use ToolTip
s to provide the more detailed information and ContextMenu
s for editing that information. The control that provided the information was a custom ListView
(as seen below).
This worked well, and the users found it easy to navigate through the database using the GUI. However, I soon realized that our users were not very careful while typing and produced a large number of spelling errors. So, I wanted to provide a way for the user to have a visual cue that they had made a spelling error.
To that end, I developed an IExtenderProvider
that would extend TextBox
es to provide Microsoft Word style spell-as-you-type spell checking capabilities. I used Hunspell for .NET and created the NHunspellTextBoxExtender
. I was able to use the TextBox
extender when they were editing the text, but that still didn't provide any spell checking capability on the main form where the information was provided by ListView
s.
So, the question was, how could I provide that visual cue that something was misspelled within the information? I was already using ToolTip
s to provide the information, so why not create a ToolTip
that could spell-check its text and provide the visual cues that way? Then, the user could see that there was a spelling error, and go in and fix it. This custom ToolTip
provides the functionality seen below:
The Spell Checking ToolTip
Unlike my previous control (the NHunspellTextBoxExtender
), for this control I didn't need to use an IExtenderProvider
since I was only changing one ToolTip
. Instead, I chose to create a new Control that inherited ToolTip
. The spell checking was very similar to the TextBox
version. But because I was limiting it to display only, and because the control's text would only be set, and not appended or trimmed, I could pare down the SpellCheckControl
.
SpellCheckControl
The SpellCheckControl
handles all of the spell checking. When the ToolTip
receives the Draw
command, it calls the SetText
method of the SpellCheckControl
. The SetText
method goes through all of the text and finds any misspelled words and identifies them using CharacterRange
s.
The class structure is shown below:
Imports System.Drawing
Imports System.IO
Imports System.Windows.Forms
Imports System.Reflection
Public Class SpellCheckControl
Private FullText As String
Private _Text(,) As String
Public myNHunspell As Object = Nothing
Private _spellingErrors() As String
Private _spellingErrorRanges() As CharacterRange
Public Sub New(ByRef HunspellObject As Object)
Public Sub SetText(ByVal Input As String)
Private Function FindFirstLetterOrDigitFromPosition(_
ByVal SelectionStart As Long) As Long
Private Function FindLastLetterOrDigitFromPosition(_
ByVal SelectionStart As Long) As Long
Public Function GetSpellingErrorRanges() As CharacterRange()
Public Function HasSpellingErrors() As Boolean
Public Sub SetSpellingErrorRanges()
End Class
ToolTip Draw
Once the spelling errors have been identified (which is remarkably quick, thanks to NHunspell), the ToolTip
then needs to be drawn. To do this, I simply handle the ToolTip_Draw
method. At this point, I've already drawn the background using the e.DrawBackground()
command. The next step is to draw the wavy red line. To do this, we first have to identify where the spelling errors are and determine where the red line should be drawn. This proved to be a bit tricky because we had to keep track of which line the current text was on.
But, once we had determined where to draw the line, we then had to actually draw it. I did this using some code that can be found at this blog. The full code of the Draw
event is shown below:
Private Sub NHunspellToolTip_Draw(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DrawToolTipEventArgs) _
Handles Me.Draw
e.DrawBackground()
mySpellCheckControl.SetText(e.ToolTipText)
myBitmap = New Bitmap(e.Bounds.Width, e.Bounds.Height)
bufferGraphics = Graphics.FromImage(myBitmap)
bufferGraphics.Clip = New Region(e.Bounds)
Dim currentRange As CharacterRange
For Each currentRange In mySpellCheckControl.GetSpellingErrorRanges
Dim startPoint, endPoint As Point
Dim bottom, left, right As Integer
Dim lastNewline As Integer = 1
For i = 1 To currentRange.First
If Mid(e.ToolTipText, i, 1) = vbLf Then
lastNewline = i + 1
End If
Next
Dim lineToEndofWord As String = Mid(e.ToolTipText, lastNewline, _
((currentRange.First - lastNewline) + _
currentRange.Length + 1))
Dim newSize As SizeF = _
TextRenderer.MeasureText(Microsoft.VisualBasic.Strings.Left(e.ToolTipText, _
(currentRange.First + currentRange.Length)), _
e.Font, e.Bounds.Size, TextFormatFlags.Left)
bottom = newSize.Height - 2
newSize = TextRenderer.MeasureText(lineToEndofWord, e.Font, _
e.Bounds.Size, TextFormatFlags.Left)
right = newSize.Width
endPoint = New Point(right, bottom)
newSize = TextRenderer.MeasureText(Mid(e.ToolTipText, _
currentRange.First + 1, currentRange.Length), _
e.Font, e.Bounds.Size, TextFormatFlags.Left)
left = right - newSize.Width
startPoint = New Point(left, bottom)
startPoint.X += 2
endPoint.X -= 4
DrawWave(startPoint, endPoint)
Next
toolTipGraphics = e.Graphics
toolTipGraphics.DrawImageUnscaled(myBitmap, 0, 0)
e.DrawBorder()
Dim stringFlags As New StringFormat()
stringFlags.Alignment = StringAlignment.Near
stringFlags.LineAlignment = StringAlignment.Near
TextRenderer.DrawText(e.Graphics, e.ToolTipText, e.Font, _
e.Bounds, Color.Black, flags:=TextFormatFlags.Left)
End Sub
Using the Code
That's really it! This tool is very simple to use... simply download the DLL using one of the links above, and add it to one of your Toolboxes. The spell checking is done automatically, and this ToolTip
can be used in the same way as the standard ToolTip
.
Points of Interest
I was able to implement something different with this ToolTip
. I am not sure why I could not get it to work with the NHunspellTextBoxExtender
, but I am able to load this ToolTip
without the NHunspell.dll file. This means that when I package up this tool, I do not need to include the NHunspell.dll as well. To accomplish this, I embed the NHunspell.dll file in the project. Then, when I create the Hunspell
object, I create it using the raw assembly.
This takes a bit of work, and you have to know the paramaters before hand. To start, I have to load the assembly. This is done with a single line of code:
Dim a As Assembly = Assembly.Load(My.Resources.NHunspell)
I then have to get the Type
of the object that I am trying to create, and create a ConstructorInfo
class. The constructor for the Hunspell
object takes two strings. The code to do this takes a few more lines of code, but it's still relatively simple:
Dim type_l As Type = a.GetType("NHunspell.Hunspell")
Dim types(1) As Type
types(0) = GetType(String)
types(1) = GetType(String)
Dim ctor As ConstructorInfo = type_l.GetConstructor(types)
The last thing that has to be done is to Invoke
the constructor which will return an Object
. To do this, I have to set up my parameters that will be passed in. The Invoke
method takes an array of Objects
s. The first parameter for the Hunspell
is the .aff file and the second is the .dic file. To do this without any error checking looks like:
Dim params(1) As Object
params(0) = USaff
params(1) = USdic
Dim result As Object = Nothing
result = ctor.Invoke(params)
When I do this for real, I place result = ctor.Invoke(params)
into a Try
/Catch
block as I have to make sure that a couple of unmanaged DLLs are available. I have provided more information on that under my NHunspellTextBoxExtender article.
The only problem with this method is that the IntelliSense of Visual Studio will not work this way. That is why this step is not implemented until the very end. Before that, I include a reference to the NHunspell DLL which gives me my IntelliSense. Then, before finalizing it, I simply comment out that Import
statement and change the Hunspell
objects to Object
.
History
- 17 February 2010: Article created.