Modern C++

Using the C++ Spell Checking API

Learn how to add spell checking to your applications.

Did you know that Windows includes a native Spell Checking API designed for C++ developers?

I've spent a lot of time teaching developers how to build modern applications for Windows using C++ and Direct2D. Along with this comes the powerful DirectWrite text layout and rendering engine, but users have come to expect more than just pretty pixels. They want useful features like spell checking that are pervasive throughout the applications they use. Now you can easily add spell checking to your own applications using the Spell Checking API. In this month's column, I'm going to introduce this API. Next month I'll show you how to apply modern C++ techniques to produce a more intuitive and elegant programming experience.

The Spell Checking API is exposed via a set of COM-style interfaces. It relies on COM activation, so I'll need to call CoInitializeEx to initialize the COM library and set the thread's concurrency model:

HR(CoInitializeEx(nullptr, // reserved
                  COINIT_MULTITHREADED));

I'm assuming there's an inline function that will check the HRESULT and throw an exception if it's not S_OK. Something like this:

inline void HR(HRESULT const result)
{
  if (S_OK != result) throw ComException(result);
}

Once the COM library is initialized, I can create the Spell Checking API factory object to get started. That involves calling the CoCreateInstance function, because this API relies on COM activation. Before I can do that, I need a COM smart pointer to take ownership of the resulting COM interface pointer:

ComPtr<ISpellCheckerFactory> factory;

The ComPtr class template is provided by the Windows Runtime C++ Template Library (WRL), which is included with the Windows SDK for Windows 8. Don't let the name confuse you: This library is primarily about implementing COM servers and clients. ComPtr is defined in the Microsoft::WRL namespace and included by the wrl.h header file:

HR(CoCreateInstance(__uuidof(SpellCheckerFactory),
                    nullptr,
                    CLSCTX_INPROC_SERVER,
                    __uuidof(factory),
                    reinterpret_cast<void **>(factory.GetAddressOf())));

The first thing CoCreateInstance needs is the class identifier (CLSID) of the Spell Checking API factory COM class. That's what the __uuidof operator retrieves. The next parameter is used for aggregation, but it's optional, and I won't need it. The CLSCTX_INPROC_SERVER constant indicates that the requested COM class should run in the same process as the caller. The second-to-last parameter indicates the interface identifier (IID) for the COM interface I'd like it to return. In this case, the __uuidof operator retrieves the IID associated with this interface. Finally, the last parameter returns the actual interface pointer in a rather roundabout way. It's a bit messy because the C++ type system needs to be switched off briefly to bridge the COM/C++ divide.

Now that I have the factory object, I can check whether my preferred language is supported:

BOOL supported;

HR(factory->IsSupported(L"en-US",
                        &supported));

if (supported) { ... }

Next, I need to create the spell checker object itself:

ComPtr<ISpellChecker> checker;

The factory's CreateSpellChecker method will create one shell checker per language:

HR(factory->CreateSpellChecker(L"en-US",
                               checker.GetAddressOf()));

Now I've come to the hard part -- I need to find some text that will trigger all the possible corrective actions provided by the Spell Checking API. The API distinguishes between suggestions, unconditional replacements and simply deleting erroneous text. It might offer a list of suggestions if an obvious replacement can't be found. If an unambiguous replacement is available, it will simply inform the caller that some text needs to be replaced, and provide the replacement. Finally, it might indicate that some text simply needs to be deleted. After trying a few different words and phrases, I finally came up with text that triggers all three corrective actions:

auto text = L"Cann I I haev some?";

Obviously, the correct text should be: "Can I have some?" Now I'll try and see if I can get the Spell Checking API to correct this for me. The errors are provided as a COM-style enumeration:

ComPtr<IEnumSpellingError> errors;

I need to call the Spell Checking API Check method and provide it with the text to check:

HR(checker->Check(text,
                  errors.GetAddressOf()));

COM-style enumerations require some kind of loop with repeated calls to a Next method. In this case, the following loop will suffice:

for (;;)
{
  ComPtr<ISpellingError> error;

  if (S_OK != errors->Next(error.GetAddressOf()))
  {
    break;
  }

  // Respond to each error here!
}

The Next method returns S_OK until there are no more spelling errors to retrieve. With the error in hand, the first thing I need to do is determine the substring in question. The get_StartIndex and get_Length methods provide the necessary offsets:

ULONG startIndex;
HR(error->get_StartIndex(&startIndex));

ULONG length;
HR(error->get_Length(&length));

I can then easily create a standard string object given the original text as well as these offsets:

wstring word(text + startIndex,
             text + startIndex + length);

Next, I need to find out what's wrong. I do that by retrieving a value that indicates the corrective action that my application should perform:

CORRECTIVE_ACTION action;
HR(error->get_CorrectiveAction(&action));

I'll start with the simplest action and work my way up to the most complex. The first is defined by the CORRECTIVE_ACTION_DELETE constant that simply indicates the span of text should be deleted. The user should be prompted to delete the text in question.

The next possible action is defined by the CORRECTIVE_ACTION_REPLACE constant. This action includes alternative text that should replace the text in question. This action is relatively rare, and the Spell Checking API only offers it when it has no doubt about the replacement text. The get_Replacement method retrieves the replacement text that my application should use:

wchar_t * replacement;
HR(error->get_Replacement(&replacement));

Text returned by the API must eventually be freed by the caller using the CoTaskMemFree function:

CoTaskMemFree(replacement);

The final possible action is by far the most common, in my experience. The CORRECTIVE_ACTION_GET_SUGGESTIONS constant indicates the user should be prompted with a list of suggestions. This list takes the form of another COM-style enumeration that the application retrieves by calling the Spell Checking API Suggest method. The Suggest method has no knowledge of the particular spelling error I'm dealing with. Therefore, I need to provide it with the erroneous text:

ComPtr<IEnumString> suggestions;

HR(checker->Suggest(word.c_str(),
                    suggestions.GetAddressOf()));

Next, I need to write another loop to call the enumeration's Next method repeatedly. In this case, the Next method simply returns one of the suggestions:

for (;;)
{
  wchar_t * suggestion;

  if (S_OK != suggestions->Next(1, &suggestion, nullptr))
  {
    break;
  }

  // Add the suggestion to a list for presentation

  CoTaskMemFree(suggestion);
}

The IEnumString Next method lets me retrieve more than one string at a time, but I'm just opting for the simplest usage in this case.

And that's it! I placed this code in a console application, and here are the results:

Check "Cann I I haev some?"

Suggest replacing "Cann" with:
  Can
  Canny
  Canon
  Cans
  Cant

Delete "I"

Replace "haev" with "have"

But Wait, There's More!
The Spell Checking API is a welcome addition to the C++ developer's toolbox. As I mentioned, it supports multiple languages, but it also lets you maintain user-specific settings and dictionaries. There's even more to the Spell Checking API, but I'll leave the rest for you to explore on your own. Until next time, happy spell checking!

About the Author

Kenny Kerr is a computer programmer based in Canada, an author for Pluralsight and a Microsoft MVP. He blogs at kennykerr.ca and you can follow him on Twitter at twitter.com/kennykerr.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube