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

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

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

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


Совет 49. Устанавливайте текст в элемент управления Masked Edit программным образом

Элемент управления Masked Edit имеет свойство Text, но оно не отображается в окне Properties, то есть начальную установку текста нельзя выполнять в процессе разработки программы. Поэтому такую установку нужно выполнять на программном уровне, например в момент загрузки формы:

Private Sub Form_Load ()
   MaskEdBox1.Text="369-76-97"
End Sub

При работе с Masked Edit следует помнить, что строковая переменная должна полностью совпадать с символами в шаблоне ввода (свойство Mask), включая литерные символы и подчеркивание. В приведенном выше примере подразумевалось, что шаблон номера имеет вид "###-##-##".

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

Совет 50. Традиционный: используйте самые быстрые конструкции

  1. Чтобы переместить программным образом элемент управления или форму в новое место, можно просто изменить значения свойств Left и Top, например:

    frmCustomer.Left = frmCustomer.Left + 100
    frmCustomer.Top = frmCustomer.Top + 50
    

    Но, используя метод Move, можно сделать то же самое процентов на 40 быстрее:

    frmCustomer.Move = frmCustomer.Left + 100, _
    frmCustomer.Top + 50
    

  2. Если вам нужно сделать проверку числовой переменной с нулем, то логическая конструкция

    If iNumber Then
    

    будет работать быстрее, чем арифметическая

    If iNumber <> 0 Then
    

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

Совет 51. Модифицируйте (если это нужно) режим Tab при вводе данных

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

Sub Text1_KeyPress (KeyAscii As Integer)
  If KeyAscii = 13 Then   ' клавиши Enter
    SendKeys "{Tab}"
    KeyAscii = 0
  End If
End Sub

А чтобы реализовать режим AutoTab при вводе данных в тестовое поле, можно использовать проверку свойства MaxLength в процедуре обработки события Change:

Sub Text1_Change ()
  If Len(Text1)=Text1.MaxLength Then
     SendKeys "{Tab}"
  End If
End Sub

В этом случае переход к следующему полю будет выполняться автоматически после ввода последнего символа.

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

Совет 52. Простой способ переключения флагов

В программах довольно часто приходится использовать переменные-флажки, которые имеют значения "ноль/не ноль". Конечно, можно написать:

If bPerform Then bPerform = False Else bPerform = True

Но так будет выглядеть симпатичнее:

bPerform = Not bPerform    ' состояние флага 0 или -1

А если вы больше привыкли иметь дело с арифметическими операциями, то можно использовать операцию Xor:

iFlag = iFlag Xor iMaskFlag

Последний вариант интересен тем, что в этом случае может использоваться любое ненулевое значение флага, которое в этом случае представлено iMaskFlag (1, -1, 777 и пр.). Еще одно замечание. Будьте внимательны, используя оператор Not при работе с целочисленными переменными, которые могут принимать значения, отличные от -1 (True) и 0 (False):

Sum cmdBool_Click()
  Dim iBool As Integer, iTemp As Integer
  iBool = True
  Print iBool     ' печатается -1
  Print Not iBool ' печатается 0
  iTemp = 5
  Print iTemp     ' печатается 5
  Print Not iTemp ' печатается -6
  ' любое ненулевое значение воспринимается как True
  If iTemp Then
    Print "iTemp = True" ' печататься будет здесь Else
    Print "iTemp = False"
  End If
End Sub

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

Совет 53. Используйте статические переменные (когда нужно)

Все переменные (в том числе и массивы) в VB могут быть динамическими или статическими. Их принципиальное отличие заключается в том, что резервирование и освобождение динамических переменных осуществляется в процессе выполнения программы по некоторым специальным запросам или автоматически при выполнении определенных операций. Например, при обращении к процедуре автоматически резервируются все ее локальные динамические переменные, а при выходе из нее они так же автоматически освобождаются. Для простых переменных это реализуется в виде некоторого стека, а для массивов — довольно сложным алгоритмом работы с динамической памятью. Но программисту особого дела до этого нет, так как здесь работает непосредственно VB. Хотя иногда при разработке сложных проектов приходится учитывать особенности механизма диспетчеризации (это бывает, когда вдруг появляются неприятные сообщения типа Out of memory).

Статические переменные формируются на этапе компиляции и существуют в процессе работы программы.

Достоинства и недостатки эти типов переменных понятны: динамические переменные позволяют оптимизировать использование памяти — они создаются только на время их реальной необходимости. Но работа с ними требует несколько большего времени для выполнения операций резервирования и освобождения. Хотя последнее замечание — чисто теоретическое: несколько моих попыток отловить на тестах эти временные отличия не привели к сколь-нибудь значимым результатам.

