社労士業務向けのRPAツールを作って運用しました

# はじめに
行政機関が発出するXML形式の書類（以下、公文書）をPDFに自動変換するRPAツール（ExcelVBA）を開発して、社会保険労務士法人（以下、社労士法人）で運用しました。その内容をまとめましたので、ご紹介します。

# １．社会保険手続代行業務ついて
社会保険労務士（以下、社労士）の業務の１つとして、社会保険手続代行があります。社会保険としては、健康保険及び厚生年金保険があり、これらの保険に関して、会社から業務を受託した上で、申請を代行し、行政の決定通知に関する公文書を取得して、会社に返却するまでの一連の作業を行います。実際の手続申請については、電子申請を利用する事が多く、社労夢、smartHR等のサービスを利用します。

社会保険で取り扱う手続については、従業員の採用や退職、結婚、出産、育児休業等のライフイベントの他、毎年６月に行われる算定や昇給又は降給に伴う月額変更等、様々な種類があり、申請するタイミングが異なります。

# ２．社会保険の公文書の仕様について
公文書の仕様については、公開されていません。ただし、実務で取り扱う事で分かっている部分がありますので、その内容をご紹介します。仕様については、今後、予告なく変わりうる可能性がある点に留意する必要があります。

社会保険手続の電子申請を行うと、各申請毎に行政機関が決定した内容を示す公文書が発出されます。例えば、従業員の採用時に申請する資格取得の公文書を確認すると、次の４ないし５種類の電子ファイルがあります。

| 電子ファイル名 | 文書のタイトル | 内容                                |
|:-----------|:------------------------------------|:---------------|
| 7100001.xml -①| 資格取得確認および標準報酬決定通知 | 公文書（この場合は資格取得に関する行政機関の決定内容） |
| 7100001.xsl   | － | 710001.xmlのスタイルシート                                       |
| 7100001.pdf -②| 資格取得確認および標準報酬決定通知 | 公文書（例えば、はしごだかの「髙」のように氏名に外字を利用している人の資格取得に関する行政機関の決定内容） |
| 202011161015255880.xml -③|日本年金機構からのお知らせ | 鑑文書（添付ファイルを含む公文書全体をXML署名したもの）|
| kagami.xsl | － |  202011161015255880.xmlのスタイルシート|

文書としては、①②「資格取得確認および標準報酬決定通知書」、③「日本年金機構からのお知らせ」の３つがあります。

①②については、社労士が申請した内容に関する行政側の決定通知です。②については、申請対象者の氏名にJIS規格の文字コード以外の文字（以下、外字）を使う人向けの公文書です。外字を使う人がいなければ、②は発出されません。7で始まる7桁の数字列＋拡張子（.xml又は.pdf）という命名規則に従います。

③については、添付ファイルを含む公文書全体をXML署名した鑑文書（以下、鑑文書XML）であり、データの改ざん、なりすましを検出するために発出されます。鑑文書のファイル名は到達番号（行政に申請内容が到達した時点のタイムスタンプで数字17桁）＋「0」（合計18桁）＋「.xml」という命名規則に従います。上記ファイル名から2020年11月16日10時15分25秒588ミリ秒に本手続が行政機関に到達したという事が分かります。

鑑文書XMLのAPPENDIXタグには、公文書及びスタイルシートの一覧が記載されています。申請時の条件によって、記載される内容が異なります。以下にいくつかの例を示します。

```xml:ケース１：標準報酬改定通知書
<APPENDIX ID="mhlw.go.jp">
  <DOCLINK REF="kagami.xsl"/>
</APPENDIX>
<APPENDIX ID="mhlw.go.jp">
  <DOCLINK REF="7140001.xml"/>
</APPENDIX>
<APPENDIX ID="mhlw.go.jp">
  <DOCLINK REF="7140001.xsl"/>
</APPENDIX>
```
ケース１については、標準報酬月額の改定に関する通知書です。昇給又は降給に伴う月額変更の申請に対する公文書であり、7140001.pdfが無いため、申請対象者の氏名に外字を持つ人はいません。

```xml:ケース２：標準報酬改定通知書（外字１名のみ）
<APPENDIX ID="mhlw.go.jp">
  <DOCLINK REF="kagami.xsl"/>
</APPENDIX>
<APPENDIX ID="mhlw.go.jp">
  <DOCLINK REF="7140001.pdf"/>
</APPENDIX>
```
ケース２については、標準報酬月額の改定に関する通知書です。氏名に外字を持つ人１名分の申請であるため、外字向けのPDFのみが発出されました。

```xml:ケース３：資格喪失確認通知書及び70歳以上被用者不該当のお知らせ
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険資格喪失確認通知書</APPTITLE>
  <DOCLINK REF="7120002.xml"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険資格喪失確認通知書</APPTITLE>
  <DOCLINK REF="7120002.pdf"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>厚生年金保険７０歳以上被用者不該当のお知らせ</APPTITLE>
  <DOCLINK REF="7190001.xml"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>厚生年金保険７０歳以上被用者不該当のお知らせ</APPTITLE>
  <DOCLINK REF="7190001.pdf"/>
</APPENDIX>
```
ケース３については、退職に伴う資格喪失に係る公文書です。被保険者の中に70歳以上の人が含まれている、氏名に外字を使っている人がいる等、公文書の内容が複雑になっています。

