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

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

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

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


Совет 285. Как получить значения цветовой палитры

В состав VB входит удобное средство преобразования отдельных значений Red, Green и Blue в одно цветовое значение типа Long — это функция RGB. К сожалению, VB не позволяет проводить обратное преобразование, но вы можете получить конкретные цветовые значения из шестнадцатеричного представления значения типа Long, создаваемого функцией RGB. Для этого создадим следующую функцию и поместим ее в стандартном модуле программы:

Public Type RGB_Type
  R As Long
  G As Long
  B As Long
End Type

Public Function ToRGB(ByVal Color _
  As Long) As RGB_Type
  '
  Dim ColorStr As String
  ColorStr = Right$("000000" & Hex$(Color), 6)
  With ToRGB
    .R = Val("&h" & Right$(ColorStr, 2))
    .G = Val("&h" & Mid$(ColorStr, 3, 2))
    .B = Val("&h" & Left$(ColorStr, 2))
  End With
End Function

Чтобы воспользоваться данной функцией, поместите в форму какое-либо изображение, задав необходимое имя в свойстве Picture формы, а затем введите такой код:

Private Sub Form_MouseUp(Button _
  As Integer, Shift As Integer, _
  X As Single, Y As Single)
  '
  Dim RGB_Point As RGB_Type
  RGB_Point = ToRGB(Point(X, Y))
  With RGB_Point
    Me.Caption = "R = " & .R & " G = " & .G & " B = " & .B
  End With
End Sub

Запустите программу на выполнение. Щелкая мышью на различных частях изображения, вы будете видеть в заголовке формы соответствующие значения RGB. Обратите внимание, что при работе в VB3 вам нужно получать эти значения по отдельности, поскольку VB не поддерживал возвращение заданных пользователем типов данных до версии VB4. Подобное преобразование можно осуществить более быстрым способом, если воспользоваться командой LSet, которая копирует содержимое одного заданного пользователем типа данных (user-defined type, udt) в другой. Для этого заменим функцию, находящуюся в стандартном модуле программы, на такую:

Public Type RGB_Type
     R As Byte
     G As Byte
     B As Byte
     Filler As Byte
End Type
Private Type RGB_Full_Type
     lngRGB As Long
End Type

Public Function ToRGB(ByVal _
  vlngColor As Long) As RGB_Type
  '
  Dim udtRGBFull As RGB_Full_Type
  udtRGBFull.lngRGB = vlngColor
  LSet ToRGB = udtRGBFull
End Function

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

Совет 286. Будьте внимательны при использовании оператора Debug.Print

Вопреки распространенному мнению, операторы Debug.Print не всегда удаляются из исполняемых файлов. Продемонстрируем, например, такой случай. Создайте новый проект, поместите на форму командную кнопку Command1 и напишите для нее такой код:

Private Sub Command1_Click()
  Debug.Print DebugTime
End Sub
Public Function DebugTime()
  MsgBox "Привет!"
End Function

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

Таким образом, сформулируем основные выводы:

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

Совет 287. Как упростить математические вычисления в VBA

Существует возможность упростить некоторые вычислительные процедуры в Excel, а также добавить в Word отсутствующие в нем функции. К сожалению, большинство учебных пособий по VBA в Office 97 предназначены для изучения нематематических функциональных возможностей пакетов, например форматирования текста или вывода графических изображений.

Отсутствие необходимой документации ставит в затруднительное положение тех, кто хочет использовать макросы для решения своих математических задач, поскольку обработка числовых значений в ячейке таблицы Word происходит иначе, чем в ячейке электронной таблицы Excel. Потратив некоторое время, вы, конечно же, найдете необходимое описание синтаксиса математических функций в справочной системе Word или Excel. Кроме того, можно воспользоваться руководством Microsoft Office 97: Visual Basic Programmer's Guide, изданным корпорацией Microsoft (русская версия была выпущена "Русской Редакцией"), где рассматриваются практически все вопросы на данную тему.

Здесь мы приводим два примера, которые в явном виде демонстрируют разницу при работе в Word и Excel. Они вычисляют кубическую сумму значений первых восьми ячеек столбца 1 и помещают результат в девятую ячейку столбца 1:

Данные вычисления могут быть проведены целиком в рамках функциональных возможностей Excel, однако для этого потребуется создать дополнительный столбец. А вот как выполнить подобные действия внутри Word? Попытаемся разобраться в этом. Напомним, что Word VBA имеет неинтуитивный синтаксис для обработки ячеек. Чтобы прочитать значение ячейки, следует использовать ключевое слово Calculate.

