【エクセルVBA】エクセル表のヘッダーの列番号を動的に取得する(項目名、列番号をCollectionに詰めて返却するFunctionプロシージャ)

「たくさんの項目があるメンバー表から、指定した人の特定の項目値だけを抽出できるマクロを作成してくれ」

という依頼がきたのだけど。

マクロを作って先輩に提出したら「動かしてみたけど欲しい項目の値が取れていない」と言われてしまった。。
テストしたときは正しく取れていることを確認したのに、今動かしてみると確かに別の項目値が取れてしまっている。

どういうことだ。。

もしかしたら、表の列が追加・削除・移動されていたりしない?

あ、ほんとだ!

最初にもらった表から列の場所が変わっている!

これじゃ取得する項目の列がずれるから、結果も間違えるわけだ。。
マクロの列番号を修正すれば直るけど、また列が変更されてずれたら、結局間違えた結果になってしまうし。

マクロを理由に「列を変更しないでください」なんて言えないし。

取得する項目の列番号を固定値で指定しているとか?

え、まあ、そうですけど。
でも「Cells(1,1)」のように、固定値になりますよね?

普通はそうなんだけど、工夫すれば動的に列番号を指定できるかな。
表のヘッダー部の項目名と列番号を一組としてCollectionに詰めてみたらいいかもね。
項目名を指定したらその列番号が取れるから動的になるよ。

はじめに

マクロで表を扱う場合、取得したい項目の列番号の指定は「固定値」にすることが多いかと思います。

例えば「C列」の値を取得したい場合、「Cells(1, 3)」といった具合です。

このように固定値を指定した場合、表の列が追加・削除・移動などで変更されて列がずれると、ソースコードの列番号の部分を修正しないといけません。

表の列が勝手に変更されているのに「このマクロ、なんか結果が変だよ」なんて言われた経験ないでしょうか?

マクロを作った側からすれば「そら勝手に表を変更されたら、結果もおかしくなるわ」となりますよね。

けど、使う側からすれば「表は見やすいように変更が入ることもあるだろう」とか考えているかもしれません。

列がずれてもソースコードの修正を不要とするには、列番号を「固定値」にするのではなく、列に応じた列番号を「動的に取得」できればよさそうです。

ということで、

エクセル表のヘッダー部の列番号を動的に取得するFunctionプロシージャ

を作ってみたいと思います。

ソースコード

表のヘッダー部の「項目名」と「その列番号」を一組としてCollectionに詰めて返却するFunctionプロシージャです。

'-----------------------------------------------------------------------
'表のヘッダー部の項目名、列番号を詰めたCollectionを取得する
'引数「tableHeaderRng」には、表のヘッダー部のセル範囲のみ(1行のみ)を設定する
'-----------------------------------------------------------------------
Public Function GetTableHeaders(ByVal tableHeaderRng As Range) As Collection

    '項目名、列番号を詰めるためのCollectionを準備
    Dim headers As Collection
    Set headers = New Collection
    
    '引数に指定されたヘッダー部のセル範囲分、キーを「項目名」、値を「列番号」としてCollectionに詰める
    Dim headerCell As Variant
    For Each headerCell In tableHeaderRng
        headers.Add Key:=headerCell.Value, Item:=headerCell.Column
    Next headerCell

    'Collectionを返却する
    Set GetTableHeaders = headers
End Function

使う側は、Functionプロシージャを呼び出し、返却されたCollectionを受け取ります。

受け取ったCollectionに対して、キーに「項目名」を指定することで「列番号」が取得できます。

テストデータ

テストデータとして、以下のエクセル表(以降メンバー表とします)を使います。

まず、「列の変更前」の状態です。

列の変更前のメンバー表
列の変更前のメンバー表 ※クリックで拡大できます

「列の変更後」の状態は、以下のように列を追加・削除・移動したものにします。

列の変更後のメンバー表
列の変更後のメンバー表 ※クリックで拡大できます

取得する項目は「氏名」「生年月日」「電話番号」「住所」「マイナンバー」の5つとします。(セルに色が塗ってある項目)

上記のエクセル表は、架空のデータを自動生成するWebツール「個人情報テストデータジェネレーター」を使用して得たものです。

テストコード

メンバー表から、指定された人の「氏名」「生年月日」「電話番号」「住所」「マイナンバー」の値を取得するものとします。

