ゼロ埋め連番を作成する

Format関数の第2引数は、変数でも良かった

ゼロ埋め連番文字列を動的に生成する

たとえば、

akashi-keirin.hatenablog.com

こんなマクロを使って大量にファイルを生成するような場合、ファイル名の先頭がゼロ埋め連番になっていると非常に都合が良い。

で、やってみた。

考え方

次のような手順で、適切な桁数のゼロ埋め連番文字列が得られると考えた。

  1. 個数の桁数を数える
  2. 「0」を「個数の桁数-1」個連結した文字列を作り、変数に格納する
  3. 2.でできた文字列に、「#」を連結し、変数に格納する
  4. 3.まででできた文字列をFormat関数の第2引数とする

コーディング

リスト1
Public Sub testFormatFunction()
  Dim objNum As Integer
  objNum = 105    '……(*)'
  Debug.Print objNum & "個のゼロ埋め文字列を作ります。"
  Dim i As Integer
  Dim n As Integer
  Dim formatString As String
  n = Len(CStr(objNum)) - 1    '……(1)'
  For i = 1 To n    '……(2)'
    formatString = formatString & "0"
  Next
  formatString = formatString & "#"
  For i = 1 To objNum
    Debug.Print Format(i, formatString)    '……(3)'
  Next
End Sub

(*)で、変数objNumに105を代入しているので、とりあえず105までの連番をゼロ埋めで作ることになる。

(1)の

n = Len(CStr(objNum)) - 1

でobjNum(今回は「105」)のケタ数(今回は「3」)から1引いた数を変数 n に代入している。

Len関数の引数なんだが、

Len(CStr(objNum))

と、objNumをString型にキャストして渡している。

別に

Len(objNum)

でも良さそうなもんだが、実際そうすると、

f:id:akashi_keirin:20170709215538j:plain

こんなふうに、Len関数の返り値が「2」になっている。

イミディエイト・ウインドウに

?Len(105)

とすると、

f:id:akashi_keirin:20170709215618j:plain

こんなふうにコンパイル・エラーになる。

ちょっとggってみると、こういうことらしい。

指定された文字列の文字数、または変数の格納に必要な名目上のバイト数を含む整数型の値を返します。

ということは、単純に

Len(objNum)

としたときの返り値はInteger型の変数のバイト数だったってことか。納得。

話を元に戻そう。

(2)からの4行

For i = 1 To n
  formatString = formatString & "0"
Next
formatString = formatString & "#"

では、Format関数の第2引数を作っている。

今回の例の場合、3ケタのゼロ埋め数字ができれば良いのだから、Format関数の第2引数は、

"00#"

になれば良い。

今回、数字の個数は「105」。すなわち、3ケタのゼロ埋めになれば良い。(1)の段階で変数 n には「2」が入っているので、まずForループで「"00"」ができて、Forループ終了。

最後に、「#」を付け加えるので、変数formatStringの中身はめでたく「00#」になっている。

これを(3)の

Debug.Print Format(i, formatString)

でFormat関数の第2引数として渡してDebug.Printでイミディエイト・ウインドウに変数 i を出力する。

実行結果

f:id:akashi_keirin:20170709215526j:plain

まず最初にこんなふうに表示され、

f:id:akashi_keirin:20170709215551j:plain

f:id:akashi_keirin:20170709215608j:plain

こんなふうに、ゼロ埋めで数字が出力された。

おわりに

ゼロ埋め連番のケタ数を、対象データの個数に応じて動的に設定できるので、それなりに重宝するかも。

@akashi_keirin on Twitter

任意のワークシートを新規Excelブックとして出力するクラス

ワークシートを新規Excelブックとして出力するクラス

ワークシートを新規Excekブックとして出力する

名簿なんかの一覧表をマクロで自動作成して、相手方にPDFで提供すると、しばしば

加工したいからExcelのままでくれ!

と言われることがよくある。

大量(っつっても大した量でもないけど)の元データやマクロだらけのブックをそのまま送りつけるわけにも行かないので、必要なシートだけを新しいブックにコピペして提供、という形になる。

ただ、それも1つや2つなら手作業でポンとやっておしまい、なんだが、次から次へ、となるとさすがにめんどくさい。

で、やってみたのが

akashi-keirin.hatenablog.com

なんだが、今後こうした作業が頻発することが予測されるのでクラス化しといた。

任意のワークシートを新規Excelブックとして出力するクラス

リスト1 クラスモジュール ExcelFileCreator
Option Explicit

Private Const NOT_INITIALIZED As Integer = 1
Private Const NOT_CREATED As Integer = 2

'Fields'
Private targetSheet_ As Worksheet
Private filePath_ As String
Private newWorkbook_ As Workbook
Private isInitialized_ As Boolean

'Accessor'
Public Property Get newWorkbook() As Workbook
  Set newWorkbook = newWorkbook_
End Property

'Constructor'
Public Sub init(ByVal Sh As Worksheet)
  '引数としてExcelファイル化したいWorksheetオブジェクトを受け取る'
  Set targetSheet_ = Sh
  isInitialized_ = True
End Sub

