В этом разделе я расскажу об интерфейсе скриптов (UI - User Interface, пользовательский интерфейс) на примере свитка (rollout), поделюсь моими соображениями по оптимизации процесса создания интерфейсов и опишу баги, которые 3ds max будет вам любезно подкладывать при работе с UI.

rollout testRol "test" width:211 height:74
(
    --СЮДА КУРСОР
)
CreateDialog testRol style: #(#style_titlebar, #style_border, #style_sysmenu, #style_minimizebox)

Итак, после создания пустого свитка ставим курсор внутрь его "тела" и идем в падающее меню Tools/Edit Rollout, чтобы открыть Visual MAXScript Editor (VME) и добавить в интерфейс нужные элементы: кнопки, списки, текст и т.д., все перечислять не буду, там их немало. Некоторые элементы можно добавить только вручную, например элементы dotNet или CurveControl, но это уже для сложного UI, где простых инструментов недостаточно. Вызывать VME придется не только при создании UI, но также и при его редактировании, при этом следует иметь ввиду, что после сохранения интерфейса кнопкой из VME, 3ds max испортит ваш скрипт. Т.е. он перезаписывает измененный интерфейс и у него это очень редко получается правильно (почти никогда). Вот что автоматически происходит со скриптом после сохранения UI через VME:

  1. Если вы копировали элемент с событием, событие дублируется столько раз, сколько вы скопировали (причем в совершенно разные места скрипта), порядок скобок нарушается.
  2. Если для спиннера в параметре range заданы вещественные числа, например range: [-1.0,1.0,0], то точки заменяются на запятые: range: [-1,0,1,0,0].
  3. Если у элемента определен параметр bitmap: (bitmap 1 1 color: (color 0 200 0 0)), то после сохранения он превращается в bitmap: BitMap:
  4. Если у вас 2010-й макс, то у каждой строки, оканчивающейся на число, это число "отгрызется" и скопируется на следующую строку.

Итак, четыре бага, с которыми я непосредственно сталкивался. Как их избегать:

  1. Хранить в отдельном файле копию только интерфейса без событий, редактировать ее и потом "копипастить" в основной скрипт.
  2. Скопировать UI в отдельное место и закомментировать. Комментарии 3ds max к счастью не трогает при сохранении из VME. Потом из комментария можно взять нужные значения для испорченных параметров.
  3. Не редактировать UI через VME в 3ds max 2010.
Вот, например, такой UI сохраняем в отдельном файле:
rollout testRol "test" width:211 height:74
(
    label lbl7 "Object:" pos:[9,17] width:33 height:15
    label lbl8 "N:" pos:[147,18] width:12 height:12

    dropDownList ddlObj "" pos:[44,13] width:98 height:21
    spinner spnObj "" pos:[159,16] width:38 height:16 type: #integer
   
    button btnSel "Select Object" pos:[9,43] width:90 height:19
    checkButton ckbAutoSel "AutoSelect" pos:[107,43] width:90 height:19
)
CreateDialog testRol style: #(#style_titlebar, #style_border, #style_sysmenu, #style_minimizebox)
   

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

Во-первых, названия элементов UI должны начинаться с обозначения типа элемента, например btn для button или ddl для dropDownList. VME сам добавляет эти обозначения для элементов, очень советую их оставлять, иначе вы запутаетесь в типах элементов сложного интерфейса со всеми вытекающими - будете искать у них несуществующие свойства, постоянно "перематывать" к интерфейсу, чтобы вспомнить где какие элементы и как они называются.

Во-вторых, полезно отражать в именах элементов их предназначение (matrix has me :D), например кнопка, выделяющая объекты, может называться btnSel или btnSelect, тогда вы всегда будете помнить, что данная кнопка делает. Вообще не надо бояться длинных имен, длинное имя, особенно у элемента UI, всегда лучше короткого.

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

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

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

И наконец, при размещении элементов UI полезно визуально объединять их по группам, например метки, для которых нет событий, можно вынести в отдельный блок.


По поводу оптимизации кода также скажу пару слов (хотя это и тривиальные вещи, но в пылу программирования многие о них забывают):
  1. Экономьте место, записывайте несколько коротких операторов на одной строке через символ ;
  2. Не делайте строки, на которых только одна скобка ( или )
  3. Не дублируйте однотипный код, вместо этого используйте функции для вызовов повторяющихся операций, также для этих целей можно использовать метод .pressed().
  4. Чем короче код, тем проще навигация, и наоборот. Помните об этом, записывая многострочный комментарий или отделяя несколькими пустыми строками один фрагмент кода от другого.
  5. Используйте эти рекомендации без фанатизма.


Пример скрипта, где я постарался все это использовать. Скрипт выделяет объекты сцены.

rollout testRol "test" width:211 height:74
(
    label lbl7 "Object:" pos:[9,17] width:33 height:15
    label lbl8 "N:" pos:[147,18] width:12 height:12

    dropDownList ddlObj "" pos:[44,13] width:98 height:21
    spinner spnObj "" pos:[159,16] width:38 height:16 type: #integer
   
    button btnSel "Select Object" pos:[9,43] width:90 height:19
    checkButton ckbAutoSel "AutoSelect" pos:[107,43] width:90 height:19
   
    local buttonsArr, objectsArr
   
    on testRol open do (
        objectsArr=#(); arr=#(); buttonsArr=#(btnSel, ckbAutoSel)
        for i in objects do (append objectsArr i; append arr ((classOf i) as string))
        ddlObj.items=arr
        spnObj.range=[0,ddlObj.items.count+1,1]
        )
   
    on ddlObj selected arg do (spnObj.value=arg; btnSel.pressed())
       
    on spnObj changed arg do (
        if arg==0 then arg=ddlObj.items.count
        if arg==(ddlObj.items.count+1) then arg=1
        spnObj.value=ddlObj.selection=arg
        if buttonsArr[2].state then buttonsArr[1].pressed() --buttonsArr[2]=ckbAutoSel buttonsArr[1]=btnSel
        )
   
    on btnSel pressed do if ckbAutoSel.state then selectMore objectsArr[spnObj.value] else select objectsArr[ddlObj.selection] --spnObj.value=ddlObj.selection
   
)
CreateDialog testRol style: #(#style_titlebar, #style_border, #style_sysmenu, #style_minimizebox)