小さなクラスを作る(5)~エラーキャッチをするクラス

エラーキャッチをするクラスを作る

そんなことをする意味があるのかどうかはともかく、エラー時にメッセージを表示させるという処理もよく使うので、作ってみた。まあ、何でもかんでもクラスを作っていったら、そのうちコツがつかめるだろうということで勘弁してくださいw

例によって、クラスモジュールを挿入して、名前は「ErrorCatcher」にした。

クラス「ErrorCatcher」のコード

フィールド部分
Option Explicit
'フィールド
Private processName_ As String
Private errorNumber_ As Integer
Private errorDescription_ As String
Private isNecessaryToProvoke_ As Boolean
アクセサ部分
'アクセサ
Public Property Get processName() As String
  processName = processName_
End Property
Public Property Get errorNumber() As Integer
  errorNumber = errorNumber_
End Property
Public Property Get errorDescription() As String
  errorDescription = errorDescription_
End Property
Public Property Get isNecessaryToProvoke() As Boolean
  isNecessaryToProvoke = isNecessaryToProvoke_
End Property
コンストラクタ部分

珍しく使いどころがあった。

'コンストラクタ
Private Sub Class_Initialize()
  errorNumber_ = Err.Number
  errorDescription_ = Err.Description
End Sub

このクラスのインスタンスが生成された時点でErrオブジェクトが持っているNumberとDescriptionをセットするようにした。ただ、このやり方だと、次のような問題が生ずる。すなわち、

  • エラーキャッチしようとするたびにインスタンス化し、終わったらすぐに破棄しないといけない
  • エラーが発生する場所に「On Error GoTo 0」を書いてはいけない

う~ん、こんなに約束事が多いんじゃ、使えないかなあ。

メソッド部分
'メソッド
Public Sub showError(ByVal errorPlace As String, _
                     Optional ByVal toProvoke As Boolean = False)      '……(1)'
  processName_ = errorPlace
  MsgBox errorPlace & "で、" & vbCrLf & _
         "エラー番号:" & errorNumber_ & _
         "、「 " & errorDescription & "」エラーが発生しています。" & _
         vbCrLf & _
         "原因を確認して対応してください。", vbInformation             '……(2)'
  If toProvoke = True Then                                             '……(3)'
    MsgBox "     _________" & vbCrLf & _
           "  /           \ " & vbCrLf & _
           "/ /・\  /・\         \" & vbCrLf & _
           "|   ̄ ̄    ̄          | ち~んw" & vbCrLf & _
           "|    (_人_)         |" & vbCrLf & _
           "|     \     |              |" & vbCrLf & _
           "\      \_|        /", vbCritical
  End If
  Err.Clear                                                            '……(4)'
End Sub
コードの説明
  • (1)。引数は2つ指定。第1引数はエラーが発生した場所を表す文字列。第2引数は、「煽り」を入れるかどうか。完全に遊びですw
  • (2)は、通常のエラー発生お知らせメッセージ。
  • (3)。第2引数でTrueが渡されていたら、ユーザを煽るw
  • (4)でErrオブジェクトをクリア。

実行

標準モジュールに、

Sub test()
  Set fc = New FolderCreator
  fc.createFolder ThisWorkbook.path, "spaghetti?"                      '……(1)'
  If fc.hasError = True Then                                           '……(2)'
    Set ec = New ErrorCatcher                                          '……(3)'
    ec.showError "FolderCreatorクラスのcreateFolderメソッド", True     '……(4)'
    Set ec = Nothing                                                   '……(5)'
  End If
  Set fc = Nothing
End Sub

こんなコードを書いて実行してみた。

  • (1)で、不正な文字を使ったフォルダ名を指定
  • そうすると、FolderCreatorクラスのcreateFolderメソッドでエラーが出るはず
  • エラーが出たら、FolderCreatorクラスのインスタンスfcのhasErrorプロパティはTrueになる
  • そうなると、(2)の条件式がTrueになるので、
  • (3)でErrorCatchクラスのインスタンスecを生成して、
  • (4)でshowErrorメソッドを呼び出す
  • 第2引数をTrueにしているので、エラーが出たら煽られる

まあ、こうなるはず。

f:id:akashi_keirin:20170320084523j:plain

無事にエラー内容が表示され、

f:id:akashi_keirin:20170320084530j:plain