А чтобы записать какую-либо величину в ячейку, надо вначале преобразовать ее в строковую переменную, а затем применить абсолютно неочевидное ключевое слово InsertAfter. Нумерация таблиц в документе Word осуществляется последовательно, поэтому в первом примере мы имеем дело с первой таблицей документа. Каждая таблица рабочей книги Excel представляет собой одну большую таблицу, поэтому во втором примере мы работаем с первой таблицей рабочей книги. Кроме того, во втором примере Range("a9") можно заменить на Cells(9, 1) или на один из других вариантов, требующих использования ключевого слова Range.

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

Совет 288. Как вычислить интервал между двумя датами, измеряемый в разных единицах

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

Отгадайте такую загадку. Заданы две даты в виде переменных DateStart и DateFinish. Чтобы определить временной интервал между ними, мы написали такую процедуру:

Print "Интервал в годах = "; DateDiff("yyyy", DateStart, DateFinish)
Print "Интервал в месяцах= "; DateDiff("m", DateStart, DateFinish)
Print "Интервал в днях = "; DateDiff("d", DateStart, DateFinish)
Print "Интервал в часах = "; DateDiff("h", DateStart, DateFinish)
Print "Интервал в минутах = "; DateDiff("n", DateStart, DateFinish)
Print "Интервал в секундах= "; DateDiff("s", DateStart, DateFinish)

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

Интервал в годах = 1
Интервал в месяцах = 1
Интервал в днях = 1
Интервал в часах = 1
Интервал в минутах = 1
Интервал в секундах = 1

ВОПРОС. Почему так произошло и о каких датах шла речь?

ОТВЕТ. Дело в том, что функция DateDiff определяет временной интервал элементарно — в соответствии с заданным первым параметром просто отбрасывает значения даты "после этой точки". То есть если вы задали "день", то отбрасываются часы (0 часов), если месяц — дни (первое число месяца). В соответствии с этим алгоритмом получается, что между 31 мая 2000-го и 1 июня 2000-го в единицах "месяц" разница — один месяц (что в определенном смысле совершенно верно).

В нашем же примере исходные значения даты были равны

DateS = "31.12.2000 23:59:59"
DateF = "01.01.2001"

Изменение показателя текущего момента на одну секунду привело к изменению минут, часов, суток, месяца и года (и даже века и тысячелетия). Очевидно, что самое точное определение интервала дается в данном случае в секундах (этой точности вполне достаточно для решения большинства бытовых и деловых проблем). Но как интерпретировать величину типа 12 345 678 сек? Конечно, желательно получить информацию в более привычных единицах — месяцах, днях, минутах. В таких случаях вам поможет подпрограмма DateIntervals, позволяющая передавать две даты и свои собственные переменные для указанного интервала:

Public Sub DateIntervals(ByVal DateS _
  As Date, ByVal DateF As Date, ParamArray Prams())
  If UBound(Prams) < 0 Then Exit Sub

  Dim i As Long, itr As String
  '
  ' Если не задан день, то считаем его "сегодняшним"
  If DateValue(DateS) = 0 Then DateS = DateS + DateValue(Now)
  If DateValue(DateF) = 0 Then DateF = DateF + DateValue(Now)
  '
  For i = 0 To IIf(UBound(Prams) > 5, 5, UBound(Prams))
    If Not IsMissing(Prams(i)) Then
      If i = 0 Then
        itr = "yyyy"
      Else
        itr = Mid$("mdhns", i, 1)
      End If
      Prams(i) = DateDiff(itr, DateS, DateF)
      If DateAdd(itr, Prams(i), DateS) <= DateF Then _
        Prams(i) = Prams(i) - 1
      DateS = DateAdd(itr, Prams(i), DateS)
    End If
  Next i
End Sub
Подпрограмма DateIntervals возвращает наибольший полный интервал указанного вами типа (год, месяц, день, час, минута, секунда) между двумя датами. Например, чтобы получить интервал времени в часах и минутах между 09:00 и 17:15, передайте в подпрограмму эти две даты, а также две переменные, задающие размерность интервала. Используйте запятые, чтобы пропустить более крупные ненужные интервалы:

Dim Hours As Variant, Minutes As Variant
Call DateIntervals(Now, "23.05.2000", , , , Hours, Minutes)
MsgBox "Часов = " & Hours & _
       "Минут = " & Minutes

