【自分用】Microsoft Teamsのチャットログを無理やり保存する （自動スクロールしながら画面キャプチャするツールつくってみた）

# まえがき

いろいろやってみたが、画面キャプチャしかやりようがなさそうだったので、やってみた。
（ライセンスによってはTeamsに保存機能があるっぽい？？）

# 使用にあたって

<font color="red"><b>本プログラムはマウス入力を自動で操作するので、非常にキケンなしろものです。
安易に使用せず、プログラムおよびソース等の使用は自己責任でお願いします。</b></font>

画像のロード等を待たない（一定時間ごとにスクロールさせる）ので、<font color="red">データが正しく取り込めないケースがありえます。</font>

<font color="red">マルチスクリーンでうまく動作するか未確認</font>


# スクリーンショット／使い方

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/00616e1c-75c3-e9f2-c10e-469ef4e056fb.png)

■各コントロール
数値：`SendInput`へ指定するマウススクロールの量（単位不明。負数にすることもできます。）
Start Injectionボタン：ボタンを押すと、約3秒後にスクロールと画面キャプチャ＆保存を開始します。
Stop(Abort)ボタン：ボタンを押すと処理を停止します。<font color="red"><b>※これを押すまで止まりません。</b></font>

■使い方手順
0-a. 注意事項と手順を読む。
0-b. ネットワークを遮断しておく or チャットやメールなどの割り込みが入らない時間帯で作業する。
0-c. すぐに停止できるように、停止させるまで目を離さないこと！
1. Teamsを立ち上げておき、キャプチャしたいログを開いておく。
2. 本プログラムを起動する。
3. 数値を適宜調整する（試しに使ってみてから変更する）。
4. Start Injectionボタンを押す。
5. Teamsのログがマウスのホイール操作でログがスクロールする状態にする（ログに操作フォーカスを移す）。
6. スクロールし終わるのを待つ。
7. <b>Stop(Abort)ボタンを押す。</b>
8. <b>本プログラムを終了する。</b>
9. 出力されたファイルを確認する。
10. 適宜手順0からやり直し
11. 別途<b>[トリミングツール](https://qiita.com/kob58im/items/109781097141a1abb9cc)</b>を使って必要な箇所だけ取り出す



# ソースコード

```cs:CopyTeamsChatAsImg.cs


using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;


// KeyValueWithUpDown
public class KeyUD
{
    public enum Stroke{
        Down,
        Up
    };

    public Keys Key{get;private set;}
    public Stroke KeyStroke{get;private set;} // true: down,   false: up

    public KeyUD(Keys key, Stroke keyStroke) {
        Key = key;
        KeyStroke = keyStroke;
    }
}

class NativeUtil
{
    private static class NativeMethods
    {
        [DllImport("user32.dll", SetLastError = true)]
        public static extern void SendInput(int nInputs, Input[] pInputs, int cbsize);

        [DllImport("user32.dll", EntryPoint = "MapVirtualKeyA")]
        public static extern int MapVirtualKey(int wCode, int wMapType);

        //[DllImport("user32.dll", SetLastError = true)]
        //public extern static IntPtr GetMessageExtraInfo();


        [DllImport("user32.dll")]
        public static extern IntPtr GetDC(IntPtr hwnd);

        [DllImport("gdi32.dll")]
        public static extern int BitBlt(IntPtr hDestDC,
            int x,
            int y,
            int nWidth,
            int nHeight,
            IntPtr hSrcDC,
            int xSrc,
            int ySrc,
            int dwRop
        );

        [DllImport("user32.dll")]
        public static extern IntPtr ReleaseDC(IntPtr hwnd, IntPtr hdc);
        
        [DllImport("user32.dll")]
        public static extern IntPtr GetWindowDC(IntPtr hwnd);

        [DllImport("user32.dll")]
        public static extern IntPtr GetForegroundWindow();

        [DllImport("user32.dll")]
        public static extern int GetWindowRect(IntPtr hwnd, ref  RECT lpRect);

        
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SetProcessDPIAware();
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct RECT 
    {
        public int left;
        public int top;
        public int right;
        public int bottom;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct POINT
    {
        public int x;
        public int y;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct MouseInput
    {
        public int X;
        public int Y;
        public int Data;
        public int Flags;
        public int Time;
        public IntPtr ExtraInfo;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct KeyboardInput
    {
        public short VirtualKey;
        public short ScanCode;
        public int Flags;
        public int Time;
        public IntPtr ExtraInfo;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct HardwareInput
    {
        public int uMsg;
        public short wParamL;
        public short wParamH;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct Input
    {
        public int Type;
        public InputUnion ui;
    }

    [StructLayout(LayoutKind.Explicit)]
    private struct InputUnion
    {
        [FieldOffset(0)]
        public MouseInput Mouse;
        [FieldOffset(0)]
        public KeyboardInput Keyboard;
        [FieldOffset(0)]
        public HardwareInput Hardware;
    }

    private const int KEYEVENTF_EXTENDEDKEY = 0x0001;
    private const int KEYEVENTF_KEYUP = 0x0002;
    private const int KEYEVENTF_SCANCODE = 0x0008;
    private const int KEYEVENTF_UNICODE = 0x0004;

    private const int MAPVK_VK_TO_VSC = 0;
    // private const int MAPVK_VSC_TO_VK = 1;


    public static void SendInputKeys(KeyUD[] keys)
    {
        Input[] inputs = new Input[keys.Length];

        for(int k=0; k<keys.Length; k++) {
            int vsc = NativeMethods.MapVirtualKey((int)keys[k].Key, MAPVK_VK_TO_VSC);

            inputs[k] = new Input();
            inputs[k].Type = 1; // KeyBoard = 1
            inputs[k].ui.Keyboard.VirtualKey = (short)keys[k].Key;
            inputs[k].ui.Keyboard.ScanCode = (short)vsc;
            inputs[k].ui.Keyboard.Flags = (keys[k].KeyStroke==KeyUD.Stroke.Down)?0:KEYEVENTF_KEYUP;
            inputs[k].ui.Keyboard.Time = 0;
            inputs[k].ui.Keyboard.ExtraInfo = IntPtr.Zero;
        }

        NativeMethods.SendInput(inputs.Length, inputs, Marshal.SizeOf(inputs[0]));
    }


    //private const int MOUSEEVENTF_MOVE        = 0x0001;
    //private const int MOUSEEVENTF_LEFTDOWN    = 0x0002;
    //private const int MOUSEEVENTF_LEFTUP      = 0x0004;
    //private const int MOUSEEVENTF_VIRTUALDESK = 0x4000;
    //private const int MOUSEEVENTF_ABSOLUTE    = 0x8000;
    private const int MOUSEEVENTF_WHEEL = 0x0800;
    
    private static Input MakeMouseWheelData(int amountOfMove, IntPtr extraInfo)
    {
        Input input = new Input();
        input.Type = 0; // MOUSE = 0
        input.ui.Mouse.Flags = MOUSEEVENTF_WHEEL;
        input.ui.Mouse.Data = amountOfMove;
        input.ui.Mouse.X = 0;
        input.ui.Mouse.Y = 0;
        input.ui.Mouse.Time = 0;
        input.ui.Mouse.ExtraInfo = extraInfo;
        return input;
    }

    public static void SendMouseWheel(int amountOfMove)
    {
        Input[] inputs = new Input[]{MakeMouseWheelData(amountOfMove, IntPtr.Zero)};
        NativeMethods.SendInput(inputs.Length, inputs, Marshal.SizeOf(inputs[0]));
    }


    private const int SRCCOPY = 13369376;
    private const int CAPTUREBLT = 1073741824;

    public static Bitmap CaptureActiveWindow()
    {
        //アクティブなウィンドウのデバイスコンテキストを取得
        IntPtr hWnd = NativeMethods.GetForegroundWindow();
        IntPtr winDC = NativeMethods.GetWindowDC(hWnd);

        if (hWnd==IntPtr.Zero){return null;}

        //ウィンドウの大きさを取得
        RECT winRect = new RECT();
        NativeMethods.GetWindowRect(hWnd, ref winRect);
        Bitmap bmp = new Bitmap(winRect.right - winRect.left, winRect.bottom - winRect.top);
        Graphics g = Graphics.FromImage(bmp);
        //Graphicsのデバイスコンテキストを取得
        IntPtr hDC = g.GetHdc();
        //Bitmapに画像をコピーする
        NativeMethods.BitBlt(hDC, 0, 0, bmp.Width, bmp.Height, winDC, 0, 0, SRCCOPY);
        g.ReleaseHdc(hDC);
        g.Dispose();
        NativeMethods.ReleaseDC(hWnd, winDC);

        return bmp;
    }

    public static bool SetProcessDPIAware()
    {
        return NativeMethods.SetProcessDPIAware();
    }
}


class CopyTeamsChat : Form
{
    Button btnStart;
    Button btnAbort;
    NumericUpDown nudMouseScroll;

    System.Windows.Forms.Timer tmr;
    int _countForStartDelay;
    bool _abortReq;
    bool _timerIsRunning;
    List<MemoryStream> _msBuffer;
    readonly int IntervalInMs   = 300;
    readonly int StartDelayInMs = 3000;
    int _countOfSavedImage;

    CopyTeamsChat()
    {
        NativeUtil.SetProcessDPIAware();

        _countForStartDelay = 0;
        _abortReq = false;
        _timerIsRunning = false;
        _countOfSavedImage = 0;

        tmr = new System.Windows.Forms.Timer();
        tmr.Interval = IntervalInMs;
        tmr.Tick += (s,e)=>{
            if ( _abortReq ) {
                // 停止させる
                tmr.Stop();
                btnStart.Enabled = true;
                //btnSave.Enabled = (_msBuffer.Count>0);
                nudMouseScroll.Enabled = true;
                _abortReq = false;
                _timerIsRunning = false;
                return;
            }
            
            if ( _countForStartDelay > 0 ) {
                _countForStartDelay--;
                int inMsecForShowingCountDown = _countForStartDelay * IntervalInMs;
                Text = (inMsecForShowingCountDown/1000).ToString() + "." + ((inMsecForShowingCountDown%1000)/100).ToString();
            }
            else{
                tmr.Stop();
                NativeUtil.SendMouseWheel((int)nudMouseScroll.Value);

                Screen curScreen =  Screen.FromControl(this);
                Bitmap bmp = new Bitmap(curScreen.Bounds.Width, curScreen.Bounds.Height);
                Graphics g = Graphics.FromImage(bmp);
                //画面全体をコピーする
                g.CopyFromScreen(new Point(curScreen.Bounds.Left, curScreen.Bounds.Top), new Point(0, 0), bmp.Size);
                g.Dispose();

                bmp.Save(@"img/TeamsCapture" + _countOfSavedImage.ToString().PadLeft(6, '0') + ".png", System.Drawing.Imaging.ImageFormat.Png);
                _countOfSavedImage++;

                Text = "SavedCount:" + _countOfSavedImage.ToString();
                tmr.Start();
            }
        };

        nudMouseScroll = new NumericUpDown(){
            Location = new Point(0,0),
            Width = 100,
            Maximum = 1000,
            Value = 200,
            Minimum = -1000,
        };
        Controls.Add(nudMouseScroll);

        btnStart = new Button(){
            Location = new Point(0,40),
            Size = new Size(150,30),
            Text = "Start Injection",
        };
        btnStart.Click += (s,e)=>{
            btnStart.Enabled = false;
            //btnSave.Enabled = false;
            nudMouseScroll.Enabled = false;
            if ( !_timerIsRunning ) {
                _msBuffer = new List<MemoryStream>();
                _timerIsRunning = true;
                _countForStartDelay = StartDelayInMs/IntervalInMs;
                tmr.Start();
            }
        };
        Controls.Add(btnStart);


        btnAbort = new Button(){
            Location = new Point(0,80),
            Size = new Size(150,30),
            Text = "Stop(Abort)",
        };
        btnAbort.Click += (s,e)=>{
            _abortReq = true;
        };
        Controls.Add(btnAbort);
    }

    [STAThread]
    static void Main(string[] args)
    {
        Application.Run(new CopyTeamsChat());
    }
}
```

# 参考サイト

 - [画面をキャプチャする - .NET Tips (VB.NET,C#...)](https://dobon.net/vb/dotnet/graphics/screencapture.html#section4)