Пишем макросы для Microsoft Word с GUI like a PRO [Часть 2, финал]

Final word macro form view

Да, это макрос для MS Word!

Продолжаем тему макросов в Microsoft Word. Будем доделывать пользовательский интерфейс для нашего макроса замены двух и более последовательных переводов строки на единственный. Зачем вообще может понадобиться какой-то интерфейс для макроса? Ну, например, мы хотим удалить лишние переводы строки на всех страницах документа, кроме каких-то конкретных. Интерфейс позволил бы указать номера страниц документа, которые нужно при обработке пропустить (либо наоборот, обработать только указанные страницы). Вот этот функционал и будем реализовывать.

Открываем уже знакомое окно VBA, нажав "Visual Basic" на вкладке "Разработчик". Добавим к нашим макросам новую форму, кликнув правой кнопкой по "Normal" и выбрав "Insert" -> "UserForm":

Word add UserForm

Будет отображена новая форма с именем UserForm1. Переименовать её, как и поменять другие свойства формы, можно на вкладке "Properties":

Edit UserForm in Word

Я назову форму DocumentFixer. Накидаем на форму элементов управления. Добавим рамочку, где будут настройки и чекбокс активации нашего макроса. При клике по форме появится Toolbox, из которого можно выбрать контролы для размещения на форме:

Word UserForm toolbox

После помещения на форму некоторого количества контролов она выглядит так:

Word macro UserForm

Просмотреть форму в её рабочем виде можно, кликнув на ней в редакторе и нажав F5 или кнопку "Run" в интерфейсе редактора VBA, которой можно также запускать макросы для отладки. На форму я добавил:

  • Рамку (Frame) "Line breaks", где находятся все настройки для нашего макроса.
  • Чекбокс "Remove excessive line breaks", который будет включать или отключать выполнение нашего макроса. Назвал я этот элемент RemoveExcessiveLineBreaks.
  • Две радио-кнопки "Include pages" (с именем ExcessiveLineBreaksIncludePages) и "Exclude pages" (с именем ExcessiveLineBreaksExcludePages), которые позволяют указать, какие страницы следует обработать или исключить из обработки, соответственно. Радио-кнопки должны иметь одинаковое значение свойства GroupName, чтобы быть связанными. Я задал значение этого свойства ExcessiveLineBreaksPageOption.
  • Текстовое поле (с именем ExcessiveLineBreaksPageNumbers) с лейблом "Comma-separated page numbers", куда можно будет ввести номера страниц через запятую.
  • Кнопку "Run" с именем RunMacros, которая будет запускать выбранные макросы (у нас пока только один).

Да, это, конечно, не гибкий HTML и не WPF, но реализовать какой-никакой графический интерфейс вполне реально. Кликнем дважды на кнопке "Run" в редакторе, и это приведёт к автоматической генерации кода для обработчика события нажатия по кнопке. У меня он выглядит так:

Пока что оставим этот обработчик в покое и под ним сделаем функцию, которая будет парсить введённые через запятую номера страниц.

В функции мы создаём новую коллекцию pageNumbersCollection (по сути, индексированный ассоциативный массив), а также массив pageNumbers. В строке 4 мы в этот массив помещаем разбитые номера страниц, которые указал пользователь макроса в поле ввода ExcessiveLineBreaksPageNumbers (обратите внимание на префикс Me - мы обращаемся к текущей форме). Затем мы в строке 5 игнорируем ошибки. Далее в цикле добавляем все номера страниц в коллекцию pageNumbersCollection. Если будут встречаться дублирующиеся номера страниц, то ошибка возникать не будет - мы их игнорируем. Обратите внимание, что при возврате из функции или присваивании переменной любого объекта, созданного через New, в Visual Basic нужно использовать ключевое слово Set.

Теперь вернёмся к обработчику RunMacros_Click и напишем код, который будет вызывать наш макрос с нужными параметрами. Я напишу комментарии прямо в код, так как он достаточно длинный:

