带字典编辑器程序的免费拼写检查器






4.25/5 (10投票s)
2001年1月7日

172478

12218
关于为您的应用程序添加拼写检查器的文章。

引言
首先,我想明确一下。是的,这是一个拼写检查器;不,您不需要任何 DLL 或 ActiveX 控件;是的,它是免费的。
但它速度很慢,还需要更多工作(正如您从截图看到的——它提供了数百个建议!)。我在这里发布这个未完成版本的主要原因是为了让其他人能够改进(甚至推翻重写)它的主要“引擎”代码,并为其添加其他功能。
谢谢
部分代码来自以下人员:
- Zafir Anjum - CListCtrl排序代码
- Chris Maunder - 提供本网站和 CProgressControl类
背景
几个月前,我需要为我的一个应用程序添加拼写检查器。我研究了各种 DLL 和 ActiveX 控件,但它们似乎不能按我想要的方式工作。我还发现了一些适用于 Unix 的开源拼写检查器,但将它们轻松地转换为 Windows 并不容易。
最后,我决定尝试自己制作,并在进行了一些实验后,我意识到我首先需要一个字典来测试结果的准确性。在过去的四个月里,我一直在编译字典,然后测试了我最初的代码,看看它的效果如何。它非常慢,而且由于我对大型数组和列表没有太多经验,我认为最好在这里发布它,让其他人查看和改进/修改它。
Components
这个项目/文章包含三个部分和下载。字典本身、一个字典编辑器程序和一个测试拼写检查器程序。
词典
该字典包含近 75,000 个单词,为英语(英国/国际)。大部分单词来自网上的一些可用字典,但令人惊讶的是,尽管它们包含许多地名和我从未听说过的单词(但我在 OED 中查到它们确实存在 :)),但它们似乎缺少许多日常用语(例如,their,owner,仅举几例)。自从我开始这个项目以来,我花了几个月的时间添加单词并进行检查(以及检查现有的大部分单词),虽然我相当有信心其准确性非常好,但它不会是完美的,而且可能还会遗漏不少单词 :)。
请随意添加单词,或者将单词以文本文件的形式发送给我,我会更新它,但目前它是英式英语,所以如果我们打算添加诸如 "Color" 和 "Customize" 而不是 "Colour" 和 "Customise" 等单词,最好还是开始一个美式英语版本 <g>。
它采用示例程序使用的二进制格式,我将在下一节中解释。您可以使用字典编辑器程序将其导出为纯文本文件。
字典编辑器
首先,我将讨论字典的格式——两个程序都使用此格式来读取和编辑字典。
我基本上只使用一个包含所有单词的 CStringArray 数组,然后将其序列化到文件中。
它比使用我之前使用的 CObArray 数组更快,但仍然不够快。

