Разбираем структуру (эта статья)
В заключительной части данного цикла, в котором я постарался на минимальном уровне создать более-менее удобную среду для начала (!) экспериментов по изучения возможностей перевода автоматизации работы с документами из Microsoft Office в «МойОфис».
Напомню, в прошлой части мы перечислили все контролы, которые нам доступны при создании форм. В этой закончим разбор того, как это всё может функционировать (насколько я это понял, конечно), при этом переключившись на работу уже с документами.
Маленькое уточнение
Так же снова оговорюсь, что в Lua я нуб, и учу этот язык по ходу проработки данной темы. А для понимания написанных скриптов, основы данного языка всё надо знать, так как я надеюсь читатель знаком с ними и особо акцентироваться на его особенностях не буду!
Поскольку я хотел бы немного глубже рассмотреть структуру, то стоит вернуться к некоторым общим моментам в LuaAPI «МойОфис», чтобы было более ясно, как предлагают работать над настройками.
Запуск надстройки
Под запуском я понимаю не работу с интерфейсом «МойОфис», а уже непосредственно листинг в стартовом скрипте (entryform.lua). Напомню, все (!) доступные в меню "Надстройки" приложения "МойОфис Текст" для данной надстройки пользователю команды, сконцентрированы в функции getCommands(), таблицы Actions.
function Actions.getCommands()
return {
{
id = "DlgForm.ShowDlg",
menuItem = "Показать форму",
command = Actions.ShowControls
}
}
end
Соответственно, отсюда понятно, что можно наделать для каждого требуемого действия отдельную команду, и запускать для её выполнения, если это нужно для команды отдельную форму (но можно так и не делать, а использовать одну но хитро, о чем скажу немного в самом конце статьи). Соответственно, сама команда запуска это Actions.ShowControls():
function Actions.ShowControls(context)
frm.context=context
context.showDialog(frm.dlgBegin) --Запускаем форму frm.dlgBegin
end
Опять же, напомню, frm – загруженная тем или иным способом таблица "Forms", в которой описывается и форма и логика работы данной команды (подробнее смотрите 2 часть данной серии). Сейчас нас интересует глобальный объект contex. Это объект, который является «мостом» между самим приложением и надстройкой. Как гласит руководство по API:
Параметр context функции обработчика события определяет уровень, на котором функция обработчика события взаимодействует с приложением редактора.
Возможны следующие уровни взаимодействия:
• на уровне приложения (context.doWithApplication(application));
• на уровне открытого документа (context.doWithDocument(document));
• на уровне выделенного фрагмента в открытом документе (context.doWithSelection(DocumentAPI.Range или DocumentAPI.CellRange)).
Разработчик должен вызвать один из методов контекста, передавая ему в качестве параметра анонимную функцию для фактической обработки события. Аргументом анонимной функции является объект, представляющий приложение (глобальный объект application), документ (глобальный объект document) или выделенный фрагмент документа (selection).
Подробно как работать с контекстом, я описывать не буду (хотя и запутанно, но объяснения лучше почитать в первоисточнике), отмечу лишь свою строку в коде: frm.context=context . С её помощью передаётся контекст внутрь загруженного модуля, а значит можно организовывать логику работы уже в нём! Ах да! Чуть не забыл! Это ещё и единственный (!)из доступных разработчику способ запустить свою форму надстройки: context.showDialog(frm.dlgBegin) !
Форма надстройки
Далее, чтобы уже было о чём-то более предметно обсуждать, давайте придумаем для надстройки какую-нибудь задачу. Например, пусть надстройка создаст текстовый документ и заполнит в нём некоторую структуру. А поскольку сейчас я хочу просто немного погрузиться в некоторые моменты работы, эту структуру документа возьмём простейшую. А значит и форму нам надо создать так же простейшую. Зададим в ней пару вопросов пользователю, по поводу названия документа и названия организации (понимаю, что пример надуманный и скучный, но и задача сейчас стоит не конкретная, а учебная). Должно получиться вот такое:
И на основании ответов, запишем названия в колонтитулах и создадим нечто типа заголовка (именно «нечто», так как, оказалось, что управлять стилем написания из под надстроек мы не можем(!),что в очередной раз вызвало моё непонимание: почему?).
Немного моего недоумения на текущий момент
На самом деле, чем дальше пытаюсь разобраться с API надстроек, тем больше складывается впечатление, что те, кто занимался написанием API автоматизации, с его реальными задачами просто не сталкивался. Вот, например, как можно было не предусмотреть загрузку файла в сам редактор документа?! То есть, загрузить в оперативную память я его могу. Могу сделать манипуляции с ним. А вот отобразить его пользователю из надстройки в программе - нет! То есть, потом только ручками ищем такой документ средствами самой программы и открываем. Видимо логика такая, что большинство рабочих действий в надстройках предпринимается только к уже загруженному документу. Но ведь это не так! Очень не малая часть работ по автоматизации, это либо генерация новых документов, и желательно сразу с их визуальным отображением, либо перенесение информации в (или из) нескольких разных документов по определённой бизнес-логике. Например: можно заполнить в форме макроса некоторый набор данных (ФИО, даты) для оформления отпуска, с созданием по шаблонам документов:
служебка на отпуск за подписью непосредственного начальника, на имя генерального директора;
в отдел кадров – заявка на отпуск (для оформления приказа, а можно и сам приказ сразу так же сформировать);
в бухгалтерию служебку с просьбой рассчитать положенные отпускные;
Если это сделать в MS Office, то там все сформированные документы без проблем сразу же загрузятся пакетом в редактор, и потом можно их после проверок и правок либо ручками разослать, либо точно так же сформировать автоматическую рассылку адресатам. Иначе говоря, такой макрос позволяет сделать «своими руками» что-то типа СЭД (Системы Электронного Документооборота) пусть и простейшего уровня, но вполне себе полезный и понятный, для чего он создаётся. А вот в «МойОфис» даже если и создать такой набор документов, сперва его надо будет загрузить вручную (каждый документ!), и только потом продолжить что-то делать с каждым из документов. Странная логика! Лично мне она не ясна.
Листинг не буду убирать под спойлер, так как в нём сегодня основная суть:
Frm={}
local nameOrg="Название организации"
local nameDoc="Название документа"
--Контрол надписи
local app=nil
local labelCntl=ui:Label{
Text="Введите название документа:",
Color=Forms.Color(255,0,0), --установить красный цвет
Size=Forms.Size(200,30), -- установить размер (для позиционирования на форме)
Alignment=Forms.Alignment_MiddleCenter, --настройка положения надписи по отношению к границам контрола
}
--Контрол ввода текста
textBoxCntrl = ui:TextBox{
Text=nameDoc,
Size=Forms.Size(200,30),
}
textBoxCntrl:setOnEditingFinished(function() nameDoc=textBoxCntrl:getText() end)
local labelCntl2=ui:Label{
Text="Введите название организации:",
Color=Forms.Color(255,0,0), --установить красный цвет
Size=Forms.Size(200,30), -- установить размер (для позиционирования на форме)
Alignment=Forms.Alignment_MiddleCenter, --настройка положения надписи по отношению к границам контрола
}
--Контрол ввода текста
textBoxCntrl2 = ui:TextBox{
Text=nameOrg,
Size=Forms.Size(200,30),
}
textBoxCntrl2:setOnEditingFinished(function() nameOrg=textBoxCntrl2:getText() end)
--Специальный объект для кнопок внизу формы используемых обычно для принятия или отклонения результатов работы
local dialogButtons = ui:DialogButtons{}
dialogButtons:addButton("OK", Forms.DialogButtonRole_Accept)
dialogButtons:addButton("Cancel", Forms.DialogButtonRole_Reject)
--Непосредственно сама форма
Frm.dlgBegin = ui:Dialog {
Title = "Создание документа",
Size = Forms.Size(600,300),
Buttons = dialogButtons, --Кнопки Ок и Cancel внизу формы
--описываем "геометрию" расположения контролов на форме "сверху-вниз" и "слева-направо"
ui:Column { --зададим всё в одну колонну
ui:Row { --первая"строка"
ui:Spacer{},
ui:Row {labelCntl},
ui:Spacer{}, --разедлитель, чтобы контролы не "слипались краями"
ui:Row {textBoxCntrl},
ui:Spacer{},
},
ui:Row {
ui:Spacer{},
ui:Row {labelCntl2},
ui:Spacer{},
ui:Row {textBoxCntrl2},
ui:Spacer{},
},
},
}
--Обработчик нажатия на кнопки (добавленные как Frm.Buttons), обычно завершающие работу формы
Frm.dlgBegin:setOnDone(function(ret)
if ret == 1 then --Нажата кнопка Ок
Frm.context.doWithApplication(function(application)
doc = application:createDocument(DocumentAPI.DocumentType_Text)
if doc~=nil then
local section = doc:getBlocks():getBlock(0):getSection()
--Настройка страницы (секции)
section:setPageOrientation(DocumentAPI.PageOrientation_Landscape)
--Вставка верхнего колонтитула
local headers=section:getHeaders()
for header in headers:enumerate() do
if (header:getType() == DocumentAPI.HeaderFooterType_Header) then
header:getBlocks():getBlock(0):getRange():getBegin():insertText(nameDoc)
end
end
--Вставка нижнего колонтитула
local footers=section:getFooters()
for footer in footers:enumerate() do
if (footer:getType() == DocumentAPI.HeaderFooterType_Footer) then
footer:getBlocks():getBlock(0):getRange():getBegin():insertText(nameOrg)
end
end
--Вставка содержимого документа
local begin_pos = doc:getRange():getBegin()
begin_pos:insertText(nameDoc .. " от " .. nameOrg)
local header=doc:getBlocks():getParagraph(0)
local para_props = header:getParagraphProperties()
para_props.alignment = DocumentAPI.Alignment_Center
header:setParagraphProperties(para_props)
doc:saveAs("d:\\NewDocument.xodt")
end
end)
else
-- Cancel pressed
end
end
)
return Frm
Не знаю, стоит ли сильно углубляться в данном случае в подробности скрипта? "Геометрию" расположения контролов на форме я пожалуй не буду разбирать, так как по этому я проходился уже ранее, во второй части. А вот по созданию и наполнению документов, я всё таки поясню, так как есть там несколько моментов и они могут вызвать недоумение при начальном знакомством с API. Сосредоточен интересующий код в основном в функции Frm.dlgBegin:setOnDone(), которая является обработчиком нажатия на кнопки Ok или Cancel, но конечно можно и вынести весь код в отдельную локальную функцию.
1) Как я уже отметил ранее, связь между редактором и нашим кодом в плане работы осуществляется через специализированную объектную переменную context. Поскольку я планирую создавать новый документ, то используется следующий формат Frm.context.doWithApplication. Используется Frm.context а не context как в руководстве, так как я работаю в отдельном модуле, и в нём context просто окажется равным nil. Именно для этого, я ранее и сохранял значение context в модуле «entryform.lua» (смотрите в выше, и подробнее в прошлых частях).
2) Создать документ (application:createDocument()), или его загрузить для обработки можно только через глобальный объект application. Как-то иначе, кроме как получить его через функцию контекста: context.doWithApplication(application), как я понял, мы не можем. Поэтому, если хотим что то с ним дальше сделать, то надо сохранить его значение в отдельную локальную переменную всего скрипта (или таблицы "Form") внутри анонимной функции, создаваемой для обработки doWithApplication().
3) В основе внутренней структуры документа лежит понятие блока (DocumentAPI.Block). Блок является родительским для всех остальных типов отображаемых данных, и большинство работы с документом, так или иначе, начинается с поиска или получения блоков и дочерних типов данных, от него происходящих: параграфов (DocumentAPI. Paragraph), таблиц (DocumentAPI.Table), фигур (DocumentAPI. Shape) или полей (DocumentAPI.Field)
Как видно из диаграммы, блоки с помощью соответствующих функций, можно преобразовать в дочерние типы, а так же их удалить.
4)Страницы в понятиях API «МойОфис» это секции. Немного странное название, и по моему немного нелогичное, но какое есть! У каждой секции можно с помощью таблицы свойств DocumentAPI.PageProperties установить размеры и поля (DocumentAPI.Insets) . Можно так же дополнительно отдельно задавать ориентацию страницы.
Итак, для начала работы, нужно получить секцию (страницу) и допустим, сменить ориентацию на «ландшафтную»:
local section = doc:getBlocks():getBlock(0):getSection()
--Настройка страницы (секции)
section:setPageOrientation(DocumentAPI.PageOrientation_Landscape)
Для нахождения первой страницы по логике этого API нужно найти самый первый блок и по нему найти секцию (страницу), где он расположен (doc:getBlocks() возвращает таблицу со всеми блоками документа, который мы создали, а getBlock(0) - самый первый блок, и он уже позволяет через getSection() найти первую страницу). Далее найденной первой странице задаём горизонтальное расположение: section:setPageOrientation(DocumentAPI.PageOrientation_Landscape)
5) Колонтитулы это отдельные области на странице, которые в свою очередь так же состоят из блоков:
Туда (в верхний и нижний колонтитулы) я подставлю значения, которые пользователь вводит в полях для типа документа (верхний колонтитул) и названия организации (нижний).
--Вставка верхнего колонтитула
local headers=section:getHeaders()
for header in headers:enumerate() do
if (header:getType() == DocumentAPI.HeaderFooterType_Header) then
header:getBlocks():getBlock(0):getRange():getBegin():insertText(nameDoc)
end
end
Интерес здесь представляет разве что функция-итератор enumerate() с помощью которой можно перебирать блоки соответствующего типа. Такие функции имеются почти у всех коллекция разных типов блоков (и не только их), что может облегчить жизнь при разных манипуляциях (особенно поиске) с данными разных типов.
6) Любая манипуляция по добавлению информации в документ, осуществляется с помощью таблицы DocumentAPI.Position, которая в свою очередь является границами диапазона DocumentAPI.Range:
Диаграмма объектной модели для диапазонов
Диаграмма
Почти любая информация может сперва выбираться по диапазону, и уже только потом, из этого диапазона можно дополнительно выбрать, например, только параграфы. Что достаточно удобно. Но вот дальше логика работы опять мне не ясна: я могу получить только начало диапазона, и его конец (Range:getBegin() и Range:getBegin()), но не могу в итоге двигаться (используя что-то типа next() итераторов) по позициям внутри этого диапазона, чтобы, например, вставить в третий параграф от начала документа новый параграф. Причем вставить я могу только исключительно через Position, что в дальнейшем важно но непонятно. Я должен найти именно третий параграф, задать его как диапазон, встать в его начало и только потом вставить через Position:insertText() текст, который при этом вставится после третьего блока как параграф. И сюрприз-сюрприз! В отличии от например функции Position:insertTable(), которая возвращает объект вставленной таблицы (чтобы я потом мог если мне нужно продолжить работу с ней), Position:insertText() не возвращает ничего! То есть, хочешь, что-то поменять во вставленном параграфе – ищи заново! Причины такого поведения к основному типу текстовых данных в API для меня не ясны, от слова совсем! Ну ладно! Будем работать как нам предлагают. Итак, вставим «типа заголовок» ибо как я уже писал, стиль заголовка, мы из API применить не можем (снова к вопросу о том, как там вообще автоматизацию представляют, если в ней столько "нельзя" что-то сделать, при не очень многом, что можно)!
local begin_pos = doc:getRange():getBegin()
begin_pos:insertText(nameDoc .. " от " .. nameOrg)
local header=doc:getBlocks():getParagraph(0)
local para_props = header:getParagraphProperties()
para_props.alignment = DocumentAPI.Alignment_Center
header:setParagraphProperties(para_props)
В качестве текста заголовка будет просто соединённые две строки вводимые пользователем в форме (begin_pos:insertText(nameDoc .. " от " .. nameOrg)) и расположенные просто по центру документа (para_props.alignment = DocumentAPI.Alignment_Center). Как видно, параграфы так же имеют свои наборы свойств DocumentAPI.ParagraphProperties, которые мы можем менять.
Но к сожалению, для стиля в форматировании места почему-то не нашлось, хотя, в самом офисе, они явно есть:
7) Далее просто сохраняем документ
doc:saveAs("d:\\NewDocument.xodt")
Сохраняем данный файл forms.lua (или init.lua если используется загрузка через require “Forms”, см прошлые выпуски). И если всё прошло "пучком", то после всех манипуляций в форме расширения, которая должна выглядеть как показано на самом первом скриншоте этой статьи, по нажатию на кнопку "Ок" , ничего внешне не произойдёт (не по моей лени, а просто потому, что в API не предусмотрена загрузка в редактор файла документа!). Но на диске d:,в его корне, должен создаться документ NewDocument.xodt, который для проверки результата надо будет загрузить в «МойОфис Текст» вручную, и выглядеть он должен примерно вот так:
Вот такой получился простой, учебный пример. Но и он потрепал мне не мало нервов, так как честно говоря, я привык к немного другому.
Краткие выводы:
Работать с API можно. Есть свои особенности работы Lua, но к ним можно вполне привыкнуть, особенно если немного изучить принципы его работы. Если всё таки придётся писать надстройки - без этого никак!
Само API "сырое" и явно недоделанное. Такое ощущение, что оно лепилось наспех, и а потом просто было заброшено, ибо "есть же!". Судя по тому, как отвечает мне тех.поддрежка в ходе работы над статьями (почти на все мои вопросы: «этого нет, но передадим в отдел разработки»), автоматизацию отложили на лучшие времена, и особо прогресса ждать не приходится. Часть из того что меня, хм, скажем - удивило, я уже написал, а сколько осталось за рамками статьи мне просто лень писать…
Визуальная сторона форм вызывает недоумение. Мне с трудом верится, что я единственный кто озадачен тем, что в текстовых полях ввода, например, нет wordwrap. Да и вообще, походу многострочного ввод там никто не предусматривал. Нельзя изменить в контролах ни сам шрифт, ни его размер, ни его начертание. Или, почему нет простых но классических контролов "Frame", для визуального и контекстного разделения зон работы с контролами? Нет полос прокрутки. Нет спиннеров. Нет возможности вставить картинки. Но и то что есть, в ряде случаев работает не всегда корректно. Так иногда при нажатии на кнопки, размеры других контролов вдруг меняются. Да и формы в целом, тоже ещё тот квест! Сама логика построения дизайна с помощью табличного Layout и агрегаторов ui:Column и ui:Row, конечно современна, но с точки зрения построения большинства форм, требует слишком немалых усилий чтобы правильно всё расположить! Особенно при том, что нет визуального редактора для GUI, а значит вместо того, чтобы выбрать контролы, быстро их расположить на форме и привязать к обработчикам событий функции для обработки бизнес-логики, прийдется сперва раз н-цать позапускать надстройку, чтобы проверить как всё выглядит. А потом если вдруг, не дай бог, потребуется изменить текст внутри контролов на что то большее по размеру, нежели после такого проектирования! Надписи скорее всего"съедятся" и придётся снова мучатся, меняя расположения и размеры, чтобы привести форму снова к более-менее нормальному виду.
Также мне так же не ясно, почему разработчики,похоже не предусматривали сложные варианты надстроек, в которых может быть много форм, или одна форма, но с большим числом контролов, которые раскиданы по нескольким вкладкам? Отчасти такие ситуации можно решить с помощью большого числа предусматриваемых команд в надстройке:
Или как вариант, создать некий набор внутри одной формы:
Но такие варианты «из коробки» не решают вопроса для сложного набора контролов (например в форме для всех типов документов может быть один общий для всех типов набор контролов, и специфичный для каждого по отдельности, и просто так, сходу, такой вариант не решить). Но «голь на выдумки хитра», и я нашёл способы, позволяющий обойти такие проблемы, с помощью некоторой смекалки и опыта работы:
Но такие хитрые схемы работы с формами надстроек сами по себе требуют отдельной серии статей для обсуждения! И честно говоря, как по мне, такие методы являются некоторым «извращением» над здравым смыслом, так как, судя по всему, изначально в API такая схема работы не предусмотрена!
Итог
Заканчивая эту вводную серию статей, хочу подвести итог:
Пользоваться надстройками можно безусловно и в том состоянии, каком сейчас предлагают разработчики. Да, работать сложно и требует переучивания на Lua и пристального изучения API надстроек SDK «МойОфис». Но, при определённых усилиях, вполне можно сделать что-то напоминающее макросы VBA от Microsoft Office. Хотя, думаю что массовый перевод наработок в макросах будет, скорее всего, болезненным и тяжёлым. А кое-что перевести не удастся совсем, поскольку пока API «МойОфис» не сильно приближён к API Microsoft Office. Ну, или, хотя бы, пока он не будет расширен до уровня, требуемой для типовых задач автоматизации офисной работы.
Очень надеюсь, что эта серия статей оказалась кому-то полезной! Если будет желание, то я могу сделать продолжение, в котором будут рассмотрены надстройки уже более приближенные к реальным задачам автоматизации типового документооборота, в частности в таблицах, которые я вообще не касался.