解决64位VBA的打开文件及文件兼容问题


问题描述

最近修改一个非常古老的CAD插件程序,由VBA编写,在32位软件下没问题,但在64位上出现PtrSafe、Addressof、 CommonDialog无法调用等问题。通过研究测试,重写了文件打开、文件保存和文件夹打开的操作方法,实用对64位系统和高版本CAD的支持。

操作系统:Win10 64位

CAD版本: AutoCAD 2016 x64

文件浏览

文件打开、文件保存:

Option Explicit

Private Declare PtrSafe Function GetOpenFileName Lib "comdlg32.dll" Alias "GetOpenFileNameA" (pOpenfilename As OPENFILENAME) As Long

Private Declare PtrSafe Function GetSaveFileName Lib "comdlg32.dll" Alias "GetSaveFileNameA" (pOpenfilename As OPENFILENAME) As Long

Private Declare PtrSafe Function SHBrowseForFolder Lib "shell32.dll" Alias "SHBrowseForFolderA" (lpBrowseInfo As BrowseInfo) As Long

Private Declare PtrSafe Function SHGetPathFromIDList Lib "shell32.dll" (ByVal pidList As LongPtr, ByVal lpBuffer As String) As Long

Private Declare PtrSafe Function SendMessageA Lib "user32" (ByVal Hwnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, lParam As Any) As LongPtr

Private Declare PtrSafe Sub CoTaskMemFree Lib "ole32.dll" (ByVal hMem As LongPtr)

Private Const BIF_RETURNONLYFSDIRS As Long = 1
Private Const CSIDL_DRIVES As Long = &H11
Private Const WM_USER As Long = &H400
Private Const MAX_PATH As Long = 260            '// message from browser
Private Const BFFM_INITIALIZED As Long = 1
Private Const BFFM_SELCHANGED As Long = 2
Private Const BFFM_VALIDATEFAILEDA As Long = 3  '// lParam:szPath ret:1(cont),0(EndDialog)
Private Const BFFM_VALIDATEFAILEDW As Long = 4  '// lParam:wzPath ret:1(cont),0(EndDialog)
Private Const BFFM_IUNKNOWN As Long = 5         '// provides IUnknown to client. lParam: IUnknown*
'// messages to browser
Private Const BFFM_SETSTATUSTEXTA As Long = WM_USER + 100
Private Const BFFM_ENABLEOK As Long = WM_USER + 101
Private Const BFFM_SETSELECTIONA As Long = WM_USER + 102
Private Const BFFM_SETSELECTIONW As Long = WM_USER + 103
Private Const BFFM_SETSTATUSTEXTW As Long = WM_USER + 104
Private Const BFFM_SETOKTEXT As Long = WM_USER + 105 '// Unicode only
Private Const BFFM_SETEXPANDED As Long = WM_USER + 106 '// Unicode only
     
Private Const OFN_ALLOWMULTISELECT As Long = &H200
Private Const OFN_CREATEPROMPT As Long = &H2000
Private Const OFN_ENABLEHOOK As Long = &H20
Private Const OFN_ENABLETEMPLATE As Long = &H40
Private Const OFN_ENABLETEMPLATEHANDLE As Long = &H80
Private Const OFN_EXPLORER As Long = &H80000
Private Const OFN_EXTENSIONDIFFERENT As Long = &H400
Private Const OFN_FILEMUSTEXIST As Long = &H1000
Private Const OFN_HIDEREADONLY As Long = &H4
Private Const OFN_LONGNAMES As Long = &H200000
Private Const OFN_NOCHANGEDIR As Long = &H8
Private Const OFN_NODEREFERENCELINKS As Long = &H100000
Private Const OFN_NOLONGNAMES As Long = &H40000
Private Const OFN_NONETWORKBUTTON As Long = &H20000
Private Const OFN_NOREADONLYRETURN As Long = &H8000& '*see comments
Private Const OFN_NOTESTFILECREATE As Long = &H10000
Private Const OFN_NOVALIDATE As Long = &H100
Private Const OFN_OVERWRITEPROMPT As Long = &H2
Private Const OFN_PATHMUSTEXIST As Long = &H800
Private Const OFN_READONLY As Long = &H1
Private Const OFN_SHAREAWARE As Long = &H4000
Private Const OFN_SHAREFALLTHROUGH As Long = 2
Private Const OFN_SHAREWARN As Long = 0
Private Const OFN_SHARENOWARN As Long = 1
Private Const OFN_SHOWHELP As Long = &H10
Private Const OFN_ENABLESIZING As Long = &H800000
Private Const OFS_MAXPATHNAME As Long = 260

