Как создать COM-объект на C# для 1С и управлять окнами через WinAPI?

Программист 1С v8.3 (Обычные формы) IT и автоматизация бизнеса
← На главную

При расширении возможностей 1С разработчики нередко прибегают к созданию собственных внешних библиотек на языке C#. Это позволяет задействовать функции операционной системы Windows, работать с низкоуровневыми вызовами WinAPI или реализовывать нестандартную логику интерфейса. Однако на этапе разработки и подключения COM-библиотеки часто возникают типовые проблемы: ошибка 0x80004002 (Интерфейс не поддерживается), сложности с поиском сборки в системе и особенности поиска дочерних окон внутри платформы 1С. Разберем по шагам, как правильно реализовать, зарегистрировать и использовать такую библиотеку.

Причины ошибки 0x80004002 и правила создания COM-класса на C#

Ошибка -2147467262(0x80004002): Интерфейс не поддерживается при вызове конструктора Новый COMОбъект(...) возникает в тех случаях, когда платформа 1С не может получить доступ к интерфейсу позднего связывания IDispatch. Платформа 1С обращается к методам и свойствам COM-объектов динамически, поэтому класс C# должен явно экспортировать этот интерфейс.

Для корректного взаимодействия класса с платформой 1С необходимо соблюдать следующие требования:

  1. Класс должен быть публичным (public) и иметь конструктор по умолчанию без параметров.
  2. Необходимо включить видимость для COM с помощью атрибута [ComVisible(true)].
  3. Следует указать уникальный GUID с помощью атрибута [Guid("...")] и программный идентификатор [ProgId("...")].
  4. Необходимо задать интерфейс класса через атрибут [ClassInterface(ClassInterfaceType.AutoDispatch)] или [ClassInterface(ClassInterfaceType.AutoDual)], чтобы среда .NET автоматически сформировала таблицу диспетчеризации для COM.

Реализация класса на C# для работы с дескрипторами окон WinAPI

Рассмотрим пример создания класса-обертки WndHlp, который экспортирует в 1С основные функции WinAPI для поиска окон, изменения их координат, стилей и взаимодействия через системные сообщения:


using System;
using System.Text;
using System.Runtime.InteropServices;

namespace _1CAddInNET
{
    [ComVisible(true)]
    [ProgId("1CAddInNET.WndHlp")]
    [ClassInterface(ClassInterfaceType.AutoDispatch)]
    [Guid("A497AB77-85C3-4F32-839D-7EDA0F5F89C8")]
    public class WndHlp
    {
        #region Private members
        private IntPtr m_hWnd = IntPtr.Zero;

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool PostMessage(IntPtr hWnd, int nMsg, int wParam, int lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr FindWindowEx(IntPtr wParam, IntPtr wParamChildAfter, string cClass, string cTitle);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool ShowWindow(IntPtr hWnd, int nMode);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetParent(IntPtr hWnd, IntPtr nWhdParent);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetParent(IntPtr hWnd);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool IsWindowVisible(IntPtr hWnd);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern uint SetWindowLong(IntPtr hWnd, int nStyle, int nParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool SetWindowPos(IntPtr hWnd, IntPtr nOrder, int nX, int nY, int nW, int nH, uint nMode);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int GetClassName(IntPtr hWnd, StringBuilder cClass, int nMaxCount);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool BringWindowToTop(IntPtr hWnd);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool SetForegroundWindow(IntPtr hWnd);

        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int left;
            public int top;
            public int right;
            public int bottom;
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int GetWindowRect(IntPtr hWnd, ref RECT rc);
        #endregion

        public int hWnd
        {
            get { return m_hWnd.ToInt32(); }
            set { m_hWnd = new IntPtr(value); }
        }

        public bool PostMsg(int nMsg, int wParam, int lParam)
        {
            return PostMessage(m_hWnd, nMsg, wParam, lParam);
        }

        public bool FindWnd(int wParam, int wParamChildAfter, string cClass, string cTitle)
        {
            m_hWnd = FindWindowEx(new IntPtr(wParam), new IntPtr(wParamChildAfter), cClass, cTitle);
            return m_hWnd != IntPtr.Zero;
        }

        public bool ShowWnd(int nMode)
        {
            return ShowWindow(m_hWnd, nMode);
        }

        public bool SetParentWnd(int nWhdParent)
        {
            SetParent(m_hWnd, new IntPtr(nWhdParent));
            return true;
        }

        public int GetParentWnd()
        {
            return GetParent(m_hWnd).ToInt32();
        }

        public bool IsVisibleWnd()
        {
            return IsWindowVisible(m_hWnd);
        }

        public bool SetWndLong(int nStyle, int nParam)
        {
            SetWindowLong(m_hWnd, nStyle, nParam);
            return true;
        }

        public bool SetWndPos(int nOrder, int nX, int nY, int nW, int nH, uint nMode)
        {
            SetWindowPos(m_hWnd, new IntPtr(nOrder), nX, nY, nW, nH, nMode);
            return true;
        }

        public string GetClassNameWnd()
        {
            StringBuilder cClass = new StringBuilder(255);
            GetClassName(m_hWnd, cClass, cClass.Capacity);
            return cClass.ToString();
        }

        public bool BringWndToTop()
        {
            return BringWindowToTop(m_hWnd);
        }

        public bool SetForegroundWnd()
        {
            return SetForegroundWindow(m_hWnd);
        }
    }
}

Регистрация DLL в операционной системе

Для того чтобы 1С могла загрузить сборку без обязательного копирования файла в каталог платформы bin, библиотеку нужно зарегистрировать в реестре Windows с указанием пути к файлу. Разберем особенности регистрации:

