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

Советы тем, кто программирует на VB & VBA

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

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


Совет 397. Как использовать свойство Filter при работе с ADO

При работе с наборами данных ADO вы можете использовать свойство Filter, например следующим образом:

rst.Filter = "pub_id ='000132' "

В этом случае будут выбраны все записи со значением 000132 в поле pub_id. Здесь важно помнить, что ADO физически не исключает из набора записи, которые не отвечают данному критерию, — просто они становятся недоступными для работы. Это, в частности, означает, что если вам нужно применить новый фильтр для работы с исходным набором, то нет необходимости производить отмену применения предыдущего фильтра — в любом случае ADO автоматически выполняет фильтрацию "исходного" набора данных.

Если же вы хотите вернуть набор данных в "неотфильтрованное" состояние, то нужно выполнить выборку с "пустым" фильтром:

rst.Filter = adFilterNone

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

Совет 398. В который раз повторяем: используйте режим Option Explicit

Мы уже неоднократно говорили о необходимости использовать режим обязательного объявления переменных, но приходится опять это повторять...

Читатель Вячеслав прислал письмо с вопросом, почему у него не работает вставка таблицы из VB-проекта в Word, точнее — почему не работает конструкция:

.ActiveDocument.Tables.Add
Selection.Range, NumRows:=2, NumColumns:=2

При выполнении этой конструкции выдается сообщение об ошибке Object required.

Судя по переписке (мы обменялись несколькими посланиями), на решение проблемы Вячеславу понадобилось больше недели, при этом он был уверен, что ее причина кроется в какой-то несовместимости VB и VBA. При этом он подозревал, что неадекватно ведет себя объект Range. Однако в данном случае все было гораздо проще: после переноса кода из VBA в VB Вячеслав забыл поставить точку перед Selection (этот код работал внутри конструкции With MyOblect).

Действительно, заметить такой дефект порой бывает непросто, а сообщение о необходимости объекта плохо помогает локализовать ошибку. Но если бы в программе был установлен режим Option Explicit, то появилось бы другое, более точное сообщение: "Не определена переменная Selection". К тому же ошибка была бы обнаружена не на этапе выполнения программы, а при компиляции кода. Очевидно, при такой диагностике понять причину неработоспособности кода было бы гораздо проще.

Напомним, что оператор Option Explicit будет автоматически вставляться в код нового программного модуля, если установить флажок Require Variable Declaration в окне Tools|Options|Editor (VB и VBA).

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

Совет 399. Задавайте вопросы в понятной форме

Именно с этого совета начиналась первая статья из серии "Размышления бывшего программиста" (КомпьютерПресс N 9/2000), но опять же приходится возвращаться к этой теме. Почему вопрос читателя, рассмотренный в предыдущем совете, решался больше недели? А потому, что его первое письмо содержало такой текст: "Я не знаю, как средствами не VBA, а VB6 создать таблицу в MS Word. Мне казалось, что все просто: скопировать содержимое макроса в процедуру. Однако VB6 ругается на именованный параметр Range. Подскажите, как мне быть?"

Кто сможет решить проблему, изложенную таким образом? Мы попросили прислать VB-проект с примером ситуации — Вячеслав прислал одну строку кода. Еще раз попросили... Только на третий раз он прислал нужный VB-проект, содержащий 7 строк кода (включая комментарии) и занимающий 1,5 Кбайт в ZIР-файле. Запустить пример, увидеть ошибку, понять причину и отправить ответ — все это заняло две минуты.

Итак, как нужно задать технический вопрос, если вы хотите действительно получить на него конкретный ответ:

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

Совет 400. Используйте режим раннего связывания

Об этой проблеме мы говорили в статье "Особенности технологий раннего и позднего связывания в Visual Basic" (КомпьютерПресс N 9/2000). Оба режима имеют свои достоинства и недостатки, но общая рекомендация такова: если нет особой нужды применять позднее связывание (иногда это просто необходимо), то лучше использовать раннее связывание.

Вот какой код прислал нам Вячеслав:

 Dim wdApp As Object
   Set wdApp = CreateObject("word.application") 'открыть Word
     With wdApp
     .Documents.Add 'Создать новый документ
        .ActiveDocument.Tables.Add _
   Selection.Range, NumRows:=2, NumColumns:=2
End With

Здесь используется позднее связывание — конкретизация объекта wpApp происходит только в момент выполнения программы (CreateObject). Но предпочтительнее выглядит вариант с ранним связыванием:

Dim wdApp As Word.Application
 Set wdApp = New Word.Application
 With wrdApp
  .Documents.Add    ' новый документ
  .ActiveDocument.SaveAs App.Path &  _
     "\RTFDOC.doc",wdFormatDocument
End With

В этом случае мы сразу четко фиксируем тип объекта (нужно также установить ссылку на библиотеку Word 9.0 Object Library). И в такой ситуации можно воспользоваться всеми преимуществами интеллектуальных подсказок при вводе кода.

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

