Советы тем, кто программирует на VB & VBA
Совет 351. Пишите замечания, присылайте советы, задавайте вопросы
Совет 352. Как решить проблему с VisData
Совет 353. Внимание при работе с булевыми типами данных
Совет 354. Как обеспечить совместимость между VBA- и VB-проектами
Совет 355. Используйте свойство Buttonдля элемента управления DataGrid
Совет 356. Как сделать Help для своего приложения
Совет 357. Преобразование текстового файла в набор данных ADO
Совет 358. Как расширить массив элементов управления во время выполнения
Совет 359. Работа с реестром и INI-файлами с помощью System.PrivateProfileString
Совет 360. Вместо DoEvents отслеживайте реальные события
Совет 361. Избегайте неявного преобразования типов данных
Совет 362. Как узнать список папок Outlook
Совет 363. Как добавить новый контакт в папку Outlook
Совет 364. Как обрабатывать входящие письма
Совет 365. Как вставить текст в создаваемое письмо
Совет 351. Пишите замечания, присылайте советы, задавайте вопросы
Не стоит даже повторять, как важна для авторов обратная связь с читателями. И мы бы очень хотели, чтобы программисты не только указывали нам на наши оплошности, но и делились собственными находками и «трюками».
Промашки у нас, конечно, бывают. Например, Евгений Иванов справедливо заметил, что в Совете 307 (КомпьютерПресс № 6’2000) мы, рассказывая об удалении каталога с помощью Windows API, совсем забыли сказать, что в большинстве случаев с этой задачей отлично справляется давно знакомая встроенная Basic-функция rmDir.
Приятно отметить, что журнал КомпьютерПресс и наши Советы читают не только в нашей стране, но и в дальнем зарубежье (кстати, хотя русскоязычных программистов в Европе и США гораздо меньше, чем в России, их активность на электронных форумах и в переписке заметно выше). Михаил Эскин, например, живет в Мюнхене и, как оказалось, является давним читателем наших публикаций. При этом он отмечает: «Ваши статьи стали постоянным моим спутником, несмотря на появление специальной литературы на прилавках». Спасибо!
В Германию российские журналы приходят с задержкой, поэтому Михаил только в ноябре прислал некоторые замечания по поводу статьи «Календарь наших дел», опубликованной в КомпьютерПресс № 5’2000. (Кстати, Михаил — автор серии статей о создании элементов управления в среде VB, опубликованных на сервере http://www.vbrussian.com/.) Об этих замечаниях мы поговорим в последующих Советах.
Совет 352. Как решить проблему с VisData
Мы уже несколько раз отмечали, что у утилиты VisData (создание баз данных) существуют проблемы с вводом и просмотром русских текстов. Михаил Эскин отмечает, что это, скорее, проблема конфигурации конкретного компьютера, которая решается следующим образом: в разделе [FontSubstitutes] файла WIN.INI нужно добавить сверху строку Tahoma,0=Tahoma,204 и затем перезагрузить компьютер.
Действительно, после этого VisData стала нормально работать с русским текстом. Но мы все равно считаем, что в данном случае имеет место дефект VisData, который возможно устранить с помощью подобного «трюка», поскольку такой способ настройки утилиты для нормальной работы с русским языком нигде не описывается.
Совет 353. Внимание при работе с булевыми типами данных
Михаил отметил также, что приведенный в нашей статье код для чтения/записи свойства DeleteMe (для пользовательского элемента управления Memos) при его привязке к значению флажка chkDeleteMe
Public Property Get DeleteMe() As Boolean If chkDeleteMe.Value = 0 Then DeleteMe = False Else DeleteMe = True End If End Property Public Property Let DeleteMe(ByVal newDelete As Boolean) If newDelete Then chkDeleteMe.Value = 1 Else chkDeleteMe.Value = 0 End If End Property
Можно упростить, записав содержимое каждой из этих процедур в одну строку:
DeleteMe = -1 * chkDeleteMe.Value chkDeleteMe.Value = -1 * newDelete
Несмотря на то что речь здесь идет вроде бы об очень частной проблеме, остановимся на ней подробнее.
Мы сами ранее говорили, что для улучшения читаемости программы желательно сокращать количество строк кода (Совет 331). Однако это не должно провоцировать потерю управляемости программой и снижение ее эффективности (обратите внимание, что в Совете 331 речь шла о разных формах записи одних и тех же конструкций). Нам кажется, что вариант, предложенный Михаилом Эскиным, приведет именно к таким негативным последствиям. По этому поводу отметим следующие моменты:
- Мы считаем принципиально неверным неявное преобразование данных, в данном случае из Integer в Boolean и наоборот. К сожалению, VB позволяет делать это, хотя вполне вероятно, что в VB.NET (7.0) такие вещи будут запрещены.
- Конечно, программисту полезно знать, в каком конкретном двоичном виде хранятся те или иные типы данных, но пользоваться такими знаниями нужно лишь в случае крайней необходимости.
- Серьезной проблемой VB является отсутствие беззнаковых целых типов данных. При этом путаница часто возникает именно из-за того, что переменные типа Integer и Long на самом деле выступают в роли то чисел со знаками (в арифметических операциях и при использовании десятичных литералов), то беззнаковыми (в логических операциях и в шестадцатеричных и восьмеричных литералах).
- Вообще говоря, практически в любой программе можно легко обойтись без использования
типа Boolean, так как она является всего лишь частным случаем Integer (те
же два байта для хранения информации). Более того, можно даже добиться экономии
памяти — если использовать переменную типа Byte со значениями 0 или 1. Это
легко можно сделать в «Календаре», где основная коррекция заключалась бы в
замене в SQL-запроса:
вместо "Where DeleteMe = True" написать -- "Where DeleteMe = 1"
При этом автоматически решается проблема обмена данными между переменной и значением флажка, поскольку они оказываются тождественно равными (можно даже использовать значение флажка 2 – «может быть»).
- Но главное достоинство конструкции IF...Then...Else...EndIf –— очевидность
логики ее работы. Попробуйте мгновенно ответить, каково будет значение DeleteMe
при chkValue = 0 в этом выражении:
DeleteMe = -1 * chkValue
- В одну более короткую строку (но все же с увеличением машинных команд) можно
было бы предложить вариант без неявного преобразования данных:
DeleteMe = (chkDeleteMe.Value = 1) chkDeleteMe.Value = IIf (1, 0, DeleteMe)
Но с точки зрения «очевидности» результата такой код также не безупречен.
- Несмотря на большое число строк в нашей конструкции IF...Then...Else...EndIf,
очевидно, что этот код является самым компактным и быстрым (он занимает всего
несколько машинных коротких команд). Более компактно его можно записать в
таком виде:
If chkDeleteMe.Value = 0 Then DeleteMe = False _ Else DeleteMe = True
Мы советуем использовать именно такой вариант.
Совет 354. Как обеспечить совместимость между VBA- и VB-проектами
Мы уже несколько раз отмечали, что, несмотря на всю схожесть VB и Office/VBA, у этих систем есть ряд серьезных различий, которые препятствуют прямому перенесению кода из одного вида проекта в другой и наоборот. Поэтому при написании кода, который предполагается для использования в разных системах, нужно специально тестировать возможность их использования в обоих вариантах. К сожалению, только изучая документацию, проверить это трудно.
Однако в общем случае следует иметь в виду, что VBA все же располагает более ограниченным набором функций по сравнению с VB (речь идет о встроенных возможностях самого языка, без учета объектов приложения, в котором используется VBA). Поэтому при прочих равных условиях для создания общих программных модулей (совместимых на уровне исходного кода) предпочтительнее среда VBA.
Это, в частности, касается и создания модулей формы. Мы уже писали (Совет 301), что Office/VBA использует для создания форм ActiveX-конструктор Microsoft Form 2.0, который доступен также в VB. То есть VB может создавать два типа форм — собственные VB-формы (Ruby Forms) и UserForms (VBA Forms). Однако проблема заключается в том, что, даже используя одинаковый конструктор, VB и VBA сохраняют модули формы в разных форматах. При этом VB может читать оба формата, а VBA — только свой собственный.
Соответственно, если вы намерены создавать модули формы двойного применения, это следует делать не просто с помощью MS Forms 2.0 , а обязательно с этой целью использовать Office/VBA.
И еще один совет, который из этого следует: для лучшей совместимости кода нужно в максимальной степени выносить код из процедур модулей формы в процедуры модуля кода или класса (эти компоненты пока — трудно сказать, что Microsoft придумает дальше — загружаются в обе среды разработки).
Перенос модулей формы в случае их несовместимости можно сделать следующим образом (например, из VB в VBA). Создайте в VBA визуальную форму со всеми компонентами. Задайте имена компонентов такие же, как в VB. Далее скопируйте содержимое кода из VB в VBA через буфер обмена.
Однако этот способ будет работать только при использовании Forms 2.0. При переносе VB-форм придется вручную корректировать имена некоторых событий и свойств. Например, в VBA события формы Initialize и Terminate соответствуют событиям Load и Unload в VB. В общем, с Microsoft не соскучишься.
Совет 355. Используйте свойство Buttonдля элемента управления DataGrid
Элемент управления DataGrid позволяет установить для ячеек одной или нескольких колонок таблицы свойство Button, которое обеспечивает их работу в режиме «кнопок». Например, установите
DataGrid1.Columns.Item(1).Button = True
В этом случае после щелчка мыши по ячейкам первой колонки будет выполняться событие ButtonClick. Программист может написать в этой процедуре любой специальный код, например вывести диалоговое окно с информацией (список, таблица и пр.), которая связана с данной ячейкой.
Совет 356. Как сделать Help для своего приложения
Программ создания HELP-файлов довольно много, ряд из них — свободно распространяемые или условно-бесплатные. Для VB и VBA, возможно, лучшим способом является использование утилиты Microsoft HTML Help Workshop, которая поставляется в составе ряда программных продуктов, в том числе MS Office 2000 Developer Edition. При желании ее можно скачать из Интернета по адресу: http://www.microsoft.com/workshop/author/htmlhelp/. Описания работы этой утилиты имеются в целом ряде книг по VB.
Совет 357. Преобразование текстового файла в набор данных ADO
Информацию из текстового файла, записанную в виде полей, разделенных запятыми, можно достаточно просто представить в виде набора данных ADO. Это достигается следующим образом:
connCSV.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _ "Data Source=" & FileName$ & _ ";Extended Properties='text;FMT=Delimited'"
В данном случае строка с параметрами соединения (Connection String) содержит раздел Extended Properties, который указывает, что используется текстовый файл с полями. Однако следует иметь в виду, что приведенный вариант обращения подразумевает наличие в первой строке текстового файла заголовков полей. Если же заголовков нет, следует указать в явном виде аргумент HDR:
connCSV.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _ "Data Source=" & FileName$ & _ ";Extended Properties='text;HDR=NO;FMT=Delimited'"
Совет 358. Как расширить массив элементов управления во время выполнения
Порой требуется добавить элемент управления, например командную кнопку, к массиву подобных компонентов во время выполнения программы. Для этого можно использовать оператор Load:
Load object(index)
где object — имя массива, а index — номер нового элемента управления, который вы хотите добавить. Но при этом в исходном состоянии в массиве уже должен иметься хотя бы один элемент управления (нумерация индекса начинается с нуля). В более общем случае создание новой кнопки может выглядеть так, как описано ниже.
Создайте новый проект и добавьте к форме командную кнопку. Затем в окне Properties введите 0 для свойства Index — VB сразу преобразует одиночную кнопку в массив. Далее введите такой код для формы:
Private Sub cmdBtn_Click(Index As Integer) ' создание новой кнопки для массива элементов управления Dim btn As CommandButton Dim iIndex As Integer iIndex = cmdBtn.Count 'текущее числе элементов массива If iIndex <= 32767 Then ' можно добавлять Load cmdBtn(iIndex) Set btn = cmdBtn(iIndex) With btn ' установка свойств .Top = cmdBtn(iIndex - 1).Top + 620 .Caption = "Command" & iIndex + 1 .Visible = True End With Set btn = Nothing End If End Sub
Совет 359. Работа с реестром и INI-файлами с помощью System.PrivateProfileString
Работа с реестром Windows может выполняться не только с помощью функций Windows API или объекта Registry (см. Совет 273), но и с применением свойства PrivateProfileString объекта System, который входит в состав библиотеки Microsoft Word 8.0/9.0 Object Library. Она автоматически подключается при работе с MS Word 97/2000 и может использоваться в любых инструментах, которые поддерживают работу с ActiveX-объектами. В частности, к среде VB или MS Office/VBA она подключается с помощью команды Project|Reference или Tools|Reference соответственно.
Вот как будет выглядеть чтение полного имени каталога, где находится Internet Explorer:
RegFile$ = "" ' пустое имя означает Системный Реестр SectionName$ = "HKEY_CURRENT_USER\Software\Microsoft\" _ & "Windows\CurrentVersion\App Paths\IEXPLORER.EXE") ' имя раздела KeyName$ = "Path" ' имя ключа IEPath$ = System.PrivateProfileString(RegFile$, SectionName$, KeyName$) If IEPath$ <> "" Then ' есть имя каталога MsgBox "Имя каталога с IE = " & IEPath$ End If
Соответственно запись нового значения параметра в реестр выполняется следующим образом:
System.PrivateProfileString(RegFile$, SectionName$, KeyName$)= IEPath$
Дополнительным преимуществом данного свойства является возможность работы не только с реестром, но и с любыми текстовыми файлами формата типа Win.INI, то есть возможность использовать для хранения параметров приложения персональные файлы-профайлы. Для этого достаточно просто задать в качестве первого параметра свойства имя соответствующего файла.
Например, при закрытии текущего документа Word можно автоматически фиксировать имя последнего использовавшегося документа:
System.PrivateProfileString("C:\MyWordSetting.ini", "MacroSettings", _ "LastFile") = ActiveDocument.FullName
А при загрузке Word можно автоматически открыть данный файл:
LastFile$ = System.PrivateProfileString("C:\Settings.Txt", _ "MacroSettings", "LastFile") If LastFile$ <> "" Then Documents.Open FileName:=LastFile$
Необходимо обратить внимание на следующие особенности применения свойства PrivateProfileString:
- При чтении пользователь не может точно определить причину получения пустого значения ключа — это может быть как наличие пустой записи, так и отсутствие ключа, раздела или даже самого файла.
- При записи замена значения ключа выполняется только в случае, если ключ найден. В противном случае формируется новая запись с заданным ключом и его значением. При этом при отсутствии раздела создается новый раздел, при отсутствии файла — создается файл.
Например, если файл D:\MyFile.INI не существует, то после выполнения кода:
System.PrivateProfileString("d:\ MyFile.INI", "test1", "key1") = "andy"
будет сформирован файл следующего содержания:
[test1] key1=andy
Совет 360. Вместо DoEvents отслеживайте реальные события
Оператор DoEvents позволяет выполнять параллельные процессы, поэтому достаточно часто используется для синхронизации двух различных вычислительных процессов. Типичным случаем является такой пример. Имеются две формы: главная (frmMain) выполняет некоторые вычисления и выводит результаты, а вторая (frmEntry) — вводит исходные данные для этих вычислений. При этом логика взаимодействия данных форм такова: главная форма запускает frmEntry и ожидает, когда там будут введены нужные данные, например, в виде нажатия пользователем кнопки Submit (Подтверждение).
Один из вариантов решения этой задачи может выглядеть следующим образом:
Private Sub Command1_Click() ' процедура в форме frmMain Dim Myform As frmEntry ' создание второй формы для ввода данных Set Myform = New frmEntry With Myform .Show .Ready = False ' начальная установка глобальной переменной Do ' ожидание DoEvents ' передача управления операционной системе ' для обработки других событий Loop Until .Ready 'Выполнение каких-то вычислений на основе введенных данных txtResults.Text = .txtNum1 * .txtNum2 End With Unload Myform Set Myform = Nothing ' освободить объект End Sub ' '============ ' код формы frmEntry1 Public Ready As Boolean Private Sub cmdSubmit_Click() Ready = True ' подтверждение ввода End Sub
Данная конструкция базируется на отслеживании состояния глобальной переменной Ready в форме frmEntry1. Однако недостатком этой конструкции является как раз использование оператора DoEvents, который требует достаточно много времени, то есть «съедает» значительную часть ресурсов.
Гораздо лучше выглядит такой вариант решения, когда создается пользовательское событие, управляемое из формы frmEntry. Для этого в данной форме нужно написать следующий код:
' описание события в секции Declaration Public Event NumbersSubmitted(Num1 As Integer, Num2 As Integer) ' выполнение операций Private Sub cmdSubmit2_Click() Dim Num1%, Num2% Num1 = CInt(txtNum1.Text) Num2 = CInt(txtNum2.Text) Unload Me ' запуск внешнего события с передачей параметров RaiseEvent NumbersSubmitted(Num1, Num2) End Sub
Соответственно в главной форме нужно описать данное событие и сформировать процедуру его обработки:
Private WithEvents frmNumEntry As frmEntry Private Sub Command2_Click() ' запуск второй формы Set frmNumEntry = New frmEntry frmNumEntry.Show End Sub Private Sub frmNumEntry_NumbersSubmitted(Num1%, Num2%) ' обработка события, инициализированного из формы frmEntry txtResults.Text = Num1 * Num2 Set frmNumEntry = Nothing End Sub
Совет 361. Избегайте неявного преобразования типов данных
Мы неоднократно подчеркивали в своих публикациях, что причиной достаточно частых ошибок является использование неявного преобразования типов данных (см., например, Совет 353). Подобная возможность (без применения специальных функций преобразования) является большим недостатком VB, а ее использование разработчиками говорит в первую очередь об их небольшом опыте...
Здесь существует достаточно много подводных камней, главный из которых — неопределенность такого рода преобразования, то есть тот факт, что программа будет вести себя совсем не так, как видится ее автору. Следует также учитывать особенности национальных форматов представления данных (для вещественных чисел и дат). Приведем еще один пример на данную тему, реализованный в Windows с русскими региональными установками.
Выполните такой код:
Dim strSource As String, strR1 As String, strR2 As String Dim sngResult As Single strSource = "2.34" ' преобразование строки в вещественное число sngResult = Val(strSource) Print sngResult ' будет напечатано 2,34 ' strR1 = Str(sngResult) strR2 = sngResult Print strR1, strR2 ' будет напечатано 2.34 2,34
Если вы попробуете выполнить код:
strSource = "2.34" sngResult = strSource
то получите на втором операторе сообщение об ошибке — неверный тип данных.
Далее выполните еще один код:
strSource = "2,34" sngResult = Val(strSource) Print sngResult ' будет напечатано 2 ! Ошибка sngResult = Val(strSource) strR1 = Str(sngResult) strR2 = sngResult Print strR1, strR2 ' будет напечатано 2.34 2,34
Из проведенных экспериментов можно сделать следующие выводы:
- Функции явного и неявного преобразования данных работают по-разному! Val и Str выполняют операции преобразования по правилам американских региональных установок, независимо от установок пользователя на его компьютере. Неявное преобразование выполняется с учетом установленных на компьютере параметров.
- Казалось бы, оба варианта имеют свои недостатки. Более того, на «русской» системе лучше использовать неявное преобразование. Но здесь стоит обратить внимание на следующий момент: функции Val и Str будут одинаково работать на любом ПК, тогда как операции неявного преобразования могут выдавать разные результаты в зависимости от региональных установок.
Совет 362. Как узнать список папок Outlook
Допустим, вы хотите собрать список папок в некий список. Казалось бы, узнать имена папок можно следующим образом:
Dim AllFolders As Folders Set AllFolders = Application.GetNamespace("MAPI").Folders MsgBox AllFolders.Count For i = 1 To AllFolders.Count MsgBox AllFolder.Item(i).Name Next
Однако выясняется, что у вас имеется всего две папки с именами Personal Folders. Здесь полезно вспомнить, что папки OutLook имеют иерархическую структуру, такую же, как знакомая файловая система. В частности, она может иметь вид, представленный на рис. 1.Приведенная выше конструкция выдала нам имена стандартных папок самого верхнего уровня.
Чтобы получить информацию о папках второго уровня, нужно написать более сложный код:
Dim allFolders As Folders Dim i%, j% Set allFolders = Application.GetNamespace("MAPI").Folders MsgBox "Число папок верхнего уровня = " & allFolders.Count ' обзор папок верхнего уровня For i = 1 To allFolders.Count MsgBox "Имя папки = " & _ allFolders.Item(i).Name & vbCrLf & _ " число вложенных папок = " & _ allFolders.Item(i).Folders.Count ' обзор папок второго уровня For j = 1 To allFolders.Item(i).Folders.Count MsgBox "Имена вложенной папки = " & _ allFolders.Item(i).Folders.Item(j).Name & vbCrLf & _ " число вложенных в нее папок = " & _ allFolders.Item(i).Folders.Item(j).Folders.Count Next Next
Однако понятно, что наращивание числа вложенных циклов для обзора иерархических структур является совершенно бесперспективным занятием. (В нашем примере одна из папок второго уровня — Contacts — имеет также вложенную папку.) Здесь требуется переходить к рекурсивным конструкциям, которые могут выглядеть примерно так:
Dim allFolders As Folders Dim intLevel% ' номер уровня intLevel = 0 Set allFolders = Application.GetNamespace("MAPI").Folders Call FoldersViewRecurse(allFolders, intLevel, "MAPI") Sub FoldersViewRecurse(allFolders As Folders, intLevel%, strName$) Dim i%, FolderName$ Dim newFolders As Folders ' Вывод информации о папках данного узла иерархической структуры Debug.Print "Уровень = "; intLevel; " Узел = "; _ strName$; Tab(45); " Вложенных папок = "; allFolders.Count If allFolders.Count > 0 Then ' есть вложенные папки For i = 1 To allFolders.Count ' обзор вложенных папок FolderName$ = allFolders.Item(i).Name Set newFolders = allFolders.Item(i).Folders ' рекурсивное обращение к самой себе: Call FoldersViewRecurse(newFolders, intLevel + 1, FolderName$) Next End If End Sub
В правильности работы данной конструкции легко убедиться, взглянув на полученную распечатку результатов (рис. 2).
Совет 363. Как добавить новый контакт в папку Outlook
Это производится приблизительно следующим образом:
Dim myNewContact As ContactItem ' создание объекта "Контакт" Set myNewContact = Application.CreateItem(olContactItem) ' далее заполняются нужные поля формы myNewContact.FirstName = "Андрей" myNewContact.LastName = "Колесов" myNewContact.Email1Address = "akolesov@online.ru" myNewContact.Close olSave ' сохранить
Можно также выдать диалоговое окно «Контакты» для заполнения пользователем:
myNewContact.Display
Однако данная конструкция записывает новый контакт в стандартную папку «Контакты». Если вам нужно работать с какой-то индивидуальной папкой, вы должны написать такой код (здесь мы вдобавок создаем новую папку):
Dim myNewContact As ContactItem Dim myNewFolder As MAPIFolder ' Создание папки типа "Контакты" Set myNewFolder = Application.GetNamespace("MAPI"). _ GetDefaultFolder(olFolderContacts).Folders.Add("Личная") ' создание объекта "Контакт" для данной папки Set myNewContact = myNewFolder.Items.Add(olContactItem) myNewContact.FirstName = "Андрей" ...
Совет 364. Как обрабатывать входящие письма
Вам бы хотелось автоматически обрабатывать входящие письма? Это довольно просто сделать с помощью такого кода:
Private Sub Application_NewMail() ' При поступлении нового письма ' производится его обработка Dim mailItems As Items Dim mailmsg As MailItem ' Набор писем из папки "Входящие" Set mailItems = Application.Session._ GetDefaultFolder(olFolderInbox).Items Set mailmsg = mailItems.GetLast ' выбираем последнее ' далее выполняется анализ письма ' (его реквизитов, содержимого и пр. ' ... ' по результатам анализа можно: mailmsg.UnRead = False ' установить признак "Прочтенное" mailmsg.Delete ' удалить mailmsg.Move(myFolder) ' переместить в другую папку End Sub
Совет 365. Как вставить текст в создаваемое письмо
Это можно сделать, например, с помощью следующей простой макрокоманды, которая создает новое письмо, автоматически заполняет его содержимое и далее предоставляет пользователю возможность вводить остальную информацию и отправлять письмо:
Sub NewMailToKolesov() ' создание нового письма Dim myMail As MailItem Set myMail = CreateItem(olMailItem) ' заполнение его полей myMail.To = "akolesov@online.ru" myMail.Subject = "Привет!" myMail.body = "Андрей!" & vbCrLf & _ "Я тут придумал такую классную штуку." myMail.Display ' выводим окно и дополняем текст End Sub
Но возможен и другой вариант — вы уже создали новое письмо и хотите в процессе его ввода сделать вставку какого-то текста. В этом случае вам пригодится макрокоманда такого вида:
Sub InsertText() 'Вставить текст в текущее окно Dim myMail As MailItem ' выбирает текущее окно (т.е. нового письма) Set myMail = Application.ActiveInspector.CurrentItem myMail.body = myMail.body + " Привет семье!" End Sub
КомпьютерПресс 3'2001