'Methods'
Public Sub createExcelFile(ByVal fullPath As String, _
                           Optional ByVal isOnlyToCreate As Boolean)
  '引数fullPathは、新たに作成するExcelファイルのフルパス'
  '新しいExcelファイルを作成するだけでよいなら、引数isOnlyToCreateをTrueにする'
  If isInitialized_ = False Then Call raiseError(NOT_INITIALIZED)
  filePath_ = fullPath
  Set newWorkbook_ = Workbooks.Add
  targetSheet_.Copy before:=newWorkbook_.Worksheets(1)
  Application.DisplayAlerts = False
  Dim i As Integer
  With newWorkbook_
    For i = .Worksheets.Count To 2 Step -1
        .Worksheets(i).Delete
    Next
  End With
  Application.DisplayAlerts = True
  If isOnlyToCreate = True Then Call closeCreatedFile
End Sub

Public Sub closeCreatedFile()
  If newWorkbook_ Is Nothing Then Call raiseError(NOT_CREATED)
  Application.DisplayAlerts = False
  newWorkbook_.SaveAs fileName:=filePath_
  newWorkbook_.Close False
  Application.DisplayAlerts = True
End Sub

Private Sub raiseError(ByVal typeOfError As Integer)
  If typeOfError = NOT_INITIALIZED Then
    Err.Raise Number:=10001, _
              Description:="インスタンス生成後にinitメソッドの使用が必要です。"
  End If
  If typeOfError = NOT_CREATED Then
    Err.Raise Number:=10002, _
              Description:="Workbook作成前に使用できません。"
  End If
End Sub

かなりタテ長になってしまったので、パーツごとに解説する。

目次
  1. 宣言セクション
  2. フィールド
  3. アクセサ
  4. コンストラクタ
  5. createExcelFileメソッド
  6. closeCreatedFileメソッド
  7. raiseErrorメソッド
宣言セクション
Private Const NOT_INITIALIZED As Integer = 1
Private Const NOT_CREATED As Integer = 2

今のところ定数を2つ設定している。誤った使い方をした場合にエラーを起こすようにしているが、どのエラーを起こすのかを区別するためのもの。

フィールド
Private targetSheet_ As Worksheet
Private filePath_ As String
Private newWorkbook_ As Workbook
Private isInitialized_ As Boolean

それぞれの変数は、

targetSheet_

Excelブック化する対象のワークシートを格納する。

filePath_

新規Excelブックのフルパスを格納する。

newWorkbook_

新規Excelブックを格納する。

isInitialized_

擬似コンストラクタinitメソッドが実行されたかどうかを格納する。initメソッド実行済みであればTrue。

アクセサ
Public Property Get newWorkbook() As Workbook
  Set newWorkbook = newWorkbook_
End Property

今のところ、外部に公開するプロパティとしては、新しく作成されたブックが格納されているnewWorkbook_フィールドだけで良いと思っている。必要があれば再検討する。

コンストラク
Public Sub init(ByVal Sh As Worksheet)
  Set targetSheet_ = Sh    '……(1)'
  isInitialized_ = True    '……(2)'
End Sub

おなじみ、擬似コンストラクタ。引数としてExcelブック化したい対象のWorksheetオブジェクトを渡して実行する。

(1)の

Set targetSheet_ = Sh

で、引数として受け取ったWorksheetオブジェクトを変数targetSheet_に格納。

(2)の

isInitialized_ = True

で変数isInitialized_をTrueにして、initメソッド実行済みかどうか分かるようにしている。

createExcelFileメソッド
Public Sub createExcelFile(ByVal fullPath As String, _
                           Optional ByVal isOnlyToCreate As Boolean)    '……(1)'
  If isInitialized_ = False Then Call raiseError(NOT_INITIALIZED)    '……(2)'
  filePath_ = fullPath
  Set newWorkbook_ = Workbooks.Add
  targetSheet_.Copy before:=newWorkbook_.Worksheets(1)
  Application.DisplayAlerts = False
  Dim i As Integer
  With newWorkbook_
    For i = .Worksheets.Count To 2 Step -1
        .Worksheets(i).Delete
    Next
  End With
  Application.DisplayAlerts = True
  If isOnlyToCreate = True Then Call closeCreatedFile    '……(3)'
End Sub

(1)の

Public Sub createExcelFile(ByVal fullPath As String, _
                           Optional ByVal isOnlyToCreate As Boolean = True)

で、引数を2つ設定している。

第1引数fullPathは、新規に作成するExcelブックのフルパス。

第2引数isOnlyToCreateは、Trueにすると指定したシートをそのままExcelブック化する。シートに何らかの加工をしてからExcelブック化したいなら、この引数をFalseにしておく必要がある。

(2)の

If isInitialized_ = False Then Call raiseError(NOT_INITIALIZED)

では、このクラスのインスタンス生成後、initメソッドが実行されているかどうかを判定し、未実行ならraiseErrorメソッドを呼び出してエラーを起こさせるようにしている。

さて、このメソッドのメインの処理については、前に書いたのと全く同じなので、詳しいことは

akashi-keirin.hatenablog.com

をどうぞ。

(3)の

If isOnlyToCreate = True Then Call closeCreatedFile

では、第2引数の値によって、このまま新規ブックの保存・終了に進むのかどうかを分岐している。

isOnlyToCreateがTrueならば、後は新規ブックを保存して終了するだけなので、closeCreatedFileメソッドを呼び出して終了。Falseだったら一旦ここで処理を中断して標準モジュールのメインのコードに戻ることになる。

