Как стать автором
Обновить

Создание надстроек для отечественного офисного пакета «МойОфис». Часть 4. Разбираем структуру

Уровень сложностиПростой
Время на прочтение13 мин
Количество просмотров1.6K
  1. Введение

  2. Расширяем структуру файлов надстройки и удалённая отладка

  3. Автозаполнение для API и знакомимся с контролами

  4. Разбираем структуру (эта статья)

В заключительной части данного цикла, в котором я постарался на минимальном уровне создать более-менее удобную среду для начала (!) экспериментов по изучения возможностей перевода автоматизации работы с документами из 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)

Взято из pdf документа "Руководство программиста МОДУЛИ НАДСТРОЕК РЕДАКТОРОВ МОЙОФИС"

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

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) Колонтитулы это отдельные области на странице, которые в свою очередь так же состоят из блоков:

Взято из pdf документа "Руководство программиста МОДУЛИ НАДСТРОЕК РЕДАКТОРОВ МОЙОФИС"

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

--Вставка верхнего колонтитула
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, который для проверки результата надо будет загрузить в «МойОфис Текст» вручную, и выглядеть он должен примерно вот так:

Созданный нашей надстройкой документ.
Созданный нашей надстройкой документ.

Вот такой получился простой, учебный пример. Но и он потрепал мне не мало нервов, так как честно говоря, я привык к немного другому.

Краткие выводы:

  1. Работать с API можно. Есть свои особенности работы Lua,  но к ним можно вполне привыкнуть, особенно если немного изучить принципы его работы. Если всё таки придётся писать надстройки - без этого никак!

  2. Само API "сырое" и явно недоделанное. Такое ощущение, что оно лепилось наспех, и а потом просто было заброшено, ибо "есть же!". Судя по тому, как отвечает мне тех.поддрежка в ходе работы над статьями (почти на все мои вопросы: «этого нет, но передадим в отдел разработки»), автоматизацию отложили на лучшие времена, и особо прогресса ждать не приходится. Часть из того что меня, хм, скажем - удивило, я уже написал, а сколько осталось за рамками статьи мне просто лень писать…

  3. Визуальная сторона форм вызывает недоумение. Мне с трудом верится, что я единственный кто озадачен тем, что в текстовых полях  ввода, например, нет wordwrap. Да и вообще, походу многострочного ввод там никто не предусматривал. Нельзя изменить в контролах ни сам шрифт, ни его размер, ни его начертание. Или, почему нет простых но классических контролов "Frame", для визуального и контекстного разделения зон работы с контролами? Нет полос прокрутки. Нет спиннеров. Нет возможности вставить картинки. Но и то что есть, в ряде случаев работает не всегда корректно. Так иногда  при нажатии на кнопки, размеры других контролов вдруг меняются. Да и формы в целом, тоже ещё тот квест! Сама логика построения дизайна с помощью табличного Layout  и агрегаторов ui:Column и ui:Row, конечно современна, но  с точки зрения построения большинства форм, требует слишком немалых усилий чтобы правильно всё расположить! Особенно при том, что нет визуального редактора для GUI, а значит вместо того, чтобы выбрать контролы, быстро их расположить на форме и привязать к обработчикам событий функции для обработки бизнес-логики, прийдется сперва раз н-цать позапускать надстройку, чтобы проверить как всё выглядит. А потом если вдруг, не дай бог, потребуется изменить текст внутри контролов на что то большее по размеру, нежели после такого проектирования! Надписи скорее всего"съедятся" и придётся снова мучатся, меняя расположения и размеры, чтобы привести форму снова  к более-менее нормальному виду.

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

Или как вариант, создать некий набор внутри одной формы:

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

Прошу прощение за качество, но смысл я думаю ясен и с таким!
Прошу прощение за качество, но смысл я думаю ясен и с таким!

Но такие хитрые схемы работы с формами надстроек сами по себе требуют отдельной серии статей для обсуждения! И честно говоря, как по мне, такие методы являются некоторым «извращением» над здравым смыслом, так как, судя по всему, изначально в API такая схема работы не предусмотрена!

Итог

Заканчивая эту вводную серию статей, хочу подвести итог:

Пользоваться надстройками можно безусловно и в том состоянии, каком сейчас предлагают разработчики. Да, работать сложно и требует переучивания на Lua и пристального изучения API надстроек SDK «МойОфис». Но, при определённых усилиях, вполне можно сделать что-то напоминающее макросы VBA от Microsoft Office. Хотя, думаю что массовый перевод наработок в макросах будет, скорее всего, болезненным и тяжёлым. А кое-что перевести не удастся совсем, поскольку пока API «МойОфис» не сильно приближён к API Microsoft Office. Ну, или, хотя бы, пока он не будет расширен до уровня, требуемой для типовых задач автоматизации офисной работы.

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

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии35

Публикации

Истории

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань