PySimpleGUIを使うプログラムのモジュール分割

## 記事の概要
PySimpleGUIを使うプログラムで、**画面部位**に応じてモジュール設ける方式を試しました。この方式ではコールバックを使って依存を避けます。

例題となるプログラムの画面を図示します。
![gamen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988024/1f198bad-cfc2-891d-800f-7f86e4a8a748.png)

上部に「一覧表」という**画面部位**があり、本の一覧が表の形で表示されます。
下部に「書誌枠」という**画面部位**があり、本の書誌（ISBN、著者、標題）が表示されます。

「一覧表」で行を選択すると、その本の書誌が「書誌枠」に表示されます。
「書誌枠」で入力を行い、適用ボタンをクリックすると、変更が「一覧表」に反映されます。

例題プログラムのモジュール構造を図示します。矢印は依存の向きです。
```mermaid
graph LR;
    Main-->ListPart;
    Main-->BiblioPart;
    ListPart-->BookData;
    BiblioPart-->BookData;
```
4モジュールのうち、ListPartとBiblioPartが**画面部位**モジュールです。

※各モジュールのコードは[後方](#例題コード)にあります。
※参考情報を末尾に書きました。

## 方式の説明
この方式では「メイン」「**画面部位**モジュール」「その他」という3種のモジュールを設け、「メイン」と各「**画面部位**モジュール」は一定の様式で記述します。

#### メイン（Main）
メインには「イベント波及マップ」「波及処理関数」があり、レイアウトの作成とメインループ内のディスパッチが独特の形です。

###### イベント波及マップ
「イベント波及マップ」は「イベントが生じた**画面部位**の名称」から「画面の更新が必要な**画面部位**のredraw()関数のリスト」を引く辞書です。

###### 波及処理関数
「波及処理関数」は各**画面部位**のイベント処理関数からコールバックされ、「イベント波及マップ」を参照して、他の**画面部位**モジュールのredraw()関数を呼びます。その結果、他の**画面部位**の再描画が生じます。

###### レイアウトの作成
レイアウトの作成に当たっては各**画面部位**モジュールのmake_layout()関数を呼び、その結果を使って全体のレイアウトを組み立てます。

###### ディスパッチ
イベントループ内のディスパッチは次の3ケースから成ります。

* プログラム終了ケース
Windowsの画面の×ボタンクリック時のケースであり、メインループを抜けます。
* タプル型イベントのケース
一覧表の行選択イベントで生じるケースであり、event[0]が関数であり、それを呼びます。その際に「波及処理関数」を引数として渡します。
* 関数イベントのケース
eventが関数であり、それを呼びます。その際に「波及処理関数」を引数として渡します。

#### **画面部位**モジュール（ListPart.、BiblioPart）
画面部位モジュールには、再描画関数redrow()、イベント処理関数on_foo()、レイアウトの該当部分を作る関数make_layoyut()があります。
エレメントの作成時には、キーにイベント処理関数on_foo()そのものを指定します。この結果、メインのディスパッチでは名前を使わずにイベント処理関数を呼び出すことが出来ます。
イベント処理関数はその画面部位に関わる処理を行った後、引数で与えられた「波及処理関数」をコールバックします。この結果、メインの中で他の画面部位の再描画が生じます。

## 方式の評価
この方式の良い点を挙げます。

* ある**画面部位**の画面か機能に変更が生じた場合、コードが変わるのはその**画面部位**モジュールだけです。メインまたは他の**画面部位**は変わりません。
* **画面部位**の追加時に、既存の**画面部位**モジュールは変わりません。変わるのは、メインの「イベント波及マップ」と「レエイアウトの作成」の箇所のみです。
* **画面部位**構成が変わっても、ディスパッチは変わりません。

この方式の悪い点を挙げます。

* イベント処理の引数が様式として決まっているので、引数が冗長な場合があります。
* イベント処理が戻り値でディスパッチの動きを指図しています。

この方式で解決しなかった点を挙げます。

* 入出力項目（Input、Multiline）のキーを一意にするために命名規則を使いました。
* **画面部位**モジュールの内部のみで使う名前（on_foo）を隠していません。

<a id="例題コード"></a>
## 例題コード
4つのファイルを同一のフォルダに夫々のファイル名で保存し、Main.pywを走らせると動きます。

:::note info
この例題は、Windows11(22H2)上のVisual Studio 2022 Communityで作りました。
:::

#### Main.pyw
```python
import PySimpleGUI as sg
import BookData
import ListPart
import BiblioPart

# イベント波及マップ
event_distribution = {
    'ListPart':[BiblioPart.redraw],
    'BiblioPart':[ListPart.redraw],
    }

# イベント波及コールバック
def xfer_event(part, window):
    funcs = event_distribution[part]
    for func in funcs:
        func(window)
    pass

# メイン処理
sg.theme('Default1')
layout = [
    [ListPart.make_layout()],
    [BiblioPart.make_layout()],
    [sg.Text('国立国会図書館サーチのAPIで書誌情報を取得しています')],
]
window = sg.Window('Bookshelf', layout)

while True: # Event Loop
    event, values = window.Read()
    if event is None or event == 'Exit': #Windowsの×ボタン
        break    
    elif isinstance(event, tuple): #タプル型イベント(sg.Tableのクリック)
        if callable(event[0]):
            more = event[0](event, values, xfer_event, window)
            if more == True: continue
            elif more == False: break
    elif callable(event): #関数イベント(sg.Buttonのクリック)
        more = event(values, xfer_event, window)
        if more == True: continue
        elif more == False: break

window.Close()
```
#### ListPart.py
```python
import PySimpleGUI as sg
import BookData

# 表示更新関数
def redraw(window, keep_current=False):
    show = []
    for data in BookData.bookshelf_data:
        line = []
        line.append(data[BookData.I_ISBN])
        line.append(data[BookData.I_AUTHOR])
        line.append(data[BookData.I_TITLE])
        show.append(line)

    window[on_table].update(show)
    if keep_current:
        window[on_table].update(select_rows=[BookData.selected_book])

# テーブルの行選択時イベント処理関数
def on_table(event, values, xfer_event, window):
    row, col = event[2]
    if row is not None:
        BookData.selected_book = row
        xfer_event('ListPart', window)
        return True
    return None

# レイアウト作成関数
def make_layout():  
    table = sg.Table(
        key=on_table,
        values=BookData.bookshelf_data,
        enable_click_events=True,  #セルのクリックを拾う。eventはtuple。
        headings=BookData.HEADING,
        num_rows=10,
        auto_size_columns=True,
        justification='left',
        selected_row_colors=("#000060", "#ccddff"),
        expand_x=True,
        expand_y=True,
    )
    return table
```
#### BiblioPart.py
```python
import PySimpleGUI as sg
import BookData

# 表示更新関数
def redraw(window):
    items = ['','',''] if BookData.selected_book is None else BookData.bookshelf_data[BookData.selected_book]
    window['-BiblioIsbn-'].update(items[BookData.I_ISBN])
    window['-BiblioAuthor-'].update(items[BookData.I_AUTHOR])
    window['-BiblioTitle-'].update(items[BookData.I_TITLE])

# 適用ボタンク処理関数
def on_apply(values, xfer_event, window):
    if(BookData.selected_book is None): return
    list = BookData.bookshelf_data[BookData.selected_book]

    list[BookData.I_TITLE] = values['-BiblioTitle-']
    list[BookData.I_AUTHOR] = values['-BiblioAuthor-']

    BookData.selected_book = None
    redraw(window)

    xfer_event('BiblioPart', window)
    return True

# レイアウト作成関数
def make_layout():
    contentIsbnPart = [[
        sg.Text('ISBN', size=(6,1)),
        sg.Input(key='-BiblioIsbn-', size=(13,1), readonly=True),
    ]]

    contentAuthorPart = [[
        sg.Text('著者', size=(6,1)),
        sg.Multiline(key='-BiblioAuthor-', size=(100,2)),
    ]]

    contentTitlePart = [[
        sg.Text('標題', size=(6,1)),
        sg.Multiline(key='-BiblioTitle-', size=(100,2)),
    ]]
        
    elmContentApplyButton = sg.Button('適用', key=on_apply)   

    frame = sg.Frame('書誌', [
        [sg.Column(contentIsbnPart)],
        [sg.Column(contentAuthorPart)],
        [sg.Column(contentTitlePart)],
        [elmContentApplyButton],
    ])
    return frame
```
#### BookData.py
```python
# データ項目名
HEADING = ['ISBN', '著者', '標題']

# データ項目番号
I_ISBN = 0
I_AUTHOR = 1
I_TITLE = 2

# 蔵書一覧データ
bookshelf_data = [
    ['9784320026926', 'Brian W. Kernighan、Dennis M. Ritchie、石田 晴久', 'プログラミング言語C : ANSI規格準拠 第2版'],
    ['9784810180473', 'Bjarne Stroustrup、斎藤 信男', 'プログラミング言語C++ 第2版'],
]

# 選択されている行
selected_book = None
```
## 参考情報
::: note
Buttonのキーに関数を指定するアイデアは[Demo_Dispatchers.py](https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Dispatchers.py
)で知りました。

PySimpleGUIの使い方については、主に[PySimpleaGUI公式サイトのcall_referenceタブ](https://www.pysimplegui.org/en/latest/call%20reference/)を参照しました。

モジュール分割にあたっては、凝集度・結合度・依存等を考慮しました。参考書を挙げておきます。
* [高信頼性ソフトウェア－複合設計](https://iss.ndl.go.jp/api/openurl?isbn=9784764900288)…ソフトウェア工学書(1975年著)
* [継続的デリバリーのソフトウェア工学](https://iss.ndl.go.jp/api/openurl?isbn=9784296070367)…ソフトウェア工学書(2022年著)
:::