煽られたwww

おわりに

正直、こんなの必要なのかなあ、とは思う。

@akashi_keirin on Twitter

小さなクラスを作る(4)~フォルダ作成クラス

きっかけ

写しハンコつきPDFを作るマクロのコードを見直していると、メインの「写しPDFを作るクラス」が結構複雑なクラスになっていることが分かった。ざっと挙げると、

  • 元のWordドキュメントに画像があるかどうかチェック
  • ハンコ用の画像ファイルが実在するかチェック
  • 各ページの先頭中央にハンコ画像を追加する
  • 保存用のフォルダの有無をチェックし、なければ作る
  • ハンコ画像付きのWordドキュメントをPDFに変換して保存
  • Wordドキュメントを閉じてWordを終了

とまあ、これだけのことを一つのクラスの一つのメソッドに請け負わせていたことになる。

こうやって改めて書き出してみたら、イマイチだなあ……、とw

たとえば、4つ目の保存先のフォルダの有無をチェックし、なければ作るなんてのは、他のマクロでも頻出の処理だし、5つ目のWordドキュメントをPDF化して保存なんてのも他で使い回せそうだ。

ということは、切り出して独立したクラスにしといた方が良いということだろう。

だから、やってみた。

フォルダの有無をチェックしてなければ作るクラス

例によってクラスモジュールを挿入。オブジェクト名は「FolderCreator」とした。

フィールド部分

Private objFolder_ As myFolder

ちょっと実験的に、フィールド部分を構造体にしてみた。

ただ、構造体の定義をクラスモジュールに書けたら分かりやすいんだけど、それはさせてもらえず、標準モジュールにPublicで書かざるを得なかった。

標準モジュールの宣言セクション

Public Type myFolder
  fdPath As String
  fdName As String
  fdIsExist As Boolean
  fdIsCreated As Boolean
End Type
Public Const SAVE_FOLDER As String = "写しPDF"
Public fc As FolderCreator

これがうまいやり方なのかどうかは分からないけど、

対象のフォルダが存在する

ということを表すんだったら、たとえば

fc.isExist = True


と書くよりも、

fc.objFolder.fdIsExist = True


と書く方が分かりやすいと思ったのだ。

ただ、構造体を定義する場所と実際に使う場所が離れてしまうので、保守性という点ではイマイチなのかも知れない。

アクセサ部分

'アクセサ
Public Property Get objFolder() As myFolder
  objFolder = objFolder_
End Property

構造体としてまとめた関係で、このように異様にシンプルになる。

メソッド部分

2つのメソッドを作ってみた。