closeCreatedFileメソッド
Public Sub closeCreatedFile()
  If newWorkbook_ Is Nothing Then Call raiseError(NOT_CREATED)    '……(1)'
  Application.DisplayAlerts = False
  newWorkbook_.SaveAs fileName:=filePath_
  newWorkbook_.Close False
  Application.DisplayAlerts = True
End Sub

こちらは、新しく作成されたExcelブックに新しいファイル名を付けて保存し、ファイルを閉じる、という処理をしているだけ。

ただ、そもそも新規のブックが作成されてもいないのに、このメソッドを先に実行されてしまったら困るので、(1)の

If newWorkbook_ Is Nothing Then Call raiseError(NOT_CREATED)

で、新規ブック未作成ならcloseCreatedFileメソッドを呼んでエラーを起こさせるようにした。

raiseErrorメソッド
Private Sub raiseError(ByVal typeOfError As Integer)    '……(1)'
  If typeOfError = NOT_INITIALIZED Then
    Err.Raise Number:=10001, _
              Description:="インスタンス生成後にinitメソッドの使用が必要です。"
  End If
  If typeOfError = NOT_CREATED Then
    Err.Raise Number:=10002, _
              Description:="Workbook作成前に使用できません。"
  End If
End Sub

(1)の

Private Sub raiseError(ByVal typeOfError As Integer)

では引数typeOfErrorを設定している。

あとは、受け取ったtypeOfErrorの値に応じてErrオブジェクトのRaiseメソッドを用いてエラーを吐かせているだけ。

今後、新たなエラー原因が分かったら逐次追加していけば良い。

使用例 その1

f:id:akashi_keirin:20170702093636j:plain

こんなシートをアクティブにして次のリスト2-1のコードを実行してみる。

リスト2-1 標準モジュール
Public Sub createExcelFileTest1()
  Dim Sh As Worksheet
  Set Sh = ActiveSheet    '……(1)'
  Dim str As String
  str = ThisWorkbook.Path & "\超人墓場\"    '……(2)'"
  str = str & "都道府県別1.xlsx"
  Dim xlsxCreator As ExcelFileCreator    '……(3)'
  Set xlsxCreator = New ExcelFileCreator    '……(4)'
  xlsxCreator.init Sh    '……(5)'
  xlsxCreator.createExcelFile str, True    '……(6)'
  End With
End Sub

(1)の

Set Sh = ActiveSheet

で、アクティブシートを一旦変数Shに格納。

(2)からの2行

str = ThisWorkbook.Path & "\超人墓場\"  '"
str = str & "都道府県別1.xlsx"

では、新しく作成するブックのフルパスを変数strに格納。2回に分けているのは、可読性とメンテナンス性のため。別に1回で書いてもいい。

(3)の

Dim xlsxCreator As ExcelFileCreator

で、ExcelFileCreatorクラスのインスタンス用の変数xlsxCreatorを準備し、

(4)の

Set xlsxCreator = New ExcelFileCreator

でExcelFileCreatorクラスのインスタンスを生成して変数xlsxCreatorに格納。

(5)の

xlsxCreator.init Sh

でExcelFileCreatorクラスのinitメソッドにSh(対象のWorksheetオブジェクト)を渡して実行。

(6)の

xlsxCreator.createExcelFile str, True

でExcelFileCreatorクラスのcreateExcelFileメソッドにstr(新規ブックのフルパス)を渡して実行。第2引数がTrueなので、指定したWorksheetオブジェクトに何も加工せずに保存・終了することになる。

実行結果

f:id:akashi_keirin:20170702093647j:plain

このように、「超人墓場」フォルダに「都道府県別1.xlsx」ができていて、開くと

f:id:akashi_keirin:20170702093700j:plain

この通り、シートがそのまま保存されている。

ただ、このままだと、いかにもA~C列が邪魔。かといって手動で削除するとなると果てしなくダルい。

そこで、新しいブックを作った後、A~C列を削除した後で保存・終了する、という処理にしてみる。

使用例 その2

リスト2-1を次のように書き換える。

リスト2-2 標準モジュール
Public Sub createExcelFileTest2()
  Dim Sh As Worksheet
  Set Sh = ActiveSheet
  Dim str As String
  str = ThisWorkbook.Path & "\超人墓場\"  '"
  str = str & "都道府県別2.xlsx"
  Dim xlsxCreator As ExcelFileCreator
  Set xlsxCreator = New ExcelFileCreator
  With xlsxCreator
    .init Sh
    .createExcelFile str, False    '……(1)'
    .newWorkbook.Worksheets(1).Columns("A:C").Delete    '……(2)'
    .closeCreatedFile    '……(3)'
  End With
End Sub

変わったのは、

With xlsxCreator
  .init Sh
  .createExcelFile str, False    '……(1)'
  .newWorkbook.Worksheets(1).Columns("A:C").Delete    '……(2)'
  .closeCreatedFile    '……(3)'
End With

この部分だけ。

(1)でcreateExcelFileメソッドを呼び出すときの第2引数をFalseにし、

(2)でA~C列を削除してから、

(3)でcloseCreatedFileメソッドを呼び出して新規ブックを保存・終了している。

実行結果

f:id:akashi_keirin:20170702093721j:plain

このように、「超人墓場」フォルダに「都道府県別2.xlsx」ができていて、開くと

f:id:akashi_keirin:20170702093731j:plain

