Как реализовать интерактивное сворачиваемое дерево в табличном документе 1С?

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

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

В этой статье мы подробно рассмотрим, почему стандартные средства не всегда подходят для таких задач, и предложим два основных подхода к реализации интерактивного сворачиваемого дерева в 1С: программную перерисовку табличного документа и использование поля HTML-документа.

Почему стандартные группировки табличного документа 1С не подходят для сложных интерактивных деревьев?

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

1С:Предприятие предоставляет ряд программных методов для работы с группировками в табличном документе, которые позволяют управлять сворачиванием и разворачиванием строк и колонок. Основной способ создания группировок при программном формировании документа — это использование метода ТабличныйДокумент.Вывести() с параметром Уровень.

  1. Параметр Уровень при выводе: Когда мы используем метод Вывести() для вывода области макета, мы можем указать параметр Уровень. Этот параметр задает уровень вложенности выводимых строк, что позволяет системе автоматически формировать группы. Дополнительно можно указать параметр Открыта, который определяет, будет ли созданная группа изначально свернута (Ложь) или развернута (Истина). Например:
  2. 
    ТабДок.Вывести(ОбластьЗаголовок, 0, Истина); // Высший уровень
    ТабДок.Вывести(ОбластьГруппа1, 1, Истина);  // Первый уровень вложенности
    ТабДок.Вывести(ОбластьЭлемент1, 2, Ложь);  // Второй уровень, свернут
    

    Это позволяет создавать иерархическую структуру, где пользователь может интерактивно сворачивать и разворачивать группы с помощью стандартных кнопок "+"/"-" в заголовках строк.

  3. Методы управления группировками: Для программного управления уже созданными группировками используются методы:
    • ТабличныйДокумент.ПоказатьУровеньГруппировокСтрок(Уровень): Этот метод позволяет свернуть или развернуть все группировки строк до указанного уровня. Если требуется свернуть все группировки, кроме верхнего уровня, мы можем вызвать этот метод в цикле, последовательно скрывая все уровни до необходимого.
    • ТабличныйДокумент.ПоказатьУровеньГруппировокКолонок(Уровень): Аналогичный метод для управления группировками колонок.

    Например, чтобы свернуть все группировки, начиная со второго уровня:

    
    Для Ур = 2 По МаксимальныйУровеньГруппировки Цикл
        ТабДок.ПоказатьУровеньГруппировокСтрок(Ур, Ложь);
    КонецЦикла;
    
  4. Явное определение групп: Помимо автоматической группировки по уровню, мы можем использовать методы НачатьАвтогруппировкуСтрок() и ЗакончитьАвтогруппировкуСтрок(), а также более явные НачатьГруппуСтрок() и ЗакончитьГруппуСтрок() для формирования иерархических групп.

Ограничения стандартного подхода:

Несмотря на наличие этих инструментов, они обладают одним критически важным ограничением, которое не позволяет реализовать желаемую автором функциональность: ПоказатьУровеньГруппировокСтрок() воздействует на все группировки указанного уровня. Если нам требуется свернуть только определенную, индивидуальную группу без затрагивания других групп того же уровня, или же если мы хотим реализовать "сворачивание нескольких ячеек по разным столбцам" (что подразумевает частичное свертывание, а не свертывание целой строки или группы строк), то прямых встроенных механизмов для такого "частичного" сворачивания именно одной ячейки или небольшого набора ячеек, которые не являются полной группой на уровне, табличный документ не предоставляет. Он работает с целыми строками или колонками как с единицами группировки.

Попытки группировки нескольких ячеек в одну линию через объединение ячеек (Область.Объединить()) и последующее скрытие этой области (Область.Видимость = Ложь) приведут к именно скрытию, а не интерактивному сворачиванию с кнопкой "+"/"-", как у полноценных групп. Более того, при скрытии одной области могут "поехать" другие, что требует сложного управления видимостью и перерасчетом макета вручную.

Именно из-за этих ограничений для реализации сложных интерактивных деревьев с произвольным сворачиванием нам приходится прибегать к альтернативным подходам.

Решение 1: Программная перерисовка табличного документа при каждом изменении

Этот подход является наиболее распространенным, когда необходимо реализовать нестандартное интерактивное поведение в табличном документе 1С, которое не обеспечивается штатными механизмами группировки. Суть метода заключается в том, что при каждом действии пользователя, которое должно привести к изменению видимости элементов (например, нажатие на кнопку "свернуть/развернуть"), мы полностью переформировываем и заново выводим весь табличный документ, учитывая новое состояние элементов.

