他のブックを参照するには、ワークシートの数式で外部参照するのであれば対象のブックを開いていなくても大丈夫です。しかし、次の関数は使えないようです。
COUNTIF
DCOUNTA
DSUM
INDIRECT
SUMIF
など
これは、ちょっと不便ですね。
しかも、「数式による外部参照はひどく不安定だから、まったくお勧めできない」などという意見も散見されたりするうえ、こちらが処理している間に先方の表などの書式が変更されてしまったり(例えば、配置や項目の順序が変わるなど)、ファイル名やシート名が変わってしまったりしたらどうなるのか、心配はつきません。
そこで、ThisBookの起動にあわせて必要な外部の表を一旦こちらにそっくり転送し、転送された表への内部参照で処理を行う、必要に応じて随時再転送を行って内容を更新できるようにしておく、という方法をとってみました。
表の転送には Microsoft Query を使うとか、いっそのことリンクなどと言わず Access できちんとしたデータベースを作るのがスマートではないか、と言われそうですが、そのあたりには慣れていないので、とりあえず VBA でやってみました。
’力づく’になるかと思いましたが、けっこう VBA でもすっきりといけました。見る人の感性にもよりますが。
こんな感じです。
ThisWorkbook
Private Sub Workbook_Open()
GetBooks
End Sub
標準モジュール1 GetBooks()
'----- 転記された表へのポインタ
Public ProjectList As LinkBookClass
' ・・・必要なだけ追加・・・
'
'これで PROJECT.xlsm を参照する ProjectList という LinkBookClass のインスタンスを生成しておき
Public Sub GetBooks()
Application.ScreenUpdating = False
Set ProjectList = Nothing: Set ProjectList = New LinkBookClass
If Not ProjectList.Init("PROJECT","PROJECT.xlsm", "LIST") Then GoTo errTrap
' ・・・上の2行必要なだけ追加・・・
errTrap:
Application.ScreenUpdating = True
End Sub
'これで必要なセルの値を得る
Public Function GetProject(名前, 項目名)
GetProject = ProjectList.GetValue(名前, 項目名)
End FunctionGetBooks() は、参照先である PROJECT.xlsm のワークシート LIST から ThisWorkbook のワークシート PROJECT に表を転記して、その表へのいわばポインタとして LinkBookClass のインスタンス ProjectList を生成します。これ以降は、ProjectList のプロパティやメソッドを利用して間接的に PROJECT.xlsm にアクセスすることになります。 LinkBookClass のインスタンスを次々に作っていけば、参照先をいくつでも追加することができます。しめしめ、これで何とかなりそう。
ここで、使い物になるような LinkBookClass を定義しなくてはなりません。
LinkBookClass.Init で、まずすべての準備を終わらせておきます。その後で、GetValue() で表内の値を参照する、という魂胆です。Init で行う準備は、転記する白紙のワークシートを用意すること (ArrangeSheet) 、そこへ目的の表を転記すること (TransferTable) 、項目名(表頭)とコラム番号との対照表を作成しておくこと (getHeadings) です。
ArrangeSheet、TransferTable、GetHeadingColumns とエラー表示のための ErrMessage はクラス内に入れると重いので、標準モジュールに記載することにしました(後述)。
とりあえず Init() がこのクラスのメソッド、ListRange と GetValue() がこのクラスのプロパティ、ということになります。SheetName(転送された表の記載されたワークシート名)も公開してありますので、実用に供する際には SheetName を使ってこのワークシートを非表示にしておいたほうがよいでしょう。
クラスモジュール LinkBookClass
'===== LinkBookClass のプロパティ
'----- 表が転記された targetSheet の名前
Public SheetName
'----- 転記された表の範囲
Public ListRange
'----- 項目名に対応したコラム番号
Private columnNum As Object
'===== 表の転送
'----- srcBookName, srcSheetName で示した元表を trgSheetName に転記して
' 必要なパラメータを作成する
Public Sub Init(trgSheetName, srcBookName, srcSheetName)
Dim description
On Error GoTo errTrap
'srcBookName が存在しなかった場合
If Dir(ThisWorkbook.path & "" & srcBookName) = "" Then Err.Raise 999
'srcBookName は存在する場合
SheetName = Trim(trgSheetName)
ArrangeSheet SheetName
Set ListRange = TransferTable(srcBookName, srcSheetName, SheetName)
description = getHeadings
If Len(description) > 0 Then Err.Raise 998
Exit Sub
errTrap:
'ユーザー定義エラーは、513-65535を使用する(735,744,746,1004を除く)
'https://excel-ubara.com/excelvba1/EXCELVBA434.html
Select Case Err.number
Case 998: description = """" & description & """ の項目名が重複しています。"
Case 999: description = "(ブック)" & srcBookName & " が見当たりません。"
Case 7: description = srcBookName & " に (シート)" & srcSheetName & " がありません。"
Case Else: description = ""
End Select
ErrMessage Err, "LinkBookClass.Init", description
Err.Clear
End Sub
'===== targetSheet 上の headingTable の参照
'----- 項目名とコラム番号の対照表作成
Private Function getHeadings()
Dim headingTable
Set columnNum = CreateObject("Scripting.Dictionary")
With Worksheets(SheetName)
Set headingTable = .Range(.Range(HeadingIndex).Value)
End With
getHeadings = GetHeadingColumns(headingTable, columnNum)
End Function
'----- コラム番号1の内容と項目名から値を取得する
Public Function GetValue(key, headingName)
GetValue = GetValueByNum(key, columnNum(headingName))
End Function
'----- コラム番号1の内容と項目番号から値を取得する
Public Function GetValueByNum(key, column)
On Error Resume Next
GetValueByNum = WorksheetFunction.VLookup(key, ListRange, column, False)
If Err.number <> 0 Then GetValueByNum = ""
End Function標準モジュールに、LinkBookClass に必要な関数を追加しておきます。
標準モジュール2 ArrangeSheet(), TransferTable(), GetHeadingColumn(), ErrMessage()
'----- 元表を掲載している sourceSheet へのパス
Private sourceSheetPath
'----- 転記する対象シート
Private sheetName
'----- 5行目に項目名の一覧を作成する
Private Const headingKeyRow = 5
'
'===== sheetName のシートがあれば全体をクリア、なければ新たに追加する
Public Sub ArrangeSheet(sheetName)
If existSheet(sheetName) Then clearSheet (sheetName): Exit Sub
Worksheets.Add after:=Worksheets(Worksheets.Count)
ActiveSheet.Name = sheetName
End Sub
'----- sheetName のシートが存在すれば TRUE を返す
Private Function existSheet(sheetName)
Dim sh
For Each sh In Worksheets
If sh.Name = sheetName Then existSheet = True: Exit Function
Next
End Function
'----- sheetName のシート全体をクリア
Private Sub clearSheet(sheetName)
Worksheets(sheetName).Cells.Clear
End Sub
'
'===== 対象シートの sourceSheet と同じ位置にヘッダ付きの表をコピーする
'ヘッダを除く表の範囲を返す
Public Function TransferTable(srcBookName, srcSheetName, shName)
Dim sourceBookPath
sheetName = Trim(shName)
sourceBookPath = ThisWorkbook.path & "\[" & Trim(srcBookName) & "]"
sourceSheetPath = "'" & sourceBookPath & Trim(srcSheetName) & "'!"
getAddress KeyIndex
getTable getAddress(HeadingIndex)
Set TransferTable = getTable(getAddress(ListIndex))
End Function
'----- sourceSheet から表の範囲を取得してセルに記録する
'記録したアドレスを返す
Private Function getAddress(index)
Dim address
With Worksheets(sheetName).Range(index)
.formula = "= " & sourceSheetPath & index
'条件設定のためのアドレス指定には、sheetName が必要
watchFinished
address = .Value
address = Mid(address, InStr(1, address, "!") + 1, 99)
getAddress = address
.Value = sheetName & "!" & address
End With
End Function
'----- address の位置にある表の値をコピーする
' 値が 0 であれば、空白に変更する
Private Function getTable(address) As Range
Dim v
Set getTable = Worksheets(sheetName).Range(address)
With getTable
.Value = "=" & sourceSheetPath & address
.Value = .Value
End With
watchFinished
For Each v In getTable
If v.Text = 0 Then v.Value = ""
Next
End Function
'----- 計算が完了するまで待つ
Private Sub watchFinished()
With Application
Do While .CalculationState <> xlDone
If .CalculationState = xlPending Then .Calculate
DoEvents
Loop
End With
End Sub
'
'===== ヘッダ範囲と連想配列を受けて、ヘッダ文字とコラム番号の対照表を作成
'ヘッダ文字の重複があった場合、その文字を返す
Public Function GetHeadingColumns(headingTable, ByRef columnNum) As String
Dim headingKeyRange, headingText
On Error GoTo errTrap
fillBlankCells headingTable
headingText = connectText(headingTable, columnNum)
Set headingKeyRange = headingTable.Parent.Cells(headingKeyRow, headingTable.column). _
Resize(1, headingTable.Columns.Count)
headingKeyRange.Value = columnNum.keys
With headingTable.Parent
.Range(HeadingKeyIndex) = .Name & "!" & headingKeyRange.address
End With
GetHeadingColumns = ""
Exit Function
errTrap:
GetHeadingColumns = headingText
End Function
'----- 下層が空白でない空白セルを直前セルの値で埋める
Private Sub fillBlankCells(headingTable)
Dim row, clm, lastText
For row = headingTable.Rows.Count - 1 To 1 Step -1
For clm = 1 To headingTable.Columns.Count
With headingTable
If Len(.Cells(row, clm)) = 0 And Len(.Cells(row + 1, clm)) > 0 Then
.Cells(row, clm).Value = lastText
Else
lastText = .Cells(row, clm).Value
End If
End With
Next
Next
End Sub
'----- 全層の値を結合して columnNum に登録する
'値の重複があった場合は、その文字を返す
Private Function connectText(headingTable, ByRef columnNum) As String
Dim row, clm, headingText
On Error GoTo errTrap
With headingTable
For clm = 1 To .Columns.Count
headingText = .Cells(1, clm).Value
For row = 2 To .Rows.Count
If Len(.Cells(row, clm)) > 0 Then headingText = headingText & "." & .Cells(row, clm)
Next
columnNum(headingText) = clm
Next
End With
connectText = ""
Exit Function
errTrap:
connectText = headingText
End Function
'
'----- エラーメッセージの表示
Public Sub ErrMessage(error, Optional location = "", Optional description = "")
If description = "" Then description = Err.description
MsgBox description, vbOKOnly, "error:" & error.Number & " " & location
End Subここで、先頭の3つの定数に注目してください。
例えば、HeadingIndex($A$1) は、リンク対象のワークシート内に記載された項目名(ヘッダ)の範囲のアドレスを記載したセルのアドレスです。手元のワークシート上にもワークシート名を追加して同じ位置にコピーされます。
同様に、ListIndex は項目名を除いた表本体の範囲、KeyIndex は表の左端列の範囲のアドレスを記載したセルのアドレスで、KeyIndex は手元のセルの入力規則のリストに使うことを想定しています。
いずれの値も、表の範囲が変更されるたびに、あるいは更新されるたびに動的に書き換えられます。
こんな厄介なことをするのは、次の理由によります。
① リンク対象の表は、表頭に何行かの項目名(ヘッダ)を列記した2次元の表であれば、そのサイズや配置にできるだけ制限を設けないようにしたい、それが自由に変更されてもかまわないようにしたい
② 転記される手元のワークシートは、更新が楽なように、最初は真っ白な状態にしておいて、数式や書式を持たなくてもいいようにしたい
それで、これらのアドレスは getAddress() が改めて設定する数式によって手元のワークシートの HeadingIndex などのセルにコピーされるようにしてあります。
表本体のコピーは、getTable() が設定する数式によって、リンク対象と同じ位置にコピーされます。マクロの For ~ Next を使わないために、大きな表であっても瞬時にコピーされるのがミソです。
For・Next ループとの比較はしていませんが、参照先のシートに12列×1000行ほどの表を作って GetBooks() を実行してみると、処理時間は毎回だいたい 2ミリ秒ほどでした。
GetHeadingColumns() のコードは少し長くなっていますが、これは次に例示する PROJECT のように、ヘッダ部分が数行にわたっているような場合に、例えば1列目は「プロジェクト名.略称」7列目なら「契約金額」、10列目なら「配置.社内.担当者」といったような名前で項目を参照できるようにしたためです。
先ほどの GetBooks() で処理されたリンク対象の表とコピーされた表の画像を以下に示します。これは、参照される側の PROJECT.xlsm のワークシート LIST の内容です。

