Как стать автором
Поиск
Написать публикацию
Обновить

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

Уровень сложностиПростой
Время на прочтение13 мин
Количество просмотров1.8K
  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

Публикации

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