pythonで画面上の指定した範囲の文字列を取得する exe版

## 概要
前回作成したツール を他の人でも使えるようにexeにしてみました。
exe化にあたり毎回起動させると時間がかかってしまうので[こちら](https://qiita.com/kotai2003/items/0d6bafb753bb0374033c)を参考に複数ウィンドウを起動させるようにしました。
こうすることでスクリーンショットを取るたびにツールを起動させる手間が省けます。


## 処理の流れ
処理としては以下のようになっています。
1. スクリーンショット取得用のウィンドウを起動
1. スクリーンショットを取得後は前回と同じ処理の流れになります
1. 文字を取得後ウィンドウを閉じることで再度別のスクリーンショットを取得できます。


ソースコード全体は以下になります。
プロパティファイル使ってTesseractのパスを読み込む形にしています。実際に手元で使う際は直接指定するなどしてください。


<details><summary>ソースはこちら</summary>

```python

import os
import tkinter
import pyocr
import pyautogui 
import configparser
from datetime import datetime
from PIL import Image, ImageEnhance, ImageTk
import tkinter as tk
from tkinter import ttk,messagebox


iniFile=configparser.ConfigParser()
iniFile.read('resources/appConfig.ini','UTF-8')
#Pah設定
TESSERACT_PATH = iniFile.get('env','TESSERACT_PATH')
TESSDATA_PATH = iniFile.get('env','TESSDATA_PATH')

os.environ['PATH'] += os.pathsep + TESSERACT_PATH
os.environ['TESSDATA_PREFIX'] = TESSDATA_PATH

#OCRエンジン取得
tools = pyocr.get_available_tools()
tool = tools[0]

#OCRの設定 ※tesseract_layout=6が精度には重要。デフォルトは3
builder = pyocr.builders.TextBuilder(tesseract_layout=6)

RESIZE_RETIO = 2 # 縮小倍率の規定
hasOpenWindow=False

class BaseWindow(tk.Frame):
    def __init__(self,master):
        super().__init__(master)
        self.pack()

        self.master.geometry("200x50")
        self.master.title("readScreen")
        self.create_widgets()

    def create_widgets(self):
        # Button
        self.button_new_win = ttk.Button(self)
        self.button_new_win.configure(text="スクリーンショット取得処理起動")
        self.button_new_win.configure(command = self.new_window)
        self.button_new_win.pack()

    #Call back function
    def new_window(self):
        global hasOpenWindow
        if hasOpenWindow:
            messagebox.showinfo('処理起動中','処理がすでに起動しているようです。\r\n ×で閉じてしまった場合はツールを再起動してください。')
        else:
            hasOpenWindow=True
            self.newWindow = tk.Toplevel(self.master)
            self.app = ReadScreen(self.newWindow)


# 画面取得用の処理 メインディスプレイのスクリーンショットを取得する
class ReadScreen(tk.Frame):
    def __init__(self,master):

        super().__init__(master)
        self.pack()

        global canvas1,img_tk
        # 表示する画像の取得（スクリーンショット） メインディスプレイのみが対象です
        img = pyautogui.screenshot()
        # スクリーンショットした画像は表示しきれないので画像リサイズ
        img_resized = img.resize(size=(int(img.width / RESIZE_RETIO),
                                    int(img.height / RESIZE_RETIO)),
                                resample=Image.BILINEAR)

        # tkinterで表示できるように画像変換
        img_tk = ImageTk.PhotoImage(img_resized)
        
        # Canvasウィジェットの描画
        canvas1 = tkinter.Canvas(master,
                                bg='black',
                                width=img_resized.width,
                                height=img_resized.height)
        # Canvasウィジェットに取得した画像を描画
        canvas1.create_image(0, 0, image=img_tk, anchor=tkinter.NW)

        def start_point_get(event):
            global start_x, start_y # グローバル変数に書き込みを行なうため宣言

            canvas1.delete('rect1')  # すでに'rect1'タグの図形があれば削除

            # canvas1上に四角形を描画（rectangleは矩形の意味）
            canvas1.create_rectangle(event.x,
                                    event.y,
                                    event.x + 1,
                                    event.y + 1,
                                    outline='red',
                                    tag='rect1')
            # グローバル変数に座標を格納
            start_x, start_y = event.x, event.y

        def rect_drawing(event):

            # ドラッグ中のマウスポインタが領域外に出た時の処理
            if event.x < 0:
                end_x = 0
            else:
                end_x = min(img_resized.width, event.x)
            if event.y < 0:
                end_y = 0
            else:
                end_y = min(img_resized.height, event.y)

            # 'rect1'タグの画像を再描画
            canvas1.coords('rect1', start_x, start_y, end_x, end_y)

        # ドラッグを離したときのイベント 処理が多いので中身は外だし
        def release_action(event):
            self.release_action_main(event,img)


        # Canvasウィジェットを配置し、各種イベントを設定
        canvas1.pack()
        canvas1.bind('<ButtonPress-1>', start_point_get)
        canvas1.bind('<Button1-Motion>', rect_drawing)
        canvas1.bind('<ButtonRelease-1>', release_action)
        self.master.title("スクリーンショット")
        self.create_widgets()


    def create_widgets(self):
        # Button
        self.button_quit = ttk.Button(self)
        self.button_quit.configure(text="スクリーンショットを閉じる")
        self.button_quit.configure(command=self.quit_window)
        self.button_quit.pack()

    def quit_window(self):
        global hasOpenWindow
        hasOpenWindow=False
        self.master.destroy()


    # はなしたときの処理を外だし
    def release_action_main(self,event,img):
        # 'rect1'タグの画像の座標を元の縮尺に戻して取得
        start_x, start_y, end_x, end_y = [
            round(n * RESIZE_RETIO) for n in canvas1.coords('rect1')
        ]

        cropImag =img.crop(box=(start_x,start_y,end_x,end_y))

        nowDate=datetime.now().strftime('%Y%m%d')
        fileName=datetime.now().strftime('%Y%m%d%H%M%S')
        currentDirectory=os.getcwd()
        workDir=os.path.join(currentDirectory,nowDate)
        tempDir=os.path.join(currentDirectory,nowDate,'tmp')
        os.makedirs(tempDir, exist_ok=True)
        # 画像のファイル名
        pngFileName=os.path.join(tempDir,nowDate+'.png')

        # 画像をいったん保存
        cropImag.save(pngFileName)
        # 保存した画像を開く
        readimg = Image.open(pngFileName)

        #適当に画像処理 このあたりはオリジナルのままです
        img_g = readimg.convert('L') #Gray変換
        enhancer= ImageEnhance.Contrast(img_g) #コントラストを上げる
        img_con = enhancer.enhance(2.0) #コントラストを上げる

        #画像からOCRで日本語を読んで、文字列として取り出す
        readText = tool.image_to_string(img_con , lang='jpn', builder=builder)

        #半角スペースを消す ※読みやすくするため 英語の場合は単語がつぶれてしまうので日本語用のみ
        readTextJp = readText.replace(' ', '')

        print(readTextJp)
        # クリップボードにコピー
        self.master.clipboard_append(readTextJp)

        # テキストファイルとしても出力しておく 日本語用と英語用で分ける
        textFileJp=os.path.join(workDir,fileName+'jp.txt')
        textFileEn=os.path.join(workDir,fileName+'.txt')
        with open(textFileJp, mode='w') as f:
            f.write(readTextJp)

        with open(textFileEn, mode='w') as f:
            f.write(readText)

        # ダイアログにも表示させる
        pyautogui.alert(readTextJp)


if __name__ == "__main__":
    root = tk.Tk()
    app = BaseWindow(master=root)
    app.mainloop()

```
</details>

## 処理の説明
* 以下の処理でウィンドウを起動します。
* 多重起動の防止処理も行っています。

```python
class BaseWindow(tk.Frame):
    def __init__(self,master):
        super().__init__(master)
        self.pack()

        self.master.geometry("200x50")
        self.master.title("readScreen")
        self.create_widgets()

    def create_widgets(self):
        # Button
        self.button_new_win = ttk.Button(self)
        self.button_new_win.configure(text="スクリーンショット取得処理起動")
        self.button_new_win.configure(command = self.new_window)
        self.button_new_win.pack()

    #Call back function
    def new_window(self):
        global hasOpenWindow
        if hasOpenWindow:
            messagebox.showinfo('処理起動中','処理がすでに起動しているようです。\r\n ×で閉じてしまった場合はツールを再起動してください。')
        else:
            hasOpenWindow=True
            self.newWindow = tk.Toplevel(self.master)
            self.app = ReadScreen(self.newWindow)
```
* 初期化処理の中で各動作の設定を行っています。
* マウスを離したときの処理は長いので別メソッドに切り出しています。
```python
    def __init__(self,master):

        super().__init__(master)
        self.pack()

        global canvas1,img_tk
        # 表示する画像の取得（スクリーンショット） メインディスプレイのみが対象です
        img = pyautogui.screenshot()
        # スクリーンショットした画像は表示しきれないので画像リサイズ
        img_resized = img.resize(size=(int(img.width / RESIZE_RETIO),
                                    int(img.height / RESIZE_RETIO)),
                                resample=Image.BILINEAR)

        # tkinterで表示できるように画像変換
        img_tk = ImageTk.PhotoImage(img_resized)
        
        # Canvasウィジェットの描画
        canvas1 = tkinter.Canvas(master,
                                bg='black',
                                width=img_resized.width,
                                height=img_resized.height)
        # Canvasウィジェットに取得した画像を描画
        canvas1.create_image(0, 0, image=img_tk, anchor=tkinter.NW)

        def start_point_get(event):
            global start_x, start_y # グローバル変数に書き込みを行なうため宣言

            canvas1.delete('rect1')  # すでに'rect1'タグの図形があれば削除

            # canvas1上に四角形を描画（rectangleは矩形の意味）
            canvas1.create_rectangle(event.x,
                                    event.y,
                                    event.x + 1,
                                    event.y + 1,
                                    outline='red',
                                    tag='rect1')
            # グローバル変数に座標を格納
            start_x, start_y = event.x, event.y

        def rect_drawing(event):

            # ドラッグ中のマウスポインタが領域外に出た時の処理
            if event.x < 0:
                end_x = 0
            else:
                end_x = min(img_resized.width, event.x)
            if event.y < 0:
                end_y = 0
            else:
                end_y = min(img_resized.height, event.y)

            # 'rect1'タグの画像を再描画
            canvas1.coords('rect1', start_x, start_y, end_x, end_y)

        # ドラッグを離したときのイベント 処理が多いので中身は外だし
        def release_action(event):
            self.release_action_main(event,img)


        # Canvasウィジェットを配置し、各種イベントを設定
        canvas1.pack()
        canvas1.bind('<ButtonPress-1>', start_point_get)
        canvas1.bind('<Button1-Motion>', rect_drawing)
        canvas1.bind('<ButtonRelease-1>', release_action)
        self.master.title("スクリーンショット")
        self.create_widgets()
```

* スクリーンショットを取得したウィンドウの設定を行っています。
* 閉じるボタン押下時にフラグを初期化して再度実行できるようにしています。

```python
    def create_widgets(self):
        # Button
        self.button_quit = ttk.Button(self)
        self.button_quit.configure(text="スクリーンショットを閉じる")
        self.button_quit.configure(command=self.quit_window)
        self.button_quit.pack()

    def quit_window(self):
        global hasOpenWindow
        hasOpenWindow=False
        self.master.destroy()
```



## 動作例
* exe形式だと以下のようなフォルダ構成になります。別途Tesseractをインストールしてもらう必要がありますが・・・

![その3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/657697/8253965f-d8a2-d644-26b7-08a088512928.png)

* 動作の例です 取得後にすぐ別の場所から文字列を取得することが可能です。
![複数起動.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/657697/49ae5356-c17c-9cc9-e185-f5792ff96411.gif)


## 最後に

pyinstallerでexeにすると起動に時間がかかります。
そこで複数のウィンドウを起動するようにするとすぐに別のスクリーンショットを取得できるようになるので欠点を補えます。
これで他の人に配布するなどする方法も可能になるかなと