Как реализовать программный отбор в дереве значений на обычных формах 1С?

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

Разработка интерфейсных механизмов в Обычных формах 1С часто вызывает трудности у начинающих специалистов. Типичный пример — необходимость сделать быстрый отбор в табличной части по выбранному значению. Если в Управляемых формах для Динамических списков предусмотрен мощный стандартный механизм отборов, то в Обычных формах при работе с коллекцией ДеревоЗначений разработчики сталкиваются с отсутствием встроенного интерактивного отбора. Попытка обратиться к свойству Отбор напрямую вызывает ошибку "Поле объекта не обнаружено".

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

В чем сложность фильтрации Дерева значений?

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

Рассмотрим рабочие сценарии решения этой задачи.

Способ 1. Программный поиск строк и перестроение дерева (Рекомендуемый)

Этот способ основан на использовании метода НайтиСтроки(). Идея заключается в том, что мы храним полную копию дерева значений в памяти (в переменной модуля или реквизите формы), а на форме отображаем результат фильтрации. При активации отбора мы очищаем дерево на форме, находим нужные строки в исходном дереве и программно воссоздаем структуру.

Разберем по шагам реализацию этого алгоритма:

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

// При заполнении основных данных формы сохраняем копию дерева
ДеревоПолное = ДеревоЗаказы.Скопировать();
  1. Реализуем процедуру фильтрации, которая будет вызываться при нажатии на кнопку отбора:

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

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

Этот метод гарантирует, что дерево на форме отобразит только те элементы, которые соответствуют условию, сохранив при этом их полную иерархическую цепочку до самого верхнего уровня.

Способ 2. Рекурсивное удаление нежелательных строк (In-place фильтрация)

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

Давайте проанализируем этот алгоритм:


Процедура ПрименитьФильтрРекурсивно()
    
    // Сначала восстанавливаем полное дерево из эталона
    ДеревоЗаказы = ДеревоПолное.Скопировать();
    
    ЗначениеПоиска = "Шкаф"; // Пример искомого значения в колонке "Состав"
    
    // Запускаем рекурсивную очистку дерева с верхнего уровня
    ФильтроватьВеткуДерева(ДеревоЗаказы.Строки, ЗначениеПоиска);
    
КонецПроцедуры

Функция ФильтроватьВеткуДерева(СтрокиДерева, ЗначениеПоиска)
    
    // Будем обходить строки с конца массива к началу, чтобы корректно работало удаление элементов в цикле
    Индекс = СтрокиДерева.Количество() - 1;
    Пока Индекс >= 0 Цикл
        ТекущаяСтрока = СтрокиДерева[Индекс];
        
        // Рекурсивно обрабатываем подчиненные строки текущей строки
        ДочерниеПодходят = ФильтроватьВеткуДерева(ТекущаяСтрока.Строки, ЗначениеПоиска);
        
        // Проверяем соответствие текущей строки условию поиска
        ТекущаяПодходит = (Найти(ВРег(ТекущаяСтрока.Состав), ВРег(ЗначениеПоиска)) > 0);
        
        // Если ни сама строка, ни ее подчиненные не подошли — удаляем строку
        Если Не (ТекущаяПодходит Или ДочерниеПодходят) Тогда
            СтрокиДерева.Удалить(ТекущаяСтрока);
        КонецЕсли;
        
        Индекс = Индекс - 1;
    КонецЦикла;
    
    // Возвращаем Истина, если в текущей ветке осталась хотя бы одна подходящая строка
    Возврат (СтрокиДерева.Количество() > 0);
    
КонецФункции

Важное преимущество метода: Обход снизу вверх гарантирует корректность удаления. Если у родителя-узла изначально не было подходящих дочерних элементов и сам он не подходил под фильтр, он будет полностью удален со всеми своими пустыми ветками.

Способ 3. Преобразование в Таблицу значений и использование Построителя запросов

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

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

Рассмотрим этот алгоритм по шагам:


Процедура ОтборЧерезПостроительЗапроса()
    
    // Шаг 1. Преобразуем иерархическое дерево в плоскую Таблицу Значений
    ТаблицаДанных = ПолучитьПлоскуюТаблицуИзДерева(ДеревоПолное);
    
    // Шаг 2. Настраиваем Построитель Запроса
    Построитель = Новый ПостроительЗапроса;
    Построитель.ИсточникДанных = Новый ОписаниеИсточникаДанных(ТаблицаДанных);
    
    // Накладываем отбор, например, по реквизиту "Номенклатура"
    НовыйОтбор = Построитель.Отбор.Добавить("Номенклатура");
    НовыйОтбор.Использование = Истина;
    НовыйОтбор.ВидСравнения = ВидСравнения.Равно;
    НовыйОтбор.Значение = ВыбраннаяНоменклатура;
    
    Построитель.Выполнить();
    
    // Шаг 3. Выгружаем отфильтрованный результат обратно в дерево с восстановлением иерархии
    // Для этого в построителе должны быть предварительно настроены группировки
    ДеревоЗаказы = Построитель.Результат.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
    
КонецПроцедуры

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

Подведем итоги

Отсутствие встроенного свойства Отбор у коллекции ДеревоЗначений в обычных формах 1С — не преграда для создания удобного интерфейса. Для решения этой задачи достаточно применить один из рассмотренных подходов:

← На главную