自作WindowsAPIクラスにユーザが選択したフォルダのパスを返すFunctionを追加した
ユーザが選択したフォルダのパスを取得するFunction
相変わらず、WindowsAPIにハマっている。
今回も、名著『大村あつしのExcel VBA Win64/32 API プログラミング』を参考に、おなじみ、自作WindowsAPI
クラスにメソッドを追加する。
このときにも書いたが、クラスモジュールWindowsAPI
の他に、標準モジュールWinAPIEnums
(参考)を使う。
列挙体だけでなく、構造体や定数の宣言用に使うことにした。今さらながら、WinAPIConstants
とでもしておけば良かった……。これは今後変更するかも知れぬ。
では早速。
ちなみに、今回参考にしたのは、
我らが教科書『大村あつしのExcel VBA Win64/32 API プログラミング』の130~137ページです。
構造体と定数の宣言
今回使用するWindowsAPI関数は、
FindWindow
関数SHBrowseForFolder
関数SHGetPathFromIDList
関数CoTaskMemFree
関数
の四つ。
このうち、FindWindow
関数は、すでに
このときに使用済み。
これらの関数のうち、SHBrowseForFolder
関数で引数に構造体を使うこと、また、その構造体に値をセットする際に定数を使用することから、標準モジュールWinAPIEnums
に構造体と定数を追加する。
リスト1 標準モジュール宣言セクション
'CustomTypes' Public Type BrowseInfo biHwndOwner As Long biPidRoot As Long biPszDisplayName As String biLpszTitle As String biUlFlags As Long biLpfn As Long biLParam As Long biIImage As Long End Type 'Constants' 'ルートフォルダを指定する定数' Public Const CSIDL_DESKTOP As Long = &H0& Public Const CSIDL_PROGRAMS As Long = &H2& Public Const CSIDL_CONTROLS As Long = &H3 Public Const CSIDL_PRINTERS As Long = &H4 Public Const CSIDL_PERSONAL As Long = &H5 Public Const CSIDL_FAVORITE As Long = &H6 Public Const CSIDL_STARTUP As Long = &H7 Public Const CSIDL_RECENT As Long = &H8 Public Const CSIDL_SENDTO As Long = &H9 Public Const CSIDL_BITBUCKET As Long = &HA Public Const CSIDL_STARTMENU As Long = &HB Public Const CSIDL_DESKTOPDIRECTORY As Long = &H10 Public Const CSIDL_DRIVES As Long = &H11 Public Const CSIDL_NETWORK As Long = &H12 Public Const CSIDL_NETHOO As Long = &H13 Public Const CSIDL_FONTS As Long = &H14 Public Const CSIDL_TEMPLATES As Long = &H15 '動作方法を指定する定数' Public Const BIF_RETURNONLYSDIRS As Long = &H1 Public Const BIF_DONTGOBELOWDOMAIN As Long = &H2 Public Const BIF_STATUSTEXT As Long = &H4 Public Const BIF_RETURNFSCANCESTORS As Long = &H8 Public Const BIF_EDITBOX As Long = &H10 Public Const BIF_VALIDATE As Long = &H20 Public Const BIF_BROWSEFORCOMPUTER As Long = &H1000 Public Const BIF_BROWSEFORPRINTER As Long = &H2000 Public Const BIF_BROWSEINCLUDEFILES As Long = &H4000
これらのうち、今回実際に使うのはごく一部だが、挙動を変えたり、機能を追加したりするときに必要になってくるので、ひととおり全て書いてある。
WindowsAPI関数の宣言
お次は、WindowsAPI関数の宣言。これは、クラスモジュールWindowsAPI
の宣言セクションに追加する。
リスト2 クラスモジュール宣言セクション
'ウインドウハンドル取得まわり' Private Declare Function FindWindow Lib "user32" _ Alias "FindWindowA" (_ ByVal lpClassName As String, _ ByVal lpWindowName As String) As Long Private Declare Function GetNextWindow Lib "user32" _ Alias "GetWindow" ( _ ByVal hwnd As Long, _ ByVal wFlag As Long) As Long Private Declare Function IsWindowVisible Lib "user32" ( _ ByVal hwnd As Long) As Long Private Declare Function GetWindowText Lib "user32" _ Alias "GetWindowTextA" ( _ ByVal hwnd As Long, _ ByVal lpString As String, _ ByVal cch As Long) As Long Private Declare Function GetClassName Lib "user32" _ Alias "GetClassNameA" ( _ ByVal hwnd As Long, _ ByVal lpClassName As String, _ ByVal nMaxCount As Long) As Long 'フォルダパス取得まわり' Private Declare Function SHBrowseForFolder Lib "shell32.dll" _ Alias "SHBrowseForFolderA" ( _ ByRef lpbi As BrowseInfo) As Long Private Declare Function SHGetPathFromIDList Lib "shell32.dll" _ Alias "SHGetPathFromIDListA" ( _ ByVal pidlRoot As Long, _ ByVal pszDispName As String) As Long '……(1)' 'メモリ解放まわり' Private Declare Function CoTaskMemFree Lib "OLE32.dll" ( _ ByVal pv As Long) As Long
(1)の第2引数「pszDispName
」については、
をご覧ください。
ユーザーが選択したフォルダパスを取得するメソッド
以上で準備は終わり。早速メソッドの本体のコードを載っけておこう。
リスト3 クラスモジュール
'ユーザが選択したフォルダのパスを取得するメソッド' Public Function getSelectedFolderPath( _ ByVal prompt As String, _ ByVal targetApp As Application) As String getSelectedFolderPath = "" Dim str As String str = Trim(Replace(targetApp.Name, "Microsoft", "")) '……(2)' Dim className As String className = getWindowClassName(str) '……(3)' Dim browseInfo_ As BrowseInfo '……(4)' With browseInfo_ .biHwndOwner = FindWindow(className, Application.Caption) .biPidRoot = CSIDL_DESKTOP .biLpszTitle = prompt .biUlFlags = BIF_RETURNONLYSDIRS End With Dim pIdl As Long pIdl = SHBrowseForFolder(browseInfo_) '……(5)' If pIdl = 0 Then Exit Function '……(6)' Dim ret As String * MAX_PATH_SIZE Dim tmp As Long tmp = SHGetPathFromIDList(pIdl, ret) '……(7)' getSelectedFolderPath = Left(ret, InStr(ret, vbNullChar) - 1) Call CoTaskMemFree(pIdl) '……(8)' End Function 'ウインドウのクラス名を取得するメソッド' Public Function getWindowClassName( _ ByVal targetAppNameKeyWord As String) As String Dim gotClassName As String * 100 Dim gotCaption As String * 200 Dim hwnd As Long hwnd = FindWindow(vbNullString, vbNullString) Do If IsWindowVisible(hwnd) Then Call GetWindowText(hwnd, gotCaption, Len(gotCaption)) If InStr(1, gotCaption, targetAppNameKeyWord) > 0 Then Call GetClassName(hwnd, gotClassName, Len(gotClassName)) Dim ret As String ret = Left(gotClassName, InStr(gotClassName, vbNullChar) - 1) Exit Do End If End If hwnd = GetNextWindow(hwnd, GW_HWNDNEXT) Loop Until hwnd = GetNextWindow(hwnd, GW_HWNDLAST) getWindowClassName = ret End Function
後半のgetWindowClassName
は、前回のリスト3と同じ。今回追加したgetSelectedFolderPath
内で使うので、再掲した次第。
(2)の
str = Trim(Replace(targetApp.Name, "Microsoft", ""))
では、(3)のgetWindowClassName
に渡す引数を作るためにReplace
関数とTrim
関数を用いてアプリケーション名(「Excel
」とか「Word
」とか)を取り出している。
(3)の
className = getWindowClassName(str)
では、自作のgetWindowClassName
を用いて、アプリケーションのクラス名(Excelだったら「XLMAIN
」)を取得し、変数className
にぶち込んでいる。
(4)からの7行
Dim browseInfo_ As BrowseInfo With browseInfo_ .biHwndOwner = FindWindow(className, Application.Caption) .biPidRoot = CSIDL_DESKTOP .biLpszTitle = prompt .biUlFlags = BIF_RETURNONLYSDIRS End With
では、上のリスト1で宣言した構造体BrowseInfo
型の変数browseInfo_
を準備して、値をぶち込んでいる。
先ほど(2)、(3)で準備したアプリケーションのクラス名は、
.biHwndOwner = FindWindow(className, Application.Caption)
のところで、FindWindow
関数の引数として使用している。
これで、BrowseInfo
型の変数browseInfo_
に値のセットが終わったので、(5)の
pIdl = SHBrowseForFolder(browseInfo_)
でSHBrowseForFolder
を呼ぶ。
ここで、フォルダ選択ダイアログが表示されることになる。
ユーザーがフォルダを選んで[OK]、または[キャンセル]をクリックするとコード実行再開。
ユーザーが[キャンセル]をクリックしていたら、SHBrowseForFolder
が「0
」を返すので、(6)の
If pIdl = 0 Then Exit Function
で処理を抜ける。このとき、getSelectedFolderPath
は「""
」を返す。
何らかのフォルダを選んで[OK]をクリックした場合は、(7)からの2行
tmp = SHGetPathFromIDList(pIdl, ret) getSelectedFolderPath = Left(ret, InStr(ret, vbNullChar) - 1)
で、SHGetPathFromIDList
にSHBrowseForFolder
の返り値pIdl
を渡す。
SHGetPathFromIDList
を実行する過程で「ret
」には、ユーザが選択したフォルダパスが固定長の文字列がぶち込まれ、
Left(ret, InStr(ret, vbNullChar) - 1)
によって必要な文字列だけが切り出されてgetSelectedFolderPath
の返り値としてreturnされることとなる。
あとは、(8)の
Call CoTaskMemFree(pIdl)
でメモリを解放して終わり。
使ってみる
イミディエイト・ウインドウに
?WindowsAPI.getSelectedFolderPath("フォルダを選択してください",Excel.Application)
と打ち込んで、[Enter]をそっと押す。
すると、
まず、フォルダダイアログが表示される。
テキトーにフォルダを選択して[OK]をクリックする。
バッチリ。
おわりに
筆者の大村あつし氏もおっしゃるように、VBAだけでは、フォルダ選択ダイアログを表示させることはできないので、ツール作りをする人にとってはめっちゃ便利なのではないかと思いました。
ちなみに、画像ではまったく伝わりませんが(笑)、
Wordでもフツーに動きます。
たぶん、OutlookとかPowerpointでも大丈夫だと思いますが、保証はしまへん。