Остаётся реализовать функцию RemoveExcessiveLineBreaksImpl, которая будет принимать два параметра: коллекцию с номерами страниц, которые нужно либо пропустить, либо наоборот обработать только их. Второй параметр (значение радио-кнопки ExcessiveLineBreaksIncludePages) как раз и будет говорить о том, что нужно сделать с этими номерами (True - включить в обработку только их, False - исключить их из обработки). Пользователь сможет также не указывать никакие номера страниц, в этом случае коллекция будет пустой, и мы будем игнорировать настройку.

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

Коллекции в Visual Basic весьма урезанные: ключи могут быть только строками, поэтому я и не преобразовывал номера страниц в числа, и функция HasKey тоже принимает значение key типа String. В этой функции мы пытаемся запросить из коллекции заданный ключ (строка 3). Если что-то пошло не так, мы получим ошибку, которую обработаем обработчиком из строки 2. Если номер ошибки не нулевой (т.е. ошибка произошла), значит, ключ в коллекции отсутствует. Вернём это значение из функции HasKey и очистим за собой ошибку.

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

Наконец, основная функция, которая выполнит нужную работу по заменам:

Обратите внимание, что эта функция не приватная (публичная, по умолчанию), так как она должна быть доступна из нашей формы. В списке макросов Word эта функция не появится, так как она принимает аргументы, а в списке появляются только те функции, которые не имеют ни одного аргумента. Функция практически полностью идентична макросу RemoveExcessiveEnters из первой части статьи за исключением того, что она принимает пару аргументов и перед вызовом RemoveNextEnters выполняет проверку, нужно ли производить замену, вызывая NeedToProcessCurrentPage. Чтобы избежать дублирования кода, подправим наш старый макрос RemoveExcessiveEnters, чтобы он тоже вызывал эту функцию:

Так мы сохранили и старый макрос, и избежали дублирования кода: этот макрос теперь вызовет новую функцию с параметрами замены лишних переводов строк на всех страницах (это он и делал раньше). Последнее, что нам требуется, это функция, которая будет отображаться в списке макросов Word и открывать нашу форму. Здесь всё совсем просто:

Теперь макрос можно проверять! Нажимаем "Разработчик" -> "Макросы" и выбираем в списке макросов "FixDocument". Откроется наша форма, в которой можно задать параметры и нажать "Run", что приведёт к выполнению макроса!

Есть несколько улучшений, которые было бы неплохо добавить. Во-первых, при выполнении макроса на большом документе экран Word будет постоянно обновляться, что приведёт к торможению Word'а и неприятным визуальным эффектам. Кроме того, нет возможности отследить прогресс выполнения. Давайте всё это доработаем. Начнём с опций запрета обновления документа во время выполнения макросов. На форму я добавлю чекбокс с именем DisableScreenUpdates, а в код обработчика клика по кноке "Run" следующую строку:

Теперь экран Word в процессе выполнения макросов обновляться не будет. Отлично, теперь перейдём к отображению прогресса операции. Также, сделаем возможность отменить операцию. Вдруг мы запустили наши макросы на огромном документе, указав какие-то некорректные настройки. В этом случае идеальным было бы отменить выполнение, поправить настройки, и запустить макросы заново. Добавим новую форму и назовём ее MacroProgressForm. Накидаем на неё несколько контролов: лейбл, который будет отображать текущую операцию (StepName), полосу прогресса текущей операции (ProgressBarLabel), лейбл, отображающий прогресс в текстовом виде (StepProgress), и кнопку отмены (CancelMacro). У меня форма выглядит так:

Word macro progress form

Полосу прогресса я сделал из двух лейблов (один - прогресс расположен над другим - рамка), поменяв им стили и цвета. Переходим к коду формы. Можно кликнуть в редакторе на форме правой кнопкой мыши и выбрать "View code". Создадим три переменные, приватные и доступные только форме. Первая будет содержать максимальное значение прогресса выполнения текущей операции, вторая - текущий прогресс, а третья - флаг, отменил ли пользователь операцию:

Далее сделаем обработчик нажатия кнопки отмены:

Тут мы спрашиваем, действительно ли требуется отменить все макросы, и если пользователь соглашается, присваиваем переменной progressCancelled значение True. Далее напишем ряд функций, которые будут управлять прогрессом выполнения. Наша форма может запускать несколько макросов подряд (хотя мы пока реализовали только один). Предусмотрим это, и напишем функции, которыми смогут пользоваться макросы:

Флоу будет следующим:

  1. Макрос получает на вход инстанс формы прогресса, у которой он сможет вызывать публичные функции.
  2. Макрос вызывает SetStep, передавая туда описание того, что он делает. Форма прогресса отобразит это описание и скроет полосу и лейбл прогресса (пока что прогресс неизвестен).
  3. Макрос вызывает SetMaxProgres, указывая максимальное значение прогресса выполнения операции. Как вариант, макрос сможет вызвать SetMaxProgressByPageCount, тогда это значение установится равным общему количеству страниц в документе. Наш макрос удаления лишних переводов строки так и будет делать. Он заранее не знает, сколько существует мест в документе, где есть последовательные переводы строк. Этот вызов приведёт к отображению полосы и лейбла прогресса, так как теперь форма знает его максимальное значение.
  4. Макрос вызывает IncreaseProgress или SetSelectionPageNumber для того, чтобы увеличить прогресс выполнения операции. Форма автоматически обновится и всё отрисует.
  5. Макрос может либо проверять возвращаемые значения из перечисленных выше функций, либо вызывать регулярно CheckCancel, чтобы определить, не была ли операция отменена.

Нам нужна ещё одна последняя функция:

Она сбрасывает флаг отмены выполнения, и вызывать эту функцию будет наша первая форма перед началом выполнения всех включённых макросов.

Теперь доработаем нашу функциюRemoveExcessiveLineBreaksImpl, чтобы она могла работать с формой прогресса и отмены. Добавим в эту функцию соответствующий аргумент и будем с ним взаимодействовать. Я напишу весь код полностью и помечу доработки комментариями:

Изменений не так и много, и вот уже наш макрос готов работать с новым интерфейсом. Не забудем также дописать старую функцию RemoveExcessiveEnters:

Наконец, доработаем форму DocumentFixer, чтобы она в те макросы, которые вызывает, передавала подготовленную форму прогресса. Ну и отображала её заодно. Тут изменений совсем мало, нет смысла перепечатывать весь код. После строки ActiveDocument.Activate добавляем:

При вызове макроса теперь передаём форму прогресса:

В конце, сразу после метки Finish добавляем строку, чтобы скрыть форму прогресса:

И это всё! Теперь наш модный макрос полностью поддерживает отображение прогресса выполнения операции, и мы имеем возможность отменить его выполнение в любой момент времени. При этом мы сохранили изначальную работоспособность старого макроса RemoveExcessiveEnters, который был написан еще в первой части статьи. Вот как выглядит форма отображения прогресса во время работы:

Word macro progress form in action

Числа 60/893 внизу - это номер страницы, на которой была осуществлена последняя замена, и общее число страниц в документе до начала выполнения макросов. А вот так выглядит запрос на отмену:

Word macro cancel request

Можно ещё дальше совершенствовать нашу инфраструктуру макросов. На форму DocumentFixer можно добавить любое количество макросов с настройками, и все они смогут работать единообразно с формой отображения прогресса и отмены операции. Можно также, например, поработать с историей операций: сейчас Word логирует каждое мельчайшее действие, выполненное макросом, а можно сделать так, чтобы часть этих действий объединялась в именованные группы с осмысленными названиями, и в меню отмены не будет такой каши из большого количества непонятных операций.

Я же закончу заметку на уже достигнутом. Если кому-то потребуется улучшение из перечисленных выше, возможно, сделаю отдельную статью на этот счёт. В качестве бонусного хардкора на форму я добавил картинку, и скриншот этой формы есть в самом начале статьи. Также, при выключении чекбокса RemoveExcessiveLineBreaks будут заблокированы все контролы, имеющие отношение к нашему макросу (и разблокированы обратно при включении чекбокса). Кому интересно, сможет ознакомиться с тем, как это реализовано, скачав полные исходники макросов и форм и имортировав их к себе в Word.

Добавить комментарий

Ваш адрес email не будет опубликован.