Enhance Your Apps with Common Dialogs: Part I

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

VBA Hacker

This content is no longer actively maintained. It is provided as is, for anyone who may still be using these technologies, with no warranties or claims of accuracy with regard to the most recent product version or service release.

Mastering the Basics of the Windows Open and Save As Dialog Boxes

By Romke Soldaat

In this two-part series, we'll explore some of Microsoft Windows' great assets - its common dialog boxes - and put the emphasis on how to use them in your Office 2000 applications. (Sorry, the VBA code used in these articles and their associated source files contains many functions and features that aren't available in previous Office versions.)

Common dialog boxes were introduced in Windows 3.1 to help developers create applications with a consistent user interface. The most "common" common dialog box is the one that lets you open and save files.

For some strange reason, one of the software companies that often refuses to use these dialog boxes is Microsoft. Office 95, 97, and 2000 have consistently been inconsistent with the Windows interface guidelines (see FIGURE 1). As a matter of fact, it looks like it's the Office developers, and not the Windows team, that sets the trend. The Windows 2000 common dialog box (see FIGURE 1, bottom) has clearly "borrowed" the Places Bar, introduced with Office 2000, and the new Windows 2000 system font has become Tahoma, pioneered by the makers of Office 95. (Wasn't it Bill Gates who once claimed that there was a brick wall between the system and applications groups?)


FIGURE 1a: Evolution of the common dialog box: Windows 3.1.


FIGURE 1b: Evolution of the common dialog box: Windows 95.


FIGURE 1c: Evolution of the common dialog box: Windows 2000.

What Are Your Options?

If you write VBA applications that require a user interface to open or save a file, you have several options. The most obvious one is to use the Open or Save As dialog box of the host application. In Word VBA, for example, you can access the Dialogs collection and retrieve a file name with the following code:

Sub GetFileFromWord()
  With Dialogs(wdDialogFileOpen) 
    If .Display Then MsgBox .Name
  End With
End Sub

It's a simple and straightforward technique. The Display method (unlike the Show method) allows you to display the dialog box and obtain a file name without actually opening the file. Unfortunately, it's unreliable and therefore unusable. Just try and run the previous code and select more than one file in the dialog box. What you get is error 5174 (see FIGURE 2). Clearly, Word's Open dialog box can't cope with multiple file selections - and never could. This bug has been around since Word 95 because nobody in Redmond ever bothered fixing it.


FIGURE 2: Don't try to select more than one file in Word!

You're much better off in Excel 2000. You can't use the Dialog object (which only has a Show method, so it doesn't let you display the dialog box without actually opening a file), but you can use the GetOpenFilename or GetSaveFilename method instead. GetOpenFilename copes well with multiple file selections:

Sub GetFilesFromExcel()
  Filename = Application.GetOpenFilename(FileFilter:= _
    "All Files (*.*),*.*,Text Files (*.txt), *.txt", _
    FilterIndex:=1, Title:="Open a File", _
    MultiSelect:= True)
  For i = LBound(Filename) To UBound(Filename) 
    MsgBox Filename(i) 
  Next
End Sub

Alas, the creators of Word and PowerPoint seem to have decided that you don't want this functionality, so you can't use the GetOpenFilename method across Office applications. A missed opportunity!

An alternative technique is to create a reference in your project to the Common Dialog ActiveX Control (COMDLG32.OCX). Unfortunately, this file isn't automatically installed with all versions of Windows or Office. You need to get Visual Basic or Visual FoxPro (or a distribution license from Microsoft) if you want to use it. However, if you do have the file, and it's properly registered, it's easy to use. The Visual Basic documentation claims that you must add the CommonDialog control to a form before you can set its properties, but that's not true. The code in FIGURE 3 doesn't need a form at all and works just fine. (As a matter of fact, this code doesn't even require a project reference to COMDLG32.OCX because it creates its own MSComDlg.CommonDialog object.)

Sub GetFileFromOCX()
  Dim CDLG As Object
  Set CDLG = CreateObject("MSComDlg.CommonDialog") 
  With CDLG
    .DialogTitle = "Get me a File!" 
    .Filter = _
      "Documents|*.doc|Templates|*.dot|Text Files|*.txt" 
    .ShowOpen
    MsgBox .FileName
  End With
  Set CDLG = Nothing
