Продолжаем тему макросов в 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.