Совет 401. Управляйте режимами программного контроля ошибок

Традиционный вариант управления программным контролем ошибок выглядит примерно так:

Sub MyProcedure
  'установка программной обработки ошибок
 On Error {GoTo MyError|Resume Next}
 ...
  'отмена программной обработки
 On Error GoTo 0
 ...
End Sub

При выходе из процедуры (End Sub или Exit Sub) отмена программной обработки ошибок выполняется автоматически, поэтому многие программисты вообще не ставят оператор On Error GoTo 0. Но тут есть один подводный камень: если вы напишете такой код:

Sub MyProcedure
  'установка программной обработки ошибок
 On Error GoTo MyError
 Call OtherProcedure
 ...
MyError:
 ' обработка 
  ...
End Sub

то в вызываемой подпрограмме OtherProcedure (и в других вложенных процедурах) будет продолжать действовать установка On Error GoTo MyError, то есть при появлении там ошибки управление будет передаваться на метку MyError. При этом нужно помнить, что если в процедуре OtherProcedure переопределить обработку ошибок, то при возврате управления в MyProcedure действие On Error GoTo MyError будет восстановлено автоматически.

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

Sub Main
  ' глобальное определение места
обработки ошибок
  ' для всего приложения
  On Error GoTo GlobalError
  Call OtherProcedure
 ...
GlobalError:
 ' обработка 
  ...
End Sub

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

Public MyErrorPoint As String
 Sub MyProcedure
  On Error GoTo GlobalError
  ...
  MyErrorPoint = "MySub"
  Call MySub
GlobalError:
 Select Case MyErrorPoint
   ...

И еще одна рекомендация: при прочих равных условиях предпочтительнее выглядит включение обработки ошибок с автоматической передачей управления на следующий оператор. Тогда вы сами можете проверять наличие ошибок в критических точках программы:

 On Error Resume Next
 ...
 Open "TestFile" For Input As #1
 If Err.Number <> 0 Then  ' ошибка

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

Совет 402. Быстрый поиск по списку

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

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 sSearch As String
Private Const LB_FINDSTRING = &H18F
Private Const lb_Err = (-1)

Private Sub Form_Load()
  ' заполнение списка
  With List1
    .AddItem "Adam"
    .AddItem "Allan"
    .AddItem "Arty"
    .AddItem "Aslan"
    .AddItem "Barbey"
    .AddItem "Bob"
  End With
  Timer1.Interval = 2000  ' сброс каждые две секунды
End Sub

Private Sub List1_KeyPress(KeyAscii As Integer)
   ' обработка нажатых клавиш
   Dim nResult As Long
   Timer1.Enabled = True   ' запуск контроля по таймеру
   sSearch = sSearch + Chr$(KeyAscii)
   With List1 
     ' поиск по списку
     nResult = SendMessage(.hwnd, LB_FINDSTRING, _
              .ListIndex, ByVal sSearch)
     If nResult <> lb_Err Then ' нашли
       .ListIndex = nResult ' установка позиции
       KeyAscii = 0  ' сброс ввода клавиши
     End If
   End With
End Sub

Private Sub Timer1_Timer()
   ' сброс поиска каждые две секунды
   sSearch = ""
   Timer1.Enabled = False
End Sub

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

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

Совет 403. При программном создании наборов данных указывайте размеры полей

Если вы попробуете выполнить такой код при программном создании набора данных ADO:

Dim rsBuild As ADODB.Recordset
Set rsBuild = New ADODB.Recordset
rsBuild.CursorLocation = adUseClient
rsBuild.Fields.Append "Question", adLongVarWChar
rsBuild.Fields.Append "AnswerA", adLongVarWChar

то, скорее всего, получите сообщение об ошибке: "Run-Time Error 3001: Method 'Append' of object 'Fields' failed". В документации VB по поводу метода Append говорится, что размер поля является необязательным параметром. Но это неверно — его нужно указывать в явном виде. Поэтому приведенный далее код будет работать без проблем:

Private Sub Form_Load()
  Dim rsBuild As ADODB.Recordset
  Set rsBuild = New ADODB.Recordset
  With rsBuild
    .CursorLocation = adUseClient
    .Fields.Append "Question", adLongVarWChar, 1
    .Fields.Append "Answer", adLongVarWChar, 1
    .Open
    .AddNew 0, String(5000, "*")
    .Update
    Debug.Print .Fields(0)
    .Close
  End With
  Set rsBuild = Nothing
End Sub

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

Совет 404. Программное отключение предупреждающих сообщений в приложениях MS Office

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

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

DoCmd.SetWarnings = False

Этот код выключает выдачу внутренних программных сообщений. Однако объект DoCmd — это возврат к временам, когда офисные приложения имели разные внутренние языки программирования. Такого объекта в других приложениях Office нет, но есть свойство DisplayAlerts, которое решает именно эту задачу. Обратите внимание, что его поддержка выполняется в различных программах по-разному. Например, в Excel этому свойству нужно просто присвоить значение логической переменной:

Public Sub DeleteWorksheet()
    ' запретить выдачу  сообщений
  Application.DisplayAlerts = False
  ActiveWindow.SelectedSheets.Delete
  Application.DisplayAlerts = True  'разрешить
End Sub

Следует иметь в виду, что нужно восстановить режим выдачи подобных сообщений, так как Excel не делает этого автоматически после завершения макрокоманды. Word для установки свойства DisplayAlerts использует специальные встроенные константы: wdAlertsNone, wdAlertsAll and wdAlertsMessageBox. Смысл первых двух установок понятен из их названий, а последняя означает, что Word будет выдавать только стандартные сообщения. Выполнение в этом случае кода:

ActiveDocument.Close

вызовет появление вопроса о сохранении документа. При выполнении следующего кода:

Application.DisplayAlerts = wdAlertsNone
ActiveDocument.Close
Application.DisplayAlerts = wdAlertsAll

сразу же появится окно SaveAs без промежуточного вопроса.

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

Совет 405. Как выводить на экран формы в динамическом режиме

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

Set frm = Forms(1)

или:

Set frm = Forms("frmEditor")

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

Private Sub Form_Load()
 
  With List1
    .AddItem "Form1"
    .AddItem "Form2"
    .AddItem "Form3"
   End With
End Sub

Private Sub List1_Click()
  Dim frm As Form
  Dim selForm As String
  selForm = List1.List(.ListIndex)
  Set frm = Forms.Add(selForm)
  frm.Show
  Set frm = Nothing
End Sub

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

Совет 406. Как узнать параметры диска

Вы хотите узнать серийный номер жесткого диска, а заодно и имя тома логического диска? Это можно сделать с помощью такого кода:

Private Declare Function GetVolumeInformation _
  Lib "kernel32" Alias "GetVolumeInformationA" _
  (ByVal lpRootPathName As String, _
  ByVal lpVolumeNameBuffer As String, _
  ByVal nVolumeNameSize As Long, _
  lpVolumeSerialNumber As Long, _
  lpMaximumComponentLength As Long, _
  lpFileSystemFlags As Long, _
  ByVal lpFileSystemNameBuffer As String, _
  ByVal nFileSystemNameSize As Long) As Long

Private Sub Form_Load()
   Dim sDrive As String
   Dim VolumeName As String
   Dim SerialNumber As Long
   '
   sDrive = "c:\"  'имя диска
   VolumeName = Space$(128)
   Call GetVolumeInformation(sDrive, _
      vbNullString, 128&, SerialNumber, _
      ByVal 0&, ByVal 0&, vbNullString, 0)
   VolumeName = Left$(VolumeName, InStr(VolumeName, Chr$(0)))
   MsgBox "Серийный номер диска = " & SerialNumber & vbCrLf & _
          "Имя тома = " & VolumeName
End Sub

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

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

Следует иметь в виду, что метод Clone не создает дубликат набора данных — он просто делает два разных указателя, которые работают с одним и тем же физическим набором. А выполнить формирование копии можно с помощью объекта Stream из состава ADO 2.5 (или более поздней версии):

Dim rsOne As New ADODB.Recordset
Dim rsTwo As New ADODB.Recordset
Dim oTempStream As New ADODB.Stream
' Далее идет формирование набора reOne
 ... 
rsOne.Save
oTempStream, adPersistXML  ' запоминаем 
rsTwo.Open
oTempStream            ' восстанавливаем

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

Совет 408. Как узнать размер свободного места на диске

Для этого можно использовать API-функцию GetDiskFreeSpaceA из библиотеки Kernel32, но она корректно работает для дисков размером до 2 Гбайт, так как была предназначена для Windows 95, где использовали FAT16. При работе с дисками больших размеров можно применить объект FileSystemObject из состава библиотеки Microsoft Scripting Runtime library (scrrun.dll), которую можно загрузить по адресу: http://www.microsoft.com/msdownload/vbscript/scripting.asp. Вот как будет выглядеть нужный нам код:

Dim fso As FileSystemObject
Dim drv As Drive
Set fso = New FileSystemObject
Set drv = fso.GetDrive("C")
MsgBox FormatNumber(drv.AvailableSpace / 1024, 0) & " Кб"

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

Совет 409. Блокировка Windows 2000

Осуществление блокировки Windows NT оказалось делом непростым. Но Windows 2000 включает в себя специальную функцию для выполнения такой операции:

Private Declare Function LockWorkStation Lib _
   "user32.dll" () As Long
Call LockWorkStation

Поскольку у этой функции нет параметров, то к ней можно обратиться напрямую следующим образом:

Call Shell ("rundll32 user32.dll, LockWorkStation", vbNormalFocus)

Такую конструкцию можно применять и в 16-разрядных приложениях. А вот как будет выглядеть операция на VBScript:

Dim WshShell
Set WshShell = CreateObject("WScript.Shell")
WshShell.Runl ("rundll32 user32.dll, LockWorkStation)

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