Главная страница Visual 2000 · Средства разработки · Общий список статей · Общий список статей по VB/VBA

Советы тем, кто программирует на Visual Basic (79-87b)

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

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

Совет 79. Как обойти все элементы дерева (TreeView Control)
Совет 80. Будьте внимательны при работе с константами типа Long
Совет 81. Как обеспечить числовой ввод
Совет 82. Как выполнить доступ к защищенным базам данных из VB4
Совет 83. Используйте некоторые правила написания кода в VB3 для его простого переноса в VB4
Совет 84. Как включить 16-разрядную версию Crystal OCX в прикладную программу
Совет 85. Как сохранить текущее значение индекса списка
Совет 86. Как уменьшить размеры приложения
Совет 87. Будьте внимательны при обращениях к функциям API

Продолжение советов этой статьи:


Совет 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, которые намного упростят такой переход. В связи с этим можно порекомендовать следующее:

Строки, содержащие несколько операторов Конкатенация строк Ограничение в 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 является неверная передача параметров (их числа, типов данных, адресации переменных и пр.). Вот один характерный пример, присланный нашим читателем:

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 уже не будет правильно работать.

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

Продолжение советов этой статьи:

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