End Sub

FIGURE 3: Adding the CommonDialog** control to a form before setting its properties isn't necessary with this code.**

Finally, there's a fourth technique: using the Windows API to create your own common dialog box objects. This isn't only a convenient way to avoid licensing problems and have a solution that works for all Office applications, but it also opens the door to some exciting customizations that aren't available with any of the other options. We'll discover that in Part II; in this article, we'll keep it simple.

What's Involved

Manipulating and displaying Windows common dialog boxes requires just two API functions: GetOpenFileName and GetSaveFileName, both of which are exposed by the COMDLG32.DLL file, installed in everybody's Windows\System folder. In a VBA module, you declare these functions as follows:

Private Declare Function GetOpenFileName _
  Lib "COMDLG32.DLL" Alias "GetOpenFileNameA" _
  (pOpenfilename As OPENFILENAME) As Long
Private Declare Function GetSaveFileName _
  Lib "COMDLG32.DLL" Alias "GetSaveFileNameA" _
  (pOpenfilename As OPENFILENAME) As Long

Both functions require a single parameter pointing to an OPENFILENAME structure. In VBA, you format Windows structures as a user-defined type (UDT). FIGURE 4 shows how to create this data type in VBA.

Private Type OPENFILENAME
  lStructSize       As Long
  hwndOwner         As Long
  hInstance         As Long
  lpstrFilter       As String
  lpstrCustomFilter As String
  nMaxCustFilter    As Long
  nFilterIndex      As Long
  lpstrFile         As String
  nMaxFile          As Long
  lpstrFileTitle    As String
  nMaxFileTitle     As Long
  lpstrInitialDir   As String
  lpstrTitle        As String
  Flags             As Long
  nFileOffset       As Integer
  nFileExtension    As Integer
  lpstrDefExt       As String
  lCustData         As Long
  lpfnHook          As Long
  lpTemplateName    As String
End Type

FIGURE 4: The OPENFILENAME structure disguised as a user-defined type (UDT).

The OPENFILENAME structure contains information that the operating system uses to initialize the Open or Save As common dialog box. After the user closes the dialog box, the system returns information about the selected file(s) in this structure. FIGURE 5 gives a brief explanation of the values you can provide and expect in return.

Member

Comment

lStructSize

Size of the structure.

hwndOwner

Handle to the window that owns the dialog box. Can be zero if the dialog box doesn't have an owner.

hInstance

Handle to a module that contains a dialog box template. Ignored in this project; will be used in Part II.

lpstrFilter

Buffer containing pairs of null-terminated filter strings.

lpstrCustomFilter

Ignored in this project.

nMaxCustFilter

Ignored in this project.

nFilterIndex

First filter to show (1-based).

lpstrFile

File name to initialize file name edit control; returns full path.

nMaxFile

Size of the buffer pointed to by lpstrFile. Must be at least 256.

lpstrFileTitle

Receives the file name and extension (without path information) of the selected file.

nMaxFileTitle

Size of the buffer pointed to by lpstrFileTitle.

lpstrInitialDir

String that can specify the initial Directory.

lpstrTitle

String to be placed in the title bar of the dialog box.

Flags

Bit flags that determine dialog box behavior (see FIGURE 7).

nFileOffset

Zero-based offset from the beginning of the path to the file name in the string pointed to by lpstrFile.

nFileExtension

Zero-based offset from the beginning of the path to the file name extension in the string pointed to by lpstrFile.

lpstrDefExt

Buffer that contains the default extension.

lCustData

Ignored in this project.

lpfnHook

Ignored in this project; will be used in Part II.

lpTemplateName

Ignored in this project; will be used in Part II.

FIGURE 5: The 20 members of the OPENFILENAME structure.

Although most OPENFILENAME members are optional, a few (lStructSize, nMaxFile, and lpstrFile) are required. The minimal listing in FIGURE 6 demonstrates how they're used.

