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

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

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

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


Совет 256. Программное обращение к CommonDialog

Если требуется выполнить выборку имени файла для операций открытия или закрытия, для этого обычно используется элемент управления CommonDialog. Однако в ряде случаев полезнее бывает выполнить прямое обращение к функциям GetOpenFileName и GetSaveFileName библиотеки COMDGL32.DLL. Например, если вы пишете собственный мастер по созданию проекта, то подключиться в проект с модулем кода гораздо удобнее, чем делать коррекцию кода модуля формы. Тем более что такая процедура будет единой для всего проекта.

В листинге 256 приведен один из вариантов реализации универсальной процедуры FileOpenSave поиска имени файла в режиме "Открыть/Сохранить", а также пример обращения к ней.

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

Листинг 256. Обращение к диалоговому окну "Открыть/Сохранить" через WinAPI

Private Declare Function GetOpenFileName Lib "comdlg32.dll" _
    Alias "GetOpenFileNameA" (ofn As OPENFILENAME) As Boolean
Private Declare Function GetSaveFileName Lib "comdlg32.dll" _
    Alias "GetSaveFileNameA" (ofn As OPENFILENAME) As Boolean
' Описатель данных для работы с окном "Открыть/Сохранить файл"
Private Type OPENFILENAME
    lStructSize As Long
    hwndOwner As Long
    hInstance As Long
    stFilter As String
    stCustomFilter As String
    nMaxCustFilter As Long
    nFilterIndex As Long
    strFile As String
    nMaxFile As Long
    stFileTitle As String
    nMaxFileTitle As Long
    stInitialDir As String
    strTitle As String
    Flags As Long
    nFileOffset As Integer
    nFileExtension As Integer
    stDefExt As String
    lCustData As Long
    lpfnHook As Long
    lpTemplateName As String
End Type
' Константы для управления флагами окна "Открыть/Сохранить файл"
Public Enum OFN_FLAGS
    OFN_READONLY = &H1
    OFN_OVERWRITEPROMPT = &H2
    OFN_HIDEREADONLY = &H4
    OFN_NOCHANGEDIR = &H8
    OFN_SHOWHELP = &H10
    OFN_ENABLEHOOK = &H20
    OFN_ENABLETEMPLATE = &H40
    OFN_ENABLETEMPLATEHANDLE = &H80
    OFN_NOVALIDATE = &H100
    OFN_ALLOWMULTISELECT = &H200
    OFN_EXTENSIONDIFFERENT = &H400
    OFN_PATHMUSTEXIST = &H800
    OFN_FILEMUSTEXIST = &H1000
    OFN_CREATEPROMPT = &H2000
    OFN_SHAREAWARE = &H4000
    OFN_NOREADONLYRETURN = &H8000
    OFN_NOTESTFILECREATE = &H10000
    OFN_NONETWORKBUTTON = &H20000
    OFN_NOLONGNAMES = &H40000
    OFN_EXPLORER = &H80000
    OFN_NODEREFERENCELINKS = &H100000
    OFN_LONGNAMES = &H200000
End Enum
Public Function FileOpenSave( _
  Optional ByVal OpenFile As Boolean = True, _
  Optional ByRef Flags As Long = 0&, _
  Optional ByVal InitialDir As Variant, _
  Optional ByVal Filter As String = vbNullString, _
  Optional ByVal FilterIndex As Long = 1, _
  Optional ByVal DefaultExt As String = vbNullString, _
  Optional ByVal FileName As String = vbNullString, _
  Optional ByVal DialogTitle As String = vbNullString, _
  Optional ByVal hwnd As Long = -1) _
  As String
  '
  ' Процедура обращения к
  ' диалоговому окну "Открыть/Закрыть файл"
  ' OpenFile = True    - ОТКРЫТЬ (по умолчанию)
  '                    = False - ЗАКРЫТЬ
  '
  Dim ofn As OPENFILENAME    ' структура для обращения
                  ' к DLL-функции
  Dim stFileName As String
  Dim stFileTitle As String
  Dim fResult As Boolean
  ' начальный каталог
  If IsMissing(InitialDir) Then InitialDir = CurDir
  If (hwnd = -1) Then hwnd = 0    ' установка описателя

  ' Подготовка строковых переменных
  stFileName = Left$(FileName & String$(256, vbNullChar), 256)
  stFileTitle = String$(256, vbNullChar)

  ' формирование данных для обращения к окну
  With ofn
    lStructSize = Len(ofn)
    hwndOwner = hwnd
    stFilter = Filter
    nFilterIndex = FilterIndex
    strFile = stFileName
    nMaxFile = Len(stFileName)
    stFileTitle = stFileTitle
    nMaxFileTitle = Len(stFileTitle)
    strTitle = DialogTitle
    Flags = Flags
    stDefExt = DefaultExt
    stInitialDir = InitialDir
    hInstance = 0
    stCustomFilter = String$(255, vbNullChar)
    nMaxCustFilter = 255
    lpfnHook = 0
  End With
  If OpenFile Then    ' открыть файл
    fResult = GetOpenFileName(ofn)
  Else    'сохранить файл
    fResult = GetSaveFileName(ofn)
  End If
  If fResult Then    ' Нажата кнопка "Open/Save"
    Flags = ofn.Flags    ' флаги
    ' имя файла
    FileOpenSave = Left$(ofn.strFile, _
      InStr(ofn.strFile, vbNullChar) - 1)
  Else: FileOpenSave = "" ' нажата Cancel
  End If
