pythonで共有画面スクショexeアプリ

# はじめに
最近は、授業や会議をzoomなどのWeb会議システムで行うことが増えてきました。しかし、オンライン授業の大きな欠点は、画面で共有される板書がメモする間もなく流れてしまうことです。そんな悩みを解決すべく、**登録した範囲（共有画面部分など）をワンクリックでスクショし、pdfで保存できるexeアプリ**をpythonで作ってみました。（※スクショの許可が出たもののみスクショしましょう）

アプリとその詳細は [GitHub](https://github.com/AoChoco/Release/tree/master/ColLect)にありますので、使ってみてコメント（アドバイスなど）頂けたら嬉しいですー。
\<GitHub版↓\>
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605747/f1b73de5-3f09-ce2c-64a5-6b1bfbebbdd1.png width=300>


今回はその作成手順のメモとして、簡略化したフルスクリーンショットアプリを
 **1. [グラフィック（見た目）設計](#1-グラフィック設計)
 2. [イベント（動作）設定](#2-イベント設定)
 3. [アイコン作成](#3-アイコン作成自己流)
 4. [exe化](#4-exe化)**
の順番で記していこうと思います。
\<簡略化版↓\>
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605747/96e9f19f-946e-a77d-2370-d45341329d0a.png width=300>

# 準備したもの
・windows10
・python 3.7.9（py2exeでexe化するなら3.7.Xまで）
・wxPython（phoenix）
・Power Point
・py2exe（インストールの詳細は[後述](#インストール)）

#1. グラフィック設計
**wxpython**でアプリの見た目を作ります。標準ライブラリに扱いやすいtkinterがありますが、wxpythonはイベント処理が多く、凝ったアプリを作れます。
　基本は`wx.Frame`（フレーム、骨格）の中に`wx.Panel`（パネル）を敷き詰め、その上に`wx.Button`（ボタン）などのウィジェットを配置します。
　フレーム、パネル、各ウィジェットはほとんど共通の引数をとりますが、その中で今回使うものを取り上げます。

|引数|説明|
|:-:|:--|
|parent|親。ここに入れたフレームやパネルの上に自らを配置する。親が消えると子である自らも消える。|
|label / value|自らの上に表示させる文字、自らが保持する値を設定する。ウィジェットごとにどちらが使えるか、使えないかが違う。値は`[ウィジェット].GetLabel()`/`[ウィジェット].GetValue()`で取得、`[ウィジェット].SetLabel([値])`/`[ウィジェット].SetValue([値])`で変更可能。|
|pos|(x, y) の形で、スクリーン、もしくは`parent`に対する自らのx-y座標を設定。左上の角|
|size|(w, h) の形で、自らの横幅と高さを設定。|

`pos`を使わず、`wx.BoxSizer`などの"サイザー"でウィジェットを並べる方法もあります。そうすると、ユーザがウィンドウサイズを変えたときに、サイズに合わせて各パーツが移動したり拡大縮小したりするようにすることが可能です。（今回はウィンドウサイズを固定するため使いません）

具体的な使い方は下で見ていきましょう。

```sample_1.py
# coding: utf-8
import wx                                       # wxpythonをインポート
try:                                            # ⏋
    from ctypes import windll                   # ｜アプリの解像度設定
    windll.shcore.SetProcessDpiAwareness(True)  # ｜（ピンぼけを防ぐ）
except:                                         # ｜
    pass                                        # ⏌

app = wx.App()  # 先頭にこれを書いてwxをスタート

# GUI設定────────────────────────────────────────────────────────────────────────────────────────────────
frame = wx.Frame(parent=None,            # フレーム（外枠）を作成
                 title='スクショアプリ',
                 pos=(0, 0),             # pos=(x, y)はスクリーン左上を原点としたフレーム位置のx-y座標
                 size=(400, 230),        # size=(w, h)はフレームの幅(w)と高さ(h)
                 style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
                                         # styleは任意の追加設定. 今回はウィンドウのリサイズを無効化する

panel = wx.Panel(parent=frame)  # フレーム上にパネルを作成

wx.StaticText(parent=panel, label='保存先:',  # パネル上に文字列を作成. posはparent(panel)上での相対座標
              pos=(20, 20))
wx.TextCtrl(parent=panel, value='未選択',     # パネル上にフォルダの入力欄を作成
            pos=(150, 17), size=(160, -1),   # sizeに"-1"を入力するとwx任せ
            style=wx.TE_READONLY)            # TE_READONLYで入力を無効化できる
wx.Button(parent=panel, label='...',
          pos=(320, 16), size=(40, 35))

wx.StaticText(parent=panel, label='前回のスクショ:', pos=(20, 70))
wx.StaticText(parent=panel, label='―', pos=(150, 70), size=(210, -1),
              style=wx.TE_CENTER)  # TE_CENTERで, sizeで与えた幅の中で文字を中央に配置できる

wx.Button(parent=panel, label='スクショ',
          pos=(20, 120), size=(340, 40))
# ───────────────────────────────────────────────────────────────────────────────────────────────────────

frame.Show()    # 見た目ができたらフレームを表示
app.MainLoop()  # イベント入力を受け付けるようにして完成
```

実行すると見た目が出来上がります。イベントを設定していないのでボタンを押しても無反応です。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605747/4fd5fb4e-59b9-a980-d18b-3cfc6c9faea5.png width=300>

#2. イベント設定
ボタンや入力欄にイベントを設定します。
　基本は各動作ごとに処理する内容をそれぞれ関数`func_1(event, [第二引数], ...)`にまとめ、`[ウィジェット].Bind([イベントの種類], func_1)`で紐づけます。
　イベントの種類は`wx.EVT_BUTTON`（ボタンが押されたとき）や'wx.EVT_SLIDER'（スライダーの値が変わったとき）などの各ウィジェット固有のものから、`wx.EVT_CLOSE`（ウィンドウが閉じたとき）など様々です。

```sample_2.py
# coding: utf-8
import os
import re
from glob import glob
from datetime import date
from PIL import ImageGrab
import wx
try:
    from ctypes import windll
    windll.shcore.SetProcessDpiAwareness(True)
except:
    pass

TODAY = format(date.today())  # 今日の日付
last_pic_num = 0              # 写真番号

# イベント関数定義────────────────────────────────────────────────────────────────────────────────────────
def open_folder_dialog(event):  # イベントで使う関数の第一引数には"event"と書く
    """
    ダイアログで選択したフォルダのパスをfolder_txctrl内に表示し,
    フォルダ内の最後の写真番号を取得, picture_infoの情報を更新する関数
    """
    global last_pic_num
    dlg = wx.DirDialog(parent=None, message='保存先選択')  # ダイアログ設定
    if dlg.ShowModal() == wx.ID_OK:                       # ShowModal()で起動, 選択されたら以下の処理をする↓
        dirpath = dlg.GetPath()                           # 選択されたフォルダまでのパスを取得し,
        folder_txctrl.SetValue(dirpath)                   # 入力欄を更新,
        os.chdir(dirpath)                                 # そのフォルダ内に移動,
        all_pictures = [p for p in glob(os.path.join(dirpath, '*.png'))  # 今日の日付が入った写真リストを作成
                        if re.search('{}_\\d'.format(TODAY), p)]
        if all_pictures:                                                             # もし写真があれば,
            last_pic_num = max(list(map(lambda x: int(re.findall('_(\\d+)', x)[0]),  # 最後の番号を取得し,
                                        all_pictures)))
            picture_info.SetLabel(str(last_pic_num))                                 # 写真情報を更新する
        else:
            last_pic_num = 0
            picture_info.SetLabel('─')
        scshot_button.Enable()  # スクショボタンを有効化

def screen_shot(event):
    """
    画面をスクショし, 繰り上げた写真番号で保存, picture_infoの情報を更新する関数
    """
    global last_pic_num
    last_pic_num += 1                                   # 番号を一つ繰り上げ,
    picture_info.SetLabel(str(last_pic_num))            # 写真情報を更新
    screen_picture = ImageGrab.grab()                   # スクショをして,
    save_name = TODAY + '_{}.png'.format(last_pic_num)  # "2020-01-23_(番号)"の形式で,
    screen_picture.save(save_name)                      # 保存
# ───────────────────────────────────────────────────────────────────────────────────────────────────────

app = wx.App()

# GUI設定────────────────────────────────────────────────────────────────────────────────────────────────
frame = wx.Frame(parent=None, title='スクショアプリ', pos=(0, 0), size=(400, 230),
                 style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
panel = wx.Panel(parent=frame)

wx.StaticText(parent=panel, label='保存先:', pos=(20, 20))
folder_txctrl = wx.TextCtrl(parent=panel, value='未選択', pos=(100, 17), size=(210, -1),  # 後で呼び出す
                            style=wx.TE_READONLY)
folder_button = wx.Button(parent=panel, label='...', pos=(320, 16), size=(40, 35))        # 後で呼び出す
wx.StaticText(parent=panel, label='今日の最終スクショ番号:', pos=(20, 70))
picture_info = wx.StaticText(parent=panel, label='―', pos=(200, 70), size=(160, -1),      # 後で呼び出す
                             style=wx.TE_CENTER)
scshot_button = wx.Button(parent=panel, label='スクショ', pos=(20, 120), size=(340, 40))  # 後で呼び出す
# ───────────────────────────────────────────────────────────────────────────────────────────────────────

scshot_button.Disable()  # 保存先が選択されるまでスクショボタンは無効化しておく

# イベント設定────────────────────────────────────────────────────────────────────────────────────────────
folder_button.Bind(wx.EVT_BUTTON, open_folder_dialog)  # ボタンを押すと, 第二引数の関数を実行
scshot_button.Bind(wx.EVT_BUTTON, screen_shot)         # 注) 関数の最後に"()"をつけないように.
# ───────────────────────────────────────────────────────────────────────────────────────────────────────

frame.SetIcon(wx.Icon('sample_icon.ico'))  # アイコン設定（指定されたパスが存在しないとエラー）
frame.Show()
app.MainLoop()
```

アイコンを作っていないので実行するとエラーがでますが、スクショボタンを押すと選んだフォルダに写真が保存されますね。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605747/eb02a800-3675-ed5b-9965-e0564d9ead38.png width=300>

#3. アイコン作成（自己流）
`.ico`という拡張子のファイルが必要ですが、ネット上に`.png`→`.ico`に変換できるサイトがありがたいことに存在するので、パワポでpng作って変換します。（パワポじゃなくても何でもいいです）

 1. まず大きめの図形を描いて、グループ化
 2. 図形をコピー→「貼り付けのオプション」→「図」
 3. それを縮小、トリミングして一辺3.91cmの正方形に収まるようにする
 4. それを「図として保存」  
で`.png`ができます。それをこちらの変換サイト[半透過マルチアイコンfavicon.icoを作ろう!](https://ao-system.net/alphaicon/)をありがたく利用させて頂くことで、`.ico`に変換できます。今回は"sample_icon.ico"という名前で`sample_2.py`**と同じディレクトリ**に保存します。（`wx.Icon([PATH])`に適切なパスを入れればどこでも大丈夫です）
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605747/eb6d89db-b75a-6086-fa9a-7f08df6c7eab.png width=600>
"sample_2.py"を実行すると、ウィンドウ左上にアイコンが設定されているのが確認できます。

#4. exe化
起動時間の短い**py2exe**を使います。
###インストール
python 3.4までのバージョンを使うなら

```
pip install py2exe
```

python 3.5-3.7（以下3.Xとする）を使う場合は、[py2exeのリリースサイト](https://github.com/albertosottile/py2exe/releases)から`py2exe-0.9.3.2-cp3X-none-win_amd64.whl`（64ビット）か`py2exe-0.9.3.2-cp3X-none-win32.whl`（32ビット）をダウンロードし、

```
py -3.X -m pip install 'ダウンロードしたファイルのパス'
```

###準備
`setup.py`というセットアップファイルと作らないといけません。（と言っても短い）
またしても`sample_2.py`**と同じディレクトリ**に置きます。

```setup.py
from distutils.core import setup
import py2exe

option = {'compressed': 1, 'optimize': 2 , 'bundle_files': 3, 'excludes': ['email', 'numpy', 'test', 'tkinter']}

setup(
    options={'py2exe': option},
    windows=[{'script': 'sample_2.py', 'icon_resources': [(1, 'sample_icon.ico')]}],
    zipfile='lib\\libs.zip'
)
```

###exe化実行
`sample_2.py`、`setup.py`のあるディレクトリでコマンドプロンプトを起動し、

```
py -3.X -m setup py2exe
```

をすると、フォルダ内に`dist`フォルダができると思うので、`.ico`ファイルをその中に入れて（もしくは記述したパスの場所に置いて）終了です！
あとは`.exe`のショートカットでも作成してデスクトップ上に置きましょう！
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605747/d905e425-130e-964b-7e67-e30f9987d8f8.png width=500>

# おわりに
上の内容に加え、時間のかかるイベント処理はThreadingを使って別スレッドで処理することを学びました。処理が終わるまで入力を受け付けなくなり、アプリがフリーズしたようになります笑

# 参考
- [wxPython - Python-izm](https://www.python-izm.com/gui/wxpython/)
- [wxPython API Documentation — wxPython Phoenix 4.1.0 documentatio](https://docs.wxpython.org/)
- [py2exeをpython 3.5 - 3.7で使う｜Yasuhiro Takano｜note](https://note.com/noelrecords/n/n0dc600acf1c0)