Sub GetFileFromAPI ()
  Dim OFN As OPENFILENAME
  With OFN
    .lStructSize = Len(OFN)     ' Size of structure. 
    .nMaxFile = 260             ' Size of buffer. 
    ' Create buffer.
    .lpstrFile = String(.nMaxFile - 1, 0) 
    Ret = GetOpenFileName(OFN)  ' Call function. 
    If Ret <> 0 Then            ' Non-zero is success. 
      ' Find first null char. 
      n = InStr(.lpstrFile, VbNullChar)   
      ' Return what's before it.      
      MsgBox Left(.lpstrFile, n - 1)      
    End If
  End With
End Sub

FIGURE 6: Code showing how OPENFILENAME structures are used.

This simple macro displays the most basic common dialog box. Since no value is specified for lpstrTitle, the dialog box defaults to the string "Open," and the lack of a value for lpstrFilter results in an empty "Files of type" list.

There are many ways to personalize common dialog boxes. One of the most powerful customization features is the Flags member of the OPENFILENAME structure. By specifying one or more of the flag values shown in FIGURE 7, you can alter the way the dialog box behaves.

Constant name

Hex value

Comment

OFN_ALLOWMULTISELECT

&H200

Allows multiple file selections; use only with Open. OFN_EXPLORER must also be set.

OFN_CREATEPROMPT

&H2000

Asks for permission to create a file that doesn't exist. Also sets OFN_PATHMUSTEXIST. Works only with Open.

OFN_ENABLEHOOK

&H20

Enables the Hook function specified in the lpfnHook member. Will be used in Part II.

OFN_ENABLETEMPLATE

&H40

Indicates that the lpTemplateName member is a pointer to the name of a dialog box template resource in the module identified by the hInstance member. Will be used in Part II.

OFN_EXPLORER

&H80000

Uses Explorer-style dialog boxes. Is automatically set, but must specifically be set if OFN_ALLOWMULTISELECT is used.

OFN_EXTENSIONDIFFERENT

&H400

On return, specifies that the user typed a file name extension that differs from the extension specified by lpstrDefExt. Not used if lpstrDefExt is unspecified.

OFN_FILEMUSTEXIST

&H1000

Accepts only existing file. Works only with Open.

OFN_HIDEREADONLY

&H4

Hides the read-only box. Should be set for Save As.

OFN_LONGNAMES

&H200000

Explorer-style dialog boxes ignore this flag and always display long file names.

OFN_NOCHANGEDIR

&H8

Restores the current directory to its original value if the user changed the directory while searching for files.

OFN_NODEREFERENCELINKS

&H100000

Returns the path and file name of the selected shortcut (.lnk) file. If not specified, the dialog box returns the path and file name of the file referenced by the shortcut.

OFN_OVERWRITEPROMPT

&H2

In the Save As dialog box, display a message box if the selected file already exists. The user must confirm whether to overwrite the file.

OFN_PATHMUSTEXIST

&H800

Accepts only existing path.

OFN_READONLY

&H1

Selects the read-only check box when the dialog box is created. Returns the state of the read-only check box when the dialog box is closed. Only useful with Open.

OFN_SHOWHELP

&H10

Displays the Help button. Will be used Part II.

FIGURE 7: OFN Flags. This list isn't comprehensive, but it contains all flags used in this project.

When you specify flags, you can simply add the constant names or use the Or operand. For example, to display the Save As dialog box, you probably don't want to show the read-only checkbox, but you do want to ensure the user specifies a valid path, and you certainly want to display a warning if the user selects the name of an existing file. The following instructions do just that:

OFN.Flags = OFN_HIDEREADONLY Or OFN_PATHMUSTEXIST Or _
            OFN_OVERWRITEPROMPT

Introducing the Common Dialog Box Class

Listing One contains a class module that encapsulates all the nitty-gritty of common dialog boxes, and automates a lot of parameters. Let's walk through the relevant elements of the class and see which properties are used to make things happen. While we do that, you may also get an insight into some exciting new features of VBA 6.0 that you may not yet have discovered.

AllowMultiSelect property. Boolean. Default value is False. If set to True, the common dialog box allows multiple file selections. Ignored if the Save As dialog box is displayed.

CheckBoxSelected property. Read/write Boolean. Default value is False. If set to True, the common dialog box displays and selects the read-only checkbox. On return, the True/False value of this property indicates whether the user selected or unselected the checkbox.