  1. Использование утилиты RegAsm: Регистрация выполняется с помощью системной утилиты RegAsm.exe. Обязательно используйте флаг /codebase, который прописывает полный путь к сборке в реестре. Без этого параметра Windows пытается найти файл DLL только в рабочей папке вызывающего процесса (в каталоге bin платформы 1С).
  2. Учет разрядности платформы:

    Если используется 32-разрядный клиент 1С, сборка должна регистрироваться через 32-битную версию RegAsm.exe:

    
    C:\Windows\Microsoft.NET\Framework\v4.0.30319\regasm.exe "C:\MyProjects\1CAddInNET.dll" /codebase /tlb
    

    Для 64-разрядного клиента регистрацию выполняют через Framework64:

    
    C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regasm.exe "C:\MyProjects\1CAddInNET.dll" /codebase /tlb
    
  3. Права администратора: Командная строка для регистрации должна быть запущена от имени администратора.

Подключение и вызов COM-объекта в коде 1С

После успешной регистрации библиотеки обращение к ней из встроенного языка 1С происходит стандартным образом:


Попытка
    ОберткаОкон = Новый COMОбъект("1CAddInNET.WndHlp");
Исключение
    Сообщить("Не удалось инициализировать COM-объект: " + ОписаниеОшибки());
    Возврат;
КонецПопытки;

Особенности поиска и управления дочерними окнами форм 1С

При поиске внутренних окон 1С разработчики часто сталкиваются с тем, что функция FindWindowEx не находит окно конкретной формы, если в качестве родительского окна передается главное окно программы. Проанализируем причину такого поведения:

  1. Иерархия MDI-окон: В многооконном интерфейсе (MDI) внутренние формы документов и обработок не являются прямыми потомками главного окна V8TopLevelFrame. Они вложены в специальное системное окно с классом MDIClient.
  2. Пошаговый поиск окна: Чтобы получить дескриптор дочерней формы, сначала находят главное окно 1С, затем внутри него находят окно с классом MDIClient, и уже внутри MDIClient выполняют поиск окна по его заголовку или классу.
  3. Внутренний менеджер компоновки: Следует учитывать, что платформа 1С непрерывно отслеживает положение своих элементов интерфейса. При изменении размеров или положения окна через SetWindowPos новые параметры могут сброситься при перерисовке формы, изменении размеров главного окна или переключении страниц.
← На главную