zenityをpythonのsubprocessで使って簡易GUI作成

# 概要
- zenityの簡単な紹介
- pythonのsubprocessチュートリアル
- 簡易GUIの使用例

zenityのオプションは一つだけ縛りで、とりあえず動かすことを目的とします。
※subprocessのオプションは使います

## 1. 事前準備
### zenityについて
`zenity`はLinuxコマンドで、簡単なGUI画面を表示できます。
Raspbery pi OSなどのデスクトップ環境Linuxなら最初から入っていることが多いです。
最初にターミナルで実行してみて、コマンドが使えるか確認してください。
```
$ zenity --info
```
コマンドが使える状態なら、GUIのダイアログが表示されます。
画像は実行例、左から`RaspberryPi OS Bookworm`, `Windows11+WSL2+Ubunru22.04`, `MacOs sonoma`。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/d89dff61-a4ba-e39d-4040-d96fbe348ee5.png" width="240"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/48d0531c-a36b-1d09-530c-bedbc3968d85.png" width="240"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/4dfc1a1c-a597-d44c-6e06-8a8a94fa8911.png" width="240">

zenityコマンドがない場合(例えばWindowsのWSL2にインストールしたてのUbuntuなど)はzenityのインストールが必要です。(`snap install zenity`や`apt install zenity`など)
MacOSも`brew install zenity`でインストールしないと使用できません。
※ ほかのコマンドに依存して、知らないうちにインストールしてる場合もある。geditを入れたとか。

コマンドがターミナルで動くなら、Pythonからsubprocessで使用可能です。

## python3でGUI使用
(確認環境はRaspberry Pi OS Desktop 64bitのBookworm)
python3を立ち上げます。
```
$ python3
Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
```
subprocessをimportします。
```python
>>> import subprocess
```
### その1: subprocess.runで実行のみ(GUIで表示表示するだけ)
ダイアログを表示するだけ。
`subprocess.run()`でコマンド実行。
windowを開いてる間はコマンドが進まないので、batファイルのpauseのようにも使えます。
```python
# 情報ダイアログ
>>> subprocess.run(['zenity', '--info'])

# アイコン違いで警告やエラーの使い分けが可能。
>>> subprocess.run(['zenity', '--warning'])
>>> subprocess.run(['zenity', '--error'])
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/2d6b23aa-f87b-a35c-0dde-dfabbcfd9d4f.png" width="240">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/fadd1e3e-2b53-badc-b65e-8d4e420ef51f.png" width="240">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/d6f6cd4a-9ffc-4108-6494-ecb46a0a8242.png" width="240">

### その2: subprocess.runとリターンコードで分岐
`subprocess.run()`のリターンコード取得。typeはint。
OKまたはyesを押した場合はリターン0。NoやCancelを押したり、window右上のxアイコンで閉じたらリターン1。
```python
>>> proc = subprocess.run(['zenity', '--question'])
>>> proc = subprocess.run(['zenity', '--question'])
>>> if proc.returncode == 0:
...     print('yesが押されました')
... else:
...     print('キャンセルします')
# 0や1以外の数値になる場合もある(スペルミスでコマンド失敗など)
>>> type(proc.returncode)
# <class 'int'>

# 下記でも分岐判断が可能。(通常はオプション追加して表示をカスタムしながら使う)
>>> proc = subprocess.run(['zenity', '--forms'])
>>> proc = subprocess.run(['zenity', '--text-info'])
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/62e5f6a0-c9be-67dc-ff55-7031c47efebb.png" width="240">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/4a293029-8ae8-1911-66a8-a03fbaf68205.png" width="240">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/55bdabc3-ac8d-384a-b6e3-4560144a8062.png" width="240">


2択の`--question`が基本。
`--forms`は本来はいろいろなフォームを組み合わせますが、上の画像は何もフォームを追加していないのでただの2択です。
`--text-info`は長文(利用規約など)を表示しつつ同意を求めたり、「ただ今キャンペーン中！」的なグラフィカルなおしらせを表示する使い方ができますが、上の画像はファイルもURLも開いてないのでただの2択です。

なお、`--info`などでも「OKを押した」か「windowsアイコンのxで閉じた」かで分岐可能。~~そういう横着をすると大体ろくなことにならない~~

### その3: subprocess.runと標準出力(stdout)で情報取得
`subprocess.run()`に`stdout=subprocess.PIPE`をつけて情報取得。

