Visual2000 · Архив статей А.Колесова & О.Павловой
Андрей Колесов, Ольга Павлова
© Андрей Колесов, Ольга Павлова, 2000Наш Совет 259 (об использовании операторов Print # и Write #) был основан на замечании, которое прислал наш читатель Константин Абакумов по поводу конструкций, использованных нами в коде утилиты Summa.vbp (сумма прописью, Cовет 228). Кроме того, он обнаружил еще одну "дырочку" в этой программе: оказалось, что следующая конструкция у него давала неверный результат:
Dim InVal ... Input #1, InVal ' здесь у Константина не срабатывала If InVal = True Then Call ... ' проверка при вводе значения True ...
Константин предложил заменить последнюю строку на такой работоспособный вариант:
If InVal Then Call ... ' при вводе значения True
Совершенно очевидно, что оба варианта в принципе тождественны. Не говоря уже о том, что у нас исходный вариант надежно работал. В чем же дело?
Проблема заключалась в том, что мы работали в среде VB5 (мы сознательно используем эту версию, если нет нужды в применении новшеств VB6), а наш читатель — в VB6. Судя по всему, в VB6 имеется ошибка, в результате чего внешне правильная конструкция оказалась неработоспособной.
Казалось бы, здесь можно поставить точку: обнаружена ошибка — избегайте ее повторения. Но на самом деле за этим частным случаем видится весьма принципиальная проблема.
Мы считаем, что VB (как и многие другие современные системы программирования) обладает серьезным технологическим недостатком, который заключается в широком использовании неявного преобразования типов данных. С точки зрения классических принципов программирования конструкция типа:
srtVal$ = IntVal%
является просто недопустимой — преобразование типов должно выполняться только с помощью специальных функций в явном виде, например так:
SrtVal$ = Str$(IntVal%)
И это при том, что ключевой идеей программирования всегда была необходимость четкого контроля за типами данных. Говоря упрощенно, проблема заключается в том, что при смешении типов переменных часто возникает двусмысленность в порядке преобразования данных и поэтому может получаться довольно неожиданный вариант.
Строго говоря, неявное преобразование данных недопустимо даже в арифметических операциях (из целых в вещественные и наоборот). Например, результат такого выражения:
sVal! = 1/3*6
в зависимости от желания разработчика компилятора может быть равным и 0 и 2. И в обоих случаях разработчик будет прав, так как нет жестких правил порядка преобразования данных.
Что же касается конструкции (вполне допустимой в VB) типа
strTrue$ = "True" If strTrue$=True Then ...,
то она вообще представляется кошмарной.
Логическим выводом из вышесказанного является то, что появление некоторого универсального типа данных, которым в VB является Variant, является грубым нарушением классических принципов программирования. Желание помочь программистам ("вам не нужно думать о форме представления данных") на самом деле усложнило разработку программ.
Пример тому — приведенная выше ситуация, когда VB сам запутался в том, как нужно интерпретировать переменную InVar. Кто-то отнесет это к невнимательности разработчиков Microsoft, но на самом деле появление этой ошибки было предопределено стратегией на смешение типов данных.
Каемся: мы были не правы, что использовали в утилите Summa общую переменную InVar (по умолчанию — Variart) для обработки ввода нескольких параметров разных типов данных. Мы исправили код, который теперь выглядит так:
Dim InBool As Boolean, InText$, InVal% Input #1, InBool: If InBool Then Call LabelSet(1) Input #1, InText: txtW1.Text = InText
ИЗБЕГАЙТЕ использования переменных типа Variant! Применяйте фиксированные типы переменных и функции явного преобразования типов данных. Если у вас есть доводы против этого тезиса — присылайте нам свои соображения.
Хотя элемент управления Picture позволяет отображать на экране графические изображения, он выводит только первый рисунок из анимационного GIF-файла. Чтобы полностью показать такой файл, не создавая заново всю последовательность изображений, можно воспользоваться элементом управления WebBrowser (который, правда, недоступен на компьютерах, где отсутствует Internet Explorer 3.0 или выше). Для этого установите компонент Microsoft Internet Controls (команда Project|Components), и тогда на панели инструментов Visual Basic появится элемент управления WebBrowser. Поместите его на форму, а затем в событии Form_Load() введите следующий код:
WebBrowser1.Navigate "filespec"
где filespec — полное имя GIF-файла (включая путь к нему) или URL-адрес. Запустите программу на выполнение, и Visual Basic выведет указанный файл.
К сожалению, WebBrowser одновременно выведет вертикальную линейку прокрутки на правой стороне формы, что не вполне сочетается с графическим изображением. Чтобы отключить ее, как ни удивительно, можно применить тот же самый метод, что обычно реализуется в HTML-коде:
WebBrowser1.Navigate @about:<html>" & _ "<body scroll='no'><img src='filespec'>" & _ </img></body></html>"
Теперь, когда вы запустите программу, Visual Basic выведет графическое изображение без линейки прокрутки.
В некоторых случаях бывает необходимо управлять Реестром Windows в VB, не прибегая к использованию функций API. Для этого существует библиотека Registry Access Functions (RegObj.dll), позволяющая создавать объект Registry, при помощи которого можно управлять конкретными ключами. Это может пригодиться, например, в следующем случае.
В предыдущем Совете мы продемонстрировали использование элемента управления WebBrowser. Однако, как мы уже отмечали, WebBrowser доступен только в том случае, если на машине-приемнике также установлен Internet Explorer. Поэтому, прежде чем приступить к выводу анимации, было бы целесообразно определить, имеется там IE или нет. Для этого вначале установите в своем проекте ссылку к библиотеке Registry Access Functions (команда Project|References) и введите код, аналогичный тому, что приводится здесь:
Dim myReg As New Registry, KeyFound As Boolean Dim HasIE As Boolean, sValue As String sValue = "" HasIE = False KeyFound = myReg.GetKeyValue(HKEY_LOCAL_MACHINE, _ "Software\Microsoft\Windows\CurrentVersion\" & _ "App Paths\IEXPLORE.EXE", "Path", sValue) If KeyFound Then HasIE = (sValue <> "") 'выводит полное название каталога, 'где установлен IE If HasIE Then MsgBox sValue Set myReg = Nothing
Для проверки правописания в RTF-документах можно было бы прибегнуть к следующему способу: внедрить Word в свое приложение (этот вариант приведен ниже, в Совете 282). Однако мы рекомендуем воспользоваться элементом управления WebBrowser, о котором шла речь в двух предыдущих Советах и с помощью которого можно организовать такую проверку. Для этого вам понадобится метод ExecWB() с флагом OLECMDID_SPELL. В качестве иллюстрации поместите WebBrowser на форму и введите такой код в событие Form_Load():
WebBrowser1.Navigate "filespec"
Замените filespec любой строкой, которая указывает путь к RTF-файлу. Затем добавьте к форме командную кнопку, для которой в событии Click напишите следующий код:
WebBrowser1.ExecWB OLECMDID_SPELL, OLECMDEXECOPT_DODEFAULT
Запустите проект на выполнение. Элемент управления WebBrowser выведет указанный RTF-документ, и, когда вы щелкнете командную кнопку, VB активизирует установленную на вашем компьютере программу проверки правописания.
Каждый объект Command в новом конструкторе Data Environment в VB 6.0 имеет соответствующий ему набор записей. Для того чтобы в коде программы сослаться на этот объект, необходимо добавить к его имени префикс rs. Так, если конструктор Data Environment содержит объект Command с именем cmdSomeQuery, то для ссылки к набору записей этого объекта напишите примерно такую строку кода:
Set rst = DataEnvironment1.rscmdSomeQuery
Использование функции PathCompactPath из библиотеки SHLWAPI — один из способов сжатия длинных имен файлов, благодаря которому часть имени слева заменяется на многоточие (...). Для работы с данной API-функцией требуется описать ее следующим образом:
Private Declare Function _ PathCompactPath Lib "shlwapi"_ Alias "PathCompactPathA" _ (ByVal hDC As Long, ByVal _ lpszPath As String, _ ByVal dx As Long) As Long
Как видно из этого описания, функция PathCompactPath содержит три аргумента: первый из них задает дескриптор контекста устройства (device context handle), второй — имя файла, которое будет сжиматься, и, наконец, последний — ширину в пикселах того компонента на форме, куда мы хотим поместить это имя. Так, чтобы вывести компактное имя файла в метке с именем lblEllipsis, напишем следующий код для события Click() командной кнопки:
Private Sub Command1_Click() Dim lhDC As Long, lCtlWidth As Long Dim FileSpec As String FileSpec = "C:\MyFolder\VisualBasic\" & _ "MyReallyWayTooLongFolderName\ButWhoCares\IhaveTheAPI.doc" Me.ScaleMode = vbPixels lCtlWidth = lblEllipsis.Width - Me.DrawWidth lhDC = Me.hDC PathCompactPath lhDC, FileSpec, lCtlWidth lblEllipsis.Caption = FileSpec End Sub
Обычное правило чтения выделенных элементов окна списка, поддерживающего выбор нескольких позиций (иными словами, того, у которого свойство MultiSelect отлично от 0), гласит, что необходимо организовать в цикле просмотр всех элементов и проверить у них значение свойства Selected. Однако такой подход (как и при работе с любыми циклами) может значительно снизить быстродействие создаваемого вами приложения, особенно на менее скоростных процессорах. В качестве более быстрой и элегантной альтернативыпредлагаем вам воспользоваться API-функцией SendMessage().
Как известно, с помощью данной функции можно отправить сообщение в одно или несколько окон. Ее описание выглядит следующим образом:
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
Поскольку мы хотим прочитать все выделенные элементы списка, необходимо отправить константу LB_GETSELITEMS в качестве аргумента wMsg и описать ее так:
Private Const LB_GETSELITEMS = &H191
По существу сообщение LB_GETSELITEMS заполняет массив значениями индексов всех выделенных элементов. В результате вместе с функцией SendMessage() необходимо передать два дополнительных аргумента. Первый из них должен содержать максимальное количество выделенных элементов, получить которое можно с помощью свойства SelCount элемента управления ListBox. Второй же аргумент должен хранить переменную массива, которую мы хотим заполнить значениями индексов.
Следующий пример демонстрирует, как можно использовать функцию SendMessage(). Поместите на форму элемент управления ListBox1, установите его свойство MultiSelect как 1-Simple и заполните список. Затем добавьте командную кнопку Command1 и напишите следующий код для ее события Click:
Dim ItemIndexes() As Long, x As Integer, iNumItems As Integer NumItems = ListBox1.SelCount If iNumItems Then ReDim ItemIndexes(iNumItems - 1) SendMessage ListBox1.hwnd, LB_GETSELITEMS, iNumItems, _ ItemIndexes(0) End If For x = 0 To iNumItems - 1 MsgBox ListBox1.List(ItemIndexes(x)) Next x
Обратите внимание, что переменная iNumItems, после того как она передается в функцию SendMessagen, хранит общее число выделенных элементов списка, а массив ItemIndexes — значения индексов выделенных элементов. При этом вы должны передать указатель к массиву ItemIndexes, а не сам массив, то есть в функцию SendMessage передается ItemIndexes(0), а не ItemIndexes().
Предположим, что на вашей форме есть два окна списка, связанных таким образом, что элементы второго списка зависят от того, какой элемент выбран в первом списке. Если вы будете решать подобную задачу с помощью стандартных массивов, то вам придется написать огромное количество кода. Мы же предлагаем использовать функции работы с массивами, что должно почти вдвое сократить объем кода. Кроме того, применение этих функций значительно упрощает добавление элементов списка, поскольку отпадает необходимость менять размерности массивов, а это, в свою очередь, снижает вероятность появления ошибок. Например, чтобы добавить элемент "Конфеты" к первому списку, просто вставьте в основной массив подмассив следующего вида:
Array(3, "Конфеты", "Мишка на Севере", "Белочка", "Красная Шапочка")
В предлагаемом нами коде каждый элемент первого списка имеет один подмассив, а элементы каждого подмассива определяются следующим образом: сначала пишется количество элементов второго списка, затем имя элемента первого списка, для которого и создается данный подмассив, далее идут элементы второго списка.
Чтобы протестировать приведенный ниже код, создайте проект Standard EXE, а потом поместите на форму два окна списка, оставив без изменения их имена List1 и List2:
Private varArray As Variant Private varSubArray As Variant Private Sub Form_Initialize() varArray = Array(Array(4, "Фрукты", 'Яблоки", _ "Апельсины", "Персики", "Груши", Array(5, _ "Овощи", "Горох", "Бобы", "Кукуруза", "Свекла", _ "Лук"), Array(3, "Ежедневные продукты", _ "Молоко", "Сметана", "Масло")) End Sub Private Sub Form_Load() Dim intIndex1 As Integer With List1 For intIndex1 = 0 To UBound(varArray) varSubArray = varArray(intIndex1) .AddItem varSubArray(1) Next intIndex1 .ListIndex = 0 End With End Sub Private Sub List1_Click() Dim intIndex2 As Integer With List2 varSubArray = varArray(List1.ListIndex) .Clear For intIndex2 = 0 To varSubArray(0) - 1 .AddItem varSubArray(intIndex2 + 2) Next intIndex2 .ListIndex = 0 .Refresh End With End Sub
Интерфейс MCI (Media Control Interface — Интерфейс управления средой) позволяет поддерживать несколько устройств для чтения аудиокомпакт-дисков. Для этого следует просто указать имя дисковода в команде MCI Open. Вначале поместите окно списка List1 на форму, а затем, чтобы определить, какие дисководы предназначены для чтения компакт-дисков, напишите в разделе General Declarations формы такой код:
Private Declare Function GetDriveType Lib _ "kernel32" Alias "GetDriveTypeA" (ByVal _ nDrive As String) As Long Private Declare Function mciSendString Lib _ "winmm.dll" Alias "mciSendStringA" (ByVal _ lpstrCommand As String, ByVal lpstrReturnString _ As String, ByVal uReturnLength As Long, ByVal _ hWndCallback As Long) As Long Private Const DRIVE_CDROM = 5
Подпрограмма Form_Load заполняет окно списка обнаруженными на компьютере именами дисководов CD-ROM:
Sub Form_Load() Dim k As Long For k = Asc("A") To Asc("Z") If GetDriveType(Chr$(k) & ":") = DRIVE_CDROM Then List1.AddItem Chr$(k) & ":" End If Next End Sub
А чтобы действительно открыть дверцу дисковода, куда можно будет вставить компакт-диск, введите такой код в событие List1_dblClick:
Private Sub List1_DblClick() mciSendString "open"; & _ List1.List(List1.ListIndex) & _ "type cdaudio alias cdaudi", _ vbNullString, 0, 0 mciSendString "set cdaudio door open", _ vbNullString, 0, 0 mciSendString "close cdaudio", _ vbNullString, 0, 0 End Sub
Предположим, вы хотите выполнить какое-нибудь действие при изменении любых свойств шрифта конкретного элемента управления или формы. Для этого вам понадобится ключевое слово WithEvents. (Не забудьте установить ссылку OLE Automation в диалоговом окне References с помощью команды Project|References):
Private WithEvents fntAny As StdFont
Затем напишите подпрограмму, которая будет реагировать на изменения свойств шрифта:
Private Sub fntAny_FontChanged(ByVal _ PropertyName As String) Select Case PropertyName Case "Name" 'Какое-либо действие Case "Size" 'Какое-либо действие Case "Italic" 'Какое-либо действие Case "Bold" 'Какое-либо действие Case "Underline" 'Какое-либо действие ' ... ' Подобным образом можно расширить ' функциональные возможности для ' каждого свойства шрифта End Select End Sub
Теперь осталось только установить свойство Font формы или элемента управления как fntAny. Так, если вы хотите отслеживать изменения свойств шрифта формы, напишите следующий код в событии Form_Load:
Set fntAny = Me.Font ' Если вы имеете дело с элементом управления, ' тогда Control.Font
При разработке крупного VB-приложения, использующего сотни COM-объектов, вы можете в какой-то момент получить сообщение об ошибке такого вида: "429 can't create object". К сожалению, эта информация не очень-то поможет вам определить, какой именно объект не может быть создан. Чтобы преодолеть подобное ограничение, можно написать функцию на базе функции CreateObject из библиотеки Visual Basic runtime objects and procedures:
Public Function CreateObject(sProgID As String) As Object ' On Error GoTo CreateErr ' Вызов функции CreateObject из библиотеки ' VB runtime objects and procedures Set CreateObject = VBA.CreateObject(sProgID) Exit Function CreateErr: ' возвращает сообщение об ошибке вместе ' с именем объекта, который не может ' быть создан Err.Raise Err.Number, "CreateObject Wrapper", _ Err.Description & ": '" & sProgID & "'" End Function
VB позволяет осуществить интеграцию возможностей проверки правописания, имеющихся в Microsoft Word 97/2000, в VB-приложения, сохраняя при этом параметры форматирования внутри элемента управления RichTextBox. Продемонстрируем это на таком примере:
Private Sub Command1_Click() On Error GoTo SpellCheckErr Dim oWord As Object Set oWord = CreateObject("Word.Application") ' ' сохраняет содержимое компонента RichTextBox 'во временном файле rtfText.SaveFile "c:\Vb-db\Test.RTF" ' ' открывает сохраненный файл и проводит ' в нем проверку правописания oWord.Documents.Open ("c:\Vb-db\Test.RTF") oWord.ActiveDocument.SpellingChecked = False oWord.Options.IgnoreUppercase = False oWord.ActiveDocument.CheckSpelling ' ' сохраняет изменения в RTF-файле ' и закрывает его oWord.ActiveDocument.Save oWord.ActiveDocument.Close oWord.Quit ' ' загружает изменения в элемент ' управления RichTextBox rtfText.LoadFile "c:\Vb-db\Test.RTF" Set oWord = Nothing Screen.MousePointer = vbDefault MsgBox "Проверка правописания закончена", _ vbInformation, "Правописание" Exit Sub ' SpellCheckErr: MsgBox Err.Description, vbCritical, "Правописание" Set oWord = Nothing End Sub
Вскоре после выпуска библиотеки ADO компания Microsoft создала для нее расширение, называемое ActiveX Data Objects Extensions, или просто ADOX, и включающее большую часть функций DAO, которые не вошли в состав стандартной версии ADO. С помощью библиотеки ADOX можно легко определить, входит ли конкретная таблица, разрез данных или запрос в базу данных.
Для этого установите ссылку к библиотеке Microsoft ADO Ext. for DDL and Security. В упрощенном виде ADOX состоит из объекта Catalog, который включает коллекции объектов, описывающих базу данных и содержащиеся в ней таблицы и разрезы данных. Чтобы проверить наличие конкретной таблицы в коллекции Tables, можно пройти шаг за шагом по всей коллекции и сравнить имя каждого элемента с заданной строкой либо воспользоваться предлагаемым ниже способом:
Private Sub Command1_Click() Dim con As New ADODB.Connection Dim cat As New ADOX.Catalog Dim tbl As ADOX.Table Dim tblName As String con.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _ "Data Source=c:\Vb-db\Biblio.mdb;" Set cat.ActiveConnection = con On Error Resume Next tblName = "Titles" Set tbl = cat.Tables(tblName) If tbl Is Nothing Then MsgBox "Таблица " & tblName & " не существует" Else MsgBox "Таблица " & tblName & " существует" Set tbl = Nothing End If Set cat = Nothing Set con = Nothing End Sub
В нашем Совете 264 мы рассказывали о том, как программным образом имитировать нажатие клавиши с помощью обращения к соответствующей API-функции. Оказывается (каемся — не знали), что в VB можно использовать для этих целей встроенный оператор SendKeys. Например, посылка кода Enter выглядит следующим образом:
SendKeys "{Enter}"
Такая программная имитация нажатия клавиши может быть полезна при создании макрокоманд в MS Office. Ведь некоторые команды в ходе их выполнения выдают диагностические сообщения с требованием подтвердить выполнение операции или выбрать нужный вариант ее реализации. Использование SendKeys позволяет автоматически выполнить подобные "нажатия" кнопок.