'OFS_FILE_OPEN_FLAGS:
Private Const OFS_FILE_OPEN_FLAGS = OFN_EXPLORER Or _
             OFN_LONGNAMES Or _
             OFN_CREATEPROMPT Or _
             OFN_NODEREFERENCELINKS
             
Private Type OPENFILENAME
    lStructSize As Long
    hWndOwner As LongPtr
    hInstance As LongPtr
    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 LongPtr
    lpTemplateName As String
End Type

Private Type BrowseInfo
    hWndOwner As LongPtr
    pIDLRoot As LongPtr
    pszDisplayName As String
    lpszTitle As String
    ulFlags As Long
    lpfnCallback As LongPtr
    lParam As LongPtr
    iImage As Long
End Type
 
'====== File Browsers for 64 bit VBA 7 ========

'选择文件
Public Function FileBrowseOpen(ByVal sInitFolder As String, ByVal sTitle As String, ByVal sFilter As String, ByVal nFilterIndex As Integer, Optional ByVal multiSelect = False) As String

    Dim OpenFile As OPENFILENAME
    Dim lReturn As Long

    sInitFolder = CorrectPath(sInitFolder)
    OpenFile.lpstrInitialDir = sInitFolder

    ' Swap filter separator for api separator
    sFilter = Replace(sFilter, "|", Chr(0))

    OpenFile.lpstrFilter = sFilter
    OpenFile.nFilterIndex = nFilterIndex
    OpenFile.lpstrTitle = sTitle
    
    
    
    OpenFile.hWndOwner = 0
    OpenFile.lpstrFile = String(257, 0)
    OpenFile.nMaxFile = LenB(OpenFile.lpstrFile) - 1
    OpenFile.lStructSize = LenB(OpenFile)
    
    OpenFile.lpstrFileTitle = OpenFile.lpstrFile
    OpenFile.nMaxFileTitle = OpenFile.nMaxFile
    
    If Not multiSelect Then
        OpenFile.flags = 0
    Else
        OpenFile.flags = OFS_FILE_OPEN_FLAGS + OFN_ALLOWMULTISELECT
    End If
    
    lReturn = GetOpenFileName(OpenFile)
    
    Dim result As String
    If lReturn = 0 Then
        FileBrowseOpen = ""
    Else
        If multiSelect Then
            Dim str As String
            str = Trim(Replace(Trim(OpenFile.lpstrFile), vbNullChar, ","))
            Dim ed As String
            ed = Mid(str, Len(str))
            While (ed = ",")
                str = Trim(Left(str, Len(str) - 1))
                ed = Mid(str, Len(str))
            Wend
            FileBrowseOpen = str
        Else
            FileBrowseOpen = Trim(Left(OpenFile.lpstrFile, InStr(1, OpenFile.lpstrFile, vbNullChar) - 1))
        End If
    End If
End Function

'获取文件列表
Public Function GetFiles( _
    ByVal sInitFolder As String, _
    ByVal sTitle As String, _
    ByVal sFilter As String, _
    ByVal nFilterIndex As Integer) As String()
    
    Dim strReturn As String
    
    strReturn = FileBrowseOpen(sInitFolder, sTitle, sFilter, nFilterIndex, True)
    GetFiles = Split(strReturn, ",")
    
