Visual2000 · Архив статей А.Колесова & О.Павловой

Советы тем, кто программирует на Visual Basic и MS Office/VBA

Андрей Колесов, Ольга Павлова

© 1997, Андрей Колесов, Ольга Павлова
Авторский вариант. Статья была опубликована c незначительной литературной правкой в журнале "КомпьютерПресс" N 2/97, с. 74-81.


Совет 79. Как обойти все элементы дерева (TreeView Control)

(совет прислал Николай Баранов)

Для графического представления объектов, имеющих иерархическую структуру, очень удобно использовать элемент управления TreeView.OCX. При работе с ним часто возникает необходимость обхода всех элементов дерева или какой-то его ветви (например, для печати дерева на принтере).

Следующая рекурсивная процедура позволяет это легко выполнить:

Sub SeeTree(Level%)
  Dim nc As Node
  If Level% = 0 And (Not CurrentNode Is Nothing) Then
    Set CurrentNode = CurrentNode.FirstSibling
    ' вставьте сюда что-нибудь,
    ' что вы хотите сделать с элементом дерева
    ' например, MsgBox CurrentNode.Text,
    ' чтобы просто посмотреть
  End If
  While (Not CurrentNode Is Nothing)
    ' у элемента есть дети?
    If CurrentNode.Children > 0 Then
      Set CurrentNode = CurrentNode.Child
      ' сюда тоже можно что-нибудь вставить
      Level% = Level% + 1
      SeeTree Level   ' рекурсия здесь
    End If
    ' следующий элемент на этом уровне
    Set nc = CurrentNode.Next
    ' элемент есть - идем дальше по уровню
    If Not nc Is Nothing Then
      Set CurrentNode = nc
      ' вставьте сюда что-нибудь,
      ' что вы хотите сделать с элементом дерева
      ' например, MsgBox CurrentNode.Text,
      ' чтобы просто посмотреть
    Else ' элемента нет - идем на верхний уровень
      ' следующая строка пригодится,
      ' если есть желание развернуть все на экране
      ' CurrentNode.EnsureVisible
      '
      Set CurrentNode = CurrentNode.Parent
      Level% = Level% - 1
      Exit Sub
    End If
  Wend
End Sub

В секцию Declarations формы, в которой расположен TreeView, нужно добавить такую строку:

Dim CurrentNode As Node

Вызов процедуры может выглядеть примерно так:

' для обхода всех элементов дерева,
' находящихся на одном уровне с выделенным:
Set CurrentNode = TreeView1.SelectedItem
...
' для обхода всех элементов-детей 
' выделенного элемента дерева
Set CurrentNode = TreeView1.SelectedItem.Child
...
' переменная, показывающая текущий уровень иерархии 
Level% = 0 
SeeTree Level%

В начало статьи

Совет 80. Будьте внимательны при работе с константами типа Long

(Важное дополнение к этому совету содержится в Совете149.)

Николай Баранов, приславший предыдущий совет, сообщил также, что заметил ошибочную ситуацию при использовании логических операций при работе с переменными типа Long(&). Проблема выглядит следующим образом.

Например, при работе с OLE-Automation серверами приходится анализировать коды возвращаемых ошибок, которые представляют собой длинное целое, например, что- нибудь вроде -2147221229 (&H80040113 - MAPI_E_USER_CANCEL). Однако вся необходимая информация об ошибке содержится в младших двух байтах, и вопрос лишь в том, как их достать. Но это оказывается совсем не так просто.

Например, если выполнить такой фрагмент программы:

Code& = &H80040113
Result& = Code& And &HFFFF

то содержимое Result& будет равно не &H0113, как хотелось бы, а &H80040113. Ситуация не изменится, даже если написать по-другому:

Const Mask& = &HFFFF
Code& = &H80040113
Result& = Code& And Mask&

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

Result& = Code& Or Mask&

получится не &H8004FFFF, а &HFFFFFFFF.

Такие ошибки (или странности) происходят только с использованием масок, занимающих не более 16 младших разрядов, для значений же больше &HFFFF все работает нормально. В связи с этим для выделения двух младших байтов Николай предлагает использовать, например, такую конструкцию:

(&H80040113 And &H7FFFFFFF) And &H8000FFFF