リンク対象の表 [PROJECT.xlsm]LIST! はこんな具合
$A$1~3 は、表のヘッダ部分と本体、それに本体中の左端列の範囲を示すアドレスの文字列がはいります。
赤枠で示した範囲にはそれぞれ Heading、ListTop という名前をつけてあり、$A$1~3 は、この名前を参照して表やヘッダの範囲が自動的に計算されます。その計算式は3行目までに次のように記載しています。

リンク対象の表のセルには、こんな数式が記載されています
小さくて見えにくいので、あらためて以下に書き出しておきます。
[A1] =K1 & ADDRESS(H1,E2) & ":" & ADDRESS(H3,E2+H2-1)
[A2] =K1 & ADDRESS(E1,E2) & ":" & ADDRESS(E3,E2+H2-1)
[A3] =K1 & ADDRESS(E1,E2) & ":" & ADDRESS(E3,E2)
[E1] =ROW(ListTop)
[E2] =COLUMN(ListTop)
[E3] =MAX((LEN(B:B)>0)*ROW(B:B))
[H1] =ROW(Heading)
[H2] =COLUMNS(Heading)
[H3] =H1+ROWS(Heading)-1
[K1] =MID(CELL("filename",J1),FIND("]",CELL("filename",J1))+1,99) & "!"
この3行は、一度作っておけば、リンクされるすべてのシートにそのままコピペできます。
ヘッダの列数や行数を変更した場合には、Heading の範囲を手動で再定義すると、3行の値も更新されます。
これらの行は、このシートを編集するユーザーが見る必要がなく、編集されても困るので、実際には1~3行の行高さを 0 として隠しておいた方がよいでしょう。
ThisWorkbook が起動されると、改めて用意された手元の PROJECT シートに、下図のような表がコピーされます。コピーされるのは、値だけで書式はコピーしません。値が空欄の場合に「0」と表示されるのが気持ち悪いので、getTable() 内で空欄に戻すようにしています。
5行目には、ヘッダの項目名を下層に向けて "." でつなげた文字列が各項目の上に列挙され(これは、各列の値を参照するためのキーとして使用されます)、そのアドレスが $A$4 に記載されます。

