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

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

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

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


Совет 133. Управление длиной элемента списка ComboBox, вариант 2

(совет прислал Михайлов Александр)

В КомпьютерПресс N 3'98 был опубликован Совет 126, где описывалась конструкция, способная компенсировать отсутствующее у элемента управления ComboBox свойство MaxLength. Предлагаю еще один способ реализации этого свойства с помощью функции API — SendMessage.

При работе с Win32 API (VB4/32 и VB5) необходимо описать данную функцию и константу CB_LIMITTEXT (как ее параметр) в таком виде:

Private Declare Function SendMessage Lib _
  "user32" Alias SendMessageA" (ByVal hWnd _
  As Long, ByVal wMsg As Long, ByVal wParam _
  As Long, lParam As Any) As Long
Private Const CB_LIMITTEXT = &H141

Для Win16 API (VB4/16 и более ранние версии VB) описание выглядит следующим образом:

Private Declare Function SendMessage Lib _
  "User" (ByVal hWnd As Integer, ByVal wMsg _
  As Integer, ByVal wParam As Integer, _
  lParam As Any) As Long
Private Const WM_USER = &H400
Private Const CB_LIMITTEXT = (WM_USER+1)

(Все описания функций API и констант взяты из файлов WIN31API.TXT и WIN32API.TXT, поставляемых с Visual Basic 4.)

Для установки ограничений ввода для списков, находящихся в форме, достаточно в событии Load вызвать функцию SendMessage для каждого элемента управления ComboBox с соответствующим параметром MaxLengthN — максимальным количеством символов, которое можно напечатать в поле ввода элемента ComboBox.

Sub Form_Load()
  Call SendMessage(Combo1.hWnd, CB_LIMITTEXT, MaxLength1, 0)
  Call SendMessage(Combo2.hWnd, CB_LIMITTEXT, MaxLength2, 0)
. . .
  Call SendMessage(ComboN.hWnd, CB_LIMITTEXT, MaxLengthN, 0)
End Sub

Для Win32 передаваемые параметры имеют тип Long, а для Win16 — Integer. Обратите внимание, что, хотя здесь используются процедуры-функции, к ним можно обращаться с помощью оператора Call. В этом случае просто не производится передача в вызывающую программу возвращаемого значения функции.

Дополнительные замечания авторов рубрики

Как видим, VB с помощью функций API может производить установку параметров элементов управления методом, который по-английски называется "send messages to controls". Более того, такую установку можно проводить и для тех параметров, которые в явном виде не представлены свойствами элементов управления. Обратите внимание, что каждый программный объект (формы, элементы управления) имеет в среде Windows свой уникальный номер-идентификатор, который хранится в свойстве hWnd.

Существуют два варианта передачи параметров объектам Windows: с помощью функций SendMessage и PostMessage. При работе Windows не всегда можно сразу установить свойства объекта из-за параллельности работы заданий. Функция SendMessage производит немедленный вызов соответствующей операции для указанного объекта и возвращает управление в вызывающую программу только после реального выполнения нужной установки. PostMessage устанавливает соответствующий запрос в очередь обращений к объекту и возвращает управление, не дожидаясь его выполнения.

Обе функции имеют целый ряд разновидностей. Например, SendMessageTimeout возвращает управление либо после выполнения установки, либо по истечении заданного промежутка времени. А функция BroadcastSystemMessage делает установку параметров всех объектов или приложений указанного типа.

Список констант, задающих выполнение конкретных операций для этих функций, весьма обширен и состоит из двух больших наборов. Первый содержит константы, закрепленные для разных типов элементов управления (LB — List Box, CB — Combo Box, EM — Edit control и пр.). Во втором наборе используются константы, имена которых имеют вид WM_xxx, — они работают с несколькими типами объектов. Например, WM_COPY=&H301 выполняет копирование выделенного текста элементов управления Text Box или Combo Box в буфер обмена.

