Постановка задачи
Всем привет, недавно столкнулся с задачей проверки орфографии и исправления опечаток в 1С. Посмотрев варианты решений (MS Word, Yandex, т.д.), понял, что они мне не подходят. Решил копнуть глубже. Лично мне понравилось решение на базе спелчекера, встроенного в Windows. Поскольку в 1С нет возможности напрямую обратиться к этому функционалу ОС, я реализовал его в виде DLL на языке C# и сделал COM-обертку. COM-объект подключил в 1С.
В итоге получилась вот такая простая форма, на которой при нажатии кнопки “Проверить опечатки” текст в Строке ввода анализируется и исправляется.
![](https://habrastorage.org/getpro/habr/upload_files/078/e52/e33/078e52e33ea4bb3f40d9a0b6fbf1fa8a.png)
Исходный код COM объекта, обработки 1С и подробные пояснения приведены ниже.
Скачать исходники можно тут:
https://github.com/amizerov/SpellChecker
Бинарики 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<string> Suggestions);
Вот такой список вернем List<SpellCheckResult>, это список сразу для всех слов с ошибками из анализируемого текста.
Word здесь это само слово с ошибкой, найденное в тексте, Action – одно из предлагаемых действий (заменить, удалить или игнорить), Replacement – основное слово для замены ошибочного и Suggestions – список всех вариков замены.
SpellCheckerAPI.cs
using System.Runtime.InteropServices; namespace SpellChecker; public record SpellCheckResult(string Word, string Action, string Replacement, List<string> Suggestions); public class SpellCheckerAPI : SpellCheckerBase { public static List<SpellCheckResult> SpellCheck(string s) { SpellCheckerFactoryClass? factory = null; ISpellCheckerFactory? ifactory = null; ISpellChecker? checker = null; ISpellingError? error = null; IEnumSpellingError? errors = null; IEnumString? suggestions = null; List<SpellCheckResult> 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<string> 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; } }
В файле описания проекта добавим строку <EnableComHosting>true</EnableComHosting>, которая скажет компилятору добавить COM обертку для нашей библиотеки.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <EnableComHosting>true</EnableComHosting> </PropertyGroup> </Project>
После этого, при сборке проекта, компилятор сгенерит дополнительный файлик SpellChecker.comhost.dll.
В нем компилятор реализует метод DllRegisterServer, что позволит нам зарегистрировать наш компонет в реестре с помощью утилиты regsvr32.
Вот что должно получиться в папке на выходе при компиляции:
![](https://habrastorage.org/getpro/habr/upload_files/a57/76e/cba/a5776ecba2c8948acf9f42320166c697.png)
Получается вроде как 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 не влияют, но можно тоже оставить.
![](https://habrastorage.org/getpro/habr/upload_files/fb2/547/f29/fb2547f29521291929e49ccf44045c02.png)
Если все правильно сделали, то увидим окно с Успешным выполнением регистрации объекта, и в реестре появится запись для нашего нового ProgID со значением SpellChecker.ComService
![](https://habrastorage.org/getpro/habr/upload_files/14a/fc0/e19/14afc0e19b313a7a4bbefba7873a9c56.png)
Обработка 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); КонецФункции &НаКлиенте Процедура ПродолжитьОбработкуТекста(Параметры = Неопределено) Экспорт // Что то делаем дальше КонецПроцедуры
Для примера, формочку в обработке я слепил вот так, без изысков, вы уже можете доработать ее как вам будет удобно для вашей задачи
![](https://habrastorage.org/getpro/habr/upload_files/a0a/7ad/c05/a0a7adc05f36771cf3cdaf21f67ccb58.png)
И теперь, если правильно дать имена всем элементам на форме, то при нажатии на кнопку “Проверить опечатки”, код обработки, приведенный выше, последовательно исправит все ошибочные слова в поле ввода.
![](https://habrastorage.org/getpro/habr/upload_files/619/cf7/767/619cf776772c7edd2b1f76ce7dda97a4.png)
![](https://habrastorage.org/getpro/habr/upload_files/db6/b75/322/db6b75322d6eba5ac6307a076ab48296.png)
![](https://habrastorage.org/getpro/habr/upload_files/57b/c67/b92/57bc67b924a7d379411ad5157c0c14af.png)
Заключение
Я понимаю, что 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<string>? 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, возникает ошибка.
Ну, как говорится, отрицательный результат тоже результат.
Пока друзья, надеюсь на жесткий обс@р.
ссылка на оригинал статьи https://habr.com/ru/articles/829014/
Добавить комментарий