Функция диалогового выбора с манипуляцией двумя списками в AutoLISP.

Российская академия архитектуры и строительных наук                                                     Уральское отделение

 

Ордена «Знак почёта»
Уральский научно-исследовательский и проектно-конструкторский институт
Российской академии архитектуры и строительных наук

УралНИИпроект РААСН

Функция диалогового выбора с манипуляцией двумя списками в AutoLISP.
Пример разработки.
Лоскутов П.В. (Alaspher)

Данный материал предназначен для тех лиспописателей, кто осваивает создание диалоговых окон средствами языка DCL в AutoCAD версий R15 (2000, 2000i и 2002) и R16 (2004 и 2005). В этом материале рассматривается одна из задач, нередко возникающая при написании программ, это манипуляция двумя списками. Основная цель, которая ставилась при написании всего нижеследующего, это показать некоторые не слишком распространённые приёмы использования DCL. Поскольку никакой необходимости теоретизирования по теме данной статьи нет, практически вся она представляет собой сугубо техническое описание. Все исходные тексты и компилированую версию описываемой здесь функции, можно взять со страницы: «Download».

Создание диалогового окна средствами DCL состоит из двух взаимосвязанных частей:

  • описания диалога, как правило, хранящегося в файле с расширением DCL;
  • управляющего LISP-кода.

Как правило, эта работа ведётся параллельно, но теоретически можно сначала сделать описание окна, а потом написать для него управляющий код. Именно такой подход и будет использован в этом материале.

Описание диалога

Прежде чем приступать к описанию диалога, надо решить, что именно на нём должно быть и как расположить элементы диалога относительно друг друга. Язык DCL имеет весьма ограниченные возможности в этой части, что предопределяет следующее правило – диалог должен быть максимально простым, иначе, сделать его удобным будет очень трудно или невозможно. С другой стороны, в диалоге должна быть вся функциональность, ради которой он собственно и затеян. Найти баланс между функциональностью и удобством может оказаться самой сложной задачей из всех возникающих при создании диалога.

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

  • два окна для списков (list_box);
  • кнопки переноса отмеченных или всех элементов списка в противоположное окно (button), их будет штук пять и разместить их логично между окнами списков;
  • хорошо бы иметь возможность выбирать элементы списка по маске, значит, понадобится поле для ввода маски (edit_box) и ещё пара кнопок (button) для применения маски на списки;
  • вероятно, надо включить в описание окна общее описание того, для чего оно предназначено и краткую подсказку по использованию маски (text);
  • завершающей частью должны стать ещё одна пара стандартных кнопок (button): «OK» и «Cancel».

Включать какие-либо картинки или дополнительные элементы в подобный диалог совершенно нецелесообразно. Осталось «нарисовать» диалог средствами DCL. При создании описания диалога, целесообразно заблокировать в описании (is_enabled = false) все элементы диалога, которые могут быть неактивными (неактуальными в какой то ситуации). Разблокировать их, как и снова – заблокировать, можно потом, из управляющей функции. Полный код описания представленного диалога можно посмотреть щёлкнув на его изображении.

Управляющий код

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

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

(defun <имя функции> (<аргументы> / <локальные переменные>)
    [проверка и обработка аргументов]
    [загрузка файла описания диалога]
    [вызов диалогового окна]
    [установка значений элементов диалога – «по умолчанию»]
    [определение действий связанных с элементами диалога]
    [передача управления диалоговому окну]
    [подготовка результатов возврата к выдаче]
    <выход>
)

Поскольку с именем функции и локальными переменными никаких вопросов быть не может (основная функция будет называться – pl:select-from-list), первое, на чём имеет смысл остановиться – это аргументы.

Aргументы

Для рассматриваемой функции понадобится 4 аргумента.

  • предварительный базовый список строк вида: (“one” “two” …) или nil;
  • предварительный список строк результатов вида: (“three” “four” …) или nil;
  • чувствительность к регистру t|nil;
  • разблокировка кнопки ‘OK’ при пустом списке результатов t|nil.

С первыми двумя аргументами – всё ясно. Они представляют собой списки строк, которые будут элементами в списках диалога. Чувствительность к регистру нужна для того, что бы строки, различающиеся только регистром символов, можно было считать, как совпадающие или, как различающиеся, в зависимости от ситуации. Разрешение на ‘OK’ при пустом списке результатов нужно потому, что в некоторых случаях, при пустом результате логика программы может не допускать такого ответа, а в других случаях это вполне возможно.

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

Назовём аргументы так (в порядке передачи): base res case okempt