```xml:ケース４：標準賞与額決定通知及び70歳以上被用者標準賞与額相当額のお知らせ
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険標準賞与額決定通知書(被保険者整理番号  6272～  8812)</APPTITLE>
  <DOCLINK REF="7150001-00001.xml"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険標準賞与額決定通知書(被保険者整理番号  8813～ 10549)</APPTITLE>
  <DOCLINK REF="7150001-00002.xml"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険標準賞与額決定通知書(被保険者整理番号 10559～ 11108)</APPTITLE>
  <DOCLINK REF="7150001-00003.xml"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険標準賞与額決定通知書</APPTITLE>
  <DOCLINK REF="7150001.pdf"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>厚生年金保険７０歳以上被用者標準賞与額相当額のお知らせ</APPTITLE>
  <DOCLINK REF="7220001.xml"/>
</APPENDIX>
```
ケース４については、賞与支払い時に申請する手続に係る公文書であり、内容が複雑です。標準賞与額決定通知（2,500名分）及び標準賞与額決定通知（氏名に外字を使用する者50名分）、70歳以上被用者　標準賞与額相当額のお知らせ（1名分）等の公文書が発出されています。標準賞与額決定通知（2,500名分）については、1ファイルに収まらず、3つに分かれて追番が振られています。

```xml:ケース５：資格取得確認および標準報酬決定通知（返戻票あり）
<APPENDIX>
  <APPTITLE>返戻票</APPTITLE>
  <DOCLINK REF="henrei.xml"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険資格取得確認および標準報酬決定通知書</APPTITLE>
  <DOCLINK REF="7100001.xml"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険資格取得確認および標準報酬決定通知書</APPTITLE>
<APPENDIX>
  <DOCLINK REF="7100001.pdf"/>
</APPTITLE>
```
ケース５については、henrei.xmlがある事から、資格取得の申請した結果、一部の人の氏名（フリガナ）や基礎年金番号等の記述ミスにより返戻されたケースです。この場合、返戻票に記載されている人については、行政からの指摘事項を修正した上で、再申請する必要があります。

拡張子xslについては、XML文書のスタイルシートです。先に示した従業員の採用時に申請する資格取得の例では、7100001.xml、202011161015255880.xmlのスタイルシートとして、7100001.xsl、kagami.xslがあります。

7100001.xslについては、Internet Explore（以下、IE）を前提としているスタイルシートです。IEについては、サポート終了期間が公開されていますが、行政機関が発出する電子文書では現在も現役のまま使われています。

# ３．業務分析及び問題点の定義
ここでは、本業務の流れを整理した上で、問題点を定義します。

## ３．１　業務分析
例えば、顧客から社会保険加入の手続の依頼を受けると、入社日が到来したタイミングで電子申請を行います。手続が完了したら、公文書を取得します。その後、①xml形式の公文書をPDFに変換した上で、②外字向けPDFを含めて複数のPDFをマージして、③到達番号にリネームしたPDFに加工します。

社会保険関係の書類については2年間の保存義務があります。そのため、管理するファイルを削減する必要があり、複数の公文書をまとめて１つのPDFにします。顧客への業務完了報告を兼ねて、このPDFを顧客に返却する事で一連の作業が終了します。

![本業務の分析.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659913/4e97860d-6e4d-6798-1ac1-91f3025a6942.png)

先ほど、ご紹介したケース４を例に作業手順を説明すると、次のようになります。

```xml:ケース４：標準賞与額決定通知及び70歳以上被用者標準賞与額相当額のお知らせ
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険標準賞与額決定通知書(被保険者整理番号  6272～  8812)</APPTITLE>
  <DOCLINK REF="7150001-00001.xml"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険標準賞与額決定通知書(被保険者整理番号  8813～ 10549)</APPTITLE>
  <DOCLINK REF="7150001-00002.xml"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険標準賞与額決定通知書(被保険者整理番号 10559～ 11108)</APPTITLE>
  <DOCLINK REF="7150001-00003.xml"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>健康保険・厚生年金保険標準賞与額決定通知書</APPTITLE>
  <DOCLINK REF="7150001.pdf"/>
</APPENDIX>
<APPENDIX>
  <APPTITLE>厚生年金保険７０歳以上被用者標準賞与額相当額のお知らせ</APPTITLE>
  <DOCLINK REF="7220001.xml"/>
</APPENDIX>
```

- （手順１）7150001-00001.xmlをPDFに変換する
- （手順２）7150001-00002.xmlをPDFに変換する
- （手順３）7150001-00003.xmlをPDFに変換する
- （手順４）7220001.xmlをPDFに変換する
- （手順５）外字PDFである7150001.pdf及び上記４つのPDFを、鑑文書に記載されている順序に従って１つのPDFにマージする
- （手順６）マージしたPDFをリネームする

また、１つのxmlファイルをPDFに変換する手順については、次のようになります。

- （手順１）IEでxmlを開く
- （手順２）右クリックで印刷プレビューを選ぶ
- （手順３）印刷を選ぶ
- （手順４）プリンターの選択にて「Microsoft Print to PDF」を選ぶ
- （手順５）印刷を押下する→「印刷結果を名前をつけて保存」画面が立ち上がる
- （手順６）ファイル名にPDFファイル名を入力する
- （手順７）保存を押下する

## ３．２　問題点の定義
本業務の問題点としては、煩雑な定型作業を人手で行っている点です。申請件数が多くなると、この作業時間が無視できなくなり、生産性低下の原因になります。
特に経験の浅い人は、どのような順番で複数あるPDFをマージすれば良いか分からず作業が停滞してしまいます。また、行政機関が想定している公文書の順番と異なる状態でPDFをマージしてしまったり、中には複数あるPDFの一部をマージし忘れて、顧客に返却してしまうというミスもあります。