手元のワークシート PROJECT 上にコピーされた表はこうなっています
この PROJECT シート上の表を内部参照して、たとえば別のシートにこんな入力画面をこしらえます。

手元で本来の処理を行う入力シートの画面
黄色で示したセルの値は、ユーザーが入力すべきセルを示します。白いセルは、内部参照=結果的には外部参照によって自動的に記載されるか、内部計算によるものです。参照の場合、これらのセルには、「略称」を検索キーとして該当する値がそれぞれのセルに設定されています。「共通管理費」とか「外注費合計」、「執行済」などは、PROJECT.xlsm とは別のファイルへの外部参照、「残余」は計算結果です。黄色のセル以外は、普通はロックしておくことになるでしょう。
たとえば、「プロジェクト名」は上の数式バーに見えるように、INDIRECT(PROJECT!$A$2) でリンク先から転送された表の本体を呼び出し、ここの「略称」$F$4 "A" と項目名のキー N6 "プロジェクト名.契約名" から検索して表中1行2列目の値を返すことで得ています。
N列には、あらかじめ検索すべき項目名を入れておくことにしました。
GetHeadingColumns() も含めて、やけに煩雑なことになりましたが、ひとえにマジックナンバーを避けようとした結果です。ここの参照式も、一度いれておけばよいし、他のセルにコピペもできるので、まあよしとしましょう。
稼働時には、N列の幅を 0 にして隠蔽しておくのがスマートですね。
これは、個別のプロジェクトごとに1枚のシートを用意してその詳細を記載するという形のものですが、そうすると、どこかのシート(ここでは、左肩のボタンにあるように”COVER”というシートを想定しています)からキーになる「略称」を選択して表示させる、ということになるでしょう。
そのキーは、リンクしたPROJECTの表の左端の列からドロップダウンリストで選択するようにすべきです。そのキーリストの範囲は、PROJECT シートの $A$3 セルのアドレスを引用しています。

