アプリのスクリーンショットをpngファイルにする

# 概要

アプリのスクリーンショットをpngファイルにするマクロ。
事前に[C#](https://qiita.com/int_main_void/items/66e74c1f63c6cc3c489f#c---%E3%82%AF%E3%83%AA%E3%83%83%E3%83%97%E3%83%9C%E3%83%BC%E3%83%89%E3%81%AE%E7%94%BB%E5%83%8F%E3%82%92png%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%8C%96%E3%81%99%E3%82%8B%E3%82%A2%E3%83%97%E3%83%AA)をビルドする必要あり。

![screenshot_to_png.gif](https://qiita-image-store.s3.amazonaws.com/0/34777/419e7a35-76a0-e052-2d37-27e956f9611d.gif)

* 画像ではマクロをショートカットに登録しています。



# 背景

アプリのスクリーンショット画像をセルに書いてあるファイル名で保存したい。
「スクリーンショットを取ってExcelに貼り付ける」記事は調べると出てくる。

- [スクショをExcelに張り付けるのに役立つ4つのテクニック (2_3)：テストエビデンス取得自動化の秘技（後編）- ＠IT](https://www.atmarkit.co.jp/ait/articles/1408/26/news030_2.html)
- [画面をキャプチャする - moug モーグ](https://www.moug.net/tech/exvba/0150121.html)
- [Excel シートにスクリーンショットを自動貼り付けするマクロ - Corredor](http://neos21.hatenablog.com/entry/2017/03/25/110910)
- [エビデンス取得マクロツール(スクリーンショットキー押すだけ) - Qiita](https://qiita.com/SoreKiita/items/3d2e6f2e573818d11e0b)

が、ちょっと違う。コレジャナイ。


## 嵌ったポイント1 - Excelへの貼り付け用の例しか見つからない

Excelだけでクリップボードからpngファイル化する例はほとんど見つけられませんでした。
近いのは・・・ペイント(mspaint.exe), ADODB, SendKeysを駆使した

- [画面をコマンドラインからキャプチャする方法(WSHバッチでPrintScreen) - 主に言語とシステム開発に関して](https://language-and-engineering.hatenablog.jp/entry/20081121/1227203100)

です。

C#でクリップボード→pngファイルを組んでしまったほうが安定するかな・・・と考えて, C#でコンソールぽく実装します(作り方はフォームアプリ)。


## 嵌ったポイント2 - アプリが最小化されているとうまく取得できない

最小化されている場合, AppActivateだけではアプリ画面が最前面化されないようです。

そこで, [Word Taskオブジェクト](https://qiita.com/int_main_void/items/6c3569dc242f843ca946#%E3%82%A6%E3%82%A3%E3%83%B3%E3%83%89%E3%82%A6%E3%81%AE%E7%8A%B6%E6%85%8B%E6%9C%80%E5%A4%A7%E6%9C%80%E5%B0%8F%E6%A8%99%E6%BA%96%E3%82%92%E5%8F%96%E5%BE%97%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)を使って, ウィンドウの状態を標準に戻してからアクティブにしています。


```vb
' ターゲットアプリのタイトルバー文字列
Const APP_TITLE = "ターゲット"
・・・
    '****
    ' 1. ターゲットアプリをアクティブにする
    '****
    
    ' 最小化されている場合, AppActivateしても画面が出てこない.
    ' WordのTaskオブジェクトでwindowStateを操作して, 標準の状態に戻したあと,
    ' アクティブにする
    
    If objWord.Tasks(APP_TITLE).WindowState = 2 Then
        objWord.Tasks(APP_TITLE).WindowState = 0
    End If
    
    Dim AppFullName
    AppFullName = objWord.Tasks(APP_TITLE).Name
    
    AppActivate AppFullName, False
```


よく考えると, **[objWord.Tasks()にActivateメソッドがある](https://qiita.com/int_main_void/items/6c3569dc242f843ca946#%E3%82%A6%E3%82%A3%E3%83%B3%E3%83%89%E3%82%A6%E3%82%92%E6%9C%80%E5%89%8D%E9%9D%A2%E3%81%AB%E3%81%99%E3%82%8B)**んだった・・・以下でよかったかな。。。

```vb
' ターゲットアプリのタイトルバー文字列
Const APP_TITLE = "ターゲット"
・・・
    '****
    ' 1. ターゲットアプリをアクティブにする
    '****
    
    ' 最小化されている場合, AppActivateしても画面が出てこない.
    ' WordのTaskオブジェクトでwindowStateを操作して, 標準の状態に戻したあと,
    ' アクティブにする
    
    If objWord.Tasks(APP_TITLE).WindowState = 2 Then
        objWord.Tasks(APP_TITLE).WindowState = 0
    End If
    objWord.Tasks(APP_TITLE).Activate
```



## 嵌ったポイント3 - Windowsのアニメーション効果が有効だとタイミングが合わない

Windows10のアニメーション効果が有効だと, AppActivate後に少しWaitを入れないとダメなようです。
「アニメーション後」を取る方法がなさそうだったのでSleepでアクティブ待ちをしています。

```vb
    '****
    ' 2. ターゲットアプリのアクティブ待ち
    '****
    ' Windows10のアニメーション効果が有効な場合, AppActivateのあと少しウェイトを入れる必要がある
    Call Sleep(300) 
```

## 嵌ったポイント4 - AppActivateは完全なタイトルバーの文字列でないといけない

`objWord.Tasks(APP_TITLE)`は部分一致でいける一方で[AppActivate](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/appactivate-statement)は完全なタイトルバーの文字列([プロセスのフレンドリ名](https://gallery.technet.microsoft.com/office/29bde7e6-cd56-4925-8918-53295f15843d)?)でないとダメなようです。

`objWord.Tasks(APP_TITLE).Name`で取得したものAppActivateに渡します。

```vb
    Dim AppFullName
    AppFullName = objWord.Tasks(APP_TITLE).Name

    AppActivate AppFullName, False
```


# コード
## VBA - 主体となる部分

```vb
Option Explicit

' win32api
Private Declare Sub Sleep Lib "kernel32" (ByVal ms As Long)
Private Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal bScan As Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long)

'仮想キーコード
Const VK_SNAPSHOT = &H2C ' Printscreen
Const VK_MENU = &H12 ' Alt

' ターゲットアプリのタイトルバー文字列
Const APP_TITLE = "ターゲット"


' ターゲットアプリのスクリーンショット画像(.png)を取得する.
' 出力ファイル名は選択したセルの文字列を使う
Public Sub GetScreenshotPng()

    Dim objWsh
    Set objWsh = CreateObject("WScript.Shell")
    
    Dim objFso
    Set objFso = CreateObject("Scripting.FileSystemObject")

    Dim objWord
    Set objWord = CreateObject("Word.Application")


    '****
    ' 0. エラーチェック
    '****
    ' ターゲットアプリの存在確認
    If Not objWord.Tasks.Exists(APP_TITLE) Then
        MsgBox APP_TITLE & "が見つかりません"
        GoTo ReleaseObjects
    End If

    ' ファイル名の確認
    Dim outFileName
    outFileName = Selection.Value
    If outFileName = "" Then
        MsgBox "出力ファイル名が空白です"
        GoTo ReleaseObjects
    End If
    
    
    '****
    ' 1. ターゲットアプリをアクティブにする
    '****
    
    ' 最小化されている場合, AppActivateしても画面が出てこない.
    ' WordのTaskオブジェクトでwindowStateを操作して, 標準の状態に戻したあと,
    ' アクティブにする
    
    If objWord.Tasks(APP_TITLE).WindowState = 2 Then
        objWord.Tasks(APP_TITLE).WindowState = 0
    End If
    
    Dim AppFullName
    AppFullName = objWord.Tasks(APP_TITLE).Name
    
    AppActivate AppFullName, False
    
    
    '****
    ' 2. ターゲットアプリのアクティブ待ち
    '****
    ' Windows10のアニメーション効果が有効な場合, AppActivateのあと少しウェイトを入れる必要がある
    Call Sleep(300)
    

    '****
    ' 3. Alt + PrintScreenを押下する
    '****
    'Alt + PrintScreen
    Call keybd_event(VK_MENU, 0, 1, 0) 'Alt押下
    Call keybd_event(VK_SNAPSHOT, 0, 1, 0) 'PrintScreen押下
    Call keybd_event(VK_SNAPSHOT, 0, 3, 0) 'PrintScreen離し
    Call keybd_event(VK_MENU, 0, 3, 0) 'Alt離し


    '****
    ' 4. クリップボードの画像をpngファイル化する
    '****
    
    ' get_clipboard_image.exeのパスを作成(.xlsmと同じフォルダにあることを期待)
    Dim cmd
    cmd = """" & objFso.BuildPath(ThisWorkbook.Path, "get_clipboard_image.exe") & """"
    
    ' get_clipboard_image.exeの引数(出力ファイル名)を作成(.xlsmと同じフォルダに出力することを期待)
    Dim arg
    arg = """" & objFso.BuildPath(ThisWorkbook.Path, Selection.Value) & """"
    
    ' 「get_clipboard_image.exe 出力ファイル名」を実行
    Call objWsh.Run(cmd & " " & arg, 0, True)

    
ReleaseObjects:
    objWord.Quit
    Set objWord = Nothing
    Set objFso = Nothing
    Set objWsh = Nothing
End Sub
```

### 注1

xlsmと同じフォルダに「[get_clipboard_image.exe](https://qiita.com/int_main_void/items/66e74c1f63c6cc3c489f#c---%E3%82%AF%E3%83%AA%E3%83%83%E3%83%97%E3%83%9C%E3%83%BC%E3%83%89%E3%81%AE%E7%94%BB%E5%83%8F%E3%82%92png%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%8C%96%E3%81%99%E3%82%8B%E3%82%A2%E3%83%97%E3%83%AA)」が置かれることを決めうちした実装になっています。

png出力先も「xlsmと同じフォルダ」の決めうちです。ファイル名だけ選択したセルからとってきています。


### 注2

```vb
' ターゲットアプリのタイトルバー文字列
Const APP_TITLE = "ターゲット"
```
を変更することでスクリーンショットを取るアプリを変更できます。

この記事では[HTAで作成したダミーアプリ](https://qiita.com/int_main_void/items/66e74c1f63c6cc3c489f#%E3%82%BF%E3%83%BC%E3%82%B2%E3%83%83%E3%83%88%E3%82%A2%E3%83%97%E3%83%AA)をターゲットにしています。

### 注3
簡単化のため, マクロ呼び出しごとに各種オブジェクトを生成＋破棄してますが、 実際使うときはWorkBookを開いたときに生成してそれを使いまわす形がいいです。
※ 概要の動画gifのように特に最初のスクリーンショットが重くなる。

### 注4
[objWord.Tasks(APP_TITLE)は部分一致で判定している](https://qiita.com/int_main_void/items/1cd864010e944b07185e)ので, 似ているタイトルのアプリがいるとうまく動作しない可能性があります。objWord.Tasks(APP_TITLE)をFor Eachでまわして, フルネームを判定に使えばもう少しマシになると思います。

```vb:フルネーム
    Dim AppFullName
    AppFullName = objWord.Tasks(APP_TITLE).Name
```



## C# - クリップボードの画像をpngファイル化するアプリ

コンソール画面が出ると気になるので「何もフォームを作らないフォームアプリ」を作成します。
プロジェクト作成時は「Windowsフォームアプリケーション」を指定し, 

![image.png](https://qiita-image-store.s3.amazonaws.com/0/34777/e06a2d5c-2549-91d3-42d1-6d8809dd9e8c.png)

プロジェクト名を「get_clipboard_image」とします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/34777/885e44a9-1760-eeb3-a3f2-688e4ee80c65.png)


ソリューションエクスプローラでProgram.csを開きます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/34777/18abe480-2c30-4628-c23a-7c1367629522.png)

これを以下のコードで置き換えます。

```c#:Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing;//追加
using System.Drawing.Imaging;//追加

namespace get_clipboard_image
{
    static class Program
    {
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static int Main(string[] args)
        {
            //Application.EnableVisualStyles();
            //Application.SetCompatibleTextRenderingDefault(false);
            //Application.Run(new Form1());

            if(args.Length == 1)
            {
                var fname = args[0];

                var clip = Clipboard.GetDataObject();
                // 画像ファイルのみ取り込み
                var bmp = clip.GetData(typeof(Bitmap)) as Bitmap;
                if (bmp != null)
                {
                    bmp.Save(fname, ImageFormat.Png);
                    Clipboard.Clear();
                    return 0;
                }
                else
                {
                    MessageBox.Show("クリップボードに画像が見つかりませんでした。");
                    return 1;
                }
            }
            else
            {
                MessageBox.Show("使い方:get_clipboard_image.exe [ファイル名]");
                return 2;
            }
        }
    }
}
```


「Release」「Any CPU」でビルドすればget_clipboard_image.exeができます。
xlsmと同じフォルダにおいてください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/34777/b0b6d3d5-52ff-14b7-65ac-e0558ae6848a.png)



## html - ターゲットアプリ

ダミーのターゲットアプリ。
なんでもよかったので古の技術[HTA](https://tech.nikkeibp.co.jp/it/article/COLUMN/20100218/344797/)
テキストエディタに貼り付けて「target.hta」で保存すれば終わりです。

ダミーのターゲットアプリは、「`<html><head><title>ターゲット</title></head></html>`」の一行だけでもいいですし, 「ターゲット.txt」をメモ帳で開いても代替できます。

```html:target.hta
<html>
<head>
<title>ターゲット</title>
</head>
<script type="text/javascript">
window.onload = function myCheck(){
    window.resizeTo(400,120);
    start=new Date();
    setInterval("DispTime()",1);
}

function DispTime(){	
    now = new Date();
    document.getElementById("display").innerText = now.getTime() - start.getTime()
}	
</script>
<body>
<font size=7><div id="display"></div></font>
</body>
</html>
```


# 参考
- [[C#] クリップボードからビットマップ形式のデータを拾って、連番でフォルダに保存する - Moonmile Solutions Blog](http://www.moonmile.net/blog/archives/4526)

- [画面をキャプチャする - moug モーグ](https://www.moug.net/tech/exvba/0150121.html)
- [Excel シートにスクリーンショットを自動貼り付けするマクロ - Corredor](http://neos21.hatenablog.com/entry/2017/03/25/110910)
- [エビデンス取得マクロツール(スクリーンショットキー押すだけ) - Qiita](https://qiita.com/SoreKiita/items/3d2e6f2e573818d11e0b)
- [画面のスクリーンショットを，Excelブック内に自動的に保存するバッチ - 主に言語とシステム開発に関して.html](https://language-and-engineering.hatenablog.jp/entry/20100425/p1#%EF%BC%91Excel%E5%81%B4%E3%81%A7%E3%82%AD%E3%83%A3%E3%83%97%E3%83%81%E3%83%A3%E3%81%99%E3%82%8B%E3%82%B3%E3%83%BC%E3%83%89)
- [画面をコマンドラインからキャプチャする方法　（WSHバッチでPrintScreen）- 主に言語とシステム開発に関して](https://language-and-engineering.hatenablog.jp/entry/20081121/1227203100)

- [AppActivate ステートメント (VBA) - Microsoft Docs](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/appactivate-statement)

- [スクショをExcelに張り付けるのに役立つ4つのテクニック (2_3)：テストエビデンス取得自動化の秘技（後編）- ＠IT](https://www.atmarkit.co.jp/ait/articles/1408/26/news030_2.html)

- [Microsoft Office を使用した実際のスクリプト作成 - gallery.technet.microsoft.com](https://gallery.technet.microsoft.com/office/29bde7e6-cd56-4925-8918-53295f15843d)

- [第10回 HTAでフォームを利用したサンプルを作る - 日経 xTECH](https://tech.nikkeibp.co.jp/it/article/COLUMN/20100218/344797/)

- [Python(pywin32)でWordを操作する[6] - 特定のタイトルが付いているウィンドウの操作](https://qiita.com/int_main_void/items/6c3569dc242f843ca946)