End Function
'保存文件
Public Function FileBrowseSave(ByVal sDefaultFilename As String, ByVal sInitFolder As String, ByVal sTitle As String, ByVal sFilter As String, ByVal nFilterIndex As Integer, Optional ByVal overwritePrompt = False) As String
    
    Dim PadCount As Integer
    Dim OpenFile As OPENFILENAME
    Dim lReturn As Long

    sInitFolder = CorrectPath(sInitFolder)
    
    ' Swap filter separator for api separator
    sFilter = Replace(sFilter, "|", Chr(0))
    
    OpenFile.lpstrFilter = sFilter
    OpenFile.nFilterIndex = 1
    OpenFile.hWndOwner = 0

    PadCount = 260 - Len(sDefaultFilename)
    OpenFile.lpstrFile = sDefaultFilename & String(PadCount, Chr(0))
    'OpenFile.lpstrFile = String(257, 0)
    OpenFile.nMaxFile = LenB(OpenFile.lpstrFile) - 1
    OpenFile.lStructSize = LenB(OpenFile)
    
    OpenFile.lpstrFileTitle = OpenFile.lpstrFile
    OpenFile.nMaxFileTitle = OpenFile.nMaxFile
    OpenFile.lpstrInitialDir = sInitFolder
    OpenFile.lpstrTitle = sTitle
    If Not IsMissing(overwritePrompt) And overwritePrompt Then
        OpenFile.flags = OFN_OVERWRITEPROMPT
    Else
        OpenFile.flags = 0
    End If
    lReturn = GetSaveFileName(OpenFile)

    If lReturn = 0 Then
        FileBrowseSave = ""
    Else
        FileBrowseSave = Trim(Left(OpenFile.lpstrFile, InStr(1, OpenFile.lpstrFile, vbNullChar) - 1))
    End If
    
End Function
 
Private Function CorrectPath(ByVal sPath As String) As String
    If Right$(sPath, 1) = "\" Then
        If Len(sPath) > 3 Then sPath = Left$(sPath, Len(sPath) - 1) ' Strip backslash from non-root
    Else
        If Len(sPath) = 2 Then sPath = sPath & "\" ' Append backslash to root
    End If
    CorrectPath = sPath
End Function

'文件夹是否存在
Public Function FolderExists(ByVal sFolderName As String) As Boolean
    Dim att As Long
    On Error Resume Next
    att = GetAttr(sFolderName)
    If Err.Number = 0 Then
    FolderExists = True
    Else
    Err.Clear
    FolderExists = False
    End If
    On Error GoTo 0
End Function

文件夹浏览

Option Explicit
#If VBA7 Then
  Private Type BrowseInfo
    Owner As LongPtr
    RootIdl As LongPtr
    DisplayName As String
    Title As String
    flags As Long
    CallbackAddress As LongPtr
    CallbackParam As LongPtr
    Image As Long
  End Type
 
  Private Type SHITEMID
    cb As Long
    abID As Byte
  End Type
 
  Private Type ITEMIDLIST
    mkid As SHITEMID
  End Type
#Else
  Private Type BrowseInfo
    Owner As Long
    RootIdl As Long
    DisplayName As String
    Title As String
    flags As Long
    CallbackAddress As Long
    CallbackParam As Long
    Image As Long
  End Type
#End If
Private Const MAX_PATH_Unicode As Long = 519 ' 260 * 2 - 1
Private Const MAX_PATH = MAX_PATH_Unicode 'As Long = 260