こうした状況を踏まえて、この作業を自動化する必要があると考えるに至りました。

# ４．ツール開発について
ここからは、ツール開発に関する内容について、説明します。

## ４．１　開発方針
プログラムでIEを操作する方法を検索したところ、VBAのUserFormにてIEのCOMオブジェクトからのイベントを捕捉する方法が見つかり、このアプローチで開発する事にしました。

なお、VBAでIE制御するためのライブラリとしては、Microsoft Internet Controls 、Microsoft HTML Object Libraryがあるため、これらのライブラリを参照できるようにVBEで設定する必要があります。

## ４．２　IEのCOMオブジェクトが返すイベントの調査
[InternetExplorer object](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa752084(v=vs.85)?redirectedfrom=MSDN)では、IEのCOMオブジェクトが返すイベントについて、説明があります。本ツールで使えそうなイベントを拾い上げてみました。

| イベント名 | 発生する条件                        |
|:-----------|:------------------------------------|
| DocumentComplete|ドキュメントが完全にロードされて初期化されたとき |
| PrintTemplateInstantiation|印刷テンプレートがインスタンス化されたとき |
| PrintTemplateTeardown|印刷テンプレートが破棄されたとき |

「印刷テンプレート」という馴染みがない言葉があります。これは、HTMLファイルであり、文書印刷又はプレビューを行うときにIEで作成されるものです。

## ４．３　状態遷移設計と状態検出方法の検討
ぼんやりと状態遷移がありそうだという認識に至ったのですが、①印刷テンプレートがインスタンス化されたタイミング、②印刷テンプレートが破棄されたタイミングが具体的に分かりません。そのため、実機を使って調べたところ、①は印刷プレビュー画面が表示されるタイミング、②はプレビュー画面が消えるタイミングで発生する事が分かりました。

以下にプレビュー画面を示します。
![プレビュー画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659913/5c3a99d2-8928-632a-4b6d-10b67272cb93.png)

①②の間において、「印刷結果を名前を付けて保存」画面が開いた状態を認識する必要があるものの、捕捉するべきイベントが見つからず、このままでは、シーケンスが繋がりません。IEのCOMオブジェクトによるイベントでは、「印刷結果を名前を付けて保存」画面が開いた状態を判別する事ができないため、別の方法で判別する必要があります。紆余曲折の末、「印刷結果を名前を付けて保存」画面のWindows Handleを取得する方法を採用しました。

以下に「印刷結果を名前を付けて保存」画面を示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659913/d7e257f0-03ca-648f-06c9-54185c0e846a.png)

以上の内容より、次のように状態遷移を設計しました。

|No|イベント等|状態|アクション|
|--:|:----------|:----|:----|
|1| なし | xmlドキュメント読込前 | xml読込実行 |
|2| DocumentComplete | xmlドキュメント読込完了 | プレビュー印刷実行 |
|3| PrintTemplateInstantiation | プレビュー画面表示中 | 5秒間隔でポーリング開始 |
|4| 「印刷結果を名前を付けて保存」のWindows Handleを取得できた | 「印刷結果を名前を付けて保存」表示中 | ファイル名にPDFファイルパスを設定した上で「保存」押下 |
|5| PrintTemplateTeardown | PDF変換完了 | 次のxmlがあればxml読込実行（No.2へ)、次のxmlが無ければフォームを閉じる |

プレビュー画面が表示された後、5秒間隔でポーリングを始めて、「印刷結果を名前を付けて保存」画面のWindows Handleを取得できないかを監視します。これは、プレビュー画面が表示されてから「印刷結果を名前を付けて保存」画面が表示されるまでの時間が分からないためです。特にファイルサイズが大きいxmlの場合、監視する時間が長くなります。

## ４．４　PDFマージリネーム
PDFのマージについては、qpdfというフリーソフトを利用する事にしました。鑑文書XMLに記載されている全てのxmlをPDFに変換した後、本ツールの中からqpdfを呼び出して、１つのPDFにマージした後、到達番号の名前にファイル名をリネームします。

## ４．５　ツールの仕様
本ツールの仕様をまとめます。

###４．５．１　前提条件

- ルートフォルダを用意する。
- ルートフォルダの中に各申請書毎の到達番号の名前を付与したフォルダを作成する。
- 作成したフォルダの中に関連する全てのxml、xsl、pdf等の電子ファイルを配置する。
- １つのフォルダに複数の到達番号に関する電子ファイルを配置する事は禁止する。
- フォルダの中にフォルダを作る事は構わない。
- Path環境変数にqpdfのbinフォルダパスを登録しておく。

excelブックを用意して、次のシートを用意します。

|No|シート名|内容|
|--:|:----------|:----|
|1| パラメタ | プログラム実行に関するパラメタを定義 |
|1| xmlリスト | PDF変換対象となるxmlフォルダーパスを設定 |

###４．５．２　入出力
ツールの入出力を以下に示します。

![入出力.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659913/b69cefdb-5716-2245-2033-3944235eadbe.png)

###４．５．３　機能仕様
PDF変換の状態遷移に合わせてシーケンスを動かしながら、ルートフォルダ以下の鑑文書XMLを探索するのは、問題が難しくなると考えました。そこで、最初にルートフォルダ以下にある鑑文書XMLを解析して、変換すべきxmlを全て把握した後、個別にPDF変換を行う方式を採用しました。

####（１）鑑文書XML探索

