空腹おやじのログと備忘録

VBA(主にExcel)でいろいろな実験的な事とか、Linuxのコマンドとか設定とかについて忘れないように、あれこれと・・・

VBAでFindFirstFile、FindNextFileを使ってファイルリストを取得する

2022/10/20 追記
この記事のコードを FindFirstFileW を使って Unicode 対応した記事がありますので、そちらも ご覧ください。
z1000s.hatenablog.com


ことりちゅんさん(id:Kotori-ChunChun)のところで、FileSystemObjectとDirを使って、ファイルパスの一覧を取得する速度の比較をしている記事を見つけました。

kotori-chunchun.hatenablog.com
www.excel-chunchun.com

個人的には、FindFirstFileFindNextFileという選択肢もあったので、比べてみることにしました。

データ

62,200個の0Byteファイルを作成しました。(ファイル名の取得だけだから、中身はいらないでしょ。うちの娘(使用年数から言えば、もう婆さんか?)ssdの空きも少ないし・・・)

データ生成コード

Private Declare Function MakeSureDirectoryPathExists Lib "imagehlp.dll" _
       (ByVal lpPath As String) As Long

Public Const TARGET_FOLDER_ROOT As String = "C:\Datas\FileListTest\Data\"


Public Sub createData()

    Dim sSubFolder  As String
    Dim sFolderPath As String

    Dim iFileNo As Integer
    Dim i   As Long
    Dim j   As Long
    Dim k   As Long
    Dim l   As Long

    For i = 1 To 10
        For j = 1 To 5
            For k = 1 To 5
                sSubFolder = "A00\B0000\C0000\"

                Mid$(sSubFolder, 2, 2) = Format$(i, "00")
                Mid$(sSubFolder, 6, 4) = Format$(j + 1000, "0000")
                Mid$(sSubFolder, 12, 4) = Format$(k + 2000, "0000")

                sFolderPath = TARGET_FOLDER_ROOT & sSubFolder

                Call createFolder(sFolderPath)

                For l = 1 To 200
                    iFileNo = FreeFile

                    Open sFolderPath & Format$(l, "000") & ".txt" For Output As iFileNo

                    Close iFileNo
                Next l
            Next k

            sSubFolder = "A00\B0000\"

            Mid$(sSubFolder, 2, 2) = Format$(i, "00")
            Mid$(sSubFolder, 6, 4) = Format$(j + 1000, "0000")

            sFolderPath = TARGET_FOLDER_ROOT & sSubFolder

            For l = 1 To 200
                iFileNo = FreeFile

                Open sFolderPath & "C" & Format$(l, "000") & ".txt" For Output As iFileNo

                Close iFileNo
            Next l
        Next j

        sSubFolder = "A00\"

        Mid$(sSubFolder, 2, 2) = Format$(i, "00")

        sFolderPath = TARGET_FOLDER_ROOT & sSubFolder

        For l = 1 To 200
            iFileNo = FreeFile

            Open sFolderPath & "B" & Format$(l, "000") & ".txt" For Output As iFileNo

            Close iFileNo
        Next l
    Next i

    sFolderPath = TARGET_FOLDER_ROOT

    For l = 1 To 200
        iFileNo = FreeFile

        Open sFolderPath & "A" & Format$(l, "000") & ".txt" For Output As iFileNo

        Close iFileNo
    Next l

    Debug.Print "Done."

End Sub

Public Function createFolder(ByVal sFolderPath As String) As Boolean

    Dim sPath   As String
    Dim lResult As Long

    If Right$(sFolderPath, 1) <> "\" Then
        sPath = sFolderPath & "\"
    Else
        sPath = sFolderPath
    End If

    lResult = MakeSureDirectoryPathExists(sPath)

    createFolder = CBool(lResult <> 0)

End Function
テスト用コード(FindFirstFile版)
Private Const MAX_PATH      As Long = 260&

Private Type FILETIME
    dwLowDateTime   As Long
    dwHighDateTime  As Long
End Type

Private Type WIN32_FIND_DATA
    dwFileAttributes        As Long
    ftCreationTime          As FILETIME
    ftLastAccessTime        As FILETIME
    ftLastWriteTime         As FILETIME
    nFileSizeHigh           As Long
    nFileSizeLow            As Long
    dwReserved0             As Long
    dwReserved1             As Long
    cFileName               As String * MAX_PATH
    cAlternateFileName      As String * 14
End Type

Private Declare Function FindFirstFile Lib "kernel32" Alias "FindFirstFileA" _
       (ByVal Filename As String, _
        ByRef FindData As WIN32_FIND_DATA) As Long

Private Declare Function FindNextFile Lib "kernel32" Alias "FindNextFileA" _
       (ByVal hFind As Long, _
        ByRef FindData As WIN32_FIND_DATA) As Long

Private Declare Function FindClose Lib "kernel32" _
       (ByVal hFind As Long) As Long


Private Const INVALID_HANDLE_VALUE  As Long = -1

'Private Const FILE_ATTRIBUTE_ARCHIVE    As Long = &H20
Private Const FILE_ATTRIBUTE_DIRECTORY  As Long = &H10
'Private Const FILE_ATTRIBUTE_HIDDEN     As Long = &H2
'Private Const FILE_ATTRIBUTE_NORMAL     As Long = &H80
'Private Const FILE_ATTRIBUTE_READONLY   As Long = &H1
'Private Const FILE_ATTRIBUTE_SYSTEM     As Long = &H4
'Private Const FILE_ATTRIBUTE_TEMPORARY  As Long = &H100


Public Sub fffTest()

    Dim colPaths    As Collection
    Dim sgStart     As Single
    Dim sgStop      As Single

    sgStart = Timer

    Set colPaths = New Collection

    Call getFilePaths("C:\Datas\FileListTest\Data", colPaths)

    sgStop = Timer

    Debug.Print Format$(sgStop - sgStart, "0.00") & " sec."

    If colPaths.Count > 0 Then
        Debug.Print colPaths.Count
    Else
        Debug.Print "No files found."
    End If

End Sub

Public Function getFilePaths(ByVal sTargetFolder As String, ByRef colFilePaths As Collection, Optional ByVal sTargetPattern As String = "*") As Long

    Dim sTargetFolderY  As String
    Dim sTargetPathName As String
    Dim hFind       As Long
    Dim fd          As WIN32_FIND_DATA
    Dim sFileName   As String
    Dim sPath       As String

    If Right$(sTargetFolder, 1) <> "\" Then
        '
        sTargetFolderY = sTargetFolder & "\"
    Else
        sTargetFolderY = sTargetFolder
    End If

    sTargetPathName = sTargetFolderY & sTargetPattern

    hFind = FindFirstFile(sTargetPathName, fd)

    If hFind = INVALID_HANDLE_VALUE Then
        getFilePaths = INVALID_HANDLE_VALUE

        Exit Function
    End If

    Do
        'fd.cFileNameは固定長文字列で、本来のファイル名以降の部分にはvbNullCharが埋められているので
        'それらを削除する必要がある
        sFileName = deleteNullChar(fd.cFileName)

        If sFileName = "." Then
            'カレントフォルダは処理しない
            GoTo LOOP_CONTINUE
        ElseIf sFileName = ".." Then
            '親フォルダは処理しない
            GoTo LOOP_CONTINUE
        End If

        If (fd.dwFileAttributes And FILE_ATTRIBUTE_DIRECTORY) = FILE_ATTRIBUTE_DIRECTORY Then
            'フォルダなら再帰処理させるためにパスを生成
            sPath = sTargetFolderY & sFileName
    
            '再帰呼び出し
            Call getFilePaths(sPath, colFilePaths, sTargetPattern)
        Else
            'ファイルならコレクションに追加
            colFilePaths.Add sTargetFolderY & sFileName
        End If