Действительно, ситуация выглядит довольно странно и неприятно. Ведь, если рассматривать общий случай логических операций двух переменных m1& и m2&, содержимое которых заранее неизвестно, то положение представляется почти безысходным (по крайней мере, универсальное решение проблемы будет весьма громоздким).

Однако при более детальном рассмотрении все оказывается не так уж и страшно. На самом деле никаких ошибок в логических операциях с переменным Long нет, а есть проблема формирования констант, которая связана с преобразованием данных из Integer в Long и наоборот. В связи с этим нужно вспомнить, что целочисленные переменные являются переменными со ЗНАКОМ, и нужно быть очень внимательным при переходе от беззнакового представления числа (&H...) к знаковому (цифровому десятичному). Рассмотрим такую конструкцию:

Mask& = &HFFFF
Print HEX$(Mask&)    
' будет напечатано &HFFFFFFFF !!!!

Дело в том, что константа &HFFFF автоматически представляется в виде переменной Integer (попадает в диапазон данных) и в числовом выражении равна -1. Соответственно, при присвоении Mask& = &HFFFF происходит преобразование из Integer в Long и переменная Mask& = -1 (&HFFFFFFFF)! Аналогичные преобразования происходят и в приведенной выше операции Result& = Code& And &HFFFF.

В этой ситуации в вину разработчикам VB можно поставить только двусмысленность операции определения константы в явном виде:

Const Mask& = &HFFFF
Print HEX$(Mask&)    
' будет напечатано &HFFFFFFFF !!!!

Здесь неожиданно константа &HFFFF опять интерпретируется как Integer, хотя она обозначена как Long. Это тоже не очень хорошо, но вполне разрешимо - надо только помнить об этом свойстве констант типа Long. Все будет работать так, как надо, если использовать следующие варианты: Const Mask& = 65635 или Mask& = &H10000 - 1.

Но конструкция Const Mask& = &H10000 - 1 опять приведет к нежелательному результату (&HFFFFFFFF).

В результате можно сделать следующий вывод: для определения констант типа Long или их использования в арифметических или логических операциях в диапазоне значений &H8000-&HFFFF (32768-65535) нельзя использовать беззнаковое шестнадцатеричное представление, а можно применять только десятичное.

В начало статьи

Совет 81. Как обеспечить числовой ввод

Часто бывает необходимо ограничить ввод данных в текстовое окно только числовым значением. VB содержит элемент управления для замаскированного редактирования, однако пользоваться им в некоторых случаях не очень удобно. Вместо этого можно применять следующую функцию для проверки числового ввода в VB4. Для работы с ней в VB3 измените два логических параметра на целочисленные:

Function ValidNumber (iAscii As Integer, txtBox As TextBox, _
  bSign As Boolean, bPoint As Boolean) As Integer
  ' Ввод символа по умолчанию
  ValidNumber = iAscii
  Select Case iAscii
    Case 8  ' Клавиша Backspace
    Case 43, 45
      ' Ввод символа знака (+, -) только в том случае,
      ' если флаг знака равен "Истина" (True), а сам
      ' символ стоит первым по порядку
      If (Not bSign) Or (txtBox.SelStart > 1) Then
        ValidNumber = 0
      End If
    Case 46
      ' Ввод десятичной точки только в том случае,
      ' если флаг равен "Истина" (True) и в строке нет
      ' ни одной другой точки
      If (Not bPoint) Or (InStr(txtBox.Text, ".")) Then
         ValidNumber = 0
      End If
    Case 48 To 57  ' Цифры от 0 до 9
    Case Else  ' Все остальное
      ValiNumber = 0
  End Select
End Function

Данная функция должна вызываться из события KeyPress текстового окна, например, так:

Private Sub txtNumber_KeyPress (KeyAscii As Integer)
  KeyAscii = ValidNumber (KeyAscii, txtNumber, True, True)
End Sub 

В начало статьи

Совет 82. Как выполнить доступ к защищенным базам данных из VB4

