Windows環境で自動テストしてみて

## はじめに⛄
Windows環境上のゲームPFの開発アプリがテストできる状態か確認する操作を自動化するために、まずはゲームPFに開発アプリをインストールさせる操作を自動化する必要がありました。airtestでは操作中にウィンドウの切り替えがうまくできなかったため、代わりにpyautoguiやpyocrを使用しました。その中で役に立った関数や知見をまとめようと思います。非エンジニアなので優しい心で見ていただきたいです。


## 動作環境
Windows 10 Enterprise 64bit
Python 3.12.0
PyAutoGUI 0.9.54
pywinauto 0.6.3
PyGetWindow           0.0.9


## 画像を探してクリックする
airtestに便利なtouch関数があるのですがpyautoguiを使用しました
### 理由
- airtestでウィンドウの切り替え方法見つけられなかった
- airtestでディスプレイ全体を探索範囲にしても別ウィンドウをtouchすると実行終了になる解決方法見つけられず

### コード
```python
import pyautogui
import time

def click_img(img: str, **kwargs):
    # 可変長引数を代入
    sleep_time = kwargs.pop('sleep_time', 1.0)  # クリック後の待機時間
    offset_x = kwargs.pop('offset_x', 0)         # 画像認識した座標とクリック座標をx方向にオフセット
    offset_y = kwargs.pop('offset_y', 0)         # 画像認識した座標とクリック座標をy方向にオフセット
    click_lr = kwargs.pop('click_lr', 'left')    # 左クリックor右クリック
    gray_scale = kwargs.pop('gray_scale', True)  # 画像認識時のグレースケールONOFF
    confidence = kwargs.pop('confidence', 0.8)   # 画像認識時の判定度合い
    double_click = kwargs.pop('double_click', False)   # ダブルクリックする＝True、しない＝False
    # 画像の座標を代入
    image_center = pyautogui.locateCenterOnScreen(f'./img/{img}', grayscale=gray_scale, confidence=confidence)
    if image_center is not None:
        x, y = image_center
        # 座標をクリック
        if double_click:
            pyautogui.doubleClick(x + offset_x, y + offset_y, button=click_lr)
        else:
            pyautogui.click(x + offset_x, y + offset_y, button=click_lr)
    else:
        print('画像が見つかりませんでした。>', str(img))
    # 指定時間の待機
    time.sleep(sleep_time)
```

引数：探したい画像のファイル名（スクリプトのある場所/img/(ココに画像を置く)）
返り値：なし
<details><summary>使用例</summary>

```python
click_img('abcd.png', click_lr='right')
```
</details>

## 画像が表示されるまで待機する
これもpyautoguiにお世話になっています。
### 理由
airtestに便利なwait関数があるのですが、ここまできたらpyautoguiでいこうということで
### コード
```python
import pyautogui
import time

def wait_img(*args, **kwargs):
    # 可変長引数を代入
    wait_limit = kwargs.pop('wait_limit', 60) * 5  # 待機時間（int）
    gray_scale = kwargs.pop('gray_scale', True)    # 画像認識時のグレースケールONOFF
    confidence = kwargs.pop('confidence', 0.8)     # 画像認識時の判定度合い
    # 待機時間ループ
    for _ in range(wait_limit):
        # 複数画像設定時のループ
        for img in args:
            try:
                image_location = pyautogui.locateOnScreen(f'./img/{img}', grayscale=gray_scale, confidence=confidence)
                if image_location:
                    print('表示された！')
                    return
            except pyautogui.ImageNotFoundException:
                pass
        time.sleep(0.2)
```
引数：探したい画像ファイル名（スクリプトのある場所/img/(ココに画像を置く)）
返り値：なし
<details><summary>使用例</summary>

```python
wait_img('abcd.png')
```
</details>