この通り、A~C列だった部分が削除されてシートが保存されている。

おわりに

今回紹介した程度の処理なんて、フツーに考えたらSubとかFunctionのレベルで再利用すればよいものだと思うが、一連の作業をやっていて、改めて「クラス」って便利だなーと思った次第。

実は、closeCreatedFileメソッドというのは、もともと一連の処理だったものの最後の部分だけを切り出したものなんだが、クラスにしたおかげでめちゃくちゃ簡単に切り出すことができた。

というのも、保存して閉じる、という作業に必要なデータである新規ブックのフルパスを、このクラスのインスタンスが既に保持しているので、いちいちこのメソッドにフルパスを引数として与えなくても済むから。

処理を小分けにして切り出す、という作業をやっていていつもぶつかるのが、

引数祭り

の問題。

処理に必要なパラメータが多いときに、プロシージャ間でのデータのやりとりがめちゃくちゃ煩雑になってしまうのだ。もちろん、変数のスコープを広げたら済む話なんだが、むやみやたらと変数のスコープを広げるのはやっぱり怖い。

その点、クラスのインスタンスにデータを持たせていたら安心。インスタンスが生きている限りいつでもデータを参照できるので。

しかも、単独のSubとかFunctionの場合、ファイル単位ではエクスポートできないけど、クラスモジュールならファイル単位でエクスポートできる。

やっぱりハードVBAerならクラスモジュールを使わない手はないよなあ。

「クソ」としか言いようのない魔Excelに勝利した

クソのようなExcel様式に打ち勝った

クソとしか言いようのないExcel様式

「クソ」としか言いようのないExcel様式に出会った。

簡単に言うと、

1つのセルに複数データが入っている

というもの。

しかも、[Alt]+[Enter]によるセル内改行ばかりかと思ったら、スペース連打で成り行き改行まで含まれている始末。

一瞬、軽く途方に暮れかけたが、幸い改行のしかたはともかくとして、全て箇条書きの体になっていたので、それらを1セル1データになるようにする方法を考えた。

1セル1データに整形するための考え方

次のように考えた。

  1. 改行記号を「@」(データ文字列内に決して出てこない記号)に置き換える
  2. 2つ以上連なっているスペースも「@」に置き換える
  3. 2つ以上「@」が連なっている部分を「@」が1つになるまで置き換える
    ここまでで、必要なデータが間に「@」が挟まった状態で1つの文字列になっているはず。
  4. Split関数で各データを配列に格納する。
  5. (データ数-1)行、挿入する
  6. データをセルに書き込んでいく

このような手順。

実装

リスト1
Public Sub splitStrings()
  Dim Sh As Worksheet
  Set Sh = ActiveSheet
  Dim objCell As Range
  Set objCell = ActiveCell
  Dim str As String
  str = objCell.Value
  str = Replace(str, vbCrLf, "@")    '……(1)'
  str = Replace(str, vbCr, "@")
  str = Replace(str, vbLf, "@")
  str = Replace(str, "  ", "@")  '半角スペース2つ'
  str = Replace(str, "  ", "@")  '半角スペースと全角スペース'
  str = Replace(str, "  ", "@")  '全角スペースと半角スペース'
  str = Replace(str, "  ", "@")  '全角スペース2つ'
  Do While InStr(str, "@@") > 0    '……(2)'
    str = Replace(str, "@@", "@")
  Loop
  Dim arrayStr As Variant    '……(3)'
  arrayStr = Split(str, "@")
  Dim i As Integer    '……(4)'
  For i = 1 To UBound(arrayStr)
    Sh.Rows(objCell.Row + i).Insert shift:=xlShiftDown
  Next
  For i = 0 To UBound(arrayStr)    '……(5)'
    With objCell.Offset(i, 0)
      .Value = arrayStr(i)
    End With
  Next
End Sub

まず、(1)からの7行、

str = Replace(str, vbCrLf, "@")
str = Replace(str, vbCr, "@")
str = Replace(str, vbLf, "@")
str = Replace(str, "  ", "@")  '半角スペース2つ'
str = Replace(str, "  ", "@")  '半角スペースと全角スペース'
str = Replace(str, "  ", "@")  '全角スペースと半角スペース'
str = Replace(str, "  ", "@")  '全角スペース2つ'

では、変数strに格納したセル内の文字列について、改行記号や2つ連なったスペースを「@」に置換している。

そして、(2)からの3行、

Do While InStr(str, "@@") > 0
  str = Replace(str, "@@", "@")
Loop

では、2つ以上の「@」の連なりがある限り「@@」を「@」に置換する、という処理を行っている。従って、このループ処理を抜けると、

もともと改行記号や2つ以上のスペースで区切られていた複数の文字列が「@」1つで区切られた状態の文字列

になっているということ。

あとは、(3)からの2行

Dim arrayStr As Variant
arrayStr = Split(str, "@")

で「@」で区切られた文字列をSplit関数でバラして配列変数arrayStrに突っ込み(要素数が未確定なのでVariant型の変数で受ける)、

(4)からの4行、

Dim i As Integer
For i = 1 To UBound(arrayStr)
  Sh.Rows(objCell.Row + i).Insert shift:=xlShiftDown
Next

で必要な数だけ行を挿入し、

(5)からの5行、