Даже прочитав документацию VB4 о том, как открыть защищенную базу данных Access, бывает совсем не просто разобраться в этом вопросе. Одна из причин - нечеткое описание того, какие команды относятся к 16-разрядными программам, а какие - к 32-разрядным. Кроме того, очевидный технический дефект в 16-разрядной версии для баз данных Jet 2.5 приводит к тому, что метод CreateWorkspace из DBEngine не работает до тех пор, пока свойствам DefaultUser и DefaultPassword не будут присвоены значения, соответствующие защищенной базе данных.

Здесь приводится пример операций, необходимых для открытия защищенной базы данных как в 16-, так и в 32-разрядных приложениях. Для 16-разрядных программ сначала создайте файл APPNAME.INI (где APPNAME - это имя исполняемого файла вашей программы), который содержит по крайней мере следующее:

[Data]
Database = D:\PATH\DBNAME.MDB
[Options]
SystemDB = D:\PATH\SYSTEM.MDA

Затем, чтобы открыть базу данных, выполните следующее:

Dim sUserName As String
Dim sPassword As String
Dim db As Database
Dim ws As Workspace
sUserName = "Здесь находится ваше имя"
sPassword = "Здесь находится ваш пароль"
' Создание защищенного рабочего пространства
With DBEngine
  .IniPath = "D:\PATH\APPNAME.INI"
  .DefaultUser = sUserName
  .DefaultPassword = sPassword