- ユーザがルートフォルダを指定すると、プログラムは、そこを起点に下位階層のフォルダの中にある鑑文書XMLを探索する。
- 鑑文書XMLについては、正規表現"^[0-9]{17}[0].xml"で見つける。
- 鑑文書の中を解析して、社会保険の公文書である事を確認する。
- 社会保険の公文書である場合、APPENDIXタグに記載されている全てのxmlをxmlリストシートに記録する。
- これを繰り返し、PDF変換対象となるxmlをあらかじめ抽出しておく。

####（２）xml->PDF変換、PDFマージリネーム

- １つのxmlに着目して、IEを使ってPDFに変換する。これは先に説明した状態遷移に従う。
- 鑑文書XMLに記載されている全てのxmlをPDFに変換したら、鑑文書XMLの記載順にPDFをマージする。
- マージが終わったら、ファイル名が到達番号になるようにリネームする。
- 以上の処理をxmlリストシートに登録されているxmlファイル毎に繰り返す。

####（３）フロー
以下に概念フローを示します。
![概念フロー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659913/7f4b7128-aa9e-1cca-bd66-22d43ac8676a.png)

####（４）クラス図
鑑文書XML探索に関するクラス図を示します。
![鑑文書XML探索クラス図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659913/769e1e37-65bb-f997-cb70-061fd6f4a97e.png)

xml->PDF変換及びPDFマージリネームに関するクラス図を示します。
![xml2PDF変換クラス図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659913/55001c02-41ff-45e9-4f67-0ccc1f4e5042.png)