CheckBoxVisible property. Boolean. Default value is False. If set to True, the common dialog box displays the read-only checkbox. If the CheckBoxSelected property is set to True, CheckBoxVisible is also automatically set to True.

DialogTitle property. String. Specifies the title bar text. Especially useful if you don't want to use the dialog box to open or save a file, but need a file name (or a list of file names) for another purpose. If omitted, Windows uses the default caption ("Open" or "Save As").

Directory property. Read/write String. Specifies the initial path in the dialog box. If omitted, the initial path is determined by the operating system. (See also FileName property.) On return, this property contains the path in which the user has selected the file(s).

ExistFlags property. Long. Use any combination of the following enumeration variables: PathMustExist, FileMustExist, and PromptToCreateFile. These values correspond to the constants OFN_PATHMUSTEXIST, OFN_FILEMUSTEXIST, and OFN_CREATEPROMPT, shown in FIGURE 7. PathMustExist is automatically set if the Save As dialog box is displayed. (See also the "Enumerations" section.)

Extension property. Read/write String. Specifies the extension appended to the file name if the user fails to type an extension. This string can be any length, but only the first three characters are appended. If this property isn't specified and the user doesn't type an extension, no extension is appended. On return, this property contains the extension of the selected file. If multiple files are selected, the return value is an empty string.

FileName property. Read/write String. Specifies the file name used to initialize the "File name" edit control. If the specified file name contains a path name, and no value is specified for the Directory property, Windows 98 and earlier versions use the path component of FileName to initialize the dialog box. Windows 2000 uses this path to initialize the dialog box, even if a value is specified for the Directory property. On return, this property contains the name (including the path) of the selected file. If multiple files are selected, this property contains an empty string; use the FileNames property instead.

FileNames property. Read-only Collection. On return, contains the names (including the path) of all selected files. Use FileNames.Count to obtain the number of selected files. See the first example for its implementation. If the Save As dialog box is displayed, this collection contains a single file name.

FileTitle property. Read-only String. On return, this property contains the name (without the path) of the selected file. If multiple files are selected, this property contains an empty string; use the FileTitles property instead.

FileTitles property. Read-only Collection. On return, contains the names (without the path) of all selected files. Use FileTitles.Count to obtain the number of selected files. If the Save As dialog box is displayed, this collection contains a single file title. Returning file titles is especially useful if you have a form with a listbox that you want to fill with file names.

Filter property. Read/write Variant. Specifies the filters in the "Files of type" list. Can be a single string, or an array of strings. Filters are pairs of strings, separated by a pipe symbol (|). The first string in each pair is a string that describes the filter (e.g. "Documents"), and the second string specifies the filter pattern (e.g. "*.doc"). To specify multiple filter patterns for a single display string, use a semicolon to separate the patterns (e.g. "*.doc;*.dot;*.wiz"). A pattern string can be a combination of valid file name characters and the asterisk (*) or question mark wildcard character (e.g. "*.do?"). Don't include spaces in the pattern string. See the examples for its implementation. On return, this property contains the filter selected by the user.

FilterIndex property. Read/write Long. Specifies the initial index of the selected filter in the "Files of type" list. The first filter has an index value of 1, the second filter 2, etc. On return, this property contains the index of the filter selected by the user.

RestoreCurDir property. Boolean. Default value is False. If set to True, the common dialog box restores the current directory to its original value if the user changed the directory while searching for files.

ShowOpen method. Displays the Open dialog box. Returns True if one or more files were selected and False if the user clicked the Cancel button.

ShowSave method. Displays the Save As dialog box. Returns True if a file was selected and False if the user clicked the Cancel button.

Join, Split, and Replace

The class module uses a number of features that didn't exist before VBA 6.0. If you haven't played with them yet, have a look at the Property procedures in FIGURE 8.

Property Let Filter(vFilter) 
  If IsArray(vFilter) Then _
    vFilter = Join(vFilter, vbNullChar) 
  OFN.lpstrFilter = Replace(vFilter, sPipe, vbNullChar) 
End Property
 
Property Get Filter()
  With OFN
    If .nFilterIndex Then
      Dim sTemp()
      sTemp = Split(.lpstrFilter, vbNullChar) 
      Filter = sTemp(.nFilterIndex * 2 - 2) & _
               sPipe & sTemp(.nFilterIndex * 2 - 1) 
    End If
  End With
