PowerShell でスクリーンショットを Excel シートに自動ペースト

# C♯ のたすけをかりてクリップボードのイベント駆動に成功

拙文『[スクリーンショットをExcelシートに半自動でペーストする(エビデンス取得用)](https://qiita.com/ktrarai/items/636812bd5b74e554bb2a "スクリーンショットをExcelシートに半自動でペーストする(エビデンス取得用)")』 (Excel VBA) ではイベント駆動に挫折し無限ループをまわしていますが、こちらは成功いたしました。

2019/04/09 更新

- ワークシートを追加するとアクティブ ワークシートにペーストされるようにしました。
- ただし、このスクリプトから起動した Excel でべつのワークブックをひらいても干渉しないようにしました[^1]。

[^1]: アクティブ ワークシートおよびアクティブ ワークブックのきりかえで対処してるのでちらつきます。また、フォームをとじるとこのプロセスの Excel からひらいたワークブックはすべてとじます。

2019/04/09 再更新

- アクティブ ワークブックのきりかえではアクティブ ワークシートが「リセット」されてしまうので修正しました。

```powershell:Paste-ScreenshotToExcelSheet.ps1
Add-Type -TypeDefinition @'
    using System;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;

    public class ClipboardListeningForm : Form
    {
        [DllImport("user32.dll", SetLastError = true)]
        private extern static void AddClipboardFormatListener(IntPtr hwnd);

        [DllImport("user32.dll", SetLastError = true)]
        private extern static void RemoveClipboardFormatListener(IntPtr hwnd);

        public event EventHandler ClipboardUpdate;
        public const int WM_CLIPBOARDUPDATE = 0x031D;

        protected override void OnLoad(EventArgs e)
        {
            AddClipboardFormatListener(Handle);
            base.OnLoad(e);
        }

        protected override void Dispose(bool disposing)
        {
            RemoveClipboardFormatListener(Handle);
            base.Dispose(disposing);
        }

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == WM_CLIPBOARDUPDATE)
            {
                OnClipboardUpdate(EventArgs.Empty);
                m.Result = IntPtr.Zero;
            }
            else
                base.WndProc(ref m);
        }

        protected virtual void OnClipboardUpdate(EventArgs e)
        {
            EventHandler handler = ClipboardUpdate;
            if (handler != null)
            {
                handler(this, e);
            }
        }            
    }
'@ -ReferencedAssemblies System.Windows.Forms

Add-Type -AssemblyName Microsoft.Office.Interop.Excel

$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true
$excel.DisplayAlerts = $true
$xlWorkbook = $excel.Workbooks.Add([Microsoft.Office.Interop.Excel.xlWBATemplate]::xlWBATWorksheet)
$excel.ActiveWindow.Caption = 'Screenshots'
$xlWorksheet = $xlWorkbook.Worksheets(1)
$xlActiveSheet = $xlWorksheet

$form = New-Object -TypeName ClipboardListeningForm
$form.Text = 'Start Capturing'

$toggleButton = New-Object -TypeName System.Windows.Forms.CheckBox
$toggleButton.Appearance = [System.Windows.Forms.Appearance]::Button
$toggleButton.Left = ($form.ClientRectangle.Width - $toggleButton.Width) / 2
$toggleButton.Top = ($form.ClientRectangle.Height - $toggleButton.Height) / 2
$toggleButton.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
$toggleButton.Text = 'Start'
$toggleButton.add_CheckedChanged({
    if ($toggleButton.Checked) {
        $toggleButton.Text = 'Stop'
        $form.Text = 'Capturing'
    } else {
        $toggleButton.Text = 'Start'
        $form.Text = 'Stopped'
    }
})
$form.Controls.Add($toggleButton)

$form.add_ClipboardUpdate({
    $xlActiveSheet = $excel.ActiveWorkbook.ActiveSheet
    $xlWorksheet = $xlWorkbook.ActiveSheet
    $targetColumn, $targetRow = 2, 2
    if ($toggleButton.Checked -and [System.Windows.Forms.Clipboard]::ContainsImage()) {
        $shapesCount = $xlWorksheet.Shapes.Count
        if ($shapesCount -gt 0) {
            $targetRow = $xlWorksheet.Shapes($shapesCount).BottomRightCell.Offset(1).Row
        }
        $xlWorksheet.Activate()
        $xlWorksheet.Cells($targetRow, $targetColumn).Select()
        $xlWorksheet.Paste()
        $xlActiveSheet.Activate()
    }
})

$form.ShowDialog()

$excel.Quit()
[void] [System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlWorksheet)
[void] [System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlActiveSheet)
[void] [System.Runtime.Interopservices.Marshal]::ReleaseComObject($xlWorkbook)
[void] [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel)
```

# 参考文献

- [C#でClipboard監視(Windows API) - Qiita](https://qiita.com/kob58im/items/2697ea4c12c72ecd86c8)
- [クリップボードを監視してテキストボックスに追加していく感じの。 · GitHub](https://gist.github.com/unarist/6342758)
- [Monitor Clipboard changes in C# using AddClipboardFormatListener  / WM_CLIPBOARDUPDATE.  See Clipboard class: http://msdn.microsoft.com/en-us/library/system.windows.clipboard(v=vs.110).aspx · GitHub](https://gist.github.com/glombard/7986317)
- [PowerShell で Excel をどうのこうのすることに興味を持ってくれると嬉しい - Qiita](https://qiita.com/miyamiya/items/161372111b68bad0744a)