Однако нужно отметить, что довольно многие из таких API-операций не доступны для VB. (Например, WM_ENABLE=&HA, которая делает объект доступным или недоступным.) С другой стороны, ряд API-операций может быть реализован собственными средствами VB. (В частности, приведенное выше копирование выделенного текста — Clipboard.SetText Text1.SelText.)

Рекомендуем книгу по Win32 API

Полное описание функций API приведено в наборе для разработчиков Win32 SDK. Однако тем, кто занимается программированием на VB, мы очень рекомендуем книгу известного американского автора и разработчика VB-продуктов Дэна Эпплмана: "Dan Appleman's VB 5.0 Programmer's Guide to the Win32 API": Daniel Appleman, Macmillan Computer Publishing/Ziff-Davis Press, 1997. ISBN: 1-56276-446-2, 1548 с. (с компакт- диском). По единодушному мнению американских специалистов, с которым согласны и авторы рубрики, эта книга является самым надежным и полным пособием по данному вопросу. Так же, как и более ранняя книга Эпплмана, посвященная Win16 API, она сразу стала одним из бестселлеров.

В данной книге расширен материал первого издания, появившегося в 1995 г., — в ней отражены новые возможности VB 5.0. Первая часть книги посвящена общим принципам построения набора Windows API и совместной работе VB-программ с процедурами статических и динамических библиотек. В следующих двух частях приведены подробные справочные сведения по функциям API с учетом специфики VB, в том числе версий 4.0 и 5.0 (при этом указаны и те функции, которые не поддерживаются VB, что позволяет, в частности, составить лучшее представление об узких местах данной системы). В четвертой части и шести приложениях даны примеры их практического применения и различные дополнительные сведения.

Прилагаемый компакт-диск содержит ПОЛНЫЙ текст книги и всех программных примеров, а также три дополнительные главы ("Serial Communications", "Network Functions" и "API Types Libraries"), которые не вошли в печатный вариант. Кроме того, на диске находится ряд отдельных статей автора, большое число вспомогательных программ и демо-версии некоторых дополнительных продуктов.

Стоимость книги в США — 60 долл. Нам ее привез из Америки приятель, который предупредил перед отъездом, что у него не будет времени заниматься поиском книги. Однако потом он сообщил, что нашел ее в первом же книжном магазине небольшого провинциального городка.

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

Совет 134. Передача адреса процедуры в функцию API с помощью AddressOf

Один из традиционных недостатков систем MS Basic, в том числе и VB — отсутствие возможности работать напрямую с адресами функций (function pointer). Хотелось бы подчеркнуть, что это не является неизбежной характеристикой языка-интерпретатора (как это часто представляется некоторым программистам) или Basic, а связано с представлением о необходимой функциональности инструментария со стороны его создателя, Microsoft. В версии 5.0 (что никак не связано с появлением компилятора) Microsoft решила частично устранить данное узкое место в VB, позволив использовать с помощью оператора AddressOf передачу адреса функции в качестве аргумента при обращении к процедуре. Однако, что весьма примечательно, в состав VBA 5.0 по прихоти Microsoft этот оператор не попал.

Такое нововведение существенно упрощает использование в VB функций API (и аналогичных процедур в любых DLL), при выполнении которых, в свою очередь, осуществляется обратный вызов (callback, как это называется в C) к указанной пользовательской функции, написанной на VB. Мы говорим "упрощает", поскольку при большом желании и наличии соответствующих знаний применять подобные функции можно было и в более ранних версиях VB. Обычно для этого приходилось использовать некоторые нестандартные элементы управления. Работа с одним из них, DWCBK32D.OCX, описана в книге Эпплмана, а на компакт-диске записан сам модуль.

При использовании оператора AddressOf следует иметь в виду два ключевых момента:

Использование оператора AddressOf требует правильного понимания идеи применения функций с обратным вызовом и четкого представления о том, как с ними работают конкретные DLL-функции. Отладка таких обращений довольно затруднительна, так как программа работает в том же самом процессе, что и среда разработки. В ряде случаев традиционные методы отладки здесь просто невозможны. Следует иметь в виду, что обработку возможных ошибок в вызываемой процедуре нужно выполнять именно в ней, поставив, например, в ее начало оператор On Error Resume Next.

