【VBA】ExcelVBAで実現するエビデンスツール

# はじめに
インフラ系のエンジニアでWindowsServerやWebコンソール周りの操作をする際などにエビデンスとして画面のスクリーンショットを撮るといったことは結構定番かなと思います。

自分も新人の頃は「自分の身を守るためにもエビデンスを残すように！」と何度も忠告をうけたなーなんてことを思い出します。

エビデンスの残し方としてはWinshotのようなツールを利用して効率よくとっていくこともありますが、私の周りではExcelシートにべた張りしていく方法がよく用いられてましたね。

## Excelエビデンスのメリット

Excelシートに貼り付けていくことは以下のようなメリットがあるかと思います。
- 単一のファイルで管理可能
- シートを分けることで、作業単位でエビデンスを記録できる
- 適宜コメントなどを書き込める(Excelだから)

とはいっても、以下のフローを毎回実施するのは面倒です。
1. 記録対象のウィンドウを開きprintscreenを押す
2. Excelにウィンドウを切り替える
3. 貼り付ける位置のセルを選択状態にする
4. ペーストする

今回はツールにより、先のフローの内2-4を自動化します。

# ソースコード

<details>
<summary>ソースコードを見る</summary>

```vb:ソースコード
Option Explicit
'キャプチャ収集状態ならTrue
Private isLogging As Boolean

'キャプチャを貼り付けるブック名を保持する
Private fileName As String

'############################
'#キャプチャ取得プロシージャ
'############################
Private Sub Capture()
    On Error GoTo errorHandler
 
 'クリップボードに画像が格納されていたら貼り付ける
    If Application.ClipboardFormats(1) = xlClipboardFormatBitmap Then
       Dim rows As Integer: rows = 63 '行数
    
       'キャプチャを貼り付ける
       Workbooks(fileName).Activate 'マクロ実行時にfileName変数にアクティブbookの名前をいれる。
       
       '選択しているセルを基準セルとして取得する
       Dim baseCell As Variant
       Set baseCell = Selection
       
       'クリップボードのデータを貼付け、行数に合わせて縮小する
       baseCell.Offset(1, 1).Select
       ActiveSheet.Paste
       With Selection.ShapeRange
           .LockAspectRatio = msoTrue
           .Height = .Height * 0.7
       End With
       
       '次の画像を貼るために基準セルを移動し、クリップボードに現在のセルの値をコピーする(クリップボードの中身をBitmapでなくすため)
       With baseCell.Offset(rows + 1, 0)
           .Select
           .Copy
       End With
       
       '切り取り・コピーモードを解除する
       Application.CutCopyMode = False
       
       'Windows通知処理(外部PWスクリプトの実行)
        Dim PWobj
        Set PWobj = CreateObject("WScript.Shell")
        PWobj.Run ("Powershell -ExecutionPolicy RemoteSigned -Command <通知用Powershellスクリプト>"), 0
       
    End If
    
    '1秒間隔で再実行するようにタイマーをセットする
    Application.OnTime Now + TimeValue("00:00:01"), "Capture", , isLogging
    
    Exit Sub
    
errorHandler:
    isLogging = False
    
End Sub

'########################
'#キャプチャを開始する
'########################
Sub StartCapture()
    MsgBox "キャプチャの取得を開始します。終了時にはEscキーを押下してください。"
    
    'Escキーで停止できるようにしておく
    Application.OnKey "{ESC}", "StopCapture"
    
    'キャプチャを貼り付けるブック名を取得する
    fileName = ActiveWorkbook.Name
    
    'キャプチャ取得状態を設定する
    isLogging = True
    
    'キャプチャ実行中であることを示すためシート見出しを赤く塗りつぶす
    ActiveSheet.Tab.Color = RGB(255, 0, 0)
    
    'キャプチャの取得を開始する
    Capture
    
End Sub

'########################
'#キャプチャを終了する
'########################
Sub StopCapture()
    If isLogging = True Then
    
        'キャプチャの取得状態を解除する
        isLogging = False
        
        'ESCキーへの登録を解除する
        Application.OnKey "{ESC}", ""
        
        'シート見出しの色を戻す
        ActiveSheet.Tab.Color = RGB(0, 0, 0)
        
        MsgBox "キャプチャの取得を停止しました。"
        
    End If
    
End Sub

```
</details>

## 解説

### プロシージャ
| プロシージャ | 説明                                                                  | 
| :----------- | :-------------------------------------------------------------------- | 
| Capture      | クリップボードにある画像データをExcelシートに一定の間隔で張り付ける。 | 
| StartCapture | Captureプロシージャを実行する                                        | 
| StopCapture  | Captureプロシージャを停止する                                        | 

``Capture``は``Application.OnTime``メソッドにより、1秒間隔でループ実行されています。
つまるところ1度``StartCapture``で動き出せば、``StopCapture``を実行するまで止まりません。

``StartCapture``実行時に``StopCapture``の実行をESCキーに割り当てているため、マクロ実行時はESCキーから停止可能。