LOOP_CONTINUE:
        '次のファイルが見つからなくなるまでループ
    Loop Until FindNextFile(hFind, fd) = 0

    Call FindClose(hFind)

    getFilePaths = 0

End Function

Private Function deleteNullChar(ByVal sSource As String) As String

    Dim lPos    As Long

    lPos = InStr(sSource, vbNullChar)

    If lPos > 0 Then
        deleteNullChar = Left$(sSource, lPos - 1)
    Else
        deleteNullChar = sSource
    End If

End Function
テストコード(Dir版)

ことりちゅんさん(id:Kotori-ChunChun)のところの処理を流用。

但し、test_GetFileListTmpfileの時間計測部分と結果表示部分のみアレンジ。

  • Time→Timer
  • 結果表示フォーマット 0.00 sec.
  • ファイル数表示追加
Const WshHide = 0               '非表示
Const WshNormalFocus = 1        '通常サイズ

Function TrimEx(TargetString As String, Optional TrimLeft As Boolean = True, Optional TrimRight As Boolean = True) As String
    Dim reg_pattern As String

    If TrimLeft And TrimRight Then
        reg_pattern = "(?:^\s+|\s+$)"
    ElseIf TrimLeft Then
        reg_pattern = "^\s+"
    ElseIf TrimRight Then
        reg_pattern = "\s+$"
    Else
        TrimEx = TargetString
        Exit Function
    End If

    With CreateObject("VBScript.RegExp")
        .Pattern = reg_pattern
        .IgnoreCase = False
        .Global = True
        TrimEx = .Replace(TargetString, "")
    End With
End Function

Function GetFileListTmpfile(FolderLocation As String, Optional ShowCmdWindow As Boolean = True) As String()
    GetFileListTmpfile = Split(vbNullString)

    Dim tmpfile As String
    Dim filelist() As String

    With CreateObject("Scripting.FileSystemObject")
        If Not .FolderExists(FolderLocation) Then
            Debug.Print "* Error * Folder(" & FolderLocation & ") not found"
            Exit Function
        End If
        Do
            tmpfile = .GetSpecialFolder(2) & "\" & .GetTempName
        Loop While .FileExists(tmpfile)
    End With

    CreateObject("Wscript.Shell").Run "cmd /U /C dir /S /B /A-D """ & FolderLocation & """ > " & tmpfile, _
                                        IIf(ShowCmdWindow, WshNormalFocus, WshHide), True

    'コマンドプロンプト表示がONで強制終了(中断)された時の対策
    With CreateObject("Scripting.FileSystemObject")
        If Not .FileExists(tmpfile) Then
            Exit Function
        End If
    End With

    With CreateObject("ADODB.Stream")
        .Charset = "Unicode"
        .Open
        .LoadFromFile tmpfile
        filelist = Split(TrimEx(.ReadText), vbCrLf)
        .Close
    End With

    Kill tmpfile

    GetFileListTmpfile = filelist
End Function

Sub test_GetFileListTmpfile()
    Dim t As Single
    t = Timer
    Dim V As Variant
    'エクセルが応答不能になる。使用する場合は超注意
    'Application.Interactive = False
    V = GetFileListTmpfile("C:\Datas\FileListTest\Data", True)
    'Application.Interactive = True
    Debug.Print Format$(Timer - t, "0.00") & "sec."

    Debug.Print UBound(V) - LBound(V) + 1
End Sub
実行結果
call fffTest
0.31 sec.
 62200 

call test_GetFileListTmpfile
7.27sec.
 62200 

やっぱりAPI速いっすねぇ。1桁違います。
  件数は同じですけど、検索順は違いますよ。詳細は省きますけど・・・

ちなみに、Visual Studio 2017 C++で同様の処理を実行してみたら・・・

さらにその半分以下になりました。
C++のソースは省略。VBAのソースのCollectionがVectorになったくらいだから、誰も見ないでしょ?

おまけ

FindFirstFile、FindNextFileのファイルの検索順は、ファイルシステムに依存があります。

とりあえず、こんなコードで

Public Sub fffTest2()

    Dim colPaths    As Collection
    Dim v           As Variant

    Dim sFolderPath     As String
    Dim sFileName(4)    As String
    Dim iFileNo As Integer
    Dim i       As Long

    sFolderPath = "D:\FFF_Test\"

    Call createFolder(sFolderPath)

    On Error Resume Next

    Kill sFolderPath & "*"

    On Error GoTo 0

    sFileName(0) = "003.txt"
    sFileName(1) = "005.txt"
    sFileName(2) = "004.txt"
    sFileName(3) = "002.txt"
    sFileName(4) = "001.txt"

    For i = 0 To 4
        iFileNo = FreeFile

        Open sFolderPath & sFileName(i) For Output As iFileNo

        Close iFileNo

        Call Sleep(3000)
    Next i

    Set colPaths = New Collection

    Call getFilePaths("D:\FFF_Test", colPaths)

    If colPaths.Count > 0 Then
        Debug.Print "FileSystem:" & getFileSystemName("D:\")

        For Each v In colPaths
            Debug.Print v
        Next v

        Debug.Print "Done."
    Else
        Debug.Print "No files found."
    End If

End Sub

実行結果は以下の通り。
Dドライブは、USBメモリで、ファイルシステムの違いは、USBメモリの差し替えで実現しています。
(これのためだけに、1個のUSBメモリNTFSでフォーマットしました。)

call fffTest2
FileSystem:exFAT
D:\FFF_Test\003.txt
D:\FFF_Test\005.txt
D:\FFF_Test\004.txt
D:\FFF_Test\002.txt
D:\FFF_Test\001.txt
Done.

call fffTest2
FileSystem:NTFS
D:\FFF_Test\001.txt
D:\FFF_Test\002.txt
D:\FFF_Test\003.txt
D:\FFF_Test\004.txt
D:\FFF_Test\005.txt
Done.

call fffTest2
FileSystem:FAT32
D:\FFF_Test\003.txt
D:\FFF_Test\005.txt
D:\FFF_Test\004.txt
D:\FFF_Test\002.txt
D:\FFF_Test\001.txt
Done.

NTFSファイル名順
exFatFAT32は、ファイル作成順
となってます。
厳密には、CollectionをFor Eachで出力している時点で駄目なんだけど、簡易的な確認なので、まぁ良しとしてしまえ。

ここにも

The order in which this function returns the file names is dependent on the file system type. With the NTFS file system and CDFS file systems, the names are usually returned in alphabetical order. With FAT file systems, the names are usually returned in the order the files were written to the disk, which may or may not be in alphabetical order. However, as stated previously, these behaviors are not guaranteed.

と書いてありました。


さらに、Dir版でも試してみたところ、FindFirstFile版と同じ結果になりました。
これは、ちょっとした発見でした。





あぁ、なんか話が全然別の方に行ってる・・・

Excelのワークシート名取得 ADOでOpenSchema と Workbooks.Open を比べてみた

ADOというとデータベースに使うというイメージが強いような感じがしますが、Excelに対しても使えます。

Excelのワークブックを指定して、ワークシート名の一覧取得を、ADOの使用有無で行ってみます。

使用環境

Windows 10 Home
Excel 2013
CPU:古い(恥ずかしいので秘密)
メモリ:少し(同上)

使用する環境によっては、参照設定や接続文字列が変えないといけない場合がありえますのでご注意ください。

ADOを使用する場合

参照設定

最初に、参照設定を行います。
Microsoft ActiveX Datta Object 6.1 Library
にチェックを入れておきます。

モジュールレベルの変数

コネクション変数をモジュールレベルで宣言しておきます。
モジュール内で、共用するためです。

Private cn_     As ADODB.Connection
接続
Public Function connectDB(ByVal sDBPath As String, Optional ByVal bExistsHeaderRow As Boolean = True) As Long

    Dim sExistsHeaderRow    As String
    Dim sConnectionString   As String

    If bExistsHeaderRow = True Then
        sExistsHeaderRow = "Yes"
    Else
        sExistsHeaderRow = "No"
    End If

'    HDR=Yes===>先頭行を列名とみなす
'    『IMEX=1』とはドライバにインポート モードを使用するように指示して
'    混在データが文字列に変換されることを強制するものらしい
    sConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;" & _
                        "Data Source=" & sDBPath & ";" & _
                        "Extended Properties=""Excel 12.0 Xml;" & _
                                "HDR=" & sExistsHeaderRow & ";" & _
                                "IMEX=1;"""