'メソッド
Public Sub checkExistenceOfFolder(ByVal tgtPath As String, _
                                  ByVal folderName As String)  '……(1)
  With objFolder_
    .fdIsExist = False
    .fdPath = tgtPath
    .fdName = folderName
    If Dir(.fdPath & "\" & .fdName, vbDirectory) = "" Then     '……(2)
      .fdIsExist = False
    Else
      .fdIsExist = True
    End If
  End With
End Sub

Public Sub createFolder(ByVal tgtPath As String, _
                        ByVal folderName As String)            '……(3)
  With objFolder_
    .fdIsExist = False
    .fdIsCreated = False
    .fdPath = tgtPath
    .fdName = folderName
    If Dir(.fdPath & "\" & .fdName, vbDirectory) = "" Then
      MkDir .fdPath & "\" & .fdName                            '……(4)
      .fdIsCreated = True                                      '……(5)
      .fdIsExist = True
    Else
      .fdIsExist = True
    End If
  End With
End Sub

1つ目のcheckExistenceOfFolderメソッド(1)は、その名の通り、指定したフォルダが存在するかどうかを調べるメソッド。存在していたらfdIsExistプロパティにTrueが格納される。それだけ。使い道があるのかどうかは不明w

2つ目のcreteFolderメソッド(3)は、第1引数で指定したフォルダに、第2引数で指定した名前のフォルダがあるかどうかを調べて、なかったら作る、というもの。

コードの説明

  • (1)からのcheckExistenceOfFolderメソッド。(2)では、Dir関数を使ってフォルダが存在するかどうか調べている。引数で指定したフォルダが存在しなかったら、Dir関数は""を返す。これはよく使うと思う。
  • (3)からのcreteFolderメソッド。checkExistenceOfFolderメソッドと同じようにフォルダの有無を調べて、なかったら(4)でMkDirステートメントで新しいフォルダを作成している。
  • 新たにフォルダを作成した場合は、元々フォルダが存在していた場合と区別するために、(5)でobjFolderプロパティのfdIsCreated要素(擬似的なプロパティ)をTrueにする。

おわりに

構造体の使用の是非はともかく、

他で使い回せるかどうか

を基準に、クラスを切り分けていくのが良いのかもしれない。

@akashi_keirin on Twitter

平成29年3月20日追記

なずな (id:nazuna_0124)さんからのコメントを見て気がついた。

エラー対応がないんじゃね???

ということに。

だからといって、今さら上の方を書き改めるのはメンドクサイので、とりあえず現段階での「FolderCreator」クラスのコードだけ投げやりに載っけとこう。

クラスFolderCreatorのコード

Option Explicit
'フィールド
Private objFolder_ As myFolder
Private hasError_ As Boolean
'アクセサ
Public Property Get objFolder() As myFolder
  objFolder = objFolder_
End Property
Public Property Get hasError() As Boolean
  hasError = hasError_
End Property
'コンストラクタ

'メソッド
Public Sub checkExistenceOfFolder(ByVal tgtPath As String, _
                                  ByVal folderName As String)
  With objFolder_
    .fdIsExist = False
    .fdPath = tgtPath
    .fdName = folderName
    If Dir(.fdPath & "\" & .fdName, vbDirectory) = "" Then
      .fdIsExist = False
    Else
      .fdIsExist = True
    End If
  End With
End Sub

Public Sub createFolder(ByVal tgtPath As String, _
                        ByVal folderName As String)
On Error Resume Next
  Err.Clear
  hasError_ = False
  With objFolder_
    .fdIsExist = False
    .fdIsCreated = False
    .fdPath = tgtPath
    .fdName = folderName
    If Dir(.fdPath & "\" & .fdName, vbDirectory) = "" Then
      MkDir .fdPath & "\" & .fdName
      .fdIsCreated = True
      .fdIsExist = True
    Else
      .fdIsExist = True
    End If
  End With
  If Err.Number > 0 Then
    hasError_ = True
  End If
End Sub

いちおう説明

「hasError」というプロパティを加えた。エラーが発生していたら、Trueになるので、メソッドの呼び出し元で条件判定してエラー時の処理を書けばよい。エラー時の処理にはたぶんErr.Numberとか、Err.Descriptionを使うことになるだろうから、「On Error GoTo 0」は書いていない。

写しPDF作成マクロ~(3)―ページ先頭位置を割り出して画像を追加する―

Wordドキュメントに画像を貼り付ける

今回作成するマクロでは、Wordドキュメントにpng画像を貼り付ける必要がある。しかも、その処理をExcelから行う、という無駄にややこしい仕様w

とりあえず、今回は

Wordドキュメントの各ページの先頭中央にpng画像を貼り付ける

ことに特化して書く。

ExcelVBA使いにとって、WordVBAって、

似ているのに何かちょっとクセがつかみづらい

気がするので、自身の覚書も兼ねてちょっとこってり書くよ。

やるべきこと

ひとまず、やるべきことを整理しておこうかね。

参考文献は土屋和人さんの『Wordマクロ/VBA徹底入門』という本。

WordVBA関連の情報ってホントに乏しくて、本屋で見つけて清水の舞台から飛び降りるつもりで定価購入した直後に敦賀ブックオフで500円ぐらいで売っていたのを見つけてずっこけたのは良い思い出だw

閑話休題

  1. ドキュメントの先頭を選択する
  2. png画像を貼り付ける(追加する)
  3. Wordの「検索」機能を用いて改ページ位置を割り出す
  4. 改ページ位置の次の場所を選択する
  5. 改ページが見つからなくなるまで繰り返し

とまあ、これだけのことをやらせればよい。

使用したコード

下に挙げるコードは、クラスのメソッドとして書いたものの抜粋。従ってよく分からん変数が使われていると思うので、先に説明しておく。

objRange

こいつは、WordのRangeオブジェクトを格納するための変数。

objDoc_

こいつは、クラス内で操作対象のWordのDocumentをセットしておくための変数。

imgPath

こいつは、このメソッドに渡される引数で、ハンコ用のpngファイルのフルパスが入っている。

まあ、これだけのことを頭に置いて、次のコードを読んでくだされ。

Dim objRange As Word.Range                                       '……(1)
Set objRange = objDoc_.Range(0, 0)                               '……(2)
objRange.Select                                                  '……(3)
Do
  objDoc_.Shapes.AddPicture fileName:=imgPath, _
                            Top:=0, _
                            Left:=200                            '……(4)
  With objWord_.Selection.Find                                   '……(5)
    .MatchWildcards = False                                      '……(6)
    .MatchFuzzy = False                                          '……(7)
    .Text = "^m"                                                 '……(8)
    .Execute Forward:=True                                       '……(9)
	End With
  Set objRange = objDoc_.Range(objWord_.Selection.End + 1, _
                               objWord_.Selection.End + 1)       '……(10)
    objRange.Select                                              '……(11)
Loop Until objWord_.Selection.Find.Found = False                 '……(12)

コードの説明

久しぶりに読んでみたら、すっかり意味を忘れていたのでw 復習も兼ねてしっかり説明しよう。

  • (1)は、WordのRangeオブジェクトを格納するための変数の準備。このコードはExcelのモジュールに書いているので、型を指定するときに「Word.Range」と書いているところが注意かな。
    ※実は、私、これでしばしハマりました。
  • (2)で、Wordドキュメントの0文字目~0文字目、つまり文書の先頭位置をobjRange_に格納している。
  • (3)で、その位置を選択している。
    f:id:akashi_keirin:20170319084940j:plain
  • (4)で、png画像を追加。AddPictureメソッドを使用。Topプロパティの「0」はともかく、Leftプロパティの「200」は完全に目分量。
  • いよいよ(5)からが本番。(5)~(9)の説明の前に画像をば。

f:id:akashi_keirin:20170319084955j:plain

スキルがないので、画像が汚くてすまん。そのうち勉強する。

赤で書き込んだのが今回使用するプロパティ。

  • (5)では、SelectionオブジェクトのFindプロパティを参照。Withしているので、ここから先はFindオブジェクトに対する操作。

最初はコレが分からなかった。「Find」なんて言ったらメソッドだと思うよねえ……。このFindオブジェクトというのは、
f:id:akashi_keirin:20170319084955j:plain
こいつのことなんだな。

  • (6)、(7)で、ワイルドカードとあいまい検索(日)をオフにしている。特にMatchFuzzyプロパティをFalseにしておかないと、この後の特殊文字の検索ができないので注意。
    ※ちなみにここでも相当長時間ハマりました。
  • (8)で検索する対象をセット。「^m」ってのは「改ページ」を表す特殊文字
  • ここまでで検索の設定ができたので、(9)で検索を実行。「execute」ってのは、「exeファイル」でおなじみ、「実行する」って意味だよね。
  • あと、Excecuteメソッドの引数「Forward」ってのは「次を検索」ってやつだな。

f:id:akashi_keirin:20170319085006j:plain

ちなみに、この時点で、「Selection」オブジェクトの「End」プロパティには「171」が入っていることが分かる。

f:id:akashi_keirin:20170319085016j:plain

Wordの方では、改ページ記号が選択されている。

  • (10)で、現在選択中の最後の場所の次の場所、すなわち、次のページの先頭を新たにobjRangeにセット。
  • (11)でその場所を選択。
    f:id:akashi_keirin:20170319085026j:plain
  • (12)は繰り返し判定。検索の結果、検索対象が見つかっていなければ、FindオブジェクトのFoundプロパティにFalseが返るので、その場合はループを抜ける、ということ。

おわりに

WordVBAって、いまいち使いどころがよく分からないけれど、このFindオブジェクトの使い方はしっかり身につけておいたら、いろいろ面白そうだ。

写しPDF作成マクロ~(2)

ファイル選択用のクラスを作る

指定した拡張子のファイルを選択させる

今回のマクロでは、写し作成元のWordファイル、ハンコ用のpngファイル、と、ファイル形式を限定して取得したい。そうすると、ファイル選択ダイアログでユーザにファイル選択を迫る際に、フィルターをかける必要がある。

クラスモジュールのコード

まず、クラスモジュールを挿入し、「オブジェクト名」を「FileNameGetter」にした。

フィールド部分
Option Explicit
'フィールド
Private gotName_ As String         '……(1)
Private isFailed_ As Boolean       '……(2)
Private isCancelled_ As Boolean    '……(3)

まずは、フィールド部分。

  • (1)は、取得したファイル名(フルパス)を入れておく変数。
  • (2)は、処理が失敗したかどうかを保持する変数。処理が失敗したらTrueが入る。
  • (3)は、キャンセルされたかどうかを保持する変数。ファイル選択がキャンセルされたらTrueが入る。
アクセサ部分
'アクセサ
Public Property Get gotName() As String
  gotName = gotName_
End Property
Public Property Get isFailed() As String
  isFailed = isFailed_
End Property
Public Property Get isCancelled() As String
  isCancelled = isCancelled_
End Property

特記事項なし。いづれも読み取り専用にしている。

コンストラクタ部分
'コンストラクタ
Private Sub Class_Initialize()
  gotName_ = ""
  isFailed_ = False
  isCancelled_ = False
End Sub

一応、初期化しているけど、全部デフォルト値を代入しているだけなので、別になくても良いと思う。

メソッド部分
'メソッド
Public Sub getFileName(ByVal prompt As String, _
                       ByVal appName As String, _
                       ByVal baseStr As String)             '……(1)
  '引数「baseStr」に3文字以外の文字列を指定すると、実行時エラーを返す。
  If Len(baseStr) <> 3 Then                                 '……(2)
    Err.Raise 10000, Description:="拡張子は必ず最初の3文字を指定せよ。" & vbCrLf & _
                                  "getFileNameメソッドの第3引数を訂正せよ。"
  End If
  isCancelled_ = False
  isFailed_ = False
  Dim str As Variant                                        '……(3)
  Dim baseName As String
  str = Application.GetOpenFilename(Title:=prompt, _
                                    FileFilter:=appName & "," & "*." & baseStr & "?") '……(4)
  If str = False Or str = "" Then                           '……(5)
    MsgBox "キャンセルされました。", vbInformation
    isCancelled_ = True
    Exit Sub
  End If
  baseName = Right(str, Len(str) - InStrRev(str, "."))      '……(6)
  If Left(baseName, 3) = baseStr Then
    gotName_ = str
  Else
    isFailed_ = True
    MsgBox "ファイルの指定が間違えています。" & vbCrLf & _
           appName & "ファイルを選択せよ。", vbCritical
  End If
End Sub

このクラスの唯一のメソッド。

3つの引数を渡して実行するようにしている。すなわち、

  • 第1引数「prompt」は、ファイル選択ダイアログのタイトル
  • 第2引数「appName」は、FileFilterで表示するアプリケーション名
  • 第3引数「baseStr」は、FileFilterで表示する拡張子
  • の3つ。

ちなみに、「FileFilter」ってのは、

f:id:akashi_keirin:20170318214721j:plain

この赤枠囲みのところね。

コード内の(1)については説明したから、(2)以降について説明。

  • (2)で、渡された引数のチェック。第3引数に必ず3文字の引数が渡されるようにしている。これは、このクラスを使用するプログラマに知らせる必要のあることなので、自作のエラーを表示させている。
  • (3)で変数「str」を準備。GetOpenFilenameメソッドの戻り値を格納するための変数なんだが、ユーザがファイル選択ダイアログで「キャンセル」を選択すると、Booleanの「False」が返るので、StringでもBooleanでも格納できるVariantにしている。
  • (4)がGetOpenFilenameメソッド。引数FileFilterを指定することにより、ダイアログに必要なファイルだけを表示させることができる。引数FileFilterの内容をよく見たら、上の画像の赤枠囲み部分と同じであることが分かるだろう。
  • (5)の条件指定だが、一つ目の「False」は、ユーザがファイル選択ダイアログで「キャンセル」を選んだ場合、二つ目は、何もファイルを選択せずに「OK」をクリックした場合。つまり、ファイルが選択されなかった場合、ということ。

ところで、上記コード中の(6)だが、たぶん必要ないと思う。

一応、説明しておくと、

Right関数とLen関数、InStrRev関数を組み合わせて、取得したファイル名から拡張子の部分を取り出し、引数で受け取った拡張子と比較。拡張子が違っていたら、isFailedプロパティにTrueをセットし、メッセージを表示

ということなのだが、そもそもGetOpenFilenameメソッドでファイルフィルターをかけているのだから、拡張子が食い違うことなどないと思う。なんでこんなコード書いたんだろ……?

追記

>そもそもGetOpenFilenameメソッドでファイルフィルターをかけているのだから、拡張子が食い違うことなどない
……と思っていたけど、「ファイル名」のところに表示されていないけど存在するファイル名をジカ打ちしたら拡張子が食い違うことがあり得る。すげえな、このコード書いた当時の私……w そんなことまで想定していたのか……!

FileNameGetterを使用する

標準モジュールのコード

まずは、宣言セクションで、

Public fng As FileNameGetter

Public変数としてインスタンス用の変数を宣言しておく。

Sub getStampFile()
  Set fng = New FileNameGetter          '……(1)
  With fng                              '……(2)
    .getFileName "ハンコ用のpngファイルを選べ。", "png", "png"  '……(3)
    If .isCancelled = False And .isFailed = False Then          '……(4)
      Range("ImageFilePath").Value = .gotName
    End If
  End With
  Set fng = Nothing
End Sub

コードの説明

  • (1)は、おなじみFileNameGetterクラスのインスタンス化。
  • この先、FileNameGetterクラスのインスタンス「fng」に対する操作が多くなるので、(2)のようにWithでまとめてしまう。インスタンス化した後、Withでまとめるのはよく使う手法だと思う。
  • (3)でgetFileNameメソッドを使用。引数の渡し方に注目。こんなふうに使う。
  • (4)。getFileNameメソッドを経て、isCancelledプロパティか、isFailedプロパティがTrueになっていたとしたら、ファイル名の取得に失敗しているということ。つまり、両方Falseだったら、無事にファイル名が取得できているということだ。

シートモジュールのコード

Wordファイルのフルパスを取得する処理は、セルのダブルクリックイベントをきっかけに発動するようにした。従って、シートモジュールにイベントマクロとして書いている。

Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
  If Target.Row = 2 And Target.Column = 1 Then    '……(1)
    Set fng = New FileNameGetter
    With fng
      .getFileName "写しを作成するWordファイルを指定するがよい。", _
                   "Word", "doc"                   '……(2)
      If .isCancelled = True Or .isFailed = True Then                 '……(3)
        Cancel = True
      Else
        Target.Value = .gotName
        Cancel = True
      End If
    End With
  Else
    Cancel = True
  End If
Set fng = Nothing
End Sub

コードの説明

  • (1)は、イベントマクロを起動させる条件の設定。A2セルをダブルクリックしたときだけで良いからこうなる。

ちなみに、A2セルに「DocumentPath」と名前をつけているにもかかわらず、(1)を

If Target.Name = "DocumentPath" Then

としてもうまくいかない。なんでだろ???

  • (2)は、getFileNameメソッドの呼び出し。Wordファイルの場合だと、こんな指定の仕方になる。
  • (3)。getFileNameメソッドの実行後、isCancelledプロパティか、isFailedプロパティのいづれかがTrueになっているということは、ファイル名が取得できていないということだから、何もせずにダブルクリックイベントをキャンセルする。逆に、いづれもFalseだったら、ファイル名が取得できているということだから、取得したファイル名をA2セルに書き込む。

おわりに

ファイル名を取得する、というのはよくやる処理だと思うが、クラスとして設定しておくことで、非常に使いやすくなる。クラスって、状態と振る舞いを兼ね備えているので、「FileNameGetterさん、ファイル名を取ってきて~」と、あたかも人に頼むかのようにコードを書くことができるのが強みだと思う。

今回紹介した「FileNameGetter」クラスは、そのまま他のマクロでも使えると思う。

よく使う機能をクラス単位で蓄積していったら、マクロ作りがメチャクチャはかどるようになると思う。

写しPDF作成マクロ~(1)

書類が「写し」であることを示すために、「写」ハンコを押して送ったり、職場内で回覧したり、ということがある。

まあ、職場内で回覧する分にはハンコを押すだけの話なので何ということはないのだが、「写し」であることを明示した上でメールで送る、ということになると、Wordで作った書類なのに、

  • 一旦プリントアウトする
  • ハンコを押す
  • スキャンしてPDFにする


という実にマヌケなことをやる羽目になる。

……とまあ、そんなわけで無駄に

Wordファイルから「写しPDF」を作成するマクロ

を作ってみた。

仕様

  • Wordファイルと写しハンコ用画像ファイル(透過png)からPDFファイルを生成する
  • Wordファイルの指定は、セルのダブルクリックでファイル選択ダイアログを呼び出すことで行う
  • ハンコ用画像ファイルの指定は、シート上のボタンクリックでファイル選択ダイアログを呼び出すことで行う
  • 両方のファイル名(フルパス)が指定されていたら、シート上のボタンクリックでマクロを実行する
  • Wordファイルが複数ページある場合は、各ページの上中央に「写」ハンコ画像をセットする
  • PDFファイルとして出力し、「写しPDF」というフォルダに保存する
  • 本体のExcelファイルがあるフォルダに「写しPDF」フォルダがなかったら自動で作成する

まあ、こんな感じ。

操作イメージ

f:id:akashi_keirin:20170318173834j:plain

画面構成はこんなの。

赤枠のセルをダブルクリックすると、

f:id:akashi_keirin:20170318173845j:plain

ファイル選択ダイアログが出てくるので、元になるWordファイルを選択する。

f:id:akashi_keirin:20170318173855j:plain

セルにファイルのフルパスが書き込まれた。

f:id:akashi_keirin:20170318173933j:plain

このボタンをクリックすると、

f:id:akashi_keirin:20170318173953j:plain

ファイル選択ダイアログが表示されるので、ハンコ用のpngファイルを選択する。

f:id:akashi_keirin:20170318174003j:plain

セルにファイルのフルパスが書き込まれている。

ちなみに、ハンコ用の画像ファイルは、

f:id:akashi_keirin:20170318174024j:plain

こんなの。私はスキルがないので、Wordで作ってIrfanViewで透過pngにした。

f:id:akashi_keirin:20170318174116j:plain

ここまで準備ができたら、このボタンをポチッ!

f:id:akashi_keirin:20170318174124j:plain

あっという間にできあがり。

f:id:akashi_keirin:20170318174136j:plain

「写しPDF」フォルダ内にはPDFファイルが保存されている。

f:id:akashi_keirin:20170318174144j:plain

仕上がりは、こんな感じ。ちゃんと透けているところがリアルでしょう?w

ちなみに、元のWordファイルは、

f:id:akashi_keirin:20170318174152j:plain

こんな感じです。

ソースコード等については、追々書いていきます。

私がクラスモジュールを使い始めたきっかけ

きっかけ

そもそもは、自分に降りかかる火の粉を、少しでも楽に振り落としたいからだった。

thom(id:t-hom)さんが、コチラで言及してくださったが、私はあくまでも素人に過ぎないのに、職場では

それって、プログラマの仕事じゃね?

的なことをさせられてしまっている。通常業務は通常業務として他の職員と変わらず割り当てられているにもかかわらず。

今の上司にVBAのスキルがバレるまでは、まあ、自分の仕事を少しでも楽にできるように、と好き勝手にコードを書き散らしていた。

必要な機能さえとにかく盛ればいいんだから、チャチャッと書いてサクッと使う。次に同じようなことをするときには、そのコードの意味なんか忘れているけど、読み直したらまあ分かるからチョコチョコッと直しては使い……という感じだった。

しかし、今の上司と出会ってしまってからは、ひつこいようだが、

完全に他人が使うためのマクロ

を作らされることが多くなった。

このときにも書いたけど、

「マクロ? それ何ていう寿司ネタ?」

みたいな人が使うのを想定したマクロって、本当に大変。

しかも、上司の指示がテキトーで、いっつも

こんな風にできたらええなーと思うんや

みたいな雑な指示で作成を命じ、できあがって納品(?)したらしたで、ろくにテストもしてくれずに他の部署に提供し、だいぶ経ってから

あ、こことここをチョイチョイッと直しといて

とか、

この機能やけど、ここがこんなふうになった方が便利やと思うんや

と、一事が万事この調子。

後出しジャンケン祭り状態。

こうなると、そもそもマクロを作るときに思いつきでいわゆる「スパゲティ・コード」なんか書いてしまった日には、私の命がいくつあっても足りない状態になるわけ。

それまで、「設計」なんて概念とは無縁で、

コメントしっかり書いときゃ大丈夫っしょ?

ぐらいに思っていたけど、そんなことを言ってられなくなったのだ。

クラスモジュールとの出会い

たまたま、意味も分からず「オブジェクト指向」に憧れていたこともあって、

オブジェクト指向というのはプログラムの変更・追加に強いプログラミング・テクニックらしい

というようなことが記憶の片隅にあったので、本格的にJavaの勉強を始めたのが「クラス」という概念との出会いだった。

そして、thom(id:t-hom)さんのブログとか、コチラのおかげで、VBAでもオブジェクト指向っぽいことができることが分かり、導入しはじめたというわけ。

まだまだクラスモジュールを十分に使いこなせているとは言えないけど、ついこないだ、機能変更の問い合わせを受けたときには、ものの1分ほどで対応ができた。そのマクロは、クラスモジュールを活用して作ったもので、問い合わせのあった機能の部分がうまくカプセル化できていたことからすると、少しづつ上達はしてきているのだと思う。

おわりに

thom(id:t-hom)さんがおっしゃってくださったように、スキルを身につけた人というのは、スキルを持っていない人が他のことに費やしていたお金とか時間をそのスキルの習得に費やしているのである。

しかも、見落とされがちなことかも知れないが、

スキルを維持するのにもお金と時間がかかる

のである。

それを、まるで背が高い人に高いところにある荷物を取ってもらうような調子で

チャチャッと作っといて

と気安く頼んでくる人の気が知れない。

年度が変わったらきっぱりと言ってやろう。

Thunderbirdメール自動作成マクロを改良した

準備

セルに名前をつける。

f:id:akashi_keirin:20170318080804j:plain

こんな具合に「ThunderBirdPath」と名前をつけた。

標準モジュールのコード

Sub getThunderbirdPath()
  Set flp = New FilePicker                                  '……(1)
  With flp
  .showFilePicker ("Thunderbirdの実行ファイルを指定せよ(ショートカットでも良い)")  '……(2)
    If .isCancelled = False Then                            '……(3)
      Range("ThunderbirdPath").Value = .gotFileFullPath     '……(4)
    End If
  End With
End Sub

たったこんだけ。このときに作成した「FilePicker」クラスを使用。うん、メッチャ便利w 自作Functionでやっていた時期もあったけど、自作クラスの方が圧倒的に使い勝手がいい! アドヴァイスをくださったフォロワーさんに感謝!

どうでもいいけど、最近、EdgeでURLを選択して右クリックしたら、いきなりEdgeが落ちるんだけど、何なのこれ? 不便でしゃーないんやけど。

コードの説明

  • (1)でFilePickerクラスのインスタンスを生成。
  • (2)で、showFilePickerメソッドを呼び出す。引数で渡した文字列が、ウインドウのタイトルとして表示される。
    ※この引数はOptionalにした方が良いと思った。
  • (3)。ファイル選択をキャンセルしたら、isCancelledプロパティがTrueになるようにしているので、Falseだったら何らかのファイルフルパスがgotFileFullPathプロパティにセットされていることになる。
  • (4)でめでたく取得したファイルのフルパスを「ThunderbirdPath」と名付けたセルに書き込む。
    セルに独自の名前を付けておくと、いちいちブックやシートの指定をせずに済むから楽。

実行

マクロをシート上のボタンに登録して、

f:id:akashi_keirin:20170318081442j:plain

ボタンをポチッ!

f:id:akashi_keirin:20170318080810j:plain

Thunderbirdのショートカットを選択して[OK]。

結果

f:id:akashi_keirin:20170318080817j:plain

ほれ、この通り、Thunderbirdの実行ファイルのフルパスがセルにセットされた。

後は、このセルの値を引数として渡してThunderbirdメール作成メソッドを実行するようにしたら良い。

たとえば、このとき作成したメソッドだったら、まずメソッド名を

Public Sub createThunderbirdMail(ByVal tbp As String)

と、このように文字列を引数として受け取るようにしておいて、

Thunderbirdのフルパスを、

Dim thunderbirdPath As String
  thunderbirdPath = """" & THUNDERBIRD_PATH & """ -compose """

と、こんなふうに定数にしていたのを、

Dim thunderbirdPath As String
  thunderbirdPath = """" & tbp & """ -compose """

こんなふうに引数名に変えて、実行時に、

md.createThunderbirdMail (Range("ThunderbirdPath").Value)

としてやれば良い。

これでより一層便利になった。

@akashi_keirin on Twitter