Visual Basic - Step by Step
Диалоговые
панели
Microsoft Common Dialog Control.
Очевидно, что программа должна как-то открывать и сохранять файлы, а так же изменять параметры принтера, шрифт, цвет фона и текста. К счастью всю эту функциональность в одном флаконе предоставляет элемент управления Common Dialog. Но ряд не больших проблем всё же остаётся.
Во первых, Common Dialog - действительно элемент управления, а нам это как раз и не нужно. Он должен находится в каком ни будь контейнере, например в форме, а часто бывает необходимо использовать его в программе без форм. Но мои основные претензии в том, что его архитектура... Как бы выразится по мягче... Отсутствует напрочь. Его разработали в те далёкие времена, когда элементы управления были единственным средством, обеспечивающим повторное использование, и его интерфейс представляет собой мешанину свойств. Сейчас компоненты которым не нужны визуальный интерфейс и события, можно поставлять в гораздо более удобной форме открытых классов.
Опять же у счастью, Visual Basic включает в себя не визуальный компонент Microsoft Dialog Automation Objects (DLGOBJS.DLL). Забудьте об элементах управления. Теперь можно создавать диалоговые объекты и вызывать их методы и свойства без всяких форм. Я был приятно удивлен обнаружив этот компонент в каталоге \Tool\Unsupprt на установочном диске Visual Basic. Им намного проще пользоваться чем элементом управления Common Dialog. Но увы, когда говорят "Не поддерживается", значит так оно и есть. Я нашел у него одно серьёзное ограничение, которое смог обойти, и несколько небольших которые обойти не смог. Надеюсь, что в следующей версии Visual Basic диалоговые объекты станут "поддерживаться" и будут интегрированы в библиотеку самого языка.
А тем временем... элемент управления. Стоп!!! Какой элемент управления?! Не нужен нам этот ужасный Common Dialog!
Реализация стандартных диалоговых окон: Windows плюс Basic.
Сравним несколько подходов к стандартным диалоговым окнам. Начнём с одного из самых простых - предназначенного для выбора цвета - и рассмотрим способы вывода его на экран.
Первый - традиционный приём с использованием Common Dialog. Здесь предполагается, что элемент управления расположен на Form, UserControl или PropertyPage:
Function
OptionColor(Optional ByVal clr As Long = vbBlack) As Long
With dlgColor
' в VB нет
константы CC_SOLIDCOLOR, но всё работает
.Flags = cdlCCRGBInit Or CC_SOLIDCOLOR
' Убедимся, что
это RGB формат
.Color = TranslateColor(clr)
.hWnd = hWnd
' Узнаем об отмене,
перехватывая ошибку
.CancelError = True
On Error Resume Next
.ShowColor
' Возвращаем цвет,
всё ли прошло удачно
If Err Then
OptionColor = clr
Else
OptionColor = .Color
End If
End With
End Function
Этот код не без шероховатостей. Управление поведением требует записи в свойство Flags определённой константы, а VB представляет н все константы. Хорошо когда знаешь что этот элемент управления лишь очень тонкая оболочка API - функций и распознаёт любые константы, какие только упомянуты в документации Windows API. Обработка кнопки Cancel организована просто отвратительно, но раньше было ещё хуже. Кто работал с более ранними версиями знает, о чём я говорю, а кто не работал, тому и знать не зачем.
Версия с Dialog Automation Objects выглядит гораздо лучше:
Function
OptionColor(Optional ByVal clr As Long = vbBlack) As Long
Dim choose As New ChooseColor
With choose
' Убедимся,
что это RGB формат.
.Color =
TranslateColor(clr)
.hWnd = hWnd
' Нет
свойства позволяющие показать лишь
основные цвета.
' Возвращаем цвет, всё ли прошло
удачно.
If .Show Then
OptionColor = choose.Color
Else
OptionColor = clr
End If
End With
End Function
Характеристики окна определяются свойствами. Перед выводом окна на экран вы их устанавливаете, а за тем считываете. Только вот разработчики не дали нам одного важного свойства : для показа лишь основных цветов (без составных). RichTextBox не понимает составных цветов ни для фона ни для текста - зачем же их показывать? Понятно, что разработчики хотели упростить работу большинству пользователей, а если их выбор не совпал с моим или Вашим, то нам просто не повезло.
И наконец, моя версия:
Function
OptionColor(Optional ByVal clr As Long = vbBlack) As Long
' Убедимся,
что это RGB формат.
clr =
TranslateColor(clr)
' Возвращаем
только основные цвета
Call
VBChooseColor(Color:=clr, AnyColor:=False, Owner:=hWnd)
' Возвращаем
цвет, всё ли прошло удачно.
OptionColor = clr
End Function
В место свойств я применил именованные аргументы. Поскольку разработчик я, то я позаботился обо всех нужных мне параметрах, но Вам, возможно, понадобятся и другие. В этом примере мой код короче, но так бывает не всегда. Я, конечно, предпочёл бы более структурированный интерфейс доступа к диалоговым объектом, и, как только в Dialog Automation Objects появится всё, что мне надо, тут же выброшу свою версию.
Есть еще один способ вывода стандартного диалогового окна: разработать свой собственный вариант, но об этом в другой раз.
Использование стандартных диалоговых окон.
Если вы видели хоть одну функцию стандартного диалогового окна, значит, Вы видели все. Поэтому я расскажу только о создании самой сложной и самой нужной из них - функции, открывающей диалоговое окно Open. Диалоговое окно Save As ему практически идентично, Font и Color сравнительно просты, а Print Setup и Page Setup, хоть они и несколько отличаются я не стану разбирать в деталях.
Вызываем функцию VBGetOpenFileName.
Реализация оболочки - VBGetOpenFileName.
И наконец, о самом трудном - о реализации оболочки для GetOpenFileName. Фокус в том, чтобы заполнить поля UDT (User Defended Type),ожидаемый GetOpenFileName, замаскировав этот процесс именованными аргументами. Все закрытые типы и объявления, используемые в открытых функциях VBGetOpenFileName, VBGetSaveFileName, VBChooseFont, VBPrintDlg и VBPageSetupDlg находятся в модуле COMDLG.
Структура OPENFILENAME
Private Type OPENFILENAME
lStructSize As Long
' Размер UDT
hwndOwner As Long
' Связанно с Owner
hInstance As Long
'
Игнорируется (используется только для
шаблонов)
lpstrFilter As String
' Связанно с Filter
lpstrCustomFilter As String '
Игнорируется
nMaxCustFilter As Long '
Игнорируется
nFilterIndex As Long
' Связанно с FilterIndex
lpstrFile As String
' Связанно с FileName
nMaxFile As Long
' Для
внутренних целей
lpstrFileTitle As String '
Связанно с FileTitle
nMaxFileTitle As Long
' Для
внутренних целей
lpstrInitialDir As String '
Связанно с InitDir
lpstrTitle As String
' Связанно с DlgTitle
Flags As Long
' Связанно с Flags
nFileOffset As Integer '
Игнорируется
nFileExtension As Integer '
Игнорируется
lpstrDefExt As String
' Связанно с DefaultExt
lCustData As Long
'
Игнорируется (требуется для ловушек)
lpfnHook As Long
'
Игнорируется (требуется для ловушек)
lpTemplateName As String '
Игнорируется (используется только для
шаблонов)
End Type
Это громоздкий UDT, но не все его поля нужны. Некоторые можно игнорировать, так как это просто выкрутасы. Вам например совершенно не зачем получать здесь позицию начала имени файла и его расширение в полном пути, когда это необходимо то проще самому разбить путь или вызвать GetFullPathName. Обрабатываемых полей вполне достаточно в 98% случаев.
В прежних версиях применение ловушек (hooks) и шаблонов (templates) для настройки стандартных диалоговых окон было уделом богов. Но оператор AdddressOf сделал эту задачу посильной и нам, крутым смертным. Удачи!
Поля UDT служат и для входных, и для выходных данных. Перед вызовом инициализируются одни поля, после вызова считываются другие. Некоторые (к примеру Flags) работают в обе стороны. Наша функция может получить выходные данные через переменные передаваемые по ссылки. Конечно, переменные нужны не всегда, так как все аргументы, кроме одного, необязательные. Можно , например, опустить параметр Flags или передать его как константу. В последнем случае функция не заметит разницы и запишет результат во временную переменную, созданную Visual Basic. Ничего страшного не произойдёт, Вы просто не увидите результатов.
Обработка не обязательных параметров.
Не обязательные параметры обрабатывает первая часть функции VBGetOpenFileName, присваивая всем пропущенным значения по умолчанию:
Dim
opfile As OPENFILENAME, s As String, afFlags As Long
With opfile
.lStructSize = Len(opfile)
'
Задаём конкретные флаги и сбрасываем не
поддерживаемые в VB
.Flags = (-FileMustExist * OFN_FILEMUSTEXIST) Or _
(-MultiSelect * OFN_ALLOWMULTISELECT) Or _
(-ReadOnly * OFN_READONLY) Or _
(-HideReadOnly * OFN_HIDEREADONLY) Or _
(Flags And CLng(Not (OFN_ENABLEHOOK Or _
OFN_ENABLETEMPLATE)))
'
Владелиц может работать с окном
If
Owner <> -1 Then .hwndOwner = Owner
'
InitDir - Строка с именем начальной директории
.lpstrInitialDir = InitDir
'
DefaultExt - расширение по умолчанию
.lpstrDefExt = DefaultExt
'
DlgTitle - заголовок окна
.lpstrTitle = DlgTitle
:
Как видите параметров много, но лишь один обязательный - FileName, поскольку без него функция недееспособна.
Остальные параметры определяют входные и в нескольких случаях выходные данные. Так например HideReadOnly указывает, надо ли помещать в диалоговое окно флажок Open As Read-Only, а параметр ReadOnly задаёт начальное состояние этого флажка. На выходе из функции в параметр ReadOnly записывается значение, которое сообщает конечное состояние флажка Open As Read-Only. И вновь вы должны передать переменную. Если вы передадите константу, то поместить результат будет некуда.
Параметры FileMustExist, MultiSelect, ReadOnly и HideReadOnly представляют наиболее часто используемые флаги. Сейчас их свыше десятка, а в будущих версиях Windows к ним могут добавится новые. Параметр Flags позволяет обрабатывать любые интересующие вас флаги. Иными словами, моя реализация Вас ни чем не сковывает.
Обработка фильтров, имён файлов и флагов.
VBGetOpenFileName ожидает, что строки фильтров разделены вертикальной чертой ( | ) или двоеточием. Элемент управления Common Dialog допускает в качестве разделителя только первый символ, но я следую стилю компонента Dialog Automation Objects, где описание можно отделить от соответствующего ему расширения двоеточием:
Rich text files (*.rtf): *.rtf| .....
Работает и формат Common Dialog. Но ни один из этих форматов не устраивает Windows, которая ждёт в качестве разделителя нулевой символ и два нулевых символа в конце списка. Приходится самому преобразовывать строку:
' Чтобы создать фильтр в стиле Windows, заменяем | и : null-символами Dim ch As String, i As Long For i = 1 To Len(filter) ch = Mid$(filter, i, 1) If ch = "|" Or ch = ":" Then s = s & vbNullChar Else s = s & ch End If Next ' Помещаем в конец два null-символа s = s & vbNullChar & vbNullChar .lpstrFilter = s .nFilterIndex = FilterIndex
Создав новую строку фильтра, Вы просто записываете её в поле UDT-переменной.
Поля lpstrFile и lpstrFileTitle должны указывать на строковые буферы, размер которых достаточен для размещения в них любого возможного значения. Этот максимальный размер задаётся в полях nMaxFile и nMaxFileTitle. Вы записываете в них константы cMaxPath и cMaxFile (из библиотеки типов Windows API), а строковые буфера заполняете строками этой длинны. Это позволяет избежать неприятностей из-за слишком малого буфера. Полученные строки преобразуем в формат Visual Basic:
' Создаём буфер максимальной длинны s = FileName & String$(cMaxPath - Len(FileName), 0) .lpstrFile = s .nMaxFile = cMaxPath s = FileTitle & String$(cMaxFile - Len(FileTitle), 0) .lpstrFileTitle = s .nMaxFileTitle = cMaxFile ' Остальные поля приравниваем к нулю
Остальное пусть делает Windows.
К этому моменту мы уже поместили все необходимые входные данные в UDT-переменную и готовы вызвать API-функцию GetOpenFileName. Данные, которые диалоговое окно вернёт вам, будут представлены в формате Windows с нулевым символом на конце. Придётся слегка преобразовать их чтобы вернуть вызывающей программе в формате Visual Basic:
If GetOpenFileName(opfile) Then VBGetOpenFileName = True FileName = MUtility.StrZToStr(.lpstrFile) FileTitle = MUtility.StrZToStr(.lpstrFileTitle) Flags = .Flags ' Возвращаем индекс фильтра FilterIndex = .nFilterIndex ' Подставляем, выбранный пользователем в диалоговом окне filter = FilterLookup(.lpstrFilter, FilterIndex) If (.Flags And OFN_READONLY) Then ReadOnly = True Else VBGetOpenFileName = False FileName = sEmpty FileTitle = sEmpty Flags = 0 FilterIndex = -1 filter = sEmpty End If End With End Function
Строки в UDT-переменной - на самом деле буферы максимальной длинны. Windows запишет в буфер выходную строку, но не изменит размер буфера. Вы должны сами отсечь лишние символы и вернуть строку в ссылочную переменную, переданную пользователем. Если же пользователь такой переменной не передал, Visual Basic создаст временную переменную, но результат будет утерян.
Параметр Filter на выходе изменяется и возвращает выбранный фильтр, поэтому предавайте ему копию строки фильтра, а не оригинал.
VBGetOpenFileName служит моделью для других функций, написанных на Visual Basic и поддерживающие стандартные диалоговые окна. Все они находятся в модуле COMDLG.BAS.
Необходимые файлы:
Библиотека
типов Windows API
Comdlg.bas