Вы можете создать прототип функции обратного вызова в библиотеке DLL, созданной с помощью VC++ или другого аналогичного инструмента. Однако, чтобы применять AddressOf, этот прототип должен использовать при вызове преобразование __stdcall, а не __cdecl, которое реализуется по умолчанию.

Ниже приведен пример применения оператора AddressOf, взятый нами из книги "MS VB 5.0. Мастерская разработчика" Дж. Крейга и Дж. Уэбба. В нем производится обращение к API-функции EnumChildWindows, которая в свою очередь вызывает VB- функцию ChildWindowProc. Обратите внимание, что последняя должна находиться в модуле кода, а не формы. После запуска этого примера (установите Sub Main в поле Startup Object окна свойств проекта) в окно Immediate будут выведены все описатели (handles) окон среды разработки. Однако заметьте: если вы скомпилируете программы и запустите автономный EXE-модуль, список описателей будет совсем другим.

Option Explicit
Private Declare Function GetActiveWindow Lib "User32" () As Long

Private Declare Function EnumChildWindows _
  Lib "User32" ( ByVal hWnd As Long, ByVal _
  lpWndProc As Long, ByVal lp As Long) As Long

Sub Main()
  Dim hWnd As Long
  Dim x As Long
  ' получаем описатель активного окна
  hWnd = GetActiveWindow()
  If (hWnd) Then
    ' вызываем API-функцию EnumChildWindows,
    ' а та вызывает ChildWindowProc для каждого
    ' дочернего окна
    x = EnumChildWindows(hWnd, AddressOf _
    ChildWindowProc, 0)
  End If
End Sub

' вызывается API-функцией EnumChildWindows
Function ChildWindowProc(ByVal hWnd As Long, _
  ByVal lp As Long ) As Long
  ' параметры hWnd и lp передаются функцией
  ' EnumChildWindows
  Debug.Print "Window: "; hWnd
  ' сообщаем, успешен ли вызов
  ' (в стиле C, 1 - True, 0 - False)
  ChildWindowProc = 1
End Function

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

Совет 135. Как ускорить создание строковых буферов

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

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

  1. Первый вариант реализует наиболее очевидный и простой алгоритм:

    strBuffer = strBuffer & strNewData
    

    Однако с ростом числа циклов lCount время операций резко возрастает — почти в квадратичной зависимости (что является следствием увеличения размера буфера).

  2. Во втором варианте используется промежуточный, временный буфер. Когда он становится достаточно большим, его содержимое добавляется к основному буферу, затем содержимое временного буфера очищается и работа продолжается. Скорость операций возрастает в десятки и даже сотни раз. Однако с какого-то момента и здесь рост идет по нелинейному закону.

  3. Третий вариант реализует принципиально другой механизм. Ключевым моментом здесь является использование операции Mid$ вместо сложения двух переменных. Если в предыдущем случае эффект достигается за счет уменьшения размера накапливающего буфера, то здесь сводятся к минимуму операции динамического резервирования памяти — Mid$ записывает строку в тот же самый буфер достаточно большой длины. В результате скорость операций не просто увеличивается: время их выполнения практически не зависит от размера буфера и растет строго пропорционально числу циклов.

    Основная проблема здесь — контроль и автоматическое увеличение этого буфера (второй вопрос решается очень просто — методом удвоения буфера — начиная с длины, равной одному байту!). Обратите внимание, что вычисление длины добавляемой строки специально производится внутри цикла, чтобы показать, что она может быть различной. Конечно, сложность программного кода возрастает, но посмотрите, как растет быстродействие! В ряде случаев это бывает весьма критичным.

Таблица. Результаты тестирования программы из Совета 135

Количество циклов, lCount
Время, сек.
Вариант 1 Вариант 2 Вариант 3

1000

1

-

-

2000

5

-

-

3000

10

-

-

4000

21

1

-

7000

61

-

-

10000

131

3

-

30000

-

15

0

50000

-

36

1

100000