Проверка и обработка аргументов

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

  • передаваемые аргументы;
  • производимые действия;
  • возврат.

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

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

  • аргументы: <список строк> <учёт регистра>;
  • действия: удалить у всех строк начальные и концевые пробелы и табуляции, затем удалить дубликаты и отсортировать список;
  • возврат: список строк.

Выполнять очистку имеет смысл только при наличии списка, соответственно, вызов функции, видимо, целесообразно предварить проверкой на существование соответствующего списка. Хранить результаты можно в том же аргументе, который проверяется. Таким образом, получается что-то такое (функции дадим имя – _pl:lst_clear):

  (if base
    (setq base (_pl:lst_clear base case))
  )
  (if res
    (setq res (_pl:lst_clear res case))
  )

Кроме очистки переданных списков, необходимо проверить списки на предмет совпадающих значений и, при наличии таковых, из одного списка дубликаты удалить. Для этого предусмотрим соответствующую функцию, назовём её –_pl:clr_strlst_memb. Запускаться эта функция должна только при наличии обоих списков, а результатом работы должен стать базовый список без элементов имеющихся в списке результатов. Получается нечто такое:

  (if (and base res)
    (setq base (_pl:clr_strlst_memb base res case))
  )

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

Загрузка файла описания диалога

Теперь надо загрузить файл диалога, а так как при неудаче загрузки дальнейшие действия невозможны, то загрузка должна являться условием ветвления. Кроме того, в это же условие можно поместить проверку того, что после процедур очистки переданных списков, хотя бы в одном из них что-нибудь осталось, причём сделать это лучше до загрузки файла диалога, примерно так:

  (if (and (or base res) (setq _diafile (load_dialog "PL_Select-From-List.DCL")))
    (progn
      ...

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

Вызов диалогового окна

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

Поскольку ошибки возможны и при вызове диалогового окна, этот вызов, так же имеет смысл сделать условием ветвления, примерно так:

      (if (setq _dialog (new_dialog "PL_SelectFromList" _diafile))
        (progn
          ...
Установка значений элементов диалога – «по умолчанию»

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

Заполнение окон списков производится функциями – start_listadd_list (столько раз, сколько элементов в списке) и end_list. С одной стороны, всё предельно просто, но с другой – эти действия придётся выполнять многократно, а значит, имеет смысл сделать упрощённый механизм заполнения списков, такой, что бы все действия записывались в одну строку. Пока достаточно будет определить имя и формат вызова данной функции, возврат, в данном случае, не важен. Назовём функцию – _pl:set-lst-to-wind, в качестве аргументов будем передавать ей строку с ID окна списка и список строк для заполнения. Вызывать эту функцию имеет смысл только при непустом списке значений, выглядеть это должно так:

               (if base
                 (_pl:set-lst-to-wind "bas_list" base)
               )

Вторым часто повторяемым действием будет блокировка или разблокировка элементов диалога, как правило – кнопок. Выполняется это действие функцией mode_tile. Может показаться неоправданным создание специальной библиотечной функции для данного действия, но в функции mode_tile есть одна неприятность – она, в качестве аргумента отвечающего за блокировку ожидает передачи цифрового значения 0 или 1, а тест-функции, как правило, возвращают t|nil. Соответственно, при каждом вызове mode_tile придётся использовать конвертацию значений, а вот это уже повод для создания библиотечной функции. Называться она будет – _pl:butt-enabler, а в качестве аргументов принимать строку ID элемента и признак блокировки в таком виде, что любое значение не равное nil приводит к разблокировке указанного элемента. Пример вызова в основной функции:

                 (if okempt
                   (_pl:butt-enabler "ok" t)
                 )

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

Определение действий связанных с элементами диалога

Процедура связывания определённых действий с элементами диалога, является одной из самых важных и сложных частей при создании диалогового окна. На этой стадии определяются все реакции диалога на действия пользователя. Осуществляется это при помощи функции – action_tile, которая принимает, в качестве аргументов, строку ID элемента и собственно связываемое действие. По сути, ничего особо сложного в использовании этой функции нет. Вся сложность заключается в тщательном согласовании действий различных элементов диалога и в том, что в качестве аргумента задающего действие, данная функция принимает строку, а не имя функции (что было бы гораздо логичнее). Это не доставляло бы слишком много проблем, если бы в качестве действия не приходилось передавать достаточно длинные строки, включающие кавычки. При наличии таких строк исходный текст функции становится практически нечитаемым. И если с первым моментом ничего не поделаешь (способов сделать процесс согласования предсказуемым, кроме тщательного планирования – нет), то со строками можно кое-что сделать, при помощи функции – vl-prin1-to-string. Благодаря этой функции, можно все назначаемые действия записывать в привычном виде, например так:

               (action_tile
                 "bas_list"
                 (vl-prin1-to-string
                   '(mapcar
                     (function _pl:butt-enabler)
                     (list "add" "chan")
                     (list (setq _from (read (strcat "(" $value ")"))) (and _from _to))
                    )
                 )
               )

В данном примере действие назначается основному (левому) окну списка. Заключается оно в блокировке или разблокировке двух кнопок в зависимости от того – есть ли выбранные элементы в списке или нет.

Но это было задание действия для пассивного элемента (в окне списка ничего, кроме выбора делать не полагается), а для кнопок переноса задание действия будет несколько сложнее. Во-первых, при нажатии на такую кнопку, надо изменить состояние обоих списков. Для этого придётся создать специальную функцию, называться она будет – _pl:change-lsts. Аргументами для неё будут два идентификатора окон списков, два списка и список номеров, которые подлежат переносу из первого списка во второй. Возвращать она должна список из четырёх списков, два из них представляют собой обновлённые списки строк, а ещё два – это списки выбранных элементов.

После отработки этой функции, нужно будет изменить состояние блокировок кнопок в соответствии с изменившимися условиями. Функция для этого у нас уже предусмотрена.

Таким образом, задание действия для кнопок переноса будет выглядеть примерно так:

               (action_tile
                 "add"
                 (vl-prin1-to-string
                   '(progn
                     (mapcar
                      (function set)
                      (list 'base 'res '_to '_from)
                      (_pl:change-lsts "bas_list" base "res_list" res _from)
                     )
                     (mapcar
                      (function _pl:butt-enabler)
                      (list "add" "rem" "adda" "rema" "ok" "chan" "selb" "selt")
                      (list nil t base t t nil (and _mask base) (and _mask res))
                     )
                    )
                 )
               )

Некоторые отличия будут у функции предназначенной для кнопок переноса одного списка в другой, целиком. В этом случае, нет нужды передавать в виде аргумента список номеров элементов, для переноса. В сторону усложнения будет отличаться функция для кнопки встречного переноса, где придётся передавать не один, а два списка номеров элементов для переноса. Для этого придётся разработать ещё пару функций – _pl:change-all-lsts и _pl:change-lsts-ahсоответственно. Но, поскольку, принципиально эти функции, по производимым действиям, не отличаются, то рассматривать назначение действий с их помощью, мы не будем.

Остановиться отдельно имеет смысл на задании действия для поля ввода маски. Здесь потребуется задать три последовательных действия. Первым действием должна стать проверка маски. Поскольку пробелы в начале и в конце элементов списков мы очистили, то и в маске их быть не должно, соответственно очистим и маску от них. Если после очистки маска превратилась в пустую строку, то можно считать, что её нет, значит, функция, которая будет чистить маску в этом случае должна вернуть nil. После завершения чистки, очищенную маску надо вернуть в окно ввода маски и затем разблокировать или заблокировать соответствующие кнопки:

               (action_tile
                 "mask"
                 (vl-prin1-to-string
                   '(progn
                     (setq _mask (_pl:mask_clr $value))
                     (set_tile
                      "mask"
                      (cond
                       (_mask)
                       (t "")
                      )
                     )
                     (mapcar
                      (function _pl:butt-enabler)
                      (list "selb" "selt")
                      (list (and _mask base) (and _mask res))
                     )
                    )
                 )
               )

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

               (action_tile
                 "selb"
                 (vl-prin1-to-string
                   '(progn
                     (setq _from (_pl:match_sel "bas_list" base _mask))
                     (mapcar
                      (function _pl:butt-enabler)
                      (list "add" "chan")
                      (list _from (and _from _to))
                     )
                    )
                 )
               )

На этом задание действий для элементов диалога завершается. Дальше, всё значительно проще – осталось организовать передачу управления диалогу и обработать возврат.

Передача управления диалоговому окну

Передача управления диалогу заключается в простейшем действии заключающемся всего в одной строке:

               (setq _close (start_dialog))

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

      (unload_dialog _diafile)
Подготовка результатов возврата к выдаче

Осталось выяснить решение пользователя и выдать результат его манипуляций. Поскольку возврат у функции может быть только один, а нам надо вернуть два списка, то их придётся объединить в список списков:

  (if (= _close 1)
    (list base res)
  )

Результатом работы приведённой функции, в случае выхода из диалога кнопкой «OK» будет список двух списков, содержащих состояние окон диалога на момент выхода.

Заключение

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

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