On Error GoTo ERR_CONNECT_DB

    If cn_ Is Nothing Then
        Set cn_ = New ADODB.Connection
    End If

    If cn_.State <> adStateOpen Then
        cn_.Open sConnectionString
    End If

    connectDB = 0

    Exit Function

ERR_CONNECT_DB:
    Debug.Print Err.Number & ":" & Err.Description

    connectDB = Err.Number

End Function
切断
Public Sub disconnectDB()

    If Not cn_ Is Nothing Then
        If cn_.State <> adStateClosed Then
            cn_.Close
        End If

        Set cn_ = Nothing
    End If

End Sub
シート名取得
Public Function getSheetNames(ByRef sSheetNames() As String) As Long

    Dim rs          As ADODB.Recordset
    Dim fld         As ADODB.Field
    Dim lCount      As Long
    Dim sTableName  As String

    getSheetNames = 0

On Error GoTo ERR_GET_SHEET_NAMES

    Set rs = cn_.OpenSchema(adSchemaTables)

    lCount = 0

    ReDim sSheetNames(lCount)

    Do Until (rs.EOF)
        sTableName = rs.Fields("TABLE_NAME").Value

        If Right$(sTableName, 1) = "$" Then
            '定義された名前には、"$"が付かない
            'オートフィルタや、印刷範囲の指定等により、"_xlnm#"を含む名前が出てくる場合があるので
            'それらは無視する

            '末尾の"$"を除去
            sTableName = Left$(sTableName, Len(sTableName) - 1)

            ReDim Preserve sSheetNames(lCount)

            sSheetNames(lCount) = sTableName

            lCount = lCount + 1
        End If

        rs.MoveNext
    Loop

ERR_GET_SHEET_NAMES:
    If Err.Number <> 0 Then
        getSheetNames = -Err.Number
    Else
        getSheetNames = lCount
    End If

    If Not rs Is Nothing Then
        If rs.State <> adStateClosed Then
            rs.Close
        End If

        Set rs = Nothing
    End If
 
End Function
呼び出し
Public Sub getTargetSheetNameADO()

    Dim sTargetPath     As String
    Dim sSheetNames()   As String
    Dim i               As Long

    Dim sgStart         As Single
    Dim sgStop          As Single

    sgStart = Timer

    'シート名を取得するファイルのパス
    sTargetPath = "C:\Datas\大量データ.xlsm"

    Call connectDB(sTargetPath)

    Call getSheetNames(sSheetNames)

    For i = 0 To UBound(sSheetNames)
        Debug.Print sSheetNames(i)
    Next i

    Debug.Print ""

    Call disconnectDB

    sgStop = Timer

    Debug.Print "ADO     : " & Format$(sgStop - sgStart, "0.00 sec.")

End Sub

呼び出すファイルがマクロありなのは気にしないでください。

ADOを使用しない(Workbooks.Open + For Each)

こっちは、接続とか切断とかないので、いきなり呼び出して処理

Public Sub getTargetSheetNameForEach()

    Dim wb              As Workbook
    Dim ws              As Worksheet
    Dim sTargetPath     As String
    Dim i               As Long

    Dim sgStart         As Single
    Dim sgStop          As Single

    sgStart = Timer

    Application.ScreenUpdating = False

    'シート名を取得するファイルのパス
    sTargetPath = "C:\Datas\大量データ.xlsm"

    Set wb = Workbooks.Open(Filename:=sTargetPath, ReadOnly:=True)

    Debug.Print "  Opend : " & Format$(Timer - sgStart, "0.00 sec.")

    For Each ws In wb.Worksheets
        Debug.Print ws.Name
    Next ws

    Debug.Print ""

    wb.Close
    Set wb = Nothing

    Application.ScreenUpdating = True

    sgStop = Timer

    Debug.Print "ForEach : " & Format$(sgStop - sgStart, "0.00 sec.")

End Sub

サンプルデータ1

50万件のデータが入ったやつ。
ファイルサイズは54MBくらい。

実行結果

call getTargetSheetNameADO
Data
FilterCondition
Key
Result
Sheet1
総合結果

ADO     : 0.09 sec.

call getTargetSheetNameForEach
  Opend : 8.80 sec.
Data
FilterCondition
Key
Result
総合結果
Sheet1

ForEach : 9.25 sec.

サンプルデータ2

6万件のデータが入ったやつ。
ファイルサイズは1.1MBくらい。

実行結果

call getTargetSheetNameADO
Sheet1
Sheet2

ADO     : 0.09 sec.

