Windows 10にGreenshotを入れて、スクリーンショットに手動ローテート機能を付けてみた

# 前置き
以前に「[Windowsのスクリーンショットを自動保存するC#のスクリプトを少し改変したメモ](https://qiita.com/crotczet/items/cfd4bf480e6e1c63e56b)」にて書いたスクリプトを使って今まで作業の証跡を採取していたのですが、ちょっと物足りない点が出てきたので、手許のスクリーンショットの機構を一新することにしました。

## 以前の実装のデメリット
- クリップボードを使用するので、作業にてコピペする際、前後で証跡を取るとコピーし直しになってしまう
- たまに落ちるので、スクショの通知が確認できない場合はタスクトレイを確認して再起動しないといけない
- クリップボードのデータフォーマットの切り分けが不十分のため、Excelなどを操作中にスクショ扱いになることがある
- Windows 10の通知を用いるため、スクショを撮るたびに通知が数秒間表示される(長い)

C#をちゃんと調べればここら辺を改善したバージョンのものも書けると思うのですが、生憎詳しくないのでおとなしく既存のソフトを活用することにしました。

## WinShotじゃ駄目なの？
Windows用のスクショソフトだと、WinShotが有名だと思います。私も使用したことがあるのですが、あれもあれでちょっと物足りない点が見受けられるんですよね。

- 古いソフトなので、表示スケールが100%以外の場合はWindowsのプロパティの「互換性」タブで設定変更が必要
- マルチディスプレイ環境にて、2枚目以降の画面でスクショが撮れない

そもそもWindows 10環境ではWindows 10に対応したソフトを使いたいなという思いがあり、今回は別のソフトを使うことにしました。
とはいえ今はWindows 11が出てきていますけど、ちょっとまだ触ったことないのでもしそっちに対応していなかったらごめんなさい。

## Greenshotのメリット
Greenshotには「External command Plugin」という機能がついており、撮ったスクショを一旦Tempフォルダに保存し自動で外部のソフトウェアに流すことができます。
これにバッチファイルを指定できるので、実質なんでもできます。
あとはマルチディスプレイ環境でもスクショが撮れます。ただし2枚目以降の画面ではマウスポインタをちゃんとキャプチャできない様です。

# 実装
折角新しくするので、ついでに作業中とかにサブフォルダ分けとかが自動でできたらいいなと思い、他のソフトと併用して、手動で保存先フォルダを切り替える機能を追加してみました。
結果として、以下の構成になりました。

1. [Greenshot](https://getgreenshot.org/) … スクショソフト
2. Greenshotがトリガーとなるバッチファイル(gs_savefile.bat: スクショを保存するバッチ)
3. [HotkeyP](https://sourceforge.net/projects/hotkeyp/files/) … キーリマップソフト
4. HotkeyPがトリガーとなるバッチファイル(hk_rotate.bat: スクショの保存先を切り替えるバッチ)
5. 両方のバッチファイルの共通設定を入れたバッチファイル(con_define.bat)
6. バッチファイルの実行時間調整用のスクリプト(usleep.c)

結果的に保存されるスクショのファイル名が、「(親ディレクトリ)\\(日付)-(連番)\\(日時)-(連番).(拡張子)」となるようにバッチファイルを書きました。

## Greenshotの設定
- 「一般」タブの「ホットキー」にて、キャプチャを実施するキーを指定する
- 「出力」タブの「ファイル名パターン」を、「${capturetime:d"yyyyMMddHHmmssff"}」などのように、YYYYMMDDで始まりかつ重複が発生しないファイル名に指定する
- 「プラグイン」タブにて「External command Plugin」を選択して「環境設定」をクリックし、「Command」が「gs_savefile.bat」のパス、「Argument」が「"{0}"」となる行を追加する
- 「撮影後の動作」タブにて、「撮影後の操作を毎回選択する」のチェックを外し、追加したコマンドだけが選択されている状態にする

## HotkeyPの設定
これについてはHotkeyPでなくても、何かしらのキー入力をトリガーとしてバッチファイルを起動することができるソフトであればなんでもよいです。
設定も、特定のキーで「hk_rotate.bat」を実行するという設定しか入れていません。

## 共通設定を入れたバッチファイル (con_define.bat)

<details><summary>ソースコード</summary><div>

```bat:con_define.bat
@echo off
rem code by Crotczet

rem 共通設定を以下に記載

rem 第一引数のサブルーチン化
goto %~1
exit /b

rem 変数定義(適宜編集可)
rem call con_define.bat
:setvalue

  rem スクショの保存先ルートフォルダ
  set dest="保存先のフォルダをフルパスで指定"

  rem サブフォルダ名の接頭辞(カレントマーク)
  set sectprefixcurr=#
  rem サブフォルダ名の接頭辞
  set sectprefix=
  rem サブフォルダ名の日付形式(YYYYMMDD=0,8: YYMMDD=2,6: YYYYMM=0,6: YYMM=2,4)
  set sectdatestart=2
  set sectdatechar=6
  rem サブフォルダ名の日付と連番をつなぐ文字列
  set sectinterfix=-
  rem サブフォルダ名の連番の桁数
  set sectnumdigit=3
  rem サブフォルダ名の接尾辞
  set sectsuffix=

  rem 画像ファイル名の接頭辞
  set captprefix=GS-
  rem 画像ファイル名の日付形式(YYYYMMDD=0,8: YYMMDD=2,6: YYYYMM=0,6: YYMM=2,4)
  set captdatestart=2
  set captdatechar=6
  rem 画像ファイル名の日付と連番をつなぐ文字列
  set captinterfix=-
  rem 画像ファイル名の連番の桁数
  set captnumdigit=5
  rem 画像ファイル名の接尾辞
  set captsuffix=

  rem サブディレクトリ名を保存するconfファイル
  set sectconffile=sect.conf
  rem 日ごとの連番を保存するconfファイル
  set captconffile=num.conf
  rem バッチの実行後待機時間(マイクロ秒)
  set utimeout=100000

exit /b

rem 指定桁数で0埋め
rem call con_define.bat padding <0埋めする数値> <桁数> (戻り値: retval)
:padding
setlocal enabledelayedexpansion
  set paddingzeros=00000000000000000000
  set num=%paddingzeros%%~2
  set num=!num:~-%~3!
endlocal && set retval=%num%
exit /b

rem 文字数取得
rem call con_define.bat strlen <対象の文字列> (戻り値: retval)
:strlen
setlocal enabledelayedexpansion
  set inputstr=%~2
  set length=0
  :strlen_loop
  if not "%inputstr%" == "" (
    set inputstr=%inputstr:~1%
    set /a length+=1
    goto :strlen_loop
  )
endlocal && set retval=%length%
exit /b
```
</div></details>

バッチファイルにてサブルーチンの仕組みを作るのは以下を参考にしました。

- [バッチファイルから別のバッチファイルのサブルーチンを呼び出す方法](http://blog.livedoor.jp/crackstars/archives/2759216.html)

なお、setvalue内の各変数は適宜書き換え可能です。ソースコードをそのままコピペした場合、保存の際のサブフォルダ名とファイル名は以下の様になります。
「保存先フォルダ\\#YYMMDD-001\\GS-YYMMDD-00001.png」
またsectprefixcurrについてですが、こちらに文字を指定すると、現在の保存先のサブフォルダにその文字が付与されます。hk_rotate.batの実行時にその文字を外し、新しいサブフォルダに対して同様に文字を付与する動作を入れることで、現在の保存先が見た目でわかるようにしました。

## バッチファイルの実行時間調整用のスクリプト
Greenshotの設定による通知だと、Windows 10の通知を用いるためどうしても数秒間画面を占有してしまいます。
あくまでスクショしたということを認知できさえすればよいので、バッチファイルの実行時間を0.1秒ほどに伸ばすことで、コマンドプロンプトの画面で認知できるようにすることを考えました。
しかしtimeout.exeは秒単位しかなく、PowerShellのStart-SleepはそもそもPowerShellの起動に時間がかかるため0.1秒など短時間の長さでは調整できません。
そのためおざなりですがC言語のusleep関数を借りることにしました。

```c:usleep.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
  if (argc) usleep(atoi(argv[1]));
  return 0;
}
```

上のソースをコンパイルし、usleep.exeとして配置します。

## スクショを保存するバッチ (gs_savefile.bat)

<details><summary>ソースコード</summary><div>

```bat:gs_savefile.bat
@echo off
rem code by Crotczet

rem スクショ時に実行させ、画像ファイルを指定場所に保存する

setlocal enabledelayedexpansion
pushd "%~dp0"

rem 変数定義の読み込み
call con_define.bat setvalue

rem sect.confファイルがなければhk_rotate.batを呼び出す
if not exist %sectconffile% call hk_rotate.bat

rem 保存対象のフォルダ名がなければ作成(カレントマーク付き、複数ある場合は最新のものを選択)
(set /p foldername=) < %sectconffile%
pushd %dest%
for /f "usebackq delims=" %%i in (`dir /od /ad /b %sectprefixcurr%%foldername%* 2^>nul`) do set sectname=%%i
if not defined sectname (
  mkdir %sectprefixcurr%%foldername% >nul 2>&1
  set sectname=%sectprefixcurr%%foldername%
)
popd

rem ファイル名から日付を取り出す(ファイル名パターンがYYYYMMDDで始まるようにGreenshot側で設定)
set captdate=%~n1
set captdate=!captdate:~%captdatestart%,%captdatechar%!
rem confファイルがなければ作成
if not exist %captconffile% (echo 0) > %captconffile%
rem confファイルの更新日付がスクショの日付と異なる場合連番を初期化
for %%i in (%captconffile%) do set conftime=%%~ti
set conftime=%conftime:/=%
set conftime=!conftime:~%captdatestart%,%captdatechar%!
if not %captdate% == %conftime% (echo 0) > %captconffile%
rem confファイルに入れた連番を代入し、インクリメント後0埋め
(set /p num=) < %captconffile%
set /a num+=1
call con_define.bat padding %num% %captnumdigit%
set captnum=%retval%

rem ファイルをTempフォルダから移動
move /y "%~1" "%dest%\%sectname%\%captprefix%%captdate%%captinterfix%%captnum%%captsuffix%%~x1" >nul 2>&1
rem 連番を上書き
(echo %num%) > %captconffile%

rem 実行動作を知覚できるようにバッチの実行時間をのばす
.\usleep.exe %utimeout%

popd
endlocal
```
</div></details>

サブフォルダに関しては前方一致で確認することにしました。そのため、サブフォルダ名の末尾に手動で作業内容をメモする(例: 「#220123-001_導入設定」)などをしても動作するようにしています。
ただし対応するフォルダが複数あった場合は一応保存はされますがちゃんと振り分けられないので注意してください。

## スクショの保存先を切り替えるバッチ (hk_rotate.bat)
<details><summary>ソースコード</summary><div>

```bat:hk_rotate.bat
@echo off
rem code by Crotczet

rem 画像ファイルの保存先フォルダを切り替える

setlocal enabledelayedexpansion
pushd "%~dp0"

rem 共通定義変数の読み込み
call con_define.bat setvalue

rem 定義変数の文字数(使うやつ)を変数に格納
call con_define.bat strlen %sectprefix%
set sectprefixlen=%retval%
call con_define.bat strlen %sectprefixcurr%
set sectprefixcurrlen=%retval%
call con_define.bat strlen %sectinterfix%
set sectinterfixlen=%retval%

rem 今日の日付(ディレクトリ名用)
set nowsectdate=%date:/=%
set nowsectdate=!nowsectdate:~%sectdatestart%,%sectdatechar%!

rem sect.confファイルがなければ作成(インクリメント前)
if not exist %sectconffile% (
  call con_define.bat padding 0 %sectnumdigit%
  (echo %sectprefix%%nowsectdate%%sectinterfix%!retval!%sectsuffix%) > %sectconffile%
)

rem confファイルに入れてある現在の対象ディレクトリ名を取り出す
(set /p foldername=) < %sectconffile%

rem 既存の対象ディレクトリにカレントマークがある場合外す(フォルダ名が重複する場合マージ)
if %sectprefixcurrlen% geq 1 (
  setlocal
  pushd %dest%
  for /f "usebackq delims=" %%i in (`dir /od /ad /b %sectprefixcurr%%foldername%* 2^>nul`) do set sectname=%%i
  if defined sectname (
    call robocopy /move /e /is /xo "!sectname!" "!sectname:~%sectprefixcurrlen%!" >nul 2>&1
    rmdir /s /q "!sectname!" >nul 2>&1
  )
  popd
  endlocal
)

rem ディレクトリ名を日付と連番に分ける(連番は一旦0を詰めてインクリメント)
set folderdate=!foldername:~%sectprefixlen%,%sectdatechar%!
set /a foldernumstart=sectprefixlen+sectdatechar+sectinterfixlen
rem 8進数扱いを回避しつつ左側の0を除去
set /a foldernum=1!foldername:~%foldernumstart%,%sectnumdigit%! * 2 - 2!foldername:~%foldernumstart%,%sectnumdigit%! + 1
rem 連番を0埋めする。日付が異なる場合は連番をリセットする
if not %folderdate% == %nowsectdate% (
  set folderdate=%nowsectdate%
  set foldernum=1
)
call con_define.bat padding %foldernum% %sectnumdigit%
set sectnum=%retval%

rem confファイルを上書き
set foldername=%sectprefix%%folderdate%%sectinterfix%%sectnum%%sectsuffix%
(echo %foldername%) > %sectconffile%

rem ディレクトリを作成(カレントマーク付き)
pushd %dest%
for /f "usebackq delims=" %%i in (`dir /od /ad /b %sectprefixcurr%%foldername%* 2^>nul`) do set sectname=%%i
if not defined sectname mkdir %sectprefixcurr%%foldername% >nul 2>&1
popd

rem 実行動作を知覚できるようにバッチの実行時間をのばす
.\usleep.exe %utimeout%

popd
endlocal
```
</div></details>

左0除去については以下を参考にしました。

- [数値の左0埋めと左0除去](http://jnats.blog.fc2.com/blog-entry-7.html)

マークの書き換えを仕組みに取り入れた関係で、フォルダが競合する可能性を考えないといけなくなりましたが、基本的に普通に使っていれば競合しないはずなので、robocopyで適当にマージするようにしています。競合の状態によってはファイルが消える可能性があるので、大事なファイルは名前がかぶりそうなややこしいところに置かないようにしてください。

# まとめ
まだあまり使っていませんが、今のところ個人的には満足しています。
また今後改善点がでてきたら修正しようと思います。