Подпрограмма вернет "Часов = 8, Минут = 15".

Однако здесь следует обратить внимание на такой любопытный момент. Если вы выполните такое обращение к функции:

Call DateIntervals("28.02.2000", "01.03.2001", Years, , Days)
MsgBox Years & " " & Days
Call DateIntervals("29.02.2000", "01.03.2001", Years, , Days)
MsgBox Years & " " & Days

то получите для разных начальных дат один и тот же результат — 1 год и 1 день. Казалось бы, в подпрограмме есть ошибка, но это не так. Данный парадокс объясняется неопределенностью интервала в один год — он может быть 365 и 366 дней (так же, как и в один месяц). Соответственно в первом случае "год" является високосным (366 дней), а во втором — обычным (365 дней). Чтобы представить эту ситуацию, вообразите, что ваш знакомый говорит 31 января: "Позвони мне ровно через месяц" (или 29 февраля 2000 года — "ровно через год"). Когда же будет эта точная дата намеченного звонка?

Отметим также, что алгоритм расчета интервала с использованием привычных единиц можно выполнить и по-другому — сначала определить интервал в секундах, а потом выделить из него минуты, часы и сутки (с месяцами и годами тут возникнут те же проблемы). Но он будет выглядеть не так изящно, как приведенная выше подпрограмма.

В процедуре DateIntervals хотелось бы обратить внимание еще на три используемые нами конструкции:

  1. Для передачи возвращаемых параметров мы используем массив Param() с ключевым словом ParamArray. Такая конструкция применима только в конце списка аргументов подпрограммы и указывает, что данный аргумент является массивом типа Optional элементов типа Variant. С его помощью можно задать произвольное число аргументов. Кроме того, ParamArray нельзя использовать вместе с ключевыми словами ByVal, ByRef, или Optional.

    В принципе, можно было бы просто зарезервировать в вызывающей подпрограмме массив Param (0 To 5) и использовать непосредственно его. Но в данном случае подпрограмма выполнила бы расчет для всех элементов этого массива. Применение ParamArray позволяет нам пропускать "ненужные" параметры. Например, в нашем обращении мы получим результат в полных годах и весь остаток интервала — в секундах:

    Call DateIntervals("28.02.2000", "01.03.2001", Years, , , , ,Secs)
    

  2. Для выбора нужного значения из двух вариантов мы используем функцию:

    MyVal = IIf(expr, truepart, falsepart)
    

    которая равнозначна такому варианту:

    If expr Then
      MyVal = truepart
    Else
      MyVal = falsepart
    End If
    

  3. При коррекции временного интервала мы использовали такую конструкцию:

    If DateAdd(itr, Prams(i), DateS) <= DateF Then _
                    Prams(i) = Prams(i) - 1
    

    Любители хитроумных преобразований данных могли бы предложить более "изящный" вариант:

    Prams(i) = Prams(i) + (DateAdd(itr, Prams(i), DateS) > DateF)
    

    имея в виду, что арифметическое значение логического выражения будет равно -1 (True) или 0 (False). Мы, со своей стороны, настоятельно не рекомендуем пользоваться неявными преобразованиями типов данных.

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

Совет 289. Как правильно прочитать имя каталога

Проблема заключается в том, что в некоторых случаях имя каталога содержит символ обратной косой черты в конце, а иногда — нет. Пример такой путаницы — обращение к свойству Path объекта App. Если приложение находится в подкаталоге, то такой черты в конце не будет (например, C:\dir_x\dir_y\dir_z), но если оно располагается в корневом каталоге диска, то черта появится (например, С:\). Это нужно, в частности, учитывать при формировании полного имени файла, то есть вместо:

strFullFileName = App.Path & strFileName

нужно применять, например, такую конструкцию:

strFullFileName = App.Path & IIf(Right$( _
     App.Path, 1) = "\", "", '\') & strFileName

Если вам лень каждый раз писать этот код, можете создать функцию AppPath:

Public Function AppPath() As String
  ' Замените App.Path на
  ' AppPath во всем тексте программы
  AppPath = App.Path & IIf(Right$(App.Path, 1) = "\", "", "\")
End Function

Обратите внимание, что если вы автоматически добавляете обратную косую черту ко всем вызовам App.Path, а путь окажется корневым каталогом, то вы получите совершенно неинформативное сообщение об ошибке: Run-time error '5': Invalid procedure call.

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