среда, 15 апреля 2009 г.

Объект Range и работа с ним.

Последнее время я часто сталкиваюсь с его использованием. На изученных мною ресурсах, посвященных VBA для Word, почему-то мало внимания уделяется такому важному представителю объектной модели. С его помощью можно, поистине, творить чудеса с документом.
Этот объект гораздо удобнее Selection, потому что позволяет не выделять нужную область на экране, а работать прямо с представлением документа в памяти. Это заметно ускоряет работу макросов, которые, по правде сказать, не отличаются быстродействием. При отладке программ бывает полезно воспользоваться методом Range.Select, чтобы убедиться, что вы работаете с нужным вам диапазоном.
Что же это за объект? Сразу хочу предупредить, что писать я буду, основываясь на своем понимании найденного и прочитанного в других источниках. В дословном переводе Range означает «Диапазон». Применительно к Word  — это означает диапазон свойств или методов, доступных для того или иного объекта. Например, как узнать текст второго абзаца в документе? Вот так:
1 ActiveDocument.Paragraphs(2).Range.Text

Стоит отметить, что это вернет весь текст в абзаце с символом конца абзаца (). Его можно удалить функцией Replace заменив символ vbCr на пустую строку.
Но главная фишка объекта Range совсем не в этом. А в том, что его можно передвигать и изменять в размерах практически произвольно (в пределах документа, естественно). У каждого объекта Range есть два свойства: Range.Start и Range.End. Начальный и конечный символ диапазона, считая от начала документа. Начало и конец диапазона можно задавать, указывая эти свойства напрямую, а можно такой конструкцией:
1 2 Dim oRng As Range Set oRng = ActiveDocument.Range(20, 50)

Изменить размер уже существующего диапазона можно с помощью метода SetRange, в котором указать номер символа, с которого диапазон начинается, и каким заканчивается. Этот метод используется тогда, когда нужно изменить уже существующий диапазон. Этот метод ничего не возвращает, а работает со своим родительским объектом
1 2 3 Dim oRng As Range Set oRng = ActiveDocument.Range oRng.SetRange 20, 50

С помощью Range можно получить такие объекты документа, для которых не предусмотрено коллекций, как, например, для абзацев (коллекция Paragraphs). Такими «бесхозными» объектами являются строки и страницы. Кто знает другие, пусть напишет.
Как получить страницу из документа со всем ее содержимым? Коллекции Pages не существует, что же делать? Вот здесь и понадобится Range и его метод GoTo
1 2 3 4 5 6 7 8 9 10 11 12 13 Sub TestGoTo() Dim oRng As Range 'Даем в переменную oRng начало третьей страницы в документе. Set oRng = ThisDocument.Range.GoTo(wdGoToPage, wdGoToNext, , "3") MsgBox "Третья страница начинается с " & oRng.Start & " символа.", 64, "Метод GoTo" 'Расширяем диапазон oRng на всю третью страницу Set oRng = ThisDocument.Range(oRng.Start, oRng.GoToNext(wdGoToPage).Start) MsgBox "На третьей странице находится " & oRng.Paragraphs.Count & " абзацев.", 64, "Метод GoTo" 'Берем 10 строку с третьей страницы Set oRng = oRng.GoTo(wdGoToLine, wdGoToNext, 10) Set oRng = ThisDocument.Range(oRng.Start, oRng.GoToNext(wdGoToLine).Start) MsgBox "В десятой строке третьей страницы содержится " & oRng.Characters.Count & " символов.", 64, "Метод GoTo" End Sub

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

пятница, 27 февраля 2009 г.

Вставка картинок в документ.

Тривиальная, казалось бы, задача. Но иногда возникает необходимость вставить в документ картинку, а подписью сделать имя файла этой картинки. Конечно, можно это делать вручную. Но очень неудобно. Тем более, если картинка не одна.
Вот как эту задачу можно решить с помощью макроса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Sub ДобавлениеКартинок() Dim oPicture As InlineShape Dim sFileName As String, sFileNameShort As String '*********************************************************************************** With Application.FileDialog(msoFileDialogOpen) .AllowMultiSelect = False .ButtonName = "Вставить": .InitialView = msoFileDialogViewPreview: .Title = "Вставить картинку с названием" .Filters.Clear: .Filters.Add "Изображения", "*.jpg;*.jpeg;*.gif;*.png;*.bmp": .Filters.Add "Все файлы", "*.*" If .Show Then sFileName = .SelectedItems(1) Else Exit Sub End With sFileNameShort = CreateObject("Scripting.FileSystemObject").GetFile(sFileName).ShortName sFileNameShort = Mid(sFileNameShort, 1, InStrRev(sFileNameShort, ".") - 1) Set oPicture = ActiveDocument.InlineShapes.AddPicture(sFileName, False, True, Selection.Range) oPicture.Select: Selection.InsertCaption "Рисунок", " " & sFileNameShort End Sub