End Property

FIGURE 8: Property procedures new to VBA 6.0.

Recall that the Filter property of the common dialog box class can be specified as a single string or an array. So, for example, you can use a string assignment such as this:

OFN.Filter = _
  "Documents|*.doc|Templates|*.dot|Text Files|*.txt" 

or you can assign an array:

Dim f(2) As String
f(0) = "Documents|*.doc" 
f(1) = "Templates|*.dot" 
f(2) = "Text Files|*.txt" 
OFN.Filter = f

Windows doesn't like either format, and wants its filter strings separated by null characters. The Property Let Filter procedure converts the value it receives (a Variant) into the required format. First, the procedure checks to see if it received an array by using the IsArray function. If this function returns True, the Join function is called to create a single string from the individual array elements. In this case, the elements are joined with null characters. This results in a single string:

"Documents|*.doc" & Chr(0) & "Templates|*.dot" & _
  Chr(0) & "Text Files|*.txt" 

The second instruction in the procedure then uses the Replace function to convert all remaining pipe characters (|) into null characters, and adds a null character at the end. This results in a string that Windows can handle:

"Documents"& Chr(0) & "*.doc" & Chr(0) & _
  "Templates" & Chr(0) & "*.dot" & Chr(0) & _
  "Text Files" & Chr(0) & "*.txt" & Chr(0) 

The Property GetFilter procedure (which returns the filter selected by the user) reverses the process. The Split function is used to break the string into array elements (using the null character as the separator), which creates an array such as this:

sTemp(0) = "Documents" 
sTemp(1) = "*.doc" 
sTemp(2) = "Templates" 
sTemp(3) = "*.dot" 
sTemp(4) = "Text Files" 
sTemp(5) = "*.txt" 

Depending on the value of the filter index (which is 1-based), the two matching array elements are then joined, delimited by the pipe symbol.

Enumerations

Enumerations were also introduced with VBA 6.0. Enumeration variables are variables declared with an Enum type. The elements of the Enum type are initialized to constant values within the Enum statement. The assigned values can't be modified at run time, and can include both positive and negative numbers. The class module contains the following enumeration:

Public Enum XFlags
  PathMustExist = OFN_PATHMUSTEXIST
  FileMustExist = OFN_FILEMUSTEXIST
  PromptToCreateFile = OFN_CREATEPROMPT
End Enum

The Property Let ExistFlags procedure takes this enumeration as its parameter:

Property Let
ExistFlags(nFlags As XFlags) 
  OFN.Flags = OFN.Flags Or nFlags
End Property

The great advantage of enumeration variables is that they facilitate the input of values. FIGURE 9 demonstrates how Microsoft's IntelliSense technology helps you provide the correct parameters.


FIGURE 9: IntelliSense in action: Enumerations facilitate the selection of values.

Examples

If you want to use common dialog boxes in your applications, you must add the ComDlg.cls module to your project. The following examples demonstrate how you can use the ComDlg objects in your application.

Example 1

The code in FIGURE 10 displays the Open dialog box with a user-defined caption. The initial directory is Word's default document path. The filter is provided as an array, and the last filter is displayed as the default. The read-only checkbox is pre-selected, multiple file selection is enabled, and flags are set to make sure that only existing file paths are returned. If the Open button in the dialog box is clicked, the files are extracted from the FileNames collection, and opened in Word. If the read-only checkbox was selected when the dialog box closed, the files are opened as read-only.

Sub GetWordFiles()
  Dim CDLG As ComDlg
  Set CDLG = New ComDlg
  With CDLG
    .DialogTitle = "Open a Word file" 
    .Directory = Options.DefaultFilePath(wdDocumentsPath) 
    Dim f(3) As String
    f(0) = "Documents (*.doc)|*.doc" 
    f(1) = "Templates (*.dot)|*.dot" 
    f(2) = "Wizards (*.wiz)|*.wiz" 
    f(3) = "All Files (*.*)|*.*" 
    .Filter = f
    .FilterIndex = 4
    .CheckBoxSelected = True
    .AllowMultiSelect = True
    .ExistFlags = FileMustExist + PathMustExist
    If .ShowOpen Then
      For i = 1 To .FileNames.Count
        Documents.Open FileName:=.FileNames(i), _
          ReadOnly:=.CheckBoxSelected
      Next
    End If
  End With
  Set CDLG = Nothing
