-
Разбираем структуру (эта статья)
В заключительной части данного цикла, в котором я постарался на минимальном уровне создать более-менее удобную среду для начала (!) экспериментов по изучения возможностей перевода автоматизации работы с документами из 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. Ну, или, хотя бы, пока он не будет расширен до уровня, требуемой для типовых задач автоматизации офисной работы.
Очень надеюсь, что эта серия статей оказалась кому-то полезной! Если будет желание, то я могу сделать продолжение, в котором будут рассмотрены надстройки уже более приближенные к реальным задачам автоматизации типового документооборота, в частности в таблицах, которые я вообще не касался.
ссылка на оригинал статьи https://habr.com/ru/articles/741744/
Добавить комментарий