【PySimpleGUI備忘録】Windowの機能とZオーダー・モーダルの設定方法

# はじめに
PythonでGUIを作成するとなると最初に考えたのがtkinterを使うことでした。
しかしtkinter、なかなかコードがわかりにくく、管理に自信を無くしていました。

そんなときふと下記のサイトを拝見し、**PySimpleGUI**の存在を知りました。
[Tkinterを使うのであればPySimpleGUIを使ってみたらという話](https://qiita.com/dario_okazaki/items/656de21cab5c81cabe59)

この記事ではtkinterとPySimpleGUIで同じ処理をコードで比較しており、
その差が一目瞭然な内容になっています。
PySimpleGUIの長所がわかるので、是非読んでみることをお勧めします。

皆さんにお勧めしたいのと同時に、自分も使いこなしたいので、
ドキュメントを読んで今回は**ウィンドウ**の機能を把握していきたいと思います。

# これを読んでわかること
・PySimpleGUIのWindowの引数とその機能

・ポップアップではなく第2画面が表示された場合、
　メインウィンドウの操作ができないようにする(Zオーダーを設定する)方法

・間接的に第2画面をモーダルにする方法

# ドキュメントのURL
[PySimpleGUI公式ドキュメント](https://pysimplegui.readthedocs.io/en/latest/)
tkinterに比べてドキュメントがしっかりしている点も高評価です。

ちなみにGitにたくさんのサンプルプログラムを置いてくれています。
2018年と比較的最近から開発されているので、ネット上の記事もそこまで多くありません。
自分の求める結果を得る方法がなかなかわからないとき、
下記のサンプルプログラムを漁ればどこかでヒントが得られるはずです。
[PySimpleGUI公式サンプルプログラム](https://github.com/PySimpleGUI/PySimpleGUI/tree/master/DemoPrograms)

# 実行環境
Windows10
anaconda 2020.02
Python 3.7.6
PySimpleGUI 4.15.2

# 以下、備忘録
## sg.Windowの引数
デフォルトの設定

    Window(title,
           layout=None,
           default_element_size=(45, 1),
           default_button_element_size=(None, None),
           auto_size_text=None,
           auto_size_buttons=None,
           location=(None, None),
           size=(None, None),
           element_padding=None,
           margins=(None, None),
           button_color=None,
           font=None,
           progress_bar_color=(None, None),
           background_color=None,
           border_depth=None,
           auto_close=False,
           auto_close_duration=3,
           icon=None,
           force_toplevel=False,
           alpha_channel=1,
           return_keyboard_events=False,
           use_default_focus=True,
           text_justification=None,
           no_titlebar=False,
           grab_anywhere=False,
           keep_on_top=False,
           resizable=False,
           disable_close=False,
           disable_minimize=False,
           right_click_menu=None,
           transparent_color=None,
           debugger_enabled=True,
           finalize=False,
           element_justification="left",
           ttk_theme=None,
           use_ttk_buttons=None,
           modal=False,
           metadata=None)'

**title**=[str]
ディスプレイのタイトルバーに表示される文字列

**layout**=[List]
ウィンドウに並ぶウィジェットのリスト

**default_element_size**=(width,height)[int]
ウィンドウ内のすべての文字の幅と行間のサイズ

**default_button_element_size**=(width,height)[int]
ウィンドウ内のすべてのボタンの文字幅と行間のサイズ

**auto_size_text**=[bool]
Trueの場合ウィンドウ内のウィジェットのサイズが
テキストの長さに合わせられる。

**auto_size_buttons**=[bool]
Trueの場合ウィンドウ内のボタンのサイズがボタンのテキストの長さに合わせられる。

**location**=(x,y)[int]
ピクセル単位で指定するウィンドウの左上隅の座標(位置)
※デフォルトではウィンドウは画面の中央に配置される。

**size**=(width,height)[int]
ピクセル単位で指定するウィンドウのサイズ
※デフォルトではウィンドウは自動サイズ調整され、ユーザーは絶対サイズを設定しない。

**element_padding**=((left,right),(top,bottom))[int]
ウィンドウ内のウィジェットを囲むデフォルトのパディング幅

**margins**=((left,right),(top,bottom))[int]
ウィンドウ内のウィジェットの並ぶ矩形からウィンドウの端までのピクセル数

**button_color**=[str]
ウィンドウ内のすべてのボタンのデフォルトの色

**font**=(font,size)[str,int]
tkinterで使えるフォントを指定できる。フォント一覧は[こちら](https://www.delftstack.com/ja/howto/python-tkinter/how-to-set-font-of-tkinter-text-widget/)を参考に。

**progress_bar_color**=(bar_color,background_color)[str,str]
ウィンドウ内のすべての進捗バーのデフォルトの色

**background_color**=[str]
背景色

**border_depth**=[int]
ウィンドウ内のすべてのウィジェットのデフォルトのボーダーの幅

**auto_close**=[bool]
Trueの場合ウィンドウは自動的に閉じる

**auto_close_duration**=[int]
ウィンドウが自動で閉じる前の待機時間[秒]

**icon**=[Union][str]
ファイル名またはBase64のいずれか。
※Windowsの場合、ファイル名はICO形式。Linuxの場合、ICOはだめ。

**force_toplevel**=[bool]
Trueの場合、このウィンドウは隠れたマスターウィンドウの操作を受け付けないようにする

**alpha_channel**=[float]
ウィンドウの不透明度(0=非表示,1=完全に表示)。0~1の値は半透明のウィンドウを生成。
※ただしRaspberryPiでは常に１

**return_keyboard_events**=[bool]
Trueの場合キーボード入力が有効になる

**use_default_focus**=[bool]
Trueの場合デフォルトのフォーカスアルゴリズムによってウィジェットにフォーカスする。

**text_justification**=(Union)('left'or'right'or'center')
ウィンドウ内のテキストのデフォルトの揃い位置

**no_titlebar**=[bool]
trueの場合タイトルバーもフレームもウィンドウに表示されない。
(ウィンドウの最小化やクローズができない)

**grab_anywhere**=[bool]
Trueの場合マウスでクリックやドラッグによりウィンドウを移動できる。

**keep_on_top**=[bool]
Trueの場合ウィンドウは画面上の他のすべてのウィンドウの上に作成される。

**resizable**=[bool]
Trueの場合ユーザーはウィンドウのサイズを変更できる。
※すべての要素がサイズ変更にあわせてサイズや位置が変更されるわけではない。

**disable_close**=[bool]
Trueの場合ウィンドウの右上隅にあるXボタンは機能しない。

**disable_minimize**=[bool]
Trueの場合ユーザーはウィンドウを最小化できない。

**right_click_menu**=[List]
右クリックすると現れるメニュー項目の要素のリスト

**transparent_color**=[str]
ウィンドウ内のこの色の部分は完全に透明になる。
この色のスポットをクリックして、このウィンドウの下のウィンドウに移動することもできる。

**debugger_enabled**=[bool]
Trueの場合内部デバッガーが有効になる。
(GUIライブラリなだけあってGUIでエラーを出してくる。)

**finalize**=[bool]
Trueの場合Finalizeメソッドが呼び出される。

**element_justification**=(Union)('left'or'right'or'center')
テキストのみならずウィンドウ内のすべてのウィジェットの揃い位置

**ttk_theme**=[str]
ウィンドウにtkinter ttk "テーマ"を設定する。(デフォルト= DEFAULT_TTK_THEME)
すべてのttkウィジェットのデフォルトをこのテーマに設定する。

**use_ttk_buttons**=[bool]
ウィンドウ内のすべてのボタンにおいて下記適用される。
True = ttkボタンを使用します
False = ttkボタンを使用しない
なし= Macの場合にのみttkボタンを使用

**modal**=[bool]
Trueの場合、このウィンドウはそれが閉じられるまで、ユーザーが操作できる唯一のウィンドウになる。
(このウィンドウが閉じない限りほかのウィンドウの操作はできない)

**metadata**=[any]
ユーザーメタデータ

## 2画面出してZオーダーを設定してみる
第２画面が常に上に来るようにZオーダーを設定してみます。
引数とその機能を参考にして、第２画面のWindow生成時にkeep_on_top=Trueにしてみます。

コードは以下
(クラスまで作んなくてもいいんですが、まとまり重視で…)

```python:qiita_psg_window.py
import PySimpleGUI as sg

#メイン画面の設定
class MainDisplay:
    def __init__(self):
        sg.theme("DarkBlue12")
        self.layout = [[sg.Text("ボタンを押すとプログラムを終了します")],
                       [sg.Button("Stop",key="-STOP-",size=(10,1))],
                       [sg.Text("ボタンを押すとウィンドウを作成します")],
                       [sg.Button("Make",key="-MAKE-",size=(10,1))]]
        self.window = sg.Window(title="MainDisplay",layout=self.layout)

    def make_second_display(self):
        disp2 = SecondDisplay()
        disp2.main()
        del disp2

    def main(self):
        while True:
            event,value =self.window.read()
            if event in (None,"-STOP-"):
                sg.Popup("終了します")
                break
            elif event == "-MAKE-":
                self.make_second_display()
        self.window.close()


#第２画面の設定
class SecondDisplay:
    def __init__(self):
        sg.theme("DarkBlue11")
        self.layout = [[sg.Text("ボタンを押すとこのウィンドウを閉じます")],
                       [sg.Button("Exit",key="-EXIT-",size=(10,1))]]
#keep_on_top=Trueにする
        self.window = sg.Window("SecondDisplay",self.layout,keep_on_top=True)

    def main(self):
        while True:
            event, value = self.window.read()
            if event == "-EXIT-":
                sg.Popup("このウィンドウを閉じます")
                break
        self.window.close()


#メイン画面の表示ループ
if __name__ == "__main__":
    disp1 = MainDisplay()
    disp1.main()
```

確認の流れですが、起動したメイン画面のMakeボタンを押して第２画面を出します。
この時、メイン画面をクリックしてどうなるかを見ます。

・keep_on_top=Falseの場合
![Main_False.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680120/6b94f8dc-e7f7-83c9-0fa1-9363944bfc3f.png)
メイン画面をクリックすると第２画面が後ろに回ってしまいました。

・keep_on_top=Trueの場合
![Second_True.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680120/b479efe4-a513-2a6f-bf5f-9897c49703d4.png)
メイン画面をクリックしても第２画面は上に表示されたままです。

ここで注意点があります。
このコードのままだと**Exitボタンを押してもポップアップが表示されません**。
それは、第２画面が最上面に表示されるように設定しているからです。
ポップアップも最上面に出したいので、**ポップアップの引数も変更します**。

```
def main(self):
        while True:
            event, value = self.window.read()
            if event == "-EXIT-":
                #keep_on_top=Trueにします
                sg.Popup("このウィンドウを閉じます",keep_on_top=True)
                break
        self.window.close()
```
これで、ポップアップも表示されるようになりました。

また、この最上面表示の設定はPySimpleGUIだけでなく
**ほかのソフトの画面に対しても有効**なので注意が必要です。

場合によってはメイン画面もTrueにする必要が出てきますが、
そうなるとのちにお話ししますが第2画面をモーダルにしないと、
第2画面もメイン画面も上位に来れてしまうので、
keep_on_top=Trueにした意味がなくなってしまいます。

## メイン画面のボタンが押せてしまう問題
**＊＊＊＊＊2020/9/20 追記＊＊＊＊＊**
@meganeoさんのコメントのおかげで、
PySimpleGUIのバージョンを4.15.2 -> **4.29.0**に更新したことで
以下、
**windowの引数modalでエラーが出る問題**は解決いたしました。
また、これは今気づいたことなのですが、
modalの引数をTrueにすると、その画面だけが操作可能になる、
というものですので、**Trueにすべきwindowは第二ウィンドウです**。
以下エラー画面を見ますと、どうやら当時メイン画面を
modal=Trueにしようとしていたようなので、
ここで訂正させて頂きます。
**＊＊＊＊＊　＊＊＊＊＊**
 
Zオーダーは設定できましたが、はみ出ているメインボタンは押せてしまいます。
できればこれも阻止したい。
ということでwindowの引数modal=Trueにしてみました。
![modal_error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680120/aef29c3c-0308-fea8-02ae-20ca03c0e5bd.png)

エラー…。ドキュメントには引数名は確かにmodalと書いていますが、
もしかしたら間違っているのかもと思い、
modalで検索すると下記のページに行き当たりました。
[公式ドキュメント:Making your window modal](https://pysimplegui.readthedocs.io/en/latest/#making-your-window-modal)

２種類の方法が書かれていますので、どちらも試しました。
・moodel=Trueにする(引数名が違った)
・Window.make_modal()を追加する
結果…
![moodel_error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680120/8d4458f3-a664-1ca8-5e9c-95be35cacedd.png)
![make_modal_error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680120/52eaa0ee-1f89-2f79-9379-56c4d3284f62.png)
どちらもダメでした。

ちなみにそれっぽいforce_toplevel=Trueもやってみましたが、
別々のクラスですしマスターウィンドウというわけではなさそう？
なので効果なしでした。(簡易説明ではよくわかっていない)

先のページ内で、この方法を提示してくれている後に、
なにやら**ウィンドウをモーダルにするような直接的な方法はサポートしていない**
だか何だか書いているような気がします(英語苦手)。
最近はそういう風潮があるそうなので、仕方ないです。

~~少なくともドキュメントからわかる直接的なモーダル化の方法はだめでしたので、
**間接的な方法**で実現するしかありません。~~

## 間接的に第2画面をモーダルにする方法
###その１ ウィンドウサイズを指定してメインウィンドウよりも大きくする

この場合第2画面が移動できてしまうと意味がないので、
第2画面のタイトルバーをなくすために、**no_titlebar=True**にします。
ついでにメインウィンドウのほうも同様にTrueに設定します。

※タイトルバー以外のウィンドウ部分をクリックしながら、
　ウィンドウを移動させることが出来るgrab_anywhereは、
　デフォルトでFalseなので明記不要です。

コードの変更箇所は以下です。少しメインウィンドウが小さくなるようにしています。

```
class MainDisplay:
    def __init__(self):
        sg.theme("DarkBlue12")
        self.layout = [[sg.Text("ボタンを押すとプログラムを終了します")],
                       [sg.Button("Stop",key="-STOP-",size=(10,1))],
                       [sg.Text("ボタンを押すとウィンドウを作成します")],
                       [sg.Button("Make",key="-MAKE-",size=(10,1))]
                      ]
        self.window = sg.Window(title="MainDisplay",layout=self.layout,
                                size=(400,200),no_titlebar=True)  #変更箇所

```

```
class SecondDisplay:
    def __init__(self):
        sg.theme("DarkBlue11")
        self.layout = [[sg.Text("ボタンを押すとこのウィンドウを閉じます")],
                       [sg.Button("Exit",key="-EXIT-",size=(10,1))]
　　　　　　　　　　　　]
        self.window = sg.Window("SecondDisplay",self.layout,keep_on_top=True,
                                size=(420,220),grab_anywhere=True)  #変更箇所
```

**実行結果**
まずはメインウィンドウが開きます
![sono1_main.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680120/c3402de2-bda7-2671-042a-7d8a4b9fa1ec.png)
第2画面が完全にメインウィンドウの上に表示されるため、
メインウィンドウをクリックすることが出来ません。
![sono1_second.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680120/adb96682-e75d-84ee-4e78-983c6d4ca23c.png)
この作戦は成功です。


### その2 イベントの発生するウィジェットを操作不可にする

たとえばメイン画面ほど大きくすると第2画面の余白が目立つなど、
第2画面のサイズはメインより小さいままでモーダルにしたい場合です。

ウィジェットの操作可否はあとからwindow.update()で変更できますので、
第2画面の作成前と終了後にそれらの設定をし直す方法です。
各ウィジェットの操作可否を決めるupdateの引数はdisabledで、
Trueで操作不可に、Falseで操作可能になります。

※ただし、この場合メイン画面がクリックできてしまう位置関係なので、
　Zオーダーを設定したい場合はメイン画面のkeep_on_topはTrueにできません。
　keep_on_topのbool値とウィジェットのenabledは関係ないので、
　メイン画面が上面にでてもウィジェットが操作できないようにはできます。

コードの変更点は以下です。

```
def make_second_display(self):
        #第2画面作成前にボタンを操作不可にする
        self.window["-MAKE-"].update(disabled=True)
        self.window["-STOP-"].update(disabled=True)
        disp2 = SecondDisplay()  #第2画面表示
        disp2.main()
        #第2画面が閉じたらボタンを操作可能に戻す
        self.window["-MAKE-"].update(disabled=False)
        self.window["-STOP-"].update(disabled=False)
```

コードが増えて直接的な感じがしますが、変更したいウィジェットが少ない場合は簡単です。
使用不可状態になったボタンは下記のように見た目が変わります。
(わかりやすいように第2画面を移動しています)
![button_disable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680120/cbd4c600-d5b2-c981-a041-d3cdb93d73d0.png)
これで第2画面だけをkeep_on_top=Trueにして、
両画面をno_titlebar=Trueにすれば下記のように、
第2画面が小さいままメインウィンドウが前に出ることなく、
ボタンも押せない実質モーダル化ができます。
![sono2_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680120/9d27def2-4426-78cf-9646-28f1e0725351.png)

変更した部分のコードは以下です。

```
class MainDisplay:
    def __init__(self):
        sg.theme("DarkBlue12")

        self.layout = [[sg.Text("ボタンを押すとプログラムを終了します")],
                       [sg.Button("Stop",key="-STOP-",size=(10,1))],
                       [sg.Text("ボタンを押すとウィンドウを作成します")],
                       [sg.Button("Make",key="-MAKE-",size=(10,1))]]

        self.window = sg.Window(title="MainDisplay",layout=self.layout,
                                no_titlebar=True)  #←ココ
```

```
class SecondDisplay:
    def __init__(self):
        sg.theme("DarkBlue11")

        self.layout = [[sg.Text("ボタンを押すとこのウィンドウを閉じます")],
                       [sg.Button("Exit",key="-EXIT-",size=(10,1))]]

        self.window = sg.Window("SecondDisplay",self.layout,
                                keep_on_top=True,no_titlebar=True)  #←ココ
```

# おわりに
いかがでしたでしょうか。
直接的なモーダルの設定ができないところがまだもやもやしますが、
間接的な方法でも十分目的の実装はできそうな気がしました。

他にも方法はあると思うので、これは一例ということで、
同じように困っている方の参考になれば幸いです。