Private Const WM_USER = &H400
Private Const BFFM_INITIALIZED As Long = 1
Private Const BFFM_SELCHANGED As Long = 2
Private Const BFFM_SETSELECTIONA = WM_USER + 102
Private Const BFFM_SETSELECTION = BFFM_SETSELECTIONA
Private Const BFFM_SETSTATUSTEXTA As Long = WM_USER + 100
Private Const BFFM_ENABLEOK As Long = WM_USER + 101
Private Const BFFM_SETSELECTIONW As Long = WM_USER + 103
Private Const BFFM_SETSTATUSTEXTW As Long = WM_USER + 104
Private Const BFFM_SETOKTEXT As Long = WM_USER + 105 '// Unicode only
Private Const BFFM_SETEXPANDED As Long = WM_USER + 106 '// Unicode only

Private Const BIF_RETURNONLYFSDIRS   As Long = &H1 'only file system directories
Private Const BIF_DONTGOBELOWDOMAIN As Long = &H2  'no network folders below domain level
Private Const BIF_STATUSTEXT As Long = &H4         'include status area for callback
Private Const BIF_RETURNFSANCESTORS As Long = &H8  'only return file system ancestors
Private Const BIF_EDITBOX As Long = &H10           'add edit box
Private Const BIF_NEWDIALOGSTYLE As Long = &H40    'use the new dialog layout
Private Const BIF_UAHINT As Long = &H100
Private Const BIF_NONEWFOLDERBUTTON As Long = &H200 'hide new folder button
Private Const BIF_NOTRANSLATETARGETS As Long = &H400 'return lnk file
Private Const BIF_USENEWUI As Long = BIF_NEWDIALOGSTYLE Or BIF_EDITBOX
Private Const BIF_BROWSEFORCOMPUTER As Long = &H1000 'only return computers
Private Const BIF_BROWSEFORPRINTER As Long = &H2000 'only return printers
Private Const BIF_BROWSEINCLUDEFILES As Long = &H4000 'browse for everything
Private Const BIF_SHAREABLE As Long = &H8000 'sharable resources, requires BIF_USENEWUI
'class ID values
Private Const CSIDL_DESKTOP As Long = &H0
Private Const CSIDL_INTERNET As Long = &H1
Private Const CSIDL_PROGRAMS As Long = &H2
Private Const CSIDL_CONTROLS As Long = &H3
Private Const CSIDL_PRINTERS As Long = &H4
Private Const CSIDL_PERSONAL As Long = &H5
Private Const CSIDL_FAVORITES As Long = &H6
Private Const CSIDL_STARTUP As Long = &H7
Private Const CSIDL_RECENT As Long = &H8
Private Const CSIDL_SENDTO As Long = &H9
Private Const CSIDL_BITBUCKET As Long = &HA 'reycle bin
Private Const CSIDL_STARTMENU As Long = &HB
Private Const CSIDL_MYDOCUMENTS As Long = &HC
Private Const CSIDL_MYMUSIC As Long = &HD
Private Const CSIDL_MYVIDEO As Long = &HE
Private Const CSIDL_UNUSED1 As Long = &HF '&HF not currently implemented
Private Const CSIDL_DESKTOPDIRECTORY As Long = &H10
Private Const CSIDL_DRIVES As Long = &H11
Private Const CSIDL_NETWORK As Long = &H12
Private Const CSIDL_NETHOOD As Long = &H13
Private Const CSIDL_FONTS As Long = &H14
Private Const CSIDL_TEMPLATES As Long = &H15
Private Const CSIDL_COMMON_STARTMENU As Long = &H16
Private Const CSIDL_COMMON_PROGRAMS As Long = &H17
Private Const CSIDL_COMMON_STARTUP As Long = &H18
Private Const CSIDL_COMMON_DESKTOPDIRECTORY As Long = &H19
Private Const CSIDL_APPDATA As Long = &H1A
Private Const CSIDL_PRINTHOOD As Long = &H1B
Private Const CSIDL_LOCAL_APPDATA As Long = &H1C
Private Const CSIDL_ALTSTARTUP As Long = &H1D
Private Const CSIDL_COMMON_ALTSTARTUP As Long = &H1E
Private Const CSIDL_COMMON_FAVORITES As Long = &H1F
Private Const CSIDL_INTERNET_CACHE As Long = &H20
Private Const CSIDL_COOKIES As Long = &H21
Private Const CSIDL_HISTORY As Long = &H22
Private Const CSIDL_COMMON_APPDATA As Long = &H23
Private Const CSIDL_WINDOWS As Long = &H24
Private Const CSIDL_SYSTEM As Long = &H25
Private Const CSIDL_PROGRAM_FILES As Long = &H26
Private Const CSIDL_MYPICTURES As Long = &H27
Private Const CSIDL_PROFILE As Long = &H28
Private Const CSIDL_SYSTEMX86 As Long = &H29 'RISC only
Private Const CSIDL_PROGRAM_FILESX86 As Long = &H2A 'RISC only
Private Const CSIDL_PROGRAM_FILES_COMMON As Long = &H2B
Private Const CSIDL_PROGRAM_FILES_COMMONX86 As Long = &H2C 'RISC only
Private Const CSIDL_COMMON_TEMPLATES As Long = &H2D
Private Const CSIDL_COMMON_DOCUMENTS As Long = &H2E
Private Const CSIDL_COMMON_ADMINTOOLS As Long = &H2F
Private Const CSIDL_ADMINTOOLS As Long = &H30
Private Const CSIDL_CONNECTIONS As Long = &H31
Private Const CSIDL_COMMON_MUSIC As Long = &H35
Private Const CSIDL_COMMON_PICTURES As Long = &H36
Private Const CSIDL_COMMON_VIDEO As Long = &H37
Private Const CSIDL_RESOURCES As Long = &H38
Private Const CSIDL_RESOURCES_LOCALIZED As Long = &H39
Private Const CSIDL_COMMON_OEM_LINKS As Long = &H3A
Private Const CSIDL_CDBURN_AREA As Long = &H3B
Private Const CSIDL_UNUSED2 As Long = &H3C '&H3C not currently implemented
Private Const CSIDL_COMPUTERSNEARME As Long = &H3D