Как работает? Пользователю дают выбрать файл. Затем этот файл вставляется как картинка в документ и к картинке добавляется автоназвание. Вот и все. Просто и эффективно.

четверг, 5 февраля 2009 г.

Кризис, однако…

В связи с кризисом остается мало времени на обновление данных для блога. Хотя есть чем поделиться. Например, методы работы с классами и коллекциями. Очень полезная штука. А также тонкости работы с графическими объектами в Word (как оказалось они требуют очень бережного отношения).
Сегодня кратко
1 With Application.FileDialog(msoFileDialogFolderPicker)
2 .AllowMultiSelect = False
3 .Title = "Выберите папку с фотографиями"
4 If .Show <> 0 Then
5 sPath = .SelectedItems(1) & "\"
6 Else: MsgBox "Вы не выбрали папку. Запустите макрос снова.": Exit Sub:
7 End If
8 End With

1 With Application.FileDialog(msoFileDialogFilePicker)
2 .AllowMultiSelect = False
3 If .Show <> 0 Then
4 dfile = .SelectedItems(1)
5 Workbooks.Open dfile
6 Else
7 MsgBox "Вы нажали «Отмена»"
8 End If
9 End With

воскресенье, 11 января 2009 г.

Хочу поделиться некоторыми приемами, которые я использую при написании своих поделок на VBA. Приемы эти касаются даже не самой техники программирования, а стиля. Они позволяют сделать тексты программ более аккуратными, читаемыми и понятными.
Основные правила такие:

  1. Объявлять все переменные, которые используются. Чтобы об этом помнить всегда, в настройках редактора VBA установите флажок «Require Variable Declaration». Тогда в начале каждого нового модуля будет вставляться строка Option Explicit. И если вы переменную не объявили, то будет выдано предупреждение.

  2. Использовать понятные имена для функций, процедур, переменных и классов. однобуквенные переменные лучше всего использовать в циклах в качестве счетчиков. Если нет вложенных циклов, то одну переменную, традиционно это i. Если циклы вложенные, тогда i, j, k
  3. и т.д.
  4. Разбивать, при возможности, одну сложную процедуру или функцию на несколько простых.

  5. Использовать отступы для форматирования кода.

  6. Обязательно комментировать код, особенно если вы его передаете другому человеку.

  7. И, наконец, личное: предварять имена функций, переменных и процедур одной буквой, которая указывает на тип данных, возвращаемых данным объектом. Например, iConfig
  8. указывает, что эта переменная объявлена как Integer, или для функции sfCheck f означает, что это функция, а s, что она возвращает строку


Чтобы не мучаться с запоминанием какая буква, за какой тип отвечает, я использую операторы, которые по умолчанию указывают, что если объект начинается с этой буквы, то он такого-то типа.
1 DefBool B'для типа Boolean
2 DefDbl D' для типа Double
3 DefInt I'для типа Integer
4 DefLng L' для типа Long
5 DefObj O' для типа Object
6 DefStr S' для типа String
7 DefVar V' для типа Variant
8 'После этого можно объвить все переменные разных типов одной строкой
9 Dim iCounter, sMyString, oMyObject

Это не все операторы. Все можно узнать в справке по запросу «Deftype Statements»

вторник, 6 января 2009 г.

Работа с макросами из макросов


Заголовок сообщения звучит несколько странно, но, как мне кажется, правильно.
Возникла у меня задача: создать динамическое меню, которое содержало бы в себе все доступные для выполнения макросы. Т.е. чтобы не открывать окно с макросами по Alt+F8, а иметь для этого дела меню, которое к тому же обновлялось автоматически, в зависимости от количества доступных макросов.
Кроме того, что нужно следить за формированием самого меню, которое рассматривается здесь, нужно решить такие задачи:

  1. Определить загруженные шаблоны, макросы из которых доступны для выполнения.

  2. Определить имена модулей, в которых содержится программный код макросов.

  3. В каждом модуле найти собственно сами макросы.

  4. Скомпоновать все это в меню.

  5. Сделать, чтобы все это работало