В VB по умолчанию все локальные переменные любой процедуры являются динамическими. Естественно, что в результате этого все значения локальных переменных теряются после выхода из нее. Поэтому, если вы хотите сохранить содержимое переменных между обращениями к процедуре, следует объявить их статическими, заменив ключевое слово Dim на Static. Это особенно полезно при создании постоянных счетчиков, а также в режиме отладки.

Приведем пример, демонстрирующий общее число одиночных щелчков мыши по кнопке (первый раз, когда вы щелкаете кнопку, счетчик начинает счет со значения по умолчанию, равного нулю):

Sub Command1_Click()
  Static Counter As Integer   ' Счет начинается с 0
  Counter = Counter + 1
  Print Counter
End Sub

Обратите внимание, что все переменные, описанные как глобальные на уровне модуля (оператором Dim в разделе Declarations), являются также статическими. Используя их, можно легко создать общие счетчики для нескольких процедур, например так:

'  раздел Declarations:
Dim Counter As Integer
' Counter - общий счетчик щелчков по двум кнопкам

Sub Command1_Click()
  Counter = Counter + 1
  Print Counter
End Sub

Sub Command2_Click()
  Counter = Counter + 1
  Print Counter
End Sub

А если вы хотите, чтобы отсчет начинался с нуля при каждой загрузке формы, можно добавить следующий код:

Sub Form_Load ()
  Counter = 0
End Sub

Для описания глобальных переменных на уровне модуля в VB 4.0 можно использовать оператор Private, а для создания переменных, доступных в любых процедурах всего приложения, — Public.

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

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

Динамические массивы представляют особую ценность для программиста. Они позволяют резервировать размеры массива, соответствующие реальным требованиям задачи, меняя их в случае необходимости (здесь есть интересные моменты, связанные с возможностью сохранения данных при перерезервировании массивов). В DOS'овских версиях они также позволяли использовать свободную основную память компьютера (статические массивы вместе с простыми переменными размещались в ближнем сегменте данных 64 Кбайт).

Динамические массивы можно создавать как на уровне процедуры, так и на уровне модуля. В первом случае они создаются и существуют, как и простые локальные переменные процедуры, только на время ее выполнения (с момента выполнения оператора Dim до Exit Sub/Function). Во втором случае они становятся как бы псевдостатическими: хранятся постоянно в памяти, но управление их резервированием (изменением размерности) выполняется в явном виде с помощью операторов ReDim в процедурах данного модуля. Однако при объявлении динамических массивов на уровне модуля есть некоторые нюансы, на которые следует обратить внимание.

Но прежде чем рассмотреть их для VB/Win, имеет смысл вспомнить, как это выглядело для DOS'овских версий (Quick, PDS, Visual). Общими правилами здесь являются следующие:

Рассмотрим такой пример для Basic/DOS:

' Модуль TEST1.BAS
' описание типа и размерности динамического массива,
' глобального на уровне модуля:
REDIM SHARED Array(10,20) AS INTEGER

SUB TestSub1
  PRINT Array(3,3)
  REDIM Array(8,97) AS INTEGER ' перерезервирование массива
END SUB

SUB TestSub2
  REDIM Array(4,7) AS INTEGER  ' начальное создание массива
  PRINT Array(2,2)
END SUB

Оператор REDIM SHARED... в разделе объявлений говорит о том, что определяется целочисленный двухмерный массив, глобальный на уровне модуля (для этого используется слово SHARED). Но фактически никакого массива не создается (имеется в виду, что это не головной модуль программы, с которого начинается ее выполнение). Описание границ массива (10,20) игнорируется и нужно только для того, чтобы показать число его индексов. Поэтому если мы сразу обратимся к процедуре TestSub1, то при выполнении оператора PRINT Array(3,3) будет выдано сообщение об ошибке ь 9 (хотя в нем говорится о нарушении границ, на самом деле массива просто не существует). Для работы с массивом нужно его создать, обратившись, например, вначале к процедуре TestSub2, а уже после этого вызывать TestSub1, в которой в том числе можно изменять его границы.

Обратите внимание, что при работе в среде QB (только в ней!) при определении массива в процедуре можно опустить описание типа AS INTEGER, но массив все равно будет резервироваться целочисленным. Впрочем, компилятор в любом случае выдаст ошибку о несовпадении типов данных и заставит вас устранить эту двусмысленность.

Если в процедуры этого же модуля добавить такие операторы, то они будет сразу определены как ошибочные:

REDIM Array(100)        ' Изменение числа индексов массива
REDIM Array(4,7) AS SINGLE ' Изменение типа данных
DIM Array(4,7) AS INTEGER ' Дубликатное резервирование массива: 
' имя локального массива совпадает с
' глобальным на уровне модуля