Private Const CSIDCC_DESKTOP = &H0
Private Const MAX_LEN = MAX_PATH_Unicode '= 260

Private mstrSTARTFOLDER As String
'-----------------------------------------------
' API calls.
'-----------------------------------------------
#If VBA7 Then
  Private Declare PtrSafe Function SHGetSpecialFolderLocation Lib "shell32.dll" (ByVal hWndOwner As LongPtr, ByVal nFolder As Long, pidl As ITEMIDLIST) As LongPtr
  Private Declare PtrSafe Function SHGetPathFromIDList Lib "shell32.dll" (ByVal pidl As LongPtr, ByVal pszPath As String) As Boolean
  Private Declare PtrSafe Function SHBrowseForFolder Lib "shell32.dll" (lpBrowseInfo As BrowseInfo) As LongPtr
  Private Declare PtrSafe Function GlobalFree Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr
  Private Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal Hwnd As LongPtr, ByVal Msg As Long, wParam As Any, lParam As Any) As LongPtr
#Else
  Private Declare Function SHGetSpecialFolderLocation Lib "Shell32.DLL" (ByVal hwndOwner As Long, ByVal Folder As Long, ByRef IDL As Long) As Long
  Private Declare Function SHGetPathFromIDList Lib "Shell32.DLL" (ByVal IDL As Long, ByVal Path As String) As Long
  Private Declare Function SHBrowseForFolder Lib "Shell32.DLL" (ByRef bi As BrowseInfo) As Long
  Private Declare Function GlobalFree Lib "kernel32" (ByVal hMem As Long) As Long
  Declare Function SendMessage Lib "USER32" Alias "SendMessageA" (ByVal hWnd As Long, ByVal Msg As Long, wParam As Any, lParam As Any) As Long

#End If

