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