-

139

2

200000

-

-

5

Примечание. Выполнение в среде VB 5.0, процессор Pentium/MMX 200 МГц, 32 Мбайт ОЗУ; "-" замер не проводился.

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

Совет 136. Управление вводом в текстовых полях

При создании пользовательских интерфейсов достаточно часто встает задача управления вводом данных в текстовых полях окон, например в элементе управления Text Box. Это может быть связано, например, с блокировкой ввода определенных символов или их автоматической заменой (мы уже давали ряд советов на данную тему). Такое управление реализуется с помощью написания соответствующего программного кода в событийной процедуре KeyPress (по поводу отличия кодов KeyAscii и KeyCode см. Совет 129).

Однако при разработке приложений бывает полезно использовать некую единую универсальную процедуру для выполнения таких преобразований. Выгоды здесь две: возможна некоторая экономия объема программы, а самое главное, обеспечивается гибкое управление режимами ввода непосредственно в процессе работы приложения. Один из вариантов такой программы приведен в листинге 136. Для ее использования достаточно, указав нужное значение Filter$, вставить в процедуры KeyPress следующее обращение:

Call ChangeKeyAscii(KeyAscii, Filter$)

Вот некоторые примеры значений параметра Filter$:

В этом примере мы специально не использовали операторы Lcase/Ucase, чтобы показать возможность их альтернативной реализации (см. Советы 89 и 90). Обратите внимание, что наш вариант будет работать для русских букв, даже если не установлена русская кодовая таблица cp1251. Разумеется, данный пример — лишь один из возможных вариантов таких процедур преобразования. Учитывая специфику своих приложений, вы легко создадите свои собственные вспомогательные подпрограммы.

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

Совет 137. Как перетащить элементы из одного списка в другой

Чтобы взять элементы из одного списка и поместить их в другой, создайте два списка (lstDraggedItems и lstDroppedItems) и текстовое поле (txtItem) в форме (frmTip). Поместите такой код в событие Load для формы:

Private Sub Form_Load()
  ' Установите свойство Visible
  ' для текстового поля как False
  txtItem.Visible = False
  ' Добавьте элементы к списку 1 (lstDraggedItems)
  lstDraggedItems.AddItem "Яблоко"
  lstDraggedItems.AddItem "Апельсин"
  lstDraggedItems.AddItem "Грейпфрут"
  lstDraggedItems.AddItem "Банан"
  lstDraggedItems.AddItem "Лимон"
End Sub

В событии MouseDown списка lstDraggedItems напишите следующее:

Private Sub lstDraggedItems_MouseDown _
  (Button As Integer, Shift As Integer, _
  X As Single, Y As Single)
  '
  txtItem.Text = lstDraggedItems.Text
  txtItem.Top = Y + lstDraggedItems.Top
  txtItem.Left = X + lstDraggedItems.Left
  txtItem.Drag
End Sub

А в событии DragDrop списка lstDroppedItems введите такой код:

Private Sub lstDroppedItems_DragDrop _
  (Source As Control, X As Single, Y As Single)
  '
  If lstDraggedItems.ItemData _
    (lstDraggedItems.ListIndex) = 9 Then Exit Sub
  ' Убедимся, что данный элемент
  ' не будет перемещен снова
  lstDraggedItems.ItemData _
  (lstDraggedItems.ListIndex) = 9
  lstDroppedItems.AddItem txtItem.Text
End Sub

Теперь запустите этот тест на выполнение и попробуйте перетащить элементы из списка lstDraggedItems и поместить их в список LstDroppedItems.

Обратите внимание, что вы не можете перемещать элементы второго списка в первый. Кроме того, перетаскиваемые элементы сохраняются в первом списке. Надеемся, вы сумеете легко устранить данные ограничения.

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

Совет 138. Используйте ключевое слово ParamArray для передачи произвольного числа параметров

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

Public Sub FillList(ListControl As ListBox, ParamArray Items())
  Dim i As Variant
  With ListControl
    .Clear
    For Each i In Items
      .AddItem i
    Next
  End With