For i = 0 To UBound(arrayStr)
  With objCell.Offset(i, 0)
    .Value = arrayStr(i)
  End With
Next

でそれぞれ1セル1データの形で書き込んでおしまい。

実行結果

f:id:akashi_keirin:20170630223732j:plain

こんなふうにアホみたいに書き込まれた5つのデータ。

見た目では分からないが、③と④の間は、セル内改行ではなくスペース連打による見かけ上の改行w

こんなデタラメなデータでも、このセルを選択してマクロを実行すると、

f:id:akashi_keirin:20170630223827j:plain

ほれ、この通り。1セル1データになった。

ちなみに、

f:id:akashi_keirin:20170630223836j:plain

こんなふうに、セル内改行連打の場合でも、

f:id:akashi_keirin:20170630223857j:plain

大丈夫です。

おわりに

それにしても、アクロバチックなExcelの使い方をする人が多すぎて、マジでメンタルやられそうです。。。

@akashi_keirin on Twitter

インタフェースを用いたポリモーフィズム

インタフェースを使ったポリモーフィズム

コードの微修正

akashi-keirin.hatenablog.com

前回のコードをちょっと書き換えてみた。

擬似コンストラクタのinitメソッドをまとめてみただけですが。

リスト1 インタフェース IMusicPlayer
Option Explicit
'Fields'
Private Name_ As String
'Accessor'
Public Property Get Name() As String
  Name = Name_
End Property
'Methods'
Public Sub init(ByVal n As String)
End Sub
Public Sub play()
End Sub
リスト2-1 RecordPlayerクラス
Option Explicit
Implements IMusicPlayer
'Fields'
Private IMusicPlayer_Name_ As String
'Accessor'
Public Property Get IMusicPlayer_Name() As String
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property
'Methods'
Public Sub IMusicPlayer_init(ByVal n As String)
  IMusicPlayer_Name_ = n
End Sub
Public Sub IMusicPlayer_play()
  Debug.Print "レコードを再生するぜ~♪" & vbCrLf & _
              "針が飛ぶから、暴れるんじゃねーぞw"
End Sub
リスト2-2 CDPlayerクラス
Option Explicit
Implements IMusicPlayer
'Fields'
Private IMusicPlayer_Name_ As String
'Accessor'
Public Property Get IMusicPlayer_Name() As String
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property
'Methods'
Public Sub IMusicPlayer_init(ByVal n As String)
  IMusicPlayer_Name_ = n
End Sub
Public Sub IMusicPlayer_play()
  Debug.Print "CDを再生するぜ~♪"
End Sub
リスト2-3 MP3Playerクラス
Option Explicit
Implements IMusicPlayer
'Fields'
Private IMusicPlayer_Name_ As String
'Accessor'
Public Property Get IMusicPlayer_Name() As String
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property
'Methods'
Public Sub IMusicPlayer_init(ByVal n As String)
  IMusicPlayer_Name_ = n
End Sub
Public Sub IMusicPlayer_play()
  Debug.Print "MP3を再生するぜ~♪"
End Sub

見てお分かりの通り、RecordPlayer、CDPlayer、MP3Playerクラスそれぞれに、「IMusicPlayer_init」メソッドを置いてある。

実行用コード

リスト3 標準モジュール
Public Sub musicPlayerTest()
  'インタフェース型配列に各インスタンスを格納'
  Dim iMPlayer(0 To 2) As IMusicPlayer
  Set iMPlayer(0) = New RecordPlayer
  iMPlayer(0).init "レコードプレーヤー1号"
  Set iMPlayer(1) = New CDPlayer
  iMPlayer(1).init "CDプレーヤ1号"
  Set iMPlayer(2) = New MP3Player
  iMPlayer(2).init "ストロングマシン1号"
  'Nameプロパティの出力とplayメソッドの実行'
  Dim i As Integer
  For i = 0 To 2
    With iMPlayer(i)
      Debug.Print .Name
      .play
    End With
  Next
End Sub

実行結果は前回と全く同じなので省略。

おわりに

initメソッドにしても、playメソッドにしても同じような処理なので、これだけだとイマイチありがた味がないけれど、それぞれのクラスが持つメソッドが

似た機能を実現するんだけれども処理の過程が全然違う

ようなものだったら、ポリモーフィズムの威力が分かりやすくなるかも。

たとえば、ThunderbirdのメールとLotusNotesのメールの作成をどちらも同じcreateMailメソッドの呼び出しだけでできるようにするとか。

何にせよ、うまく使いこなせるようになりたいね。

Wordドキュメントの表の行を削除する

Wordの表の行を削除する

Wordドキュメント内の表の任意の行を削除する

差込印刷機能を使って似たような書類のWordファイルを自動的に作成するマクロをよく使う。

akashi-keirin.hatenablog.com

Excelで表さえ作ってしまえば、人力でポチポチやるよりも圧倒的に正確かつ高速なので重宝している。

ただ、表を伴うようなもの、しかも表の行数がそれぞれ異なるようなものを作成する羽目になった。

そうしょっちゅう使うこともないと思うので、覚書代わりにアップしとく。

表の行を削除するマクロ

f:id:akashi_keirin:20170625165157j:plain

Wordドキュメントにこんな表があったとする。

何行目かが分かりやすいように表の先頭行以外の左端の列には行番号を入れといた。

