Хочу поделиться с вами практикой динамического формирования формы на примере создания кнопочной формы подбора справочника.
При открытии формы будет происходить расчет количества элементов, которые можно уместить в виде кнопок на развернутой форме без прокрутки.
Для простоты зададим фиксированный размер кнопок: ширина - 14 пункта, высота - 3 пункта. В общем случае можно дать возможность пользователям варьировать эти размеры.
Итак, начнем. Нам понадобится форма со следующим наполнением:
Начальное состояние формы
Реквизиты формы
Иерархический - тип "Булево", признак, является ли справочник, из которого выбираем, иерархическим (пока не поддерживаются справочники с иерархией элементов)
КнопокПоВертикали - тип "Число", количество кнопок на форме по вертикали
КнопокПоГоризонтали - тип "Число", количество кнопок на форме по горизонтали
ВсегоСтраниц - на сколько страниц необходимо разбить список ссылок для выбора
ИмяСправочника - имя справочника, как оно задано в конфигураторе
КэшЭлементов - тип "ТаблицаЗначений", таблица для хранения элементов в выбранной группе справочника
Ссылка - тип "СправочникСсылка", ссылка на элемент справочника
Представление - тип "Строка", строковое представление ссылки, выводится на самой кнопке
СоответствияКнопок - тип "ТаблицаЗначений", таблица для хранения соответствий имен кнопок ссылкам справочника
ИмяКнопки - тип "Строка", имя программно добавленной кнопки
Ссылка - тип "СправочникСсылка", ссылка на справочник, с которой связана кнопка
ТекущаяСтраница - тип "Число", номер отображаемой страницы, необходим для определения списка ссылок на вывод
ТекущийРодитель - тип "СправочникСсылка", группа справочника, элементы которой отображаются на форме. При первом выводе всегда содержит пустую ссылку
ТоваровГруппа - тип "Число", количество ссылок в текущей группе справочника, необходимо для расчета количества страниц
Команды формы
ВыбратьСсылку - назначается в качестве команды добавляемых кнопок
ЗакрытьПодбор - закрывает форму без выбора ссылки
Наверх - выполняет переход на уровень вверх для иерархических справочников
ПредыдущаяСтраница и СледующаяСтраница - команды для переключения страниц
Шаблон формы
В итоге получаем некий начальный шаблон формы:
Итак, при создании формы в общем случае заполняем список выбора элемента "Имя справочника" по метаданным конфигурации:
Листинг "ПриСозданииНаСервере"
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
//Формируем список выбора справочника
Для Каждого Справочник Из Метаданные.Справочники Цикл
Элементы.ИмяСправочника.СписокВыбора.Добавить(Справочник.Имя, Справочник.Синоним);
КонецЦикла;
КонецПроцедуры
и непосредственно при открытии делаем расчет количества кнопок, исходя из разрешения экрана (считаем, что форма развернута на весь экран). Коэффициенты для расчета получены обычным подбором
&НаКлиенте
Процедура ПриОткрытии(Отказ)
РассчитатьРазмерыСтраницы();
КонецПроцедуры
Процедура РассчитатьРазмерыСтраницы()
ПараметрыЭкрана = ПолучитьИнформациюЭкрановКлиента()[0];
ШиринаВПикселях = ПараметрыЭкрана.Ширина;
ВысотаВПикселях = ПараметрыЭкрана.Высота;
КнопокПоГоризонтали = Цел(ШиринаВПикселях/192); //рассчитано опытным путем
КнопокПоВертикали = Цел(ВысотаВПикселях/108); //рассчитано опытным путем
КонецПроцедуры
Переходим непосредственно к выбору имени справочника и добавлению кнопок на форму. У элемента формы "Имя справочника" добавляем обработчик при изменении
Листинг "ПриИзменении" элемента "ИмяСправочника"
&НаКлиенте
Процедура ИмяСправочникаПриИзменении(Элемент)
Отказ = Ложь;
Если ПустаяСтрока(ИмяСправочника) Тогда
Возврат;
КонецЕсли;
ОбновитьПризнакИерархический(Отказ);
Если Отказ Тогда
Возврат;
КонецЕсли;
ОбновитьКнопки(Истина);
КонецПроцедуры
Первым этапом мы проверяем, а заполнили мы вообще имя справочника. Если нет - прерываем дальнейшую обработку. Если заполнили - вызываем процедуру "ОбновитьПризнакИерархический":
Листинг серверной процедуры "ОбновитьПризнакИерархический"
&НаСервере
Процедура ОбновитьПризнакИерархический(Отказ)
Если Метаданные.Справочники.Найти(ИмяСправочника) = Неопределено Тогда
Текст = "Справочник с именем """+ИмяСправочника+""" не найден!";
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(Текст,,,,Отказ);
Возврат;
КонецЕсли;
Иерархический = Метаданные.Справочники[ИмяСправочника].Иерархический;
Если Метаданные.Справочники[ИмяСправочника].ВидИерархии = Метаданные.СвойстваОбъектов.ВидИерархии.ИерархияЭлементов Тогда
Текст = "Справочники с иерархией элементов не поддерживаются!";
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(Текст,,,,Отказ);
Возврат;
КонецЕсли;
ТекущийРодитель = Справочники[ИмяСправочника].ПустаяСсылка();
КонецПроцедуры
В этой процедуре во избежании ошибок проверяем, есть ли вообще справочник с таким именем в составе конфигурации. Если есть, проверяем, какой вид иерархии у него задан: в случае, если вид иерархии "Иерархия элементов" - выдаем сообщение и прерываем выполнение кода. Ну и, собственно, инициализируем признак "Иерархический" и реквизит формы "ТекущийРодитель": в первую очередь нам нужно показать элементы корневой группы справочника.
После инициализации основных настроек приступаем к добавлению кнопок. Помним, что изменение элементов формы доступно только на сервере. Кнопки будем добавлять в интерактивно добавленную в конфигураторе группу "ГруппаКнопки" с вертикальным расположением подчиненных элементов. Для управления построчным выводом элементов будем добавлять группы с уже горизонтальным расположением элементов: как только количество кнопок в горизонтальной группе станет равным максимальному числу кнопок по горизонтали (реквизит "КнопокПоГоризонтали"), создаем новую группу. Начнем разбор процедуры вывода:
Листинг серверной процедуры "ОбновитьКнопки"
&НаСервере
Процедура ОбновитьКнопки(НужноПерекешировать)
СоответствияКнопок.Очистить();
//Удаляем все кнопки
МассивДляУдаления = ПолучитьДобавленныеЭлементы(ЭтотОбъект);
Для Каждого ТекЭлемент Из МассивДляУдаления Цикл
Элементы.Удалить(ТекЭлемент);
КонецЦикла;
//Если изменены настройки, требующие обновления списка
//то переделываем запрос и снова получаем данные
Если НужноПерекешировать Тогда
ЗакешироватьЭлементы();
КонецЕсли;
Итератор = 1; //для имен кнопок
КоличествоВГруппе = 0; //для контроля кнопок в ряду
НомерТекущейГруппы = 0; //для имен групп, в которых размещаем кнопки
ГруппаКнопки = Элементы.ГруппаКнопки; //корневая группа с вертикальной группировкой элементов
КнопокНаСтранице = КнопокПоВертикали*КнопокПоГоризонтали;
Для К = (1+КнопокНаСтранице*(ТекущаяСтраница-1)) По ТекущаяСтраница*КнопокНаСтранице Цикл
Если К > КэшЭлементов.Количество() Тогда
Прервать;
КонецЕсли;
Выборка = КэшЭлементов[К-1];
Если КоличествоВГруппе = 0 Или КоличествоВГруппе = КнопокПоГоризонтали Тогда
//добавляется первая кнопка или достигнут лимит кнопок в ряду
НомерТекущейГруппы = НомерТекущейГруппы+1;
ТекущаяГруппа = ДобавитьГруппу(ЭтотОбъект, НомерТекущейГруппы, ГруппаКнопки);
КоличествоВГруппе = 0;
КонецЕсли;
//Готовим структуру параметров для добавления кнопки
Стр = Новый Структура("Имя, ТекущаяГруппа, Строка, ВысотаКнопок, ШиринаКнопок, Иерархический");
Стр.Имя = "Кнопка"+Формат(Итератор, "ЧГ=");
Стр.ТекущаяГруппа = ТекущаяГруппа;
Стр.Строка = Выборка;
Стр.ВысотаКнопок = 3;
Стр.ШиринаКнопок = 14;
Стр.Иерархический = Иерархический;
//Добавляем кнопку
ДобавитьКнопку(ЭтотОбъект, Стр);
Итератор = Итератор+1;
КоличествоВГруппе = КоличествоВГруппе + 1;
КонецЦикла;
Пом_ОбновитьКнопкиСтраниц();
Элементы.НадписьСтраница.Заголовок = "Стр. "+ТекущаяСтраница+" из "+ВсегоСтраниц;
КонецПроцедуры
На первом шаге очищаем таблицу соответствий кнопок ссылкам справочника, получаем массив групп с горизонтальной группировкой ("ГруппаКнопокХ", где Х - порядковый номер группы) и удаляем с формы эти группы вместе с кнопками.
Листинг функции "ПолучитьДобавленныеЭлементы"
//Возвращает массив программно добавленных элементов формы
//Форма - форма, на которой ищем элементы
Функция ПолучитьДобавленныеЭлементы(Форма)
Массив = Новый Массив;
Для Каждого Элемент Из Форма.Элементы Цикл
Если ТипЗнч(Элемент) = Тип("ГруппаФормы") И
СтрНайти(Элемент.Имя, "ГруппаКнопок") > 0 Тогда
Массив.Добавить(Элемент);
КонецЕсли;
КонецЦикла;
Возврат Массив;
КонецФункции
Если процедура вызывается впервые или пользователь выбрал другую группу справочника, то нам необходимо обновить кэш элементов, которые будем выводить. Запрос, которым будем выбирать элементы для иерархического справочника будет состоять из 2 пакетов: первым пакетом выбираем группы, вторым - элементы. Каждый пакет сортируем по алфавиту. Это необходимо для того, чтобы сначала выводить группы, а потом уже элементы. После заполнения таблицы "КэшЭлементов" получаем общее количество ссылок и рассчитываем количество страниц в зависимости от количества кнопок по горизонтали и вертикали с округлением в большую сторону: ОбщееКоличствоСсылок/КоличествоКнопокНаСтранице
Листинг серверной процедуры "ЗакешироватьЭлементы"
//Обновляет кэш ссылок, которые нужно отобразить
&НаСервере
Процедура ЗакешироватьЭлементы()
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("Родитель", ТекущийРодитель);
Если Иерархический Тогда
ТекстГруппы = "ВЫБРАТЬ
| "+ИмяСправочника+".Ссылка КАК Ссылка,
| Представление("+ИмяСправочника+".Ссылка) КАК Представление
|ИЗ
| Справочник."+ИмяСправочника+" КАК "+ИмяСправочника+"
|ГДЕ
| "+ИмяСправочника+".ЭтоГруппа
| И "+ИмяСправочника+".Родитель = &Родитель
|
|УПОРЯДОЧИТЬ ПО
| "+ИмяСправочника+".Ссылка
|;
|
|////////////////////////////////////////////////////////////////////////////////
|";
Иначе
ТекстГруппы = "";
КонецЕсли;
ТекстЭлементы = "ВЫБРАТЬ
| "+ИмяСправочника+".Ссылка КАК Ссылка,
| Представление("+ИмяСправочника+".Ссылка) КАК Представление
|ИЗ
| Справочник."+ИмяСправочника+" КАК "+ИмяСправочника;
Если Иерархический Тогда
ТекстЭлементы = ТекстЭлементы+"
|ГДЕ
| НЕ "+ИмяСправочника+".ЭтоГруппа
| И "+ИмяСправочника+".Родитель = &Родитель
|";
КонецЕсли;
ТекстЭлементы = ТекстЭлементы+"
|УПОРЯДОЧИТЬ ПО
| "+ИмяСправочника+".Ссылка";
Запрос.Текст = ТекстГруппы+ТекстЭлементы;
Результат = Запрос.ВыполнитьПакет();
КэшЭлементов.Очистить();
Если Иерархический Тогда
Группы = Результат[0];
КэшЭлементов.Загрузить(Группы.Выгрузить()); //сначала выводим кнопки для групп...
Товары = Результат[1].Выбрать();
Иначе
Товары = Результат[0].Выбрать();
КонецЕсли;
Пока Товары.Следующий() Цикл
//...а потом выводим кнопки для товаров
ЗаполнитьЗначенияСвойств(КэшЭлементов.Добавить(), Товары);
КонецЦикла;
ТоваровГруппа = КэшЭлементов.Количество(); //общее количество элементов на вывод
ТекущаяСтраница = 1;
Деление = КэшЭлементов.Количество()/(КнопокПоВертикали*КнопокПоГоризонтали);
//Вычисляем количество страниц с округлением в большую сторону до целого числа
Если Цел(Деление) = Деление Тогда
ВсегоСтраниц = Деление;
Иначе
ВсегоСтраниц = Деление + 1;
КонецЕсли;
КонецПроцедуры
Если пользователь только меняет страницу нам уже не нужно снова получать список элементов запросом, только лишь вывести ссылки из подготовленной таблицы.
После обновления списка ссылок готовим итератор для формирования имен кнопок, подсчета кнопок в горизонтальных группах и итератор для формирования наименований горизонтальных групп
Итератор = 1; //для имен кнопок
КоличествоВГруппе = 0; //для контроля количества кнопок по горизонтали
НомерТекущейГруппы = 0; //для имен групп, в которых размещаем кнопки
Зная количество страниц и количество кнопок на 1 странице делаем расчет первой и последней строк из таблицы КэшЭлементов для вывода:
первая строка - (1+КнопокНаСтранице*(ТекущаяСтраница-1))
последняя строка - ТекущаяСтраница*КнопокНаСтранице
Первым делом проверяем: если горизонтальная группа заполнена или это первый шаг цикла, то нужно добавить новую группу для горизонтальной группировки кнопок:
Листинг функции "ДобавитьГруппу"
//Добавляет обычную группу на форму. Можно вынести в общий модуль
//Форма - Тип "Форма", на какую форму добавляем обычную группу
//НомерГруппы - Тип "Число", порядковый номер группы, нужен для формирования имени элемента формы
//Родитель - Тип "ГруппаФормы", родительская группа с вертикальным расположением элементов
&НаСервере
Функция ДобавитьГруппу(Форма, НомерГруппы, Родитель)
ИмяЭлемента = "ГруппаКнопок"+Формат(НомерГруппы, "ЧГ=");
ГруппаКнопки = Форма.Элементы.ГруппаКнопки;
НоваяГруппа = Форма.Элементы.Добавить(ИмяЭлемента, Тип("ГруппаФормы"), Родитель);
НоваяГруппа.Вид = ВидГруппыФормы.ОбычнаяГруппа;
НоваяГруппа.Группировка = ГруппировкаПодчиненныхЭлементовФормы.Горизонтальная;
НоваяГруппа.ОтображатьЗаголовок = Ложь;
НоваяГруппа.Отображение = ОтображениеОбычнойГруппы.Нет;
НоваяГруппа.Объединенная = Истина;
Возврат НоваяГруппа;
КонецФункции
Добавленную группу запоминаем в переменную "ТекущаяГруппа": в нее будем добавлять кнопки.
После этого готовим параметры кнопки и добавляем ее на форму:
Листинг процедуры "ДобавитьКнопку"
//Добавляет кнопку, соответствующую элементу справочника
//Форма - Тип "Форма". Форма, на которую добавляем кнопку
//Параметры - Тип "Структура", определяет параметры создания элемента формы:
// *ТекущаяГруппа - Тип "ГруппаФормы", группа с горизонтальным расположением элементов
// *Строка - Строка выборки из таблицы кэшированных элементов
// *ВысотаКнопок - Тип "Число", высота кнопки в пунктах
// *ШиринаКнопок - Тип "Число", ширина кнопки в пунктах
// *Имя - Тип "Строка", имя добавляемой кнопки
// *Иерархический - Тип "Булево", является ли текущий справочник иерархическим
&НаСервере
Процедура ДобавитьКнопку(Форма, Параметры) Экспорт
Имя = Параметры.Имя;
ТекущаяГруппа = Параметры.ТекущаяГруппа;
Выборка = Параметры.Строка;
Новая = Форма.Элементы.Добавить(Имя, Тип("КнопкаФормы"), ТекущаяГруппа);
Новая.Вид = ВидКнопкиФормы.ОбычнаяКнопка;
Новая.ИмяКоманды = "ВыбратьСсылку";
Новая.Высота = Параметры.ВысотаКнопок;
Новая.Ширина = Параметры.ШиринаКнопок;
Новая.Заголовок = Строка(Выборка.Представление);
Новая.ВысотаЗаголовка = Параметры.ВысотаКнопок;
Новая.РастягиватьПоГоризонтали = Истина;
Новая.РастягиватьПоВертикали = Ложь;
Новая.Шрифт = Новый Шрифт(Новая.Шрифт,,8);
Ссылка = Выборка.Ссылка;
Если Параметры.Иерархический Тогда
//Справочник иерархический
Если ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Ссылка, "ЭтоГруппа") Тогда
//для кнопок, соответствующих группам делаем фон желтым
Новая.ЦветФона = Новый Цвет(255, 204, 0);
Иначе
//При необходимости можно вывести под кнопку подсказку, например, о ценах и остатках
//Цены и остатки тогда лучше тоже кэшировать в таблицу "КэшЭлементов"
Подсказка = Новая.РасширеннаяПодсказка;
Подсказка.ВысотаЗаголовка = 1;
Подсказка.Заголовок = "Доп.информация";
Новая.ОтображениеПодсказки = ОтображениеПодсказки.ОтображатьСнизу;
КонецЕсли;
Иначе
//справочник без иерархии
Подсказка = Новая.РасширеннаяПодсказка;
Подсказка.ВысотаЗаголовка = 1;
Подсказка.Заголовок = "Доп.информация";
Новая.ОтображениеПодсказки = ОтображениеПодсказки.ОтображатьСнизу;
КонецЕсли;
//В зависимости от типы ссылки можно на кнопку добавить картинку.
//Ограничение - картинка должна быть из библиотеки картинок, внешние - нельзя
//Если ТипЗнч(Ссылка) = Тип("СправочникСсылка.Номенклатура") Тогда
// Новая.Картинка = КартинкаДляНоменклатуры(Ссылка);
//Иначе
// Новая.Картинка = КартинкаДляХарактеристики(Ссылка);
//КонецЕсли;
//Новая.Отображение = ОтображениеКнопки.КартинкаИТекст;
НовСоотв = Форма.СоответствияКнопок.Добавить();
НовСоотв.ИмяКнопки = Новая.Имя;
НовСоотв.Ссылка = Ссылка;
КонецПроцедуры
Из дополнительных возможностей вывода можно, например, на кнопку добавить картинку (например, если элемент помечен на удаление; если выбираем номенклатуру, то можно выводить картинку для товаров с характеристиками, для наборов и т.д.). Для вывода доп.информации здесь использовал возможности расширенной подсказки: организовал вывод расширенной подсказки под самой кнопкой: здесь можно, например, вывести цену и остатки для товара.
На каждом шаге цикла не забываем увеличить итераторы.
После вывода все ссылок обновляем доступность кнопок "Предыдущая страница" и "Следующая страница": на первой странице снимаем доступность с кнопки "Предыдущая страница", на последней - с кнопки "Следующая страница"
Листинг процедуры "Пом_ОбновитьКнопкиСтраниц"
Процедура Пом_ОбновитьКнопкиСтраниц()
Элементы.ПредыдущаяСтраница.Доступность = Не (ТекущаяСтраница = 1);
Элементы.СледующаяСтраница.Доступность = Не (ТекущаяСтраница = ВсегоСтраниц);
КонецПроцедуры
И в последнюю очередь выводим информацию о том, на какой странице находимся и сколько их всего:
Элементы.НадписьСтраница.Заголовок = "Стр. "+ТекущаяСтраница+" из "+ВсегоСтраниц;
На этом работа с изменением элементов формы почти закончилась, нам только осталось определить процедуру, которая будет выполняться при нажатии на кнопки.
Листинг процедуры "ВыбратьСсылку"
&НаКлиенте
Процедура ВыбратьСсылку(Команда)
Имя = ТекущийЭлемент.Имя;
ПоискТовара = Новый Структура("ИмяКнопки", Имя);
//Ищем ссылку из таблицы соответствий кнопок
Строка = СоответствияКнопок.НайтиСтроки(ПоискТовара)[0];
Ссылка = Строка.Ссылка;
Если Иерархический И ЭтоГруппа(Ссылка) Тогда
//Выбрали группу, меняем текущего родителя и обновляем список кнопок
ТекущийРодитель = Ссылка;
ОбновитьКнопки(Истина);
Возврат;
КонецЕсли;
//Закрываем форму с передачей выбранной ссылки
//Можно заменить на Оповестить и открывать форму с признаком "ЗакрыватьПриВыборе = Истина"
Закрыть(Ссылка);
КонецПроцедуры
Первым делом мы получаем имя текущей кнопки из свойства "ТекущийЭлемент" формы. Далее по имени кнопки ищем ссылку на справочник из таблицы "СоответствияКнопок". В случае, если выбранный элемент является группой
Листинг процедуры "ЭтоГруппа"
&НаСервереБезКонтекста
Функция ЭтоГруппа(Номенклатура)
Возврат ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Номенклатура, "ЭтоГруппа");
КонецФункции
то выполняем обновление формы, предварительно сформировав новый кэш по выбранной группе
Если выбрали элемент, то закрываем форму с параметром, где в качестве параметра указываем в самом простом случае выбранную ссылку. Как альтернативный вариант (например, можно использовать в РМК в 1С:Розница"), можно использовать метод "Оповестить".
Ну и напоследок осталось определить процедуры для команд формы "Наверх", "Предыдущая/Следующая страница" и "Закрыть подбор"
Листинг процедур команд формы
&НаКлиенте
Процедура Наверх(Команда)
Если Иерархический Тогда
//Переходим на уровень вверх
НаверхНаСервере();
КонецЕсли;
КонецПроцедуры
&НаСервере
Процедура НаверхНаСервере()
ТекущийРодитель = ТекущийРодитель.Родитель;
ОбновитьКнопки(Истина);
КонецПроцедуры
&НаКлиенте
Процедура ПредыдущаяСтраница(Команда)
ТекущаяСтраница = ТекущаяСтраница - 1;
//Обновлеям список кнопок без кэширования
ОбновитьКнопки(Ложь);
КонецПроцедуры
&НаКлиенте
Процедура СледующаяСтраница(Команда)
ТекущаяСтраница = ТекущаяСтраница + 1;
//Обновлеям список кнопок без кэширования
ОбновитьКнопки(Ложь);
КонецПроцедуры
&НаКлиенте
Процедура ЗакрытьПодбор(Команда)
Закрыть(Неопределено);
КонецПроцедуры
Данная обработка должна работать на всех конфигурациях на управляемых формах на основе БСП. Из последней используются процедура "ОбщегоНазначенияКлиентСервер.СообщитьПользователю" и функция "ОбщегоНазначения.ЗначениеРеквизитаОбъекта", так что переделать для самописной конфы не составит труда.
Тестирование проводилось под платформами 8.3.11.3034 и 8.3.12.1529 в режиме совместимости конфигураций 8.3.10 (Розница, УТ, самописная на основе БСП)
Добавил к публикации готовую форму в виде обработки. Как всегда жду от вас комментариев, критики и предложений!