Форма поиска банка Primer. Часть 4. Диалоговое окно с деревом

2022, Feb 20    

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

Предыдущие статьи серии:

Часть 1. Готовим макет формы

Часть 2. Организуем контроль правильности ввода

Часть 3. Добавляем диалоговые окна

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

Сегодня добавим еще одно окно, предназначенное для выбора ограничений на место проживания кандидата. Задавать ограничение будем в виде списка населенных пунктов (с учетом страны, региона), указанных в записях базы “Адрес”.

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

Создаем диалоговое окно

Создаем управляющую форму следующего вида: Форма "Города"

Единственное отличие от формы из предыдущей статьи - вместо элемента “Список” (ListBox) мы используем дерево (TreeControl).

Свойства формы установим такими же, как и для окна списков:

  • Тип границы - “Окно диалога”;
  • Модальное - Да;
  • Всплывающее - Да;
  • Кнопки размера - Отсутствуют.

Обмен данными с главной формой

Мы по прежнему используем для обмена данными между окном и главной формой таблицу аргументов Arg, но исключим из нее поля field и title, а в поле selection вместо обычного массива строк будем записывать хеш-таблицу, ключами в которой являются словарные коды стран, а их значениями - массивы названий городов с дополнительным полем Name, в котором хранится название страны - раскодированное понятие словаря:

Пример заполнения таблицы:

selection = {
   ["801"] = {Name = "Россия", "Москва", "Серпухов", ...},
   ["286"] = {Name = "Италия"},
}	

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

Заполняем дерево значениями

Начинаем описывать функцию, заполняющую дерево всеми присутствующими в банке названиями стран и городов:

function fill_tree(selection)

Включим в дереве отображение отметок и режим автоматической отметки родительских узлов дерева в зависимости от состояния флажков дочерних узлов:

   Me.tree1.ShowCheckBoxes = true
   Me.tree1.SmartCheck = true

Отберем из базы “Адрес” записи, содержащие уникальные пары “Страна-город”:

   local rs = GetBank():StringRequest("ОТ АД01 1 УЗ 3")
   if not rs or rs.Count == 0 then
      return
   end

Сформируем вспомогательную структуру tree_items, аналогичную Arg.selections, но содержащую все наименования городов из банка. Одновременно будем формировать массив index, содержащий пары {код,понятие}, который в дальнейшем потребуется для того, чтобы вывести названия стран в определенном порядке - по алфавиту:

   local tree_items = {}
   local index = {}
   for rec in rs.Records do
      local code = rec:GetValue(1,1,false)
      local area = sl(3,code)
      if not tree_items[code] then
         tree_items[code] = {["Name"] = area}
         table.insert(index,{code,area})
      end
      local val = rec:GetValue(3)
      if val ~= "" then
         table.insert(tree_items[code],val)
      end	
   end
   table.sort(index, function(a,b) return a[2] < b[2] end)

Теперь переберем все элементы tree_items в порядке, определяемом массивом index, и создадим в дереве соответствующие узлы. Названия стран будем помещать в дерево корневыми узлами, а названия входящих в них городов - дочерними. Для корневых узлов в свойство ItemData будем записывать соответствующий код словаря. Дочерние узлы, содержащие названия городов, которые содержатся в selection, будем отмечать, а их идентификаторы записывать в массив pre_checked. В дальнейшем этот массив будет использоваться для выявления сделанных изменений в выборе пользователя:

   for k = 1,#index do
      code, cities = index[k][1], tree_items[index[k][1]]
      local node = Me.tree1:InsertItem(cities.Name)
      node.ItemData = code
      for i=1,#cities do
         table.sort(cities)
         local sub_node = Me.tree1:InsertItem(cities[i], node)
         if selection[code] and (#(selection[code]) == 0 or
            table.getkey(selection[code],cities[i])) then
            sub_node.Checked = true
            table.insert(pre_checked,sub_node.Id)
         end
      end
   end	

И в завершение выделим и развернем первый узел в дереве:

   if #Me.tree1.RootItems > 0 then
      Me.tree1.RootItems[1].Selected = true
      Me.tree1.RootItems[1]:Expand()
   end

Функцию будем вызывать из обработчика события Load формы:

function Форма_Load( form, event )
   if not Arg then
      Arg = {}
   end
   if type(Arg.selection) ~= "table" then
      Arg.selection = {}
   end
	
   fill_tree(Arg.selection or {})
	
   Me.ApplyControl = Me.btnOk
   Me.CancelControl = Me.btnCancel
end

Фиксируем выбор пользователя

Когда пользователь нажимает кнопку Ok нам следует вновь сформировать структуру Arg.selection, включив в нее отмеченные узлы дерева:

function SaveSelections()
   Arg.selection = {}
   local codes = Me.tree1.RootItems
   for i=1,#codes do
      local node = codes[i]
      if node.Checked then
         Arg.selection[node.ItemData] = {["Name"] = node.Text}
      else
         local cities = node.Children
         for j=1,#cities do
            if cities[j].Checked then
               if not Arg.selection[node.ItemData] then
                  Arg.selection[node.ItemData] = {["Name"] = node.Text}
               end	
               table.insert(Arg.selection[node.ItemData],cities[j].Text)
            end	
         end	
      end	
   end	
end

