ゼロ埋め連番を作成する
Format関数の第2引数は、変数でも良かった
ゼロ埋め連番文字列を動的に生成する
たとえば、
こんなマクロを使って大量にファイルを生成するような場合、ファイル名の先頭がゼロ埋め連番になっていると非常に都合が良い。
で、やってみた。
考え方
次のような手順で、適切な桁数のゼロ埋め連番文字列が得られると考えた。
- 個数の桁数を数える
- 「0」を「個数の桁数-1」個連結した文字列を作り、変数に格納する
- 2.でできた文字列に、「#」を連結し、変数に格納する
- 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)
でも良さそうなもんだが、実際そうすると、
こんなふうに、Len関数の返り値が「2」になっている。
イミディエイト・ウインドウに
?Len(105)
とすると、
こんなふうにコンパイル・エラーになる。
ちょっと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 を出力する。
実行結果
まず最初にこんなふうに表示され、
こんなふうに、ゼロ埋めで数字が出力された。
おわりに
ゼロ埋め連番のケタ数を、対象データの個数に応じて動的に設定できるので、それなりに重宝するかも。
任意のワークシートを新規Excelブックとして出力するクラス
ワークシートを新規Excelブックとして出力するクラス
ワークシートを新規Excekブックとして出力する
名簿なんかの一覧表をマクロで自動作成して、相手方にPDFで提供すると、しばしば
加工したいからExcelのままでくれ!
と言われることがよくある。
大量(っつっても大した量でもないけど)の元データやマクロだらけのブックをそのまま送りつけるわけにも行かないので、必要なシートだけを新しいブックにコピペして提供、という形になる。
ただ、それも1つや2つなら手作業でポンとやっておしまい、なんだが、次から次へ、となるとさすがにめんどくさい。
で、やってみたのが
なんだが、今後こうした作業が頻発することが予測されるのでクラス化しといた。
任意のワークシートを新規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
かなりタテ長になってしまったので、パーツごとに解説する。
目次
宣言セクション
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メソッドを呼び出してエラーを起こさせるようにしている。
さて、このメソッドのメインの処理については、前に書いたのと全く同じなので、詳しいことは
をどうぞ。
(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
こんなシートをアクティブにして次のリスト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オブジェクトに何も加工せずに保存・終了することになる。
実行結果
このように、「超人墓場」フォルダに「都道府県別1.xlsx」ができていて、開くと
この通り、シートがそのまま保存されている。
ただ、このままだと、いかにも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メソッドを呼び出して新規ブックを保存・終了している。
実行結果
このように、「超人墓場」フォルダに「都道府県別2.xlsx」ができていて、開くと
この通り、A~C列だった部分が削除されてシートが保存されている。
おわりに
今回紹介した程度の処理なんて、フツーに考えたらSubとかFunctionのレベルで再利用すればよいものだと思うが、一連の作業をやっていて、改めて「クラス」って便利だなーと思った次第。
実は、closeCreatedFileメソッドというのは、もともと一連の処理だったものの最後の部分だけを切り出したものなんだが、クラスにしたおかげでめちゃくちゃ簡単に切り出すことができた。
というのも、保存して閉じる、という作業に必要なデータである新規ブックのフルパスを、このクラスのインスタンスが既に保持しているので、いちいちこのメソッドにフルパスを引数として与えなくても済むから。
処理を小分けにして切り出す、という作業をやっていていつもぶつかるのが、
引数祭り
の問題。
処理に必要なパラメータが多いときに、プロシージャ間でのデータのやりとりがめちゃくちゃ煩雑になってしまうのだ。もちろん、変数のスコープを広げたら済む話なんだが、むやみやたらと変数のスコープを広げるのはやっぱり怖い。
その点、クラスのインスタンスにデータを持たせていたら安心。インスタンスが生きている限りいつでもデータを参照できるので。
しかも、単独のSubとかFunctionの場合、ファイル単位ではエクスポートできないけど、クラスモジュールならファイル単位でエクスポートできる。
やっぱりハードVBAerならクラスモジュールを使わない手はないよなあ。
「クソ」としか言いようのない魔Excelに勝利した
クソのようなExcel様式に打ち勝った
クソとしか言いようのないExcel様式
「クソ」としか言いようのないExcel様式に出会った。
簡単に言うと、
1つのセルに複数データが入っている
というもの。
しかも、[Alt]+[Enter]によるセル内改行ばかりかと思ったら、スペース連打で成り行き改行まで含まれている始末。
一瞬、軽く途方に暮れかけたが、幸い改行のしかたはともかくとして、全て箇条書きの体になっていたので、それらを1セル1データになるようにする方法を考えた。
1セル1データに整形するための考え方
次のように考えた。
- 改行記号を「@」(データ文字列内に決して出てこない記号)に置き換える
- 2つ以上連なっているスペースも「@」に置き換える
- 2つ以上「@」が連なっている部分を「@」が1つになるまで置き換える
ここまでで、必要なデータが間に「@」が挟まった状態で1つの文字列になっているはず。 - Split関数で各データを配列に格納する。
- (データ数-1)行、挿入する
- データをセルに書き込んでいく
このような手順。
実装
リスト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データの形で書き込んでおしまい。
実行結果
こんなふうにアホみたいに書き込まれた5つのデータ。
見た目では分からないが、③と④の間は、セル内改行ではなくスペース連打による見かけ上の改行w
こんなデタラメなデータでも、このセルを選択してマクロを実行すると、
ほれ、この通り。1セル1データになった。
ちなみに、
こんなふうに、セル内改行連打の場合でも、
大丈夫です。
おわりに
それにしても、アクロバチックなExcelの使い方をする人が多すぎて、マジでメンタルやられそうです。。。
インタフェースを用いたポリモーフィズム
インタフェースを使ったポリモーフィズム
コードの微修正
前回のコードをちょっと書き換えてみた。
擬似コンストラクタの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ファイルを自動的に作成するマクロをよく使う。
Excelで表さえ作ってしまえば、人力でポチポチやるよりも圧倒的に正確かつ高速なので重宝している。
ただ、表を伴うようなもの、しかも表の行数がそれぞれ異なるようなものを作成する羽目になった。
そうしょっちゅう使うこともないと思うので、覚書代わりにアップしとく。
表の行を削除するマクロ
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メソッドを使用。
すると、
こうなる。簡単!
任意の行を削除する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)で実行後の表の行数を表示するようにした。
ほれ、この通り、指定した行を削除した上で、処理後の表を獲得することができている。
終わりに
WordのVBAは、あまり使う機会がないので、たまに使ったときにちゃんと記録を残しておかないとなあ。
LotusNotesのメール作成・送信をフルオート化してみた
LotusNotesでメール作成→送信をフルオートにする
LotusNotesでメールを送る作業が大量発生したときは、メール自動作成ツールで対応している。
ただ、誤送信が怖いので、基本的にメール作成までは自動でやっても、送信自体は手作業でやっていた。
しかしながら、ついに背に腹は替えられぬようになり、作成・送信フルオートをやってみた。
作業の手順としては、
といった感じ。
フルオート送信の壁
これまでのマクロに、「送信」という過程と「次へ移る」という過程を付け加えるだけなので、楽勝だと思っていたんだが、いざやってみると、
- 送信後、閉じるときに「……保存しますか? [はい][いいえ]」という確認メッセージが出てしまう
- メール作成後のNotesDocumentクラスのSaveメソッドを実行しなければ確認メッセージは出ないが、送信履歴に残らない
という問題が生じた。
復習~LotusNotesのメールができるまで
【スキップ】
ちょっと復習として、LotusNotesでメールができるまでの処理の過程をおさらいしておく。興味がなかったら上の「スキップ」リンクを踏んだら下にワープするのでどうぞ。
やっとクラスリファレンスの読み方が分かってきたので、自身の勉強のために書くだけ。よって、説明の中には素人の推定によるものも含まれているw
Notesメールができるまで
- NotesSessionクラスのインスタンスを作成
→これで現在使用中のNotesのいろんなものが使える。 - NotesUIWorkSpaceクラスのインスタンスを作成
→これでNotesのワークスペースのうち、現在使用中のウインドウが使える - NotesDatabaseクラスのインスタンスを作成
→これで現在使用中のNotesデータベースにアクセスできる。 - NotesDatabaseクラスのOpenMailメソッドで現在のユーザーのメールデータベースを開く
→これでメールデータベースが使える。 - NotesDatabaseクラスのCreateDocumentメソッドを用いて、NotesDocumentクラスのインスタンスを作成
- NotesDocumentクラスのインスタンスに、宛先、件名、受信確認の有無など諸データをセット
- NotesDocumentクラスのCreateRichTextItemメソッドを用いて、NotesRichTextItemクラスのインスタンスを作成
- NotesSessionクラスのCreateRichTextStyleメソッドを用いて、NotesRichTextStyleクラスのインスタンスを作成
- NotesRichTextItemクラスのAppendTextメソッド等を用いて本文を作成
- 必要ならNotesRichTextItemクラスのEmbdedObjectメソッドを用いて添付ファイルを追加
- NotesDocumentクラスのSaveメソッドを用いて文書を保存する
→これで作成したメールがドラフトに保存される。 - 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のクラスリファレンス
- NotesSessionクラス
- NotesDatabaseクラス
- NotesUIWorkSpaceクラス
- NotesUIDocumentクラス
- NotesDocumentクラス
- NotesRichTextItemクラス
- NotesRichTextStyleクラス
- NotesEmbededObjectクラス
※
VBAでインタフェースを使ってみた
インタフェースを用いたポリモーフィズムをやってみた
立山秀利さんが著書の中で使っていた音楽プレーヤのたとえが私にとっては一番分かりやすかったので、それをVBAでやってみる。
方針としては、
- 「RecordPlayer」クラス、「CDPlayer」クラス、「MP3Player」クラスの3つのクラスを作る
- 3つのクラスに共通する「音楽を再生する」という機能を「play」メソッドとして括り出す
- 「IMusicPlayer」インタフェースに「play」メソッドを定義する
- 「RecordPlayer」、「CDPlayer」、「MP3Player」の各クラスで「IMusicPlayer」インタフェースを実装する
- 「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」の各要素は、それぞれ別々のクラスのインスタンスなのに、全て同じ名前でプロパティやメソッドが呼び出せているというところがミソ。
実行結果
ほれ。同じメソッド名で呼び出したにもかかわらず、それぞれのクラスがそれぞれのやり方でplayメソッドを実行していることが分かる。
おわりに
正直、まだインタフェースの使いどころについてはピンときていないが、実際にコードを書いてみると、思ったより簡単だった。やっぱり、実際にコードを書いて動かしてみるというのが大事なんだなあ。
……と、ここまで書いてきてから気づいたんだが、
initメソッドもまとめてしまったらよかったんじゃね?
……orz
ちなみに、クラス側でちゃんとメソッドを置かないと、こんなふうに叱られる。