リスト1 表の5行目を削除するマクロ
Public Sub tableTest()
  Dim tbl As Table    '……(1)'
  Set tbl = ThisDocument.Tables(1)
  tbl.Rows(5).Delete    '……(2)'
End Sub

(1)からの2行、

Dim tbl As Table
Set tbl = ThisDocument.Tables(1)

では、Table型の変数tblを準備して、ThisDocument上の1つ目の表を格納している。

で、(2)の

tbl.Rows(5).Delete

ではtblに格納したTableオブジェクトのRowsコレクションのうち、5番目のRowオブジェクトに対してDeleteメソッドを使用。

すると、

f:id:akashi_keirin:20170625165205j:plain

こうなる。簡単!

任意の行を削除するFunction

作ってみた。

スト2 表の任意の行を削除するFunction
Private Function deleteRowFromTable(ByVal objTable As Table, _
                                    ByVal rowNum As Integer) As Table    '……(1)'
On Error GoTo errorCatch    '……(2)'
  objTable.Rows(rowNum).Delete
  Set deleteRowFromTable = objTable    '……(3)'
  Exit Function
errorCatch:    '……(*)'
  Set deleteRowFromTable = objTable
End Function

(1)は、

Private Function deleteRowFromTable(ByVal objTable As Table, _
                                    ByVal rowNum As Integer) As Table

の形で引数と返り値を設定。

第1引数がTableオブジェクト、第2引数は削除したい行番号。

で、返り値をTableオブジェクトにした。

行削除後のTableオブジェクトを返すようにしたら便利じゃないかと思っただけw

(2)の

On Error GoTo errorCatch

はエラー対策。今のところ

  • 存在しない行番号を引数として与えた

ぐらいしかエラーの原因は思いつかないけれど、メンドウなのでこの形を使った。

エラーが発生すると、(*)の

errorCatch:
  Set deleteRowFromTable = objTable

に飛んで、処理する。返り値には何もせずに元の表を返す。

(3)の

Set deleteRowFromTable = objTable

で処理済みの表を返す。

実行結果

リスト2のFunctionを、次のコードで使ってみる。

リスト3 実行用コード
Public Sub tableTest()
  Dim tbl As Table
  Set tbl = ThisDocument.Tables(1)
  Debug.Print "使用前:" & tbl.Rows.Count    '……(1)'
  Set tbl = deleteRowFromTable(tbl, 5)
  Debug.Print "使用後:" & tbl.Rows.Count    '……(2)'
End Sub

(1)で実行前の表の行数をイミディエイト・ウインドウに表示し、(2)で実行後の表の行数を表示するようにした。

f:id:akashi_keirin:20170625165219j:plain

ほれ、この通り、指定した行を削除した上で、処理後の表を獲得することができている。

終わりに

WordのVBAは、あまり使う機会がないので、たまに使ったときにちゃんと記録を残しておかないとなあ。

@akashi_keirin on Twitter

LotusNotesのメール作成・送信をフルオート化してみた

LotusNotesでメール作成→送信をフルオートにする

LotusNotesでメールを送る作業が大量発生したときは、メール自動作成ツールで対応している。

akashi-keirin.hatenablog.com

ただ、誤送信が怖いので、基本的にメール作成までは自動でやっても、送信自体は手作業でやっていた。

しかしながら、ついに背に腹は替えられぬようになり、作成・送信フルオートをやってみた。

作業の手順としては、

  1. Excelファイルにメール作成用のデータを作成
  2. 1.をもとに、このときのマクロを使ってメールを作成
  3. 送信
  4. 文書を閉じて次へ

といった感じ。

フルオート送信の壁

これまでのマクロに、「送信」という過程と「次へ移る」という過程を付け加えるだけなので、楽勝だと思っていたんだが、いざやってみると、

  • 送信後、閉じるときに「……保存しますか? [はい][いいえ]」という確認メッセージが出てしまう
  • メール作成後のNotesDocumentクラスのSaveメソッドを実行しなければ確認メッセージは出ないが、送信履歴に残らない

という問題が生じた。

復習~LotusNotesのメールができるまで

スキップ

ちょっと復習として、LotusNotesでメールができるまでの処理の過程をおさらいしておく。興味がなかったら上の「スキップ」リンクを踏んだら下にワープするのでどうぞ。

やっとクラスリファレンスの読み方が分かってきたので、自身の勉強のために書くだけ。よって、説明の中には素人の推定によるものも含まれているw

Notesメールができるまで
  1. NotesSessionクラスのインスタンスを作成
    →これで現在使用中のNotesのいろんなものが使える。
  2. NotesUIWorkSpaceクラスのインスタンスを作成
    →これでNotesのワークスペースのうち、現在使用中のウインドウが使える
  3. NotesDatabaseクラスのインスタンスを作成
    →これで現在使用中のNotesデータベースにアクセスできる。
  4. NotesDatabaseクラスのOpenMailメソッドで現在のユーザーのメールデータベースを開く
    →これでメールデータベースが使える。
  5. NotesDatabaseクラスのCreateDocumentメソッドを用いて、NotesDocumentクラスのインスタンスを作成
  6. NotesDocumentクラスのインスタンスに、宛先、件名、受信確認の有無など諸データをセット
  7. NotesDocumentクラスのCreateRichTextItemメソッドを用いて、NotesRichTextItemクラスのインスタンスを作成
  8. NotesSessionクラスのCreateRichTextStyleメソッドを用いて、NotesRichTextStyleクラスのインスタンスを作成
  9. NotesRichTextItemクラスのAppendTextメソッド等を用いて本文を作成
  10. 必要ならNotesRichTextItemクラスのEmbdedObjectメソッドを用いて添付ファイルを追加
  11. NotesDocumentクラスのSaveメソッドを用いて文書を保存する
    →これで作成したメールがドラフトに保存される。
  12. NotesUIWorkSpaceクラスのEditDocumentメソッドを用いて、NotesUIDocumentクラスのインスタンスを作成
    →これで画面に作成したメールが表示される。

