
Продолжаем тему макросов в Microsoft Word. Будем доделывать пользовательский интерфейс для нашего макроса замены двух и более последовательных переводов строки на единственный. Зачем вообще может понадобиться какой-то интерфейс для макроса? Ну, например, мы хотим удалить лишние переводы строки на всех страницах документа, кроме каких-то конкретных. Интерфейс позволил бы указать номера страниц документа, которые нужно при обработке пропустить (либо наоборот, обработать только указанные страницы). Вот этот функционал и будем реализовывать.
Открываем уже знакомое окно VBA, нажав "Visual Basic" на вкладке "Разработчик". Добавим к нашим макросам новую форму, кликнув правой кнопкой по "Normal" и выбрав "Insert" -> "UserForm":

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

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

Просмотреть форму в её рабочем виде можно, кликнув на ней в редакторе и нажав 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" в редакторе, и это приведёт к автоматической генерации кода для обработчика события нажатия по кнопке. У меня он выглядит так:
|
1 2 3 |
Private Sub RunMacros_Click() End Sub |
Пока что оставим этот обработчик в покое и под ним сделаем функцию, которая будет парсить введённые через запятую номера страниц.
|
1 2 3 4 5 6 7 8 9 10 11 |
Private Function ParsePageNumbers() Dim pageNumbersCollection As New Collection Dim pageNumbers() As String pageNumbers() = Split(Me.ExcessiveLineBreaksPageNumbers.Text, ",") On Error Resume Next Dim i As Integer For i = 0 To UBound(pageNumbers) pageNumbersCollection.Add True, Trim$(pageNumbers(i)) Next Set ParsePageNumbers = pageNumbersCollection End Function |
В функции мы создаём новую коллекцию pageNumbersCollection (по сути, индексированный ассоциативный массив), а также массив pageNumbers. В строке 4 мы в этот массив помещаем разбитые номера страниц, которые указал пользователь макроса в поле ввода ExcessiveLineBreaksPageNumbers (обратите внимание на префикс Me - мы обращаемся к текущей форме). Затем мы в строке 5 игнорируем ошибки. Далее в цикле добавляем все номера страниц в коллекцию pageNumbersCollection. Если будут встречаться дублирующиеся номера страниц, то ошибка возникать не будет - мы их игнорируем. Обратите внимание, что при возврате из функции или присваивании переменной любого объекта, созданного через New, в Visual Basic нужно использовать ключевое слово Set.
Теперь вернёмся к обработчику RunMacros_Click и напишем код, который будет вызывать наш макрос с нужными параметрами. Я напишу комментарии прямо в код, так как он достаточно длинный:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
Private Sub RunMacros_Click() ' При любой ошибке переходим к метке ErrorHandler On Error GoTo ErrorHandler Dim pageNumbers As Collection ' Если чекбокс RemoveExcessiveLineBreaks установлен, ' парсим введённые номера страниц If Me.RemoveExcessiveLineBreaks.value = True Then Set pageNumbers = ParsePageNumbers() End If ' Скрываем нашу форму Me.Hide ' Активируем текущий документ ActiveDocument.Activate ' Если чекбокс RemoveExcessiveLineBreaks установлен... If Me.RemoveExcessiveLineBreaks.value = True Then ' ...выполняем функцию RemoveExcessiveEntersImpl ' Её мы напишем позже. В эту функцию передаём ' номера страниц, а также значение ' радио-кнопки ExcessiveLineBreaksIncludePages RemoveExcessiveEntersImpl pageNumbers, _ Me.ExcessiveLineBreaksIncludePages.value ' Коллекция с номерами страниц больше не нужна Set pageNumbers = Nothing End If ' Всё окей, переходим к метке Finish GoTo Finish ErrorHandler: ' Если ошибка - покажем сообщение с её текстом MsgBox Err.Description, vbError, "Error" Finish: MsgBox "Done!", vbInformation, "Ready" End Sub |
Остаётся реализовать функцию RemoveExcessiveLineBreaksImpl, которая будет принимать два параметра: коллекцию с номерами страниц, которые нужно либо пропустить, либо наоборот обработать только их. Второй параметр (значение радио-кнопки ExcessiveLineBreaksIncludePages) как раз и будет говорить о том, что нужно сделать с этими номерами (True - включить в обработку только их, False - исключить их из обработки). Пользователь сможет также не указывать никакие номера страниц, в этом случае коллекция будет пустой, и мы будем игнорировать настройку.
Перейдём в файл модуля AllMacros, кликнув по его имени два раза. Его я реализовывал в прошлой статье. Напишем несколько вспомогательных приватных функций. Нам потребуется определять, есть ли ключ в коллекции, чтобы узнать, есть ли текущая страница в списке страниц, заданном пользователем:
|
1 2 3 4 5 6 |
Private Function HasKey(coll As Collection, key As String) As Boolean On Error Resume Next coll (key) HasKey = (Err.Number = 0) Err.Clear End Function |
Коллекции в Visual Basic весьма урезанные: ключи могут быть только строками, поэтому я и не преобразовывал номера страниц в числа, и функция HasKey тоже принимает значение key типа String. В этой функции мы пытаемся запросить из коллекции заданный ключ (строка 3). Если что-то пошло не так, мы получим ошибку, которую обработаем обработчиком из строки 2. Если номер ошибки не нулевой (т.е. ошибка произошла), значит, ключ в коллекции отсутствует. Вернём это значение из функции HasKey и очистим за собой ошибку.
Теперь напишем вспомогательную функцию, которая позволит определить, следует ли на текущей странице выполнять удаление излишних переводов строк:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
Private Function NeedToProcessCurrentPage(pageNumbers As Collection, _ includePageNumbers As Boolean) As Boolean ' Если коллекция не передана, обрабатываем ' все страницы If pageNumbers Is Nothing Then NeedToProcessCurrentPage = True Exit Function End If ' Если не передано ни одного номера страницы, ' значит всегда обрабатываем всё If pageNumbers.Count = 0 Then NeedToProcessCurrentPage = True Exit Function End If ' Определим, есть ли текущий номер страницы ' в переданной коллекции. Номер берём из ' текущего выделения и преобразуем в строку Dim hasPageInCollection As Boolean hasPageInCollection = HasKey(pageNumbers, _ CStr(Selection.Range.Information(wdActiveEndPageNumber))) ' Если includePageNumbers = True, то обрабатываем страницу, ' только если её номер есть в коллекции. А иначе ' обрабатываем её, только если в коллекции её номер ' отсутствует If includePageNumbers = True Then NeedToProcessCurrentPage = hasPageInCollection Else NeedToProcessCurrentPage = Not hasPageInCollection End If End Function |
Наконец, основная функция, которая выполнит нужную работу по заменам:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Sub RemoveExcessiveLineBreaksImpl(pageNumbers As Collection, _ includePageNumbers As Boolean) Dim lineBreakSearchRegExp As String lineBreakSearchRegExp = GetLineBreakSearchRegExp() Selection.HomeKey Unit:=wdStory While FindNextText(lineBreakSearchRegExp, True) = True If NeedToProcessCurrentPage(pageNumbers, includePageNumbers) Then RemoveNextEnters End If Wend ClearFindAndReplaceParameters End Sub |
Обратите внимание, что эта функция не приватная (публичная, по умолчанию), так как она должна быть доступна из нашей формы. В списке макросов Word эта функция не появится, так как она принимает аргументы, а в списке появляются только те функции, которые не имеют ни одного аргумента. Функция практически полностью идентична макросу RemoveExcessiveEnters из первой части статьи за исключением того, что она принимает пару аргументов и перед вызовом RemoveNextEnters выполняет проверку, нужно ли производить замену, вызывая NeedToProcessCurrentPage. Чтобы избежать дублирования кода, подправим наш старый макрос RemoveExcessiveEnters, чтобы он тоже вызывал эту функцию:
|
1 2 3 |
Sub RemoveExcessiveEnters() RemoveExcessiveLineBreaksImpl Nothing, False End Sub |
Так мы сохранили и старый макрос, и избежали дублирования кода: этот макрос теперь вызовет новую функцию с параметрами замены лишних переводов строк на всех страницах (это он и делал раньше). Последнее, что нам требуется, это функция, которая будет отображаться в списке макросов Word и открывать нашу форму. Здесь всё совсем просто:
|
1 2 3 |
Sub FixDocument() DocumentFixer.Show End Sub |
Теперь макрос можно проверять! Нажимаем "Разработчик" -> "Макросы" и выбираем в списке макросов "FixDocument". Откроется наша форма, в которой можно задать параметры и нажать "Run", что приведёт к выполнению макроса!
Есть несколько улучшений, которые было бы неплохо добавить. Во-первых, при выполнении макроса на большом документе экран Word будет постоянно обновляться, что приведёт к торможению Word'а и неприятным визуальным эффектам. Кроме того, нет возможности отследить прогресс выполнения. Давайте всё это доработаем. Начнём с опций запрета обновления документа во время выполнения макросов. На форму я добавлю чекбокс с именем DisableScreenUpdates, а в код обработчика клика по кноке "Run" следующую строку:
|
1 2 3 4 5 6 7 8 |
... Me.Hide ' Новая строка между двумя старыми If Me.DisableScreenUpdates.value = True Then Application.ScreenUpdating = False ActiveDocument.Activate ... |
Теперь экран Word в процессе выполнения макросов обновляться не будет. Отлично, теперь перейдём к отображению прогресса операции. Также, сделаем возможность отменить операцию. Вдруг мы запустили наши макросы на огромном документе, указав какие-то некорректные настройки. В этом случае идеальным было бы отменить выполнение, поправить настройки, и запустить макросы заново. Добавим новую форму и назовём ее MacroProgressForm. Накидаем на неё несколько контролов: лейбл, который будет отображать текущую операцию (StepName), полосу прогресса текущей операции (ProgressBarLabel), лейбл, отображающий прогресс в текстовом виде (StepProgress), и кнопку отмены (CancelMacro). У меня форма выглядит так:

