Программная работа с документами Word
Общий план решения задачи
Давайте перейдем теперь к решению поставленной задачи и программным текстам. В целом процесс решения можно представить, состоящим из следующих основных этапов:
- Составление списка персон (фамилия, имя, отчество) и предъявление этого списка пользователю.
- Составление списка персон, выбранных пользователем.
- Для каждой выбранной персоны получение информации, требуемой для контакта в Outlook и создание очередного контакта. Одновременно с этим создание события, связанного с контактом.
И я начну с того, что приведу описание глобальных переменных, которые будут использоваться в процессе решения задачи. Эти переменные описаны в модуле Tool, в который и будут помещены все основные процедуры, требуемые для решения задачи.
Option Explicit 'Объект Outlook и его компоненты Public myOl As Outlook.Application, olNameSpace As NameSpace 'Коллекция избранных личностей Public CollectionOfPersons As New Collection 'Коллекция номеров абзацев, задающих начало записей Public Numbers As New Collection Public Con As New Collection 'Определение типа - записи, характеризующей личность Public Type Person FirstName As String LastName As String MiddleName As String Post As String DOB As Date Address As String Tel As String Email As String Fax As String Other As String End TypeЛистинг 2.27.
Нам потребовалось описать объект Outlook и связанный с ним объект NameSpace, задающий пространство имен. Описание этих объектов необходимо для обеспечения взаимодействия двух приложений, с тем, чтобы мы могли в проекте документа Word работать с объектами Outlook. Коллекция CollectionOfPersons содержит фамилии персон, входящих в справочник. Две следующие коллекции Numbers и Con носят вспомогательный характер, но важный для общего понимания алгоритма решения. Первая из них содержит номера абзацев, начинающих описание персоны, вторая - порядковые номера персон, отобранных пользователем. Создание этих коллекций позволяет избежать лишних просмотров полного текста документа и получать доступ непосредственно к нужному абзацу. Наконец, в разделе общих объявлений дано определение пользовательского типа Person, формально описывающего запись и поля этой записи, которые будут заполняться в процессе анализа записанной информации, а затем будут переноситься в поля контакта Outlook.
Приведу теперь текст основной процедуры модуля Tool, с вызова которой и начинается решение нашей задачи:
Public Sub WTOOL() 'Процедура преобразует справочник персоналий 'в базу данных Контакты приложения Outlook 'Формирование списка персоналий и выбор пользователя Call FormList 'Создание записей для избранных Call SelectPerson End SubЛистинг 2.28.
Она, как это и должно быть для основных процедур, достаточно проста, чтобы можно было понять ее действие. Кроме комментариев она содержит вызовы двух процедур, решающих задачи первого и третьего этапа в соответствии с нашим планом. А как же быть со вторым этапом, спросите Вы. Конечно же, и он не забыт. Просто вызываемая на первом этапе процедура FormList заканчивает свою работу вызовом формы с созданным списком персоналий, с которым и будет работать пользователь. В ответ на его выбор будет вызываться обработчик соответствующего события, в котором и будет решена задача второго этапа. После чего продолжит свою работу процедура WTOOL, вызвав процедуру SelectPerson. Эта процедура и выполняет, по существу, главную работу, анализируя содержимое соответствующих фрагментов текстового документа, создавая запись и преобразуя ее содержимое в контакт приложения Outlook.
Создание списка персоналий
Для представления списка персоналий я спроектировал, как обычно, форму, в которую поместил элемент управления типа ListBox и командную кнопку. Элементы списка ListBox будут отображать фамилии персон справочника, а обработчик события, возникающего при нажатии командной кнопки, будет вызывать процедуру, выполняющую нужные действия в ответ на выбор пользователя. О том, как проектировать подобную форму, как заполнять элементы списка ListBox, говорилось уже неоднократно, в том числе и в данной лекции. Поэтому основные объяснения будут связаны с тем, как из текстового документа выделить фрагменты, соответствующие записи информации об очередной персоне, как выделить фамилию персоны, отображаемую в списке. Рассмотрением этих вопросов сейчас и займемся. Начну с текста процедуры FormList, решающей поставленную задачу:
Public Sub FormList() 'Эта процедура формирует список личностей Dim par As Paragraph, parStyle As String Dim i As Integer, n As Integer With ActiveDocument parStyle = .Paragraphs(1).Style n = ActiveDocument.Paragraphs.Count i = 1 For Each par In .Paragraphs If par.Style = parStyle Then 'Добавить элемент в список и номер абзаца в коллекцию frmPersons.lstPersons.AddItem par.Range.Text Numbers.Add i End If i = i + 1 Next par End With frmPersons.Show End SubЛистинг 2.29.
Как видите, процедура, полностью решающая задачу, получилась довольно короткая и простая. Все это, конечно, благодаря тем возможностям, которые предоставляет Word для этих целей. В цикле (конечно же, For Each ) по всем абзацам текста получаем очередной абзац - объект par. Далее существенно используется тот факт, что информация о каждой персоне начинается с нового абзаца, имеющего специальный стиль, в данном случае, задаваемый переменной parStyle, характерный только для таких абзацев. Используется также и тот факт, что этот абзац содержит только фамилию, имя и отчество персоны.
Затем создаются элементы списка формы. Нетрудно догадаться, что frmPersons - это имя формы, lstPersons - имя списка (элемента управления ListBox ) этой формы. Хочу обратить внимание, на то, что по ходу дела создается коллекция Numbers с номерами абзацев, начинающих информацию об очередной персоне. Эта информация получается почти бесплатно, но в дальнейшем существенно сократит время работы, позволив лишний раз не проходить по всему документу. Последний оператор этой процедуры показывает на экране форму с заполненным списком фамилий персон справочника. Вот как выглядит эта форма в процессе работы с ней:
Создание коллекции "избранных" персон
При нажатии командной кнопки "Выбери нас" создается список, содержащий фамилии персон, выбранных пользователем для занесения информации о них в папку "Контакты". Вот текст вызываемых процедур:
Private Sub cmdSelectPerson_Click() Dim intLoop As Integer, intSelect As Integer Dim strSelect As String Dim ВыборСделан As Boolean Dim Num As Integer ВыборСделан = False intLoop = 0 intSelect = 0 'Поиск выделенных элементов Do If frmPersons.lstPersons.Selected(intLoop) Then 'Найден очередной элемент strSelect = frmPersons.lstPersons.List(intLoop) Num = intLoop + 1 ВыборСделан = True intSelect = intSelect + 1 CollectionOfPersons.Add strSelect Con.Add Num End If intLoop = intLoop + 1 Loop Until intLoop = frmPersons.lstPersons.ListCount If ВыборСделан Then Unload Me Set myOl = CreateObject("Outlook.Application") Else MsgBox ("Выбор не сделан") End If 'Печать коллекции 'For Each pers In CollectionOfPersons ' Debug.Print pers 'Next pers End SubЛистинг 2.30.
Здесь, как бычно, в цикле по всем персонам анализируется множественный выбор пользователя, и фамилии выбранных персон добавляются в коллекцию CollectionOfPersons. Одновременно, добавляются элементы в коллекцию Con, позволяя запомнить порядковые номера выбранных персон в списке. Процедура анализирует, сделал ли пользователь свой выбор, и, если таковой сделан, то после заполнения коллекций форма закрывается. При желании можно включить отладочную печать элементов коллекции CollectionOfPersons. Обратите внимание, в конце работы этой процедуры я создаю объект Outlook, подготавливая почву для следующего этапа работы, когда потребуется создание контактов. Теперь все готово для продолжения работы процедуры WTOOL, вызывающей процедуру SelectPerson, которая выполняет основной и завершающий этап работы.
Создание записи Person
Нам предстоит теперь разобраться с более сложными вопросами. В той части, которая связана с обработкой текстового документа, предстоит понять, как выделить из текста нужную информацию о персоне, для того чтобы создать формальный объект (переменную) созданного нами ранее пользовательского типа Person, и заполнить поля этого объекта. Другая часть работы связана с созданием объекта Outlook - элемента папки Contacts. Давайте посмотрим, как все это можно реализовать. Вот текст процедуры SelectPerson:
Public Sub SelectPerson() ' Выделение записи Dim par As Paragraph, CountPar As Integer Dim ibeg As Integer, ifin As Integer, nPerson As Integer Dim PersonRange As Range Dim i As Integer, Num As Variant With ActiveDocument Set PersonRange = .Paragraphs(1).Range i = 0 'Цикл по записям, отобранных пользователем For Each Num In Con i = i + 1 'Выделение области документа, занятой записью 'Номер абзаца, начинающего запись ibeg = Numbers(Num) 'Номер абзаца, заканчивающего запись ifin = Numbers(Con(i) + 1) - 1 PersonRange.Start = .Paragraphs(ibeg).Range.Start PersonRange.End = .Paragraphs(ifin).Range.End 'Выделение записи PersonRange.Select 'Обработать запись - объект Selection Call WorkWithSelected Next End With myOl.Quit End SubЛистинг 2.31.
Внешний цикл организован по элементам коллекции Con, элементов у которой ровно столько, сколько персон выделил пользователь для преобразования информации о них в контакты папки Outlook. Первая возникающая задача для каждого элемента этого списка состоит в том, чтобы выделить область текстового документа, в которой записана информация о соответствующей персоне. Чтобы задать эту область - объект Range, достаточно знать параметры Start и End, определяющие местоположение начала и конца области. Вот как можно их определить. Я напомню, что текущий элемент Num коллекции Con задает порядковый номер персоны в списке, тогда по определению коллекции Number номер первого абзаца будет задаваться выражением Number (Num). Номер последнего абзаца записи можно определить, как номер первого абзаца следующей записи, уменьшенный на единицу. Этим алгоритмом я и пользуюсь в процедуре. Заметьте, если не принять дополнительных мер предосторожности, то он приведет к ошибке, когда выбрана последняя запись справочника. Чтобы избежать этого, я использовал стратегию, называемую введением "барьера", добавив в справочник специальную служебную запись (барьер) "Конец записей". Эта запись информирует пользователя об окончании списка персон и, естественно, никогда не будет входить в его выбор. Тем самым удается достаточно просто получить объект Range, задающий фрагмент текстового документа, описывающий информацию о нужной персоне. Выделение этой области задает объект Selection, с которым продолжает работу вызываемая процедура WorkWithSelected. Прежде, чем обсуждать ее работу, приведу ее текст:
Public Sub WorkWithSelected() 'Обработка с выбранной и отмеченной записью Dim pers As Person, pars As Paragraphs, par As Paragraph Dim i As Integer, n As Integer Dim myR As Range Dim FirstWord As String Set pars = Selection.Paragraphs With pers 'Обработка первого абзаца - фамилии Set par = pars(1) Set myR = par.Range .FirstName = myR.Words(2).Text .MiddleName = myR.Words(3).Text .LastName = myR.Words(1).Text 'Обработка должности - следующего непустого абзаца Set par = pars(2) If par.Range.Words.Count = 1 Then Set par = pars(3) Set myR = par.Range n = myR.Words.Count myR.End = par.Range.Words(n - 1).End .Post = myR.Text 'Обработка оставшихся абзацев For Each par In pars Set myR = par.Range n = myR.Words.Count FirstWord = myR.Words(1).Text Select Case FirstWord Case "Родился ", "Родилась " .DOB = SelectDate(par.Range) Case "Тел" myR.Start = par.Range.Words(3).Start myR.End = par.Range.Words(n - 1).End .Tel = myR.Text Case "Факс" myR.Start = par.Range.Words(3).Start myR.End = par.Range.Words(n - 1).End .Fax = myR.Text Case "Адрес" myR.Start = par.Range.Words(3).Start myR.End = par.Range.Words(n - 1).End .Address = myR.Text Case "e" myR.Start = par.Range.Words(5).Start myR.End = par.Range.Words(n - 1).End .Email = myR.Text Case Else .Other = .Other + myR.Text End Select Next par If Not IsDate(.DOB) Then .DOB = "1 января " 'Debug.Print Selection.Range.Text 'Debug.Print .FIO, .Post, .Address, .DOB, .Tel, .Fax, .Email, .Other End With 'Запись создана - теперь создается контакт в Outlook Call WriteToContact(pers) End SubЛистинг 2.32.
В этой процедуре заполняются поля переменной pers пользовательского типа Person. При этом существенно используются принятые соглашения о структуре справочника. Так по предположению первый абзац содержит только информацию о фамилии, имени и отчестве персоны, так что анализ первых трех слов этого абзаца позволяет заполнить поля LastName, FirstName и SecondName записи pers. Следующий абзац, который может следовать сразу за первым или быть отделенным пустым абзацем, содержит информацию о должности персоны. Запоминается весь текст этого абзаца, который и определяет описание должности персоны. Заметьте техническую деталь, при формировании данного поля из коллекции Words, задающей слова абзаца, удаляется последнее слово, в котором записан символ конца абзаца. Это же правило применяется и при работе с другими полями. При заполнении других полей не предполагается жесткий порядок их следования в тексте документа. Распознавание идет по ключевым словам, начинающим абзац, и программно осуществляется разбором случаев. Так заполнялись поля, определяющие телефон, факс, адрес и другие, подобные им. Все абзацы, не содержащие заданных ключевых слов, составляли поле Other. Пожалуй, наибольшую трудность вызывает распознавание даты рождения персоны. Дело в том, что для записи даты используются различные форматы, сокращения и прочие особенности. Более того, некоторые из персон, в особенности женщины, предпочитали не указывать год рождения, а мужчины не считали необходимым указывать число и месяц рождения. Чтобы справиться, хотя бы частично, с возникающими проблемами, я написал отдельную процедуру, занимающуюся разбором даты рождения. Вот ее текст:
Public Function SelectDate(ran As Range) As String Dim Dat As String With ran If .Words(3) = "февраля " Then .Words(3) = "фев " Dat = .Words(2) & .Words(3) & .Words(4) If IsDate(Dat) Then SelectDate = Dat Else Dat = .Words(2) & .Words(3) If IsDate(Dat) Then SelectDate = Dat Else Dat = "1 января " & .Words(3) If IsDate(Dat) Then SelectDate = Dat Else Dat = "1 января " End If End If End If End With End FunctionЛистинг 2.33.
Здесь я, прежде всего, исправляю ошибку, перекочевавшую в Office 2000 из предыдущей версии, когда в датах не воспринимается месяц февраль. Заметьте, что в Office 2000 имеет место:
?IsDate("13 февраля 1961") False ?IsDate("13 фев. 1961") TrueЛистинг 2.34.
В этой процедуре я не старался исправить все возможные ошибки, скорее я проверял корректность той или иной комбинации, используя для проверки функцию IsData, возвращающую истину, когда ее аргумент является правильной датой с точки зрения Office 2000. Если же установить дату рождения не удавалось, то в качестве даты принималась некоторая условная дата (1 января текущего года), что позволяло позже при работе с контактом понимать, что точная дата рождения контакта не известна.
По завершении формирования записи pers эта запись в качестве аргумента передавалась при вызове процедуры WriteContact, в которой и реализована работа с объектами Outlook.