とまあ、こんな手順。

ちょっとづつ分かってきたような気がする。

確認メッセージの表示を防止するのは簡単

上掲Notesメールができるまで11.12.の過程は、

リスト1
notesDocument.Save False, False
Set notesUIDocument = notesUIWorkSpace.EditDocument(True, notesDocument, False)

というコードになる。

で、これに「送信→閉じる」という過程を付け加えれば、あとは処理全体をループで回すだけなのだが、単純に

スト2
notesDocument.Save False, False
Set notesUIDocument = notesUIWorkSpace.EditDocument(True, notesDocument, False)
CallByName notesUIDocument, "Send", VbMethod, False
notesUIDocument.Close True    '……(*)'

と書いて実行すると(*)を実行したところで、保存確認のメッセージボックスが出てきてしまう。

VBAでいうところの「Saved」みたいなプロパティはないものか、とクラスリファレンスを探してみると、NotesUIDocumentクラスのリファレンスSaveメソッドというやつがあった。

そこで、リスト2

リスト3
notesDocument.Save False, False
Set notesUIDocument = notesUIWorkSpace.EditDocument(True, notesDocument, False)
CallByName notesUIDocument, "Send", VbMethod, False
notesUIDocument.Save    '……(*)'
notesUIDocument.Close True

このたった1行(*)を加えるだけでうまく行ったw

おかげで、80件近くの宛先に文面も添付ファイルも異なるメールを送る、という苦行が便所に行っている間に完了w

おわりに

一昨年の今頃は、まったく意味も分からずに使っていたマクロだが、「クラス」の概念が理解できてきたことによってLotusScriptのクラスリファレンスがだんだん読めるようになってきた。

その結果、コードの意味が分かるようになり、いろいろアレンジを加えることができるようになってきた。

オブジェクト指向を勉強してよかったなあ、と思う今日この頃。

参考~LotusScriptのクラスリファレンス

VBAでインタフェースを使ってみた

インタフェースを用いたポリモーフィズムをやってみた

立山秀利さんが著書の中で使っていた音楽プレーヤのたとえが私にとっては一番分かりやすかったので、それをVBAでやってみる。

方針としては、

  1. 「RecordPlayer」クラス、「CDPlayer」クラス、「MP3Player」クラスの3つのクラスを作る
  2. 3つのクラスに共通する「音楽を再生する」という機能を「play」メソッドとして括り出す
  3. 「IMusicPlayer」インタフェースに「play」メソッドを定義する
  4. 「RecordPlayer」、「CDPlayer」、「MP3Player」の各クラスで「IMusicPlayer」インタフェースを実装する
  5. 「RecordPlayer」、「CDPlayer」、「MP3Player」の各クラスで「play」メソッドを独自に定義する

これでポリモーフィズムが実現できるはず。

イメージとしては、

音楽を再生する方法はどうあれ、ともかく「play」と命令すればそれぞれのオブジェクトがそれぞれのやり方で音楽を再生する

といったところか。

インタフェースの準備

リスト1 インタフェース「IMusicPlayer」
'クラスモジュール'
'オブジェクト名「IMusicPlayer」'
Option Explicit
'Fields'
Private Name_ As String
'Accessor'
Public Property Get Name() As String
  Name = Name_
End Property
'Methods'
Public Sub play()
End Sub

「Name」というプロパティ(フィールド)を持たせていること、あとは「play」というメソッドを持たせているだけ。

特に、メソッドについては、名前を定義しているだけで処理の中身は空っぽ。

具体的な処理は、このインタフェースを実装する各クラスで独自に定義するのだから、これでいいのだ(ですよね?)。

各クラスでのインタフェースの実装

リスト2-1 「RecordPlayer」クラス
'クラスモジュール'
'オブジェクト名「RecordPlayer」'
Option Explicit
Implements IMusicPlayer    '……(1)'
'Fields'
Private IMusicPlayer_Name_ As String    '……(2)'
'Accessor'
Public Property Get IMusicPlayer_Name() As String    '……(3)'
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property
'Methods'
Public Sub init(ByVal n As String)
  IMusicPlayer_Name_ = n    '……(4)'
End Sub

Public Sub IMusicPlayer_play()    '……(5)'
  Debug.Print "レコードを再生するぜ~♪" & vbCrLf & _
              "針が飛ぶから、暴れるんじゃねーぞw"
End Sub

ほとんど通常のクラスモジュールと同じ書き方なんだけど、至る所に「IMusicPlayer」というインタフェース名が出てくるところがポイント。

まず(1)、

Implements IMusicPlayer

宣言セクションでこう書く。変なの。

(2)と(3)、フィールド・アクセサの設定も、