程序本身只是一个默认的 MFC AppWizard 程序,其中有一个 CListView 视图,该视图被子类化为一个自定义的 CListCtrl,该自定义 CListCtrl 具有一个排序函数(感谢 CodeGuru 上的 Zafir Anjum),并且该函数被修改得稍微不区分大小写。
它使用一个 CStringArray 数组来存储每个单词。
字典编辑器中的大部分代码都有注释,所以我在这里不作完整解释。
它有三个工具栏按钮(添加、删除和修改),允许您添加、删除和修改单词。
它还在 文件 菜单上具有 导入 和 导出 命令,允许您将单词从文本文件导入和导出到文本文件,每个单词占一行。
Import 函数中有注释掉的代码,可以检查要添加的每个单词,并确保它不存在。
它被注释掉的原因是它不起作用——导入完整字典时,它运行了 7 个小时,而 UltraEdit 在不到两秒钟的时间内完成了删除重复项和排序,这充分说明了这段代码的效率 :)。
单击 单词 列标题控件将对联系人进行排序。
工具 菜单包含三个命令
修剪
Trim 命令会遍历所有单词,对它们执行 TrimLeft() 和 TrimRight() 操作,以删除前导和尾随空格。
void CDictionaryEditorView::OnToolsTrim() { CWaitCursor wait; // Display a wait cursor CListCtrl& m_List = GetListCtrl(); // Get handle to the base List Control // Get the number of words in the list control int x = m_List.GetItemCount(); GetDocument()->array.RemoveAll(); // Remove all words in our dictionary for (int j = 0; j < x; j++) // For each item in the list { CString strWord; strWord = m_List.GetItemText(j, 0); // Get the word strWord.TrimLeft(); // Remove leading spaces strWord.TrimRight(); // Remove trailing spaces // Overwrite the old word with the new trimmed word m_List.SetItemText(j, 0, strWord); GetDocument()->AddWord(strWord); // Add the word to the dictionary } GetDocument()->SetModifiedFlag(); // Set the document to be modified }
小写
Lowercase 命令会遍历所有单词,并对它们执行 MakeLower() 操作,使它们全部变为小写。
void CDictionaryEditorView::OnToolsLowercase() { CWaitCursor wait; // Display a wait cursor CListCtrl& m_List = GetListCtrl(); // Get handle to the base List Control // Get the number of words in the list control int x = m_List.GetItemCount(); GetDocument()->array.RemoveAll(); // Remove all words in our dictionary for (int j = 0; j < x; j++) // For each item in the list { CString strWord; strWord = m_List.GetItemText(j, 0); // Get the word strWord.MakeLower(); // Make the word lowercase // Overwrite the old word with the new lowercase word m_List.SetItemText(j, 0, strWord); GetDocument()->AddWord(strWord); // Add the word to the dictionary } GetDocument()->SetModifiedFlag(); // Set the document to be modified }
查找和查找下一个
Find 命令会遍历单词列表,查找包含特定字符的单词,然后逐个突出显示并选择它们。
我用它来查找未正确导入(由于二进制格式)并且两侧有 Trim 命令找不到的无效字符的单词,然后手动编辑它们。Find Next 命令从上一个找到的单词继续。
void CDictionaryEditorView::OnToolsFind() { CListCtrl& m_List = GetListCtrl(); // Get handle to the base List Control m_nFind = 0; // Reset FindNext counter to 0 // Get the number of words in the list control int x = m_List.GetItemCount(); for (int j = m_nFind; j < x; j++) // For each item in the list { CString strWord; strWord = m_List.GetItemText(j, 0); // Get the word // Does that word have one of the following characters in it? if (strWord.FindOneOf("»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ" "רÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòôõö÷øùúûüýþÿ") != -1) { // Select the word in the list m_List.SetItemState(j, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED); // Make sure it's visible in the list m_List.EnsureVisible(j, FALSE); m_nFind = j + 1; // Update the FindNext counter return; // Return as we've found what we wanted } } }
测试拼写检查器
问题就出在这里。它很混乱,而且很慢。
总之,主要的测试程序包含一个主对话框,其中有一个文本框和两个命令按钮。CSpellDlg 类负责处理此对话框以及主要的拼写检查功能。
选项 按钮会打开选项对话框,让您配置字典的位置和自定义字典应存储的位置。
您需要在测试程序之前执行此操作。
还有一个拼写检查器对话框,您可能熟悉它的某种形式(这个类似于 Visio 的),当找到一个未识别的单词时就会显示出来。
它会显示单词和任何拼写建议,并将单击的按钮返回到主对话框。
因此,它的工作原理是这样的(或者说,它的“不”工作原理 :)):假设主文本框中有句子,当您单击 Spell 按钮时,会执行 OnSpell() 函数。
它执行以下操作:
首先,它使用 UpdateData() 函数从文本框中获取文本,然后检查字典是否存在,如果存在,则加载它们。然后它创建一个 CStringArray 来存储应在当前会话中忽略的单词(当用户单击 Ignore All 时),并初始化计数器。
UpdateData(TRUE); // Get the text from the text box // Check the main dictionary exists CFileStatus status; if(CFile::GetStatus(AfxGetApp()->GetProfileString("Settings", "Main", ""), status)) { CFile cfSettingsFile (AfxGetApp()->GetProfileString("Settings", "Main", ""), CFile::modeNoTruncate | CFile::modeReadWrite ); CArchive ar ( &cfSettingsFile, CArchive::load ); array.Serialize ( ar ); // Load the main dictionary } // See if a custom dictionary exists CFileStatus status2; if(CFile::GetStatus(AfxGetApp()->GetProfileString("Settings", "Custom", ""), status2)) { CFile cfSettingsFile (AfxGetApp()->GetProfileString("Settings", "Custom", ""), CFile::modeNoTruncate | CFile::modeReadWrite ); CArchive ar ( &cfSettingsFile, CArchive::load ); custarray.Serialize ( ar ); // Load the custom dictionary } CString strStart = m_strText; CStringArray IgnoreAll; // Initialise Ignore All list int nPos = 0; // Initialise position counters int nPos2 = 0;
下一步是执行一个 While 循环,查找特定字符(使用自定义的 FindOneOf() 函数,该函数允许您指定从何处开始查找)。这些字符通常是分隔单词的字符(“!'()[]<>,.). 这个循环基本上找到了每个新单词。
在此循环中,提取单词,删除前导和尾随空格,如果它为空、长度为一字符或包含数字和其他特定字符,则忽略它。
CString strWord = strStart.Mid(nPos, nPos2 - nPos); strWord.TrimLeft(); strWord.TrimRight(); if (strWord == "" || strWord.GetLength() == 1 || strWord.FindOneOf("0123456789+-/@?:*.,") != -1) { // Ignore the word }
否则,程序会遍历字典以查看单词是否存在。它还会遍历自定义字典以查看它是否存在,并查看 IgnoreAll 列表以查看是否应忽略该单词。如果在任何这些情况下找到该单词,它会将 bFound 标志设置为 TRUE 并返回到主循环。
如果在检查完字典和 IgnoreAll 列表后找到了该单词,那么它会返回到主 While 语句,忽略该单词,然后尝试下一个单词。
BOOL bFound = FALSE; for (int i = 0; i < array.GetSize(); i++) // Is the word in the main dictionary? { CString strCheckWord; strCheckWord = GetWord(i); if (strWord.CompareNoCase(strCheckWord) == 0) { bFound = TRUE; // Yes, exit this for statement break; } } for (int j = 0; j < custarray.GetSize(); j++) // Is the word in the custom dictionary? { CString strCheckWord; strCheckWord = GetWordCustom(j); if (strWord.CompareNoCase(strCheckWord) == 0) { bFound = TRUE; // Yes, exit this for statement break; } } for (int f = 0; f < IgnoreAll.GetSize(); f++) // Should we ignore the word? { if (strWord.CompareNoCase((LPCTSTR)IgnoreAll[f]) == 0) { bFound = TRUE; // Yes, exit this for statement break; } }
否则,如果未在任何地方找到该单词,我们就到了有趣的部分,在该部分中,单词将在文本框中突出显示,找到可能的单词建议,并显示拼写检查对话框,以便用户可以选择要采取的操作。
这一部分需要重写,因为它很混乱,而且效果不佳。
我尝试做的是遍历字典中的每个单词,看看它是否从左边开始与拼写错误的单词的所有字符匹配 - 1 个字符,然后是左边 - 2 个字符、-3 个字符等,并且字典中的单词与拼写错误的单词越接近,就越将其添加到建议列表的顶部。
但是,我使用的代码在这些条件下浪费了大量时间来查找相同单词,即使起始字符不同,所以 25/26 的时间循环都是无用的,因此需要更改。
这种查找正确单词的方法也找不到“upon”,如果输入的是“apon”,而大多数其他拼写检查器都可以毫无问题地找到它,所以如果引入某种元音交换,并且从右边开始反向执行算法,那会更准确。
m_Text.SetSel(nPos, nPos2); // Select the unfound word in the text box CSpellDialog dlg; dlg.m_strWord = strWord; // Initialise the Spelldialog int nGood = 0; for (int i = 0; i < array.GetSize(); i++) // for each word in the main dictionary { CString strGetWord; strGetWord = GetWord(i); // Get the word // Loop through from the number of characters backwards for (int j = strWord.GetLength(); j >= 1; --j) { CString strGetWord; strGetWord = GetWord(i); /* Does the left hand side of the dictionary word equal the left hand side of the misspelt word?*/ if (strGetWord.Left(j).CompareNoCase(strWord.Left(j)) == 0) { BOOL bGood = FALSE; if (strWord.GetLength() <= 3) // Is the word roughly of the same length { // Are the first to characters the same? if (strGetWord.Left(2).CompareNoCase(strWord.Left(2)) == 0) { bGood = TRUE; // *Must* be a good match! <g> } } else { if (strGetWord.Left(strWord.GetLength() - 2).CompareNoCase( strWord.Left(strWord.GetLength() - 2)) == 0) { bGood = TRUE; } } BOOL bFoundWord = FALSE; for( int k = 0; k < dlg.m_saSuggestions.GetSize(); k++ ) // Is this word already suggested? { if (strGetWord.CompareNoCase( (LPCTSTR)dlg.m_saSuggestions[k]) == 0) { bFoundWord = TRUE; break; } } if (bFoundWord == FALSE) { if ((strGetWord.GetLength() >= (strWord.GetLength() - 1)) && (strGetWord.GetLength() <= (strWord.GetLength() + 1))) { if (bGood == TRUE) // Good match, so add it near // the top of the suggestion list { dlg.m_saSuggestions.InsertAt(nGood, strGetWord); nGood ++; } else { dlg.m_saSuggestions.Add(strGetWord); } } break; } } } }
然后对自定义字典重复此过程。
最后阶段是显示拼写检查对话框,它允许用户选择是 忽略单词、将其添加到自定义字典,还是 更改 为列表框中的一个建议。然后程序根据用户单击的按钮执行相应的操作。
if (dlg.DoModal() == IDOK) // Display the Spell Dialog and wait for the user to do something { if (dlg.m_nOption == 1) // Add Word was clicked, so add the word // to the custom dictionary { CString strAddWord = strWord; strAddWord.MakeLower(); AddWordCustom(strAddWord); CFile cfSettingsFile (AfxGetApp()->GetProfileString("Settings", "Custom", ""), CFile::modeCreate | CFile::modeNoTruncate | CFile::modeReadWrite ); CArchive ar2 ( &cfSettingsFile, CArchive::store ); custarray.Serialize ( ar2 ); // Save the custom dictionary } else if (dlg.m_nOption == 2) // Change button was clicked, so change the word { strStart.Delete(nPos, nPos2 - nPos); m_strText.Delete(nPos, nPos2 - nPos); strStart.Insert(nPos, dlg.m_strChangeTo); m_strText.Insert(nPos, dlg.m_strChangeTo); nPos2 = nPos + dlg.m_strChangeTo.GetLength(); UpdateData(FALSE); // Update the text box } else if (dlg.m_nOption == 3) { // ignore button was clicked, so ignore the word this once } else if (dlg.m_nOption == 4) // Ignore all button was clicked, so add it to the IgnoreAll list { CString strIgnoreAllWord = strWord; strIgnoreAllWord.MakeLower(); IgnoreAll.Add(strIgnoreAllWord); } } else { return; // Cancel button or Close button where clicked, so quit function }
现在它应该“可以工作”了 :)。
改进
我知道有很多 <g>。
查找建议算法需要大量工作,并且需要大大提速,因为它在比较字符串时浪费了大量时间,而第一个字符甚至都不相同——这是主要的瓶颈,但实际上所有东西都需要优化。
目前,它只尝试查找以拼写错误单词的相同字符开头的正确单词,但这并不总是情况。一个交换元音的算法可能效果最好,但看看 Microsoft Word 的建议单词的方式,它知道您想要什么单词,即使在某些情况下它与您输入的单词不太相似,所以也许有什么更复杂的事情正在发生,比如字典被细分成几个部分,这样经常拼写错误的单词就会在一起,这样程序就更容易选择建议。
另一件可能应该添加的事情是 全部更改 按钮,用于一次性更正所有以某种方式拼写错误的单词。
问题的一半可能是我使用了所有 MFC 函数,但我尝试将所有内容(除了数组)都使用标准 C 函数,但似乎没有帮助,所以为了本文的目的,我把它们都去掉了(我留下了一个函数,这样您就可以看到我尝试过的那种东西)。
如果有任何建议或改进,请发送给我或在此处发布。