End Function

Public Sub Main()
    ' Тестирование конструкции FileOpenSave:
    ' обращение к окнам "Open/Save File" напрямую через DLL
    '=============================
    
    Dim bOpenFile As Boolean
       ' тип операции — TRUE (открыть)/FALSE (закрыть)
    Dim Filter$, Flags&
    Dim FileName$, InitDir$, Title$
    
    InitDir$ = App.Path
    Title$ = "Как выбрать имя каталога?"
    tab]Filter$ = "Текстовые файлы (*.txt)" & _
    Chr$(0) & "*.TXT" & Chr$(0) & _
        "Все файлы (*.*)" & Chr$(0) & "*.*" & Chr$(0)
    bOpenFile = True
    FileName$ = FileOpenSave( _
    bOpenFile, , InitDir, Filter, , ".txt", , Title$)
    MsgBox FileName$
End Sub

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

Совет 257. Как узнать, существует ли файл?

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

If Dir$(FileName$)="" Then ' такого файла нет!

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

Совет 258. Как выбрать имя каталога

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

К сожалению, диалоговое окно Open (или DLL-функция GetOpenFileName) позволяет выбирать только имя файла. Конечно, можно из этого имени по специальной команде пользователя сделать выделение каталога. А что делать, если нужный каталог содержит только подкаталоги?

Разумеется, лучшим вариантом является создание собственного диалогового окна, которое будет учитывать все режимы, необходимые в конкретной задаче. Но можно использовать и стандартное окно Open (рис. 258):

Рис. 258

Для этого в поле File Name нужно просто ввести любое имя несуществующего в данном каталоге файла (например, QQQQ). Соответственно в программе, которая обращается к окну (например, с использованием функции FileOpenSave, о чем говорилось в предыдущем совете), напишется такой программный код:

MyFile$ = FileOpenSave$(...)
If Dir$(MyFile$) = "" Then ' задан несуществующий файл
    ' значит задано имя каталога
    In1% = InstrRev (MyFile$, "\")
    MyDir$ = Left$ (MyFile$, in1% - 1) ' имя каталога
    ' обработка данных в режиме "выделен каталог"
End If

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

Совет 259. Какие операторы лучше использовать: Print # или Write #?

В своем совете 228 мы привели пример утилиты Summa.vbp (представление числового значения прописью), которая, в частности, сохраняет введенные параметры (для последующего восстановления) в виде простого текстового файла. Данные последовательно записываются оператором Print #, а при запуске утилиты читаются оператором Input #.

В связи с этим наш читатель Константин Абакумов обратил внимание на то, что в справке Visual Basic подчеркивается следующее: "Для записи данных в файл, который в будущем планируется читать с помощью инструкции Input #, следует вместо инструкции Print # использовать Write #. Это гарантирует, что записанные данные будут корректно разделены и могут быть корректно прочитаны при наличии любых национальных настроек".

Это замечание совершенно справедливо (хотя конкретно в случае утилиты Summa — не существенно), однако порой Print # является предпочтительнее. Рассмотрим эту ситуацию подробнее.

  1. Преимущества Write #

    Предположим, вы хотите сохранить четыре переменные — дату, две символьные строки и число:

    Dim MyDate As Date, MyStr1$, MyStr2$, MyInt%
    MyDate = Date$ ' текущая дата
    MyStr1 = "Коля"
    MyStr2 = "Петя, Лена, 2000"
    MyInt = 12
    Open MyFile$ For Output As #1
        Print #1, MyDate
        Print #1, MyStr1; MyStr2; MyInt
    Close
    Open MyFile$ For Output As #1
    Input #1, MyDate ' ! здесь будет выдана ошибка!
    Input #1, MyStr1; MyStr2; MyInt
    Close
    

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

    Input #1, MyDate2$ ' чтение строки
    MyDate = MyDate2$  ' и преобразование ее в дату
    

    Однако здесь также имеется потенциальная опасность — нужно быть уверенным, что данные читаются и записываются в Windows с одинаковыми региональными установками. (Представьте себе, что вам нужно переслать файл с расчетными данными в виде текстового файла своему коллеге в США.) Еще больше проблем возникнет с конструкцией:

    Print #1, MyStr1; MyStr2; MyInt
    ...
    Input #1, MyStr1; MyStr2; MyInt
    В случае нашего примера вы получите следующие результаты при чтении:
    MySrt1 = "КоляПетя"
    MySrt2 = "Лена"
    MyInt = 2000
    
    Видно, что они отличаются от исходных значений. Если же использовать в операторе Print # в качестве разделителя в списке запятую, то получится следующий результат:

    MySrt1 = "Коля Петя"
    MySrt2 = "Лена"
    MyInt = 2000
    

    Если мы заменим оператор Print # на Write #, то все данные будут сохраняться и читаться верно, поскольку дата будет записываться в виде литерала #2000-02-26# (обратите внимание, что литералы даты хранятся в американском формате, независимо от региональных установок), строковые переменные — в двойных кавычках и все поля в текстовом файле будут разделены запятыми.

    Но здесь есть очевидная проблема: если ваши строковые переменные содержат двойные кавычки, то будут происходить неприятные искажения данных. Например, вы ввели в текстовое поле название ЗАВОД "САЛЮТ" и хотите сохранить его значение в текстовом файле. Чтобы гарантированно избежать подобной ситуации, нужно проверять строки на наличие двойных кавычек и, в частности, автоматически их удалять (можно поставить соответствующий контроль при вводе данных) или менять на одинарные кавычки.

  2. Преимущества Input #

    Предположим, вам нужно сохранить в виде текстового файла числовой массив с переменными границами Arr (M, N). Конечно, можно воспользоваться оператором Write #:

    For i = 1 To M
      For j = 1 To N
        Write #1, Arr(i, j)
      Next
    Next
    

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

    For i = 1 To M
      For j = 1 To N
        Print #1, Arr(i, j); ' запись в одну строку
      Next
      Print #1 ' перевод строки
    Next
    

    Таким образом, оператор Print # очень полезен в случае, когда нужно вывести в одну строку текстового файла набор данных переменной длины или просто большое число однородных данных.

В свое время мы именно в силу необходимости получения структурируемого текстового файла (хорошо читаемого в текстовом редакторе) полностью отказались от использования оператора Write #. Для беспроблемного применения Print # мы создали очень простой набор процедур, который имитировал Write — заключал в кавычки строки, писал даты в виде литералов, расставлял запятые в качестве разделителей.

Какой вариант удобнее? Это, конечно же, зависит от конкретной решаемой задачи.

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

Совет 260. Где хранить INI-файл

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

Поэтому мы советуем создавать для отдельных приложений собственные INI-файлы. И хранить их лучше всего в одном каталоге с самим приложением — тогда вам не придется писать инструкции пользователям "Создайте каталог IniFolder для хранения файла MyApplication.INI". Более того, может оказаться удобным предоставить возможность использования нескольких вариантов INI-файлов (например, при работе нескольких пользователей). В этом случае вызов утилиты (через ярлык) может выполняться командной строкой:

MyUtility [User.INI]

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

Public Sub Main()
    Dim IniFile$, Say$
    '
    ' Запуск приложения
    ' SomeUtil.EXE [MyIniFile]
    '=====================
    ' открытие INI-файла приложения:
    IniFile$ = Command$ ' читаем командную строку
    
    If IniFile$ = "" Then ' не задан в командной строке
    ' по умолчанию: имя самой утилиты с расширением INI
        IniFile$ = App.EXEName + ".INI"
    End If
    ' формируем полный путь - там же, где находится приложение
    IniFile$ = App.Path + "\" + IniFile$
    
    Say$ = "INI-файл = " & IniFile$
    If Dir(IniFile$) <> "" Then
        MsgBox Say$, , "СУЩЕСТВУЕТ"
        ' открытие INI-файла, чтение исходных данных
    Else
        MsgBox Say$, , "НЕ СУЩЕСТВУЕТ"
        ' Тут можно спросить, нужно ли запускать приложение
        ' с параметрами по умолчанию и создавать новый INI-файл
    End If
End Sub

При отладке приложений в среде командная строка задается в поле Command Line Arguments в диалоговом окне Project Properties|Make.

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