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






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 函数,但似乎没有帮助,所以为了本文的目的,我把它们都去掉了(我留下了一个函数,这样您就可以看到我尝试过的那种东西)。
如果有任何建议或改进,请发送给我或在此处发布。