NHunspellTextBoxExtender - 用于 .NET 的 Hunspell 文本框拼写检查 IExtenderProvider






4.84/5 (36投票s)
扩展继承 TextBoxBase 的控件以提供离线拼写检查功能。
- 下载源代码 (更新: 2010年10月29日) - 644.67 KB
- 下载示例可执行文件 (更新: 2010年10月29日) - 1.39 MB
- 下载 DLL (更新: 2010年11月1日) - 1.2 MB
- 下载 .NET 3.5 的 DLL (符合 Visual Studio 2008 标准) (更新: 2010年11月1日) - 1.2 MB
目录
引言
许多应用程序中,拼写检查是不可或缺的一部分。大多数用户都熟悉 Microsoft Word 或 OpenOffice 等产品的拼写检查功能。市面上也有一些付费产品可以提供拼写检查功能,例如 SharpSpell,其价格可能高达数百美元。不幸的是,目前缺乏开源、免费的工具来提供 Microsoft Word 的功能。因此,我开始着手开发一个拼写检查 IExtenderProvider
,它可以扩展继承自 TextBoxBase
的任何控件(TextBox
和 RichTextBox
都继承自 TextBoxBase
)。
背景
作为我日常工作的一部分,我被要求开发一个数据库来存储鲑鱼恢复行动,并将其纳入鲑鱼恢复计划。为了提供我能达到的最高水平的功能,我还开发了一个独立的应用程序,提供了与数据库交互的所有 GUI。然而,我很快就发现,缺乏拼写检查功能导致数据库中存在大量的拼写错误。消除这些拼写错误唯一的方法就是进入 Access 并使用 Access 的拼写检查功能。这意味着我作为唯一被允许直接使用数据库的人,承担了这项责任。
为了避免这种情况,我想为我的 GUI 应用程序提供拼写检查功能。有很多方法可以进行文本拼写检查,从使用 NetSpell,到以类似 本文 的方式以编程方式使用 Microsoft Word 的拼写检查器。并非所有用户都能保证拥有 Microsoft Word,而启动一个新的 Word 应用程序可能需要一些时间并消耗资源,因此我想避免这种情况。我选择使用 NHunspell。一篇关于它的有用文章可以在 这里 找到。
我还想为用户提供视觉提示,表明存在拼写错误。对于 RichTextBox
,这可以通过简单的下划线来实现,就像在 本文 中那样。我的问题是我写了大量使用简单文本框的代码,而我不想将它们全部更改为 RichTextBox
。相反,我想使用 IExtenderProvider
来扩展任何具有拼写检查功能的文本框。老实说,我不知道从何开始。直到我找到了 这个 SharpSpell 博客,它确切地描述了如何在任何文本框上绘制波浪红线。有了这段基础代码,我就能够使用 NHunspell 来确定在哪里绘制这条线。
探索代码
IExtenderProvider
s 非常有用,我们中的许多编码人员都会定期使用它们,甚至可能不知道它们的存在。最简单的例子是 ToolTip
。当您将 ToolTip
添加到窗体时,它不会直接显示为窗体上的控件。并且,虽然它有自己的属性,但它也为其他控件添加了属性。IExtenderProvider
是我的控件的基础。有一篇非常好的文章描述了 IExtenderProvider
,地址是 这里。
下面展示了类声明。该类继承自 Component
并实现 IExtenderProvider
。当实现 IExtenderProvider
时,通常会为其他控件提供属性。这是通过类声明之前的 ProvideProperty
声明完成的。
<ToolboxBitmap(GetType(NHunspellTextBoxExtender), "spellcheck.png"), _
ProvideProperty("SpellCheckEnabled", GetType(Control))> _
Public Class NHunspellTextBoxExtender
Inherits Component
Implements IExtenderProvider
'
'Rest of the code here
'
End Class
然而,类的主体仍然不完整。每当实现 IExtenderProvider
时,都必须实现 IExtenderProvider.CanExtend
函数。在我的例子中,它实现如下:
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
此控件扩展了继承自 TextBoxBase
的任何控件。我还希望确保在允许扩展之前创建 NHunspell
对象。如果无法创建 NHunspell
对象,那么其他任何事情一开始都不会起作用。
在继续之前,我想描述一下我创建的自定义类。第一个是 SpellCheckControl
类。每个控件都会创建一个该类的新实例。它用于存储控件的文本,解析它,确定是否存在拼写错误,拼写错误的位置以及拼写错误的单词的建议。类声明及其 Sub
和 Function
声明包含在下面。完整的代码可以在源代码下载中找到。
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
我创建的第二个自定义类是处理自定义绘制的类。该类的许多代码都取自上面提到的 SharpSpell 博客。基本上,该类会等到控件接收到 WM_PAINT
消息,然后它就会生效。该类将遍历所有拼写错误的范围,并确定该单词是否可见。它还会确保该单词不是应该被忽略的单词。如果它是可见的并且不应被忽略,那么它会确定该单词的位置,并在其下方绘制红色波浪线。
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
为每个启用了拼写检查的控件创建这些类的每个实例。这是通过提供的属性完成的。每个控件的默认值为禁用拼写检查。如果默认值为 True
,则 SetEnabled
属性永远不会触发。也是通过此 Set 属性来设置哈希表和事件处理程序。代码如下所示。如果这是该 Sub
第一次被调用,它将向哈希表中添加一个新值。第一个是控件是否已启用。然后,该 Sub
创建一个新的 SpellCheckControl
和一个新的 CustomPaintTextBox
,并将它们添加到它们的哈希表中。也是通过这个 Sub
设置 ContextMenuStrip
。如果控件已经关联了 ContextMenuStrip
,我们就获取它的 ContextMenuStrip
;否则,我们创建一个新的 ContextMenuStrip
。设置完所有哈希表后,我们就设置事件处理程序。我们关心用户在输入文本框时以及鼠标移动时。我们关心后者,以便在打开 ContextMenuStrip
时确定鼠标的位置。
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
语言支持
默认情况下,此 Extender 支持英语。DLL 中包含英语的dic 和aff 文件。但是,为其他语言提供内置支持会将 DLL 大小增加约 2MB/语言。我没有这样做,而是允许动态选择语言文件。这也可以允许更新原始的英语文件。
这一切都是通过一系列方法完成的。
#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
由设计者决定如何向用户呈现此功能,无论是通过菜单项、上下文菜单等。唯一实现的 UI 功能是在 AddNewLanguage
方法中。此方法将打开一个选择表单,要求用户为语言命名并提供Aff 和Dic 文件的位置。
示例项目使用菜单。我将其设置为动态创建菜单,以便添加语言。我通过 DropDownOpening
选项来实现。代码如下所示:
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
添加新语言很简单,因为 Extender 负责处理其 UI。
Private Sub AddLanguage_Click(ByVal sender As Object, ByVal e As EventArgs)
NHunspellTextBoxExtender1.AddNewLanguage()
End Sub
要移除或更新语言,我制作了简单的 UI。对于移除语言,它仅包含一个 ComboBox
和一个 Button
。然后,我显示表单,如果表单返回了必要的信息,我就会调用 extender。例如,这是我用于更新语言的代码:
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
然后,如上所示,要设置新语言,我将每个语言选项加载为一个新的 ToolStripMenuItem
,如果它是默认语言,则将其设置为选中状态。这部分代码也很简单:
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
我还向 extender 添加了几个属性,以便在设计时访问。第一个称为 MaintainUserChoice
。默认值为 True
,但如果设置为 False
,则每次应用程序启动时,它将默认为设计者选择的语言。此属性没有什么特别之处,它只是一个 Boolean
值。
有趣的是 Language
属性。我想让设计者能够从加载的语言中进行选择。但是,这个列表必须动态创建。这意味着需要创建 UITypeEditor
和自定义 ListBox
类。
可以为属性设置的属性之一是 EditorAttribute
。这是我使用自定义 UITypeEditor
类的地方。这个类不是很大,只包含两个方法:GetEditStyle
和 EditValue
。该类的实现方式如下:
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
当设计者尝试编辑值时,它首先调用 GetEditStyle
方法。这会设置 editor_service
。然后我们只需告诉 editor_service
使用什么作为下拉列表。因此,我们创建自定义类,并将 editor_service
和当前选定的值传递给它。我们必须将 editor_service
传递给控件,以便当做出新选择时,控件可以告诉它关闭。一旦关闭,我们就更新注册表。
自定义 ListBox
类是基础的。创建时,我们从注册表中加载所有可用语言,并选择当前选定的项目以及添加新语言的选项。然后,当做出新选择时,我们首先检查设计者是否要添加新语言。如果他/她这样做,我们就会打开我们之前讨论过的 AddLanguage
表单。但是,如果我们得到任何其他选择,我们只需告诉 editor_service
关闭此类。
通过所有这些,我能够实现设计时语言支持和运行时语言支持,从而提供更好的功能和适应性。
关注点
在大多数情况下,这是直接编码。但是,我在工作中遇到了一些有趣的问题。第一个是 NHunspell DLL 需要 NHunspell 附带的 x86 或 x64 DLL。但是,我想要一个单一的 DLL 来提供给用户。它仍然需要 NHunspell.dll 文件与我的 Extender DLL 位于同一目录中,但我能够将 x86 和 x64 文件嵌入到 Extender DLL 中,当我尝试创建新的 Hunspell
对象时,如果不起作用,它会尝试找出原因。为此,我尝试创建对象并检查错误是否是 DllNotFoundException
。如果是,我就会获取找不到的 DLL 的名称以及它应该在的位置。然后我将其添加到该位置并重试。我还没有弄清楚如何包含 NHunspell.dll 文件;如果有人有什么建议,请告诉我。在我调用 New
之前,它需要该文件到位。我猜是因为全局变量声明包含一个 Hunspell
对象。
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
我的代码中也有几个 bug。发现这些 bug 需要将组件用于各种我从未用过的情况。其中一些 bug 需要有趣的解决方案。其中一个重点是 TextBoxBase
中的滚动。使用 ContextMenu
,用户可以将单词添加到词典,忽略单词,或替换为五个建议之一。但是,为了更新波浪形的红色线条,我必须每次重置每个 TextBoxBase
的文本。然而,这不仅会重置滚动位置,还会重置插入符。修复插入符很容易。在重置文本之前,我只需获取 SelectionStart
和 SelectionLength
值,然后在控件更新后重新设置它们。
然而,这每次都会改变滚动条的位置,这对用户来说看起来不太好(尤其是在屏幕上有大量 TextBox
时)。因此,我不得不研究一种重置滚动条位置的方法。稍微研究一下,我找到了 CP 上的一篇文章,名为 使用 API 控制滚动[^]。我基本上是逐字遵循了那篇文章的代码。在更改 TextBox
之前,我获取滚动条位置,然后在更新它之后,我重置它。看起来是这样:
'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
这按预期工作,即它保留了滚动条位置。然而,每当我重置 TextBox
时,插入符就会移动到第一个字符,这会移动滚动条。然后我重置滚动条,这会导致控件再次移动。所有这些都对用户可见。SuspendLayout
实际上并没有停止重绘,所以我需要找到一种方法来告诉控件暂停重绘。我找到了 Herfried K. Wagner 在 防止控件重绘[^] 中写的一个解决方案。它使用 "user32.dll" 中的 SendMessage
函数实现了一个非常简单的解决方案。看起来是这样:
'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)
优化
虽然严格来说不是 bug,但用户抱怨说,对于较大的 RichTextBox
,显示波浪形的红色线条通常需要很长时间。其中一部分是 RichTextBox
可以在同一行上具有不同大小的字体,这是直接原因。但是,其中一部分是我的代码效率低下造成的。
因此,我检查了代码,试图确定是什么导致绘制时间过长。效率低下之一存在于 CustomPaintTextBox
类中的 GetOffests
方法。此方法需要找出给定行上最高的字体(如果是 RichTextBox
)。为此,我必须循环 RichTextBox
的 SelectionStart
属性。然而,如果我对原始文本框执行此操作,将会导致各种问题。因此,我创建了原始副本并使用该副本确定字体高度。
这一切都很好,直到显示了许多错误。对于每个错误,都会创建一个新的“临时”RichTextBox
。事实证明,仅此一项就需要大约 15 毫秒。因此,如果有 22 个错误,这本身就需要 330 毫秒。为了避免这种情况,我只需在 CustomPaint
方法的开头创建一个临时 RichTextBox
,然后在每次调用 GetOffsets
时将其传递过去。通过这样做,我可以将绘制时间减少大约 (15ms * (# of errors - 1))。
我还做了一些其他小的修改,减少了需要调用 RichTextBox
的次数,这有助于加快绘制速度。总而言之,我能够将绘制时间减少大约一半。但是,在确定一行中最高字体时仍然会花费时间,而且我不确定是否有办法绕过它。
通过解决一些其他 bug,我意识到我用来确定字体高度的一种方法非常低效。这种方法与 RichTextBox
在打开 ContextMenu
和绘制波浪形红色线条时使用。由于可以更改字体大小,因此我需要确定相关行上的最大字体。首先,我必须找到该行的第一个和最后一个字符。在我最初开始编写时,我的绝妙(讽刺地说)想法是开始于 0,逐个字符地确定这一点。当时我不知道,RichTextBox
有 GetFirstCharIndexFromLine
方法和 GetLineFromCharIndex
方法。这被证明效率要高得多。新代码如下所示:
'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
这个简单的修复极大地加快了控件的绘制速度和 ContextMenu
的打开速度。
我还向 extender 添加了一个自定义事件,称为 CustomPaintComplete
,它返回完成绘制的 TextBoxBase
以及绘制的总时间(毫秒)。这使我能够显示 RichTextBox
和标准 TextBox
之间的区别。为了展示 RichTextBox
的差异和问题,我复制了本文的一些文本并将其粘贴到示例项目中。有 33 个拼写错误,尺寸为 690 x 522,RichTextBox
花费了 891 毫秒进行绘制,而标准 TextBox
只花费了 47 毫秒进行绘制。这显示了 RichTextBox
的效率低下。
如何使用
下载上面的 NHunspellTextBoxExtenderDLL.zip 文件并将其解压到任何文件夹。在 Visual Studio 中,右键单击任何工具箱,然后选择“选择项目...”。使用浏览选项并选择 NHunspellTextBoxExtender.dll 文件。(确保 NHunspell.dll 文件与您的 Extender DLL 位于同一目录中。)添加后,您可以像添加任何其他控件一样将其添加到您的窗体中。
未来工作
我计划接下来要处理的与此相关的项目是找出一种视觉方式来告知用户 ListView
或 ListBox
项中存在拼写错误。我的初步想法是尝试修改 ToolTip
,使其显示拼写错误,方式与此项目类似。
更新:我已完成拼写检查 ToolTip,可在 此处 找到。
历史
- 2010年2月3日:文章创建
- 2010年2月8日:文章更新
- 2010年2月12日:添加了 .NET 3.5 Framework 的新下载
- 2010年2月19日:修复了
SpellCheckForm
的 bug 并更新了可下载文件 - 2010年3月22日:修复了启用/禁用拼写检查和显示速度的 bug,并更新了源代码
- 2010年3月23日:优化了绘制“极其重要”的波浪红线的速度
- 2010年3月25日:修复了启用拼写检查的另一个 bug
- 2010年3月30日:修复了
SpellCheckForm
的 bug(显然,RichTextBox
会剥离回车符,这会导致在基控件是标准TextBox
时出现编号错误;此外,虽然它允许用户更改超过拼写错误的单词,但在发生这种情况时更新文本时也遇到了问题) - 2010年5月6日:修复了多个 bug 并进行了一些优化
- 2010年5月20日:添加了语言支持
- 2010年5月27日:由于最近的更新,该工具将不再能在 Visual Studio 2008 中工作。我重新编码了一些内容以使其符合 2008 标准,并将其包含在 3.5 DLL 中。
- 2010年7月29日:添加了
IDisposable
接口,将注册表项从LocalMachine
更改为CurrentUser
,修复了一些 bug - 2010年10月29日:更新了下载文件
- 2010年11月1日:更新了 2 个下载文件