End Sub

FIGURE 10: The routine that displays the Open dialog box.

FIGURE 11 shows the dialog box created with the routine shown in FIGURE 10.


FIGURE 11: The Open dialog box created with the code in FIGURE 10.

Example 2

The macro in FIGURE 12 displays the Save As dialog box using a non-standard caption. No value is specified for the Directory property, so the dialog box defaults to the path of the active workbook. The current name of the workbook is displayed in the File name box. A default extension is specified, so the file will have the .xls extension, even if the user doesn't type an extension. The Filter property is specified as a single string. No FilterIndex property is specified, so the first filter becomes the default. If the Save button is clicked, a copy of the workbook is saved to a file, without modifying the open workbook in memory.

Sub SaveWorkbookCopy()
  With New ComDlg
    .DialogTitle = "Save Copy As" 
    .FileName = ActiveWorkbook.FullName
    .Extension = "xls" 
    .Filter = "Workbook (*.xls)|*.xls " & _
      "|Web Page (*.htm)|*.htm|Template (*.xlt)|*.xlt" 
    If .ShowSave Then _
      ActiveWorkbook.SaveCopyAs .FileName
  End With
End Sub

FIGURE 12: The routine to display the Save As dialog box.

FIGURE 13 shows the dialog box created with the routine in FIGURE 12.


FIGURE 13: The Save As dialog box created with the macro in FIGURE 12.

Conclusion

The purpose of this series is to demonstrate that there's a lot more to VBA programming than you may think. With a bit of hacking, you can greatly enhance your Office applications.

In this first installment of a two-part series, I showed you how you can implement Windows common dialog boxes in your projects. In the next installment, we'll discover some tricks that enhance the common dialog boxes beyond your wildest dreams. You'll be able to rename or hide any existing dialog box control, add your own functionality to the read-only checkbox, display a status bar and instant file information, and even add your own buttons. Watch this space for more VBA hacks!

Dutchman Romke Soldaat was hired by Microsoft in 1988 to co-found the Microsoft International Product Group in Dublin, Ireland. That same year he started working with the prototypes of WinWord, writing his first macros long before the rest of the world. In 1992, he left Microsoft and created a number of successful add-ons for Office. Living in Italy, he divides his time between writing articles for this magazine, enjoying the Mediterranean climate, and steering his Landrover through the world's most deserted areas. Romke can be contacted at mailto:romke@soldaat.com.

Begin Listing One - ComDlg.cls

Option Explicit
 
DefStr S
DefLng N
DefBool B
DefVar V
 
' OFN constants. 
Const OFN_ALLOWMULTISELECT   As Long = &H200
Const OFN_CREATEPROMPT       As Long = &H2000
Const OFN_EXPLORER           As Long = &H80000
Const OFN_EXTENSIONDIFFERENT As Long = &H400
Const OFN_FILEMUSTEXIST      As Long = &H1000
Const OFN_HIDEREADONLY       As Long = &H4
Const OFN_LONGNAMES          As Long = &H200000
Const OFN_NOCHANGEDIR        As Long = &H8
Const OFN_NODEREFERENCELINKS As Long = &H100000
Const OFN_OVERWRITEPROMPT    As Long = &H2
Const OFN_PATHMUSTEXIST      As Long = &H800
Const OFN_READONLY           As Long = &H1
 
' The maximum length of a single file path. 
Const MAX_PATH As Long = 260
' This MAX_BUFFER value allows you to select approx. 
' 500 files with an average length of 25 characters. 
' Change this value as needed. 
Const MAX_BUFFER As Long = 50 * MAX_PATH
' String constants: 
Const sBackSlash As String = "\" 
Const sPipe As String = "|" 
 
' API functions to use the Windows common dialog boxes. 
Private Declare Function GetOpenFileName _
  Lib "COMDLG32.DLL" Alias"GetOpenFileNameA" _
  (pOpenfilename As OPENFILENAME) As Long