End Sub

Обратите внимание, что ключевое слово ParamArray идет ВПЕРЕДИ параметра в строке описания.

Используя эту процедуру (довольно универсальную!), можно заменить одним оператором сразу пять строк в процедуре Form_Load:

Call FillList (lstDraggedItems, "Яблоко", "Апельсин", _
"Грейпфрут", "Банан", "Лимон")

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

Совет 139. Создание нового контекстного меню

Следующая программа позволит вам заменить исходное контекстное меню на свое собственное. Для этого добавьте следующий код к форме или BAS-модулю:

Private Const WM_RBUTTONDOWN = &H204
Private Declare Function SendMessage Lib "user32" _
  Alias "SendMessageA" (ByVal hwnd As Long, ByVal _
  wMsg As Long, ByVal wParam As Long, lParam As Any) As Long

Public Sub OpenContextMenu(FormName As Form, MenuName As Menu)
'
  ' Говорит системе, что пользователь щелкнул
  ' правой кнопкой мыши на форме
  Call SendMessage(FormName.hwnd, _
  WM_RBUTTONDOWN, 0, 0&)
  ' Показывает контекстное меню
  FormName.PopupMenu MenuName
End Sub

После этого с помощью редактора Visual Basic Menu Editor и приведенной здесь таблицы создайте простое меню.

Caption           Name         Visible

Контекстное меню  mnuContext   No
Первый элемент    mnuContext1
Второй элемент    mnuContext2

Обратите внимание, что два последних элемента смещены на один уровень (...) и что у первого элемента ("Контекстное меню") свойство Visible установлено как NO.

Теперь добавьте текстовое поле к форме и введите следующий код в событие MouseDown для этого элемента управления:

Private Sub Text1_MouseDown(Button As Integer, _
  Shift As Integer, X As Single, Y As Single)
  '
  If Button = vbRightButton Then
    Call OpenContextMenu(Me, Me.mnuContext)
  End If
End Sub

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

Совет 140. Используйте функцию FreeFile для предотвращения конфликтов при открытии файлов

VB и Access позволяют жестко кодировать номера файлов при использовании оператора File Open. Например:

Open "myfile.txt" For Append As #1
Print #1,"строка текста"
Close #1

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

intFile=FreeFile
Open "myfile.txt" For Append As #intFile
Print #intFile,"строка текста"
Close #intFile

Здесь следует иметь в виду следующее:

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

Совет 141. Как предупредить пользователей о разрешении экрана

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

Public Function CheckRez(pixelWidth As Long, _
  pixelHeight As Long) As Boolean
  '
  Dim lngTwipsX As Long
  Dim lngTwipsY As Long
  '
  ' Преобразование пикселов
  lngTwipsX = pixelWidth * 15
  lngTwipsY = pixelHeight * 15
  '
  ' Проверка текущих параметров
  If lngTwipsX <> Screen.Width Then
    CheckRez = False
  Else
    If lngTwipsY <> Screen.Height Then
      CheckRez = False
   Else
     CheckRez = True
   End If
  End If
End Function

После этого поместите следующий код в начало программы:

If CheckRez(640, 480) = False Then
  MsgBox "Неправильный размер экрана!"
Else
  MsgBox "Разрешение экрана соответствует!"
End If

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

Совет 142. Простой способ определения логической переменной

Довольно часто требуется зафиксировать некоторое соотношение чисел с помощью логической переменной. Пример этого приведен в предыдущем совете в следующем фрагменте:

If lngTwipsY <> Screen.Height Then
  CheckRez = False
Else
  CheckRez = True
End If

Однако гораздо проще записать его так:

CheckRez = (lngTwipsY = Screen.Height)

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

Совет 143. Как быстро выделить текст для события GotFocus

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

Public Sub FocusMe(ctlName As Control)
'
  With ctlName
   .SelStart = 0
   .SelLength = Len(ctlName)
  End With
End Sub

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

Private Sub txtFocusMe_GotFocus()
  Call FocusMe(txtFocusMe)
End Sub 

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