Теперь посмотрим, как подобная конструкция может выглядеть для VB/Win 4.0:

'  описание ТОЛЬКО типа (целочисленного) динамического массива,
'  глобального на уровне модуля:
Dim Array() As Integer

Private Sub Form_Load()
  Dim Array(10)   ' это локальный динамический массив!!!
  Array(3) = 15.5
  Debug.Print Array(3)   ' к тому же - Variant
  ' (будет напечатано 15.5)
End Sub

Private Sub Command1_Click()
  Print Array(3)  ' будет напечатано 3
  ' или появится сообщение об ошибке
End Sub

Private Sub Command2_Click()
  ' резервируем двумерный массив
  ReDim Array(10, 20)
  Array(2, 2) = 22.3   ' но Integer!
  Print Array(2, 2)    ' будет напечатано 22
  ' резервируем одномерный массив
  ReDim Array(15)
  Array(3) = 3.3
  Print Array(3) ' будет напечатано 3
End Sub

Private Sub Command3_Click()
  ' резервируем трехмерный массив
  ReDim Array(10, 20, 30)
  Array(1, 2, 3) = 123.1
  Print Array(1, 2, 3)
End Sub

Private Sub Command4_Click()
  ' пытаемся изменить тип данных
  ReDim Array(10, 20) As Single
End Sub

Прежде всего следует отметить, что запуск такого приложения в среде VB/Win 4.0 выполняется без проблем — никаких ошибок синтаксиса не обнаружено (Basic/DOS тут покажется чрезмерно строгим). Здесь следует обратить внимание на следующее:

Некоторые выводы

  1. К работе с глобальными динамическими массивами в VB нужно относиться очень внимательно.

  2. Потенциально использование дубликатных имен несет в себе определенную угрозу путаницы. Например, возможно, в приведенном выше фрагменте мы просто ошиблись, написав в процедуре Form_Load оператор Dim вместо ReDim, и при этом уверены, что имеем дело с глобальным массивом. На уровне модуля и его отдельных процедур противоречий между именами локальных и глобальных переменных не должно быть (ведь модуль пишется одним человеком). Реально это может пригодиться только для устранения возможных конфликтов между идентификаторами переменных, определенных на уровне модуля (Private) и на уровне приложения (Public). Хотя для разрешения подобных конфликтов классические принципы языков программирования требуют описания глобальных переменных не только в модулях, где они резервируются, но и в модулях, где они используются (в качестве входных точек). К сожалению, в VB этого механизма почему-то нет.

  3. Зачем нужно переопределять число индексов массива, просто не приходит в голову (такая возможность появилась только в версии 4.0). По нашему мнению, кроме путаницы, это ничего не приносит. Если у читателей есть хорошие примеры практического использования такого механизма, то было бы очень интересно узнать об этом. Кстати, реализованный в Basic/DOS механизм определения числа индексов массива на уровне Declarations представляется наиболее удобным. В VB 3.0 просто выдается сообщение о том, что имеет место противоречие описания массива с каким-то другим описанием, которое находится неизвестно где. А в Basic/DOS все подобные сравнения выполняются с известным главным описанием массива.

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

Совет 55. Будьте внимательны при переходе из Win16 в Win32

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

При этом особое внимание требуется тогда, когда внешний вид обращений остался вроде бы тем же, например целые данные просто превратились из 16-разрядных в 32-разрядные (это относится и к кодам ошибок). Вот как, например, будет выглядеть одновременная реализация процедуры GetWindowRect в Win16 и Win32:

 #If Win32 Then
    Private Declare Function GetWindowRect Lib "user32" _
            (ByVal lHwnd As Long, uRect As RectType) As Long
    Private Type RectType
            Left As Long
            Top As Long
            Right As Long
            Bottom As Long
    End Type
 #Else
    Declare Sub GetWindowRect Lib "user" _
            (ByVal iHwnd As Integer, uRect As RectType)
    Private Type RectType
            Left As Integer
            Top As Integer
            Right As Integer
            Bottom As Integer
    End Type
 #End If
Dim uRect As RectType

Private Function lGetWindowRect(iHwnd As Integer, uRect As RectType) 
  #If Win32 Then
     lHwnd = iHwnd
     lGetWindowRect = GetWindowRect(lHwnd, uRect)
  #Else
     lGetWindowRect = 0
     Call GetWindowRect(iHwnd, uRect)
  #End If
End Function