Private Declare Function GetSaveFileName _
  Lib "COMDLG32.DLL" Alias"GetSaveFileNameA" _
  (pOpenfilename As OPENFILENAME) As Long
Private Declare Function GetActiveWindow _
  Lib "user32" ()As Long
 
' Type declaration, used by GetOpenFileName and
' GetSaveFileName. 
Private Type OPENFILENAME
  lStructSize       As Long
  hwndOwner         As Long
  hInstance         As Long
  lpstrFilter       As String
  lpstrCustomFilter As String
  nMaxCustFilter    As Long
  nFilterIndex      As Long
  lpstrFile         As String
  nMaxFile          As Long
  lpstrFileTitle    As String
  nMaxFileTitle     As Long
  lpstrInitialDir   As String
  lpstrTitle        As String
  Flags             As Long
  nFileOffset       As Integer
  nFileExtension    As Integer
  lpstrDefExt       As String
  lCustData         As Long
  lpfnHook          As Long
  lpTemplateName    As String  ' Can also be a Long. 
End Type
 
' Private variables. 
Private OFN As OPENFILENAME
Private colFileTitles As New Collection
Private colFileNames As New Collection
Private sFullName
Private sFileTitle
Private sPath
Private sExtension
 
' Public enumeration variable. 
Public Enum XFlags
  PathMustExist = OFN_PATHMUSTEXIST
  FileMustExist = OFN_FILEMUSTEXIST
  PromptToCreateFile = OFN_CREATEPROMPT
End Enum
 
Property Let AllowMultiSelect(bFlag) 
  SetFlag OFN_ALLOWMULTISELECT, bFlag
End Property
 
Property Let DialogTitle(sCaption) 
  OFN.lpstrTitle = sCaption
End Property
 
Property Let Filter(vFilter) 
  If IsArray(vFilter) Then _
    vFilter = Join(vFilter, vbNullChar) 
  OFN.lpstrFilter = _
    Replace(vFilter, sPipe, vbNullChar) & vbNullChar
End Property
 
Property Get Filter()
  With OFN
    If .nFilterIndex Then
      Dim sTemp()
      sTemp = Split(.lpstrFilter, vbNullChar) 
      Filter = sTemp(.nFilterIndex * 2 - 2) & sPipe & _
        sTemp(.nFilterIndex * 2 - 1) 
    End If
  End With
End Property
 
Property Let FilterIndex(nIndex) 
  OFN.nFilterIndex = nIndex
End Property
 
Property Get FilterIndex()As Long
  FilterIndex = OFN.nFilterIndex
End Property
 
Property Let RestoreCurDir(bFlag) 
  SetFlag OFN_NOCHANGEDIR, bFlag
End Property
 
Property Let ExistFlags(nFlags As XFlags) 
  OFN.Flags = OFN.Flags Or nFlags
End Property
 
Property Let CheckBoxVisible(bFlag) 
  SetFlag OFN_HIDEREADONLY, Not bFlag
End Property
 
Property Let CheckBoxSelected(bFlag) 
  SetFlag OFN_READONLY, bFlag
End Property
 
Property Get CheckBoxSelected()As Boolean
  CheckBoxSelected = OFN.Flags And OFN_READONLY
End Property
 
Property Let FileName(sFileName) 
  If Len(sFileName) <= MAX_PATH Then _
    OFN.lpstrFile = sFileName
End Property
 
Property Get FileName()As String
  FileName = sFullName
End Property
 
Property Get FileNames()As Collection
  Set FileNames = colFileNames
End Property
 
Property Get FileTitle()As String
  FileTitle = sFileTitle
End Property
 
Property Get FileTitles()As Collection
  Set FileTitles = colFileTitles
End Property
 
Property Let Directory(sInitDir) 
  OFN.lpstrInitialDir = sInitDir
End Property
 
Property Get Directory()As String
  Directory = sPath
End Property
 
Property Let Extension(sDefExt) 
  OFN.lpstrDefExt = LCase$(Left$( _
    Replace(sDefExt, ".", vbNullString), 3)) 
End Property
 
Property Get Extension()As String
  Extension = sExtension
End Property
 
Function ShowOpen()As Boolean
  ShowOpen = Show(True)
End Function
 
