ExcelのVBA中級者となってくると「他のブックを操作したい」という要求が出てきます。VBAは苦手な方や嫌いな方も多いため、普段使用するブックをマクロ有効ブックとしてしまうよりは、自分や同僚用の操作用ブックから目的の操作をする方が安心感があるように思います。VBAで一番有効なデザインパターンはこのようなFacadeパターンじゃないかと思います。
おお、依存性逆転っぽくてかっこよくありませんか?
他のブックを操作する際に必要になってくるのが、ブックが保存されているフォルダパスの取得です。
前回記述したフォルダパスを取得するスクリプトがお気に入りなので、そこだけ切り出して整理してみました。
今回は、フォルダを単純に選ぶ、というものと、何かしらの「原本となるブック」を選ぶというものを作成してみました。
例として「成績ブック」という原本がありそれが保存されているフォルダをダイアログから選べる、というものです。
原本となるブック
よくあるのが「原本となるブック」が指定されていて、それらを業務のたびにコピーしたりして使用するものです。
その原本はおそらく多岐にわたると思われます。その流動性のところにカプセル化をかましておきましょう。
「原本です」と取得できればよいので「どんなブック名が原本なのかは(流動性があるので)感知しない」というイメージです。
また今回は「ブック名にキーワードが含まれているExcelブック(.xlsx)であればよい」というルールで作成しました。このあたりのルールも流動性がありそうだと思いませんか?ですのでこちらもカプセル化してしまいましょう。カプセル化すればカプセルの外側の我々は感知しなくてもよくなります。「これは原本ですかね?原本をください。」と言えばいいだけで原本かどうかの処理はカプセル化の内側で行われるわけですね。
整理すると
- 「どんなブック名か」という流動性
- 「どのようなルールで原本とみなすか」という流動性
の2種類があるように思いました。ですのでそれぞれカプセル化してみましょう。「どんなブック名?(流動性があるので)知りません。どれを原本と判断する?(流動性があるので)知りません。」というスタンスです。
カプセル化ということでインターフェイスを使用します。何度も恐縮ですがVBAにも実はインターフェイスがあります。あまり人気ないんですけどね。
IBookLocator.cls
Option Explicit
Public Function Exists(p_folderpath As FolderPath, i_genponPolicy As IGenponPolicy) As Boolean
End Function
Public Function Load(p_folderpath As FolderPath, i_genponPolicy As IGenponPolicy) As Workbook
End Function
インターフェイスは何も中身がないプロシージャやモジュールレベル変数などを記述することで使用できます。どれか1つはPublicでないと使用できないので、どれかは必然的にPublicになります。
今回は「原本ブックが存在するか」と「原本ブックを開く」の2種類のプロシージャを設定しました。
では今回の例の「ブック名にキーワードが含まれているExcelブック(.xlsx)であればよい」というルールを作成してみます。
GenponBookLocatore.cls
Option Explicit
Implements IBookLocator
Private listbookName_ As String
Public Function IBookLocator_Exists(p_folderpath As FolderPath, i_genponPolicy As IGenponPolicy) As Boolean
Dim FileName As String
FileName = Dir(p_folderpath.path & "\" & "*" & i_genponPolicy.GetName & "*" & i_genponPolicy.GetExtension)
Do While FileName <> ""
' 拡張子を除いたファイル名に対してキーワードを検索
If InStr(1, Left(FileName, Len(FileName) - 5), i_genponPolicy.GetName, vbTextCompare) > 0 Then
IBookLocator_Exists = True
listbookName_ = p_folderpath.path & "\" & FileName
Exit Function
End If
FileName = Dir()
Loop
IBookLocator_Exists = False
FileName = ""
End Function
Public Function IBookLocator_Load(p_folderpath As FolderPath, i_genponPolicy As IGenponPolicy) As Workbook
Dim filePath As String
filePath = listbookName_
If Not IBookLocator_Exists(p_folderpath, i_genponPolicy) Then
Err.Raise vbObjectError + 1001, "BookLocator.Load", "ファイルが存在しません: " & filePath
End If
Set IBookLocator_Load = Workbooks.Open(FileName:=filePath)
End Function
Google検索してみたところ古のDir関数が引っ掛かりましたのでそちらを使用してみました。あいまいな検索でキーワードがあればよい、ということで「*」を使用しています。
Dir関数は古いだけありまして、ヒットするとそのファイル名が返ってくるという仕様です。(地味にハマりました。)
ファイルが存在しない時は「ファイルが存在しませんエラー」を発生させることにしてみました。
次に「どんなブック名が原本なのか」という流動性に対応しましょう。
IGenponPolicy.cls
Option Explicit
Public Function GetName() As String
End Function
Public Function GetPass() As String
End Function
Public Function GetExtension() As String
End Function
また、空のプロシージャばかりのインターフェイスを作成しました。いずれはパスワードがかかったブックがあるかもしれませんので、パスワードも設定できます。
今回のルールは「成績ブック」というなまえで拡張子は普通の「.xlsx」だというルールだとしましょう。
GenponBookGenponPolicy.cls
Option Explicit
Private bookname_ As String
Private pass_ As String
Private extension_ As String
Private Const KEY_WORD As String = "成績ブック"
Private Const EXTENSION As String = ".xlsx" '".xlsm"
Implements IGenponPolicy
Private Sub Class_Initialize()
bookname_ = KEY_WORD
pass_ = ""
extension_ = EXTENSION
End Sub
Private Function IGenponPolicy_GetName() As String
IGenponPolicy_GetName = bookname_
End Function
Private Function IGenponPolicy_GetPass() As String
IGenponPolicy_GetPass = pass_
End Function
Private Function IGenponPolicy_GetExtension() As String
IGenponPolicy_GetExtension = extension_
End Function
VBAの文法上の理由で Implementsステートメントのあとにインターフェイス名が来ています。また、プロシージャ名が「インターフェイス名、アンダーバー、プロシージャ名」となっています。
やや固執する方も多いのが、インターフェイスではない具象クラスモジュール側ではPrivateで宣言する点です。私はどちらでも困ったことはそこまでないのですが、具体的な内容を記載した具象クラスモジュール側がPrivateだとインターフェイス型での宣言がより強要されていいと思います。
これで「成績ブック」というキーワードがあるブック名で拡張子が「.xlsm」というブックを判定するクラスモジュールが完成しました。
フォルダパス
実際のフォルダパスをクラスモジュールとして記述していきます。
FolderPath.cls
Option Explicit
Private folderpath_ As String
Public Sub Construct(p_folderpath As String)
If Exists(p_folderpath) Then
folderpath_ = p_folderpath
Else
Err.Raise vbObjectError + 1000, _
"FolderPathクラスモジュール", _
"指定されたフォルダは存在しません: " & p_folderpath
End If
End Sub
Private Function Exists(p_folderpath As String) As Boolean
Dim fso As Object
Set fso = CreateObject("Scripting.FileSystemObject")
If fso.FolderExists(p_folderpath) Then
Exists = True
Else
Exists = False
End If
End Function
Public Property Get path() As String
path = folderpath_
End Propertyこ
こちらでも不正な値だった場合は「指定されたフォルダは存在しませんエラー」を発生させることにしました。
さて、このFolderPathクラスモジュールを生成するに際しまして
「フォルダを直接指定する」
「原本のブックが保存されているフォルダを取得する」
という2種類の挙動を考えました。
それぞれ別のクラスモジュールとして記述していきます。
また、実際にFolderPathクラスモジュールを生成する具体的な動きをするファクトリークラスモジュールを作成して、そこに実際の動作のプロシージャを作成し、それを引数にとることにしました。
おお、依存性の注入っぽくてかっこよくありませんか?
GetTargetFolderPathService.cls
Option Explicit
Public Function GetDefault(fp_factory As FolderPathFactory) As FolderPath
Set GetDefault = fp_factory.GetDefaultFolderPath
End Function
Public Function GetFromDialog(fp_factory As FolderPathFactory) As FolderPath
Set GetFromDialog = fp_factory.GetFolderPathFromDialog
End Function
Public Function GetFromFolderName(p_foldername As String, fp_factory As FolderPathFactory) As FolderPath
Set GetFromFolderName = fp_factory.CreateFromFolderName(p_foldername)
End Function
こちらは「フォルダを直接指定する」のをイメージしたクラスモジュールです。
ダイアログから指定したり、起動時に今のフォルダを取得したり、フォルダパスの文字列からそのフォルダパスクラスモジュールを生成することが出来ます。
GetGenponPathService.cls
Option Explicit
Const ERROR_INVALID_FOLDER_PATH As Long = vbObjectError + 1000
Public Function GetFromDialog(i_genponPolicy As IGenponPolicy, i_booklocator As IBookLocator, fp_factory As FolderPathFactory) As FolderPath
Dim fd As FileDialog
Dim SelectedFileName As String
Dim FolderPath As FolderPath
Do
Set fd = Application.FileDialog(msoFileDialogFilePicker)
With fd
.Title = i_genponPolicy.GetName & "ファイルを選択してください"
.Filters.Clear
.Filters.Add "Excelファイル", "*.xls; *.xlsx; *.xlsm"
.InitialFileName = ThisWorkbook.path & "\"
.AllowMultiSelect = False
If .Show = -1 Then
SelectedFileName = .SelectedItems(1)
Dim GenponPath As FolderPath
Set GenponPath = fp_factory.CreateFormFullFilePath(SelectedFileName)
If i_booklocator.Exists(GenponPath, i_genponPolicy) Then
Set GetFromDialog = GenponPath
Exit Function
Else
MsgBox i_genponPolicy.GetName & "ファイルを選択してください。", vbExclamation
SelectedFileName = ""
End If
Else
MsgBox i_genponPolicy.GetName & "ファイルを選択してください。", vbExclamation
SelectedFileName = ""
End If
End With
Loop While SelectedFileName = ""
Set fd = Nothing
End Function
Public Function CreateFormFullFilePath(p_fullfilepath As String, i_genponPolicy As IGenponPolicy, i_booklocator As IBookLocator, fp_factory As FolderPathFactory) As FolderPath
Dim GenponPath As FolderPath
Set GenponPath = fp_factory.CreateFormFullFilePath(p_fullfilepath)
If i_booklocator.Exists(GenponPath, i_genponPolicy) Then
Set CreateFormFullFilePath = GenponPath
Exit Function
Else
Set CreateFormFullFilePath = GetFromDialog(i_genponPolicy, i_booklocator, fp_factory)
End If
End Functionつ
続きましてこちらは、「原本のブックが保存されているフォルダを取得する」をイメージしたクラスモジュールです。
ダイアログではExcelファイルを選んでもらい、それが条件に一致しているものであるかを判定しています。またファイルパスからFolderPathクラスモジュールを生成できます。
フォルダを選ぶダイアログと任意のファイルを選ぶダイアログと2種類あるみたいですね、VBAには。便利ですね。
これら2つのクラスモジュールが依存するのが実際の動作を集めた「FolderPathFactoryクラスモジュール」です。こちらを引数で注入して依存性注入を行いましょう。
FolderPathFactory.cls
Option Explicit
Private Const ERROR_INVALID_FOLDER_PATH As Long = vbObjectError + 1000
Private Const ERROR_NOT_FOUND_FOLDER As Long = vbObjectError + 2000
Public Function CreateFromFolderName(p_foldername As String) As FolderPath
On Error GoTo ErrHandler
Dim FolderName As String
FolderName = p_foldername
Dim FolderPath As FolderPath
Set FolderPath = New FolderPath
Call FolderPath.Construct(FolderName)
Set CreateFromFolderName = FolderPath
Exit Function
ErrHandler:
If Err.Number = ERROR_INVALID_FOLDER_PATH Then
FolderName = GetFolderPathStringFromDialog
Resume Next
End If
End Function
Public Function CreateFormFullFilePath(p_filepath As String) As FolderPath
Dim FolderName As String
FolderName = GetFolderPathStringFromFilePath(p_filepath)
Set CreateFormFullFilePath = CreateFromFolderName(FolderName)
End Function
Private Function GetFolderPathStringFromFilePath(filePath As String) As String
On Error GoTo ErrHandler
Dim pos As Long
pos = InStrRev(filePath, Application.PathSeparator)
If pos > 1 Then
GetFolderPathStringFromFilePath = Left(filePath, pos - 1)
Exit Function
Else
Err.Raise vbObjectError + 2000, _
"FolderPathFactoryクラスモジュール", _
"指定されたファイルパスにフォルダが存在しません: " & filePath
End If
ErrHandler:
If Err.Number = ERROR_NOT_FOUND_FOLDER Then
GetFolderPathStringFromFilePath = GetFolderPathStringFromDialog
Err.Clear
Else
' 想定外のエラーはそのまま再スロー
Err.Raise Err.Number, Err.Source, Err.Description
End If
End Function
Public Function GetDefaultFolderPath() As FolderPath
Dim DefaultFolder As String
Dim currentFolder As String
' 現在のブックのフォルダを取得
currentFolder = ThisWorkbook.path
If currentFolder = "" Or InStr(currentFolder, "\") = 0 Then
DefaultFolder = "C:"
Exit Function
Else
'GetDefaultFolderPath = Left(currentFolder, InStrRev(currentFolder, "\") - 1) ' 一つ上のフォルダを取得
DefaultFolder = currentFolder
End If
Set GetDefaultFolderPath = CreateFromFolderName(DefaultFolder)
End Function
Public Function GetFolderPathFromDialog() As FolderPath
Dim FolderName As String
FolderName = GetFolderPathStringFromDialog
Set GetFolderPathFromDialog = CreateFromFolderName(FolderName)
End Function
Private Function GetFolderPathStringFromDialog() As String
Dim fd As FileDialog
Dim SelectedFolder As String
Dim currentFolder As String
Dim parentFolder As String
' フォルダ選択ダイアログを作成
Set fd = Application.FileDialog(msoFileDialogFolderPicker)
Do
With fd
.Title = "フォルダを選択してください"
.InitialFileName = ThisWorkbook.path & "\"
.AllowMultiSelect = False
If .Show = -1 Then
SelectedFolder = .SelectedItems(1)
GetFolderPathStringFromDialog = SelectedFolder
Else
MsgBox "フォルダを選択する必要があります。", vbExclamation
End If
End With
Loop While SelectedFolder = ""
Set fd = Nothing
End Function
実際には先ほど記述した、IBookLocator型やIGenponPolicy型も引数にとりまして、総合的に作成していきます。
このように、なるべく引数で色々なオブジェクトを注入するのが一つのやり方であるみたいですね。
セルに表示する
次にセルに表示するクラスモジュールを作成しました。いよいよVBAって感じになってきましたね。
今回は「フォルダを直接指定する」のと「原本のブックが保存されているフォルダを取得する」の2種類の結果を表示することにしました。それぞれのボタンもつければそれっぽくなりそうですね。
色々なセルに表示する機能を持つのではなく今回はそれぞれ専任のクラスモジュールを作成しました。
表示するセルには名前を付けて、そこからRangeオブジェクトを指定しています。
それらのセルは勝手に削除されても困るので、その2つのセルだけロックを残してシートを保護しています。
DisplayFolderPathService.cls
Option Explicit
Public Sub DisplayFolderPath(p_folderpath As FolderPath)
Dim ws As Worksheet
Set ws = FolderPathAndGenponSheet
ws.Unprotect
ws.Range("FolderPath").ClearComments
ws.Range("FolderPath").Value = p_folderpath.path
ws.Protect UserInterfaceOnly:=True
End Sub
DisplayGenponService.cls
Option Explicit
Public Sub DisplayGenponName(p_folderpath As FolderPath, i_genponPolicy As IGenponPolicy)
Dim ws As Worksheet
Set ws = FolderPathAndGenponSheet
Dim filePath As String
filePath = p_folderpath.path & "\" & i_genponPolicy.GetName
ws.Unprotect
ws.Range("GenponFilePath").ClearComments
ws.Range("GenponFilePath").Value = filePath
ws.Protect UserInterfaceOnly:=True
End Sub
これで無事完成です。
ブックを開く際に初期設定してこれらを表示させています。
FolderPathAndGenponSheetモジュール
Public Sub initializeSheet()
Call InitializeFolderPath
Call InitializeGenponPath
End Sub
Private Sub InitializeFolderPath()
Dim GetTargetFolderPathService As GetTargetFolderPathService
Set GetTargetFolderPathService = New GetTargetFolderPathService
Dim FolderPathFactory As FolderPathFactory
Set FolderPathFactory = New FolderPathFactory
Dim FolderPath As FolderPath
Set FolderPath = GetTargetFolderPathService.GetDefault(FolderPathFactory)
Dim DisplayFolderPathService As DisplayFolderPathService
Set DisplayFolderPathService = New DisplayFolderPathService
Call DisplayFolderPathService.DisplayFolderPath(FolderPath)
End Sub
Private Sub InitializeGenponPath()
Dim GenponBookLocator As IBookLocator
Set GenponBookLocator = New GenponBookLocatore
Dim GenponBookGenponPolicy As IGenponPolicy
Set GenponBookGenponPolicy = New GenponBookGenponPolicy
Dim FolderPathFactory As FolderPathFactory
Set FolderPathFactory = New FolderPathFactory
Dim GetGenponPathService As GetGenponPathService
Set GetGenponPathService = New GetGenponPathService
Dim GenponFolderPath As FolderPath
Set GenponFolderPath = GetGenponPathService.CreateFormFullFilePath(FolderPathAndGenponSheet.Range("GenponFilePath").Value, GenponBookGenponPolicy, GenponBookLocator, FolderPathFactory)
Dim ListWb As Workbook
If Not GenponBookLocator.Exists(GenponFolderPath, GenponBookGenponPolicy) Then
Set GenponFolderPath = GetGenponPathService.GetFromDialog(GenponBookGenponPolicy, GenponBookLocator, FolderPathFactory)
End If
Dim DisplayGenponService As DisplayGenponService
Set DisplayGenponService = New DisplayGenponService
Call DisplayGenponService.DisplayGenponName(GenponFolderPath, GenponBookGenponPolicy)
End Sub
今回はシートモジュールに記述しました。シートモジュールのオブジェクト名は「FolderPathAndGenponSheet」に変更しています。
フォルダ取得ボタン、原本ブック取得ボタン
最後の最後にボタンを押した際のプロシージャを作成しました。
先程の初期設定もそうでしたが、実際のプロシージャではクラスの宣言ばっかりで特別なことはしていません。「表示して」とか「フォルダ名ください」とかいうだけで、カプセル化の中身については感知していません。ExcelやVBAがどんなことをしているかには依存せず、「表示して」とか「フォルダ名下さい」という仕様に逆に依存しているわけですね。
また、それらのカプセル化の中身も何かに依存はいていなくて、必要なものは外部から注入してもらっている、というあたりがお気に入りポイントです。
FolderPathAndGenponSheetモジュール
'*** 「フォルダ名取得」ボタン
'***
'*** デフォルト値からフォルダを変更して表示する。
Public Sub SelectFolder()
Dim GetTargetFolderPathService As GetTargetFolderPathService
Set GetTargetFolderPathService = New GetTargetFolderPathService
Dim FolderPathFactory As FolderPathFactory
Set FolderPathFactory = New FolderPathFactory
Dim FolderPath As FolderPath
Set FolderPath = GetTargetFolderPathService.GetFromDialog(FolderPathFactory)
Dim DisplayFolderPathService As DisplayFolderPathService
Set DisplayFolderPathService = New DisplayFolderPathService
Call DisplayFolderPathService.DisplayFolderPath(FolderPath)
End Sub
'***「原本ブック取得」ボタン
'***
'***原本ブックを選択して取得する
Public Sub SelectGenponFolder()
Dim GetGenponPathService As GetGenponPathService
Set GetGenponPathService = New GetGenponPathService
Dim GenponBookGenponPolicy As IGenponPolicy
Set GenponBookGenponPolicy = New GenponBookGenponPolicy
Dim GenponBookLocatore As IBookLocator
Set GenponBookLocatore = New GenponBookLocatore
Dim FolderPathFactory As FolderPathFactory
Set FolderPathFactory = New FolderPathFactory
Dim GenponFolderPath As FolderPath
Set GenponFolderPath = GetGenponPathService.GetFromDialog(GenponBookGenponPolicy, GenponBookLocatore, FolderPathFactory)
Dim DisplayGenponBookName As DisplayGenponService
Set DisplayGenponBookName = New DisplayGenponService
Call DisplayGenponBookName.DisplayGenponName(GenponFolderPath, GenponBookGenponPolicy)
End Sub
フォルダの取得がうまくいけば、そのフォルダ内のブックや、指定した原本ブックなどを操作するきっかけになりそうですね。
