Visual2000 · Архив статей А.Колесова & О.Павловой
Андрей Колесов, Ольга Павлова
© 2000, Андрей Колесов, Ольга ПавловаЕсли требуется выполнить выборку имени файла для операций открытия или закрытия, для этого обычно используется элемент управления CommonDialog. Однако в ряде случаев полезнее бывает выполнить прямое обращение к функциям GetOpenFileName и GetSaveFileName библиотеки COMDGL32.DLL. Например, если вы пишете собственный мастер по созданию проекта, то подключиться в проект с модулем кода гораздо удобнее, чем делать коррекцию кода модуля формы. Тем более что такая процедура будет единой для всего проекта.
В листинге 256 приведен один из вариантов реализации универсальной процедуры FileOpenSave поиска имени файла в режиме "Открыть/Сохранить", а также пример обращения к ней.
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
Проще всего сделать это следующей операцией в одну строчку:
If Dir$(FileName$)="" Then ' такого файла нет!
В разных вспомогательных утилитах довольно часто встречается задача выборки имени файла или имени каталога. Например, если вы пишете утилиту по перекодировке текстовых файлов, будет достаточно предоставить пользователю возможность выбрать или конкретный файл, или каталог, в котором нужно преобразовать все файлы.
К сожалению, диалоговое окно 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
В своем совете 228 мы привели пример утилиты Summa.vbp (представление числового значения прописью), которая, в частности, сохраняет введенные параметры (для последующего восстановления) в виде простого текстового файла. Данные последовательно записываются оператором Print #, а при запуске утилиты читаются оператором Input #.
В связи с этим наш читатель Константин Абакумов обратил внимание на то, что в справке Visual Basic подчеркивается следующее: "Для записи данных в файл, который в будущем планируется читать с помощью инструкции Input #, следует вместо инструкции Print # использовать Write #. Это гарантирует, что записанные данные будут корректно разделены и могут быть корректно прочитаны при наличии любых национальных настроек".
Это замечание совершенно справедливо (хотя конкретно в случае утилиты Summa — не существенно), однако порой Print # является предпочтительнее. Рассмотрим эту ситуацию подробнее.
Предположим, вы хотите сохранить четыре переменные — дату, две символьные строки и число:
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# (обратите внимание, что литералы даты хранятся в американском формате, независимо от региональных установок), строковые переменные — в двойных кавычках и все поля в текстовом файле будут разделены запятыми.
Но здесь есть очевидная проблема: если ваши строковые переменные содержат двойные кавычки, то будут происходить неприятные искажения данных. Например, вы ввели в текстовое поле название ЗАВОД "САЛЮТ" и хотите сохранить его значение в текстовом файле. Чтобы гарантированно избежать подобной ситуации, нужно проверять строки на наличие двойных кавычек и, в частности, автоматически их удалять (можно поставить соответствующий контроль при вводе данных) или менять на одинарные кавычки.
Предположим, вам нужно сохранить в виде текстового файла числовой массив с переменными границами 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 — заключал в кавычки строки, писал даты в виде литералов, расставлял запятые в качестве разделителей.
Какой вариант удобнее? Это, конечно же, зависит от конкретной решаемой задачи.
В данном случае мы имеем в виду файлы, в которых хранятся некоторые параметры приложения между его перезапусками. (В общем случае они могут иметь любое расширение и любую удобную для разработчика структуру.) Конечно, для хранения подобных файлов можно выделить специальный каталог, например системный 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.