В основном цикле (for i=1,#codes) do мы перебираем корневые узлы дерева и, если узел отмечен - а это означает, что выбраны все относящиеся к данной стране города, - включаем в Arg.selection соответствующий элемент с единственным полем Name. Если же свойство Checked корневого узла содержит false, т.е. отмечены лишь некоторые дочерние по отношению к нему узлы или не отмечен ни один, то перебирая дочерние узлы мы ищем среди них отмеченные и, обнаружив такой, создаем в Arg.selection элемент по коду страны (значение свойства ItemData родительского элемента) и добавляем название города в массив значений этого элемента.

Обработчик события Click кнопки Ok теперь выглядит так:

function btnOk_Click( control, event )
   if Arg then
      Arg.ModalResult = 1
      SaveSelections()
   end
   Me:CloseForm()
end

Проверяем наличие изменений

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

Функция сопоставления приведена ниже:

function SelectionChanged()
   local t = Me.tree1:GetAllItems()
   for i=1,#t do
      local item = t[i].Text
      if t[i].ParentItem then
         if t[i].Checked and not table.getkey(pre_checked,t[i].Id) or
            not t[i].Checked and table.getkey(pre_checked,t[i].Id) then
            return true
         end 
      end	
   end
   return false
end

Данные различаются, если хотя бы один из отмеченных дочерних узлов отсутствует в pre_checked или, наоборот, хотя бы один не отмеченный там содержится.

Вызываем диалоговое окно из главной формы и получаем результат

Обработчик нажатия кнопки выбора для места проживания (btnAddress) опишем аналогично обработчикам кнопок btnEducation и btnCareer из предыдущей статьи.

function btnAddress_Click( control, event )
   args = {selection = address}
   GetBank():OpenForm("Города",0,Me,args)
   if args.ModalResult == 1 then
      address = args.selection
      local tmpAreas = {}
      for area, cities in pairs(address) do
         if #cities > 0 then
            table.insert(tmpAreas,cities.Name..": "..table.concat(cities,","))
         else	
            table.insert(tmpAreas,cities.Name) 
         end
      end
      Me.txtAddress.Text = table.concat(tmpAreas,"; ")
   end
end

Полученный от диалогового окна перечень выбранных городов сохраняется в массиве address (как и массивы education и career, он должен быть объявлен в начале модуля главной формы). Далее на его основе формируется текстовая строка для отображения на форме:

txtAddress

Включаем адреса в запрос

Теперь добавим в функцию MakeRequest код, включающий в строчный запрос критерий отбора для адреса места жительства кандидата:

-- место проживания	
   if table.count(address) > 0 then
      local tmpAreas = {}
      for area,cities in pairs(address) do
         local s = "1 РВ "..area
         if #cities > 0 then
            local tmp = {}
            table.foreach(cities, function(k,item) tmp[k] = "3 РВ "..item:quote([["]]) end)
            s = s.." И "..table.concat(tmp," ИЛИ ")
         end
         table.insert(tmpAreas,"("..s..")")
      end
      conditions.address = "АД04 "..table.concat(tmpAreas, " ИЛИ ")
      table.insert(conditions,"80 АД04")
   end

Перебирая элементы таблицы address мы постепенно формируем условия для характеристик “Страна” и “Город”, а затем объединяем их связкой ИЛИ. Сформированный критерий выглядит так:

АД04 (1 РВ код1) ИЛИ (1 РВ код2 И 3 РВ "Город1" ИЛИ 3 РВ "Город2" ...) ИЛИ ...

Он записывается в поле address таблицы conditions, а к условиям отбора корневой базы добавляется “80 АД04” (лицо связано ссылкой № 80 с записями базы “Адрес”).

На этом этапе функция MakeRequest принимает свой окончательный вид:

function MakeRequest()
-- Формирование строчной записи запроса
   local req = "ОТ ЛЦ01"

   conditions = {}
-- возраст	
   if Me.txtAgeFrom.Text ~= "" then
      local cur_year = DateTime.Now.Year
      local val = tonumber(Me.txtAgeFrom.Text)
      table.insert(conditions,"9 МР 31.12."..(cur_year - val))
   end
    
   if Me.txtAgeTo.Text ~= "" then
      local cur_year = DateTime.Now.Year
      local val = tonumber(Me.txtAgeTo.Text)
      table.insert(conditions,"9 БР 00.00."..(cur_year - val))
   end
	
-- стаж
   AgeValidation({Control = Me.txtExperience})
   if Me.txtExperience.Text ~= "" then
      conditions.experience = tonumber(Me.txtExperience.Text)
   end
	
-- образование
   if #education > 0 then
      local tmp = {}
      table.foreach(education, function(k,item) tmp[k] = "4 РВ "..item:quote([["]]) end)
      conditions.edu = "ОБ02 "..table.concat(tmp, " ИЛИ ")
      table.insert(conditions,"201 ОБ02")
   end
	
-- опыт работы	
   if #career > 0 then
      local tmp = {}
      table.foreach(career, function(k,item) tmp[k] = "3 РВ "..item:quote([["]]) end)
      conditions.career = "ТД03 "..table.concat(tmp, " ИЛИ ")
      table.insert(conditions,"90 ТД03")
   end

-- место проживания	
   if table.count(address) > 0 then
      local tmpAreas = {}
      for area,cities in pairs(address) do
         local s = "1 РВ "..area
         if #cities > 0 then
            local tmp = {}
            table.foreach(cities, function(k,item) tmp[k] = "3 РВ "..item:quote([["]]) end)
            s = s.." И "..table.concat(tmp," ИЛИ ")
         end
         table.insert(tmpAreas,"("..s..")")
      end
      conditions.address = "АД04 "..table.concat(tmpAreas, " ИЛИ ")
      table.insert(conditions,"80 АД04")
   end

   if #conditions > 0 then
      req = req.." "..table.concat(conditions," И ")
      if conditions.edu then
         req = req.." "..conditions.edu
      end
      if conditions.career then 
         req = req.." "..conditions.career
      end
      if conditions.address then
         req = req.." "..conditions.address
      end
   end
		
   return req
end

Дополнительные материалы

Страница создана на основе шаблона flexible-jekyll