'---------------------------------------------------------------------------------------
Public Function BrowseFolders(ByVal strStartFolder As String, ByVal strTitle As String) As String
    BrowseFolders = DoBrowse(BIF_RETURNONLYFSDIRS Or BIF_NEWDIALOGSTYLE, strTitle, strStartFolder)
End Function


'---------------------------------------------------------------------------------------
Private Function DoBrowse(ByVal lngFlags As Long, ByVal strTitle As String, ByVal strStartFolder As String) As String
    Dim stBif As BrowseInfo
    Dim strFolderPath As String
    #If VBA7 Then
    Dim lRet As Long
    Dim IDL As ITEMIDLIST
    Dim lngHandle As LongPtr
    #Else
    Dim lngHandle As Long
    #End If

       
     
    strFolderPath = Space(MAX_LEN)
    With stBif
      .Owner = 0
      .RootIdl = 0
      .DisplayName = Space(MAX_LEN)
      .Title = strTitle
      .flags = lngFlags
    End With
    If strStartFolder <> "" Then
      mstrSTARTFOLDER = strStartFolder & vbNullChar
      stBif.CallbackAddress = GetAddressofFunction(AddressOf BrowseCallbackProc)  'get address of function.
    End If
     
    lngHandle = SHBrowseForFolder(stBif)
    If (lngHandle <> 0) Then
      strFolderPath = Space(MAX_LEN)
      If (CBool(SHGetPathFromIDList(lngHandle, strFolderPath))) Then
        DoBrowse = TrimStringAtNull(strFolderPath)
      Else
        DoBrowse = TrimStringAtNull(strFolderPath = stBif.Title)
      End If
    End If
    Call GlobalFree(lngHandle)
     
End Function


Private Function TrimStringAtNull(ByVal strValue As String) As String
  
   Dim intPos As Integer
   
    intPos = InStr(strValue, vbNullChar)
    Select Case intPos
        Case Is > 1
            TrimStringAtNull = Left$(strValue, intPos - 1)
        Case 0
            TrimStringAtNull = intPos
        Case 1
            TrimStringAtNull = ""
    End Select
   
End Function

#If VBA7 Then
Private Function BrowseCallbackProc(ByVal Hwnd As LongPtr, ByVal uMsg As LongPtr, ByVal lP As LongPtr, ByVal pData As String) As LongPtr
#Else
Private Function BrowseCallbackProc(ByVal Hwnd As Long, ByVal uMsg As Long, ByVal lP As Long, ByVal pData As Long) As Long
#End If
   On Error Resume Next
   Dim lpIDList As LongPtr
   Dim ret As Long
   Dim sBuffer As String
   Select Case uMsg
       Case BFFM_INITIALIZED
           Call SendMessage(Hwnd, BFFM_SETSELECTION, 1, ByVal mstrSTARTFOLDER)
       Case BFFM_SELCHANGED
           sBuffer = Space(MAX_PATH)
           ret = SHGetPathFromIDList(lP, sBuffer)
           If ret = 1 Then
              Call SendMessage(Hwnd, BFFM_SETSTATUSTEXTA, 0, sBuffer)
         End If
   End Select
   BrowseCallbackProc = 0
End Function
#If VBA7 Then
Private Function GetAddressofFunction(add As LongPtr) As LongPtr
#Else
Private Function GetAddressofFunction(add As Long) As Long
#End If
 GetAddressofFunction = add
End Function

Function IsFolderExists(strFullPath As String) As Boolean
    Dim fso
    Set fso = CreateObject("Scripting.FileSystemObject")
    If fso.IsFolderExists(strFullPath) Then IsFolderExists = True
    Set fso = Nothing
End Function

Function IsFileExists(ByVal strFileName As String) As Boolean
    Dim objFileSystem As Object
    Set objFileSystem = CreateObject("Scripting.FileSystemObject")
    If objFileSystem.fileExists(strFileName) = True Then
        IsFileExists = True
    Else
        IsFileExists = False
    End If
End Function
cad