65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (36投票s)

2010年2月3日

CPOL

16分钟阅读

viewsIcon

376123

downloadIcon

9829

扩展继承 TextBoxBase 的控件以提供离线拼写检查功能。

目录

引言

许多应用程序中,拼写检查是不可或缺的一部分。大多数用户都熟悉 Microsoft Word 或 OpenOffice 等产品的拼写检查功能。市面上也有一些付费产品可以提供拼写检查功能,例如 SharpSpell,其价格可能高达数百美元。不幸的是,目前缺乏开源、免费的工具来提供 Microsoft Word 的功能。因此,我开始着手开发一个拼写检查 IExtenderProvider,它可以扩展继承自 TextBoxBase 的任何控件(TextBoxRichTextBox 都继承自 TextBoxBase)。

背景

作为我日常工作的一部分,我被要求开发一个数据库来存储鲑鱼恢复行动,并将其纳入鲑鱼恢复计划。为了提供我能达到的最高水平的功能,我还开发了一个独立的应用程序,提供了与数据库交互的所有 GUI。然而,我很快就发现,缺乏拼写检查功能导致数据库中存在大量的拼写错误。消除这些拼写错误唯一的方法就是进入 Access 并使用 Access 的拼写检查功能。这意味着我作为唯一被允许直接使用数据库的人,承担了这项责任。

为了避免这种情况,我想为我的 GUI 应用程序提供拼写检查功能。有很多方法可以进行文本拼写检查,从使用 NetSpell,到以类似 本文 的方式以编程方式使用 Microsoft Word 的拼写检查器。并非所有用户都能保证拥有 Microsoft Word,而启动一个新的 Word 应用程序可能需要一些时间并消耗资源,因此我想避免这种情况。我选择使用 NHunspell。一篇关于它的有用文章可以在 这里 找到。

我还想为用户提供视觉提示,表明存在拼写错误。对于 RichTextBox,这可以通过简单的下划线来实现,就像在 本文 中那样。我的问题是我写了大量使用简单文本框的代码,而我不想将它们全部更改为 RichTextBox。相反,我想使用 IExtenderProvider 来扩展任何具有拼写检查功能的文本框。老实说,我不知道从何开始。直到我找到了 这个 SharpSpell 博客,它确切地描述了如何在任何文本框上绘制波浪红线。有了这段基础代码,我就能够使用 NHunspell 来确定在哪里绘制这条线。

探索代码

IExtenderProviders 非常有用,我们中的许多编码人员都会定期使用它们,甚至可能不知道它们的存在。最简单的例子是 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 类。每个控件都会创建一个该类的新实例。它用于存储控件的文本,解析它,确定是否存在拼写错误,拼写错误的位置以及拼写错误的单词的建议。类声明及其 SubFunction 声明包含在下面。完整的代码可以在源代码下载中找到。

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 中包含英语的dicaff 文件。但是,为其他语言提供内置支持会将 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 方法中。此方法将打开一个选择表单,要求用户为语言命名并提供AffDic 文件的位置。

示例项目使用菜单。我将其设置为动态创建菜单,以便添加语言。我通过 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 类的地方。这个类不是很大,只包含两个方法:GetEditStyleEditValue。该类的实现方式如下:

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 的文本。然而,这不仅会重置滚动位置,还会重置插入符。修复插入符很容易。在重置文本之前,我只需获取 SelectionStartSelectionLength 值,然后在控件更新后重新设置它们。

然而,这每次都会改变滚动条的位置,这对用户来说看起来不太好(尤其是在屏幕上有大量 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)。为此,我必须循环 RichTextBoxSelectionStart 属性。然而,如果我对原始文本框执行此操作,将会导致各种问题。因此,我创建了原始副本并使用该副本确定字体高度。

这一切都很好,直到显示了许多错误。对于每个错误,都会创建一个新的“临时”RichTextBox。事实证明,仅此一项就需要大约 15 毫秒。因此,如果有 22 个错误,这本身就需要 330 毫秒。为了避免这种情况,我只需在 CustomPaint 方法的开头创建一个临时 RichTextBox,然后在每次调用 GetOffsets 时将其传递过去。通过这样做,我可以将绘制时间减少大约 (15ms * (# of errors - 1))。

我还做了一些其他小的修改,减少了需要调用 RichTextBox 的次数,这有助于加快绘制速度。总而言之,我能够将绘制时间减少大约一半。但是,在确定一行中最高字体时仍然会花费时间,而且我不确定是否有办法绕过它。

通过解决一些其他 bug,我意识到我用来确定字体高度的一种方法非常低效。这种方法与 RichTextBox 在打开 ContextMenu 和绘制波浪形红色线条时使用。由于可以更改字体大小,因此我需要确定相关行上的最大字体。首先,我必须找到该行的第一个和最后一个字符。在我最初开始编写时,我的绝妙(讽刺地说)想法是开始于 0,逐个字符地确定这一点。当时我不知道,RichTextBoxGetFirstCharIndexFromLine 方法和 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 位于同一目录中。)添加后,您可以像添加任何其他控件一样将其添加到您的窗体中。

未来工作

回到顶部

我计划接下来要处理的与此相关的项目是找出一种视觉方式来告知用户 ListViewListBox 项中存在拼写错误。我的初步想法是尝试修改 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 个下载文件
© . All rights reserved.