## ４．６　ソースコード
ソースコードから重要な部分を抜粋して説明します。全体は[GitHub](https://github.com/itaruMatumoto/xml2PdfConverter)にあります。必要に応じて参照して下さい。

|No|モジュール名|内容|
|--:|:----------|:----|
|1| PdfConvertForm.frm | 状態遷移に関するシーケンスを組む |
|2| MainModule.bas | 「印刷結果を名前を付けて保存」画面のWindows Handleを取得する |
|3| SignedXmlParserClass.cls | 鑑文書XMLを解析する |
|4| MergerClass.cls | PDFをマージする |

### ４．６．１　PdfConvertForm.frm
####（１）UserForm_Initialize
```vb:xml読込実行
Option Explicit

Private Declare Sub Sleep Lib "kernel32" (ByVal ms As Long)

Private Declare Function MessageBoxTimeoutA Lib "user32" ( _
        ByVal hWnd As Long, _
        ByVal lpText As String, _
        ByVal lpCaption As String, _
        ByVal uType As VbMsgBoxStyle, _
        ByVal wLanguageID As Long, _
        ByVal dwMilliseconds As Long) As Long

' プロパティ宣言
Private targetPdfPath_ As String
Private targetXmlPath_ As String

' プライベート変数
Private xmlListObj As XmlListSheetClass
Private shellObj As Object
Private helperObj As HelperClass
Private paramObj As ParamSheetClass
Private debugObj As DebugSheetClass
Private timeStampObj As TimeStampClass
Private mergerObj As MergerClass

Private defaultPrinterName As String

'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：ユーザフォーム生成時
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Private Sub UserForm_Initialize()
    Dim list As Variant

    Set xmlListObj = New XmlListSheetClass
    Set shellObj = CreateObject("WScript.Shell")
    Set helperObj = New HelperClass
    Set paramObj = New ParamSheetClass
    Set debugObj = New DebugSheetClass
    Set timeStampObj = New TimeStampClass
    Set mergerObj = New MergerClass

    xmlListObj.SheetName = "xmlリスト"

    targetXmlPath_ = xmlListObj.Reader()

    Call debugObj.OutPut("ブラウザを表示します...")

    ' ブラウザを表示する
    With Me
        .WebBrowser1.Navigate targetXmlPath_
    End With
    
    ' PDF出力に切り替える
    list = Split(Application.ActivePrinter, " on ")
    defaultPrinterName = list(0)
    
    Call helperObj.ChangeActivePrinter("Microsoft Print to PDF")

FIN_LABEL:

End Sub
```
UserForm_Initializeは、ユーザフォームを初期化する時に呼ばれます。「.WebBrowser1.Navigate targetXmlPath_」の行で、変換対象となるxmlファイルパスをパラメタとして、IEを起動しています。なお、xmlファイルパスには外字PDFファイルパスを指定するケースもあります。

最後に、HelperClassのChangeActivePrinterメソッドにて、既定プリンタを"Microsoft Print to PDF"に変更して、PDF変換に備えています（フォームを閉じる際には元に戻します）。

####（２）WebBrowser1_DocumentComplete
```vb:プレビュー印刷実行
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：ドキュメント読み込み完了時に呼ばれるイベントハンドラー
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Private Sub WebBrowser1_DocumentComplete(ByVal pDisp As Object, URL As Variant)
    Dim rc As Boolean

    Call debugObj.OutPut("ドキュメントが読み込まれました...")

    With Me
        .MessageTextBox = "PDFに変換しています..." & Progress
    
        ' 変換するPDFパスを組み立てる
        targetPdfPath_ = helperObj.GetFileFso(targetXmlPath_).ParentFolder & "\temp_" & timeStampObj.GetTimeStamp2 & ".pdf"

        If helperObj.GetExtensionName(targetXmlPath_) = "xml" Then
        
            Call debugObj.OutPut("印刷を実行します...")

            ' 印刷実行
            ' 第1引数：OLECMDID_PRINT=6              　[ファイル]メニューの[印刷]
            ' 第2引数：OLECMDEXECOPT_DONTPROMPTUSER=2  ユーザーに入力を促すことなくコマンドを実行する
            .WebBrowser1.ExecWB OLECMDID_PRINT, OLECMDEXECOPT_DONTPROMPTUSER
    
        Else
            ' PDFファイルのコピーを作成する
            Call helperObj.CopyFile(targetXmlPath_, targetPdfPath_, rc)
        
            ' 成果物を作成する
            Call MakeDeliverables
        End If
    
    End With

End Sub
```
ここでは、xmlの読み込みが完了した時点で発火するDocumentCompleteイベントを受けて、プレビュー印刷を実行しています。「.WebBrowser1.ExecWB OLECMDID_PRINT, OLECMDEXECOPT_DONTPROMPTUSER」の行が該当します。

なお、targetXmlPath_がPDFの場合は、成果物を作成するため、MakeDeliverablesを呼び出します。

####（３）WebBrowser1_PrintTemplateInstantiation
```vb:5秒間隔でポーリング開始
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：PrintTemplateが生成された時に呼ばれるイベントハンドラー
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Private Sub WebBrowser1_PrintTemplateInstantiation(ByVal pDisp As Object)

    Call debugObj.OutPut("PrintTemplateが生成されました...")

    Application.OnTime Now + TimeSerial(0, 0, 5), "PollingPrintWindow"

End Sub
```
ここでは、印刷テンプレートがインスタンス化された（印刷プレビュー画面が表示された）ときに発火するWebBrowser1_PrintTemplateInstantiationイベントを受けて、５秒間隔のタイマーを張り、「印刷結果を名前を付けて保存」画面が表示されるまでポーリングを続けます。

Application.OnTimeでポーリングを行う場合、監視ルーチンであるPollingPrintWindow（詳細はMainModule.basで説明）については、標準モジュールの中にある必要があります。そのため、フォームであるPdfConvertForm.frmの中にはありません。

####（４）WebBrowser1_PrintTemplateTeardown
```vb:次のxmlがあればxml読込実行
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：PrintTemplateが消滅した時に呼ばれるイベントハンドラー
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Private Sub WebBrowser1_PrintTemplateTeardown(ByVal pDisp As Object)

    Call debugObj.OutPut("PrintTemplateが消滅しました...")
    Call debugObj.OutPut("PDFに変換しました...")

    Call xmlListObj.LogOnSheet("PDFに変換しました。")

    ' 成果物を作成する
    Call MakeDeliverables

End Sub

'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：成果物を作成する。
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Private Sub MakeDeliverables()
    Dim rc As Long

    ' 納品物を作成する
    Call mergerObj.MakeDeliverables(targetXmlPath_, targetPdfPath_, rc)
    
    ' 作成できた？
    Select Case rc
        Case Is = 1
            Call debugObj.OutPut("PDFをリネームしました...")
            Call xmlListObj.LogOnSheet("PDFをリネームしました。")
        Case Is = 2
            Call debugObj.OutPut("その他PDFをマージしました...")
            Call xmlListObj.LogOnSheet("その他PDFをマージしました。")
        Case Else
            ' 何もしない
    End Select

    ' シーケンスを続ける
    Call ContinueSequence(rc)

End Sub

'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：シーケンスを続ける
' 第１引数（入力）：rc As Long          成果物の状態（>0：完成、=0：途中）
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Private Sub ContinueSequence(rc As Long)
    Dim m As String

    ' 次のxmlを読み込む
    targetXmlPath_ = xmlListObj.Reader()
    
    ' 次のxmlがある？
    If targetXmlPath_ <> "" Then
        ' 成果物が作成できた？
        If rc > 0 Then
            ' 一時停止を判断させる
            m = "中止しますか？" & vbCrLf & "指示が無ければ、３秒後に再開します。"
            rc = MessageBoxTimeoutA(0&, m, "確認", vbYesNo + vbQuestion + vbDefaultButton2, 0&, 3000)
            If rc = vbYes Then
                GoTo UNLOAD_LABEL
            End If
        End If

        Call debugObj.OutPut("ブラウザを表示します...")

        With Me
            ' ブラウザを表示する
            .WebBrowser1.Navigate targetXmlPath_
        End With
    
        ' シーケンスを継続する
        GoTo CONTINUE_LABEL
    
    End If
    
UNLOAD_LABEL:
    ' 次のxmlは無いため、フォームを閉じる（シーケンスを打ち切る）
    Unload Me

CONTINUE_LABEL:

End Sub
```
ここでは、印刷テンプレートが破棄された（印刷プレビュー画面が消えた）ときに発火するWebBrowser1_PrintTemplateTeardownイベントを受けて、鑑文書内の全てのxmlがPDFに変換できていれば、PDFをマージリネームします。

その後、次に読み込むべきxmlがあれば、MessageBoxTimeoutAを使って３秒間待機した後、IEでxmlを読み込みます。次に読み込むべきxmlが無ければフォームを閉じて、シーケンスを打ち切ります。

### ４．６．２　MainModule.bas
```vb:MainModule.basから抜粋
Private Declare Sub Sleep Lib "kernel32" (ByVal ms As Long)

Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _
    ByVal lpClassName As String, _
    ByVal lpWindowName As String) As Long

Private Declare Function FindWindowEx Lib "user32.dll" Alias "FindWindowExA" ( _
    ByVal hwndParent As Long, _
    ByVal hwndChildAfter As Long, _
    ByVal lpszClass As String, _
    ByVal lpszWindow As String) As Long

Private Declare Function SendMessageAny Lib "user32.dll" Alias "SendMessageA" ( _
    ByVal hWnd As Long, _
    ByVal Msg As Long, _
    ByVal wParam As Long, _
    ByVal lParam As String) As Long

Private Declare Function PostMessage Lib "user32" Alias "PostMessageA" ( _
    ByVal hWnd As Long, _
    ByVal wMsg As Long, _
    ByVal wParam As Long, _
    ByVal lParam As Long) As Long

Private Const WM_SETTEXT = &HC
Private Const WM_KEYDOWN = &H100
Private Const VK_RETURN = &HD

'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：アクティブウインドウが印刷保存画面であるかを監視する
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Public Sub PollingPrintWindow()
    Dim hWnd As Long
    Dim titles As String * 1000
    Static cnt As Long
    Dim message As String

    Dim debugObj As DebugSheetClass

    Set debugObj = New DebugSheetClass

    Call debugObj.OutPut("ポーリングしています...")

    ' ウインドウハンドルを取得する
    hWnd = FindWindow(vbNullString, "印刷結果を名前を付けて保存")
    
    ' ウインドウハンドルが取得できた？
    If hWnd > 0 Then
        cnt = 0

        Dim hChildWnd As Long

        ' ファイル名のウインドウハンドルを求める
        hChildWnd = FindWindowEx(hWnd, 0, "DUIViewWndClassName", vbNullString)
        hChildWnd = FindWindowEx(hChildWnd, 0, "DirectUIHWND", vbNullString)
        hChildWnd = FindWindowEx(hChildWnd, 0, "FloatNotifySink", vbNullString)
        hChildWnd = FindWindowEx(hChildWnd, 0, "ComboBox", vbNullString)

        ' ファイル名のウインドウハンドルに対して、PDFファイルパスを送る
        Call SendMessageAny(hChildWnd, WM_SETTEXT, 0, PdfConvertForm.targetPdfPath)

        ' 保存(&S)のウインドウハンドルを求める
        hChildWnd = FindWindowEx(hWnd, 0, "Button", "保存(&S)")

        ' 保存(&S)のウインドウハンドルに対して、リターンキーを送る
        Call PostMessage(hChildWnd, WM_KEYDOWN, VK_RETURN, 0)

    Else
        cnt = cnt + 1

        message = "PDFに変換しています..." & PdfConvertForm.Progress & " - 監視中（" & cnt & "回）"

        Call PdfConvertForm.SetMessageTextBox(message)

        Application.OnTime Now + TimeSerial(0, 0, 5), "PollingPrintWindow"
    End If

    Set debugObj = Nothing

End Sub
```
PollingPrintWindowは、５秒間隔で呼び出されて、「印刷結果を名前を付けて保存」画面のWindows Handleが取得できないかを確認します。

Microsoft Windowsのウインドウは、ウインドウの中にウインドウがある入れ子構造になっています。「印刷結果を名前を付けて保存」画面のWindows Handleについては、FindWindow関数を一度呼び出す事で取得できます。

一方、「印刷結果を名前を付けて保存」画面の中の「ファイル名」については、ウインドウの階層に合わせてFindWindowEx関数を複数回呼び出す事で目的のWindows Handleを取得する必要があります。ソースコードを見て頂くと、DUIViewWndClassName→DirectUIHWND→FloatNotifySink→ComboBoxという順にたどって、「ファイル名」のWindows Handleを取得している事が分かります。

ウインドウの階層については、Spy++というツールを使って調べました。以下にSpy++におけるウインドウの階層表示を示します。
![Spy++.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659913/83af3955-19ce-24f3-0fb3-0440464019a4.png)

「ファイル名」のWindows Handleが取得できたら、SendMessageAnyでPDFファイルパスを送信します。その後、「保存」のWindows Handleを取得して、PostMessageでリターンキーを送信します。

説明を分かりやすくするため、もう一度「印刷結果を名前を付けて保存」画面を掲載します。「ファイル名」「保存」のウインドウに赤枠をつけておきます。
![印刷結果を名前をつけて保存（赤枠あり）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659913/0275bc4c-03b6-fc5f-5d57-5491a0f1a23d.png)

###４．６．３ SignedXmlParserClass.cls
```vb:SignedXmlParserClassから抜粋
Option Explicit

' プロパティ変数
Private FileCollect_ As Collection          ' 公文書（通知書）
Private SocialInsurance_ As Boolean         ' =True：社会保険／=False：労働保険
Private XmlFileExist_ As Boolean            ' =True：XML形式の公文書がある／=False：無い
Private ArrivalNumber_ As String            ' 到達番号

' プライベート変数
Private xmlObj As MSXML2.DOMDocument60
Private helperObj As HelperClass

'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：署名付きXMLを解析する
' 第１引数（入力）：path As String          署名付きXMLのファイルパス
' 第２引数（出力）：rc As Long              リターンコード =0:正常
'                                                          =1:xmlファイルがない
'                                                          =2:xml解析エラーが発生した
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Public Sub ParseXml(path As String, rc As Long)
    Dim eachNode As IXMLDOMNode
    Dim childNode As IXMLDOMNode
    Dim docName As String

    On Error GoTo ERROR_LABEL

    ' プライベート変数を初期化する
    InitializePrivateVariables

    rc = 0

    ' xmlが存在する？
    If helperObj.IsFileExist(path) Then
        ' xmlを取り込む
        xmlObj.Load (path)
        
        ' 到達番号を取得する
        Set eachNode = xmlObj.SelectSingleNode("//BODY/DOCNO")
        
        ArrivalNumber_ = eachNode.ChildNodes(0).Text
         
        ' 発出者を取得する
        Set eachNode = xmlObj.SelectSingleNode("//BODY/AUTHOR/AFF")
        
        If eachNode.ChildNodes(0).Text = "日本年金機構" Then
            SocialInsurance_ = True
        Else
            SocialInsurance_ = False
        End If

        ' 公文書リストを取得する
        Set eachNode = xmlObj.SelectSingleNode("//BODY/APPENDIX")
        
        ' 深さ優先探索を行い、公文書名を取得する
        Do
            ' 子要素を探索する
            For Each childNode In eachNode.ChildNodes
                Select Case childNode.nodeName
                    Case "DOCLINK"
                        docName = childNode.Attributes.getNamedItem("REF").NodeValue
                        Select Case docName
                            Case "henrei.xml"
                                ' 何もしない
                            Case Else
                                ' 拡張子がxml？
                                If Right(docName, 4) = ".xml" Then
                                    XmlFileExist_ = True
                                End If
                                FileCollect_.Add docName
                        End Select
                    Case Else
                        ' 何もしない
                End Select
            Next childNode
            ' 同じ階層の次のノードに移る
            Set eachNode = eachNode.NextSibling
        Loop While Not eachNode Is Nothing
    Else
        rc = 1
    End If
    
    GoTo FIN_LABEL

ERROR_LABEL:
    rc = 2

FIN_LABEL:

End Sub
```

ここでは、鑑文書XMLの内容を解析した上で、PDF変換対象となるxmlをリスト化しています。"henrei.xml"については、xmlであるもののPDF変換対象外とします。

###４．６．４　MergerClass.cls
####（１）MakeDeliverables
```vb:MergerClassから抜粋
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：納品物を作成する
' 第１引数（入力）：xmlFilePath As String   xmlファイルパス
' 第２引数（入力）：pdfFilePath As String   変換済みPDFファイルパス
' 第３引数（出力）：rc As Long              リターンコード =0：未処理
'                                                          =1：リネームした
'                                                          =2：マージした
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Public Sub MakeDeliverables(xmlFilePath As String, pdfFilePath As String, rc As Long)

    rc = 0

    ' マージ環境を初期化する
    Call InitializeMergeEnvironment(xmlFilePath)

    ' xmlファイルパスをキー、変換済みPDFファイルパスを値として、辞書に登録する
    convertedFilesDict.Add xmlFilePath, pdfFilePath

    ' マージファイルを作成できるか確認する
    If IsPossibleMerge Then
        Select Case xmlObj.FileCollect.Count
            Case Is = 1
                ' 変換済みPDFファイルをリネームする
                Call RenamePdf(pdfFilePath)
                rc = 1
            Case Is > 1
                ' マージファイルを作成する
                Call MergeFiles
                ' 変換済みPDFを全て削除する
                Call RemoveConvertedFiles
                rc = 2
            Case Else
                ' 何もしない
        End Select
    End If

End Sub
```
印刷テンプレートが破棄された（印刷プレビュー画面が消えた）ときに発火するWebBrowser1_PrintTemplateTeardownイベントを受けて、本ルーチンが呼び出されます。

本ツールが呼び出されると、InitializeMergeEnvironmentで鑑文書XMLを解析した後、xmlファイルパスをキー、変換済みPDFファイルパスを値として、辞書型であるconvertedFilesDictに登録します。

IsPossibleMergeで、当該フォルダにある鑑文書の中に記載されている全てのxmlがPDFに変換できているか、convertedFilesDictの登録状況を確認した上で、MergeFilesでPDFをマージします。

####（２）InitializeMergeEnvironment
```vb:MergerClassから抜粋
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：baseFolder未設定時、baseFolder変更時にマージ環境を初期化する
' 第１引数（入力）：xmlFilePath As String       xmlファイルパス
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Private Sub InitializeMergeEnvironment(xmlFilePath As String)
    Dim f As File

    Set f = helperObj.GetFileFso(xmlFilePath)

    ' baseFolder未設定？
    If baseFolder Is Nothing Then
        Set baseFolder = helperObj.GetFolderFso(f.ParentFolder)
        ' 署名付きxmlを解析する
        Call ParseSignedXml
    Else
        ' ベースフォルダが変わった？
        If baseFolder.path <> f.ParentFolder Then
            convertedFilesDict.RemoveAll
            Set baseFolder = helperObj.GetFolderFso(f.ParentFolder)
            ' 署名付きxmlを解析する
            Call ParseSignedXml
        End If
    End If

End Sub
```
xmlリストシートには、変換対象となるxmlがフォルダ単位で記録されており、フォルダの切り替わりが生じる際、マージ環境を初期化するため、本ルーチンが呼ばれます。マージ環境の初期化では、鑑文書XMLを解析して、変換対象となるxmlを抽出します。

####（３）IsPossibleMerge
```vb:MergerClassから抜粋
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：マージファイルを作成できるか否かを返す
' 返り値          ：As Boolean　=True:作成できる／=False:作成できない
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Private Function IsPossibleMerge() As Boolean
    Dim eachValue As Variant
    Dim path As String
    
    IsPossibleMerge = True
    
    For Each eachValue In xmlObj.FileCollect
        path = baseFolder & "\" & eachValue
        ' xmlファイル？
        If helperObj.GetExtensionName(path) = "xml" Then
            ' 変換済みPDFは存在しない？
            If Not convertedFilesDict.Exists(path) Then
                IsPossibleMerge = False
                ' １ファイルでもPDF未変換の場合、マージ出来ないので抜ける
                Exit For
            End If
        End If
    Next eachValue

End Function
```
ここでは、変換対象xmlが全てPDFに変換されているかを確認しています。辞書型のconvertedFilesDictには、変換対象xmlのファイルパスをキー、変換済みPDFのファイルパスを値として、登録されています。鑑文書に記載されている全ての変換対象xmlをキーに値が存在しているかを確認して、マージ可能であるかを判定します。

####（４）MergeFiles
```vb:MergerClassから抜粋
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：ファイルをマージする
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Private Sub MergeFiles()
    Dim command As String
    Dim pdf As String
    Dim f As File

    Call debugObj.OutPut("マージします...")

    ' マージ後のPDF名を決める
    pdf = Replace(signedXmlFile.path, ".xml", ".pdf")
    
    ' PDFがあれば削除する
    If helperObj.IsFileExist(pdf) Then
        Set f = helperObj.GetFileFso(pdf)
        f.Delete
    End If
       
    ' コマンドを組み立てる
    command = MakeCommand
    
    Call debugObj.OutPut(command)
    
    ' コマンドを実行する
    shellObj.Run command, 0, True
        
    ' マージ後のPDFが出来るまで待つ
    While (Not helperObj.IsFileExist(pdf))
        Sleep 1000
        DoEvents
    Wend

End Sub

'--------+---------+---------+---------+---------+---------+---------+---------+---------+
' 機能　　　　　　：コマンドを組み立てる
' 注意　qpdfは、パス名に空白があると動かない。
'       コマンドを組み立てる際にパスをダブルクオーテーションで囲む必要がある。
'--------+---------+---------+---------+---------+---------+---------+---------+---------+
Private Function MakeCommand() As String
    Dim pdfAfterMerging As String
    Dim pdfMergeList As String
    Dim eachValue As Variant
    Dim inputPath As String
    Dim eachPath As String

    ' マージ後のPDF名を決める
    pdfAfterMerging = EnclosePathDoubleQuotes(Replace(signedXmlFile.path, ".xml", ".pdf"))
    
    ' マージリストを作成する
    For Each eachValue In xmlObj.FileCollect
        ' パスに組み立てる
        eachPath = baseFolder & "\" & eachValue
        
        ' xmlファイル？
        If helperObj.GetExtensionName(eachPath) = "xml" Then
            If pdfMergeList = "" Then
                pdfMergeList = EnclosePathDoubleQuotes(convertedFilesDict.Item(eachPath))
            Else
                pdfMergeList = pdfMergeList & " " & EnclosePathDoubleQuotes(convertedFilesDict.Item(eachPath))
            End If
        Else
            If pdfMergeList = "" Then
                pdfMergeList = EnclosePathDoubleQuotes(eachPath)
            Else
                pdfMergeList = pdfMergeList & " " & EnclosePathDoubleQuotes(eachPath)
            End If
        End If
        
        ' １つ目のパスを入力パスとして覚えておく
        If inputPath = "" Then
            inputPath = pdfMergeList
        End If

    Next eachValue
    
    ' コマンドを組み立てる
    ' （注意）qpdfは、パス名に空白があると動かない。
    '         コマンドを組み立てる際にパスをダブルクオーテーションで囲む必要がある。
    MakeCommand = externalToolPath & " " & inputPath & " " & _
                    "--pages " & pdfMergeList & " " & _
                    "-- " & pdfAfterMerging

End Function
```

ここでは、qpdf向けにコマンドラインを組み立てた後、PDFをマージリネームしています。

# ５．生産性向上等の効果について
社労士法人にて、本ツールを実際に業務に使ってもらいました。その効果等について、考察します。

## ５．１　心理的効果
社労士法人の職員が持つ「公文書を顧客に返却する作業は人手がかかり、煩雑であるものの、どうにもできない」という考え方を払拭し、仕事のやり方の見直しを働きかける心理的効果が一番大きく感じました。

## ５．２　生産性向上
ツールを実行すると、必ず「印刷結果を名前を付けて保存」画面が立ち上がるため、バックグランドで実行して他の作業を並行して行う事ができません。そのため、生産性向上という意味では限定的に感じました。今回は一部の職員に試してもらいましたが、夜間帯に社労士法人全体でツールを自動運転するように運用面で工夫すれば、規模の経済が働き、効果が見えるかもしれません。

また、本ツール単体の取り組みで効果を図るという事自体が無理な事かもしれません。他にも人手で行う煩雑な作業があるため、いくつかの改善策を一定期間実施した上で効果を計らないと目に見えてこないとも感じています。

## ５．３　課題
ツールに関する課題としては、バックグラウンド実行を実現する点があります。また、テレワーク環境で本ツールを動かす事もできません。「印刷結果を名前を付けて保存」画面はテレワーク先のパソコンの画面を利用しているため、テレワーク環境だとWindow Handleが取得できないため、この点も改善する必要があります。

社労士業務においては、人手に頼る煩雑な作業が多々あります。今回の公文書xmlをPDFに変換する作業も、そのような作業の一つであり、「煩雑であるものの、どうにもできない」という考え方に囚われてしまう傾向にあります。社労夢やSmartHR等の便利なサービスはあるものの、実務となると、必ずその隙間における作業が発生し、そこに工数がかかり、生産性が向上しないというジレンマに陥ります。そのため、こうした「すき間作業」を地道に改善しながら職員の心理的な変革を促す必要があると感じています。