Разберем этот подход по шагам:

  1. Подготовка структуры данных дерева:

    В основе этого решения лежит древовидная структура данных, которую мы будем отображать. Для этого идеально подходит объект ДеревоЗначений или коллекция объектов, каждый из которых содержит информацию о своем родителе и дочерних элементах. В дополнение к данным, отображаемым в дереве, каждый узел должен хранить состояние своей видимости (например, "свернут" или "развернут") и уровень вложенности.

    
    // Пример структуры ДереваЗначений для хранения информации
    // Добавим колонки для уровня и состояния свертки
    Дерево = Новый ДеревоЗначений;
    Дерево.Колонки.Добавить("Наименование");
    Дерево.Колонки.Добавить("Уровень", Новый ОписаниеТипов("Число"));
    Дерево.Колонки.Добавить("Свернут", Новый ОписаниеТипов("Булево"));
    Дерево.Колонки.Добавить("Ссылка", Новый ОписаниеТипов("Строка")); // Для расшифровки
    
    // Добавление корневых элементов
    НоваяСтрока = Дерево.Строки.Добавить();
    НоваяСтрока.Наименование = "Подразделение 1";
    НоваяСтрока.Уровень = 0;
    НоваяСтрока.Свернут = Ложь;
    НоваяСтрока.Ссылка = "П1";
    
    // Добавление дочерних элементов
    НоваяСтрокаДочерняя = НоваяСтрока.Строки.Добавить();
    НоваяСтрокаДочерняя.Наименование = "Подразделение 1.1";
    НоваяСтрокаДочерняя.Уровень = 1;
    НоваяСтрокаДочерняя.Свернут = Ложь;
    НоваяСтрокаДочерняя.Ссылка = "П11";
    

    Ссылка в данном случае будет использоваться для уникальной идентификации элемента при обработке расшифровки.

  2. Создание макета табличного документа:

    Разработаем макет табличного документа, который будет включать области для каждого типа элемента дерева. Например, у нас могут быть области для заголовков, для элементов с дочерними элементами (с кнопкой "+/-") и для конечных элементов. Важно предусмотреть возможность вывода отступа для имитации иерархии. Это можно сделать, объединяя несколько пустых ячеек перед основной ячейкой с текстом или используя параметр ОтступСлева, если он поддерживается для соответствующей ячейки.

    В макете обязательно должны быть ячейки, которые будут выступать в роли кнопок свертывания/развертывания (например, ячейка с текстом "+" или "-"). Для этих ячеек мы должны установить параметр Заполнение = "Расшифровка" и указать имя параметра расшифровки, например, РасшифровкаДерева.

  3. Рекурсивный алгоритм вывода дерева:

    При формировании табличного документа мы используем рекурсивный алгоритм для обхода нашей структуры данных (ДеревоЗначений). Этот алгоритм будет выводить каждый элемент дерева, учитывая его уровень вложенности и состояние свертки.

    
    // Процедура формирования табличного документа
    Процедура СформироватьТабличныйДокумент()
    
        ТабДок.Очистить(); // Очищаем документ перед перерисовкой
        // Получаем данные дерева (предполагаем, что ДеревоЗначений уже сформировано и находится в переменной Дерево)
                
        // Вывод заголовков (если есть)
        // ТабДок.Вывести(Макет.ПолучитьОбласть("Заголовок"));
    
        ВывестиУзлыДерева(Дерево.Строки, 0); // Начинаем с корневых строк и уровня 0
                
        // Прокрутить в начало, установить область выделения и т.д.
    КонецПроцедуры
    
    // Рекурсивная процедура для вывода узлов дерева
    Процедура ВывестиУзлыДерева(СтрокиДерева, ТекущийУровень, РодительСвернут = Ложь)
    
        Для Каждого СтрокаДерева Из СтрокиДерева Цикл
            Если РодительСвернут Тогда
                // Если родитель свернут, то все его потомки невидимы
                Продолжить; 
            КонецЕсли;
    
            // Определяем, есть ли у текущей строки дочерние элементы
            ЕстьДочерние = СтрокаДерева.Строки.Количество() > 0;
    
            // Получаем область макета (например, "СтрокаДерева" или "СтрокаГруппа", "СтрокаЭлемент")
            Область = Макет.ПолучитьОбласть(ЕстьДочерние ? "ОбластьГруппа" : "ОбластьЭлемент");
    
            // Устанавливаем отступ для имитации иерархии
            // В макете нужно предусмотреть ячейку Отступ
            Если Область.Параметры.Свойство("Отступ") Тогда
                Область.Параметры.Отступ = ТекущийУровень * 3; // Например, 3 символа на каждый уровень
            КонецЕсли;
                    
            // Заполняем параметры области
            Область.Параметры.Наименование = СтрокаДерева.Наименование;
            Область.Параметры.ИконкаСвернутьРазвернуть = СтрокаДерева.Свернут ? "+" : "-"; // Текст кнопки
    
            // Для ячейки-кнопки
            Если Область.Области.Свойство("ЯчейкаКнопки") Тогда
                Область.Области.ЯчейкаКнопки.Расшифровка = "ДеревоРасшифровка";
                Область.Области.ЯчейкаКнопки.ПараметрРасшифровки = СтрокаДерева.Ссылка;
            КонецЕсли;
    
            ТабДок.Вывести(Область);
    
            // Если есть дочерние элементы и текущая строка не свернута, рекурсивно вызываем для дочерних
            Если ЕстьДочерние Тогда
                ВывестиУзлыДерева(СтрокаДерева.Строки, ТекущийУровень + 1, СтрокаДерева.Свернут);
            КонецЕсли;
    
        КонецЦикла;
    
    КонецПроцедуры
    

    Важный момент: для обеспечения корректной работы свертывания/развертывания дочерних элементов, мы передаем параметр РодительСвернут в рекурсивный вызов. Если родитель свернут, то его дочерние элементы не будут выводиться вовсе, что и создает эффект свертывания.

  4. Обработка нажатий пользователя (Расшифровка):

    Когда пользователь нажимает на ячейку-кнопку свертывания/развертывания, возникает событие ТабличныйДокумент.ОбработкаРасшифровки() на форме, где размещен табличный документ. В этом событии мы получаем параметр расшифровки (ПараметрРасшифровки), который мы ранее задали для кнопки.

    
    // Обработчик события расшифровки табличного документа
    Процедура ТабличныйДокументОбработкаРасшифровки(Элемент, Расшифровка, СтандартнаяОбработка)
        // Отключаем стандартную обработку, чтобы наша логика сработала
        СтандартнаяОбработка = Ложь;
    
        Если Расшифровка.Свойство("ПараметрРасшифровки") Тогда
            ИдентификаторЭлемента = Расшифровка.ПараметрРасшифровки;
            // Находим соответствующий элемент в нашем ДеревеЗначений по его ИдентификаторуЭлемента
            СтрокаДерева = НайтиЭлементДереваПоСсылке(Дерево, ИдентификаторЭлемента);
    
            Если СтрокаДерева <> Неопределено Тогда
                СтрокаДерева.Свернут = НЕ СтрокаДерева.Свернут; // Инвертируем состояние
                СформироватьТабличныйДокумент(); // Заново перерисовываем весь документ
            КонецЕсли;
        КонецЕсли;
    КонецПроцедуры
    
    // Вспомогательная функция для поиска элемента в дереве
    Функция НайтиЭлементДереваПоСсылке(Дерево, СсылкаЭлемента)
        Для Каждого СтрокаДерева Из Дерево.Строки Цикл
            Если СтрокаДерева.Ссылка = СсылкаЭлемента Тогда
                Возврат СтрокаДерева;
            КонецЕсли;
            // Рекурсивный поиск в дочерних строках
            Результат = НайтиЭлементДереваПоСсылке(СтрокаДерева, СсылкаЭлемента);
            Если Результат <> Неопределено Тогда
                Возврат Результат;
            КонецЕсли;
        КонецЦикла;
        Возврат Неопределено;
    КонецФункции
    

    В этой процедуре мы: находим узел дерева по переданному идентификатору, инвертируем его состояние свертывания/развертывания (Свернут = НЕ Свернут), а затем повторно вызываем процедуру формирования всего табличного документа. Это приводит к полной перерисовке документа с учетом нового состояния свернутого элемента.