Полосу прогресса я сделал из двух лейблов (один - прогресс расположен над другим - рамка), поменяв им стили и цвета. Переходим к коду формы. Можно кликнуть в редакторе на форме правой кнопкой мыши и выбрать "View code". Создадим три переменные, приватные и доступные только форме. Первая будет содержать максимальное значение прогресса выполнения текущей операции, вторая - текущий прогресс, а третья - флаг, отменил ли пользователь операцию:
|
1 2 3 |
Private maxProgress As Integer Private currentProgress As Integer Private progressCancelled As Boolean |
Далее сделаем обработчик нажатия кнопки отмены:
|
1 2 3 4 5 6 |
Private Sub CancelMacro_Click() If MsgBox("Do you want to cancel your fixing operations?", _ vbQuestion Or vbYesNo, "Cancel?") = vbYes Then progressCancelled = True End If End Sub |
Тут мы спрашиваем, действительно ли требуется отменить все макросы, и если пользователь соглашается, присваиваем переменной progressCancelled значение True. Далее напишем ряд функций, которые будут управлять прогрессом выполнения. Наша форма может запускать несколько макросов подряд (хотя мы пока реализовали только один). Предусмотрим это, и напишем функции, которыми смогут пользоваться макросы:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
' Увеличивает прогресс на единицу ' Возвращает False, если операция была отменена Public Function IncreaseProgress() As Boolean IncreaseProgress = IncreaseProgressImpl(currentProgress + 1) End Function ' Устанавливает текущее значение ' прогресса, равное номеру текущей страницы. ' Возвращает False, если операция была отменена Public Function SetSelectionPageNumber() As Boolean SetSelectionPageNumber = IncreaseProgressImpl( _ Selection.Information(wdActiveEndPageNumber)) End Function ' Устанавливает новое значение прогресса, ' переданное в функцию. ' Возвращает False, если операция была отменена Private Function IncreaseProgressImpl(newProgress As Integer) As Boolean currentProgress = newProgress If currentProgress > maxProgress Then currentProgress = maxProgress ' Меняем ширину лейбла-прогрессбара Me.ProgressBarLabel.Width = _ (currentProgress / maxProgress) * (Me.ProgressBarBackLabel.Width - 4) ' Обновляем текстовое значение прогресса RefreshProgressText ' Возвращаем False, если операция была отменена ' нажатием кнопки отмены IncreaseProgressImpl = Not progressCancelled End Function ' Устанавливает максимально допустимое значение ' прогресса. ' Возвращает False, если операция была отменена Public Function SetMaxProgress(value As Integer) maxProgress = value currentProgress = 0 Me.ProgressBarLabel.Width = 0 Me.ProgressBarBackLabel.Visible = True Me.ProgressBarLabel.Visible = True RefreshProgressText SetMaxProgress = Not progressCancelled End Function ' Устанавливает максимально допустимое значение ' прогресса исходя из общего количества ' страниц в документе. ' Возвращает False, если операция была отменена Public Function SetMaxProgressByPageCount() SetMaxProgressByPageCount = SetMaxProgress( _ ActiveDocument.ComputeStatistics(wdStatisticPages)) End Function ' Проверяет, была ли отменена операция, и возвращает ' True, если была. Public Function CheckCancel() ' Обрабатываем накопившиеся оконные ' события Windows, ' чтобы наша форма не зависала DoEvents CheckCancel = progressCancelled End Function ' Устанавливает название текущей операции Public Sub SetStep(name As String) Me.StepName.Caption = name & "..." Me.StepProgress.Caption = "" Me.ProgressBarBackLabel.Visible = False Me.ProgressBarLabel.Visible = False DoEvents End Sub ' Обновляет текстовое значение прогресса ' и обрабатывает накопившиеся ' оконные события Windows Private Sub RefreshProgressText() Me.StepProgress.Caption = currentProgress & "/" & maxProgress DoEvents End Sub |
Флоу будет следующим:
- Макрос получает на вход инстанс формы прогресса, у которой он сможет вызывать публичные функции.
- Макрос вызывает
SetStep, передавая туда описание того, что он делает. Форма прогресса отобразит это описание и скроет полосу и лейбл прогресса (пока что прогресс неизвестен). - Макрос вызывает
SetMaxProgres, указывая максимальное значение прогресса выполнения операции. Как вариант, макрос сможет вызватьSetMaxProgressByPageCount, тогда это значение установится равным общему количеству страниц в документе. Наш макрос удаления лишних переводов строки так и будет делать. Он заранее не знает, сколько существует мест в документе, где есть последовательные переводы строк. Этот вызов приведёт к отображению полосы и лейбла прогресса, так как теперь форма знает его максимальное значение. - Макрос вызывает
IncreaseProgressилиSetSelectionPageNumberдля того, чтобы увеличить прогресс выполнения операции. Форма автоматически обновится и всё отрисует. - Макрос может либо проверять возвращаемые значения из перечисленных выше функций, либо вызывать регулярно
CheckCancel, чтобы определить, не была ли операция отменена.
Нам нужна ещё одна последняя функция:
|
1 2 3 |
Public Sub ResetCancelState() progressCancelled = False End Sub |
Она сбрасывает флаг отмены выполнения, и вызывать эту функцию будет наша первая форма перед началом выполнения всех включённых макросов.
Теперь доработаем нашу функциюRemoveExcessiveLineBreaksImpl, чтобы она могла работать с формой прогресса и отмены. Добавим в эту функцию соответствующий аргумент и будем с ним взаимодействовать. Я напишу весь код полностью и помечу доработки комментариями:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
' Новый аргумент progressForm Sub RemoveExcessiveLineBreaksImpl(pageNumbers As Collection, _ includePageNumbers As Boolean, progressForm As MacroProgressForm) ' Если progressForm передана, то устанавливаем название ' текущей операции и максимальное значение прогресса ' исходя из общего количества страниц в документе. ' Если SetMaxProgressByPageCount вернула False, значит, ' операция уже была отменена, выходим. If Not progressForm Is Nothing Then progressForm.SetStep "Removing excessive line breaks" If progressForm.SetMaxProgressByPageCount() = False Then Exit Sub End If Dim lineBreakSearchRegExp As String lineBreakSearchRegExp = GetLineBreakSearchRegExp() Selection.HomeKey Unit:=wdStory While FindNextText(lineBreakSearchRegExp, True) = True If NeedToProcessCurrentPage(pageNumbers, includePageNumbers) Then RemoveNextEnters End If ' Если форма передана, то увеличиваем прогресс ' исходя из текущего номера страницы, где у нас ' находится курсор, а затем проверяем возвращаемое ' значение. Если вернулось False, то операцию ' отменили, выходим. If Not progressForm Is Nothing Then If progressForm.SetSelectionPageNumber() = False Then ClearFindAndReplaceParameters Exit Sub End If End If Wend ClearFindAndReplaceParameters End Sub |
Изменений не так и много, и вот уже наш макрос готов работать с новым интерфейсом. Не забудем также дописать старую функцию RemoveExcessiveEnters:
|
1 2 3 4 5 |
Sub RemoveExcessiveEnters() ' Добавился последний "Nothing" - в этом случае ' мы форму прогресса не передаём. RemoveExcessiveLineBreaksImpl Nothing, False, Nothing End Sub |
Наконец, доработаем форму DocumentFixer, чтобы она в те макросы, которые вызывает, передавала подготовленную форму прогресса. Ну и отображала её заодно. Тут изменений совсем мало, нет смысла перепечатывать весь код. После строки ActiveDocument.Activate добавляем:
|
1 2 |
MacroProgressForm.Show MacroProgressForm.ResetCancelState |
При вызове макроса теперь передаём форму прогресса:
|
1 2 3 4 5 6 |
If Me.RemoveExcessiveLineBreaks.value = True Then RemoveExcessiveLineBreaksImpl pageNumbers, _ Me.ExcessiveLineBreaksIncludePages.value, _ MacroProgressForm Set pageNumbers = Nothing End If |
В конце, сразу после метки Finish добавляем строку, чтобы скрыть форму прогресса:
|
1 |
MacroProgressForm.Hide |
И это всё! Теперь наш модный макрос полностью поддерживает отображение прогресса выполнения операции, и мы имеем возможность отменить его выполнение в любой момент времени. При этом мы сохранили изначальную работоспособность старого макроса RemoveExcessiveEnters, который был написан еще в первой части статьи. Вот как выглядит форма отображения прогресса во время работы:

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

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