call getTargetSheetNameForEach
  Opend : 0.77 sec.
Sheet1
Sheet2

ForEach : 1.08 sec.

ADOの圧勝
サンプル1の方では、2桁違うよ。
サンプル2でも、1桁違う。

Workbooks.Openがボトルネック。ファイルサイズが大きいほど効いてくる。
ADO OpenSchemaは、ファイルサイズに依存していない。

ちなみに、サンプル1では、Sheet1は非表示になっていますが、どちらも取得できています。
データの並びが違うのがちょっと気になる・・・

最後に

お気軽に取得したいのなら、コードが簡単な
 Workbooks.Open + For Each
速度絶対優先なら、コードが面倒くさくても
 ADO OpenSchema
となるんでしょうね。

2019/4/8追記
今回の例は、シート名の取得のみの結果です。
ワークシート上のデータを取得するとなると、話は変わってきて、ADOのレコードセットでも、データ数に応じて、それなり(Workbooks.Openと同じくらい?のイメージ?)に時間がかかります。
興味のある人は、ご自分で試してみてください。


エラー発生時の復帰値が統一性がない・・・
Functionにしてるのに復帰値使ってないし・・・
何も考えないでコーディングしてるのがバレバレ

VBAで乱数を使って、意味不明(?)な文字列を生成する・・・コケヲチヰヱタトンシ

いろいろな処理をするコードを書いていると、テスト用の文字列データが欲しくなる時があります。
5個とか10個位なら適当にキーボードから入力して作ってもいいのですが、数百とか数万とか、それ以上となるととても手入力では無理なので、どこかから探してくるか、あるいは自分で作るかということになります。

内容はどうでもいいから、とにかくデータが欲しいような時には、乱数を使って作ってしまいましょう。

基本的な事

乱数を使用するにあたって、必要なのは、以下の2つです。
Rnd関数
Randomize ステートメント

Rnd関数の使い方は、ここでは省略します。

特定の範囲の整数を生成するには以下のような計算を行います。

Randomize
ランダム整数 = Int((最大値 - 最小値 + 1) * Rnd) + 最小値

最大値、最小値のいずれも整数を指定します。
負の数を指定しても大丈夫ですが
最小値<最大値
はmustです。

数字の場合

前述の式を使って取得した値をCStr関数またはFormat関数で文字列変換します。
固定桁数にし、前に0を付けたい場合には、

? Format$(123,String$(7,"0"))
0000123
? Right$(String$(6,"0") & CStr(123),7)
0000123

こんな感じで出来ます。
注)元データの桁数が指定桁を超えた場合(上の例では7桁)、上記の2例は結果が異なりますので、ご注意ください。

アルファベットの場合

最初の式で得られるのは整数なので、これをアルファベットに変換しなければいけません。
この変換には、Chr関数を使います。
Chr関数には、文字コードを渡しますが、アルファベットの場合、ASCIIコードを指定すればOKです。
ASCIIコードは、こちらで確認できます。

大文字、小文字それぞれの範囲は、以下のようになることがわかります。

文字 文字コード
A &H41
Z &H5A
a &H61
z &h7A

イミディエイトウィンドウで確認してみると

? Chr(&H41)
A
? Chr(&H5A)
Z
? Chr(&H61)
a
? Chr(&H7A)
z

となります。

以上を踏まえて、コードは以下のようになります。

Public Function creaeStringAlpha(ByVal lLength As Long, ByVal useUpperCase As Boolean) As String

    Dim iBeginCode  As Integer
    Dim iEndCode    As Integer
    Dim sResult As String
    Dim i   As Long

    If useUpperCase Then
        iBeginCode = Asc("A")
        iEndCode = Asc("Z")
    Else
        iBeginCode = Asc("a")
        iEndCode = Asc("z")
    End If

    Randomize

    For i = 1 To lLength
        sResult = sResult & Chr(Int((iEndCode - iBeginCode + 1) * Rnd) + iBeginCode)
    Next i

    creaeStringAlpha = sResult

End Function

useUpperCase に Trueを指定すれば大文字で、Falseを指定すれば小文字で生成します。
大小混在としたい場合には、開始コードをAsc("A")、終了コードをAsc("z")とした上で、一旦生成した文字(文字ではないですよ)が、A~Z、a~zであるかを確認し、OKなら結合、駄目なら再度文字生成し直しというような処理にすればできます。(下記の英数混在を参照)

開始の文字コードと終了の文字コード&H○○と直接書いてもいいのですが、あとから見て「なんだこれ?」となるのが(私の場合)普通なので、あえてコメント無しでもわかるような書き方をしています。

実行例(イミディエイトウィンドウ)
for i=1 to 5:? creaeStringAlpha(10,True):next
YPMLCFTQXW
WOIGQGMJCU
ODWMWPUGGP
UJOWHJNLLM
FKPAEYZHKK

for i=1 to 5:? creaeStringAlpha(10,false):next
rkwqfoyvpr
nyzkqsiren
hygqzyzlyl
djbrscyzxh
fpenxaqfrd

英数混在の場合

先程ちょっと出てきた「対象外のコードを除外」をするとこんな感じで出来ます。
ASCIIコードは、
数字 < アルファベット大文字 < アルファベット小文字
なので、
最小文字コードは、数字の最小:Asc("0")
最大文字コードは、アルファベット小文字の最大:Asc("z")
を使用します。

Public Function creaeStringAlphaNum(ByVal lLength As Long) As String

    Dim iBeginCodeN     As Integer
    Dim iEndCodeN       As Integer
    Dim iBeginCodeAU    As Integer
    Dim iEndCodeAU      As Integer
    Dim iBeginCodeAL    As Integer
    Dim iEndCodeAL      As Integer
    Dim iCode   As Integer
    Dim sResult As String
    Dim i       As Long

    iBeginCodeN = Asc("0")
    iEndCodeN = Asc("9")
    iBeginCodeAU = Asc("A")
    iEndCodeAU = Asc("Z")
    iBeginCodeAL = Asc("a")
    iEndCodeAL = Asc("z")

    Randomize

    For i = 1 To lLength
        Do While True
            iCode = Int((iEndCodeAL - iBeginCodeN + 1) * Rnd) + iBeginCodeN

            Select Case iCode
            Case iBeginCodeN To iEndCodeN
                Exit Do
            Case iBeginCodeAU To iEndCodeAU
                Exit Do
            Case iBeginCodeAL To iEndCodeAL
                Exit Do
            End Select
        Loop

        sResult = sResult & Chr(iCode)
    Next i

    creaeStringAlphaNum = sResult

End Function
実行例(イミディエイトウィンドウ)
for i=1 to 5:? creaeStringAlphaNum(20):next
sC2TMIaUKFpJgxGLCjV8
UDnhkqy3OOG6Lk4QdwZC
5AldQ84Ug33RKwTtbIyj
W1Wd1iifx2qrJ66IxNp9
Lj31yD74w0In3i8zqEn2