`--entry`で基本的な挙動の確認。後述するほかのオプションも共通の挙動。
```python
# テキスト入力(基本動作)
>>> proc = subprocess.run(['zenity', '--entry'],
...                        stdout=subprocess.PIPE)
# stdoutは「typeがbytes」かつ「末尾に改行コード(\n)付き」
>>> print(proc.stdout)
b'TEST1234\n'
# bytesではなくstrにしたい場合はdecode()を使う(デコードできないとエラーになるので注意)
>>> print(proc.stdout.decode())
TEST1234

# returncodeは--questionオプションと同様。returncodeが0以外のときはstdoutが空なので注意。
>>> print(proc.returncode)
0
# 改行コードはrstrip()で削除できる。slice指定などでも可能。
>>> print(proc.stdout.rstrip())
b'TEST1234'
# subprocess.runのオプションで「text=True」を指定する方法でもstr型で取得可能。
>>> proc = subprocess.run(['zenity', '--entry'],
...                        stdout=subprocess.PIPE, text=True)
>>> print(proc.stdout)
TEXT1234

```
![31a_entry_入力済み.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/3a3ec653-e978-e121-4307-3c52241de073.png)

他のオプションの使用例を下記に示す。
```python
# --password : 入力中の文字を隠しながらテキスト入力
>>> proc = subprocess.run(['zenity', '--password'],
...                        stdout=subprocess.PIPE)
# 出力内容はentryと同じ。
>>> print(proc.stdout)
b'TEST1234\n'

# --scale : スライダーを動かして0から100の値を指定
>>> proc = subprocess.run(['zenity', '--scale'],
...                        stdout=subprocess.PIPE)
# 0から100の値を出力
>>> print(proc.stdout)
b'80\n'

# --calendar : カレンダーで年月日を指定
>>> proc = subprocess.run(['zenity', '--calendar'],
...                        stdout=subprocess.PIPE)
# 年月日を出力(注意として、デフォルトでは表示フォーマットがOSや設定で変わる)
>>> print(proc.stdout)
b'12/08/2023\n'

# --color-selection : 色を選択してRGB(透過率も変更した場合はRGBA)を取得
>>> proc = subprocess.run(['zenity', '--color-selection'],
...                        stdout=subprocess.PIPE)
# 色だけを変更した場合はRGBの合計3個の数値を出力
>>> print(proc.stdout)
b'rgb(255,0,0)\n'
# 透過率も変更したらRGBAの合計4個の数値を出力
# b'rgba(140,207,6,0.697917)\n'

# --file-selection : ファイルを選択してパスを出力
>>> proc = subprocess.run(['zenity', '--file-selection'],
...                        stdout=subprocess.PIPE)
# 選択したファイルの絶対パスを取得する。
>>> print(proc.stdout)
b'/home/pitest/Pictures/20231205_14h16m54s_grim.png\n'
# pathlibで使用する場合、decode()でbytesからstrへ変換し、rstrip()で改行コード削除して対応可能。
>>> import pathlib
>>> path_picture = pathlib.Path(proc.stdout.decode().rstrip())
>>> path_picture.exists()
True
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/baa19b55-54c0-3c35-1562-24fe99372f22.png" width="240">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/518d6973-ca9d-62d8-7182-5b2273c0e8c7.png" width="240">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/0be79f40-5dc8-ef7d-8063-0d9ea9f53604.png" width="240">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/0bcc3bb0-51b0-6f6c-e050-b805cdf3218a.png" width="240">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/3a9bec15-7f00-ac90-ba26-1493dca9d721.png" width="480">

### その4: subprocess.Popenと標準入力(stdin)で進捗表示(プログレスバー)
GUIで「処理完了まで〇〇%」と表示する進捗バー。
ここまでのGUIと違って、画面に表示してるときに止まってほしくないので、`subprocess.run()`ではなく`subptrocess.Popon()`を使います。
進捗具合は0から100までを自己申告する形式。
```python
# --progress : プログレスバーの表示。標準入力に数値(0から100)を渡すとバーが動く。
# 100%になるとOKボタンが有効化する。
>>> proc = subprocess.Popen(['zenity', '--progress'],
...                          stdin=subprocess.PIPE)
# stdinにwrite()で数値と改行コードを渡す。
>>> proc.stdin.write(b'25\n')
# flush()でGUI表示に反映される
>>> proc.stdin.flush()

# 100を渡すとOKを押せるようになる。
>>> proc.stdin.write(b'100\n')
>>> proc.stdin.flush()

# GUIが表示中か、閉じられたかはpoll()で確認可能。
>>> print(proc.poll())
# None : まだGUI表示中
# 0 : OKを押してGUIを閉じてる
# 1 : Cancelしたり、右上のxアイコンで閉じてる
# プロセスをkillしたなど、0や1以外の数字になる場合あり。