COVERシートでのシート選択欄の入力規則
これで、手元のブックを立ち上げるだけで、標準モジュール1の GetProject(名前, 項目名) に書いたように、マクロからは ProjectList.GetValue(略称, 項目名) で外部のブック PROJECT.xlsm のワークシート LIST 上にある表の中の略称に対応した所定の値が参照されるようになります。
ここまでの道程はけっこう大変でしたが、後は楽ちんです。
実際には、COVERで選択したシートがまだなかった場合、上に示した入力シートをTEMPLATEとして用意しておき、そのコピーを新しいシートにしてそこへ飛ぶ、というような処理をしています。
同じ PROJECT.xlsm 内の別のシートへのリンクや、別ファイル内のシートへのリンクの追加は、新たな LinkBookClass インスタンスを作ることで簡単にできます。たとえば別ファイル BUDGET.xlsm へのリンクを追加しようと思えば、標準モジュール1にオブジェクト変数 BudgetList を追加し、 GetBooks() に BudgetList.Init "BUDGET","BUDGET.xlsm","LIST" などと書き加えれば、後はその GetValue プロパティで参照できます。このリンク先の追加は、原理的にはいくらでも追加できます。ただし、リンク対象用に新しいワークシートを割り当てますので、ワークシートの総数の上限(一説には、255と言われています)まで。
どこかにボタンを用意して、GetBooks のマクロを登録しておくとよいでしょう。ボタンをクリックするごとに、リンクが更新されます。「略称」の右に見える「リンク更新」ボタンは、そのためのものです。
そんなに使い込んだものではありませんので、以上のコード中、エラー処理などは十分ではないかもしれません。
また、たとえば転送先の手元のシート(上の例では"PROJECT") を削除すると、そこを参照する数式内の参照シート名が消えて #REF となってしまいます。マクロで参照するようにしていた場合は OK でも、上に紹介した入力画面のように数式で参照していた場合は、再度数式を入れ直す必要が生じます。これ、なんとかならないものか、と思案中。今のところは、削除しないように気を付ける、という以外ありません。
いろいろと宿題が残されていますが、こんな方針でも外部参照ができ、一度こういうクラスを用意しておけばメリットもありそうだ、ということで紹介しました。