Для работы с загруженными шаблонами используем свойство приложения Application.Templates. Чтобы определить, есть ли в шаблоне макросы пользуемся свойством VBProject, которое предоставляет некоторые инструменты для работы с содержимым шаблона.
Мной были разработаны некоторые функции:

  • fGetModulesNames — функция, которая определяет имена модулей в конкретном шаблоне;

  • fGetFuncNames — функция, которая определяет имена функций (макросов) в конкретном модуле конкретного шаблона;

  • fGetFuncQuant — функция, которая определяет количество функций (макросов) в конкретном модуле конкретного шаблона;

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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 Public Function fGetFuncQuant(ByVal oCodeModuleName As VBComponent) As Long ' 'Функция определяет количество функций в указанном модуле. ' Dim nCounterOfProc, nEmptyCounter, i As Integer Dim sOld, sNew As String 'Если в указанном модуле есть более 1 строки If oCodeModuleName.codemodule.CountOfLines <> 0 Then 'Определяем номер строки, с которой считать строки кода Do nEmptyCounter = nEmptyCounter + 1 sOld = oCodeModuleName.codemodule.ProcOfLine(nEmptyCounter, vbext_pk_Proc) Loop Until sOld <> "" 'Считаем процедуры в модуле, начиная со строки, номер которой определили выше For i = nEmptyCounter To oCodeModuleName.codemodule.CountOfLines If oCodeModuleName.codemodule.ProcOfLine(i, vbext_pk_Proc) <> "" Then sNew = oCodeModuleName.codemodule.ProcOfLine(i, vbext_pk_Proc) If sNew <> sOld Then sOld = sNew nCounterOfProc = nCounterOfProc + 1 End If End If Next i End If fGetFuncQuant = nCounterOfProc End Function Public Function fGetFuncNames(ByVal oDocOrTemplName As Object, ByVal oCodeModuleName As VBComponent) As Variant ' 'Функция получает имена всех функций в модуле и записывает их в массив. ' Dim sProcNameNew, sProcNameOld As String Dim i, j, k As Integer Dim asFuncNames() As String 'массив для хранения имен функций в модуле ReDim asFuncNames(fGetFuncQuant(oCodeModuleName)) 'задаем размер массива 'Выбираем модуль документа или стандартный модуль с макросами. 'Также проверяем, чтобы в модуле были непустые строки If oCodeModuleName.Type = vbext_ct_StdModule _ Or vbext_ct_Document _ And oCodeModuleName.codemodule.CountOfLines <> 0 Then Do k = k + 1 asFuncNames(0) = oCodeModuleName.codemodule.ProcOfLine(k, vbext_pk_Proc) Loop Until asFuncNames(0) <> "" For i = k To oCodeModuleName.codemodule.CountOfLines If oCodeModuleName.codemodule.ProcOfLine(i, vbext_pk_Proc) <> "" Then sProcNameNew = oCodeModuleName.codemodule.ProcOfLine(i, vbext_pk_Proc) If sProcNameNew <> asFuncNames(j) Then asFuncNames(j + 1) = sProcNameNew j = j + 1 End If End If Next i k = 0 End If fGetFuncNames = asFuncNames End Function Public Function fGetModulesNames(ByVal oDocOrTemplName As Object) As Variant ' 'Процедура определяем имена модулей с макросами в документе или шаблоне и записываем их в массив. ' Dim oCodeModuleName As VBComponent Dim nCounterOfModules As Integer 'счетчик программных модулей Dim asModulesNames() As String 'массив для хранения имен модулей с макросами. После выполнения 'этот массив возвращается как результат функции. 'Определяем количество нужных нам модулей, чтобы затем правильно задать размер массива. Свойство '«Count» компонента «VBProject» не используем, т.к. нужно определить только количество модулей 'определенного типа. For Each oCodeModuleName In oDocOrTemplName.VBProject.VBComponents If oCodeModuleName.Type <> vbext_ct_ClassModule _ And vbext_ct_ActiveXDesigner _ And vbext_ct_MSForm _ And InStr(oCodeModuleName.Name, "NNN") = 0 _ And oCodeModuleName.codemodule.CountOfLines <> 0 Then nCounterOfModules = nCounterOfModules + 1 End If Next oCodeModuleName ReDim asModulesNames(nCounterOfModules - 1) nCounterOfModules = 0 'Записываем в массив имена модулей For Each oCodeModuleName In oDocOrTemplName.VBProject.VBComponents If oCodeModuleName.Type <> vbext_ct_ClassModule _ And vbext_ct_ActiveXDesigner _ And vbext_ct_MSForm _ And InStr(oCodeModuleName.Name, "NNN") = 0 _ And oCodeModuleName.codemodule.CountOfLines <> 0 Then asModulesNames(nCounterOfModules) = oCodeModuleName.Name nCounterOfModules = nCounterOfModules + 1 End If Next oCodeModuleName fGetModulesNames = asModulesNames End Function

