Орфография в 1С через COM на C#
Постановка задачи
Всем привет, недавно столкнулся с задачей проверки орфографии и исправления опечаток в 1С. Посмотрев варианты решений (MS Word, Yandex, т.д.) понял, что они мне не подходят. Решил капнуть глубже. Лично мне понравилось решение на базе спелчекера, встроенного в Windows. Поскольку в 1С нет возможности напрямую обратиться к этому функционалу ОС, я реализовал его в виде DLL на языке C# и сделал COM обертку. COM объект подключил в 1С.
В итоге получилась вот такая простая форма, на которой при нажатии кнопки «Проверить опечатки», текст в Строке ввода анализируется и исправляется.
Исходный код COM объекта, обработки 1С и подробные пояснения приведены ниже.
Скачать исходники можно тут:
https://github.com/amizerov/SpellChecker
ПроверкаОрфографии.epf
Бинарики COM объекта тут:
https://mizer.dev/Files/SpellChecker.rar
Введение
Задача проверки орфографии при вводе текста и нахождение опечаток, может возникнуть в любом проекте, где пользователь должен руками вносить какие то данные, типа наименования товаров, предметов обучающих курсов и т.д.
Встроенные средства проверки орфографии Windows, описанные тут, предоставляют интерфейсы, которые можно использовать для автоматического обнаружения и исправления опечаток в текстах. Это особенно полезно для разработчиков, которые хотят добавить функцию проверки орфографии в свои приложения без обращения к сторонним сервисам и необходимости реализации сложных алгоритмов с нуля.
В данной статье мы рассмотрим, как использовать интерфейсы COM (Component Object Model) Windows для интеграции встроенного чекера орфографии в свои проекты на языке C#.
Покажу как опубликовать функционал поиска ошибок в виде своего COM компонента, и как его использовать в коде обработки 1С. Часто 1С-ники пытаются решить эту задачу с помощью SpellChecker MS Word, но извините, это на много медленнее работает, чем тот способ, который описан ниже.
Проект SpellChecker на C# в Visual Studio 2022
Для создания COM компоненты на C# используем шаблон проекта — Библиотека классов (Микрософт), я назвал проект SpellChecker. Включим в него класс SpellCheckerBase, в котором просто объявим все интерфейсы Spell Checking — га из официальной документации Microsoft https://learn.microsoft.com/en-us/windows/win32/intl/spell-checker-interfaces
SpellCheckerBase.cs
using System.Runtime.InteropServices;
namespace SpellChecker;
public class SpellCheckerBase
{
protected enum CORRECTIVE_ACTION
{
CORRECTIVE_ACTION_NONE,
CORRECTIVE_ACTION_GET_SUGGESTIONS,
CORRECTIVE_ACTION_REPLACE,
CORRECTIVE_ACTION_DELETE,
}
[Guid("B7C82D61-FBE8-4B47-9B27-6C0D2E0DE0A3")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport]
protected interface ISpellingError
{
uint StartIndex { get; }
uint Length { get; }
CORRECTIVE_ACTION CorrectiveAction { get; }
string Replacement { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("803E3BD4-2828-4410-8290-418D1D73C762")]
[ComImport]
protected interface IEnumSpellingError
{
[return: MarshalAs(UnmanagedType.Interface)]
ISpellingError Next();
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00000101-0000-0000-C000-000000000046")]
[ComImport]
protected interface IEnumString
{
void Next([In] uint celt, [MarshalAs(UnmanagedType.LPWStr)] out string rgelt, out uint pceltFetched);
void Skip([In] uint celt);
void Reset();
void Clone([MarshalAs(UnmanagedType.Interface)] out IEnumString ppenum);
}
[Guid("432E5F85-35CF-4606-A801-6F70277E1D7A")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport]
protected interface IOptionDescription
{
string Id { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
string Heading { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
string Description { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
IEnumString Labels { [return: MarshalAs(UnmanagedType.Interface)] get; }
}
[Guid("0B83A5B0-792F-4EAB-9799-ACF52C5ED08A")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport]
protected interface ISpellCheckerChangedEventHandler
{
void Invoke([MarshalAs(UnmanagedType.Interface), In] ISpellChecker sender);
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("B6FD0B71-E2BC-4653-8D05-F197E412770B")]
[ComImport]
protected interface ISpellChecker
{
string languageTag { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
[return: MarshalAs(UnmanagedType.Interface)]
IEnumSpellingError Check([MarshalAs(UnmanagedType.LPWStr), In] string text);
[return: MarshalAs(UnmanagedType.Interface)]
IEnumString Suggest([MarshalAs(UnmanagedType.LPWStr), In] string word);
void Add([MarshalAs(UnmanagedType.LPWStr), In] string word);
void Ignore([MarshalAs(UnmanagedType.LPWStr), In] string word);
void AutoCorrect([MarshalAs(UnmanagedType.LPWStr), In] string from, [MarshalAs(UnmanagedType.LPWStr), In] string to);
byte GetOptionValue([MarshalAs(UnmanagedType.LPWStr), In] string optionId);
IEnumString OptionIds { [return: MarshalAs(UnmanagedType.Interface)] get; }
string Id { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
string LocalizedName { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
uint add_SpellCheckerChanged([MarshalAs(UnmanagedType.Interface), In] ISpellCheckerChangedEventHandler handler);
void remove_SpellCheckerChanged([In] uint eventCookie);
[return: MarshalAs(UnmanagedType.Interface)]
IOptionDescription GetOptionDescription([MarshalAs(UnmanagedType.LPWStr), In] string optionId);
[return: MarshalAs(UnmanagedType.Interface)]
IEnumSpellingError ComprehensiveCheck([MarshalAs(UnmanagedType.LPWStr), In] string text);
}
[Guid("8E018A9D-2415-4677-BF08-794EA61F94BB")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport]
protected interface ISpellCheckerFactory
{
IEnumString SupportedLanguages { [return: MarshalAs(UnmanagedType.Interface)] get; }
int IsSupported([MarshalAs(UnmanagedType.LPWStr), In] string languageTag);
[return: MarshalAs(UnmanagedType.Interface)]
ISpellChecker CreateSpellChecker([MarshalAs(UnmanagedType.LPWStr), In] string languageTag);
}
[Guid("7AB36653-1796-484B-BDFA-E74F1DB7C1DC")]
[ComImport]
protected class SpellCheckerFactoryClass
{
}
}
Добавим в проект класс SpellCheckerAPI, который унаследуем от SpellCheckerBase и реализуем в нем наш единственный, основной статический метод для проверки орфографии SpellCheck с удобным для нас результатом в виде списка объектов типа SpellCheckResult. Для краткости записи объявлен рекордом.
public record SpellCheckResult (string Word, string Action, string Replacement, List
Вот такой список вернем List
Word здесь это само слово с ошибкой, найденное в тексте, Action — одно из предлагаемых действий (заменить, удалить или игнорить), Replacement — основное слово для замены ошибочного и Suggestions — список всех вариков замены.
SpellCheckerAPI.cs
using System.Runtime.InteropServices;
namespace SpellChecker;
public record SpellCheckResult(string Word, string Action, string Replacement, List Suggestions);
public class SpellCheckerAPI : SpellCheckerBase
{
public static List SpellCheck(string s)
{
SpellCheckerFactoryClass? factory = null;
ISpellCheckerFactory? ifactory = null;
ISpellChecker? checker = null;
ISpellingError? error = null;
IEnumSpellingError? errors = null;
IEnumString? suggestions = null;
List spellCheckResults = new();
try
{
factory = new SpellCheckerFactoryClass();
ifactory = (ISpellCheckerFactory)factory;
//проверим поддержку русского языка
int res = ifactory.IsSupported("ru-RU");
if (res == 0) { throw new Exception("Fatal error: russian language not supported!"); }
checker = ifactory.CreateSpellChecker("ru-RU");
errors = checker.Check(s);
while (true)
{
//получаем ошибку
if (error != null) { Marshal.ReleaseComObject(error); error = null; }
error = errors.Next();
if (error == null) break;
//получаем слово с ошибкой
string word = s.Substring((int)error.StartIndex, (int)error.Length);
string action = "";
string replac = error.Replacement;
List sugges = new();
//получаем рекомендуемое действие
switch (error.CorrectiveAction)
{
case CORRECTIVE_ACTION.CORRECTIVE_ACTION_DELETE:
action = "удалить";
break;
case CORRECTIVE_ACTION.CORRECTIVE_ACTION_REPLACE:
action = "заменить";
break;
case CORRECTIVE_ACTION.CORRECTIVE_ACTION_GET_SUGGESTIONS:
action = "заменить на одно из";
if (suggestions != null) { Marshal.ReleaseComObject(suggestions); suggestions = null; }
//получаем список слов, предложенных для замены
suggestions = checker.Suggest(word);
while (true)
{
string suggestion;
uint count = 0;
suggestions.Next(1, out suggestion, out count);
if (count == 1) sugges.Add(suggestion);
else break;
}
break;
}
if(replac == "") replac = sugges.Count > 0 ? sugges[0] : "";
spellCheckResults.Add(new SpellCheckResult(word, action, replac, sugges));
}
}
finally
{
if (suggestions != null) { Marshal.ReleaseComObject(suggestions); }
if (factory != null) { Marshal.ReleaseComObject(factory); }
if (ifactory != null) { Marshal.ReleaseComObject(ifactory); }
if (checker != null) { Marshal.ReleaseComObject(checker); }
if (error != null) { Marshal.ReleaseComObject(error); }
if (errors != null) { Marshal.ReleaseComObject(errors); }
}
return spellCheckResults;
}
}
Теперь, для всего этого надо сделать COM обертку, для этого, в том же пространстве имен проекта SpellChecker, создадим еще один класс ComService с атрибутом [ComVisible (true)] и назначим ему уникальный Guid.
Реализуем в нем публичный метод SpellCheck, который и будет доступен через COM. Пусть, для простаты, он возвращает JSON строку с результатами проверки, ошибочными словами и предложениями по замене ошибочных слов.
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace SpellChecker;
[ComVisible(true)]
[Guid("fe103d6e-e71b-414c-80bf-982f18f23237")]
public class ComService
{
public string SpellCheck(string text)
{
var spellCheckResults = SpellCheckerAPI.SpellCheck(text);
string x = JsonSerializer.Serialize(spellCheckResults);
string jsonString = Regex.Unescape(x);
return jsonString;
}
}
В файле описания проекта добавим строку
net8.0
enable
enable
true
После этого, при сборке проекта, компилятор сгенерит дополнительный файлик SpellChecker.comhost.dll.
В нем компилятор реализует метод DllRegisterServer, что позволит нам зарегистрировать наш компонет в реестре с помощью утилиты regsvr32.
Вот что должно получиться в папке на выходе при компиляции:
Получается вроде как SpellChecker.comhost.dll тут основной, но без SpellChecker.dll и SpellChecker.runtimeconfig.json, без этих 2-х файлов он не работает, это просто обертка, всю полезную работу все таки делает SpellChecker.dll
Регистрация COM объекта
Итак, регистрируем файлик SpellChecker.comhost.dll в реестре утилитой regsvr32, из командной строки, запущенной с правами администратора.
Надо просто запустить команду: regsvr32 SpellChecker.comhost.dll в той директории где лежит наша библиотека. Очень важно отметить, что в папке должны быть обязательно 3-и файла:
SpellCheck.comhost.dll
SpellCheck.dll
SpellCheck.runtimeconfig.json
Иначе regsvr32 выдаст ошибку. Файлы SpellCheck.pdb и второй json не влияют, но можно тоже оставить.
Если все правильно сделали, то увидим окно с Успешным выполнением регистрации объекта, и в реестре появится запись для нашего нового ProgID со значением SpellChecker.ComService
Обработка 1С
Теперь в коде 1С мы можем использовать функционал, реализованный в нашей DLL, подключая COM объект обычным манером, как говорится, поздним связыванием:
srv = Новый COMОбъект («SpellChecker.ComService»);
и далее, вызвав метод srv.SpellCheck, получим результат в виде JSON строки таким образом:
res = srv.SpellCheck (txt);
результат превратим в массив объектов:
obj = ПростоеЧтениеJSON (res);
Где obj это массив объектов, содержащих ошибочное слово obj[i].Word и списков вариантов для его замены в виде массива строк obj[i].Suggestions. Еще одно свойство obj[i].Replacement — это основной варик замены слова с ошибкой.
Ниже полный код обработки:
ПроверкаОрфографии.epf
&НаКлиенте
Перем ОшибкаОрфографии;
&НаКлиенте
Процедура ПроверитьОпечатки(Команда)
ОшибкаОрфографии = Ложь;
ПроверкаОрфографии();
КонецПроцедуры
&НаКлиенте
Процедура ПроверкаОрфографии()
txt = СтрокаВвода;
srv = Новый COMОбъект("SpellChecker.ComService");
res = srv.SpellCheck(txt);
Если СтрДлина(res) = 2 Тогда
Если ОшибкаОрфографии Тогда
Оповещение = Новый ОписаниеОповещения("ПродолжитьОбработкуТекста", ЭтотОбъект);
ПоказатьЗначение(Оповещение,
"Отлично!" + Символы.ПС + " Все ошибки исправлены!");
ОшибкаОрфографии = Ложь;
Иначе
ПродолжитьОбработкуТекста();
КонецЕсли
Иначе
ОшибкаОрфографии = Истина;
obj = ПростоеЧтениеJSON(res);
wor = obj[0].Word;
rep = obj[0].Replacement;
Оповещение = Новый ОписаниеОповещения("ПослеЗакрытияВопроса", ЭтотОбъект, obj[0]);
ПоказатьВопрос(Оповещение, "Заменить " + wor + " на " + rep + "?",
РежимДиалогаВопрос.ДаНет,,, "Ошибка орфографии");
КонецЕсли
КонецПроцедуры
&НаКлиенте
Процедура ПослеЗакрытияВопроса(Результат, Параметры) Экспорт
Если Результат = КодВозвратаДиалога.Да Тогда
txt = СтрокаВвода;
wor = Параметры.Word;
rep = Параметры.Replacement;
СтрокаВвода = СтрЗаменить(txt, wor, rep);
ПроверкаОрфографии();
Иначе
ПродолжитьОбработкуТекста();
КонецЕсли;
КонецПроцедуры
Функция ПростоеЧтениеJSON(Данные)
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(Данные);
Возврат ПрочитатьJSON(ЧтениеJSON);
КонецФункции
&НаКлиенте
Процедура ПродолжитьОбработкуТекста(Параметры = Неопределено) Экспорт
// Что то делаем дальше
КонецПроцедуры
Для примера, формочку в обработке я слепил вот так, без изысков, вы уже можете доработать ее как вам будет удобно для вашей задачи
И теперь, если правильно дать имена всем элементам на форме, то при нажатии на кнопку «Проверить опечатки», код обработки, приведенный выше, последовательно исправит все ошибочные слова в поле ввода.
Заключение
Я понимаю, что COM технология наверно устарела, все уже перешли на вэб сервисы, REST API, gRPC, Message Brokers, WebSockets и GraphQL, но я решил попробовать интегрировать C# в 1С напрямую, а поскольку вставить C# dll в 1С без COM не получилось, завернул ее в COM. И кстати, как я не пытался добавить в COM объект SpellChecker событие OnComplete, ничего не получилось. Если кому интересно, вот такой код
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace SpellCheckerV2;
[ComVisible(true)]
[Guid("fe103d6e-e71b-414c-80bf-982f18f23235"),
InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IComService
{
string SpellCheck(string text);
}
[ComVisible(true)]
[Guid("fe103d6e-e71b-414c-80bf-982f18f23236"),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ISpellCheckerEvents
{
[DispId(1)]
public void OnComplete(string msg);
}
[ComVisible(true)]
[Guid("fe103d6e-e71b-414c-80bf-982f18f23238")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ISpellCheckerEvents))]
public class ComService : IComService
{
[ComVisible(true)]
public Action? OnComplete;
public string SpellCheck(string text)
{
var spellCheckResults = SpellCheckerAPI.SpellCheck(text);
string x = JsonSerializer.Serialize(spellCheckResults);
string jsonString = Regex.Unescape(x);
OnComplete?.Invoke("Ok");
return jsonString;
}
}
Этот код не работает, не публикует, как предполагается, событие OnComplete, 1С его не видит. И соответственно, если в 1С написать:
srv = Новый COMОбъект («SpellChecker.ComService»);
ДобавитьОбработчик srv.OnComplete, Пикака;
в строке где ДобавитьОбработчик пытается найти событие srv.OnComplete, возникает ошибка.
Ну, как говорится, отрицательный результат тоже результат.
Пока друзья, надеюсь на жесткий обс@р.