ひらがなの場合

基本的には、アルファベットの時と同じですが、ひらがなの場合文字コードがASCIIコードではなく、ユニコードを使用することになるのでChr関数がChrW関数に、Asc関数がAscW関数を使用することになります。

ユニコードのひらがなは、
こちらを見てください。
VBAでは、一部の文字(&H3094以降の一部)が使用できないため、それらの文字は除外する必要があります。

コードは、

Public Function creaeStringKana(ByVal lLength As Long) As String

    Dim iBeginCode  As Integer
    Dim iEndCode    As Integer
    Dim sResult As String
    Dim i   As Long

    '厳密には違うが、VBAで対応できないので、対応可能範囲に限定する
    iBeginCode = AscW("ぁ")
    iEndCode = AscW("ん")

    Randomize

    For i = 1 To lLength
        sResult = sResult & ChrW(Int((iEndCode - iBeginCode + 1) * Rnd) + iBeginCode)
    Next i

    creaeStringKana = sResult

End Function
実行例(イミディエイトウィンドウ)
for i=1 to 5:? creaeStringKana(10):next
ふだわんぇよずぽへゎ
ろすいらうぱこだぶよ
ゎゆめぐほばぽきぴぺ
わたのぞらぼづきそあ
をむゑろもべぬぶぼよ

カタカナの場合

ひらがなの時と同じですね。コードの範囲が変わるだけです。
文字コードこちらになります。

コードは、

Public Function creaeStringKatakana(ByVal lLength As Long) As String

    Dim iBeginCode  As Integer
    Dim iEndCode    As Integer
    Dim sResult As String
    Dim i   As Long

    '厳密には違うが、VBAで対応できないので、対応可能範囲に限定する
    iBeginCode = AscW("ァ")
    iEndCode = AscW("ヶ")

    Randomize

    For i = 1 To lLength
        sResult = sResult & ChrW(Int((iEndCode - iBeginCode + 1) * Rnd) + iBeginCode)
    Next i

    creaeStringKatakana = sResult

End Function
実行例(イミディエイトウィンドウ)
for i=1 to 5:? creaeStringKatakana(10):next
ヶェコゼヂクズザシヤ
ゥゼゼキピォンナイベ
ヲヒユテビゲネモツノ
ズェャヨベリコデグダ
コケヲチヰヱタトンシ

"ヰヱ"なんて誰が使うんだよっ! orz

2019/4/1追記
不連続の多い文字群の場合、キーコードを使用しないで、予め使用可能な文字を配列に1文字ずつ突っ込んで全部結合してから、rndで発生させた乱数で配列のインデックスを指定してもいいのかもしれない全部結合してから、rndで発生させた乱数を使って、midで抜き出してもいいのかもしれない。
これなら、半角、全角混在でも簡単(?)に出来そう。

2019/4/2追記
昨日のアイデアを元に、簡単なパスワード生成関数を作ってみた。

Public Function createPassword(ByVal lLength As Long) As String

    Const USABLE_CHARS  As String = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%(){}"

    Dim lPos        As Long
    Dim sPassword   As String
    Dim i           As Long

    Randomize

    For i = 1 To lLength
        lPos = Int(Len(USABLE_CHARS) * Rnd) + 1

        sPassword = sPassword & Mid$(USABLE_CHARS, lPos, 1)
    Next i

    createPassword = sPassword

End Function

実行結果