Проблема одновременного сопровождения 16- и 32-разрядного вариантов одного приложения несколько упрощается с появлением в VB 4.0 директив условной компиляции, хотя даже из этого простого примера видно, что хлопот с настройкой кода на работу и в Win16, и в Win32 вполне хватает. В данном случае API-процедура из подпрограммы превратилась в функцию, а первый передаваемый параметр поменял тип с Integer на Long. Еще одно отличие, которое не сразу заметно: поля структуры uRect также стали Long.

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

Что касается определяемых пользователем структур, то следует иметь в виду, что в VB выполняется выравнивание полей. Например, длинное целое будет иметь адрес, кратный 4. Это надо учитывать при работе с библиотеками, использующими иной способ упаковки.

Довольно неприятным моментом является и то, что в Win32 имена функций стали чувствительными к регистру букв. Если в приведенном выше примере в операторе Declare вы напишите имя GetwindowRect, то во время исполнения приложения при обращении к этой функции появится сообщение об ошибке "Specified DLL function not found" (запрошенная DLL-функция не найдена). Но за соответствием идентификаторов в Declare и в последующем коде программы VB следит сам.

В заключение этой темы имеет смысл напомнить, что с помощью ключевого слова Alias в операторе Declare можно менять фактические имена DLL-функций на альтернативные. Это может, например, пригодиться, если имена аналогичных API функций в версиях Win16 и Win32 отличаются.

Разумеется, здесь рассмотрены далеко не все проблемы перехода из Win16 в Win32, в том числе в плане использования функций API. По довольно единодушному мнению различных экспертов, самым надежным и лучшим пособием в этом вопросе является книга известного американского автора и разработчика VB-продуктов Дэна Эпплмана (Daniel Appleman) "Visual Basic Programmer's Guide to the Win32 API" (издательство Macmillan Computer Publishing/Ziff-Davis Press, 1996). Ранее его же книга, посвященная Win16 API, была бестселлером в течение нескольких лет.

Для тех, кто занимается преобразованием программ из VB 3.0 в VB 4.0, можно посоветовать использовать бесплатную копию комплекта программ и документации Upgrade Wizard подразделения Crescent фирмы Progress Software, которую можно найти на Web-странице: http://crescent.progress.com. Там содержится много ценной информации и полезных рекомендаций, а утилита-"мастер" автоматически преобразует значительную часть кода, расставив пометки там, где нужно исправить его вручную. Причем с ее помощью можно получить код как для Win16, так и для Win32.

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

Совет 56. Как определить, с каким приложением вы работаете в VBA

Когда вы создаете вспомогательные программные модули, предназначенные для использования в разных приложениях, которые могут быть как 16-, так и 32-разрядными, то в них нужно уметь определять тип приложений.

При работе с VBA и Access проверку режима работы приходится выполнять непосредственно в процессе выполнения приложения. Приведенные ниже примеры процедур используются для определения того, является ли конкретное приложение 32-разрядным или нет. Обратите внимание, что свойство Application.OperatingSystem в Microsoft Excel и в Microsoft Project возвращает не установленную версию Windows, а слой Windows, на котором выполняется приложение, например 16-разрядная подсистема в Windows NT.

  1. Microsoft Excel 5.0, Microsoft Excel 95, Microsoft Project 4.0 и Microsoft Project 95:
    Function Engine32%()
      If instr(Application.OperatingSystem, "32") Then Engine32% = True
    End Function
    

  2. Word 6.0 или Word 95:

    Function Engine32
      If Val(GetSystemInfo$(23)) > 6.3 Or Len(GetSystemInfo$(23)) = 0 _
        Then Engine32 = -1 Else Engine32=0
    End Function
    

  3. Microsoft Access 1.x:

    Function Engine32%()
      If SysCmd(7) > 2 Then Engine32% = True
    End Function
    

Вот пример работы, связанный с "разрядностью":

Declare Function GetTickCount32 Lib "KERNEL32" _
  Alias "GetTickCount" () As Long
Declare Function GetTickCount16 Lib "USER" Alias _
  "GetTickCount" () As Long

Function GetTickCount() As Long
  If Engine32%() Then
     GetTickCount=GetTickCount32()
  Else
     GetTickCount=GetTickCount16()
  End If
End Function

Здесь следует обратить внимание на то, что в Win16 и Win32 используемая API-функция GetTickCount имеет одно и тоже название. Поэтому, когда нет возможности применить механизм условной компиляции, необходимо использовать управляющее слово Alias для того, чтобы изменить имя функции по крайней мере в одном из операторов Declare (в данном примере GetTickCount32 и GetTickCount16). Затем в зависимости от разрядности приложения переменная GetTickCount устанавливается в соответствии с точным именем функции API (GetTickCount32 или GetTickCount16) и вызовом данной функции API.

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