Преимущества решения с перерисовкой:

Недостатки решения с перерисовкой:

Решение 2: Использование поля HTML-документа

Если требования к интерактивности, скорости отклика, анимации или сложности визуального оформления превышают возможности табличного документа, а также если вы готовы использовать веб-технологии, то ПолеHTMLДокумента на форме 1С может стать мощным инструментом. Этот элемент позволяет встроить в форму полноценную HTML-страницу, которая может быть дополнена стилями CSS и логикой JavaScript.

Рассмотрим процесс реализации с использованием ПолеHTMLДокумента:

  1. Размещение элемента на форме:

    Начнем с добавления на управляемую форму элемента ПолеHTMLДокумента. Перетащите его из палитры элементов формы или создайте программно. При этом важно указать имя элемента, например, ПолеHTMLДокументаСвертки.

  2. Генерация HTML-структуры дерева:

    На стороне 1С-кода мы динамически генерируем HTML-строку, которая представляет собой наше дерево. Для этого обычно используются стандартные HTML-теги для списков и иерархии, такие как <div>, <ul> (unordered list), <li> (list item).

    
    // Пример генерации HTML-структуры
    Функция СформироватьHTMLДерево(Дерево, ТекущийУровень = 0)
        HTML = "";
        Если ТекущийУровень = 0 Тогда
            HTML = "<ul class="tree">"; // Корневой список
        КонецЕсли;
    
        Для Каждодо СтрокаДерева Из Дерево.Строки Цикл
            HTML = HTML + "<li>";
            // Добавляем элемент управления сверткой
            Если СтрокаДерева.Строки.Количество() > 0 Тогда
                HTML = HTML + "<span class="toggle" data-id="" + СтрокаДерева.Ссылка + "">" + (СтрокаДерева.Свернут ? "+" : "-") + "</span>";
            Иначе
                HTML = HTML + "<span class="empty-toggle">  </span>"; // Для выравнивания
            КонецЕсли;
                    
            HTML = HTML + "<span class="item-name">" + СтрокаДерева.Наименование + "</span>";
    
            Если СтрокаДерева.Строки.Количество() > 0 Тогда
                // Дочерние элементы помещаем в div, который будем скрывать/показывать
                HTML = HTML + "<div class="children" style="display: " + (СтрокаДерева.Свернут ? "none" : "block") + ";">";
                HTML = HTML + СформироватьHTMLДерево(СтрокаДерева, ТекущийУровень + 1); // Рекурсивный вызов
                HTML = HTML + "</div>";
            КонецЕсли;
            HTML = HTML + "</li>";
        КонецЦикла;
    
        Если ТекущийУровень = 0 Тогда
            HTML = HTML + "</ul>";
        КонецЕсли;
        Возврат HTML;
    КонецФункции
    
    // Основная процедура для установки HTML в поле
    Процедура УстановитьHTMLВПоле()
        // ДеревоЗначений должно быть подготовлено ранее
        HTMLДокумент = СформироватьHTMLДерево(Дерево);
        Элементы.ПолеHTMLДокументаСвертки.УстановитьТекст(HTMLДокумент);
    КонецПроцедуры
    

    Обратите внимание на использование атрибута data-id, который позволит нам идентифицировать элемент при обработке кликов в JavaScript.

  3. Стилизация с помощью CSS:

    Для придания дереву желаемого вида (отступы, шрифты, цвета, стили кнопок) используем CSS. CSS можно встроить непосредственно в генерируемый HTML-код (<style>...</style>) или передать как отдельный файл.

    
    // Пример CSS для дерева
    CSSСтиль = "
    <style>
    .tree ul {
        list-style-type: none;
        padding-left: 20px;
    }
    .tree li {
        margin: 5px 0;
        cursor: pointer;
    }
    .toggle {
        display: inline-block;
        width: 15px;
        text-align: center;
        font-weight: bold;
        color: blue;
    }
    .empty-toggle {
        display: inline-block;
        width: 15px;
        text-align: center;
    }
    .item-name {
        margin-left: 5px;
    }
    .children {
        margin-left: 15px;
        border-left: 1px dashed #ccc;
        padding-left: 5px;
    }
    </style>
    ";
    // ...вставляем CSSСтиль перед HTMLДокумент при формировании
    Элементы.ПолеHTMLДокументаСвертки.УстановитьТекст(CSSСтиль + HTMLДокумент);
    
  4. Интерактивность с JavaScript:

    Для реализации логики свертывания/развертывания понадобится JavaScript. Мы добавим обработчики событий (например, click) на наши кнопки-переключатели (элементы с классом toggle). При клике JavaScript будет изменять стиль дочерних элементов (например, свойство display на none или block), а также менять текст кнопки на "+" или "-".

    
    // Пример JavaScript для управления сверткой/разверткой
    JSКод = "
    <script>
    document.addEventListener('DOMContentLoaded', function() {
        var toggles = document.querySelectorAll('.toggle');
        toggles.forEach(function(toggle) {
            toggle.addEventListener('click', function() {
                var childrenContainer = this.nextElementSibling.nextElementSibling; // div.children
                if (childrenContainer && childrenContainer.classList.contains('children')) {
                    if (childrenContainer.style.display === 'none') {
                        childrenContainer.style.display = 'block';
                        this.textContent = '-';
                        // Оповестить 1С, что элемент развернут
                        if (window.external && window.external.callScript) {
                            window.external.callScript('OnToggle', [this.dataset.id, 'expanded']);
                        }
                    } else {
                        childrenContainer.style.display = 'none';
                        this.textContent = '+';
                        // Оповестить 1С, что элемент свернут
                        if (window.external && window.external.callScript) {
                            window.external.callScript('OnToggle', [this.dataset.id, 'collapsed']);
                        }
                    }
                }
            });
        });
    });
    </script>
    ";
    // ...вставляем JSКод после HTMLДокумент
    Элементы.ПолеHTMLДокументаСвертки.УстановитьТекст(CSSСтиль + HTMLДокумент + JSКод);
    

    В этом примере JavaScript изменяет видимость дочернего элемента путем манипуляции со стилем display. Также мы используем window.external.callScript для возможности взаимодействия с кодом 1С, передавая идентификатор элемента и его новое состояние.

    Для более сложных визуализаций и анимаций можно использовать специализированные JavaScript-библиотеки, такие как jQuery или PixiJS (как упоминалось в исходной дискуссии), но это значительно увеличивает сложность реализации и требуемые компетенции.

  5. Взаимодействие с кодом 1С:

    Для обратного взаимодействия – то есть, если JavaScript должен передать какую-либо информацию в код 1С (например, о том, какой элемент был свернут/развернут), мы можем использовать событие ПриНажатииНаПолеHTMLДокумента на форме. Этот обработчик срабатывает при вызове JavaScript-функций window.external.callScript() или window.external.callFunction().

    
    // Обработчик события ПриНажатииНаПолеHTMLДокумента
    Процедура ПолеHTMLДокументаСверткиПриНажатииНаПолеHTMLДокумента(Элемент, ИмяДействия, Параметры)
        Если ИмяДействия = "OnToggle" Тогда
            ИдентификаторЭлемента = Параметры[0];
            Состояние = Параметры[1];
            // Здесь можно обновить состояние в нашем ДеревеЗначений
            // или выполнить другую логику на стороне 1С
            Сообщить("Элемент " + ИдентификаторЭлемента + " был " + Состояние);
        КонецЕсли;
    КонецПроцедуры
    

    Такой подход позволяет нам сохранять состояние элементов дерева в данных 1С, даже если визуальное представление управляется JavaScript.