Private IMusicPlayer_Name_ As String
Public Property Get IMusicPlayer_Name() As String
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property

「Name」というフィールド(プロパティ)名にいちいち「IMusicPlayer」をスネーク記法で付けないといけない。これも何だかなー。

(4)の

IMusicPlayer_Name_ = n

では、擬似コンストラクタのinitメソッドの引数を受け取っている。当然ここにも「IMusicPlayer」が……。

あと、(5)のメソッド名の定義、

Public Sub IMusicPlayer_play()

にもやっぱり「IMusicPlayer」……。

仕様とはいえ、何だか美しくないんだよなー……。

あと、「CDPlayer」、「MP3Player」についても、やり方は全く同じなので、リストだけ載っけときます。

リスト2-2 「CDPlayer」クラス
'クラスモジュール'
'オブジェクト名「CDPlayer」'
Option Explicit
Implements IMusicPlayer
'Fields'
Private IMusicPlayer_Name_ As String
'Accessor'
Public Property Get IMusicPlayer_Name() As String
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property
'Methods'
Public Sub init(ByVal n As String)
  IMusicPlayer_Name_ = n
End Sub

Public Sub IMusicPlayer_play()
  Debug.Print "CDを再生するぜ~♪"
End Sub
リスト2-3 「MP3Player」クラス
'クラスモジュール'
'オブジェクト名「MP3Player」'
Option Explicit
Implements IMusicPlayer
'Fields'
Private IMusicPlayer_Name_ As String
'Accessor'
Public Property Get IMusicPlayer_Name() As String
  IMusicPlayer_Name = IMusicPlayer_Name_
End Property
'Methods'
Public Sub init(ByVal n As String)
  IMusicPlayer_Name_ = n
End Sub

Public Sub IMusicPlayer_play()
  Debug.Print "MP3を再生するぜ~♪"
End Sub

長ったらしくなってすまん。

まあ、これで3つのクラス全てに「IMusicPlayer」インタフェースを実装したことになる。

(「実装」の使い方、これで合ってるんだろうか……?)

ポリモーフィズムをやってみる

次のコードで各種音楽プレーヤを使ってみる。

Option Explicit
Public Sub musicPlayerTest()
  'インスタンス用変数を準備'    '……(1)'
  Dim cdp As CDPlayer
  Dim rp As RecordPlayer
  Dim mp3p As MP3Player
  'インスタンス生成&擬似コンストラクタ発動'    '……(2)'
  Set rp = New RecordPlayer
  rp.init "レコードプレーヤー1号"
  Set cdp = New CDPlayer
  cdp.init "CDプレーヤ1号"
  Set mp3p = New MP3Player
  mp3p.init "ストロングマシン1号"
  'インタフェース型配列に各インスタンスを格納'    '……(3)'
  Dim iMPlayer(0 To 2) As IMusicPlayer    '……(4)'
  Set iMPlayer(0) = rp    '……(5)'
  Set iMPlayer(1) = cdp
  Set iMPlayer(2) = mp3p
  'Nameプロパティの出力とplayメソッドの実行'    '……(6)'
  Dim i As Integer
  For i = 0 To 2
    With iMPlayer(i)
      Debug.Print .Name    '……(7)'
      .play    '……(8)'
    End With
  Next
End Sub

(1)の後の3行

Dim cdp As CDPlayer
Dim rp As RecordPlayer
Dim mp3p As MP3Player

で各音楽プレーヤ用の変数を準備。

(2)の後の6行では、たとえば「RecordPlayer」クラスの場合、

Set rp = New RecordPlayer
rp.init "レコードプレーヤー1号"

こんなふうに、インスタンスを生成した後、擬似コンストラクタinitメソッドでNameプロパティを設定している。

「CDPlayer」にしても「MP3Player」にしてもやっていることは同じ。

で、(3)からは、(4)の

Dim iMPlayer(0 To 2) As IMusicPlayer

インタフェース「IMusicPlayer」型の配列変数を用意して、

(5)からの

Set iMPlayer(0) = rp
Set iMPlayer(1) = cdp
Set iMPlayer(2) = mp3p

でそれぞれのクラスのインスタンスを配列に格納している。

後は、(6)の後の7行、

Dim i As Integer
For i = 0 To 2
  With iMPlayer(i)
    Debug.Print .Name    '……(7)'
    .play    '……(8)'
  End With
Next

で、配列の各要素について、(7)で「Name」プロパティを表示させ、(8)でplayメソッドを実行している。

このとき、配列「iMPlayer」の各要素は、それぞれ別々のクラスのインスタンスなのに、全て同じ名前でプロパティやメソッドが呼び出せているというところがミソ。

実行結果

f:id:akashi_keirin:20170619050311j:plain

ほれ。同じメソッド名で呼び出したにもかかわらず、それぞれのクラスがそれぞれのやり方でplayメソッドを実行していることが分かる。

おわりに

正直、まだインタフェースの使いどころについてはピンときていないが、実際にコードを書いてみると、思ったより簡単だった。やっぱり、実際にコードを書いて動かしてみるというのが大事なんだなあ。

……と、ここまで書いてきてから気づいたんだが、

initメソッドもまとめてしまったらよかったんじゃね?

……orz

f:id:akashi_keirin:20170619050303j:plain

ちなみに、クラス側でちゃんとメソッドを置かないと、こんなふうに叱られる。