суббота, 27 декабря 2008 г.

Обработка событий на уровне приложения.

При работе над совершенствованием и подгонкой под себя «ленточного» интерфейса я столкнулся с необходимостью реагировать на действия пользователя. В частности, при изменении выделенного блока текста.
Справка Word дает довольно скудные сведения об обработке событий. Вы можете в этом убедиться, запросив в «Справке» данные по теме Using Events with the Application Object.
Итак, что же нужно делать?
Во-первых, создать класс. Это делается в редакторе Visual Basic через меню Insert>Class Module. В этом классе нужно назначить ответственного за обработку событий. А затем для каждого необходимого события писать обработчик.
1 2 3 4 5 6 Public WithEvents App As Word.Application 'ответственный за события Private Sub App_WindowSelectionChange(ByVal Sel As Selection) 'Здесь пишем любой код, который должен выполняться. CustomRibbon.InvalidateControl "SetLineSpacing" 'обновление ленты при изменении выделенного диапазона. End Sub

Но это не все. Еще нужно указать в модуле, что для обработки событий мы используем свой собственный класс. Делается это так:
1 2 3 4 5 Dim MyEvents As New EventClassModule 'конструктор нашего класса Sub LoadRibbon(ribbon As IRibbonUI) Set MyEvents.App = Word.Application 'перенаправление событий приложения в объект нашего класса End Sub

Перенаправление событий можно делать в любом месте кода. Чаще всего это нужно делать при открытии документа, либо, как у меня, при загрузке ленты. В Справке Word об этом ничего не сказано. Пришлось доходить самому.
Более подробно об использовании этого события при работе с «ленточным» интерфейсом читаем здесь.

четверг, 25 декабря 2008 г.

Сохранение значений формы в переменных документа

В прошлой заметке я привел несколько макросов, разработанных самостоятельно, которые позволяют сохранять содержимое полей ввода при закрытии формы и восстанавливать их при открытии формы. Пояснять код нет необходимости — он достаточно хорошо раскомментирован. В пояснении, на мой взгляд, нуждаются предпосылки для реализации именно такого алгоритма.
Сохранение значений
Для хранения значений я использовал такое понятие как ActiveDocument.Variables или переменные документа.
Этим я преследовал единственную цель: все данные, которые необходимы для работы программы хранить в одном файле, что очень удобно с точки зрения конечного пользователя.
Работа с переменными документа довольно понятно расписана в справке по Visual Basic for Application. Я хотел бы сказать о другом. О принципе хранения значений.
Каждая переменная в документе получает свое имя (Name), причем не случайное, а содержащее имя компонента, значение которого она содержит. Если компонент содержит несколько значений, например, это список, то кроме имени компонента в имя переменной нужно записать и номер каждой записи списка. Т.е. каждая запись в списке хранится в своей переменной.
Я в приведенном примере пользовался такими именами
1 2 FatherName_ComboBox00 'хранение списка отчеств. Эта переменная хранит первый элемент списка FatherName_ComboBoxSelectedItem 'здесь хранится номер выбранной строки списка отчеств

Если нужно хранить логическое значение True или False, как например для CheckBox, то для этого просто записываем значение как текст.
Нюанс при работе с переменными.
При сохранении каждой переменной присваивается индекс, но не в порядке создания, а по правилам сортировки. У меня возникла ситуация, когда при количестве элементов списка более 10, после переменной с именем FatherName_ComboBox1 (индекс 0) сразу шла переменная FatherName_ComboBox10 (индекс 1), а не FatherName_ComboBox2 как хотелось бы. При чтении переменных это вызывало ошибку, т.к. происходила попытка записи в 10 строку списка, а строки такой еще не было.
Выход из этой ситуации был найден такой. Номер строки списка задавался не одной, двумя цифрами 00, 01 … и т.д. Это позволит без проблем сохранять список из 100 значений. Если нужно больше, то нужно соответственно увеличить количество цифр в номере.