Function ShowSave()As Boolean
  ' Set or clear appropriate flags for Save As dialog. 
  SetFlag OFN_ALLOWMULTISELECT, False
  SetFlag OFN_PATHMUSTEXIST, True
  SetFlag OFN_OVERWRITEPROMPT, True
  ShowSave = Show(False)
End Function
 
Private Function Show(bOpen) 
  With OFN
    .lStructSize = Len(OFN) 
    ' Could be zero if no owner is required. 
    .hwndOwner = GetActiveWindow
    ' If the RO checkbox must be checked, we should also
    ' display it. 
    If .Flags And OFN_READONLY Then _
      SetFlag OFN_HIDEREADONLY, False
    ' Create large buffer if multiple file selection
    ' is allowed. 
    .nMaxFile = IIf(.Flags And OFN_ALLOWMULTISELECT, _
      MAX_BUFFER + 1, MAX_PATH + 1) 
    .nMaxFileTitle = MAX_PATH + 1
    ' Initialize the buffers. 
    .lpstrFile = .lpstrFile & String$( _
      .nMaxFile - 1 - Len(.lpstrFile), 0) 
    .lpstrFileTitle = String$(.nMaxFileTitle - 1, 0) 
 
    ' Display the appropriate dialog. 
    If bOpen Then
      Show = GetOpenFileName(OFN) 
    Else
      Show = GetSaveFileName(OFN) 
    End If
 
    If Show Then
      ' Remove trailing null characters. 
      Dim nDoubleNullPos
      nDoubleNullPos = InStr(.lpstrFile & vbNullChar, _
                              String$(2, 0)) 
      If nDoubleNullPos Then
        ' Get the file name including the path name. 
        sFullName = Left$(.lpstrFile, nDoubleNullPos - 1) 
        ' Get the file name without the path name. 
        sFileTitle = Left$(.lpstrFileTitle, _
          InStr(.lpstrFileTitle, vbNullChar) - 1) 
        ' Get the path name. 
        sPath = Left$(sFullName, .nFileOffset - 1) 
        ' Get the extension. 
        If .nFileExtension Then
          sExtension = Mid$(sFullName, .nFileExtension + 1) 
        End If
        ' If sFileTitle is a string, 
        ' we have a single selection. 
        If Len(sFileTitle) Then
          ' Add to the collections. 
          colFileTitles.Add _
            Mid$(sFullName, .nFileOffset + 1) 
          colFileNames.Add sFullName
        Else  ' Tear multiple selection apart. 
          Dim sTemp(), nCount
          sTemp = Split(sFullName, vbNullChar) 
          ' If array contains no elements, 
          ' UBound returns -1. 
          If UBound(sTemp) > LBound(sTemp) Then
            ' We have more than one array element! 
            ' Remove backslash if sPath is the root folder. 
            If Len(sPath) = 3 Then _
              sPath = Left$(sPath, 2) 
            ' Loop through the array, and create the
            ' collections; skip the first element
            ' (containing the path name), so start the
            ' counter at 1, not at 0. 
            For nCount = 1 To UBound(sTemp) 
              colFileTitles.Add sTemp(nCount) 
              ' If the string already contains a backslash, 
              ' the user must have selected a shortcut
              ' file, so we don't add the path. 
              colFileNames.Add If(InStr(sTemp(nCount), _
                sBackSlash), sTemp(nCount),  
                sPath & sBackSlash & sTemp(nCount)) 
            Next
            ' Clear this variable. 
            sFullName = vbNullString
          End If
        End If
        ' Add backslash if sPath is the root folder. 
        If Len(sPath) = 2 Then _
          sPath = sPath & sBackSlash
      End If
    End If
  End With
End Function
 
Private Sub SetFlag(nValue, bTrue) 
  ' Wrapper routine to set or clear bit flags. 
  With OFN
    If bTrue Then
      .Flags = .Flags Or nValue
    Else
      .Flags = .Flags And Not nValue
    End If
  End With
End Sub
 
Private Sub Class_Initialize()
  ' This routine runs when the object is created. 
  OFN.Flags = OFN.Flags Or OFN_EXPLORER Or _
              OFN_LONGNAMES Or OFN_HIDEREADONL
End Sub

End Listing One