Sub Test()
    'メンバー表を開く
    Dim wb As Workbook
    Set wb = Workbooks.Open("C:\00_myenv\10_macro\01_test\member.xlsx")
    Dim ws As Worksheet
    Set ws = wb.Worksheets("Sheet1")
    
    'メンバー表のヘッダー部のセル範囲のみ(1行のみ)を取得
    With ws.Range("A1").CurrentRegion
        Dim headerRng As Range
        Set headerRng = .Resize(1, .Columns.Count)
    End With
    
    'Functionプロシージャを呼び出し、ヘッダー部の項目名と列番号が詰まったCollectionを取得
    Dim headers As Collection
    Set headers = GetTableHeaders(headerRng)
    
    'インプットボックスを表示し、入力された氏名を取得
    '(未入力やキャンセルに対する処理は、ここでは割愛する)
    Dim inputName As String
    inputName = InputBox("取得対象の氏名を入力してください")
    
    '氏名が合致する行番号を取得
    '(合致する値がない場合、Match関数はエラーとなるが、ここではエラー処理は割愛する)
    Dim targetRow As Long
    targetRow = WorksheetFunction.Match(inputName, ws.Columns(1), 0)
    
    'メンバー表から値を取得し、イミディエイトウィンドウに表示する
    '列番号は固定値とせず、項目名をキーにしてCollectionから取り出した値を使用する
    Debug.Print ws.Cells(targetRow, headers.Item("氏名"))
    Debug.Print ws.Cells(targetRow, headers.Item("生年月日"))
    Debug.Print ws.Cells(targetRow, headers.Item("電話番号"))
    Debug.Print ws.Cells(targetRow, headers.Item("住所"))
    Debug.Print ws.Cells(targetRow, headers.Item("マイナンバー"))
    
    wb.Close
    
End Sub

実行結果

メンバー表の10行目「大野 友梨」を取得対象として、テストコードを実行してみます。

表示されたインプットボックスに、取得対象の氏名を入力します。

氏名のインプットボックス

まずは、「列の変更前」のメンバー表から取得した結果です。

実行結果(列の変更前)
「列の変更前」のメンバー表から取得した結果

次に、テストコードを一切修正することなく、「列の変更後」のメンバー表から取得した結果です。

実行結果(列の変更後)
「列の変更後」のメンバー表から取得した結果

列に変更が入ってずれが生じても、テストコードの列番号の部分を修正することなく、取得したい項目値が取れています。

説明

ソースコードの説明はコメントの通りです。

ここでは、列番号を「固定値」と「動的に取得」した場合の、Functionプロシージャを使う側の実装を見てみます。

「固定値」の場合、「列の変更前」のメンバー表から値を取得するには、以下のような実装になります。

    Debug.Print ws.Cells(targetRow, 1)     '氏名
    Debug.Print ws.Cells(targetRow, 6)     '生年月日
    Debug.Print ws.Cells(targetRow, 10)    '電話番号
    Debug.Print ws.Cells(targetRow, 13)    '住所
    Debug.Print ws.Cells(targetRow, 17)    'マイナンバー

ここで、メンバー表に変更が入って「列の変更後」の状態になった場合、以下のように列番号を修正します。

    Debug.Print ws.Cells(targetRow, 1)     '氏名
    Debug.Print ws.Cells(targetRow, 7)     '生年月日    「6 → 7」に変更
    Debug.Print ws.Cells(targetRow, 10)    '電話番号
    Debug.Print ws.Cells(targetRow, 17)    '住所      「13 → 17」に変更
    Debug.Print ws.Cells(targetRow, 16)    'マイナンバー  「17 → 16」に変更

当然のことですが、列の変更後には列番号の修正が必要であることが分かります。

次に「動的に取得」した場合の実装は、以下のようになります。

    Debug.Print ws.Cells(targetRow, headers.Item("氏名"))
    Debug.Print ws.Cells(targetRow, headers.Item("生年月日"))
    Debug.Print ws.Cells(targetRow, headers.Item("電話番号"))
    Debug.Print ws.Cells(targetRow, headers.Item("住所"))
    Debug.Print ws.Cells(targetRow, headers.Item("マイナンバー"))

こちらは、列の変更前でも変更後でも実装はこのままで、修正は必要ありません。

なぜ動的に取得できるのかは、Functionプロシージャから返却される変更前後のCollectionの中身を見れば分かります。

Collectionの「Key(項目名)」と「Item(列番号)」の格納イメージは以下のようになっています。

Key(項目名)“氏名”“生年月日”“電話番号”“住所”“マイナンバー”
Item(列番号)16101317
列の変更前
Key(項目名)“氏名”“生年月日”“電話番号”“住所”“マイナンバー”
Item(列番号)17101716
列の変更後

例えば、「住所」であれば、「headers.Item(“住所”)」で取得できる列番号は

 変更前 → 13

 変更後 → 17

となります。

取得できる列番号が動的に変わるので、列の変更後でも実装の修正は不要なことが分かります。

最後に、今回紹介したFunctionプロシージャを使う場合には、以下の点にご注意ください。

  • 表の項目名自体が変更された場合は、使う側のコード修正が必要となる。
    「氏名」→「名前」と変更された場合など
  • 表の項目名に同じ名称がある場合、実行時にエラーとなってしまう。
    (CollectionのKeyの値は重複不可であり、Collectionに詰めることができないため)

まとめ

項目数が少なかったり、列の変更がほとんど入らないような表であれば、列がずれても実装の修正は微々たるものですから、列番号は「固定値」でよいかもしれません。

逆に、項目数が多かったり、列の変更がちょいちょい入るような表であれば、列番号を「動的に取得」することでコード修正の手間が大幅に省けるのではないかと思います。

以上、ご覧いただきありがとうございました。