for i=1 to 15:? createPassword(10):next
lG%wfn9kA7
#VkE0X!xN}
pAFE6hQHhK
}EhwtPoBEE
nZZeeiDYC4
Ivrs96dxq%
0)NsDNg#8v
f8JG3rd07o
hamu)ojZXd
Y%Zwlxh4VT
dCgkztKKFK
VU6(y!xdfB
ba!2Pu6bs4
Wi4ei!Gbc)
w{oG{ZZ(ov

指定日が、月内で第何度目の何曜日かを求める(VBA)

最近、表題の処理を2件ほど、はてなブログで見かけた。
いずれの処理も、月始めから指定日までループ処理をしていたが、この手の処理は1週間(7日)の周期性があるので、それを利用してループ処理なしで求めてみた。

thom.hateblo.jp
b004nws862zx.hatenablog.com

抑えておくべきこと

第1の日曜日から土曜日は、毎月1日から7日にある。
第2の日曜日から土曜日は、毎月8日から15日にある。
以下同様

従って、

1~7
8~14
15~21
22~28
29~31

となる。

7日の周期で、週が1加算されていくので、7で割った商を使えばよい。
ただし、そのまま日を7で割ってしまっては求めたい値(週)は得られない。
7で割って商が1となる整数は、7から13である。そこで1から7を7から13に補正するため、6を加算した上で、7で割ることで目的の値が得られる。
これは商が2以上の場合でも同じである。

コード

Public Function 第N○曜日(ByVal dtTargetDate As Date) As String

    Const DAY_OF_WEEK As String = "日月火水木金土"

    Dim lNthWeek        As Long
    Dim sWeek           As String

    '第N
    lNthWeek = (Day(dtTargetDate) + 6) \ 7

    '曜日
    sWeek = Mid$(DAY_OF_WEEK, Weekday(dtTargetDate), 1)

    第N○曜日 = "第" & CStr(lNthWeek) & sWeek & "曜日"

End Function

第何週目の何曜日なのかの場合

基本は上の例と同じ。
違うのは、日に対する補正値。
上の例では、単純に6を加算すればよかったが、この場合には、第1土曜日が13(7で割って商が1になる最大の整数)になるようにすればよい。

しかし、第1土曜日が何日かによって補正値が変わってくる。

1日の曜日 第1土曜日の日 第1土曜日の日を13にするために必要な値
10
11
12

となることから、
第1土曜日の日+5+Weekday(1日の日付)
とすることで13を得る事が出来るので、目的の補正値が得られる。

コード

Public Function 第N週の○曜日(ByVal dtTargetDate As Date) As String

    Const DAY_OF_WEEK  As String = "日月火水木金土"

    Dim dtFirstDate     As Date
    Dim lCorrection     As Long
    Dim lNthWeek        As Long
    Dim sWeek           As String

    '指定日の月の1日
    dtFirstDate = DateAdd("d", -Day(dtTargetDate) + 1, dtTargetDate)

    '第N週計算のための補正値
    lCorrection = Weekday(dtFirstDate) + 5

    '第N週
    lNthWeek = (Day(dtTargetDate) + lCorrection) \ 7

    '曜日
    sWeek = Mid$(DAY_OF_WEEK, Weekday(dtTargetDate), 1)

    第N週の○曜日 = "第" & CStr(lNthWeek) & "週の" & sWeek & "曜日"

End Function

実行結果

確認用コード

Public Sub 第○曜日テスト()

    Dim d   As Date

    For d = #3/1/2019# To #3/31/2019#
        Debug.Print Format$(d, "yyyy/mm/dd");
        Debug.Print vbTab;
        Debug.Print 第N週の○曜日(d);
        Debug.Print vbTab;
        Debug.Print 第N○曜日(d)
    Next d

End Sub

結果

call 第○曜日テスト
2019/03/01 第1週の金曜日 第1金曜日
2019/03/02 第1週の土曜日 第1土曜日
2019/03/03 第2週の日曜日 第1日曜日
2019/03/04 第2週の月曜日 第1月曜日
2019/03/05 第2週の火曜日 第1火曜日
2019/03/06 第2週の水曜日 第1水曜日
2019/03/07 第2週の木曜日 第1木曜日
2019/03/08 第2週の金曜日 第2金曜日
2019/03/09 第2週の土曜日 第2土曜日
2019/03/10 第3週の日曜日 第2日曜日
2019/03/11 第3週の月曜日 第2月曜日
2019/03/12 第3週の火曜日 第2火曜日
2019/03/13 第3週の水曜日 第2水曜日
2019/03/14 第3週の木曜日 第2木曜日
2019/03/15 第3週の金曜日 第3金曜日
2019/03/16 第3週の土曜日 第3土曜日
2019/03/17 第4週の日曜日 第3日曜日
2019/03/18 第4週の月曜日 第3月曜日
2019/03/19 第4週の火曜日 第3火曜日
2019/03/20 第4週の水曜日 第3水曜日
2019/03/21 第4週の木曜日 第3木曜日
2019/03/22 第4週の金曜日 第4金曜日
2019/03/23 第4週の土曜日 第4土曜日
2019/03/24 第5週の日曜日 第4日曜日
2019/03/25 第5週の月曜日 第4月曜日
2019/03/26 第5週の火曜日 第4火曜日
2019/03/27 第5週の水曜日 第4水曜日
2019/03/28 第5週の木曜日 第4木曜日
2019/03/29 第5週の金曜日 第5金曜日
2019/03/30 第5週の土曜日 第5土曜日
2019/03/31 第6週の日曜日 第5日曜日


こちらも参考になります。(かなりオススメ)
www.waenavi.com

DictionaryのItemに格納したExcelのRangeは更新可能か?

以前の記事でDictionaryのItemに配列を格納して、後から更新しようとして出来なかったが、懲りずに今度はRangeを格納して試してみた。

z1000s.hatenablog.com

結論から言うと、今回は更新可能でした!!!

  1. DictionaryのItemを更新することで、ワークシートの値も更新されます。
  2. また、ワークシートの対象Rangeのセルの値を更新すると、DictionaryのItemを参照しても更新された値を取得できます。

サンプルコード

Public Sub dictionaryItemUpdate()

    Const TARGET_SHEET_NAME As String = "Sample"
    Const KEY_ROW           As Long = 2
    Const TARGET_COLUMNS    As Long = 6

    Dim dicRange    As Dictionary
    Dim sLineBefore As String
    Dim sLine       As String
    Dim i           As Long

    Set dicRange = New Dictionary

    With Worksheets(TARGET_SHEET_NAME)
        '現在のセルのデータを取得、表示用に結合
        For i = 1 To TARGET_COLUMNS
            sLineBefore = sLineBefore & vbTab & .Cells(KEY_ROW, i).Value
        Next i

        '現在のセルのデータを表示
        Debug.Print "基準となるセルデータ"
        Debug.Print Mid$(sLineBefore, 2) & vbCrLf

        '対象となるRangeをDictionaryに追加
        dicRange.Add 2, .Range(.Cells(KEY_ROW, 1), .Cells(KEY_ROW, TARGET_COLUMNS))

        'Dictionaryに格納されているRangeから、データを取得、表示用に結合
        For i = 1 To TARGET_COLUMNS
            sLine = sLine & vbTab & dicRange(KEY_ROW).Parent.Cells(KEY_ROW, i).Value
        Next i

        'Dictionaryに格納されているデータを表示
        Debug.Print "Dictionaryに格納されているデータ(更新前)"
        Debug.Print Mid$(sLine, 2) & vbCrLf

        '格納したRange内のセルの値を更新
        Debug.Print "ワークシートの値を更新"
        Debug.Print "C2(更新前):" & .Cells(KEY_ROW, 3).Value
        .Cells(2, 3).Value = "----- " & .Cells(KEY_ROW, 3).Value & " -----"
        Debug.Print "C2(更新後) :" & .Cells(KEY_ROW, 3).Value & vbCrLf

        'DictionaryのItemを更新
        Debug.Print "DictionaryのItemを更新"
        Debug.Print "D2セル相当の値を更新(更新前):" & dicRange(KEY_ROW).Parent.Cells(KEY_ROW, 4).Value
        dicRange(KEY_ROW).Parent.Cells(KEY_ROW, 4).Value = "+++++ " & dicRange(KEY_ROW).Parent.Cells(KEY_ROW, 4).Value & " +++++"
        Debug.Print "D2セル相当の値を更新(更新後):" & dicRange(KEY_ROW).Parent.Cells(KEY_ROW, 4).Value & vbCrLf

        sLine = ""

        'Dictionaryに格納されているRangeから、データを取得、表示用に結合
        For i = 1 To TARGET_COLUMNS
            sLine = sLine & vbTab & dicRange(KEY_ROW).Parent.Cells(KEY_ROW, i).Value
'            sLine = sLine & vbTab & dicRange(KEY_ROW).Cells(1, i).Value
        Next i

        Debug.Print "Dictionaryに格納されているデータ(更新後)"
        Debug.Print "更新前:" & Mid$(sLineBefore, 2)
        Debug.Print "更新後:" & Mid$(sLine, 2)

        sLine = ""

        '最終のセルのデータを取得、表示用に結合
        For i = 1 To TARGET_COLUMNS
            sLine = sLine & vbTab & .Cells(KEY_ROW, i).Value
        Next i

        '最終のセルのデータを表示
        Debug.Print "最終のセルデータ"
        Debug.Print Mid$(sLine, 2) & vbCrLf
    End With

End Sub

サンプルコードを試すときは、参照設定忘れると動きませんので・・・

実行結果

call dictionaryItemUpdate
基準となるセルデータ
1 4820831 1256948 AFPKAUNYAI gKoo0fpjSf ゑたさえんぐそつぢろ


Dictionaryに格納されているデータ(更新前)
1 4820831 1256948 AFPKAUNYAI gKoo0fpjSf ゑたさえんぐそつぢろ


ワークシートの値を変更
C2(更新前):1256948
C2(更新後) :----- 1256948 -----


DictionaryのItemを更新
D2セル相当の値を更新(更新前):AFPKAUNYAI
D2セル相当の値を更新(更新後):+++++ AFPKAUNYAI +++++


Dictionaryに格納されているデータ(更新後)
更新前:1 4820831 1256948 AFPKAUNYAI gKoo0fpjSf ゑたさえんぐそつぢろ
更新後:1 4820831 ----- 1256948 ----- +++++ AFPKAUNYAI +++++ gKoo0fpjSf ゑたさえんぐそつぢろ
最終のセルデータ
1 4820831 ----- 1256948 ----- +++++ AFPKAUNYAI +++++ gKoo0fpjSf ゑたさえんぐそつぢろ

ポイント

ItemとしてRangeを格納したので、dicRange(KEY_ROW)がRangeを返します。
なので、セルの値は、

dicRange(KEY_ROW).Parent.Cells(KEY_ROW, i).Value

のように取得していますが、Parentの有無でCellsのアドレス指定が変わってきます。
今回の例でC2のセルの値を取得したい場合、

Parentがある場合 dicRange(KEY_ROW).Parent.Cells(2, 3).Value
Parentがない場合 dicRange(KEY_ROW).Cells(1, 3).Value

と指定する必要があります。
Parentがある場合には、普段使用している行、列の指定となります。
Rangeに対するParent(親)なので、ParentはWorksheetとなるためです。

? typename(dicRange(KEY_ROW).Parent)
Worksheet

として確かめることが出来ます。

一方Parentがない場合には、
ワークシート全体で見れば2行目のセルですが、Dictionaryに格納したRangeの左上のセル(A2:ワークシート基準でのCells(2,1))を1行目、1列目とみなした相対的な行、列として指定する必要があります。従って、同じ2行目なので行を「1」としなければいけません。

今回のサンプルでは、A列から始まるRangeを使用したのですが、B列以降から始まるRangeを指定した場合には、列であっても同様のことが言えます。

Parentの有無、どちらの方法でも出来ますので、どのような処理を行うかによって使い分ければよいかと思います。

最後に

うまく使えばデータベースのように、キーを指定してレコードを取得するといった使い方とかに応用できそう?
更新もできるし・・・
あとは、背景の塗りつぶしも出来ましたので、一通りのことは出来るのではないかと思われます。

Unionで纏められたRange内のセルにVBAでアクセスする(Excel)

あまり使う事はないような気がするが、複数のセル範囲の集合をひとつのRangeとした時、そのRange内のデータにどうやってアクセスするのか?やってみました。
複数のセル範囲が不連続の場合のアクセス方法を調べることが目的です。

Unionで纏める記事はそれなりにあるようですが、纏めた後にどうやってアクセスするのか書かれているサイトは多くはないようです。

まずは、以下のようなデータを用意し、赤で囲まれた部分と青で囲まれた部分を対象のRangeとすることとします。

f:id:Z1000S:20190301221055j:plain
サンプルデータ

結論から書くと

対象Range.Areas(n).Cells(row, col)

といった指定をすることで出来ます。

ポイントとなるのは、Range.Areas プロパティです。
これは、「複数領域選択範囲内のすべての範囲を表す**Areas** コレクションを返します。 読み取り専用です。」と説明されています。

複数のセル範囲の集合をひとつのRangeとして設定する

Unionメソッドを使用し、下記のように使用します。

Set r = Union(Sheet1.Range("B2:D4"), Sheet1.Range("F3:G4"))

Range("B2:D4") <===で囲まれた範囲
Range("F3:G4") <===で囲まれた範囲

セル範囲数を調べる

使うのはAreas.Count プロパティです。

下記コードでは、

r.Areas.Count

の部分が該当し、今回の場合、2が返ってきます。

各セル範囲のアドレスを調べる

使うのは、Range.Address プロパティです。
セル範囲は、Areasに1から始まるインデックスにより指定します。
下記コードでは、

r.Areas(k).Address(False, False)

の部分が該当します。
今回の場合、
で囲まれた範囲がArea(1)
で囲まれた範囲がArea(2)
となり、
r.Areas(1).Address(False, False)は
B2:D4
r.Areas(2).Address(False, False)は
F3:G4
が返ります。
Unionで指定した順番に返ってくるようです。(そうでないと困ります。)
ちなみに、Unionした全体の範囲は

r.Address(False, False)

で取得でき、
B2:D4,F3:G4
と返ってきます。

各セル範囲の行数、列数を調べる

r.Areas(n)が通常のRangeを返すので、それぞれ以下のように指定して取得することが出来ます。

r.Areas(n).Rows.Count
r.Areas(n).Columns.Count

セルの値を取得する

Range.Cells プロパティを使用して、指定したセルにアクセスできます。

r.Areas(n).Cells(i, j).Value

ここで、Cellsに指定する行、列の値は、対象Range(各Area(n)とも)の左上のセルを1行目、1列目とした相対的指定になるようで、Cells(1,1)といった指定を行うようです。
これに関しては、サンプルコードと実行結果を見ていただいた方がわかりやすいと思います。
さらに、対象Rangeの行数、列数を超える値をCellsに指定してもエラーにはならないようです。
指定方法を間違えると、対象Rangeに含まれないセルにアクセスすることになるので注意が必要です。
例えば、

? r.Areas(1).Cells(1, 4).Value

とすると
E2
と出力され、Area(1)の列は、B~Dであるにもかかわらず、Area(1)には含まれないセルE2が参照することが出来てしまいます。

サンプルコード

Public Sub AccessToUnionRange()

    Dim r   As Range
    Dim sLine   As String
    Dim i   As Long
    Dim j   As Long
    Dim k   As Long

    '複数のRangeをUnionで纏める
    Set r = Union(Sheet1.Range("B2:D4"), Sheet1.Range("F3:G4"))

    Debug.Print "エリア数:" & r.Areas.Count
    Debug.Print "全エリア:" & r.Address(False, False)

    For k = 1 To r.Areas.Count
        Debug.Print "エリア" & CStr(k) & ":" & r.Areas(k).Address(False, False)
    Next k

    For k = 1 To r.Areas.Count
        Debug.Print "----- Area" & CStr(k) & " -----"

        For i = 1 To r.Areas(k).Rows.Count
            sLine = CStr(i) & ":"

            For j = 1 To r.Areas(k).Columns.Count
                sLine = sLine & vbTab & r.Areas(k).Cells(i, j).Value
            Next j

            Debug.Print sLine
        Next i
    Next k

End Sub

実行結果

Call AccessToUnionRange
エリア数:2
全エリア:B2:D4,F3:G4
エリア1:B2:D4
エリア2:F3:G4
----- Area1 -----
1: B2 C2 D2
2: B3 C3 D3
3: B4 C4 D4
----- Area2 -----
1: F3 G3
2: F4 G4

最後に

Areas(n)を指定することで、個別のセル範囲を特定することが出来、かつRange.Areas(n)がRangeオブジェクトと分かってしまえば、それほど難しくなく処理を書けそうです。

Excelワークシートに指定フォルダ(サブフォルダを含む)から画像を読み込んで貼り付けてみた

もうすぐ3月です。
私のいる会社では、月末近くになると年度末の棚卸しがあります。

担当の方がデジカメで写真を撮っては、Excelのワークシートに
ちまちまと貼り付けてはサイズと位置を調整して・・・

面倒くさそうです。(私はその作業に関しては部外者なので関係ないので、他人事)

それ(棚卸)に、完璧に対応するのはとても無理だけど、
少しだけ楽(手抜き?)が出来るかもしれない処理を作ってみました。

やることは、ざっくり言えば、画像の貼り付けとリサイズの(もう少し少ない?)自動化。

もうちょっと詳しく言えば

  1. 画像のあるフォルダを指定出来ること
  2. 画像は、指定したフォルダのサブフォルダを含めて抽出できること
  3. 画像の並びは、ファイル名ではなく、ファイルのフルパスで昇順とすること
  4. 画像を貼り付ける先頭セルを指定できること
  5. 画像はセル1個につき1ファイルとすること
  6. 貼り付けた画像は、貼り付けるセルの大きさに一致させること(オリジナルと縦横の比率が変わってもOKということ)

といった感じでしょうか。

Option Explicit

'参照設定(FileSystemObject用)
'Microsoft Scripting Runtime

'貼り付けする画像があるフォルダ
Private Const TARGET_FOLDER     As String = "C:\Datas\棚卸写真"

'画像を貼り付けるシート名
Private Const TARGET_SHEET_NAME As String = "Sheet1"

'画像を貼り付ける最初のセルアドレス
Private Const IMAGE_PASET_BEGIN_ADDRESS As String = "B1"

'貼り付けする画像の拡張子
Private Const TARGET_EXT        As String = ".png"


Public Sub addPictures()

    Dim r               As Range
    Dim sTargetPaths()  As String
    Dim i               As Long

    ReDim sTargetPaths(0)

    '指定フォルダにある指定拡張子の画像のパス一覧を取得
    Call getTargetPaths(TARGET_FOLDER, sTargetPaths)

    '画像のパスを昇順で並べ替え
    Call sortArrayList(sTargetPaths)

    '画像を貼り付ける最初のセル
    Set r = ThisWorkbook.Worksheets(TARGET_SHEET_NAME).Range(IMAGE_PASET_BEGIN_ADDRESS)

    '指定のセルに画像を貼り付け
    Call insertImage(sTargetPaths, r)
'    Call insertImage(sTargetPaths, r, 1)

End Sub


'指定フォルダにある指定拡張子の画像のパス一覧を取得
Private Sub getTargetPaths(ByVal sSrcFolder As String, ByRef sTargetPaths() As String)

    Dim fso     As FileSystemObject
    Dim fl      As Folder
    Dim f       As File
    Dim sTargetFolder   As String
    Dim lElements       As Long

    Set fso = New FileSystemObject

    If Not fso.FolderExists(sSrcFolder) Then
        Exit Sub
    End If

    For Each fl In fso.GetFolder(sSrcFolder).SubFolders
        'サブフォルダがあれば、再帰処理
        Call getTargetPaths(fl.Path, sTargetPaths)
    Next fl

    For Each f In fso.GetFolder(sSrcFolder).Files
        '拡張子チェック
        If Right$(LCase(f.Name), Len(TARGET_EXT)) = TARGET_EXT Then
            If Right$(sSrcFolder, 1) = "\" Then
                sTargetFolder = sSrcFolder
            Else
                sTargetFolder = sSrcFolder & "\"
            End If

            lElements = UBound(sTargetPaths)

            If lElements > 0 Then
                lElements = lElements + 1

                ReDim Preserve sTargetPaths(lElements)

                sTargetPaths(lElements) = sTargetFolder & f.Name
            ElseIf lElements = 0 Then
                If sTargetPaths(lElements) = "" Then
                    sTargetPaths(lElements) = sTargetFolder & f.Name
                Else
                    lElements = lElements + 1

                    ReDim Preserve sTargetPaths(lElements)

                    sTargetPaths(lElements) = sTargetFolder & f.Name
                End If
            End If
        End If
    Next f

End Sub

Private Sub sortArrayList(ByRef sDatas() As String)

    Dim ws  As Worksheet
    Dim sortRange   As Range
    Dim vDatas      As Variant
    Dim i           As Long

    With ThisWorkbook.Worksheets.Add
        .Visible = xlSheetHidden

        Set sortRange = .Range("A1").Resize(UBound(sDatas) + 1)

        sortRange = WorksheetFunction.Transpose(sDatas)
        
        .Sort.SortFields.Clear
        .Sort.SortFields.Add Key:=sortRange, SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal

        With .Sort
            .SetRange sortRange
            .Header = xlGuess
            .MatchCase = False
            .Orientation = xlTopToBottom
            .SortMethod = xlPinYin
            .Apply
        End With

        vDatas = sortRange

        For i = LBound(vDatas) To UBound(vDatas)
            sDatas(i - 1) = CStr(vDatas(i, 1))
        Next i

        Application.DisplayAlerts = False
        .Delete
        Application.DisplayAlerts = True
    End With

End Sub

'画像を貼り付け
'
'   sTargetPaths()  :貼り付ける画像のパスを格納した配列
'   r               :画像を貼り付ける最初のセル
'   lIntervalRow    :画像を貼り付ける間隔(デフォルト0。省略可)
'
Private Sub insertImage(ByRef sTargetPaths() As String, ByRef r As Range, Optional ByVal lIntervalRow = 0)

    Dim rng As Range
    Dim fso As FileSystemObject
    Dim i   As Long

    Set fso = New FileSystemObject

    For i = 0 To UBound(sTargetPaths)
        '画像を貼り付けるセルの位置、サイズ情報取得用
        Set rng = r.Offset((lIntervalRow + 1) * i)

        Call r.Parent.Shapes.AddPicture( _
                Filename:=sTargetPaths(i), _
                LinkToFile:=False, _
                SaveWithDocument:=True, _
                Left:=rng.Left, _
                Top:=rng.Top, _
                Width:=rng.Width, _
                Height:=rng.Height)

        rng.Offset(, 1).Value = fso.GetFileName(sTargetPaths(i))
    Next i

End Sub


画像データはこんな感じ

f:id:Z1000S:20190227173726j:plain
おにぎりフォルダ
f:id:Z1000S:20190227173737j:plain
スイーツフォルダ
f:id:Z1000S:20190227173813j:plain
パンフォルダ
f:id:Z1000S:20190227173902j:plain
洋食フォルダ

実行結果

f:id:Z1000S:20190227180741j:plain
実行結果

とりあえず、先頭の定数をいじれば、それなりに使えないかなぁ~



ファイルパスのソートは、一時的に追加したダミーのワークシートに配列から貼り付けて、ワークシート上でソートしてから再度配列に取り込んでます。
データ数が少ないから、セルのデータをフルスキャンしてもいいんだけど、そうしない方向でやってみました。
昔作ったクイックソートのコードを引っ張り出してきても良かったんだけど、いろいろやってみたかったので・・・


サンプルとして使用した素材は、いらすとやよりダウンロードし、使わせていただきました。