### 参考
[[Python] PyAutoGUI 画像認識によるクリック・待機処理](https://qiita.com/takafi/items/9dfac954b39a897d9c61)
[PyAutoGUI公式ドキュメント](https://pyautogui.readthedocs.io/en/latest/)

### 知見
- pyautoguiで画像検索する時、マルチディスプレイだと範囲はディスプレイ1のみのようです
#### 
知見のある方airtestでできる方法教えてほしいです…！


## 文字を探してクリックする
pyocrとTesseractを使用しました
### 理由
- 導入が簡単そうだった
- 表示される文字が変化するボタンは参照用画像を用意できない
- airtestのpocoでもできそうだが操作したいところが開発ゲーム外の部分だったので

### 前準備
Googleが提供しているオープンソース「Tesseract」をWindowsにインストール
（手順は[Tesseract OCR をWindowsにインストールする](https://gammasoft.jp/blog/tesseract-ocr-install-on-windows/)方法通り）
### コード
- 探す範囲を特定のウィンドウ内限定する
- 探す範囲内にある文字を矩形ですべて抽出
- 抽出された文字を1個1個比較して合致したら矩形の中心座標をクリック
```python
import time
import pyocr

# 探す範囲を特定のウィンドウ内に限定する
search_window = pygetwindow.getWindowsWithTitle('探したいウィンドウ名')[0]#ウィンドウ名部分一致で取得可能
x, y = search_window.topleft
width, height = search_window.size
img = pyautogui.screenshot(region=(x,y,width,height))#探す範囲の画像

# 探す範囲内にある文字を矩形ですべて抽出
lang = 'eng'
word_boxes = tool.image_to_string(
    img,
    lang=lang,
    builder=pyocr.builders.WordBoxBuilder(tesseract_layout=6)
)

# 抽出された文字を1個1個比較して合致したら矩形の中心座標をクリック
search_text = 'abcd'
for d in word_boxes:
    print(d.content)
    print(d.position)
    x1,y1 = d.position[0]
    x2,y2 = d.position[1]
    if(d.content==search_text): 
        x3 = round((x1+x2)/2) + x
        y3 = round((y1+y2)/2) + y
        print('見つけた！')
        print('クリック座標=(',x3,',',y3,')')
        time.sleep(1)
        pyautogui.click(x3,y3)
        break
    else:
        print('ちがう')
```


### 知見
- pyocrで探した文字の位置座標は探す範囲の左上が原点なので注意（探す範囲の左上座標を加える必要があります）

- 上のコードそのまま使うとターゲットの文字が見つからない時に「ちがう」が連出して少し怖いので注意
- グレースケール等の画像処理しなくても十分な精度でテキスト抽出でした（処理時間も気になるほどではなかった）


参考：[OCR機能を用いて、画像内の対象文字列をクリックしたい](https://teratail.com/questions/323059)
　　　[Tesseract OCR をWindowsにインストールする](https://gammasoft.jp/blog/tesseract-ocr-install-on-windows/)

## 画像Aの右側の範囲で画像Bを探してクリックする
目印となる画像(A)のそばからターゲットの画像(B)を探す関数です
### 理由
探索範囲の左上から右下にかけて画像検索されるため、クリックしたい位置(画像B)よりも左上に似たような画像があるとそちらをクリックしてしまうため
### コード
```python
# imgAの右(or上下左)側だけimgBを探してクリックする
def click_imgB_side_imgA(imgB: str, imgA: str, **kwargs):
    # 可変長引数を代入
    sleep_time = kwargs.pop('sleep_time', 1.0)  # クリック後の待機時間
    offset_x = kwargs.pop('offset_x', 0)         # 画像認識した座標とクリック座標をx方向にオフセット
    offset_y = kwargs.pop('offset_y', 0)         # 画像認識した座標とクリック座標をy方向にオフセット
    click_lr = kwargs.pop('click_lr', 'left')    # 左クリックor右クリック
    gray_scale = kwargs.pop('gray_scale', True)  # 画像認識時のグレースケールONOFF
    confidence = kwargs.pop('confidence', 0.8)   # 画像認識時の判定度合い
    side_wasd = kwargs.pop('side_wasd', 'right') #探す方向Up down left right（左と上は要らなかった・・・）
    search_width = kwargs.pop('search_width', 200)    # 探す幅の長さ

    # imgA座標を代入
    imgA_x,imgA_y = pyautogui.locateCenterOnScreen(f'./img/{imgA}', grayscale=gray_scale, confidence=confidence)
    # ディスプレイの幅、高さを代入
    scr_w,scr_h = pyautogui.size()
    if side_wasd == 'right':
        # imgA座標の右側
        start_x = imgA_x
        start_y = imgA_y - search_width//2
        width = scr_w - imgA_x
        height = search_width
    elif side_wasd == 'left':
        # imgA座標の左側
        start_x = 0
        start_y = imgA_y - search_width//2
        width = imgA_x
        height = search_width
    elif side_wasd == 'up':
        # imgA座標の上側
        start_x = imgA_x - search_width//2
        start_y = 0
        width = search_width
        height = imgA_y
    elif side_wasd == 'down':
        # imgA座標の下側
        start_x = imgA_x - search_width//2
        start_y = imgA_y
        width = search_width
        height = scr_h - imgA_y
    # 特定の領域を指定した矩形領域に設定
    search_area = (start_x, start_y, width, height)
    # print('search_area=',start_x, start_y, width, height)
    # imgBを探す
    image_location = pyautogui.locateOnScreen(f'./img/{imgB}', region=search_area)
    # print('image_location=',image_location)
    if image_location is not None:
        x, y = pyautogui.center(image_location)
        # 座標をクリック
        pyautogui.click(x + offset_x, y + offset_y, button=click_lr)
    else:
        print('画像が見つかりませんでした。>', str(imgB))
    # 指定時間の待機
    time.sleep(sleep_time)
```
<details><summary>使用例</summary>

```python
#「imgA」の右側にある「imgB」をクリック
click_imgB_side_imgA('imgB.png','imgA.png')
```
</details>

## batファイルを実行する
subprocessを使用しました
### 理由
人力の時と同じように画像認識でエクスプローラー開いていってbatファイルダブルクリックもできますが、参照用画像の用意が結構大変なのでサブプロセスというものを利用して楽に
### コード
- サブプロセスでコマンドを実行する
- 外部プロセスでエラーが生じた場合、エラー内容を表示する
```python
#.batファイルのパス
bat_path = r'C:\ほにゃらら.bat'
# サブプロセスでコマンドを実行する
try:
    result = subprocess.run(
        [bat_path],
        shell = False, # シェルを介して直接実行するかどうか。Trueの場合はコマンド引数をリストではなくて文字列で渡す。
        check = True, # 実行されたプロセスが正常に終了したかどうかを確認する
        capture_output = True, # 標準出力および標準エラー出力をキャプチャするかどうか
        text = True, # 標準入力(input引数)や標準出力(stdoutおよびstderr引数)がテキストとして扱われる
    )
# 外部プロセスでエラーが生じた場合に、エラー内容を表示する
except subprocess.CalledProcessError as e:
    print(f"Error: Command '{e.cmd}' returned non-zero exit status {e.returncode}")
    print(f"Output: {e.stdout}")
    print(f"Error Output: {e.stderr}")
time.sleep(5)# 念のため休み
```
参考：[Python 外部プロセスによってバッチ処理する「subprocess.run()」](https://hk29.hatenablog.jp/entry/2021/08/01/210456)

## chatGPT便利
上記コードはブログ等で紹介されたコードを元にしたものだったり、chatGPTにベースを書いてもらったものだったりします。
chatGPTは「〇〇ライブラリで〇〇するコードを書いて」と注文するとわかりやすい解説付きでコードが返ってくるので、初心者の自分にはとてもありがたかったです。ただ、たまにコードを書いてもらった時に存在しないライブラリが出てきたりしたので、アルゴリズムだけ参考にする程度がいいのかなと思いました。