Преимущества решения с ПолеHTMLДокумента:

Недостатки решения с ПолеHTMLДокумента:

Заключение

Как мы видим, реализация сложного интерактивного сворачиваемого дерева в табличном документе 1С — это задача, требующая нестандартных подходов. Стандартные механизмы группировки в ТабличномДокументе эффективны для простых иерархий, но не справляются с требованиями по частичному сворачиванию, произвольному расположению элементов и высокой интерактивности.

Если ваша задача жестко привязана к использованию ТабличногоДокумента, то программная перерисовка всего документа при каждом изменении состояния свертки является рабочим, хоть и ресурсоемким решением. Этот метод предоставляет полный контроль над внешним видом и логикой, но требует значительных усилий по написанию кода и может страдать от проблем с производительностью на больших объемах данных.

Для более гибких, интерактивных и визуально привлекательных решений, особенно если требования к UI высоки, стоит рассмотреть использование ПоляHTMLДокумента в связке с CSS и JavaScript. Этот подход открывает доступ к мощным веб-технологиям, но требует соответствующих компетенций и готовности работать с внешней для 1С средой.

Выбор между этими подходами зависит от конкретных требований вашего проекта, имеющихся ресурсов и компетенций команды разработки. В любом случае, мы не рекомендуем пытаться "выжать" из стандартных группировок табличного документа то, для чего они не предназначены, так как это приведет к избыточной сложности и нестабильности решения.

← На главную