# pythonからGUIを終了させたい場合はterminate()やkill()などを使用可能
>>> proc.terminate()
# terminateで閉じた場合、poll()すると-15
>>> proc.kill()
# killで閉じた場合、poll()すると-9
>>> proc.send_signal(subprocess.signal.SIGINT)
# SIGINT送信で閉じた場合、poll()すると-2
```
![41_プログレスバー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2822812/ef47d1fd-c7e1-b3ae-af9f-2731ebe7c9e7.png)

※Popenでstdin.write()を使うとデッドロックの原因になりうるらしく非推奨になっているので、使用時はその点を考慮してください。

簡単なプログレスバー使用例
```python
import subprocess
import time

# プログレスバーを別プロセスで表示
proc = subprocess.Popen(['zenity', '--progress'],
                         stdin=subprocess.PIPE)
# プログレスバーを表示したまま、処理を模擬
for i in range(101):
    # 進捗の途中でキャンセルしたら中断。ループをbreakして終了表示に進む。
    if proc.poll() is not None:
        print('ABORT!!')
        break
    # 0からカウントアップしてGUIにwrite
    bin_send = f'{i}\n'.encode()
    proc.stdin.write(bin_send)
    proc.stdin.flush()
    # 本来はここで何らかの処理。処理のかわりに0.1秒待機
    time.sleep(0.1)

# GUI表示している間、ループして待機
while proc.poll() is None:
    time.sleep(0.1)

if proc.poll() == 0:
    # OKが押された(100%になるまで待って、正規の手順でGUIを閉じた)場合
    subprocess.run(['zenity', '--info'])
else:
    # 進捗の途中で中止した、100%まで待ったけどキャンセルで閉じた、など
    subprocess.run(['zenity', '--error'])
```


### その5: オプション無しだと使えないので今回対象外
```python
# --list : リスト選択。オプションを使って選択肢を用意しないとGUI表示できない
>>> subprocess.run(['zenity', '--list'])
No column titles specified for List dialog.
CompletedProcess(args=['zenity', '--list'], returncode=255)

# --notification : 通知。デスクトップ画面の隅っこにピロンッと出てくるやつ。
# window表示じゃないためか、いろいろと追加で準備が必要。
# macOSだとこのオプション自体が存在しないようです。
>>> subprocess.run(['zenity', '--notification'])
CompletedProcess(args=['zenity', '--notification'], returncode=1)
```

## おまけ: help表示
ターミナルで`zenity --help`と入力するとzenityの概要が、`zenity --help-general`と入力するとGUIの横幅指定やタイトル変更など汎用的なオプションの説明が、`zenity --help-progress`などGUIの種類をつけるとそのGUI特有のオプション説明が確認できます。

実行環境によって機能に差があるようなので、まずは自前の環境で確認するのが無難です。

Raspberry pi OS(Bookworm)の場合
```terminal
$ zenity --help
Usage:
  zenity [OPTION…]

Help Options:
  -h, --help                                        Show help options
  --help-all                                        Show all help options
  --help-general                                    Show general options
  --help-calendar                                   Show calendar options
  --help-entry                                      Show text entry options
  --help-error                                      Show error options
  --help-info                                       Show info options
  --help-file-selection                             Show file selection options
  --help-list                                       Show list options
  --help-notification                               Show notification icon options
  --help-progress                                   Show progress options
  --help-question                                   Show question options
  --help-warning                                    Show warning options
  --help-scale                                      Show scale options
  --help-text-info                                  Show text information options
  --help-color-selection                            Show color selection options
  --help-password                                   Show password dialog options
  --help-forms                                      Show forms dialog options
  --help-misc                                       Show miscellaneous options
  --help-gtk                                        Show GTK+ Options

Application Options:
  --calendar                                        Display calendar dialog
  --entry                                           Display text entry dialog
  --error                                           Display error dialog
  --info                                            Display info dialog
  --file-selection                                  Display file selection dialog
  --list                                            Display list dialog
  --notification                                    Display notification
  --progress                                        Display progress indication dialog
  --question                                        Display question dialog
  --warning                                         Display warning dialog
  --scale                                           Display scale dialog
  --text-info                                       Display text information dialog
  --color-selection                                 Display color selection dialog
  --password                                        Display password dialog
  --forms                                           Display forms dialog
  --display=DISPLAY                                 X display to use
```

今回はここまで。
皆さんも、pythonでお手軽にGUIを使ってみましょう。
もっと凝ったGUIを組みたくなったらGTKやQtなどでガッツリ作りましょう。