End With
' Имя рабочего пространства является 
' произвольным, но должно быть уникальным
Set ws = DBEngine.CreateWorkspace ("Name", _
sUserName, sPassword)
' Открытие базы данных через защищенное 
' рабочее пространство
Set db = ws.OpenDatabase (D:\PATH\DBNAME.MDB"...)

32-разрядные программы не требуют файла INI, кроме того, не нужно определять свойства DefaulUser и DefaulPassword. Установите свойство SystemDB из DBEngine таким образом, чтобы оно указывало на вашу системную базу данных:

Dim sUserName As String
Dim sPassword As String
Dim db As Database
Dim ws As Workspace
  sUserName = "Здесь находится ваше имя"
  sPassword = "Здесь находится ваш пароль"
  ' Создание защищенного рабочего пространства
  DBEngine.SystemDB  = "D:\PATH\SYSTEM.MDW"
  Set ws = DBEngine.CreateWorkspace ("Name", _
    sUserName, sPassword)
  ' И это все! Сезам, откройся ...
  Set db = ws.OpenDatabase ("D:\PATH\DBNAME.MDB"...)

В начало статьи

Совет 83. Используйте некоторые правила написания кода в VB3 для его простого переноса в VB4

Вполне возможно, что по каким-то причинам вы продолжаете работать в VB3, понимая, что в будущем все же придется переводить свои программы в VB4. Поэтому имеет смысл использовать несколько правил написания кода в VB3, которые намного упростят такой переход.

Строки, содержащие несколько операторов

VB4 обрабатывает такие строки не так, как VB3. В VB4 первый оператор в строке воспринимается как метка, если он состоит из одного ключевого слова. Самый простой пример:

Beep: Beep: Beep

В VB3 вы услышите три гудка, в VB4 - два. Так что, если вы любите объединять несколько строк в одну, пора распрощаться с этой привычкой.

Конкатенация строк

Хотя программисты в течение долгого времени предпочитали объединять строки при помощи знака "амперсанд" (&), поскольку это более быстрый и надежный способ, тем не менее у них также была возможность использовать знак "плюс" (+). Однако время, когда у программистов был выбор, прошло. В VB4 необходимо использовать только знак "амперсанд", так как оператор "плюс" в некоторых случаях имеет иное действие при работе со строками.

Ограничение в 64К

Ограничение в 64К на размер исходного кода в одной процедуре не изменилось со времен VB3. Тем не менее в самом языке произошли некоторые изменения, так что есть смысл еще раз рассмотреть это ограничение.

Поскольку VB4 использует в своей языковой основе более общий стандарт VBA, он создает код, который не столь оптимизирован, как в VB3. И хотя полученный в VB4 код предназначен для более широкого диапазона процессоров, он слегка увеличивает размеры процедуры. Поэтому, если размеры исходного кода в какой-либо из процедур приближаются к 64К, необходимо разбить ее на несколько более мелких процедур.

В начало статьи

Совет 84. Как включить 16-разрядную версию Crystal OCX в прикладную программу

Такая проблема возникает при работе в 16-разрядной версии VB4, в которой вы создаете некое приложение, использующее 16-разрядную версию Crystal OCX. Если это приложение предназначено для дальнейшего распространения (продажи), то можно создать его дистрибутив с помощью средства Microsoft VB Distribution Expert. Однако когда пользователи пытаются установить ваше приложение на свой компьютер, то они получают сообщение "Missing CRXLAT16.DLL" ("Не найдена библиотека CRXLAT16.DLL"). То есть программа установки ищет библиотеку CRXLAT16.DLL, чтобы присоединить ее к вашему приложению, но не находит такого файла.

Данная ситуация - ошибка, для исправления которой необходимо отредактировать файл SWDEPEND.INI. Найдите в нем ссылку на библиотеку CRXLAT16.DLL и замените ее на CRXLATE.DLL. Именно такой совет был опубликован в разделе Tech Tips ("Технические советы") в летнем выпуске Crystal Reporter - бесплатно распространяемом издании, которое выпускается компанией Seagate Software (бывшей Crystal Services).

В начало статьи

Совет 85. Как сохранить текущее значение индекса списка

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

Решение здесь очень простое: в момент выхода из списка в процедуре LostFocus нужно запомнить состояние свойства ListIndex, а в момент возврата - восстановить это состояние в процедуре GotFocus. В принципе, для этого можно использовать какую- нибудь переменную, глобальную на уровне данной формы, но сохранение значения индекса в свойстве Tag, имеющего тип строковой переменной и применяемого специально для хранения любых данных для дальнейшего их использования, выглядит более элегантно.

Сохраните свойство ListIndex в тот момент, когда окно списка теряет фокус:

Sub List1_LostFocus()
  List1.Tag = Format$(List1.ListIndex)
End Sub

Соответственно при возврате в список восстановите значение индекса:

Sub List1_GotFocus()
  If List1.Tag <> "" Then List1.ListIndex = Cint(List1.Tag)
End Sub

Обратите внимание, что, если в процедуре GotFocus убрать проверку значения Tag, то при первом входе в список произойдет ошибка, так как свойство Tag не было еще определено.

В начало статьи

Совет 86. Как уменьшить размеры приложения

Здесь приводятся некоторые обобщенные рекомендации экспертов журнала VBPJ (9'96) по этому поводу:

Исходный код:

Переменные

Формы и элементы управления

Средство JET

В начало статьи

Совет 87. Будьте внимательны при обращениях к функциям API

Мы уже неоднократно говорили, что основной причиной ошибок при обращении к функциям API является неверная передача параметров (их числа, типов данных, адресации переменных и пр.).

Вот характерный пример, присланный нашим читателем:

'========== В модуле BAS =================
Declare Function Polygon Lib "GDI" (ByVal hDC _
  As Integer, lpPoints As POINTAPI, ByVal nCount _
  As Integer) As Integer
  Type POINTAPI
    x As Integer
    y As Integer
  End Type
  Dim m1(1 to 4) as POINTAPI

'=========== В форме =====================
' точки  функции, которая будет 
' обрабатываться функцией Polygon
  m1(1).x = 130: m1(1).y = 40
  m1(2).x = 90: m1(2).y = 60
  m1(3).x = 80: m1(3).y = 90
  m1(4).x = 30: m1(4).y = 40
  fz% = Polygon(Form1.hDC, m1(1), 4)

Вопрос: Почему в функции Polygon() во втором параметре надо указывать первый элемент массива, а не сам массив, как это принято при обращении к функциям VB?

Наш ответ таков:

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

При передаче данных между процедурами в виде m1() или Sym$ на самом деле передается адрес этого описателя, которой умеют понимать только процедуры, написанные на Basic. Универсальным же способом передачи данных, используемым во многих других языках программирования (C, Fortran, Pascal), является передача адреса памяти данных. При этом предполагается, что вызываемая подпрограмма должна сама знать о том, каков тип этих данных, их структура, число индексов (для массива) и пр. Если какие-то из этих характеристик являются переменными, то их нужно передавать в виде дополнительных параметров.

Именно такие универсальные способы передачи параметров используются при обращении к функциям API. Обратите внимание, что в обращении fz% = Polygon(Form1.hDC, m1(1), 4) во втором параметре передается адрес начала массива данных, а в третьем - число его элементов. При этом важно отметить, что вся ответственность за правильное описание структуры массива в вызывающей программе полностью ложится на программиста. Если вы в Type POINTAPI, например, укажите еще одно поле z, то никакой диагностики выдано не будет, но функция Polygon правильно работать уже не будет.

В начало статьи

Совет 87a. Как проиграть Wav-файл

В конце 1996 года мы получили почти одновременно такие два письма читателей:

"Я долгое время работаю на VB и нигде не могу узнать,- как работать со звуковыми файлами *.WAV кроме как исполь- зовать SOUND RECORDER. Если можете - ПОМОГИТЕ !!!"

"У меня возник такой вопрос : как на VB 4.0 при нажатии кнопки мыши проиграть определснный WAV файл."

Мы не знали ответа (не работали со звуком) и хотели опубликовать вопросы в журнале с подзаголовком "Вопросы без ответов". Однако наш редактор журнала "КомпьютерПресс" быстро написал нужное решение и опубликовал его с подзаголовком "Вопросов без ответов не бывает!".

Ответ Алексея Федорова:

Чтобы по нажатию кнопки мыши воспроизводился WAV-файл, Чтобы проиграть WAV-файл следует воспользоваться стандартной функцией Windows API sndPlaySound, которая находится в системной DLL-библиотеке WINMM.DLL. Эта функция имеет два параметра: первый указывает имя WAV-файла, второй является флагом, определяющем вид выполняемой функции:

Private Declare Function sndPlaySound _
  Lib "winmm.dll" Alias "sndPlaySoundA" _
  (ByVal lpszName As String, ByVal dwFlags As Long) As Long
'
' Описание значений параметра dwFlags:

Const SND_SYNC = &H0      ' Файл воспроизводится синхронно
      ' и функция не возвращает управление до окончания воспроизведения

Const SND_ASYNC = &H1  ' Файл воспроизводится асинхронно
      ' и функция возвращает управление сразу же после
      ' начала воспроизведения. Для того, чтобы
      ' прервать воспроизведение, необходимо вызвать
      ' функцию sndPlaySound с именем файла, равным ""

Const SND_NODEFAULT = &H2 ' Указывает на то, что если файл,
      ' заданный первым параметром, не найден, то не должен
      ' воспроизводиться файл по умолчанию

Const SND_MEMORY = &H4 ' Указывает на то, что имя файла
      ' соответствует WAV-файлу, находящемуся в памяти,
      ' например, загруженному из ресурса

Const SND_LOOP = &H8  ' Файл воспроизводится от начала до
      ' конца бесконечное число раз до тех пор, пока не
      ' вызвана функцию sndPlaySound с именем файла, равным "".
      ' При таком воспроизведении должен быть указан и флаг
      ' SND_ASYNC

Const SND_NOSTOP = &H10   ' Функция возвращает FALSE, если
      ' в момент ее вызова уже воспроизводится какой-нибудь файл

' Соответственно, реальное обращение
' к функции sndPlaySound может выглядеть примерно так

 Result = sndPlaySound("c:\wav\demo.wav", SDN_ASYNC)

Теперь еще один вопрос — как сделать, чтобы файл проигрывался при щелчке мыши по форме? Для этого нужно поместить обращение к sndPlaySound в событийную процедуру:

Sub Form_MouseDown(Button As Integer, _
    Shift As Integer, X As Single, Y As Single)
'
  Result = sndPlaySound("c:\wav\demo.wav", SDN_ASYNC)
End Sub

В этом случае файл будет воспроизводится при нажатии кнопки мыши в любой части клиентской области окна. Чтобы разделить эту область на подобласти, для каждой из которых воспроизводится свой WAV-файл, следует проверять координаты, в которых произошло нажатие кнопки мыши. Они передаются в качестве параметров обработчика MouseDown. Если же проверка координат не требуется, то можно использовать обработчик события Click.

И еще одно замечание, если области на экране достаточно большие, то можно использовать кнопки, сделав их невидимыми. В обработчике нажатия каждой кнопки на соответствующем элементе управления с помощью функции sndPlaySound будет воспроизводить необходимый WAV-файл.

В начало статьи