### クリップボードに画像が格納されていたら貼り付ける
```vb
If Application.ClipboardFormats(1) = xlClipboardFormatBitmap Then
```
エラー発生時用のGoto文の次はいきなりif文による判定です。
これはクリップボードに含まれているデータがビットマップかを判定してます。
``Application.ClipboardFormats``メソッドはクリップボードに格納されたデータ形式に基づいて数値を返します。
Bitmapの場合9が返ります。
``xlClipboardFormatBitmap``は列挙型で定義されており中身は9です。
これにより、Bitmapの場合True判定として処理を続行します。

Falseの場合、``Application.OnTime``まで飛びます。
→先頭からやり直し。

余談ですが、
この箇所でいまだに不明な点がひとつあります。
``Application.ClipboardFormats()``は()の中にindexを入れるのですが、
このインデックスは返す配列の要素を選択するそうです。
「クリップボードに記録されているデータがそれぞれ配列としてインデックスが割り振られていて、取り出したい要素を指定する」そんなイメージを持っていました。
つまり(2)としたら、２つ前のクリップボードのデータの形式を返すのかな？とか思いましたが、debug.printで値を確認しても一致しなかった…
しかも``44``なる値が返ってきて、列挙型にも定義されていない値でした。
自分の認識に誤りがありそうですが、これはちょっと気になりますね。

### 貼付け間隔を定義する
```vb:間隔
Dim rows As Integer: rows = 63 '行数
```

この間隔はディスプレイの解像度によって変わります。
また画像間隔をどの程度設けるのか好みがあると思いますので、
環境ごとに適宜修正頂くのが良いと思います。

### 図形のサイズを調整

```vb:図形サイズ調整
With Selection.ShapeRange
    .LockAspectRatio = msoTrue
    .Height = .Height * 0.7
End With
```
``.LockAspectRatio = msoTrue``とすることで図形のサイズを変更した場合も縦横比を維持させます。
``.Height = .Height * 0.7``で高さを調整します。
事前にmsoTrueとしているため、縦横比は保ちながらサイズ変更可能です。

ここも個人の趣向が強い箇所かと思いますので環境ごとに適宜設定頂くのがいいかなと思います。

### ペースト完了時にWindows通知を出す
``Application.OnTime``でCaptureプロシージャを無限ループしている訳だが、
気づかない間に処理が停止していてエビデンスが取れていなかった！
なんてことが起こったら元も子もない。
ということで、処理の最後にペースト処理が完了した旨のWindows通知を出す処理を入れます。

通知例)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292417/6023cc17-87c7-c8ff-09c4-a1d56a478426.png)


Windows通知はWinRTのAPIを呼び出す必要があるがVBAで直接実行することはできない。
ここではPowerShellで通知スクリプトを作成しそれをVBAから呼び出す形にする。

```powershell:トースト通知
$bodyText = 'ペースト処理が完了しました'

$ToastText01 = [Windows.UI.Notifications.ToastTemplateType, Windows.UI.Notifications, ContentType = WindowsRuntime]::ToastText01
$TemplateContent = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]::GetTemplateContent($ToastText01)
$TemplateContent.SelectSingleNode('//text[@id="1"]').InnerText = $bodyText
$AppId = 'Microsoft.Office.EXCEL.EXE.15'
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Show($TemplateContent)
```
ここで通知を出すアプリケーションのAppIDを指定する必要がある。
AppIDの確認は調べても中々思うように値を確認することができず苦戦したが、
以下のコマンドで取得できた。
```powershell:AppIDの確認
get-StartApps
```
:::note
AppIDはAUMIDと記載されるケースもあるらしい
:::

これをVBAから呼び出すが、PowershellスクリプトもVBAから直接呼び出すことができないため、
WSHを使って、スクリプトを呼び出す。

```vb:Psスクリプトの実行
Dim PWobj
Set PWobj = CreateObject("WScript.Shell")
PWobj.Run ("Powershell -ExecutionPolicy RemoteSigned -Command <Powershellスクリプト>, 0
```

runメソッドを使用することで外部コマンドを実行することができる。
これを利用して、Powershellを外部コマンドとして実行する。

# Windows11環境での注意点
Windows11からPrintScreenキー押下時の挙動がsnnipingtoolの起動になった模様。
※以前は直接クリップボードに格納されていた。

これだと効率が落ちるため、
従来の設定に戻すことをお勧めします。

【設定方法】
[アクセシビリティ]-[キーボード]-[PrintScreen]キーを使用して画面キャプチャを開く→**オフ**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292417/9d891a8d-a726-b66b-94b6-95664e334de5.png)

余談ですが、Windows10環境の時よりも通知のレスポンスが遅くなっている点がちょっと気になる点ですね。
コンパイルして実行速度上げた方がいいのかもしれない...

# 参考
https://learn.microsoft.com/ja-jp/office/vba/api/excel.application.clipboardformats

https://learn.microsoft.com/ja-jp/office/vba/api/excel.xlclipboardformat

https://jannikreinhard.com/2021/07/22/get-the-application-user-model-id-aumid/

https://learn.microsoft.com/ja-jp/powershell/module/startlayout/get-startapps?view=windowsserver2022-ps